); return promise.then(this.createFile()); } // Creates the notification file once the directory is created. createFile() { return IOUtils.writeUTF8(this.#storagePath, "", { tmpPath: this.#storagePath + ".tmp", }); } // Save current notifications to the file. save() { var data = JSON.stringify(this.#notifications); return IOUtils.writeUTF8(this.#storagePath, data, { tmpPath: this.#storagePath + ".tmp", }); } testGetRawMap() { return { notifications: this.#notifications, byTag: this.#byTag, }; } // Helper function: promise will be resolved once file exists and/or is loaded. #ensureLoaded() { if (!this.#loaded) { return this.load(); } return Promise.resolve(); } // We need to make sure any read/write operations are atomic, // so use a queue to run each operation sequentially. queueTask(operation, data) { lazy.console.debug(`Queueing task: ${operation}`); var defer = {}; this.#tasks.push({ operation, data, defer, }); var promise = new Promise((resolve, reject) => { defer.resolve = resolve; defer.reject = reject; }); // Only run immediately if we aren't currently running another task. if (!this.#runningTask) { lazy.console.debug("Task queue was not running, starting now..."); this.runNextTask(); this.#queueDrainedPromise = new Promise(resolve => { this.#queueDrainedPromiseResolve = resolve; }); } return promise; } runNextTask() { if (this.#tasks.length === 0) { lazy.console.debug("No more tasks to run, queue depleted"); this.#runningTask = null; if (this.#queueDrainedPromiseResolve) { this.#queueDrainedPromiseResolve(); } else { lazy.console.debug( "#queueDrainedPromiseResolve was null somehow, no promise to resolve" ); } return; } this.#runningTask = this.#tasks.shift(); // Always make sure we are loaded before performing any read/write tasks. this.#ensureLoaded() .then(() => { var task = this.#runningTask; switch (task.operation) { case "getall": return this.taskGetAll(task.data); case "get": return this.taskGet(task.data); case "save": return this.taskSave(task.data); case "delete": return this.taskDelete(task.data); case "deleteAllExcept": return this.taskDeleteAllExcept(task.data); default: return Promise.reject( new Error(`Found a task with unknown operation ${task.operation}`) ); } }) .then(payload => { lazy.console.debug(`Finishing task: ${this.#runningTask.operation}`); this.#runningTask.defer.resolve(payload); }) .catch(err => { lazy.console.debug( `Error while running ${this.#runningTask.operation}: ${err}` ); this.#runningTask.defer.reject(err); }) .then(() => { this.runNextTask(); }); } removeOriginIfEmpty(origin) { if (!Object.keys(this.#notifications[origin]).length) { delete this.#notifications[origin]; delete this.#byTag[origin]; } } taskGetAll(data) { let { origin, scope } = data; lazy.console.debug( `Task, getting all for the origin ${origin} and SWR scope ${scope}` ); // Grab only the notifications for specified origin. if (!this.#notifications[origin]) { return []; } // XXX(krosylight): same-tagged notifications from different SWRs can collide. // See bug 1950159. if (data.tag) { let n = this.#byTag[origin][data.tag]; if (n && n.serviceWorkerRegistrationScope === data.scope) { return [n]; } return []; } let notifications = Object.values(this.#notifications[origin]).filter( n => n.serviceWorkerRegistrationScope === data.scope ); return notifications; } taskGet(data) { let { origin, id } = data; lazy.console.debug(`Task, getting for the origin ${origin} and ID ${id}`); return this.#notifications[origin]?.[id]; } taskSave(data) { lazy.console.debug("Task, saving"); var origin = data.origin; var notification = data.notification; if (!this.#notifications[origin]) { this.#notifications[origin] = Object.create(null); this.#byTag[origin] = Object.create(null); } // We might have existing notification with this tag, // if so we need to remove it before saving the new one. if (notification.tag) { var oldNotification = this.#byTag[origin][notification.tag]; if (oldNotification) { delete this.#notifications[origin][oldNotification.id]; } this.#byTag[origin][notification.tag] = notification; } this.#notifications[origin][notification.id] = notification; return this.save(); } taskDelete(data) { lazy.console.debug("Task, deleting"); var origin = data.origin; var id = data.id; if (!this.#notifications[origin]) { lazy.console.debug(`No notifications found for origin: ${origin}`); return Promise.resolve(); } // Make sure we can find the notification to delete. var oldNotification = this.#notifications[origin][id]; if (!oldNotification) { lazy.console.debug(`No notification found with id: ${id}`); return Promise.resolve(); } if (oldNotification.tag) { delete this.#byTag[origin][oldNotification.tag]; } delete this.#notifications[origin][id]; this.removeOriginIfEmpty(origin); return this.save(); } taskDeleteAllExcept({ ids }) { lazy.console.debug("Task, deleting all"); const entries = Object.entries(this.#notifications); for (const [origin, data] of entries) { const originEntries = Object.entries(data).filter( ([id]) => !ids.includes(id) ); for (const [id, oldNotification] of originEntries) { delete data[id]; if (oldNotification.tag) { delete this.#byTag[origin][oldNotification.tag]; } } this.removeOriginIfEmpty(origin); } return this.save(); } } export const db = new NotificationDB(); PK