diff options
Diffstat (limited to 'dom/notification/NotificationDB.jsm')
-rw-r--r-- | dom/notification/NotificationDB.jsm | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/dom/notification/NotificationDB.jsm b/dom/notification/NotificationDB.jsm new file mode 100644 index 000000000..863dd2484 --- /dev/null +++ b/dom/notification/NotificationDB.jsm @@ -0,0 +1,360 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = []; + +const DEBUG = false; +function debug(s) { dump("-*- NotificationDB component: " + s + "\n"); } + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageListenerManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage", + "@mozilla.org/notificationStorage;1", + "nsINotificationStorage"); + +const NOTIFICATION_STORE_DIR = OS.Constants.Path.profileDir; +const NOTIFICATION_STORE_PATH = + OS.Path.join(NOTIFICATION_STORE_DIR, "notificationstore.json"); + +const kMessages = [ + "Notification:Save", + "Notification:Delete", + "Notification:GetAll" +]; + +var NotificationDB = { + + // Ensure we won't call init() while xpcom-shutdown is performed + _shutdownInProgress: false, + + init: function() { + if (this._shutdownInProgress) { + return; + } + + this.notifications = {}; + this.byTag = {}; + this.loaded = false; + + this.tasks = []; // read/write operation queue + this.runningTask = null; + + Services.obs.addObserver(this, "xpcom-shutdown", false); + this.registerListeners(); + }, + + registerListeners: function() { + for (let message of kMessages) { + ppmm.addMessageListener(message, this); + } + }, + + unregisterListeners: function() { + for (let message of kMessages) { + ppmm.removeMessageListener(message, this); + } + }, + + observe: function(aSubject, aTopic, aData) { + if (DEBUG) debug("Topic: " + aTopic); + if (aTopic == "xpcom-shutdown") { + this._shutdownInProgress = true; + Services.obs.removeObserver(this, "xpcom-shutdown"); + this.unregisterListeners(); + } + }, + + filterNonAppNotifications: function(notifications) { + for (let origin in notifications) { + let persistentNotificationCount = 0; + for (let id in notifications[origin]) { + if (notifications[origin][id].serviceWorkerRegistrationScope) { + persistentNotificationCount++; + } else { + delete notifications[origin][id]; + } + } + if (persistentNotificationCount == 0) { + if (DEBUG) debug("Origin " + origin + " is not linked to an app manifest, deleting."); + delete notifications[origin]; + } + } + + return notifications; + }, + + // Attempt to read notification file, if it's not there we will create it. + load: function() { + var promise = OS.File.read(NOTIFICATION_STORE_PATH, { encoding: "utf-8"}); + return promise.then( + function onSuccess(data) { + if (data.length > 0) { + // 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] = {}; + 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; + }.bind(this), + + // If read failed, we assume we have no notifications to load. + function onFailure(reason) { + this.loaded = true; + return this.createStore(); + }.bind(this) + ); + }, + + // Creates the notification directory. + createStore: function() { + var promise = OS.File.makeDir(NOTIFICATION_STORE_DIR, { + ignoreExisting: true + }); + return promise.then( + this.createFile.bind(this) + ); + }, + + // Creates the notification file once the directory is created. + createFile: function() { + return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, ""); + }, + + // Save current notifications to the file. + save: function() { + var data = JSON.stringify(this.notifications); + return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, data, { encoding: "utf-8"}); + }, + + // Helper function: promise will be resolved once file exists and/or is loaded. + ensureLoaded: function() { + if (!this.loaded) { + return this.load(); + } else { + return Promise.resolve(); + } + }, + + receiveMessage: function(message) { + if (DEBUG) { 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) { + if (DEBUG) { debug("Return message failed, " + name); } + } + } + + switch (message.name) { + case "Notification:GetAll": + this.queueTask("getall", message.data).then(function(notifications) { + returnMessage("Notification:GetAll:Return:OK", { + requestID: message.data.requestID, + origin: message.data.origin, + notifications: notifications + }); + }).catch(function(error) { + returnMessage("Notification:GetAll:Return:KO", { + requestID: message.data.requestID, + origin: message.data.origin, + errorMsg: error + }); + }); + break; + + case "Notification:Save": + this.queueTask("save", message.data).then(function() { + returnMessage("Notification:Save:Return:OK", { + requestID: message.data.requestID + }); + }).catch(function(error) { + returnMessage("Notification:Save:Return:KO", { + requestID: message.data.requestID, + errorMsg: error + }); + }); + break; + + case "Notification:Delete": + this.queueTask("delete", message.data).then(function() { + returnMessage("Notification:Delete:Return:OK", { + requestID: message.data.requestID + }); + }).catch(function(error) { + returnMessage("Notification:Delete:Return:KO", { + requestID: message.data.requestID, + errorMsg: error + }); + }); + break; + + default: + if (DEBUG) { 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: function(operation, data) { + if (DEBUG) { debug("Queueing task: " + operation); } + + var defer = {}; + + this.tasks.push({ + operation: operation, + data: data, + defer: defer + }); + + var promise = new Promise(function(resolve, reject) { + defer.resolve = resolve; + defer.reject = reject; + }); + + // Only run immediately if we aren't currently running another task. + if (!this.runningTask) { + if (DEBUG) { debug("Task queue was not running, starting now..."); } + this.runNextTask(); + } + + return promise; + }, + + runNextTask: function() { + if (this.tasks.length === 0) { + if (DEBUG) { debug("No more tasks to run, queue depleted"); } + this.runningTask = null; + return; + } + this.runningTask = this.tasks.shift(); + + // Always make sure we are loaded before performing any read/write tasks. + this.ensureLoaded() + .then(function() { + var task = this.runningTask; + + switch (task.operation) { + case "getall": + return this.taskGetAll(task.data); + break; + + case "save": + return this.taskSave(task.data); + break; + + case "delete": + return this.taskDelete(task.data); + break; + } + + }.bind(this)) + .then(function(payload) { + if (DEBUG) { + debug("Finishing task: " + this.runningTask.operation); + } + this.runningTask.defer.resolve(payload); + }.bind(this)) + .catch(function(err) { + if (DEBUG) { + debug("Error while running " + this.runningTask.operation + ": " + err); + } + this.runningTask.defer.reject(new String(err)); + }.bind(this)) + .then(function() { + this.runNextTask(); + }.bind(this)); + }, + + taskGetAll: function(data) { + if (DEBUG) { debug("Task, getting all"); } + var origin = data.origin; + var notifications = []; + // Grab only the notifications for specified origin. + if (this.notifications[origin]) { + for (var i in this.notifications[origin]) { + notifications.push(this.notifications[origin][i]); + } + } + return Promise.resolve(notifications); + }, + + taskSave: function(data) { + if (DEBUG) { debug("Task, saving"); } + var origin = data.origin; + var notification = data.notification; + if (!this.notifications[origin]) { + this.notifications[origin] = {}; + this.byTag[origin] = {}; + } + + // 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: function(data) { + if (DEBUG) { debug("Task, deleting"); } + var origin = data.origin; + var id = data.id; + if (!this.notifications[origin]) { + if (DEBUG) { 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) { + if (DEBUG) { 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(); + } +}; + +NotificationDB.init(); |