no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / dom / notification / old / NotificationDB.sys.mjs
blob0f8583dc10b970be30f0a6a1481db15a62da58de
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/. */
5 const DEBUG = false;
6 function debug(s) {
7   dump("-*- NotificationDB component: " + s + "\n");
10 const lazy = {};
12 ChromeUtils.defineESModuleGetters(lazy, {
13   AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
14 });
16 const NOTIFICATION_STORE_DIR = PathUtils.profileDir;
17 const NOTIFICATION_STORE_PATH = PathUtils.join(
18   NOTIFICATION_STORE_DIR,
19   "notificationstore.json"
22 const kMessages = [
23   "Notification:Save",
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,
37   init() {
38     if (this._shutdownInProgress) {
39       return;
40     }
42     this.notifications = Object.create(null);
43     this.byTag = Object.create(null);
44     this.loaded = false;
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
57     );
58   },
60   registerListeners() {
61     for (let message of kMessages) {
62       Services.ppmm.addMessageListener(message, this);
63     }
64   },
66   unregisterListeners() {
67     for (let message of kMessages) {
68       Services.ppmm.removeMessageListener(message, this);
69     }
70   },
72   observe(aSubject, aTopic, aData) {
73     if (DEBUG) {
74       debug("Topic: " + aTopic);
75     }
76     if (aTopic == "xpcom-shutdown") {
77       this._shutdownInProgress = true;
78       Services.obs.removeObserver(this, "xpcom-shutdown");
79       this.unregisterListeners();
80     }
81   },
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];
92         }
93       }
94       if (persistentNotificationCount == 0) {
95         if (DEBUG) {
96           debug(
97             "Origin " + origin + " is not linked to an app manifest, deleting."
98           );
99         }
100         delete result[origin];
101       }
102     }
104     return result;
105   },
107   // Attempt to read notification file, if it's not there we will create it.
108   load() {
109     var promise = IOUtils.readUTF8(NOTIFICATION_STORE_PATH);
110     return promise.then(
111       data => {
112         if (data.length) {
113           // Preprocessing phase intends to cleanly separate any migration-related
114           // tasks.
115           this.notifications = this.filterNonAppNotifications(JSON.parse(data));
116         }
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;
126               }
127             }
128           }
129         }
131         this.loaded = true;
132       },
134       // If read failed, we assume we have no notifications to load.
135       reason => {
136         this.loaded = true;
137         return this.createStore();
138       }
139     );
140   },
142   // Creates the notification directory.
143   createStore() {
144     var promise = IOUtils.makeDirectory(NOTIFICATION_STORE_DIR, {
145       ignoreExisting: true,
146     });
147     return promise.then(this.createFile.bind(this));
148   },
150   // Creates the notification file once the directory is created.
151   createFile() {
152     return IOUtils.writeUTF8(NOTIFICATION_STORE_PATH, "", {
153       tmpPath: NOTIFICATION_STORE_PATH + ".tmp",
154     });
155   },
157   // Save current notifications to the file.
158   save() {
159     var data = JSON.stringify(this.notifications);
160     return IOUtils.writeUTF8(NOTIFICATION_STORE_PATH, data, {
161       tmpPath: NOTIFICATION_STORE_PATH + ".tmp",
162     });
163   },
165   // Helper function: promise will be resolved once file exists and/or is loaded.
166   ensureLoaded() {
167     if (!this.loaded) {
168       return this.load();
169     }
170     return Promise.resolve();
171   },
173   receiveMessage(message) {
174     if (DEBUG) {
175       debug("Received message:" + message.name);
176     }
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) {
181       try {
182         message.target.sendAsyncMessage(name, data);
183       } catch (e) {
184         if (DEBUG) {
185           debug("Return message failed, " + name);
186         }
187       }
188     }
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,
197               notifications,
198             });
199           })
200           .catch(function (error) {
201             returnMessage("Notification:GetAll:Return:KO", {
202               requestID: message.data.requestID,
203               origin: message.data.origin,
204               errorMsg: error,
205             });
206           });
207         break;
209       case "Notification:Save":
210         this.queueTask("save", message.data)
211           .then(function () {
212             returnMessage("Notification:Save:Return:OK", {
213               requestID: message.data.requestID,
214             });
215           })
216           .catch(function (error) {
217             returnMessage("Notification:Save:Return:KO", {
218               requestID: message.data.requestID,
219               errorMsg: error,
220             });
221           });
222         break;
224       case "Notification:Delete":
225         this.queueTask("delete", message.data)
226           .then(function () {
227             returnMessage("Notification:Delete:Return:OK", {
228               requestID: message.data.requestID,
229             });
230           })
231           .catch(function (error) {
232             returnMessage("Notification:Delete:Return:KO", {
233               requestID: message.data.requestID,
234               errorMsg: error,
235             });
236           });
237         break;
239       default:
240         if (DEBUG) {
241           debug("Invalid message name" + message.name);
242         }
243     }
244   },
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) {
249     if (DEBUG) {
250       debug("Queueing task: " + operation);
251     }
253     var defer = {};
255     this.tasks.push({
256       operation,
257       data,
258       defer,
259     });
261     var promise = new Promise(function (resolve, reject) {
262       defer.resolve = resolve;
263       defer.reject = reject;
264     });
266     // Only run immediately if we aren't currently running another task.
267     if (!this.runningTask) {
268       if (DEBUG) {
269         debug("Task queue was not running, starting now...");
270       }
271       this.runNextTask();
272       this._queueDrainedPromise = new Promise(resolve => {
273         this._queueDrainedPromiseResolve = resolve;
274       });
275     }
277     return promise;
278   },
280   runNextTask() {
281     if (this.tasks.length === 0) {
282       if (DEBUG) {
283         debug("No more tasks to run, queue depleted");
284       }
285       this.runningTask = null;
286       if (this._queueDrainedPromiseResolve) {
287         this._queueDrainedPromiseResolve();
288       } else if (DEBUG) {
289         debug(
290           "_queueDrainedPromiseResolve was null somehow, no promise to resolve"
291         );
292       }
293       return;
294     }
295     this.runningTask = this.tasks.shift();
297     // Always make sure we are loaded before performing any read/write tasks.
298     this.ensureLoaded()
299       .then(() => {
300         var task = this.runningTask;
302         switch (task.operation) {
303           case "getall":
304             return this.taskGetAll(task.data);
306           case "save":
307             return this.taskSave(task.data);
309           case "delete":
310             return this.taskDelete(task.data);
312           default:
313             return Promise.reject(
314               new Error(`Found a task with unknown operation ${task.operation}`)
315             );
316         }
317       })
318       .then(payload => {
319         if (DEBUG) {
320           debug("Finishing task: " + this.runningTask.operation);
321         }
322         this.runningTask.defer.resolve(payload);
323       })
324       .catch(err => {
325         if (DEBUG) {
326           debug(
327             "Error while running " + this.runningTask.operation + ": " + err
328           );
329         }
330         this.runningTask.defer.reject(err);
331       })
332       .then(() => {
333         this.runNextTask();
334       });
335   },
337   taskGetAll(data) {
338     if (DEBUG) {
339       debug("Task, getting all");
340     }
341     var origin = data.origin;
342     var notifications = [];
343     // Grab only the notifications for specified origin.
344     if (this.notifications[origin]) {
345       if (data.tag) {
346         let n;
347         if ((n = this.byTag[origin][data.tag])) {
348           notifications.push(n);
349         }
350       } else {
351         for (var i in this.notifications[origin]) {
352           notifications.push(this.notifications[origin][i]);
353         }
354       }
355     }
356     return Promise.resolve(notifications);
357   },
359   taskSave(data) {
360     if (DEBUG) {
361       debug("Task, saving");
362     }
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);
368     }
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];
376       }
377       this.byTag[origin][notification.tag] = notification;
378     }
380     this.notifications[origin][notification.id] = notification;
381     return this.save();
382   },
384   taskDelete(data) {
385     if (DEBUG) {
386       debug("Task, deleting");
387     }
388     var origin = data.origin;
389     var id = data.id;
390     if (!this.notifications[origin]) {
391       if (DEBUG) {
392         debug("No notifications found for origin: " + origin);
393       }
394       return Promise.resolve();
395     }
397     // Make sure we can find the notification to delete.
398     var oldNotification = this.notifications[origin][id];
399     if (!oldNotification) {
400       if (DEBUG) {
401         debug("No notification found with id: " + id);
402       }
403       return Promise.resolve();
404     }
406     if (oldNotification.tag) {
407       delete this.byTag[origin][oldNotification.tag];
408     }
409     delete this.notifications[origin][id];
410     return this.save();
411   },
414 NotificationDB.init();