root-resource-command.js (11191B)
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 "use strict"; 6 7 const { throttle } = require("resource://devtools/shared/throttle.js"); 8 9 class RootResourceCommand { 10 /** 11 * This class helps retrieving existing and listening to "root" resources. 12 * 13 * This is a fork of ResourceCommand, but specific to context-less 14 * resources which can be listened to right away when connecting to the RDP server. 15 * 16 * The main difference in term of implementation is that: 17 * - we receive a root front as constructor argument (instead of `commands` object) 18 * - we only listen for RDP events on the Root actor (instead of watcher and target actors) 19 * - there is no legacy listener support 20 * - there is no resource transformers 21 * - there is a lot of logic around targets that is removed here. 22 * 23 * See ResourceCommand for comments and jsdoc. 24 * 25 * TODO Bug 1758530 - Investigate sharing code with ResourceCommand instead of forking. 26 * 27 * @param object commands 28 * The commands object with all interfaces defined from devtools/shared/commands/ 29 * @param object rootFront 30 * Front for the Root actor. 31 */ 32 constructor({ commands, rootFront }) { 33 this.rootFront = rootFront ? rootFront : commands.client.mainRoot; 34 35 this._onResourceAvailable = this._onResourceAvailable.bind(this); 36 this._onResourceDestroyed = this._onResourceDestroyed.bind(this); 37 this._onResourceAvailableArray = this._onResourceAvailableArray.bind(this); 38 this._onResourceDestroyedArray = this._onResourceDestroyedArray.bind(this); 39 40 this._watchers = []; 41 42 this._pendingWatchers = new Set(); 43 44 this._cache = []; 45 this._listenedResources = new Set(); 46 47 this._processingExistingResources = new Set(); 48 49 this._notifyWatchers = this._notifyWatchers.bind(this); 50 this._throttledNotifyWatchers = throttle(this._notifyWatchers, 100); 51 } 52 53 getAllResources(resourceType) { 54 return this._cache.filter(r => r.resourceType === resourceType); 55 } 56 57 getResourceById(resourceType, resourceId) { 58 return this._cache.find( 59 r => r.resourceType === resourceType && r.resourceId === resourceId 60 ); 61 } 62 63 async watchResources(resources, options) { 64 const { 65 onAvailable, 66 onUpdated, 67 onDestroyed, 68 ignoreExistingResources = false, 69 } = options; 70 71 if (typeof onAvailable !== "function") { 72 throw new Error( 73 "RootResourceCommand.watchResources expects an onAvailable function as argument" 74 ); 75 } 76 77 for (const type of resources) { 78 if (!this._isValidResourceType(type)) { 79 throw new Error( 80 `RootResourceCommand.watchResources invoked with an unknown type: "${type}"` 81 ); 82 } 83 } 84 85 const pendingWatcher = { 86 resources, 87 onAvailable, 88 }; 89 this._pendingWatchers.add(pendingWatcher); 90 91 if (!this._listenerRegistered) { 92 this._listenerRegistered = true; 93 this.rootFront.on( 94 "resources-available-array", 95 this._onResourceAvailableArray 96 ); 97 this.rootFront.on( 98 "resources-destroyed-array", 99 this._onResourceDestroyedArray 100 ); 101 } 102 103 const promises = []; 104 for (const resource of resources) { 105 promises.push(this._startListening(resource)); 106 } 107 await Promise.all(promises); 108 109 this._notifyWatchers(); 110 111 this._pendingWatchers.delete(pendingWatcher); 112 113 const watchedResources = pendingWatcher.resources; 114 115 if (!watchedResources.length) { 116 return; 117 } 118 119 this._watchers.push({ 120 resources: watchedResources, 121 onAvailable, 122 onUpdated, 123 onDestroyed, 124 pendingEvents: [], 125 }); 126 127 if (!ignoreExistingResources) { 128 await this._forwardExistingResources(watchedResources, onAvailable); 129 } 130 } 131 132 unwatchResources(resources, options) { 133 const { onAvailable } = options; 134 135 if (typeof onAvailable !== "function") { 136 throw new Error( 137 "RootResourceCommand.unwatchResources expects an onAvailable function as argument" 138 ); 139 } 140 141 for (const type of resources) { 142 if (!this._isValidResourceType(type)) { 143 throw new Error( 144 `RootResourceCommand.unwatchResources invoked with an unknown type: "${type}"` 145 ); 146 } 147 } 148 149 const allWatchers = [...this._watchers, ...this._pendingWatchers]; 150 for (const watcherEntry of allWatchers) { 151 if (watcherEntry.onAvailable == onAvailable) { 152 watcherEntry.resources = watcherEntry.resources.filter(resourceType => { 153 return !resources.includes(resourceType); 154 }); 155 } 156 } 157 this._watchers = this._watchers.filter(entry => { 158 return !!entry.resources.length; 159 }); 160 161 for (const resource of resources) { 162 const isResourceWatched = allWatchers.some(watcherEntry => 163 watcherEntry.resources.includes(resource) 164 ); 165 166 if (!isResourceWatched && this._listenedResources.has(resource)) { 167 this._stopListening(resource); 168 } 169 } 170 } 171 172 clearResources(resourceTypes) { 173 if (!Array.isArray(resourceTypes)) { 174 throw new Error("clearResources expects an array of resource types"); 175 } 176 // Clear the cached resources of the type. 177 this._cache = this._cache.filter( 178 cachedResource => !resourceTypes.includes(cachedResource.resourceType) 179 ); 180 181 if (resourceTypes.length) { 182 this.rootFront.clearResources(resourceTypes); 183 } 184 } 185 186 async waitForNextResource( 187 resourceType, 188 { ignoreExistingResources = false, predicate } = {} 189 ) { 190 predicate = predicate || (resource => !!resource); 191 192 let resolve; 193 const promise = new Promise(r => (resolve = r)); 194 const onAvailable = async resources => { 195 const matchingResource = resources.find(resource => predicate(resource)); 196 if (matchingResource) { 197 this.unwatchResources([resourceType], { onAvailable }); 198 resolve(matchingResource); 199 } 200 }; 201 202 await this.watchResources([resourceType], { 203 ignoreExistingResources, 204 onAvailable, 205 }); 206 return { onResource: promise }; 207 } 208 209 async _onResourceAvailableArray(array) { 210 for (const [resourceType, resources] of array) { 211 for (const resource of resources) { 212 if (!("resourceType" in resource)) { 213 resource.resourceType = resourceType; 214 } 215 } 216 this._onResourceAvailable(resources); 217 } 218 } 219 220 async _onResourceDestroyedArray(context, array) { 221 for (const [resourceType, resources] of array) { 222 for (const resource of resources) { 223 if (!("resourceType" in resource)) { 224 resource.resourceType = resourceType; 225 } 226 } 227 this._onResourceDestroyed(resources); 228 } 229 } 230 231 _onResourceAvailable(resources) { 232 for (const resource of resources) { 233 const { resourceType } = resource; 234 235 resource.isAlreadyExistingResource = 236 this._processingExistingResources.has(resourceType); 237 238 this._queueResourceEvent("available", resourceType, resource); 239 240 this._cache.push(resource); 241 } 242 243 this._throttledNotifyWatchers(); 244 } 245 246 _onResourceDestroyed(resources) { 247 for (const resource of resources) { 248 const { resourceType, resourceId } = resource; 249 250 let index = -1; 251 if (resourceId) { 252 index = this._cache.findIndex( 253 cachedResource => 254 cachedResource.resourceType == resourceType && 255 cachedResource.resourceId == resourceId 256 ); 257 } else { 258 index = this._cache.indexOf(resource); 259 } 260 if (index >= 0) { 261 this._cache.splice(index, 1); 262 } else { 263 console.warn( 264 `Resource ${resourceId || ""} of ${resourceType} was not found.` 265 ); 266 } 267 268 this._queueResourceEvent("destroyed", resourceType, resource); 269 } 270 this._throttledNotifyWatchers(); 271 } 272 273 _queueResourceEvent(callbackType, resourceType, update) { 274 for (const { resources, pendingEvents } of this._watchers) { 275 if (!resources.includes(resourceType)) { 276 continue; 277 } 278 if (pendingEvents.length) { 279 const lastEvent = pendingEvents[pendingEvents.length - 1]; 280 if (lastEvent.callbackType == callbackType) { 281 lastEvent.updates.push(update); 282 continue; 283 } 284 } 285 pendingEvents.push({ 286 callbackType, 287 updates: [update], 288 }); 289 } 290 } 291 292 _notifyWatchers() { 293 for (const watcherEntry of this._watchers) { 294 const { onAvailable, onDestroyed, pendingEvents } = watcherEntry; 295 watcherEntry.pendingEvents = []; 296 297 for (const { callbackType, updates } of pendingEvents) { 298 try { 299 if (callbackType == "available") { 300 onAvailable(updates, { areExistingResources: false }); 301 } else if (callbackType == "destroyed" && onDestroyed) { 302 onDestroyed(updates); 303 } 304 } catch (e) { 305 console.error( 306 "Exception while calling a RootResourceCommand", 307 callbackType, 308 "callback", 309 ":", 310 e 311 ); 312 } 313 } 314 } 315 } 316 317 _isValidResourceType(type) { 318 return this.ALL_TYPES.includes(type); 319 } 320 321 async _startListening(resourceType) { 322 if (this._listenedResources.has(resourceType)) { 323 return; 324 } 325 this._listenedResources.add(resourceType); 326 327 this._processingExistingResources.add(resourceType); 328 329 // For now, if the server doesn't support the resource type 330 // act as if we were listening, but do nothing. 331 // Calling watchResources/unwatchResources will work fine, 332 // but no resource will be notified. 333 if (this.rootFront.traits.resources?.[resourceType]) { 334 await this.rootFront.watchResources([resourceType]); 335 } else { 336 console.warn( 337 `Ignored watchRequest, resourceType "${resourceType}" not found in rootFront.traits.resources` 338 ); 339 } 340 this._processingExistingResources.delete(resourceType); 341 } 342 343 async _forwardExistingResources(resourceTypes, onAvailable) { 344 const existingResources = this._cache.filter(resource => 345 resourceTypes.includes(resource.resourceType) 346 ); 347 if (existingResources.length) { 348 await onAvailable(existingResources, { areExistingResources: true }); 349 } 350 } 351 352 _stopListening(resourceType) { 353 if (!this._listenedResources.has(resourceType)) { 354 throw new Error( 355 `Stopped listening for resource '${resourceType}' that isn't being listened to` 356 ); 357 } 358 this._listenedResources.delete(resourceType); 359 360 this._cache = this._cache.filter( 361 cachedResource => cachedResource.resourceType !== resourceType 362 ); 363 364 if ( 365 !this.rootFront.isDestroyed() && 366 this.rootFront.traits.resources?.[resourceType] 367 ) { 368 this.rootFront.unwatchResources([resourceType]); 369 } 370 } 371 } 372 373 RootResourceCommand.TYPES = RootResourceCommand.prototype.TYPES = { 374 EXTENSIONS_BGSCRIPT_STATUS: "extensions-backgroundscript-status", 375 }; 376 RootResourceCommand.ALL_TYPES = RootResourceCommand.prototype.ALL_TYPES = 377 Object.values(RootResourceCommand.TYPES); 378 module.exports = RootResourceCommand;