no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / dom / push / PushBroadcastService.sys.mjs
blobeea31ef1921ce5b350ac1c94a44594de37e50df9
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/. */
5 const lazy = {};
6 ChromeUtils.defineESModuleGetters(lazy, {
7   JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
8   PushService: "resource://gre/modules/PushService.sys.mjs",
9 });
11 // BroadcastService is exported for test purposes.
12 // We are supposed to ignore any updates with this version.
13 const DUMMY_VERSION_STRING = "____NOP____";
15 ChromeUtils.defineLazyGetter(lazy, "console", () => {
16   let { ConsoleAPI } = ChromeUtils.importESModule(
17     "resource://gre/modules/Console.sys.mjs"
18   );
19   return new ConsoleAPI({
20     maxLogLevelPref: "dom.push.loglevel",
21     prefix: "BroadcastService",
22   });
23 });
25 class InvalidSourceInfo extends Error {
26   constructor(message) {
27     super(message);
28     this.name = "InvalidSourceInfo";
29   }
32 const BROADCAST_SERVICE_VERSION = 1;
34 export var BroadcastService = class {
35   constructor(pushService, path) {
36     this.PHASES = {
37       HELLO: "hello",
38       REGISTER: "register",
39       BROADCAST: "broadcast",
40     };
42     this.pushService = pushService;
43     this.jsonFile = new lazy.JSONFile({
44       path,
45       dataPostProcessor: this._initializeJSONFile,
46     });
47     this.initializePromise = this.jsonFile.load();
48   }
50   /**
51    * Convert the listeners from our on-disk format to the format
52    * needed by a hello message.
53    */
54   async getListeners() {
55     await this.initializePromise;
56     return Object.entries(this.jsonFile.data.listeners).reduce(
57       (acc, [k, v]) => {
58         acc[k] = v.version;
59         return acc;
60       },
61       {}
62     );
63   }
65   _initializeJSONFile(data) {
66     if (!data.version) {
67       data.version = BROADCAST_SERVICE_VERSION;
68     }
69     if (!data.hasOwnProperty("listeners")) {
70       data.listeners = {};
71     }
72     return data;
73   }
75   /**
76    * Reset to a state akin to what you would get in a new profile.
77    * In particular, wipe anything from storage.
78    *
79    * Used mainly for testing.
80    */
81   async _resetListeners() {
82     await this.initializePromise;
83     this.jsonFile.data = this._initializeJSONFile({});
84     this.initializePromise = Promise.resolve();
85   }
87   /**
88    * Ensure that a sourceInfo is correct (has the expected fields).
89    */
90   _validateSourceInfo(sourceInfo) {
91     const { moduleURI, symbolName } = sourceInfo;
92     if (typeof moduleURI !== "string") {
93       throw new InvalidSourceInfo(
94         `moduleURI must be a string (got ${typeof moduleURI})`
95       );
96     }
97     if (typeof symbolName !== "string") {
98       throw new InvalidSourceInfo(
99         `symbolName must be a string (got ${typeof symbolName})`
100       );
101     }
102   }
104   /**
105    * Add an entry for a given listener if it isn't present, or update
106    * one if it is already present.
107    *
108    * Note that this means only a single listener can be set for a
109    * given subscription. This is a limitation in the current API that
110    * stems from the fact that there can only be one source of truth
111    * for the subscriber's version. As a workaround, you can define a
112    * listener which calls multiple other listeners.
113    *
114    * @param {string} broadcastId The broadcastID to listen for
115    * @param {string} version The most recent version we have for
116    *   updates from this broadcastID
117    * @param {Object} sourceInfo A description of the handler for
118    *   updates on this broadcastID
119    */
120   async addListener(broadcastId, version, sourceInfo) {
121     lazy.console.info(
122       "addListener: adding listener",
123       broadcastId,
124       version,
125       sourceInfo
126     );
127     await this.initializePromise;
128     this._validateSourceInfo(sourceInfo);
129     if (typeof version !== "string") {
130       throw new TypeError("version should be a string");
131     }
132     if (!version) {
133       throw new TypeError("version should not be an empty string");
134     }
136     const isNew = !this.jsonFile.data.listeners.hasOwnProperty(broadcastId);
137     const oldVersion =
138       !isNew && this.jsonFile.data.listeners[broadcastId].version;
139     if (!isNew && oldVersion != version) {
140       lazy.console.warn(
141         "Versions differ while adding listener for",
142         broadcastId,
143         ". Got",
144         version,
145         "but JSON file says",
146         oldVersion,
147         "."
148       );
149     }
151     // Update listeners before telling the pushService to subscribe,
152     // in case it would disregard the update in the small window
153     // between getting listeners and setting state to RUNNING.
154     //
155     // Keep the old version (if we have it) because Megaphone is
156     // really the source of truth for the current version of this
157     // broadcaster, and the old version is whatever we've either
158     // gotten from Megaphone or what we've told to Megaphone and
159     // haven't been corrected.
160     this.jsonFile.data.listeners[broadcastId] = {
161       version: oldVersion || version,
162       sourceInfo,
163     };
164     this.jsonFile.saveSoon();
166     if (isNew) {
167       await this.pushService.subscribeBroadcast(broadcastId, version);
168     }
169   }
171   /**
172    * Call the listeners of the specified broadcasts.
173    *
174    * @param {Array<Object>} broadcasts Map between broadcast ids and versions.
175    * @param {Object} context Additional information about the context in which the
176    *  broadcast notification was originally received. This is transmitted to listeners.
177    * @param {String} context.phase One of `BroadcastService.PHASES`
178    */
179   async receivedBroadcastMessage(broadcasts, context) {
180     lazy.console.info("receivedBroadcastMessage:", broadcasts, context);
181     await this.initializePromise;
182     for (const broadcastId in broadcasts) {
183       const version = broadcasts[broadcastId];
184       if (version === DUMMY_VERSION_STRING) {
185         lazy.console.info(
186           "Ignoring",
187           version,
188           "because it's the dummy version"
189         );
190         continue;
191       }
192       // We don't know this broadcastID. This is probably a bug?
193       if (!this.jsonFile.data.listeners.hasOwnProperty(broadcastId)) {
194         lazy.console.warn(
195           "receivedBroadcastMessage: unknown broadcastId",
196           broadcastId
197         );
198         continue;
199       }
201       const { sourceInfo } = this.jsonFile.data.listeners[broadcastId];
202       try {
203         this._validateSourceInfo(sourceInfo);
204       } catch (e) {
205         lazy.console.error(
206           "receivedBroadcastMessage: malformed sourceInfo",
207           sourceInfo,
208           e
209         );
210         continue;
211       }
213       const { moduleURI, symbolName } = sourceInfo;
215       let module;
216       try {
217         module = ChromeUtils.importESModule(moduleURI);
218       } catch (e) {
219         lazy.console.error(
220           "receivedBroadcastMessage: couldn't invoke",
221           broadcastId,
222           "because import of module",
223           moduleURI,
224           "failed",
225           e
226         );
227         continue;
228       }
230       if (!module[symbolName]) {
231         lazy.console.error(
232           "receivedBroadcastMessage: couldn't invoke",
233           broadcastId,
234           "because module",
235           moduleURI,
236           "missing attribute",
237           symbolName
238         );
239         continue;
240       }
242       const handler = module[symbolName];
244       if (!handler.receivedBroadcastMessage) {
245         lazy.console.error(
246           "receivedBroadcastMessage: couldn't invoke",
247           broadcastId,
248           "because handler returned by",
249           `${moduleURI}.${symbolName}`,
250           "has no receivedBroadcastMessage method"
251         );
252         continue;
253       }
255       try {
256         await handler.receivedBroadcastMessage(version, broadcastId, context);
257       } catch (e) {
258         lazy.console.error(
259           "receivedBroadcastMessage: handler for",
260           broadcastId,
261           "threw error:",
262           e
263         );
264         continue;
265       }
267       // Broadcast message applied successfully. Update the version we
268       // received if it's different than the one we had.  We don't
269       // enforce an ordering here (i.e. we use != instead of <)
270       // because we don't know what the ordering of the service's
271       // versions is going to be.
272       if (this.jsonFile.data.listeners[broadcastId].version != version) {
273         this.jsonFile.data.listeners[broadcastId].version = version;
274         this.jsonFile.saveSoon();
275       }
276     }
277   }
279   // For test only.
280   _saveImmediately() {
281     return this.jsonFile._save();
282   }
285 function initializeBroadcastService() {
286   // Fallback path for xpcshell tests.
287   let path = "broadcast-listeners.json";
288   try {
289     if (PathUtils.profileDir) {
290       // Real path for use in a real profile.
291       path = PathUtils.join(PathUtils.profileDir, path);
292     }
293   } catch (e) {}
294   return new BroadcastService(lazy.PushService, path);
297 export var pushBroadcastService = initializeBroadcastService();