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/. */
6 ChromeUtils.defineESModuleGetters(lazy, {
7 JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
8 PushService: "resource://gre/modules/PushService.sys.mjs",
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"
19 return new ConsoleAPI({
20 maxLogLevelPref: "dom.push.loglevel",
21 prefix: "BroadcastService",
25 class InvalidSourceInfo extends Error {
26 constructor(message) {
28 this.name = "InvalidSourceInfo";
32 const BROADCAST_SERVICE_VERSION = 1;
34 export var BroadcastService = class {
35 constructor(pushService, path) {
39 BROADCAST: "broadcast",
42 this.pushService = pushService;
43 this.jsonFile = new lazy.JSONFile({
45 dataPostProcessor: this._initializeJSONFile,
47 this.initializePromise = this.jsonFile.load();
51 * Convert the listeners from our on-disk format to the format
52 * needed by a hello message.
54 async getListeners() {
55 await this.initializePromise;
56 return Object.entries(this.jsonFile.data.listeners).reduce(
65 _initializeJSONFile(data) {
67 data.version = BROADCAST_SERVICE_VERSION;
69 if (!data.hasOwnProperty("listeners")) {
76 * Reset to a state akin to what you would get in a new profile.
77 * In particular, wipe anything from storage.
79 * Used mainly for testing.
81 async _resetListeners() {
82 await this.initializePromise;
83 this.jsonFile.data = this._initializeJSONFile({});
84 this.initializePromise = Promise.resolve();
88 * Ensure that a sourceInfo is correct (has the expected fields).
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})`
97 if (typeof symbolName !== "string") {
98 throw new InvalidSourceInfo(
99 `symbolName must be a string (got ${typeof symbolName})`
105 * Add an entry for a given listener if it isn't present, or update
106 * one if it is already present.
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.
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
120 async addListener(broadcastId, version, sourceInfo) {
122 "addListener: adding listener",
127 await this.initializePromise;
128 this._validateSourceInfo(sourceInfo);
129 if (typeof version !== "string") {
130 throw new TypeError("version should be a string");
133 throw new TypeError("version should not be an empty string");
136 const isNew = !this.jsonFile.data.listeners.hasOwnProperty(broadcastId);
138 !isNew && this.jsonFile.data.listeners[broadcastId].version;
139 if (!isNew && oldVersion != version) {
141 "Versions differ while adding listener for",
145 "but JSON file says",
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.
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,
164 this.jsonFile.saveSoon();
167 await this.pushService.subscribeBroadcast(broadcastId, version);
172 * Call the listeners of the specified broadcasts.
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`
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) {
188 "because it's the dummy version"
192 // We don't know this broadcastID. This is probably a bug?
193 if (!this.jsonFile.data.listeners.hasOwnProperty(broadcastId)) {
195 "receivedBroadcastMessage: unknown broadcastId",
201 const { sourceInfo } = this.jsonFile.data.listeners[broadcastId];
203 this._validateSourceInfo(sourceInfo);
206 "receivedBroadcastMessage: malformed sourceInfo",
213 const { moduleURI, symbolName } = sourceInfo;
217 module = ChromeUtils.importESModule(moduleURI);
220 "receivedBroadcastMessage: couldn't invoke",
222 "because import of module",
230 if (!module[symbolName]) {
232 "receivedBroadcastMessage: couldn't invoke",
242 const handler = module[symbolName];
244 if (!handler.receivedBroadcastMessage) {
246 "receivedBroadcastMessage: couldn't invoke",
248 "because handler returned by",
249 `${moduleURI}.${symbolName}`,
250 "has no receivedBroadcastMessage method"
256 await handler.receivedBroadcastMessage(version, broadcastId, context);
259 "receivedBroadcastMessage: handler for",
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();
281 return this.jsonFile._save();
285 function initializeBroadcastService() {
286 // Fallback path for xpcshell tests.
287 let path = "broadcast-listeners.json";
289 if (PathUtils.profileDir) {
290 // Real path for use in a real profile.
291 path = PathUtils.join(PathUtils.profileDir, path);
294 return new BroadcastService(lazy.PushService, path);
297 export var pushBroadcastService = initializeBroadcastService();