Files
tubestation/toolkit/components/extensions/child/ext-storage.js

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(),
},
};
}
};