Messaging.sys.mjs (8225B)
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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const IS_PARENT_PROCESS = 6 Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT; 7 8 function DispatcherDelegate(aDispatcher, aMessageManager) { 9 this._dispatcher = aDispatcher; 10 this._messageManager = aMessageManager; 11 12 if (!aDispatcher) { 13 // Child process. 14 // TODO: this doesn't work with Fission, remove this code path once every 15 // consumer has been migrated. Bug 1569360. 16 this._replies = new Map(); 17 (aMessageManager || Services.cpmm).addMessageListener( 18 "GeckoView:MessagingReply", 19 this 20 ); 21 } 22 } 23 24 DispatcherDelegate.prototype = { 25 /** 26 * Register a listener to be notified of event(s). 27 * 28 * @param aListener Target listener implementing nsIGeckoViewEventListener. 29 * @param aEvents String or array of strings of events to listen to. 30 */ 31 registerListener(aListener, aEvents) { 32 if (!this._dispatcher) { 33 throw new Error("Can only listen in parent process"); 34 } 35 this._dispatcher.registerListener(aListener, aEvents); 36 }, 37 38 /** 39 * Unregister a previously-registered listener. 40 * 41 * @param aListener Registered listener implementing nsIGeckoViewEventListener. 42 * @param aEvents String or array of strings of events to stop listening to. 43 */ 44 unregisterListener(aListener, aEvents) { 45 if (!this._dispatcher) { 46 throw new Error("Can only listen in parent process"); 47 } 48 this._dispatcher.unregisterListener(aListener, aEvents); 49 }, 50 51 /** 52 * Dispatch an event to registered listeners for that event, and pass an 53 * optional data object and/or a optional callback interface to the 54 * listeners. 55 * 56 * @param aEvent Name of event to dispatch. 57 * @param aData Optional object containing data for the event. 58 * @param aCallback Optional callback implementing nsIGeckoViewEventCallback. 59 * @param aFinalizer Optional finalizer implementing nsIGeckoViewEventFinalizer. 60 */ 61 dispatch(aEvent, aData, aCallback, aFinalizer) { 62 if (this._dispatcher) { 63 this._dispatcher.dispatch(aEvent, aData, aCallback, aFinalizer); 64 return; 65 } 66 67 const mm = this._messageManager || Services.cpmm; 68 const forwardData = { 69 global: !this._messageManager, 70 event: aEvent, 71 data: aData, 72 }; 73 74 if (aCallback) { 75 const uuid = Services.uuid.generateUUID().toString(); 76 this._replies.set(uuid, { 77 callback: aCallback, 78 finalizer: aFinalizer, 79 }); 80 forwardData.uuid = uuid; 81 } 82 83 mm.sendAsyncMessage("GeckoView:Messaging", forwardData); 84 }, 85 86 /** 87 * Sends a request to Java. 88 * 89 * @param aMsg Message to send; must be an object with a "type" property 90 * @param aCallback Optional callback implementing nsIGeckoViewEventCallback. 91 */ 92 sendRequest(aMsg, aCallback) { 93 const type = aMsg.type; 94 aMsg.type = undefined; 95 this.dispatch(type, aMsg, aCallback); 96 }, 97 98 /** 99 * Sends a request to Java, returning a Promise that resolves to the response. 100 * 101 * @param aMsg Message to send; must be an object with a "type" property 102 * @return A Promise resolving to the response 103 */ 104 sendRequestForResult(aMsg) { 105 return new Promise((resolve, reject) => { 106 const type = aMsg.type; 107 aMsg.type = undefined; 108 109 // Manually release the resolve/reject functions after one callback is 110 // received, so the JS GC is not tied up with the Java GC. 111 const onCallback = (callback, ...args) => { 112 if (callback) { 113 callback(...args); 114 } 115 resolve = undefined; 116 reject = undefined; 117 }; 118 const callback = { 119 onSuccess: result => onCallback(resolve, result), 120 onError: error => onCallback(reject, error), 121 onFinalize: _ => onCallback(reject), 122 }; 123 this.dispatch(type, aMsg, callback, callback); 124 }); 125 }, 126 127 finalize() { 128 if (!this._replies) { 129 return; 130 } 131 this._replies.forEach(reply => { 132 if (typeof reply.finalizer === "function") { 133 reply.finalizer(); 134 } else if (reply.finalizer) { 135 reply.finalizer.onFinalize(); 136 } 137 }); 138 this._replies.clear(); 139 }, 140 141 receiveMessage(aMsg) { 142 const { uuid, type } = aMsg.data; 143 const reply = this._replies.get(uuid); 144 if (!reply) { 145 return; 146 } 147 148 if (type === "success") { 149 reply.callback.onSuccess(aMsg.data.response); 150 } else if (type === "error") { 151 reply.callback.onError(aMsg.data.response); 152 } else if (type === "finalize") { 153 if (typeof reply.finalizer === "function") { 154 reply.finalizer(); 155 } else if (reply.finalizer) { 156 reply.finalizer.onFinalize(); 157 } 158 this._replies.delete(uuid); 159 } else { 160 throw new Error("invalid reply type"); 161 } 162 }, 163 }; 164 165 export var EventDispatcher = { 166 instance: new DispatcherDelegate( 167 IS_PARENT_PROCESS ? Services.geckoviewBridge : undefined 168 ), 169 170 /** 171 * Return an EventDispatcher instance for a chrome DOM window. In a content 172 * process, return a proxy through the message manager that automatically 173 * forwards events to the main process. 174 * 175 * To force using a message manager proxy (for example in a frame script 176 * environment), call forMessageManager. 177 * 178 * @param aWindow a chrome DOM window. 179 */ 180 for(aWindow) { 181 const view = 182 aWindow && 183 aWindow.arguments && 184 aWindow.arguments[0] && 185 aWindow.arguments[0].QueryInterface(Ci.nsIGeckoViewView); 186 187 if (!view) { 188 const mm = !IS_PARENT_PROCESS && aWindow && aWindow.messageManager; 189 if (!mm) { 190 throw new Error( 191 "window is not a GeckoView-connected window and does" + 192 " not have a message manager" 193 ); 194 } 195 return this.forMessageManager(mm); 196 } 197 198 return new DispatcherDelegate(view); 199 }, 200 201 /** 202 * Returns a named EventDispatcher, which can communicate with the 203 * corresponding EventDispatcher on the java side. 204 */ 205 byName(aName) { 206 if (!IS_PARENT_PROCESS) { 207 return undefined; 208 } 209 const dispatcher = Services.geckoviewBridge.getDispatcherByName(aName); 210 return new DispatcherDelegate(dispatcher); 211 }, 212 213 /** 214 * Return an EventDispatcher instance for a message manager associated with a 215 * window. 216 * 217 * @param aWindow a message manager. 218 */ 219 forMessageManager(aMessageManager) { 220 return new DispatcherDelegate(null, aMessageManager); 221 }, 222 223 receiveMessage(aMsg) { 224 // aMsg.data includes keys: global, event, data, uuid 225 let callback; 226 if (aMsg.data.uuid) { 227 const reply = (type, response) => { 228 const mm = aMsg.data.global ? aMsg.target : aMsg.target.messageManager; 229 if (!mm) { 230 if (type === "finalize") { 231 // It's normal for the finalize call to come after the browser has 232 // been destroyed. We can gracefully handle that case despite 233 // having no message manager. 234 return; 235 } 236 throw Error( 237 `No message manager for ${aMsg.data.event}:${type} reply` 238 ); 239 } 240 mm.sendAsyncMessage("GeckoView:MessagingReply", { 241 type, 242 response, 243 uuid: aMsg.data.uuid, 244 }); 245 }; 246 callback = { 247 onSuccess: response => reply("success", response), 248 onError: error => reply("error", error), 249 onFinalize: () => reply("finalize"), 250 }; 251 } 252 253 try { 254 if (aMsg.data.global) { 255 this.instance.dispatch( 256 aMsg.data.event, 257 aMsg.data.data, 258 callback, 259 callback 260 ); 261 return; 262 } 263 264 const win = aMsg.target.ownerGlobal; 265 const dispatcher = win.WindowEventDispatcher || this.for(win); 266 dispatcher.dispatch(aMsg.data.event, aMsg.data.data, callback, callback); 267 } catch (e) { 268 callback?.onError(`Error getting dispatcher: ${e}`); 269 throw e; 270 } 271 }, 272 }; 273 274 if (IS_PARENT_PROCESS) { 275 Services.mm.addMessageListener("GeckoView:Messaging", EventDispatcher); 276 Services.ppmm.addMessageListener("GeckoView:Messaging", EventDispatcher); 277 }