1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/src/notification/NotificationDB.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,294 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = []; 1.11 + 1.12 +const DEBUG = false; 1.13 +function debug(s) { dump("-*- NotificationDB component: " + s + "\n"); } 1.14 + 1.15 +const Cu = Components.utils; 1.16 +const Cc = Components.classes; 1.17 +const Ci = Components.interfaces; 1.18 + 1.19 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.20 +Cu.import("resource://gre/modules/osfile.jsm"); 1.21 + 1.22 +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", 1.23 + "@mozilla.org/parentprocessmessagemanager;1", 1.24 + "nsIMessageListenerManager"); 1.25 + 1.26 +XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { 1.27 + return new TextEncoder(); 1.28 +}); 1.29 + 1.30 +XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { 1.31 + return new TextDecoder(); 1.32 +}); 1.33 + 1.34 + 1.35 +const NOTIFICATION_STORE_DIR = OS.Constants.Path.profileDir; 1.36 +const NOTIFICATION_STORE_PATH = 1.37 + OS.Path.join(NOTIFICATION_STORE_DIR, "notificationstore.json"); 1.38 + 1.39 +let NotificationDB = { 1.40 + init: function() { 1.41 + this.notifications = {}; 1.42 + this.byTag = {}; 1.43 + this.loaded = false; 1.44 + 1.45 + this.tasks = []; // read/write operation queue 1.46 + this.runningTask = false; 1.47 + 1.48 + ppmm.addMessageListener("Notification:Save", this); 1.49 + ppmm.addMessageListener("Notification:Delete", this); 1.50 + ppmm.addMessageListener("Notification:GetAll", this); 1.51 + }, 1.52 + 1.53 + // Attempt to read notification file, if it's not there we will create it. 1.54 + load: function(callback) { 1.55 + var promise = OS.File.read(NOTIFICATION_STORE_PATH); 1.56 + promise.then( 1.57 + function onSuccess(data) { 1.58 + try { 1.59 + this.notifications = JSON.parse(gDecoder.decode(data)); 1.60 + } catch (e) { 1.61 + if (DEBUG) { debug("Unable to parse file data " + e); } 1.62 + } 1.63 + // populate the list of notifications by tag 1.64 + if (this.notifications) { 1.65 + for (var origin in this.notifications) { 1.66 + this.byTag[origin] = {}; 1.67 + for (var id in this.notifications[origin]) { 1.68 + var curNotification = this.notifications[origin][id]; 1.69 + if (curNotification.tag) { 1.70 + this.byTag[origin][curNotification.tag] = curNotification; 1.71 + } 1.72 + } 1.73 + } 1.74 + } 1.75 + this.loaded = true; 1.76 + callback && callback(); 1.77 + }.bind(this), 1.78 + 1.79 + // If read failed, we assume we have no notifications to load. 1.80 + function onFailure(reason) { 1.81 + this.loaded = true; 1.82 + this.createStore(callback); 1.83 + }.bind(this) 1.84 + ); 1.85 + }, 1.86 + 1.87 + // Creates the notification directory. 1.88 + createStore: function(callback) { 1.89 + var promise = OS.File.makeDir(NOTIFICATION_STORE_DIR, { 1.90 + ignoreExisting: true 1.91 + }); 1.92 + promise.then( 1.93 + function onSuccess() { 1.94 + this.createFile(callback); 1.95 + }.bind(this), 1.96 + 1.97 + function onFailure(reason) { 1.98 + if (DEBUG) { debug("Directory creation failed:" + reason); } 1.99 + callback && callback(); 1.100 + } 1.101 + ); 1.102 + }, 1.103 + 1.104 + // Creates the notification file once the directory is created. 1.105 + createFile: function(callback) { 1.106 + var promise = OS.File.open(NOTIFICATION_STORE_PATH, {create: true}); 1.107 + promise.then( 1.108 + function onSuccess(handle) { 1.109 + handle.close(); 1.110 + callback && callback(); 1.111 + }, 1.112 + function onFailure(reason) { 1.113 + if (DEBUG) { debug("File creation failed:" + reason); } 1.114 + callback && callback(); 1.115 + } 1.116 + ); 1.117 + }, 1.118 + 1.119 + // Save current notifications to the file. 1.120 + save: function(callback) { 1.121 + var data = gEncoder.encode(JSON.stringify(this.notifications)); 1.122 + var promise = OS.File.writeAtomic(NOTIFICATION_STORE_PATH, data); 1.123 + promise.then( 1.124 + function onSuccess() { 1.125 + callback && callback(); 1.126 + }, 1.127 + function onFailure(reason) { 1.128 + if (DEBUG) { debug("Save failed:" + reason); } 1.129 + callback && callback(); 1.130 + } 1.131 + ); 1.132 + }, 1.133 + 1.134 + // Helper function: callback will be called once file exists and/or is loaded. 1.135 + ensureLoaded: function(callback) { 1.136 + if (!this.loaded) { 1.137 + this.load(callback); 1.138 + } else { 1.139 + callback(); 1.140 + } 1.141 + }, 1.142 + 1.143 + receiveMessage: function(message) { 1.144 + if (DEBUG) { debug("Received message:" + message.name); } 1.145 + 1.146 + // sendAsyncMessage can fail if the child process exits during a 1.147 + // notification storage operation, so always wrap it in a try/catch. 1.148 + function returnMessage(name, data) { 1.149 + try { 1.150 + message.target.sendAsyncMessage(name, data); 1.151 + } catch (e) { 1.152 + if (DEBUG) { debug("Return message failed, " + name); } 1.153 + } 1.154 + } 1.155 + 1.156 + switch (message.name) { 1.157 + case "Notification:GetAll": 1.158 + this.queueTask("getall", message.data, function(notifications) { 1.159 + returnMessage("Notification:GetAll:Return:OK", { 1.160 + requestID: message.data.requestID, 1.161 + origin: message.data.origin, 1.162 + notifications: notifications 1.163 + }); 1.164 + }); 1.165 + break; 1.166 + 1.167 + case "Notification:Save": 1.168 + this.queueTask("save", message.data, function() { 1.169 + returnMessage("Notification:Save:Return:OK", { 1.170 + requestID: message.data.requestID 1.171 + }); 1.172 + }); 1.173 + break; 1.174 + 1.175 + case "Notification:Delete": 1.176 + this.queueTask("delete", message.data, function() { 1.177 + returnMessage("Notification:Delete:Return:OK", { 1.178 + requestID: message.data.requestID 1.179 + }); 1.180 + }); 1.181 + break; 1.182 + 1.183 + default: 1.184 + if (DEBUG) { debug("Invalid message name" + message.name); } 1.185 + } 1.186 + }, 1.187 + 1.188 + // We need to make sure any read/write operations are atomic, 1.189 + // so use a queue to run each operation sequentially. 1.190 + queueTask: function(operation, data, callback) { 1.191 + if (DEBUG) { debug("Queueing task: " + operation); } 1.192 + this.tasks.push({ 1.193 + operation: operation, 1.194 + data: data, 1.195 + callback: callback 1.196 + }); 1.197 + 1.198 + // Only run immediately if we aren't currently running another task. 1.199 + if (!this.runningTask) { 1.200 + if (DEBUG) { dump("Task queue was not running, starting now..."); } 1.201 + this.runNextTask(); 1.202 + } 1.203 + }, 1.204 + 1.205 + runNextTask: function() { 1.206 + if (this.tasks.length === 0) { 1.207 + if (DEBUG) { dump("No more tasks to run, queue depleted"); } 1.208 + this.runningTask = false; 1.209 + return; 1.210 + } 1.211 + this.runningTask = true; 1.212 + 1.213 + // Always make sure we are loaded before performing any read/write tasks. 1.214 + this.ensureLoaded(function() { 1.215 + var task = this.tasks.shift(); 1.216 + 1.217 + // Wrap the task callback to make sure we immediately 1.218 + // run the next task after running the original callback. 1.219 + var wrappedCallback = function() { 1.220 + if (DEBUG) { debug("Finishing task: " + task.operation); } 1.221 + task.callback.apply(this, arguments); 1.222 + this.runNextTask(); 1.223 + }.bind(this); 1.224 + 1.225 + switch (task.operation) { 1.226 + case "getall": 1.227 + this.taskGetAll(task.data, wrappedCallback); 1.228 + break; 1.229 + 1.230 + case "save": 1.231 + this.taskSave(task.data, wrappedCallback); 1.232 + break; 1.233 + 1.234 + case "delete": 1.235 + this.taskDelete(task.data, wrappedCallback); 1.236 + break; 1.237 + } 1.238 + }.bind(this)); 1.239 + }, 1.240 + 1.241 + taskGetAll: function(data, callback) { 1.242 + if (DEBUG) { debug("Task, getting all"); } 1.243 + var origin = data.origin; 1.244 + var notifications = []; 1.245 + // Grab only the notifications for specified origin. 1.246 + for (var i in this.notifications[origin]) { 1.247 + notifications.push(this.notifications[origin][i]); 1.248 + } 1.249 + callback(notifications); 1.250 + }, 1.251 + 1.252 + taskSave: function(data, callback) { 1.253 + if (DEBUG) { debug("Task, saving"); } 1.254 + var origin = data.origin; 1.255 + var notification = data.notification; 1.256 + if (!this.notifications[origin]) { 1.257 + this.notifications[origin] = {}; 1.258 + this.byTag[origin] = {}; 1.259 + } 1.260 + 1.261 + // We might have existing notification with this tag, 1.262 + // if so we need to remove it before saving the new one. 1.263 + if (notification.tag && this.byTag[origin][notification.tag]) { 1.264 + var oldNotification = this.byTag[origin][notification.tag]; 1.265 + delete this.notifications[origin][oldNotification.id]; 1.266 + this.byTag[origin][notification.tag] = notification; 1.267 + } 1.268 + 1.269 + this.notifications[origin][notification.id] = notification; 1.270 + this.save(callback); 1.271 + }, 1.272 + 1.273 + taskDelete: function(data, callback) { 1.274 + if (DEBUG) { debug("Task, deleting"); } 1.275 + var origin = data.origin; 1.276 + var id = data.id; 1.277 + if (!this.notifications[origin]) { 1.278 + if (DEBUG) { debug("No notifications found for origin: " + origin); } 1.279 + return; 1.280 + } 1.281 + 1.282 + // Make sure we can find the notification to delete. 1.283 + var oldNotification = this.notifications[origin][id]; 1.284 + if (!oldNotification) { 1.285 + if (DEBUG) { debug("No notification found with id: " + id); } 1.286 + return; 1.287 + } 1.288 + 1.289 + if (oldNotification.tag) { 1.290 + delete this.byTag[origin][oldNotification.tag]; 1.291 + } 1.292 + delete this.notifications[origin][id]; 1.293 + this.save(callback); 1.294 + } 1.295 +}; 1.296 + 1.297 +NotificationDB.init();