239 lines
7.9 KiB
JavaScript
239 lines
7.9 KiB
JavaScript
"use strict";
|
|
|
|
ChromeUtils.defineModuleGetter(this, "ExtensionStorage",
|
|
"resource://gre/modules/ExtensionStorage.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "ExtensionStorageIDB",
|
|
"resource://gre/modules/ExtensionStorageIDB.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
|
|
"resource://gre/modules/TelemetryStopwatch.jsm");
|
|
|
|
// Telemetry histogram keys for the JSONFile backend.
|
|
const storageGetHistogram = "WEBEXT_STORAGE_LOCAL_GET_MS";
|
|
const storageSetHistogram = "WEBEXT_STORAGE_LOCAL_SET_MS";
|
|
// Telemetry histogram keys for the IndexedDB backend.
|
|
const storageGetIDBHistogram = "WEBEXT_STORAGE_LOCAL_IDB_GET_MS";
|
|
const storageSetIDBHistogram = "WEBEXT_STORAGE_LOCAL_IDB_SET_MS";
|
|
|
|
|
|
// Wrap a storage operation in a TelemetryStopWatch.
|
|
async function measureOp(histogram, fn) {
|
|
const stopwatchKey = {};
|
|
TelemetryStopwatch.start(histogram, stopwatchKey);
|
|
try {
|
|
let result = await fn();
|
|
TelemetryStopwatch.finish(histogram, stopwatchKey);
|
|
return result;
|
|
} catch (err) {
|
|
TelemetryStopwatch.cancel(histogram, stopwatchKey);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
this.storage = class extends ExtensionAPI {
|
|
getLocalFileBackend(context, {deserialize, serialize}) {
|
|
return {
|
|
get(keys) {
|
|
return measureOp(storageGetHistogram, () => {
|
|
return context.childManager.callParentAsyncFunction(
|
|
"storage.local.JSONFileBackend.get",
|
|
[serialize(keys)]).then(deserialize);
|
|
});
|
|
},
|
|
set(items) {
|
|
return measureOp(storageSetHistogram, () => {
|
|
return context.childManager.callParentAsyncFunction(
|
|
"storage.local.JSONFileBackend.set", [serialize(items)]);
|
|
});
|
|
},
|
|
remove(keys) {
|
|
return context.childManager.callParentAsyncFunction(
|
|
"storage.local.JSONFileBackend.remove", [serialize(keys)]);
|
|
},
|
|
clear() {
|
|
return context.childManager.callParentAsyncFunction(
|
|
"storage.local.JSONFileBackend.clear", []);
|
|
},
|
|
};
|
|
}
|
|
|
|
getLocalIDBBackend(context, {hasParentListeners, serialize, storagePrincipal}) {
|
|
const dbPromise = ExtensionStorageIDB.open(storagePrincipal);
|
|
|
|
return {
|
|
get(keys) {
|
|
return measureOp(storageGetIDBHistogram, async () => {
|
|
const db = await dbPromise;
|
|
return db.get(keys);
|
|
});
|
|
},
|
|
set(items) {
|
|
return measureOp(storageSetIDBHistogram, async () => {
|
|
const db = await dbPromise;
|
|
const changes = await db.set(items, {
|
|
serialize: ExtensionStorage.serialize,
|
|
});
|
|
|
|
if (!changes) {
|
|
return;
|
|
}
|
|
|
|
const hasListeners = await hasParentListeners();
|
|
if (hasListeners) {
|
|
await context.childManager.callParentAsyncFunction(
|
|
"storage.local.IDBBackend.fireOnChanged", [changes]);
|
|
}
|
|
});
|
|
},
|
|
async remove(keys) {
|
|
const db = await dbPromise;
|
|
const changes = await db.remove(keys);
|
|
|
|
if (!changes) {
|
|
return;
|
|
}
|
|
|
|
const hasListeners = await hasParentListeners();
|
|
if (hasListeners) {
|
|
await context.childManager.callParentAsyncFunction(
|
|
"storage.local.IDBBackend.fireOnChanged", [changes]);
|
|
}
|
|
},
|
|
async clear() {
|
|
const db = await dbPromise;
|
|
const changes = await db.clear(context.extension);
|
|
|
|
if (!changes) {
|
|
return;
|
|
}
|
|
|
|
const hasListeners = await hasParentListeners();
|
|
if (hasListeners) {
|
|
await context.childManager.callParentAsyncFunction(
|
|
"storage.local.IDBBackend.fireOnChanged", [changes]);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
getAPI(context) {
|
|
const serialize = ExtensionStorage.serializeForContext.bind(null, context);
|
|
const deserialize = ExtensionStorage.deserializeForContext.bind(null, context);
|
|
|
|
function sanitize(items) {
|
|
// The schema validator already takes care of arrays (which are only allowed
|
|
// to contain strings). Strings and null are safe values.
|
|
if (typeof items != "object" || items === null || Array.isArray(items)) {
|
|
return items;
|
|
}
|
|
// If we got here, then `items` is an object generated by `ObjectType`'s
|
|
// `normalize` method from Schemas.jsm. The object returned by `normalize`
|
|
// lives in this compartment, while the values live in compartment of
|
|
// `context.contentWindow`. The `sanitize` method runs with the principal
|
|
// of `context`, so we cannot just use `ExtensionStorage.sanitize` because
|
|
// it is not allowed to access properties of `items`.
|
|
// So we enumerate all properties and sanitize each value individually.
|
|
let sanitized = {};
|
|
for (let [key, value] of Object.entries(items)) {
|
|
sanitized[key] = ExtensionStorage.sanitize(value, context);
|
|
}
|
|
return sanitized;
|
|
}
|
|
|
|
// Detect the actual storage.local enabled backend for the extension (as soon as the
|
|
// storage.local API has been accessed for the first time).
|
|
let promiseStorageLocalBackend;
|
|
const getStorageLocalBackend = async () => {
|
|
const {
|
|
backendEnabled,
|
|
storagePrincipal,
|
|
} = await ExtensionStorageIDB.selectBackend(context);
|
|
|
|
if (!backendEnabled) {
|
|
return this.getLocalFileBackend(context, {deserialize, serialize});
|
|
}
|
|
|
|
return this.getLocalIDBBackend(context, {
|
|
storagePrincipal,
|
|
hasParentListeners() {
|
|
// We spare a good amount of memory if there are no listeners around
|
|
// (e.g. because they have never been subscribed or they have been removed
|
|
// in the meantime).
|
|
return context.childManager.callParentAsyncFunction(
|
|
"storage.local.IDBBackend.hasListeners", []);
|
|
},
|
|
serialize,
|
|
});
|
|
};
|
|
|
|
// Generate the backend-agnostic local API wrapped methods.
|
|
const local = {};
|
|
for (let method of ["get", "set", "remove", "clear"]) {
|
|
local[method] = async function(...args) {
|
|
if (!promiseStorageLocalBackend) {
|
|
promiseStorageLocalBackend = getStorageLocalBackend();
|
|
}
|
|
const backend = await promiseStorageLocalBackend;
|
|
return backend[method](...args);
|
|
};
|
|
}
|
|
|
|
return {
|
|
storage: {
|
|
local,
|
|
|
|
sync: {
|
|
get(keys) {
|
|
keys = sanitize(keys);
|
|
return context.childManager.callParentAsyncFunction("storage.sync.get", [
|
|
keys,
|
|
]);
|
|
},
|
|
set(items) {
|
|
items = sanitize(items);
|
|
return context.childManager.callParentAsyncFunction("storage.sync.set", [
|
|
items,
|
|
]);
|
|
},
|
|
},
|
|
|
|
managed: {
|
|
get(keys) {
|
|
return context.childManager.callParentAsyncFunction("storage.managed.get", [
|
|
serialize(keys),
|
|
]).then(deserialize);
|
|
},
|
|
set(items) {
|
|
return Promise.reject({message: "storage.managed is read-only"});
|
|
},
|
|
remove(keys) {
|
|
return Promise.reject({message: "storage.managed is read-only"});
|
|
},
|
|
clear() {
|
|
return Promise.reject({message: "storage.managed is read-only"});
|
|
},
|
|
},
|
|
|
|
onChanged: new EventManager({
|
|
context,
|
|
name: "storage.onChanged",
|
|
register: fire => {
|
|
let onChanged = (data, area) => {
|
|
let changes = new context.cloneScope.Object();
|
|
for (let [key, value] of Object.entries(data)) {
|
|
changes[key] = deserialize(value);
|
|
}
|
|
fire.raw(changes, area);
|
|
};
|
|
|
|
let parent = context.childManager.getParentEvent("storage.onChanged");
|
|
parent.addListener(onChanged);
|
|
return () => {
|
|
parent.removeListener(onChanged);
|
|
};
|
|
},
|
|
}).api(),
|
|
},
|
|
};
|
|
}
|
|
};
|