|
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(); |