mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 14:45:29 +00:00
376 lines
13 KiB
JavaScript
376 lines
13 KiB
JavaScript
/* 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";
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|
|
|
// Make it possible to load LoopStorage.jsm in xpcshell tests
|
|
try {
|
|
Cu.importGlobalProperties(["indexedDB"]);
|
|
} catch (ex) {
|
|
// don't write this is out in xpcshell, since it's expected there
|
|
if (typeof window !== 'undefined' && "console" in window) {
|
|
console.log("Failed to import indexedDB; if this isn't a unit test," +
|
|
" something is wrong", ex);
|
|
}
|
|
}
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
|
|
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
|
return new EventEmitter();
|
|
});
|
|
|
|
this.EXPORTED_SYMBOLS = ["LoopStorage"];
|
|
|
|
const kDatabasePrefix = "loop-";
|
|
const kDefaultDatabaseName = "default";
|
|
let gDatabaseName = kDatabasePrefix + kDefaultDatabaseName;
|
|
const kDatabaseVersion = 1;
|
|
|
|
let gWaitForOpenCallbacks = new Set();
|
|
let gDatabase = null;
|
|
let gClosed = false;
|
|
|
|
/**
|
|
* Properly shut the database instance down. This is done on application shutdown.
|
|
*/
|
|
const closeDatabase = function() {
|
|
Services.obs.removeObserver(closeDatabase, "quit-application");
|
|
if (!gDatabase) {
|
|
return;
|
|
}
|
|
gDatabase.close();
|
|
gDatabase = null;
|
|
gClosed = true;
|
|
};
|
|
|
|
/**
|
|
* Open a connection to the IndexedDB database.
|
|
* This function is different than IndexedDBHelper.jsm provides, as it ensures
|
|
* only one connection is open during the lifetime of this API. Callbacks are
|
|
* queued when a connection attempt is in progress and are invoked once the
|
|
* connection is established.
|
|
*
|
|
* @param {Function} onOpen Callback to be invoked once a database connection is
|
|
* established. It takes an Error object as first argument
|
|
* and the database connection object as second argument,
|
|
* if successful.
|
|
*/
|
|
const ensureDatabaseOpen = function(onOpen) {
|
|
if (gClosed) {
|
|
onOpen(new Error("Database already closed"));
|
|
return;
|
|
}
|
|
|
|
if (gDatabase) {
|
|
onOpen(null, gDatabase);
|
|
return;
|
|
}
|
|
|
|
if (!gWaitForOpenCallbacks.has(onOpen)) {
|
|
gWaitForOpenCallbacks.add(onOpen);
|
|
|
|
if (gWaitForOpenCallbacks.size !== 1) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let invokeCallbacks = err => {
|
|
for (let callback of gWaitForOpenCallbacks) {
|
|
callback(err, gDatabase);
|
|
}
|
|
gWaitForOpenCallbacks.clear();
|
|
};
|
|
|
|
let openRequest = indexedDB.open(gDatabaseName, kDatabaseVersion);
|
|
|
|
openRequest.onblocked = function(event) {
|
|
invokeCallbacks(new Error("Database cannot be upgraded cause in use: " + event.target.error));
|
|
};
|
|
|
|
openRequest.onerror = function(event) {
|
|
// Try to delete the old database so that we can start this process over
|
|
// next time.
|
|
indexedDB.deleteDatabase(gDatabaseName);
|
|
invokeCallbacks(new Error("Error while opening database: " + event.target.errorCode));
|
|
};
|
|
|
|
openRequest.onupgradeneeded = function(event) {
|
|
let db = event.target.result;
|
|
eventEmitter.emit("upgrade", db, event.oldVersion, kDatabaseVersion);
|
|
};
|
|
|
|
openRequest.onsuccess = function(event) {
|
|
gDatabase = event.target.result;
|
|
invokeCallbacks();
|
|
// Close the database instance properly on application shutdown.
|
|
Services.obs.addObserver(closeDatabase, "quit-application", false);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Switch to a database with a different name by closing the current connection
|
|
* and making sure that the next connection attempt will be made using the updated
|
|
* name.
|
|
*
|
|
* @param {String} name New name of the database to switch to.
|
|
*/
|
|
const switchDatabase = function(name) {
|
|
if (!name) {
|
|
name = kDefaultDatabaseName;
|
|
}
|
|
name = kDatabasePrefix + name;
|
|
if (name == gDatabaseName) {
|
|
// This is already the current database, so there's no need to switch.
|
|
return;
|
|
}
|
|
|
|
gDatabaseName = name;
|
|
if (gDatabase) {
|
|
try {
|
|
gDatabase.close();
|
|
} finally {
|
|
gDatabase = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Start a transaction on the loop database and return it.
|
|
*
|
|
* @param {String} store Name of the object store to start a transaction on
|
|
* @param {Function} callback Callback to be invoked once a database connection
|
|
* is established and a transaction can be started.
|
|
* It takes an Error object as first argument and the
|
|
* transaction object as second argument.
|
|
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
|
*
|
|
* @note we can't use a Promise here, as they are resolved after a spin of the
|
|
* event loop; the transaction will have finished by then and no operations
|
|
* are possible anymore, yielding an error.
|
|
*/
|
|
const getTransaction = function(store, callback, mode) {
|
|
ensureDatabaseOpen((err, db) => {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
let trans;
|
|
try {
|
|
trans = db.transaction(store, mode);
|
|
} catch(ex) {
|
|
callback(ex);
|
|
return;
|
|
}
|
|
callback(null, trans);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Start a transaction on the loop database and return the requested store.
|
|
*
|
|
* @param {String} store Name of the object store to retrieve
|
|
* @param {Function} callback Callback to be invoked once a database connection
|
|
* is established and a transaction can be started.
|
|
* It takes an Error object as first argument and the
|
|
* store object as second argument.
|
|
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
|
*
|
|
* @note we can't use a Promise here, as they are resolved after a spin of the
|
|
* event loop; the transaction will have finished by then and no operations
|
|
* are possible anymore, yielding an error.
|
|
*/
|
|
const getStore = function(store, callback, mode) {
|
|
getTransaction(store, (err, trans) => {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
callback(null, trans.objectStore(store));
|
|
}, mode);
|
|
};
|
|
|
|
/**
|
|
* Public Loop Storage API.
|
|
*
|
|
* Since IndexedDB transaction can not stand a spin of the event loop _before_
|
|
* using a IDBTransaction object, we can't use Promise.jsm promises. Therefore
|
|
* LoopStorage provides two async helper functions, `asyncForEach` and `asyncParallel`.
|
|
*
|
|
* LoopStorage implements the EventEmitter interface by exposing two methods, `on`
|
|
* and `off`, to subscribe to events.
|
|
* At this point only the `upgrade` event will be emitted. This happens when the
|
|
* database is loaded in memory and consumers will be able to change its structure.
|
|
*/
|
|
this.LoopStorage = Object.freeze({
|
|
/**
|
|
* @var {String} databaseName The name of the database that is currently active,
|
|
* WITHOUT the prefix
|
|
*/
|
|
get databaseName() {
|
|
return gDatabaseName.substr(kDatabasePrefix.length);
|
|
},
|
|
|
|
/**
|
|
* Open a connection to the IndexedDB database and return the database object.
|
|
*
|
|
* @param {Function} callback Callback to be invoked once a database connection
|
|
* is established. It takes an Error object as first
|
|
* argument and the database connection object as
|
|
* second argument, if successful.
|
|
*/
|
|
getSingleton: function(callback) {
|
|
ensureDatabaseOpen(callback);
|
|
},
|
|
|
|
/**
|
|
* Switch to a database with a different name.
|
|
*
|
|
* @param {String} name New name of the database to switch to. Defaults to
|
|
* `kDefaultDatabaseName`
|
|
*/
|
|
switchDatabase: function(name = kDefaultDatabaseName) {
|
|
switchDatabase(name);
|
|
},
|
|
|
|
/**
|
|
* Start a transaction on the loop database and return it.
|
|
* If only two arguments are passed, the default mode will be assumed and the
|
|
* second argument is assumed to be a callback.
|
|
*
|
|
* @param {String} store Name of the object store to start a transaction on
|
|
* @param {Function} callback Callback to be invoked once a database connection
|
|
* is established and a transaction can be started.
|
|
* It takes an Error object as first argument and the
|
|
* transaction object as second argument.
|
|
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
|
*
|
|
* @note we can't use a Promise here, as they are resolved after a spin of the
|
|
* event loop; the transaction will have finished by then and no operations
|
|
* are possible anymore, yielding an error.
|
|
*/
|
|
getTransaction: function(store, callback, mode = "readonly") {
|
|
getTransaction(store, callback, mode);
|
|
},
|
|
|
|
/**
|
|
* Start a transaction on the loop database and return the requested store.
|
|
* If only two arguments are passed, the default mode will be assumed and the
|
|
* second argument is assumed to be a callback.
|
|
*
|
|
* @param {String} store Name of the object store to retrieve
|
|
* @param {Function} callback Callback to be invoked once a database connection
|
|
* is established and a transaction can be started.
|
|
* It takes an Error object as first argument and the
|
|
* store object as second argument.
|
|
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
|
*
|
|
* @note we can't use a Promise here, as they are resolved after a spin of the
|
|
* event loop; the transaction will have finished by then and no operations
|
|
* are possible anymore, yielding an error.
|
|
*/
|
|
getStore: function(store, callback, mode = "readonly") {
|
|
getStore(store, callback, mode);
|
|
},
|
|
|
|
/**
|
|
* Perform an async function in serial on each of the list items and call a
|
|
* callback Function when all list items are done.
|
|
* IMPORTANT: only use this iteration method if you are sure that the operations
|
|
* performed in `onItem` are guaranteed to be async in the success case.
|
|
*
|
|
* @param {Array} list Non-empty list of items to iterate
|
|
* @param {Function} onItem Callback to invoke for each item in the list. It
|
|
* takes the item is first argument and a callback
|
|
* function as second, which is to be invoked once
|
|
* the consumer is done with its async operation. If
|
|
* an error is passed as the first argument to this
|
|
* callback function, the iteration will stop and
|
|
* `onDone` callback will be invoked with that error.
|
|
* @param {callback} onDone Callback to invoke when the list is completed or
|
|
* on error. It takes an Error object as first
|
|
* argument.
|
|
*/
|
|
asyncForEach: function(list, onItem, onDone) {
|
|
let i = 0;
|
|
let len = list.length;
|
|
|
|
if (!len) {
|
|
onDone(new Error("Argument error: empty list"));
|
|
return;
|
|
}
|
|
|
|
onItem(list[i], function handler(err) {
|
|
if (err) {
|
|
onDone(err);
|
|
return;
|
|
}
|
|
|
|
i++;
|
|
if (i < len) {
|
|
onItem(list[i], handler, i);
|
|
} else {
|
|
onDone();
|
|
}
|
|
}, i);
|
|
},
|
|
|
|
/**
|
|
* Perform an async function in parallel on each of the list items and call a
|
|
* callback Function when all list items are done.
|
|
* IMPORTANT: only use this iteration method if you are sure that the operations
|
|
* performed in `onItem` are guaranteed to be async in the success case.
|
|
*
|
|
* @param {Array} list Non-empty list of items to iterate
|
|
* @param {Function} onItem Callback to invoke for each item in the list. It
|
|
* takes the item is first argument and a callback
|
|
* function as second, which is to be invoked once
|
|
* the consumer is done with its async operation. If
|
|
* an error is passed as the first argument to this
|
|
* callback function, the iteration will stop and
|
|
* `onDone` callback will be invoked with that error.
|
|
* @param {callback} onDone Callback to invoke when the list is completed or
|
|
* on error. It takes an Error object as first
|
|
* argument.
|
|
*/
|
|
asyncParallel: function(list, onItem, onDone) {
|
|
let i = 0;
|
|
let done = 0;
|
|
let callbackCalled = false;
|
|
let len = list.length;
|
|
|
|
if (!len) {
|
|
onDone(new Error("Argument error: empty list"));
|
|
return;
|
|
}
|
|
|
|
for (; i < len; ++i) {
|
|
onItem(list[i], function handler(err) {
|
|
if (callbackCalled) {
|
|
return;
|
|
}
|
|
|
|
if (err) {
|
|
onDone(err);
|
|
callbackCalled = true;
|
|
return;
|
|
}
|
|
|
|
if (++done === len) {
|
|
onDone();
|
|
callbackCalled = true;
|
|
}
|
|
}, i);
|
|
}
|
|
},
|
|
|
|
on: (...params) => eventEmitter.on(...params),
|
|
|
|
off: (...params) => eventEmitter.off(...params)
|
|
});
|