dom/src/notification/NotificationDB.jsm

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

mercurial