dom/src/notification/NotificationDB.jsm

changeset 0
6474c204b198
     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();

mercurial