1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 dump("-*- NotificationDB component: " + s + "\n");
12 ChromeUtils.defineESModuleGetters(lazy, {
13 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
16 const NOTIFICATION_STORE_DIR = PathUtils.profileDir;
17 const NOTIFICATION_STORE_PATH = PathUtils.join(
18 NOTIFICATION_STORE_DIR,
19 "notificationstore.json"
24 "Notification:Delete",
25 "Notification:GetAll",
28 var NotificationDB = {
29 // Ensure we won't call init() while xpcom-shutdown is performed
30 _shutdownInProgress: false,
32 // A promise that resolves once the ongoing task queue has been drained.
33 // The value will be reset when the queue starts again.
34 _queueDrainedPromise: null,
35 _queueDrainedPromiseResolve: null,
38 if (this._shutdownInProgress) {
42 this.notifications = Object.create(null);
43 this.byTag = Object.create(null);
46 this.tasks = []; // read/write operation queue
47 this.runningTask = null;
49 Services.obs.addObserver(this, "xpcom-shutdown");
50 this.registerListeners();
52 // This assumes that nothing will queue a new task at profile-change-teardown phase,
53 // potentially replacing the _queueDrainedPromise if there was no existing task run.
54 lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
55 "NotificationDB: Need to make sure that all notification messages are processed",
56 () => this._queueDrainedPromise
61 for (let message of kMessages) {
62 Services.ppmm.addMessageListener(message, this);
66 unregisterListeners() {
67 for (let message of kMessages) {
68 Services.ppmm.removeMessageListener(message, this);
72 observe(aSubject, aTopic, aData) {
74 debug("Topic: " + aTopic);
76 if (aTopic == "xpcom-shutdown") {
77 this._shutdownInProgress = true;
78 Services.obs.removeObserver(this, "xpcom-shutdown");
79 this.unregisterListeners();
83 filterNonAppNotifications(notifications) {
84 let result = Object.create(null);
85 for (let origin in notifications) {
86 result[origin] = Object.create(null);
87 let persistentNotificationCount = 0;
88 for (let id in notifications[origin]) {
89 if (notifications[origin][id].serviceWorkerRegistrationScope) {
90 persistentNotificationCount++;
91 result[origin][id] = notifications[origin][id];
94 if (persistentNotificationCount == 0) {
97 "Origin " + origin + " is not linked to an app manifest, deleting."
100 delete result[origin];
107 // Attempt to read notification file, if it's not there we will create it.
109 var promise = IOUtils.readUTF8(NOTIFICATION_STORE_PATH);
113 // Preprocessing phase intends to cleanly separate any migration-related
115 this.notifications = this.filterNonAppNotifications(JSON.parse(data));
118 // populate the list of notifications by tag
119 if (this.notifications) {
120 for (var origin in this.notifications) {
121 this.byTag[origin] = Object.create(null);
122 for (var id in this.notifications[origin]) {
123 var curNotification = this.notifications[origin][id];
124 if (curNotification.tag) {
125 this.byTag[origin][curNotification.tag] = curNotification;
134 // If read failed, we assume we have no notifications to load.
137 return this.createStore();
142 // Creates the notification directory.
144 var promise = IOUtils.makeDirectory(NOTIFICATION_STORE_DIR, {
145 ignoreExisting: true,
147 return promise.then(this.createFile.bind(this));
150 // Creates the notification file once the directory is created.
152 return IOUtils.writeUTF8(NOTIFICATION_STORE_PATH, "", {
153 tmpPath: NOTIFICATION_STORE_PATH + ".tmp",
157 // Save current notifications to the file.
159 var data = JSON.stringify(this.notifications);
160 return IOUtils.writeUTF8(NOTIFICATION_STORE_PATH, data, {
161 tmpPath: NOTIFICATION_STORE_PATH + ".tmp",
165 // Helper function: promise will be resolved once file exists and/or is loaded.
170 return Promise.resolve();
173 receiveMessage(message) {
175 debug("Received message:" + message.name);
178 // sendAsyncMessage can fail if the child process exits during a
179 // notification storage operation, so always wrap it in a try/catch.
180 function returnMessage(name, data) {
182 message.target.sendAsyncMessage(name, data);
185 debug("Return message failed, " + name);
190 switch (message.name) {
191 case "Notification:GetAll":
192 this.queueTask("getall", message.data)
193 .then(function (notifications) {
194 returnMessage("Notification:GetAll:Return:OK", {
195 requestID: message.data.requestID,
196 origin: message.data.origin,
200 .catch(function (error) {
201 returnMessage("Notification:GetAll:Return:KO", {
202 requestID: message.data.requestID,
203 origin: message.data.origin,
209 case "Notification:Save":
210 this.queueTask("save", message.data)
212 returnMessage("Notification:Save:Return:OK", {
213 requestID: message.data.requestID,
216 .catch(function (error) {
217 returnMessage("Notification:Save:Return:KO", {
218 requestID: message.data.requestID,
224 case "Notification:Delete":
225 this.queueTask("delete", message.data)
227 returnMessage("Notification:Delete:Return:OK", {
228 requestID: message.data.requestID,
231 .catch(function (error) {
232 returnMessage("Notification:Delete:Return:KO", {
233 requestID: message.data.requestID,
241 debug("Invalid message name" + message.name);
246 // We need to make sure any read/write operations are atomic,
247 // so use a queue to run each operation sequentially.
248 queueTask(operation, data) {
250 debug("Queueing task: " + operation);
261 var promise = new Promise(function (resolve, reject) {
262 defer.resolve = resolve;
263 defer.reject = reject;
266 // Only run immediately if we aren't currently running another task.
267 if (!this.runningTask) {
269 debug("Task queue was not running, starting now...");
272 this._queueDrainedPromise = new Promise(resolve => {
273 this._queueDrainedPromiseResolve = resolve;
281 if (this.tasks.length === 0) {
283 debug("No more tasks to run, queue depleted");
285 this.runningTask = null;
286 if (this._queueDrainedPromiseResolve) {
287 this._queueDrainedPromiseResolve();
290 "_queueDrainedPromiseResolve was null somehow, no promise to resolve"
295 this.runningTask = this.tasks.shift();
297 // Always make sure we are loaded before performing any read/write tasks.
300 var task = this.runningTask;
302 switch (task.operation) {
304 return this.taskGetAll(task.data);
307 return this.taskSave(task.data);
310 return this.taskDelete(task.data);
313 return Promise.reject(
314 new Error(`Found a task with unknown operation ${task.operation}`)
320 debug("Finishing task: " + this.runningTask.operation);
322 this.runningTask.defer.resolve(payload);
327 "Error while running " + this.runningTask.operation + ": " + err
330 this.runningTask.defer.reject(err);
339 debug("Task, getting all");
341 var origin = data.origin;
342 var notifications = [];
343 // Grab only the notifications for specified origin.
344 if (this.notifications[origin]) {
347 if ((n = this.byTag[origin][data.tag])) {
348 notifications.push(n);
351 for (var i in this.notifications[origin]) {
352 notifications.push(this.notifications[origin][i]);
356 return Promise.resolve(notifications);
361 debug("Task, saving");
363 var origin = data.origin;
364 var notification = data.notification;
365 if (!this.notifications[origin]) {
366 this.notifications[origin] = Object.create(null);
367 this.byTag[origin] = Object.create(null);
370 // We might have existing notification with this tag,
371 // if so we need to remove it before saving the new one.
372 if (notification.tag) {
373 var oldNotification = this.byTag[origin][notification.tag];
374 if (oldNotification) {
375 delete this.notifications[origin][oldNotification.id];
377 this.byTag[origin][notification.tag] = notification;
380 this.notifications[origin][notification.id] = notification;
386 debug("Task, deleting");
388 var origin = data.origin;
390 if (!this.notifications[origin]) {
392 debug("No notifications found for origin: " + origin);
394 return Promise.resolve();
397 // Make sure we can find the notification to delete.
398 var oldNotification = this.notifications[origin][id];
399 if (!oldNotification) {
401 debug("No notification found with id: " + id);
403 return Promise.resolve();
406 if (oldNotification.tag) {
407 delete this.byTag[origin][oldNotification.tag];
409 delete this.notifications[origin][id];
414 NotificationDB.init();