NotificationDB.jsm 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. this.EXPORTED_SYMBOLS = [];
  6. const DEBUG = false;
  7. function debug(s) { dump("-*- NotificationDB component: " + s + "\n"); }
  8. const Cu = Components.utils;
  9. const Cc = Components.classes;
  10. const Ci = Components.interfaces;
  11. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  12. Cu.import("resource://gre/modules/osfile.jsm");
  13. Cu.import("resource://gre/modules/Promise.jsm");
  14. XPCOMUtils.defineLazyModuleGetter(this, "Services",
  15. "resource://gre/modules/Services.jsm");
  16. XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
  17. "@mozilla.org/parentprocessmessagemanager;1",
  18. "nsIMessageListenerManager");
  19. XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage",
  20. "@mozilla.org/notificationStorage;1",
  21. "nsINotificationStorage");
  22. const NOTIFICATION_STORE_DIR = OS.Constants.Path.profileDir;
  23. const NOTIFICATION_STORE_PATH =
  24. OS.Path.join(NOTIFICATION_STORE_DIR, "notificationstore.json");
  25. const kMessages = [
  26. "Notification:Save",
  27. "Notification:Delete",
  28. "Notification:GetAll"
  29. ];
  30. var NotificationDB = {
  31. // Ensure we won't call init() while xpcom-shutdown is performed
  32. _shutdownInProgress: false,
  33. init: function() {
  34. if (this._shutdownInProgress) {
  35. return;
  36. }
  37. this.notifications = {};
  38. this.byTag = {};
  39. this.loaded = false;
  40. this.tasks = []; // read/write operation queue
  41. this.runningTask = null;
  42. Services.obs.addObserver(this, "xpcom-shutdown", false);
  43. this.registerListeners();
  44. },
  45. registerListeners: function() {
  46. for (let message of kMessages) {
  47. ppmm.addMessageListener(message, this);
  48. }
  49. },
  50. unregisterListeners: function() {
  51. for (let message of kMessages) {
  52. ppmm.removeMessageListener(message, this);
  53. }
  54. },
  55. observe: function(aSubject, aTopic, aData) {
  56. if (DEBUG) debug("Topic: " + aTopic);
  57. if (aTopic == "xpcom-shutdown") {
  58. this._shutdownInProgress = true;
  59. Services.obs.removeObserver(this, "xpcom-shutdown");
  60. this.unregisterListeners();
  61. }
  62. },
  63. filterNonAppNotifications: function(notifications) {
  64. for (let origin in notifications) {
  65. let persistentNotificationCount = 0;
  66. for (let id in notifications[origin]) {
  67. if (notifications[origin][id].serviceWorkerRegistrationScope) {
  68. persistentNotificationCount++;
  69. } else {
  70. delete notifications[origin][id];
  71. }
  72. }
  73. if (persistentNotificationCount == 0) {
  74. if (DEBUG) debug("Origin " + origin + " is not linked to an app manifest, deleting.");
  75. delete notifications[origin];
  76. }
  77. }
  78. return notifications;
  79. },
  80. // Attempt to read notification file, if it's not there we will create it.
  81. load: function() {
  82. var promise = OS.File.read(NOTIFICATION_STORE_PATH, { encoding: "utf-8"});
  83. return promise.then(
  84. function onSuccess(data) {
  85. if (data.length > 0) {
  86. // Preprocessing phase intends to cleanly separate any migration-related
  87. // tasks.
  88. this.notifications = this.filterNonAppNotifications(JSON.parse(data));
  89. }
  90. // populate the list of notifications by tag
  91. if (this.notifications) {
  92. for (var origin in this.notifications) {
  93. this.byTag[origin] = {};
  94. for (var id in this.notifications[origin]) {
  95. var curNotification = this.notifications[origin][id];
  96. if (curNotification.tag) {
  97. this.byTag[origin][curNotification.tag] = curNotification;
  98. }
  99. }
  100. }
  101. }
  102. this.loaded = true;
  103. }.bind(this),
  104. // If read failed, we assume we have no notifications to load.
  105. function onFailure(reason) {
  106. this.loaded = true;
  107. return this.createStore();
  108. }.bind(this)
  109. );
  110. },
  111. // Creates the notification directory.
  112. createStore: function() {
  113. var promise = OS.File.makeDir(NOTIFICATION_STORE_DIR, {
  114. ignoreExisting: true
  115. });
  116. return promise.then(
  117. this.createFile.bind(this)
  118. );
  119. },
  120. // Creates the notification file once the directory is created.
  121. createFile: function() {
  122. return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, "");
  123. },
  124. // Save current notifications to the file.
  125. save: function() {
  126. var data = JSON.stringify(this.notifications);
  127. return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, data, { encoding: "utf-8"});
  128. },
  129. // Helper function: promise will be resolved once file exists and/or is loaded.
  130. ensureLoaded: function() {
  131. if (!this.loaded) {
  132. return this.load();
  133. } else {
  134. return Promise.resolve();
  135. }
  136. },
  137. receiveMessage: function(message) {
  138. if (DEBUG) { debug("Received message:" + message.name); }
  139. // sendAsyncMessage can fail if the child process exits during a
  140. // notification storage operation, so always wrap it in a try/catch.
  141. function returnMessage(name, data) {
  142. try {
  143. message.target.sendAsyncMessage(name, data);
  144. } catch (e) {
  145. if (DEBUG) { debug("Return message failed, " + name); }
  146. }
  147. }
  148. switch (message.name) {
  149. case "Notification:GetAll":
  150. this.queueTask("getall", message.data).then(function(notifications) {
  151. returnMessage("Notification:GetAll:Return:OK", {
  152. requestID: message.data.requestID,
  153. origin: message.data.origin,
  154. notifications: notifications
  155. });
  156. }).catch(function(error) {
  157. returnMessage("Notification:GetAll:Return:KO", {
  158. requestID: message.data.requestID,
  159. origin: message.data.origin,
  160. errorMsg: error
  161. });
  162. });
  163. break;
  164. case "Notification:Save":
  165. this.queueTask("save", message.data).then(function() {
  166. returnMessage("Notification:Save:Return:OK", {
  167. requestID: message.data.requestID
  168. });
  169. }).catch(function(error) {
  170. returnMessage("Notification:Save:Return:KO", {
  171. requestID: message.data.requestID,
  172. errorMsg: error
  173. });
  174. });
  175. break;
  176. case "Notification:Delete":
  177. this.queueTask("delete", message.data).then(function() {
  178. returnMessage("Notification:Delete:Return:OK", {
  179. requestID: message.data.requestID
  180. });
  181. }).catch(function(error) {
  182. returnMessage("Notification:Delete:Return:KO", {
  183. requestID: message.data.requestID,
  184. errorMsg: error
  185. });
  186. });
  187. break;
  188. default:
  189. if (DEBUG) { debug("Invalid message name" + message.name); }
  190. }
  191. },
  192. // We need to make sure any read/write operations are atomic,
  193. // so use a queue to run each operation sequentially.
  194. queueTask: function(operation, data) {
  195. if (DEBUG) { debug("Queueing task: " + operation); }
  196. var defer = {};
  197. this.tasks.push({
  198. operation: operation,
  199. data: data,
  200. defer: defer
  201. });
  202. var promise = new Promise(function(resolve, reject) {
  203. defer.resolve = resolve;
  204. defer.reject = reject;
  205. });
  206. // Only run immediately if we aren't currently running another task.
  207. if (!this.runningTask) {
  208. if (DEBUG) { debug("Task queue was not running, starting now..."); }
  209. this.runNextTask();
  210. }
  211. return promise;
  212. },
  213. runNextTask: function() {
  214. if (this.tasks.length === 0) {
  215. if (DEBUG) { debug("No more tasks to run, queue depleted"); }
  216. this.runningTask = null;
  217. return;
  218. }
  219. this.runningTask = this.tasks.shift();
  220. // Always make sure we are loaded before performing any read/write tasks.
  221. this.ensureLoaded()
  222. .then(function() {
  223. var task = this.runningTask;
  224. switch (task.operation) {
  225. case "getall":
  226. return this.taskGetAll(task.data);
  227. break;
  228. case "save":
  229. return this.taskSave(task.data);
  230. break;
  231. case "delete":
  232. return this.taskDelete(task.data);
  233. break;
  234. }
  235. }.bind(this))
  236. .then(function(payload) {
  237. if (DEBUG) {
  238. debug("Finishing task: " + this.runningTask.operation);
  239. }
  240. this.runningTask.defer.resolve(payload);
  241. }.bind(this))
  242. .catch(function(err) {
  243. if (DEBUG) {
  244. debug("Error while running " + this.runningTask.operation + ": " + err);
  245. }
  246. this.runningTask.defer.reject(new String(err));
  247. }.bind(this))
  248. .then(function() {
  249. this.runNextTask();
  250. }.bind(this));
  251. },
  252. taskGetAll: function(data) {
  253. if (DEBUG) { debug("Task, getting all"); }
  254. var origin = data.origin;
  255. var notifications = [];
  256. // Grab only the notifications for specified origin.
  257. if (this.notifications[origin]) {
  258. for (var i in this.notifications[origin]) {
  259. notifications.push(this.notifications[origin][i]);
  260. }
  261. }
  262. return Promise.resolve(notifications);
  263. },
  264. taskSave: function(data) {
  265. if (DEBUG) { debug("Task, saving"); }
  266. var origin = data.origin;
  267. var notification = data.notification;
  268. if (!this.notifications[origin]) {
  269. this.notifications[origin] = {};
  270. this.byTag[origin] = {};
  271. }
  272. // We might have existing notification with this tag,
  273. // if so we need to remove it before saving the new one.
  274. if (notification.tag) {
  275. var oldNotification = this.byTag[origin][notification.tag];
  276. if (oldNotification) {
  277. delete this.notifications[origin][oldNotification.id];
  278. }
  279. this.byTag[origin][notification.tag] = notification;
  280. }
  281. this.notifications[origin][notification.id] = notification;
  282. return this.save();
  283. },
  284. taskDelete: function(data) {
  285. if (DEBUG) { debug("Task, deleting"); }
  286. var origin = data.origin;
  287. var id = data.id;
  288. if (!this.notifications[origin]) {
  289. if (DEBUG) { debug("No notifications found for origin: " + origin); }
  290. return Promise.resolve();
  291. }
  292. // Make sure we can find the notification to delete.
  293. var oldNotification = this.notifications[origin][id];
  294. if (!oldNotification) {
  295. if (DEBUG) { debug("No notification found with id: " + id); }
  296. return Promise.resolve();
  297. }
  298. if (oldNotification.tag) {
  299. delete this.byTag[origin][oldNotification.tag];
  300. }
  301. delete this.notifications[origin][id];
  302. return this.save();
  303. }
  304. };
  305. NotificationDB.init();