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",
14 KeyValueService: "resource://gre/modules/kvstore.sys.mjs",
19 "Notification:Delete",
20 "Notification:GetAll",
23 // Given its origin and ID, produce the key that uniquely identifies
25 function makeKey(origin, id) {
26 return origin.concat("\t", id);
29 var NotificationDB = {
30 // Ensure we won't call init() while xpcom-shutdown is performed
31 _shutdownInProgress: false,
33 // A handle to the kvstore, retrieved lazily when we load the data.
36 // A promise that resolves once the store has been loaded.
37 // The promise doesn't resolve to a value; it merely captures the state
38 // of the load via its resolution.
41 // A promise that resolves once the ongoing task queue has been drained.
42 // The value will be reset when the queue starts again.
43 _queueDrainedPromise: null,
44 _queueDrainedPromiseResolve: null,
47 if (this._shutdownInProgress) {
51 this.tasks = []; // read/write operation queue
52 this.runningTask = null;
54 Services.obs.addObserver(this, "xpcom-shutdown");
55 this.registerListeners();
57 // This assumes that nothing will queue a new task at profile-change-teardown phase,
58 // potentially replacing the _queueDrainedPromise if there was no existing task run.
59 lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
60 "NotificationDB: Need to make sure that all notification messages are processed",
61 () => this._queueDrainedPromise
66 for (let message of kMessages) {
67 Services.ppmm.addMessageListener(message, this);
71 unregisterListeners() {
72 for (let message of kMessages) {
73 Services.ppmm.removeMessageListener(message, this);
77 observe(aSubject, aTopic, aData) {
79 debug("Topic: " + aTopic);
81 if (aTopic == "xpcom-shutdown") {
82 this._shutdownInProgress = true;
83 Services.obs.removeObserver(this, "xpcom-shutdown");
84 this.unregisterListeners();
88 filterNonAppNotifications(notifications) {
89 for (let origin in notifications) {
90 let persistentNotificationCount = 0;
91 for (let id in notifications[origin]) {
92 if (notifications[origin][id].serviceWorkerRegistrationScope) {
93 persistentNotificationCount++;
95 delete notifications[origin][id];
98 if (persistentNotificationCount == 0) {
101 "Origin " + origin + " is not linked to an app manifest, deleting."
104 delete notifications[origin];
108 return notifications;
111 async maybeMigrateData() {
112 const oldStore = PathUtils.join(
113 Services.dirsvc.get("ProfD", Ci.nsIFile).path,
114 "notificationstore.json"
117 if (!(await IOUtils.exists(oldStore))) {
119 debug("Old store doesn't exist; not migrating data.");
126 data = await IOUtils.readUTF8(oldStore);
128 // If read failed, we assume we have no notifications to migrate.
130 debug("Failed to read old store; not migrating data.");
134 // Finally, delete the old file so we don't try to migrate it again.
135 await IOUtils.remove(oldStore);
139 // Preprocessing phase intends to cleanly separate any migration-related
142 // NB: This code existed before we migrated the data to a kvstore,
143 // and the "migration-related tasks" it references are from an earlier
144 // migration. We used to do it every time we read the JSON file;
145 // now we do it once, when migrating the JSON file to the kvstore.
146 const notifications = this.filterNonAppNotifications(JSON.parse(data));
148 // Copy the data from the JSON file to the kvstore.
149 // TODO: use a transaction to improve the performance of these operations
150 // once the kvstore API supports it (bug 1515096).
151 for (const origin in notifications) {
152 for (const id in notifications[origin]) {
153 await this._store.put(
155 JSON.stringify(notifications[origin][id])
162 // Attempt to read notification file, if it's not there we will create it.
164 // Get and cache a handle to the kvstore.
165 const dir = PathUtils.join(PathUtils.profileDir, "notificationstore");
166 await IOUtils.makeDirectory(dir, { ignoreExisting: true });
167 this._store = await lazy.KeyValueService.getOrCreate(dir, "notifications");
169 // Migrate data from the old JSON file to the new kvstore if the old file
170 // is present in the user's profile directory.
171 await this.maybeMigrateData();
174 // Helper function: promise will be resolved once file exists and/or is loaded.
176 if (!this._loadPromise) {
177 this._loadPromise = this.load();
179 return this._loadPromise;
182 receiveMessage(message) {
184 debug("Received message:" + message.name);
187 // sendAsyncMessage can fail if the child process exits during a
188 // notification storage operation, so always wrap it in a try/catch.
189 function returnMessage(name, data) {
191 message.target.sendAsyncMessage(name, data);
194 debug("Return message failed, " + name);
199 switch (message.name) {
200 case "Notification:GetAll":
201 this.queueTask("getall", message.data)
202 .then(function (notifications) {
203 returnMessage("Notification:GetAll:Return:OK", {
204 requestID: message.data.requestID,
205 origin: message.data.origin,
209 .catch(function (error) {
210 returnMessage("Notification:GetAll:Return:KO", {
211 requestID: message.data.requestID,
212 origin: message.data.origin,
218 case "Notification:Save":
219 this.queueTask("save", message.data)
221 returnMessage("Notification:Save:Return:OK", {
222 requestID: message.data.requestID,
225 .catch(function (error) {
226 returnMessage("Notification:Save:Return:KO", {
227 requestID: message.data.requestID,
233 case "Notification:Delete":
234 this.queueTask("delete", message.data)
236 returnMessage("Notification:Delete:Return:OK", {
237 requestID: message.data.requestID,
240 .catch(function (error) {
241 returnMessage("Notification:Delete:Return:KO", {
242 requestID: message.data.requestID,
250 debug("Invalid message name" + message.name);
255 // We need to make sure any read/write operations are atomic,
256 // so use a queue to run each operation sequentially.
257 queueTask(operation, data) {
259 debug("Queueing task: " + operation);
264 this.tasks.push({ operation, data, defer });
266 var promise = new Promise(function (resolve, reject) {
267 defer.resolve = resolve;
268 defer.reject = reject;
271 // Only run immediately if we aren't currently running another task.
272 if (!this.runningTask) {
274 debug("Task queue was not running, starting now...");
277 this._queueDrainedPromise = new Promise(resolve => {
278 this._queueDrainedPromiseResolve = resolve;
286 if (this.tasks.length === 0) {
288 debug("No more tasks to run, queue depleted");
290 this.runningTask = null;
291 if (this._queueDrainedPromiseResolve) {
292 this._queueDrainedPromiseResolve();
295 "_queueDrainedPromiseResolve was null somehow, no promise to resolve"
300 this.runningTask = this.tasks.shift();
302 // Always make sure we are loaded before performing any read/write tasks.
305 var task = this.runningTask;
307 switch (task.operation) {
309 return this.taskGetAll(task.data);
312 return this.taskSave(task.data);
315 return this.taskDelete(task.data);
318 throw new Error(`Unknown task operation: ${task.operation}`);
322 debug("Finishing task: " + this.runningTask.operation);
324 this.runningTask.defer.resolve(payload);
329 "Error while running " + this.runningTask.operation + ": " + err
332 this.runningTask.defer.reject(err);
340 // The "from" and "to" key parameters to nsIKeyValueStore.enumerate()
341 // are inclusive and exclusive, respectively, and keys are tuples
342 // of origin and ID joined by a tab (\t), which is character code 9;
343 // so enumerating ["origin", "origin\n"), where the line feed (\n)
344 // is character code 10, enumerates all pairs with the given origin.
345 return this._store.enumerate(origin, `${origin}\n`);
348 async taskGetAll(data) {
350 debug("Task, getting all");
352 var origin = data.origin;
353 var notifications = [];
355 for (const { value } of await this.enumerate(origin)) {
356 notifications.push(JSON.parse(value));
360 notifications = notifications.filter(n => n.tag === data.tag);
363 return notifications;
366 async taskSave(data) {
368 debug("Task, saving");
370 var origin = data.origin;
371 var notification = data.notification;
373 // We might have existing notification with this tag,
374 // if so we need to remove it before saving the new one.
375 if (notification.tag) {
376 for (const { key, value } of await this.enumerate(origin)) {
377 const oldNotification = JSON.parse(value);
378 if (oldNotification.tag === notification.tag) {
379 await this._store.delete(key);
384 await this._store.put(
385 makeKey(origin, notification.id),
386 JSON.stringify(notification)
390 async taskDelete(data) {
392 debug("Task, deleting");
394 await this._store.delete(makeKey(data.origin, data.id));
398 NotificationDB.init();