mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 14:52:16 +00:00
5a1ed5e7fe
Differential Revision: https://phabricator.services.mozilla.com/D217143
409 lines
12 KiB
JavaScript
409 lines
12 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "console", () => {
|
|
return console.createInstance({
|
|
prefix: "NotificationDB",
|
|
maxLogLevelPref: "dom.webnotifications.loglevel",
|
|
});
|
|
});
|
|
|
|
const NOTIFICATION_STORE_DIR = PathUtils.profileDir;
|
|
const NOTIFICATION_STORE_PATH = PathUtils.join(
|
|
NOTIFICATION_STORE_DIR,
|
|
"notificationstore.json"
|
|
);
|
|
|
|
export class NotificationDB {
|
|
// Ensure we won't call init() while xpcom-shutdown is performed
|
|
#shutdownInProgress = false;
|
|
|
|
// A promise that resolves once the ongoing task queue has been drained.
|
|
// The value will be reset when the queue starts again.
|
|
#queueDrainedPromise = null;
|
|
#queueDrainedPromiseResolve = null;
|
|
|
|
#byTag = Object.create(null);
|
|
#notifications = Object.create(null);
|
|
#loaded = false;
|
|
#tasks = [];
|
|
#runningTask = null;
|
|
|
|
storageQualifier() {
|
|
return "Notification";
|
|
}
|
|
|
|
prefixStorageQualifier(message) {
|
|
return `${this.storageQualifier()}:${message}`;
|
|
}
|
|
|
|
formatMessageType(message) {
|
|
return this.prefixStorageQualifier(message);
|
|
}
|
|
|
|
supportedMessageTypes() {
|
|
return [
|
|
this.formatMessageType("Save"),
|
|
this.formatMessageType("Delete"),
|
|
this.formatMessageType("GetAll"),
|
|
];
|
|
}
|
|
|
|
constructor() {
|
|
if (this.#shutdownInProgress) {
|
|
return;
|
|
}
|
|
|
|
this.#notifications = Object.create(null);
|
|
this.#byTag = Object.create(null);
|
|
this.#loaded = false;
|
|
|
|
this.#tasks = []; // read/write operation queue
|
|
this.#runningTask = null;
|
|
|
|
Services.obs.addObserver(this, "xpcom-shutdown");
|
|
this.registerListeners();
|
|
|
|
// This assumes that nothing will queue a new task at profile-change-teardown phase,
|
|
// potentially replacing the #queueDrainedPromise if there was no existing task run.
|
|
lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
|
|
"NotificationDB: Need to make sure that all notification messages are processed",
|
|
() => this.#queueDrainedPromise
|
|
);
|
|
}
|
|
|
|
registerListeners() {
|
|
for (let message of this.supportedMessageTypes()) {
|
|
Services.ppmm.addMessageListener(message, this);
|
|
}
|
|
}
|
|
|
|
unregisterListeners() {
|
|
for (let message of this.supportedMessageTypes()) {
|
|
Services.ppmm.removeMessageListener(message, this);
|
|
}
|
|
}
|
|
|
|
observe(aSubject, aTopic) {
|
|
lazy.console.debug(`Topic: ${aTopic}`);
|
|
if (aTopic == "xpcom-shutdown") {
|
|
this.#shutdownInProgress = true;
|
|
Services.obs.removeObserver(this, "xpcom-shutdown");
|
|
this.unregisterListeners();
|
|
}
|
|
}
|
|
|
|
filterNonAppNotifications(notifications) {
|
|
let result = Object.create(null);
|
|
for (let origin in notifications) {
|
|
result[origin] = Object.create(null);
|
|
let persistentNotificationCount = 0;
|
|
for (let id in notifications[origin]) {
|
|
if (notifications[origin][id].serviceWorkerRegistrationScope) {
|
|
persistentNotificationCount++;
|
|
result[origin][id] = notifications[origin][id];
|
|
}
|
|
}
|
|
if (persistentNotificationCount == 0) {
|
|
lazy.console.debug(
|
|
`Origin ${origin} is not linked to an app manifest, deleting.`
|
|
);
|
|
delete result[origin];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Attempt to read notification file, if it's not there we will create it.
|
|
load() {
|
|
var promise = IOUtils.readUTF8(NOTIFICATION_STORE_PATH);
|
|
return promise.then(
|
|
data => {
|
|
if (data.length) {
|
|
// Preprocessing phase intends to cleanly separate any migration-related
|
|
// tasks.
|
|
this.#notifications = this.filterNonAppNotifications(
|
|
JSON.parse(data)
|
|
);
|
|
}
|
|
|
|
// populate the list of notifications by tag
|
|
if (this.#notifications) {
|
|
for (var origin in this.#notifications) {
|
|
this.#byTag[origin] = Object.create(null);
|
|
for (var id in this.#notifications[origin]) {
|
|
var curNotification = this.#notifications[origin][id];
|
|
if (curNotification.tag) {
|
|
this.#byTag[origin][curNotification.tag] = curNotification;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.#loaded = true;
|
|
},
|
|
|
|
// If read failed, we assume we have no notifications to load.
|
|
() => {
|
|
this.#loaded = true;
|
|
return this.createStore();
|
|
}
|
|
);
|
|
}
|
|
|
|
// Creates the notification directory.
|
|
createStore() {
|
|
var promise = IOUtils.makeDirectory(NOTIFICATION_STORE_DIR, {
|
|
ignoreExisting: true,
|
|
});
|
|
return promise.then(this.createFile.bind(this));
|
|
}
|
|
|
|
// Creates the notification file once the directory is created.
|
|
createFile() {
|
|
return IOUtils.writeUTF8(NOTIFICATION_STORE_PATH, "", {
|
|
tmpPath: NOTIFICATION_STORE_PATH + ".tmp",
|
|
});
|
|
}
|
|
|
|
// Save current notifications to the file.
|
|
save() {
|
|
var data = JSON.stringify(this.#notifications);
|
|
return IOUtils.writeUTF8(NOTIFICATION_STORE_PATH, data, {
|
|
tmpPath: NOTIFICATION_STORE_PATH + ".tmp",
|
|
});
|
|
}
|
|
|
|
// Helper function: promise will be resolved once file exists and/or is loaded.
|
|
#ensureLoaded() {
|
|
if (!this.#loaded) {
|
|
return this.load();
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
receiveMessage(message) {
|
|
lazy.console.debug(`Received message: ${message.name}`);
|
|
|
|
// sendAsyncMessage can fail if the child process exits during a
|
|
// notification storage operation, so always wrap it in a try/catch.
|
|
function returnMessage(name, data) {
|
|
try {
|
|
message.target.sendAsyncMessage(name, data);
|
|
} catch (e) {
|
|
lazy.console.debug(`Return message failed, ${name}`);
|
|
}
|
|
}
|
|
|
|
switch (message.name) {
|
|
case this.formatMessageType("GetAll"):
|
|
this.queueTask("getall", message.data)
|
|
.then(notifications => {
|
|
returnMessage(this.formatMessageType("GetAll:Return:OK"), {
|
|
requestID: message.data.requestID,
|
|
origin: message.data.origin,
|
|
notifications,
|
|
});
|
|
})
|
|
.catch(error => {
|
|
returnMessage(this.formatMessageType("GetAll:Return:KO"), {
|
|
requestID: message.data.requestID,
|
|
origin: message.data.origin,
|
|
errorMsg: error,
|
|
});
|
|
});
|
|
break;
|
|
|
|
case this.formatMessageType("Save"):
|
|
this.queueTask("save", message.data)
|
|
.then(() => {
|
|
returnMessage(this.formatMessageType("Save:Return:OK"), {
|
|
requestID: message.data.requestID,
|
|
});
|
|
})
|
|
.catch(error => {
|
|
returnMessage(this.formatMessageType("Save:Return:KO"), {
|
|
requestID: message.data.requestID,
|
|
errorMsg: error,
|
|
});
|
|
});
|
|
break;
|
|
|
|
case this.formatMessageType("Delete"):
|
|
this.queueTask("delete", message.data)
|
|
.then(() => {
|
|
returnMessage(this.formatMessageType("Delete:Return:OK"), {
|
|
requestID: message.data.requestID,
|
|
});
|
|
})
|
|
.catch(error => {
|
|
returnMessage(this.formatMessageType("Delete:Return:KO"), {
|
|
requestID: message.data.requestID,
|
|
errorMsg: error,
|
|
});
|
|
});
|
|
break;
|
|
|
|
default:
|
|
lazy.console.debug(`Invalid message name ${message.name}`);
|
|
}
|
|
}
|
|
|
|
// 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 "save":
|
|
return this.taskSave(task.data);
|
|
|
|
case "delete":
|
|
return this.taskDelete(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();
|
|
});
|
|
}
|
|
|
|
taskGetAll(data) {
|
|
lazy.console.debug("Task, getting all");
|
|
var origin = data.origin;
|
|
var notifications = [];
|
|
// Grab only the notifications for specified origin.
|
|
if (this.#notifications[origin]) {
|
|
if (data.tag) {
|
|
let n;
|
|
if ((n = this.#byTag[origin][data.tag])) {
|
|
notifications.push(n);
|
|
}
|
|
} else {
|
|
for (var i in this.#notifications[origin]) {
|
|
notifications.push(this.#notifications[origin][i]);
|
|
}
|
|
}
|
|
}
|
|
return Promise.resolve(notifications);
|
|
}
|
|
|
|
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];
|
|
return this.save();
|
|
}
|
|
}
|
|
|
|
new NotificationDB();
|