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/* 6 * Implements low-overhead integration between components of the application. 7 * This may have different uses depending on the component, including: 8 * 9 * - Providing product-specific implementations registered at startup. 10 * - Using alternative implementations during unit tests. 11 * - Allowing add-ons to change specific behaviors. 12 * 13 * Components may define one or more integration points, each defined by a 14 * root integration object whose properties and methods are the public interface 15 * and default implementation of the integration point. For example: 16 * 17 * const DownloadIntegration = { 18 * getTemporaryDirectory() { 19 * return "/tmp/"; 20 * }, 21 * 22 * getTemporaryFile(name) { 23 * return this.getTemporaryDirectory() + name; 24 * }, 25 * }; 26 * 27 * Other parts of the application may register overrides for some or all of the 28 * defined properties and methods. The component defining the integration point 29 * does not have to be loaded at this stage, because the name of the integration 30 * point is the only information required. For example, if the integration point 31 * is called "downloads": 32 * 33 * Integration.downloads.register(base => ({ 34 * getTemporaryDirectory() { 35 * return base.getTemporaryDirectory.call(this) + "subdir/"; 36 * }, 37 * })); 38 * 39 * When the component defining the integration point needs to call a method on 40 * the integration object, instead of using it directly the component would use 41 * the "getCombined" method to retrieve an object that includes all overrides. 42 * For example: 43 * 44 * let combined = Integration.downloads.getCombined(DownloadIntegration); 45 * Assert.is(combined.getTemporaryFile("file"), "/tmp/subdir/file"); 46 * 47 * Overrides can be registered at startup or at any later time, so each call to 48 * "getCombined" may return a different object. The simplest way to create a 49 * reference to the combined object that stays updated to the latest version is 50 * to define the root object in a JSM and use the "defineModuleGetter" method. 51 * 52 * *** Registration *** 53 * 54 * Since the interface is not declared formally, the registrations can happen 55 * at startup without loading the component, so they do not affect performance. 56 * 57 * Hovever, this module does not provide a startup registry, this means that the 58 * code that registers and implements the override must be loaded at startup. 59 * 60 * If performance for the override code is a concern, you can take advantage of 61 * the fact that the function used to create the override is called lazily, and 62 * include only a stub loader for the final code in an existing startup module. 63 * 64 * The registration of overrides should be repeated for each process where the 65 * relevant integration methods will be called. 66 * 67 * *** Accessing base methods and properties *** 68 * 69 * Overrides are included in the prototype chain of the combined object in the 70 * same order they were registered, where the first is closest to the root. 71 * 72 * When defining overrides, you do not need to set the "__proto__" property of 73 * the objects you create, because their properties and methods are moved to a 74 * new object with the correct prototype. If you do, however, you can call base 75 * properties and methods using the "super" keyword. For example: 76 * 77 * Integration.downloads.register(base => ({ 78 * __proto__: base, 79 * getTemporaryDirectory() { 80 * return super.getTemporaryDirectory() + "subdir/"; 81 * }, 82 * })); 83 * 84 * *** State handling *** 85 * 86 * Storing state directly on the combined integration object using the "this" 87 * reference is not recommended. When a new integration is registered, own 88 * properties stored on the old combined object are copied to the new combined 89 * object using a shallow copy, but the "this" reference for new invocations 90 * of the methods will be different. 91 * 92 * If the root object defines a property that always points to the same object, 93 * for example a "state" property, you can safely use it across registrations. 94 * 95 * Integration overrides provided by restartless add-ons should not use the 96 * "this" reference to store state, to avoid conflicts with other add-ons. 97 * 98 * *** Interaction with XPCOM *** 99 * 100 * Providing the combined object as an argument to any XPCOM method will 101 * generate a console error message, and will throw an exception where possible. 102 * For example, you cannot register observers directly on the combined object. 103 * This helps preventing mistakes due to the fact that the combined object 104 * reference changes when new integration overrides are registered. 105 */ 106 107"use strict"; 108 109this.EXPORTED_SYMBOLS = [ 110 "Integration", 111]; 112 113const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; 114 115Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 116 117/** 118 * Maps integration point names to IntegrationPoint objects. 119 */ 120const gIntegrationPoints = new Map(); 121 122/** 123 * This Proxy object creates IntegrationPoint objects using their name as key. 124 * The objects will be the same for the duration of the process. For example: 125 * 126 * Integration.downloads.register(...); 127 * Integration["addon-provided-integration"].register(...); 128 */ 129this.Integration = new Proxy({}, { 130 get(target, name) { 131 let integrationPoint = gIntegrationPoints.get(name); 132 if (!integrationPoint) { 133 integrationPoint = new IntegrationPoint(); 134 gIntegrationPoints.set(name, integrationPoint); 135 } 136 return integrationPoint; 137 }, 138}); 139 140/** 141 * Individual integration point for which overrides can be registered. 142 */ 143this.IntegrationPoint = function () { 144 this._overrideFns = new Set(); 145 this._combined = { 146 QueryInterface: function() { 147 let ex = new Components.Exception( 148 "Integration objects should not be used with XPCOM because" + 149 " they change when new overrides are registered.", 150 Cr.NS_ERROR_NO_INTERFACE); 151 Cu.reportError(ex); 152 throw ex; 153 }, 154 }; 155} 156 157this.IntegrationPoint.prototype = { 158 /** 159 * Ordered set of registered functions defining integration overrides. 160 */ 161 _overrideFns: null, 162 163 /** 164 * Combined integration object. When this reference changes, properties 165 * defined directly on this object are copied to the new object. 166 * 167 * Initially, the only property of this object is a "QueryInterface" method 168 * that throws an exception, to prevent misuse as a permanent XPCOM listener. 169 */ 170 _combined: null, 171 172 /** 173 * Indicates whether the integration object is current based on the list of 174 * registered integration overrides. 175 */ 176 _combinedIsCurrent: false, 177 178 /** 179 * Registers new overrides for the integration methods. For example: 180 * 181 * Integration.nameOfIntegrationPoint.register(base => ({ 182 * asyncMethod: Task.async(function* () { 183 * return yield base.asyncMethod.apply(this, arguments); 184 * }), 185 * })); 186 * 187 * @param overrideFn 188 * Function returning an object defining the methods that should be 189 * overridden. Its only parameter is an object that contains the base 190 * implementation of all the available methods. 191 * 192 * @note The override function is called every time the list of registered 193 * override functions changes. Thus, it should not have any side 194 * effects or do any other initialization. 195 */ 196 register(overrideFn) { 197 this._overrideFns.add(overrideFn); 198 this._combinedIsCurrent = false; 199 }, 200 201 /** 202 * Removes a previously registered integration override. 203 * 204 * Overrides don't usually need to be unregistered, unless they are added by a 205 * restartless add-on, in which case they should be unregistered when the 206 * add-on is disabled or uninstalled. 207 * 208 * @param overrideFn 209 * This must be the same function object passed to "register". 210 */ 211 unregister(overrideFn) { 212 this._overrideFns.delete(overrideFn); 213 this._combinedIsCurrent = false; 214 }, 215 216 /** 217 * Retrieves the dynamically generated object implementing the integration 218 * methods. Platform-specific code and add-ons can override methods of this 219 * object using the "register" method. 220 */ 221 getCombined(root) { 222 if (this._combinedIsCurrent) { 223 return this._combined; 224 } 225 226 // In addition to enumerating all the registered integration overrides in 227 // order, we want to keep any state that was previously stored in the 228 // combined object using the "this" reference in integration methods. 229 let overrideFnArray = [...this._overrideFns, () => this._combined]; 230 231 let combined = root; 232 for (let overrideFn of overrideFnArray) { 233 try { 234 // Obtain a new set of methods from the next override function in the 235 // list, specifying the current combined object as the base argument. 236 let override = overrideFn.call(null, combined); 237 238 // Retrieve a list of property descriptors from the returned object, and 239 // use them to build a new combined object whose prototype points to the 240 // previous combined object. 241 let descriptors = {}; 242 for (let name of Object.getOwnPropertyNames(override)) { 243 descriptors[name] = Object.getOwnPropertyDescriptor(override, name); 244 } 245 combined = Object.create(combined, descriptors); 246 } catch (ex) { 247 // Any error will result in the current override being skipped. 248 Cu.reportError(ex); 249 } 250 } 251 252 this._combinedIsCurrent = true; 253 return this._combined = combined; 254 }, 255 256 /** 257 * Defines a getter to retrieve the dynamically generated object implementing 258 * the integration methods, loading the root implementation lazily from the 259 * specified JSM module. For example: 260 * 261 * Integration.test.defineModuleGetter(this, "TestIntegration", 262 * "resource://testing-common/TestIntegration.jsm"); 263 * 264 * @param targetObject 265 * The object on which the lazy getter will be defined. 266 * @param name 267 * The name of the getter to define. 268 * @param moduleUrl 269 * The URL used to obtain the module. 270 * @param symbol [optional] 271 * The name of the symbol exported by the module. This can be omitted 272 * if the name of the exported symbol is equal to the getter name. 273 */ 274 defineModuleGetter(targetObject, name, moduleUrl, symbol) { 275 let moduleHolder = {}; 276 XPCOMUtils.defineLazyModuleGetter(moduleHolder, name, moduleUrl, symbol); 277 Object.defineProperty(targetObject, name, { 278 get: () => this.getCombined(moduleHolder[name]), 279 configurable: true, 280 enumerable: true, 281 }); 282 }, 283}; 284