michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = []; michael@0: michael@0: const DEBUG = false; michael@0: function debug(s) { dump("-*- NotificationDB component: " + s + "\n"); } michael@0: michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/osfile.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "ppmm", michael@0: "@mozilla.org/parentprocessmessagemanager;1", michael@0: "nsIMessageListenerManager"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { michael@0: return new TextEncoder(); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { michael@0: return new TextDecoder(); michael@0: }); michael@0: michael@0: michael@0: const NOTIFICATION_STORE_DIR = OS.Constants.Path.profileDir; michael@0: const NOTIFICATION_STORE_PATH = michael@0: OS.Path.join(NOTIFICATION_STORE_DIR, "notificationstore.json"); michael@0: michael@0: let NotificationDB = { michael@0: init: function() { michael@0: this.notifications = {}; michael@0: this.byTag = {}; michael@0: this.loaded = false; michael@0: michael@0: this.tasks = []; // read/write operation queue michael@0: this.runningTask = false; michael@0: michael@0: ppmm.addMessageListener("Notification:Save", this); michael@0: ppmm.addMessageListener("Notification:Delete", this); michael@0: ppmm.addMessageListener("Notification:GetAll", this); michael@0: }, michael@0: michael@0: // Attempt to read notification file, if it's not there we will create it. michael@0: load: function(callback) { michael@0: var promise = OS.File.read(NOTIFICATION_STORE_PATH); michael@0: promise.then( michael@0: function onSuccess(data) { michael@0: try { michael@0: this.notifications = JSON.parse(gDecoder.decode(data)); michael@0: } catch (e) { michael@0: if (DEBUG) { debug("Unable to parse file data " + e); } michael@0: } michael@0: // populate the list of notifications by tag michael@0: if (this.notifications) { michael@0: for (var origin in this.notifications) { michael@0: this.byTag[origin] = {}; michael@0: for (var id in this.notifications[origin]) { michael@0: var curNotification = this.notifications[origin][id]; michael@0: if (curNotification.tag) { michael@0: this.byTag[origin][curNotification.tag] = curNotification; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: this.loaded = true; michael@0: callback && callback(); michael@0: }.bind(this), michael@0: michael@0: // If read failed, we assume we have no notifications to load. michael@0: function onFailure(reason) { michael@0: this.loaded = true; michael@0: this.createStore(callback); michael@0: }.bind(this) michael@0: ); michael@0: }, michael@0: michael@0: // Creates the notification directory. michael@0: createStore: function(callback) { michael@0: var promise = OS.File.makeDir(NOTIFICATION_STORE_DIR, { michael@0: ignoreExisting: true michael@0: }); michael@0: promise.then( michael@0: function onSuccess() { michael@0: this.createFile(callback); michael@0: }.bind(this), michael@0: michael@0: function onFailure(reason) { michael@0: if (DEBUG) { debug("Directory creation failed:" + reason); } michael@0: callback && callback(); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: // Creates the notification file once the directory is created. michael@0: createFile: function(callback) { michael@0: var promise = OS.File.open(NOTIFICATION_STORE_PATH, {create: true}); michael@0: promise.then( michael@0: function onSuccess(handle) { michael@0: handle.close(); michael@0: callback && callback(); michael@0: }, michael@0: function onFailure(reason) { michael@0: if (DEBUG) { debug("File creation failed:" + reason); } michael@0: callback && callback(); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: // Save current notifications to the file. michael@0: save: function(callback) { michael@0: var data = gEncoder.encode(JSON.stringify(this.notifications)); michael@0: var promise = OS.File.writeAtomic(NOTIFICATION_STORE_PATH, data); michael@0: promise.then( michael@0: function onSuccess() { michael@0: callback && callback(); michael@0: }, michael@0: function onFailure(reason) { michael@0: if (DEBUG) { debug("Save failed:" + reason); } michael@0: callback && callback(); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: // Helper function: callback will be called once file exists and/or is loaded. michael@0: ensureLoaded: function(callback) { michael@0: if (!this.loaded) { michael@0: this.load(callback); michael@0: } else { michael@0: callback(); michael@0: } michael@0: }, michael@0: michael@0: receiveMessage: function(message) { michael@0: if (DEBUG) { debug("Received message:" + message.name); } michael@0: michael@0: // sendAsyncMessage can fail if the child process exits during a michael@0: // notification storage operation, so always wrap it in a try/catch. michael@0: function returnMessage(name, data) { michael@0: try { michael@0: message.target.sendAsyncMessage(name, data); michael@0: } catch (e) { michael@0: if (DEBUG) { debug("Return message failed, " + name); } michael@0: } michael@0: } michael@0: michael@0: switch (message.name) { michael@0: case "Notification:GetAll": michael@0: this.queueTask("getall", message.data, function(notifications) { michael@0: returnMessage("Notification:GetAll:Return:OK", { michael@0: requestID: message.data.requestID, michael@0: origin: message.data.origin, michael@0: notifications: notifications michael@0: }); michael@0: }); michael@0: break; michael@0: michael@0: case "Notification:Save": michael@0: this.queueTask("save", message.data, function() { michael@0: returnMessage("Notification:Save:Return:OK", { michael@0: requestID: message.data.requestID michael@0: }); michael@0: }); michael@0: break; michael@0: michael@0: case "Notification:Delete": michael@0: this.queueTask("delete", message.data, function() { michael@0: returnMessage("Notification:Delete:Return:OK", { michael@0: requestID: message.data.requestID michael@0: }); michael@0: }); michael@0: break; michael@0: michael@0: default: michael@0: if (DEBUG) { debug("Invalid message name" + message.name); } michael@0: } michael@0: }, michael@0: michael@0: // We need to make sure any read/write operations are atomic, michael@0: // so use a queue to run each operation sequentially. michael@0: queueTask: function(operation, data, callback) { michael@0: if (DEBUG) { debug("Queueing task: " + operation); } michael@0: this.tasks.push({ michael@0: operation: operation, michael@0: data: data, michael@0: callback: callback michael@0: }); michael@0: michael@0: // Only run immediately if we aren't currently running another task. michael@0: if (!this.runningTask) { michael@0: if (DEBUG) { dump("Task queue was not running, starting now..."); } michael@0: this.runNextTask(); michael@0: } michael@0: }, michael@0: michael@0: runNextTask: function() { michael@0: if (this.tasks.length === 0) { michael@0: if (DEBUG) { dump("No more tasks to run, queue depleted"); } michael@0: this.runningTask = false; michael@0: return; michael@0: } michael@0: this.runningTask = true; michael@0: michael@0: // Always make sure we are loaded before performing any read/write tasks. michael@0: this.ensureLoaded(function() { michael@0: var task = this.tasks.shift(); michael@0: michael@0: // Wrap the task callback to make sure we immediately michael@0: // run the next task after running the original callback. michael@0: var wrappedCallback = function() { michael@0: if (DEBUG) { debug("Finishing task: " + task.operation); } michael@0: task.callback.apply(this, arguments); michael@0: this.runNextTask(); michael@0: }.bind(this); michael@0: michael@0: switch (task.operation) { michael@0: case "getall": michael@0: this.taskGetAll(task.data, wrappedCallback); michael@0: break; michael@0: michael@0: case "save": michael@0: this.taskSave(task.data, wrappedCallback); michael@0: break; michael@0: michael@0: case "delete": michael@0: this.taskDelete(task.data, wrappedCallback); michael@0: break; michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: taskGetAll: function(data, callback) { michael@0: if (DEBUG) { debug("Task, getting all"); } michael@0: var origin = data.origin; michael@0: var notifications = []; michael@0: // Grab only the notifications for specified origin. michael@0: for (var i in this.notifications[origin]) { michael@0: notifications.push(this.notifications[origin][i]); michael@0: } michael@0: callback(notifications); michael@0: }, michael@0: michael@0: taskSave: function(data, callback) { michael@0: if (DEBUG) { debug("Task, saving"); } michael@0: var origin = data.origin; michael@0: var notification = data.notification; michael@0: if (!this.notifications[origin]) { michael@0: this.notifications[origin] = {}; michael@0: this.byTag[origin] = {}; michael@0: } michael@0: michael@0: // We might have existing notification with this tag, michael@0: // if so we need to remove it before saving the new one. michael@0: if (notification.tag && this.byTag[origin][notification.tag]) { michael@0: var oldNotification = this.byTag[origin][notification.tag]; michael@0: delete this.notifications[origin][oldNotification.id]; michael@0: this.byTag[origin][notification.tag] = notification; michael@0: } michael@0: michael@0: this.notifications[origin][notification.id] = notification; michael@0: this.save(callback); michael@0: }, michael@0: michael@0: taskDelete: function(data, callback) { michael@0: if (DEBUG) { debug("Task, deleting"); } michael@0: var origin = data.origin; michael@0: var id = data.id; michael@0: if (!this.notifications[origin]) { michael@0: if (DEBUG) { debug("No notifications found for origin: " + origin); } michael@0: return; michael@0: } michael@0: michael@0: // Make sure we can find the notification to delete. michael@0: var oldNotification = this.notifications[origin][id]; michael@0: if (!oldNotification) { michael@0: if (DEBUG) { debug("No notification found with id: " + id); } michael@0: return; michael@0: } michael@0: michael@0: if (oldNotification.tag) { michael@0: delete this.byTag[origin][oldNotification.tag]; michael@0: } michael@0: delete this.notifications[origin][id]; michael@0: this.save(callback); michael@0: } michael@0: }; michael@0: michael@0: NotificationDB.init();