1
0
Fork 0
firefox/toolkit/components/nimbus/lib/SharedDataMap.sys.mjs
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

200 lines
5 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/. */
import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
});
const IS_MAIN_PROCESS =
Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
export class SharedDataMap extends EventEmitter {
constructor(sharedDataKey, { path } = {}) {
super();
this._sharedDataKey = sharedDataKey;
this._isReady = false;
this._readyDeferred = Promise.withResolvers();
this._data = null;
if (IS_MAIN_PROCESS) {
this._shutdownBlocker = () => {
this._checkIfShutdownStarted();
};
if (!this._checkIfShutdownStarted()) {
lazy.AsyncShutdown.appShutdownConfirmed.addBlocker(
"SharedDataMap: shutting down before init finished",
this._shutdownBlocker
);
} // else we directly rejected our _readyDeferred promise.
// Lazy-load JSON file that backs Storage instances.
ChromeUtils.defineLazyGetter(this, "_store", () => {
try {
return new lazy.JSONFile({
path:
path ??
PathUtils.join(PathUtils.profileDir, `${sharedDataKey}.json`),
});
} catch (e) {
console.error(e);
}
return null;
});
} else {
this._syncFromParent();
Services.cpmm.sharedData.addEventListener("change", this);
}
}
async init() {
if (!this._isReady && IS_MAIN_PROCESS) {
try {
await this._store.load();
this._data = this._store.data;
this._syncToChildren({ flush: true });
this._checkIfReady();
// Once we finished init, we do not need the blocker anymore.
lazy.AsyncShutdown.appShutdownConfirmed.removeBlocker(
this._shutdownBlocker
);
} catch (e) {
console.error(e);
}
}
}
get sharedDataKey() {
return this._sharedDataKey;
}
ready() {
return this._readyDeferred.promise;
}
get(key) {
if (!this._data) {
return null;
}
let entry = this._data[key];
return entry;
}
set(key, value) {
if (!IS_MAIN_PROCESS) {
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 (!IS_MAIN_PROCESS) {
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();
}
}
_checkIfShutdownStarted() {
if (
Services.startup.isInOrBeyondShutdownPhase(
Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
)
) {
// This will unblock anybody waiting for our init and leave data == null
// if it was not yet initialized, making get() return null.
this._readyDeferred.reject();
lazy.AsyncShutdown.appShutdownConfirmed.removeBlocker(
this._shutdownBlocker
);
return true;
}
return false;
}
handleEvent(event) {
if (event.type === "change") {
if (event.changedKeys.includes(this.sharedDataKey)) {
this._syncFromParent();
}
}
}
}