In a following patch, all DevTools moz.build files will use DevToolsModules to install JS modules at a path that corresponds directly to their source tree location. Here we rewrite all require and import calls to match the new location that these files are installed to.
378 lines
13 KiB
JavaScript
378 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/shared/event-emitter.js", {});
|
|
return new EventEmitter();
|
|
});
|
|
|
|
this.EXPORTED_SYMBOLS = ["LoopStorage"];
|
|
|
|
const kDatabasePrefix = "loop-";
|
|
const kDefaultDatabaseName = "default";
|
|
var gDatabaseName = kDatabasePrefix + kDefaultDatabaseName;
|
|
const kDatabaseVersion = 1;
|
|
|
|
var gWaitForOpenCallbacks = new Set();
|
|
var gDatabase = null;
|
|
var 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;
|
|
}
|
|
|
|
function handleCallback(err) {
|
|
if (callbackCalled) {
|
|
return;
|
|
}
|
|
|
|
if (err) {
|
|
onDone(err);
|
|
callbackCalled = true;
|
|
return;
|
|
}
|
|
|
|
if (++done === len) {
|
|
onDone();
|
|
callbackCalled = true;
|
|
}
|
|
}
|
|
|
|
for (; i < len; ++i) {
|
|
onItem(list[i], handleCallback, i);
|
|
}
|
|
},
|
|
|
|
on: (...params) => eventEmitter.on(...params),
|
|
|
|
off: (...params) => eventEmitter.off(...params)
|
|
});
|