summaryrefslogtreecommitdiffstats
path: root/toolkit/components/nimbus/lib/SharedDataMap.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/nimbus/lib/SharedDataMap.jsm')
-rw-r--r--toolkit/components/nimbus/lib/SharedDataMap.jsm187
1 files changed, 187 insertions, 0 deletions
diff --git a/toolkit/components/nimbus/lib/SharedDataMap.jsm b/toolkit/components/nimbus/lib/SharedDataMap.jsm
new file mode 100644
index 0000000000..c5bb9df26a
--- /dev/null
+++ b/toolkit/components/nimbus/lib/SharedDataMap.jsm
@@ -0,0 +1,187 @@
+/* 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 EXPORTED_SYMBOLS = ["SharedDataMap"];
+
+const { EventEmitter } = ChromeUtils.importESModule(
+ "resource://gre/modules/EventEmitter.sys.mjs"
+);
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+});
+
+const IS_MAIN_PROCESS =
+ Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+class SharedDataMap extends EventEmitter {
+ constructor(sharedDataKey, options = { isParent: IS_MAIN_PROCESS }) {
+ super();
+
+ this._sharedDataKey = sharedDataKey;
+ this._isParent = options.isParent;
+ this._isReady = false;
+ this._readyDeferred = lazy.PromiseUtils.defer();
+ this._data = null;
+
+ if (this.isParent) {
+ // Lazy-load JSON file that backs Storage instances.
+ XPCOMUtils.defineLazyGetter(this, "_store", () => {
+ let path = options.path;
+ let store = null;
+ if (!path) {
+ try {
+ const profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
+ path = PathUtils.join(profileDir, `${sharedDataKey}.json`);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ try {
+ store = new lazy.JSONFile({ path });
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ return store;
+ });
+ } else {
+ this._syncFromParent();
+ Services.cpmm.sharedData.addEventListener("change", this);
+ }
+ }
+
+ async init() {
+ if (!this._isReady && this.isParent) {
+ try {
+ await this._store.load();
+ this._data = this._store.data;
+ this._syncToChildren({ flush: true });
+ this._checkIfReady();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+
+ get sharedDataKey() {
+ return this._sharedDataKey;
+ }
+
+ get isParent() {
+ return this._isParent;
+ }
+
+ ready() {
+ return this._readyDeferred.promise;
+ }
+
+ get(key) {
+ if (!this._data) {
+ return null;
+ }
+
+ let entry = this._data[key];
+
+ return entry;
+ }
+
+ set(key, value) {
+ if (!this.isParent) {
+ throw new Error(
+ "Setting values from within a content process is not allowed"
+ );
+ }
+ this._store.data[key] = value;
+ this._store.saveSoon();
+ this._syncToChildren();
+ this._notifyUpdate();
+ }
+
+ /**
+ * Replace the stored data with an updated filtered dataset for cleanup
+ * purposes. We don't notify of update because we're only filtering out
+ * old unused entries.
+ *
+ * @param {string[]} keysToRemove - list of keys to remove from the persistent store
+ */
+ _removeEntriesByKeys(keysToRemove) {
+ if (!keysToRemove.length) {
+ return;
+ }
+ for (let key of keysToRemove) {
+ try {
+ delete this._store.data[key];
+ } catch (e) {
+ // It's ok if this fails
+ }
+ }
+ this._store.saveSoon();
+ }
+
+ // Only used in tests
+ _deleteForTests(key) {
+ if (!this.isParent) {
+ throw new Error(
+ "Setting values from within a content process is not allowed"
+ );
+ }
+ if (this.has(key)) {
+ delete this._store.data[key];
+ this._store.saveSoon();
+ this._syncToChildren();
+ this._notifyUpdate();
+ }
+ }
+
+ has(key) {
+ return Boolean(this.get(key));
+ }
+
+ /**
+ * Notify store listeners of updates
+ * Called both from Main and Content process
+ */
+ _notifyUpdate(process = "parent") {
+ for (let key of Object.keys(this._data || {})) {
+ this.emit(`${process}-store-update:${key}`, this._data[key]);
+ }
+ }
+
+ _syncToChildren({ flush = false } = {}) {
+ Services.ppmm.sharedData.set(this.sharedDataKey, {
+ ...this._data,
+ });
+ if (flush) {
+ Services.ppmm.sharedData.flush();
+ }
+ }
+
+ _syncFromParent() {
+ this._data = Services.cpmm.sharedData.get(this.sharedDataKey);
+ this._checkIfReady();
+ this._notifyUpdate("child");
+ }
+
+ _checkIfReady() {
+ if (!this._isReady && this._data) {
+ this._isReady = true;
+ this._readyDeferred.resolve();
+ }
+ }
+
+ handleEvent(event) {
+ if (event.type === "change") {
+ if (event.changedKeys.includes(this.sharedDataKey)) {
+ this._syncFromParent();
+ }
+ }
+ }
+}