www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 976696772d68037c113084574d010b631685c122
parent 5d42f0670706f3fa83c310199b64e3d19e258f4a
Author: Dan Stillman <dstillman@zotero.org>
Date:   Thu, 22 Aug 2013 13:08:35 -0400

Use SQLite.jsm from Fx24 to fix hang in Fx25+

Diffstat:
Mchrome/content/zotero/xpcom/db.js | 6+++++-
Aresource/Sqlite.jsm | 868+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 873 insertions(+), 1 deletion(-)

diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js @@ -35,7 +35,11 @@ Zotero.DBConnection = function(dbName) { } Components.utils.import("resource://gre/modules/Task.jsm", this); - Components.utils.import("resource://gre/modules/Sqlite.jsm", this); + // Use the Fx24 Sqlite.jsm, because the Sqlite.jsm in Firefox 25 breaks + // locking_mode=EXCLUSIVE with async DB access. In the Fx24 version, + // the main-thread DB connection is still used for async access. + //Components.utils.import("resource://gre/modules/Sqlite.jsm", this); + Components.utils.import("resource://zotero/Sqlite.jsm", this); this.skipBackup = false; this.transactionVacuum = false; diff --git a/resource/Sqlite.jsm b/resource/Sqlite.jsm @@ -0,0 +1,868 @@ +// This is Mozilla code from Firefox 24. + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "Sqlite", +]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://services-common/log4moz.js"); + +XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", + "resource://services-common/utils.js"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + + +// Counts the number of created connections per database basename(). This is +// used for logging to distinguish connection instances. +let connectionCounters = {}; + + +/** + * Opens a connection to a SQLite database. + * + * The following parameters can control the connection: + * + * path -- (string) The filesystem path of the database file to open. If the + * file does not exist, a new database will be created. + * + * sharedMemoryCache -- (bool) Whether multiple connections to the database + * share the same memory cache. Sharing the memory cache likely results + * in less memory utilization. However, sharing also requires connections + * to obtain a lock, possibly making database access slower. Defaults to + * true. + * + * shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection + * will attempt to minimize its memory usage after this many + * milliseconds of connection idle. The connection is idle when no + * statements are executing. There is no default value which means no + * automatic memory minimization will occur. Please note that this is + * *not* a timer on the idle service and this could fire while the + * application is active. + * + * FUTURE options to control: + * + * special named databases + * pragma TEMP STORE = MEMORY + * TRUNCATE JOURNAL + * SYNCHRONOUS = full + * + * @param options + * (Object) Parameters to control connection and open options. + * + * @return Promise<OpenedConnection> + */ +function openConnection(options) { + let log = Log4Moz.repository.getLogger("Sqlite.ConnectionOpener"); + + if (!options.path) { + throw new Error("path not specified in connection options."); + } + + // Retains absolute paths and normalizes relative as relative to profile. + let path = OS.Path.join(OS.Constants.Path.profileDir, options.path); + + let sharedMemoryCache = "sharedMemoryCache" in options ? + options.sharedMemoryCache : true; + + let openedOptions = {}; + + if ("shrinkMemoryOnConnectionIdleMS" in options) { + if (!Number.isInteger(options.shrinkMemoryOnConnectionIdleMS)) { + throw new Error("shrinkMemoryOnConnectionIdleMS must be an integer. " + + "Got: " + options.shrinkMemoryOnConnectionIdleMS); + } + + openedOptions.shrinkMemoryOnConnectionIdleMS = + options.shrinkMemoryOnConnectionIdleMS; + } + + let file = FileUtils.File(path); + let openDatabaseFn = sharedMemoryCache ? + Services.storage.openDatabase : + Services.storage.openUnsharedDatabase; + + let basename = OS.Path.basename(path); + + if (!connectionCounters[basename]) { + connectionCounters[basename] = 1; + } + + let number = connectionCounters[basename]++; + let identifier = basename + "#" + number; + + log.info("Opening database: " + path + " (" + identifier + ")"); + try { + let connection = openDatabaseFn(file); + + if (!connection.connectionReady) { + log.warn("Connection is not ready."); + return Promise.reject(new Error("Connection is not ready.")); + } + + return Promise.resolve(new OpenedConnection(connection, basename, number, + openedOptions)); + } catch (ex) { + log.warn("Could not open database: " + CommonUtils.exceptionStr(ex)); + return Promise.reject(ex); + } +} + + +/** + * Handle on an opened SQLite database. + * + * This is essentially a glorified wrapper around mozIStorageConnection. + * However, it offers some compelling advantages. + * + * The main functions on this type are `execute` and `executeCached`. These are + * ultimately how all SQL statements are executed. It's worth explaining their + * differences. + * + * `execute` is used to execute one-shot SQL statements. These are SQL + * statements that are executed one time and then thrown away. They are useful + * for dynamically generated SQL statements and clients who don't care about + * performance (either their own or wasting resources in the overall + * application). Because of the performance considerations, it is recommended + * to avoid `execute` unless the statement you are executing will only be + * executed once or seldomly. + * + * `executeCached` is used to execute a statement that will presumably be + * executed multiple times. The statement is parsed once and stuffed away + * inside the connection instance. Subsequent calls to `executeCached` will not + * incur the overhead of creating a new statement object. This should be used + * in preference to `execute` when a specific SQL statement will be executed + * multiple times. + * + * Instances of this type are not meant to be created outside of this file. + * Instead, first open an instance of `UnopenedSqliteConnection` and obtain + * an instance of this type by calling `open`. + * + * FUTURE IMPROVEMENTS + * + * Ability to enqueue operations. Currently there can be race conditions, + * especially as far as transactions are concerned. It would be nice to have + * an enqueueOperation(func) API that serially executes passed functions. + * + * Support for SAVEPOINT (named/nested transactions) might be useful. + * + * @param connection + * (mozIStorageConnection) Underlying SQLite connection. + * @param basename + * (string) The basename of this database name. Used for logging. + * @param number + * (Number) The connection number to this database. + * @param options + * (object) Options to control behavior of connection. See + * `openConnection`. + */ +function OpenedConnection(connection, basename, number, options) { + let log = Log4Moz.repository.getLogger("Sqlite.Connection." + basename); + + // getLogger() returns a shared object. We can't modify the functions on this + // object since they would have effect on all instances and last write would + // win. So, we create a "proxy" object with our custom functions. Everything + // else is proxied back to the shared logger instance via prototype + // inheritance. + let logProxy = {__proto__: log}; + + // Automatically prefix all log messages with the identifier. + for (let level in Log4Moz.Level) { + if (level == "Desc") { + continue; + } + + let lc = level.toLowerCase(); + logProxy[lc] = function (msg) { + return log[lc].call(log, "Conn #" + number + ": " + msg); + }; + } + + this._log = logProxy; + + this._log.info("Opened"); + + this._connection = connection; + this._open = true; + + this._cachedStatements = new Map(); + this._anonymousStatements = new Map(); + this._anonymousCounter = 0; + + // A map from statement index to mozIStoragePendingStatement, to allow for + // canceling prior to finalizing the mozIStorageStatements. + this._pendingStatements = new Map(); + + // Increments for each executed statement for the life of the connection. + this._statementCounter = 0; + + this._inProgressTransaction = null; + + this._idleShrinkMS = options.shrinkMemoryOnConnectionIdleMS; + if (this._idleShrinkMS) { + this._idleShrinkTimer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + // We wait for the first statement execute to start the timer because + // shrinking now would not do anything. + } +} + +OpenedConnection.prototype = Object.freeze({ + TRANSACTION_DEFERRED: "DEFERRED", + TRANSACTION_IMMEDIATE: "IMMEDIATE", + TRANSACTION_EXCLUSIVE: "EXCLUSIVE", + + TRANSACTION_TYPES: ["DEFERRED", "IMMEDIATE", "EXCLUSIVE"], + + get connectionReady() { + return this._open && this._connection.connectionReady; + }, + + /** + * The row ID from the last INSERT operation. + * + * Because all statements are executed asynchronously, this could + * return unexpected results if multiple statements are performed in + * parallel. It is the caller's responsibility to schedule + * appropriately. + * + * It is recommended to only use this within transactions (which are + * handled as sequential statements via Tasks). + */ + get lastInsertRowID() { + this._ensureOpen(); + return this._connection.lastInsertRowID; + }, + + /** + * The number of rows that were changed, inserted, or deleted by the + * last operation. + * + * The same caveats regarding asynchronous execution for + * `lastInsertRowID` also apply here. + */ + get affectedRows() { + this._ensureOpen(); + return this._connection.affectedRows; + }, + + /** + * The integer schema version of the database. + * + * This is 0 if not schema version has been set. + */ + get schemaVersion() { + this._ensureOpen(); + return this._connection.schemaVersion; + }, + + set schemaVersion(value) { + this._ensureOpen(); + this._connection.schemaVersion = value; + }, + + /** + * Close the database connection. + * + * This must be performed when you are finished with the database. + * + * Closing the database connection has the side effect of forcefully + * cancelling all active statements. Therefore, callers should ensure that + * all active statements have completed before closing the connection, if + * possible. + * + * The returned promise will be resolved once the connection is closed. + * + * IMPROVEMENT: Resolve the promise to a closed connection which can be + * reopened. + * + * @return Promise<> + */ + close: function () { + if (!this._connection) { + return Promise.resolve(); + } + + this._log.debug("Request to close connection."); + this._clearIdleShrinkTimer(); + let deferred = Promise.defer(); + + // We need to take extra care with transactions during shutdown. + // + // If we don't have a transaction in progress, we can proceed with shutdown + // immediately. + if (!this._inProgressTransaction) { + this._finalize(deferred); + return deferred.promise; + } + + // Else if we do have a transaction in progress, we forcefully roll it + // back. This is an async task, so we wait on it to finish before + // performing finalization. + this._log.warn("Transaction in progress at time of close. Rolling back."); + + let onRollback = this._finalize.bind(this, deferred); + + this.execute("ROLLBACK TRANSACTION").then(onRollback, onRollback); + this._inProgressTransaction.reject(new Error("Connection being closed.")); + this._inProgressTransaction = null; + + return deferred.promise; + }, + + _finalize: function (deferred) { + this._log.debug("Finalizing connection."); + // Cancel any pending statements. + for (let [k, statement] of this._pendingStatements) { + statement.cancel(); + } + this._pendingStatements.clear(); + + // We no longer need to track these. + this._statementCounter = 0; + + // Next we finalize all active statements. + for (let [k, statement] of this._anonymousStatements) { + statement.finalize(); + } + this._anonymousStatements.clear(); + + for (let [k, statement] of this._cachedStatements) { + statement.finalize(); + } + this._cachedStatements.clear(); + + // This guards against operations performed between the call to this + // function and asyncClose() finishing. See also bug 726990. + this._open = false; + + this._log.debug("Calling asyncClose()."); + this._connection.asyncClose({ + complete: function () { + this._log.info("Closed"); + this._connection = null; + deferred.resolve(); + }.bind(this), + }); + }, + + /** + * Execute a SQL statement and cache the underlying statement object. + * + * This function executes a SQL statement and also caches the underlying + * derived statement object so subsequent executions are faster and use + * less resources. + * + * This function optionally binds parameters to the statement as well as + * optionally invokes a callback for every row retrieved. + * + * By default, no parameters are bound and no callback will be invoked for + * every row. + * + * Bound parameters can be defined as an Array of positional arguments or + * an object mapping named parameters to their values. If there are no bound + * parameters, the caller can pass nothing or null for this argument. + * + * Callers are encouraged to pass objects rather than Arrays for bound + * parameters because they prevent foot guns. With positional arguments, it + * is simple to modify the parameter count or positions without fixing all + * users of the statement. Objects/named parameters are a little safer + * because changes in order alone won't result in bad things happening. + * + * When `onRow` is not specified, all returned rows are buffered before the + * returned promise is resolved. For INSERT or UPDATE statements, this has + * no effect because no rows are returned from these. However, it has + * implications for SELECT statements. + * + * If your SELECT statement could return many rows or rows with large amounts + * of data, for performance reasons it is recommended to pass an `onRow` + * handler. Otherwise, the buffering may consume unacceptable amounts of + * resources. + * + * If a `StopIteration` is thrown during execution of an `onRow` handler, + * the execution of the statement is immediately cancelled. Subsequent + * rows will not be processed and no more `onRow` invocations will be made. + * The promise is resolved immediately. + * + * If a non-`StopIteration` exception is thrown by the `onRow` handler, the + * exception is logged and processing of subsequent rows occurs as if nothing + * happened. The promise is still resolved (not rejected). + * + * The return value is a promise that will be resolved when the statement + * has completed fully. + * + * The promise will be rejected with an `Error` instance if the statement + * did not finish execution fully. The `Error` may have an `errors` property. + * If defined, it will be an Array of objects describing individual errors. + * Each object has the properties `result` and `message`. `result` is a + * numeric error code and `message` is a string description of the problem. + * + * @param name + * (string) The name of the registered statement to execute. + * @param params optional + * (Array or object) Parameters to bind. + * @param onRow optional + * (function) Callback to receive each row from result. + */ + executeCached: function (sql, params=null, onRow=null) { + this._ensureOpen(); + + if (!sql) { + throw new Error("sql argument is empty."); + } + + let statement = this._cachedStatements.get(sql); + if (!statement) { + statement = this._connection.createAsyncStatement(sql); + this._cachedStatements.set(sql, statement); + } + + this._clearIdleShrinkTimer(); + + let deferred = Promise.defer(); + + try { + this._executeStatement(sql, statement, params, onRow).then( + function onResult(result) { + this._startIdleShrinkTimer(); + deferred.resolve(result); + }.bind(this), + function onError(error) { + this._startIdleShrinkTimer(); + deferred.reject(error); + }.bind(this) + ); + } catch (ex) { + this._startIdleShrinkTimer(); + throw ex; + } + + return deferred.promise; + }, + + /** + * Execute a one-shot SQL statement. + * + * If you find yourself feeding the same SQL string in this function, you + * should *not* use this function and instead use `executeCached`. + * + * See `executeCached` for the meaning of the arguments and extended usage info. + * + * @param sql + * (string) SQL to execute. + * @param params optional + * (Array or Object) Parameters to bind to the statement. + * @param onRow optional + * (function) Callback to receive result of a single row. + */ + execute: function (sql, params=null, onRow=null) { + if (typeof(sql) != "string") { + throw new Error("Must define SQL to execute as a string: " + sql); + } + + this._ensureOpen(); + + let statement = this._connection.createAsyncStatement(sql); + let index = this._anonymousCounter++; + + this._anonymousStatements.set(index, statement); + this._clearIdleShrinkTimer(); + + let onFinished = function () { + this._anonymousStatements.delete(index); + statement.finalize(); + this._startIdleShrinkTimer(); + }.bind(this); + + let deferred = Promise.defer(); + + try { + this._executeStatement(sql, statement, params, onRow).then( + function onResult(rows) { + onFinished(); + deferred.resolve(rows); + }.bind(this), + + function onError(error) { + onFinished(); + deferred.reject(error); + }.bind(this) + ); + } catch (ex) { + onFinished(); + throw ex; + } + + return deferred.promise; + }, + + /** + * Whether a transaction is currently in progress. + */ + get transactionInProgress() { + return this._open && !!this._inProgressTransaction; + }, + + /** + * Perform a transaction. + * + * A transaction is specified by a user-supplied function that is a + * generator function which can be used by Task.jsm's Task.spawn(). The + * function receives this connection instance as its argument. + * + * The supplied function is expected to yield promises. These are often + * promises created by calling `execute` and `executeCached`. If the + * generator is exhausted without any errors being thrown, the + * transaction is committed. If an error occurs, the transaction is + * rolled back. + * + * The returned value from this function is a promise that will be resolved + * once the transaction has been committed or rolled back. The promise will + * be resolved to whatever value the supplied function resolves to. If + * the transaction is rolled back, the promise is rejected. + * + * @param func + * (function) What to perform as part of the transaction. + * @param type optional + * One of the TRANSACTION_* constants attached to this type. + */ + executeTransaction: function (func, type=this.TRANSACTION_DEFERRED) { + if (this.TRANSACTION_TYPES.indexOf(type) == -1) { + throw new Error("Unknown transaction type: " + type); + } + + this._ensureOpen(); + + if (this._inProgressTransaction) { + throw new Error("A transaction is already active. Only one transaction " + + "can be active at a time."); + } + + this._log.debug("Beginning transaction"); + let deferred = Promise.defer(); + this._inProgressTransaction = deferred; + Task.spawn(function doTransaction() { + // It's tempting to not yield here and rely on the implicit serial + // execution of issued statements. However, the yield serves an important + // purpose: catching errors in statement execution. + yield this.execute("BEGIN " + type + " TRANSACTION"); + + let result; + try { + result = yield Task.spawn(func(this)); + } catch (ex) { + // It's possible that a request to close the connection caused the + // error. + // Assertion: close() will unset this._inProgressTransaction when + // called. + if (!this._inProgressTransaction) { + this._log.warn("Connection was closed while performing transaction. " + + "Received error should be due to closed connection: " + + CommonUtils.exceptionStr(ex)); + throw ex; + } + + this._log.warn("Error during transaction. Rolling back: " + + CommonUtils.exceptionStr(ex)); + try { + yield this.execute("ROLLBACK TRANSACTION"); + } catch (inner) { + this._log.warn("Could not roll back transaction. This is weird: " + + CommonUtils.exceptionStr(inner)); + } + + throw ex; + } + + // See comment above about connection being closed during transaction. + if (!this._inProgressTransaction) { + this._log.warn("Connection was closed while performing transaction. " + + "Unable to commit."); + throw new Error("Connection closed before transaction committed."); + } + + try { + yield this.execute("COMMIT TRANSACTION"); + } catch (ex) { + this._log.warn("Error committing transaction: " + + CommonUtils.exceptionStr(ex)); + throw ex; + } + + throw new Task.Result(result); + }.bind(this)).then( + function onSuccess(result) { + this._inProgressTransaction = null; + deferred.resolve(result); + }.bind(this), + function onError(error) { + this._inProgressTransaction = null; + deferred.reject(error); + }.bind(this) + ); + + return deferred.promise; + }, + + /** + * Whether a table exists in the database. + * + * IMPROVEMENT: Look for temporary tables. + * + * @param name + * (string) Name of the table. + * + * @return Promise<bool> + */ + tableExists: function (name) { + return this.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name=?", + [name]) + .then(function onResult(rows) { + return Promise.resolve(rows.length > 0); + } + ); + }, + + /** + * Whether a named index exists. + * + * IMPROVEMENT: Look for indexes in temporary tables. + * + * @param name + * (string) Name of the index. + * + * @return Promise<bool> + */ + indexExists: function (name) { + return this.execute( + "SELECT name FROM sqlite_master WHERE type='index' AND name=?", + [name]) + .then(function onResult(rows) { + return Promise.resolve(rows.length > 0); + } + ); + }, + + /** + * Free up as much memory from the underlying database connection as possible. + * + * @return Promise<> + */ + shrinkMemory: function () { + this._log.info("Shrinking memory usage."); + + let onShrunk = this._clearIdleShrinkTimer.bind(this); + + return this.execute("PRAGMA shrink_memory").then(onShrunk, onShrunk); + }, + + /** + * Discard all cached statements. + * + * Note that this relies on us being non-interruptible between + * the insertion or retrieval of a statement in the cache and its + * execution: we finalize all statements, which is only safe if + * they will not be executed again. + * + * @return (integer) the number of statements discarded. + */ + discardCachedStatements: function () { + let count = 0; + for (let [k, statement] of this._cachedStatements) { + ++count; + statement.finalize(); + } + this._cachedStatements.clear(); + this._log.debug("Discarded " + count + " cached statements."); + return count; + }, + + /** + * Helper method to bind parameters of various kinds through + * reflection. + */ + _bindParameters: function (statement, params) { + if (!params) { + return; + } + + if (Array.isArray(params)) { + // It's an array of separate params. + if (params.length && (typeof(params[0]) == "object")) { + let paramsArray = statement.newBindingParamsArray(); + for (let p of params) { + let bindings = paramsArray.newBindingParams(); + for (let [key, value] of Iterator(p)) { + bindings.bindByName(key, value); + } + paramsArray.addParams(bindings); + } + + statement.bindParameters(paramsArray); + return; + } + + // Indexed params. + for (let i = 0; i < params.length; i++) { + statement.bindByIndex(i, params[i]); + } + return; + } + + // Named params. + if (params && typeof(params) == "object") { + for (let k in params) { + statement.bindByName(k, params[k]); + } + return; + } + + throw new Error("Invalid type for bound parameters. Expected Array or " + + "object. Got: " + params); + }, + + _executeStatement: function (sql, statement, params, onRow) { + if (statement.state != statement.MOZ_STORAGE_STATEMENT_READY) { + throw new Error("Statement is not ready for execution."); + } + + if (onRow && typeof(onRow) != "function") { + throw new Error("onRow must be a function. Got: " + onRow); + } + + this._bindParameters(statement, params); + + let index = this._statementCounter++; + + let deferred = Promise.defer(); + let userCancelled = false; + let errors = []; + let rows = []; + + // Don't incur overhead for serializing params unless the messages go + // somewhere. + if (this._log.level <= Log4Moz.Level.Trace) { + let msg = "Stmt #" + index + " " + sql; + + if (params) { + msg += " - " + JSON.stringify(params); + } + this._log.trace(msg); + } else { + this._log.debug("Stmt #" + index + " starting"); + } + + let self = this; + let pending = statement.executeAsync({ + handleResult: function (resultSet) { + // .cancel() may not be immediate and handleResult() could be called + // after a .cancel(). + for (let row = resultSet.getNextRow(); row && !userCancelled; row = resultSet.getNextRow()) { + if (!onRow) { + rows.push(row); + continue; + } + + try { + onRow(row); + } catch (e if e instanceof StopIteration) { + userCancelled = true; + pending.cancel(); + break; + } catch (ex) { + self._log.warn("Exception when calling onRow callback: " + + CommonUtils.exceptionStr(ex)); + } + } + }, + + handleError: function (error) { + self._log.info("Error when executing SQL (" + error.result + "): " + + error.message); + errors.push(error); + }, + + handleCompletion: function (reason) { + self._log.debug("Stmt #" + index + " finished."); + self._pendingStatements.delete(index); + + switch (reason) { + case Ci.mozIStorageStatementCallback.REASON_FINISHED: + // If there is an onRow handler, we always resolve to null. + let result = onRow ? null : rows; + deferred.resolve(result); + break; + + case Ci.mozIStorageStatementCallback.REASON_CANCELLED: + // It is not an error if the user explicitly requested cancel via + // the onRow handler. + if (userCancelled) { + let result = onRow ? null : rows; + deferred.resolve(result); + } else { + deferred.reject(new Error("Statement was cancelled.")); + } + + break; + + case Ci.mozIStorageStatementCallback.REASON_ERROR: + let error = new Error("Error(s) encountered during statement execution."); + error.errors = errors; + deferred.reject(error); + break; + + default: + deferred.reject(new Error("Unknown completion reason code: " + + reason)); + break; + } + }, + }); + + this._pendingStatements.set(index, pending); + return deferred.promise; + }, + + _ensureOpen: function () { + if (!this._open) { + throw new Error("Connection is not open."); + } + }, + + _clearIdleShrinkTimer: function () { + if (!this._idleShrinkTimer) { + return; + } + + this._idleShrinkTimer.cancel(); + }, + + _startIdleShrinkTimer: function () { + if (!this._idleShrinkTimer) { + return; + } + + this._idleShrinkTimer.initWithCallback(this.shrinkMemory.bind(this), + this._idleShrinkMS, + this._idleShrinkTimer.TYPE_ONE_SHOT); + }, +}); + +this.Sqlite = { + openConnection: openConnection, +}; +