123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- /* 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();
|