Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { throttle } = require("resource://devtools/shared/throttle.js");
class RootResourceCommand {
/**
* This class helps retrieving existing and listening to "root" resources.
*
* This is a fork of ResourceCommand, but specific to context-less
* resources which can be listened to right away when connecting to the RDP server.
*
* The main difference in term of implementation is that:
* - we receive a root front as constructor argument (instead of `commands` object)
* - we only listen for RDP events on the Root actor (instead of watcher and target actors)
* - there is no legacy listener support
* - there is no resource transformers
* - there is a lot of logic around targets that is removed here.
*
* See ResourceCommand for comments and jsdoc.
*
* TODO Bug 1758530 - Investigate sharing code with ResourceCommand instead of forking.
*
* @param object commands
* The commands object with all interfaces defined from devtools/shared/commands/
* @param object rootFront
* Front for the Root actor.
*/
constructor({ commands, rootFront }) {
this.rootFront = rootFront ? rootFront : commands.client.mainRoot;
this._onResourceAvailable = this._onResourceAvailable.bind(this);
this._onResourceDestroyed = this._onResourceDestroyed.bind(this);
this._watchers = [];
this._pendingWatchers = new Set();
this._cache = [];
this._listenedResources = new Set();
this._processingExistingResources = new Set();
this._notifyWatchers = this._notifyWatchers.bind(this);
this._throttledNotifyWatchers = throttle(this._notifyWatchers, 100);
}
getAllResources(resourceType) {
return this._cache.filter(r => r.resourceType === resourceType);
}
getResourceById(resourceType, resourceId) {
return this._cache.find(
r => r.resourceType === resourceType && r.resourceId === resourceId
);
}
async watchResources(resources, options) {
const {
onAvailable,
onUpdated,
onDestroyed,
ignoreExistingResources = false,
} = options;
if (typeof onAvailable !== "function") {
throw new Error(
"RootResourceCommand.watchResources expects an onAvailable function as argument"
);
}
for (const type of resources) {
if (!this._isValidResourceType(type)) {
throw new Error(
`RootResourceCommand.watchResources invoked with an unknown type: "${type}"`
);
}
}
const pendingWatcher = {
resources,
onAvailable,
};
this._pendingWatchers.add(pendingWatcher);
if (!this._listenerRegistered) {
this._listenerRegistered = true;
this.rootFront.on("resource-available-form", this._onResourceAvailable);
this.rootFront.on("resource-destroyed-form", this._onResourceDestroyed);
}
const promises = [];
for (const resource of resources) {
promises.push(this._startListening(resource));
}
await Promise.all(promises);
this._notifyWatchers();
this._pendingWatchers.delete(pendingWatcher);
const watchedResources = pendingWatcher.resources;
if (!watchedResources.length) {
return;
}
this._watchers.push({
resources: watchedResources,
onAvailable,
onUpdated,
onDestroyed,
pendingEvents: [],
});
if (!ignoreExistingResources) {
await this._forwardExistingResources(watchedResources, onAvailable);
}
}
unwatchResources(resources, options) {
const { onAvailable } = options;
if (typeof onAvailable !== "function") {
throw new Error(
"RootResourceCommand.unwatchResources expects an onAvailable function as argument"
);
}
for (const type of resources) {
if (!this._isValidResourceType(type)) {
throw new Error(
`RootResourceCommand.unwatchResources invoked with an unknown type: "${type}"`
);
}
}
const allWatchers = [...this._watchers, ...this._pendingWatchers];
for (const watcherEntry of allWatchers) {
if (watcherEntry.onAvailable == onAvailable) {
watcherEntry.resources = watcherEntry.resources.filter(resourceType => {
return !resources.includes(resourceType);
});
}
}
this._watchers = this._watchers.filter(entry => {
return !!entry.resources.length;
});
for (const resource of resources) {
const isResourceWatched = allWatchers.some(watcherEntry =>
watcherEntry.resources.includes(resource)
);
if (!isResourceWatched && this._listenedResources.has(resource)) {
this._stopListening(resource);
}
}
}
clearResources(resourceTypes) {
if (!Array.isArray(resourceTypes)) {
throw new Error("clearResources expects an array of resource types");
}
// Clear the cached resources of the type.
this._cache = this._cache.filter(
cachedResource => !resourceTypes.includes(cachedResource.resourceType)
);
if (resourceTypes.length) {
this.rootFront.clearResources(resourceTypes);
}
}
async waitForNextResource(
resourceType,
{ ignoreExistingResources = false, predicate } = {}
) {
predicate = predicate || (resource => !!resource);
let resolve;
const promise = new Promise(r => (resolve = r));
const onAvailable = async resources => {
const matchingResource = resources.find(resource => predicate(resource));
if (matchingResource) {
this.unwatchResources([resourceType], { onAvailable });
resolve(matchingResource);
}
};
await this.watchResources([resourceType], {
ignoreExistingResources,
onAvailable,
});
return { onResource: promise };
}
async _onResourceAvailable(resources) {
for (const resource of resources) {
const { resourceType } = resource;
resource.isAlreadyExistingResource =
this._processingExistingResources.has(resourceType);
this._queueResourceEvent("available", resourceType, resource);
this._cache.push(resource);
}
this._throttledNotifyWatchers();
}
async _onResourceDestroyed(resources) {
for (const resource of resources) {
const { resourceType, resourceId } = resource;
let index = -1;
if (resourceId) {
index = this._cache.findIndex(
cachedResource =>
cachedResource.resourceType == resourceType &&
cachedResource.resourceId == resourceId
);
} else {
index = this._cache.indexOf(resource);
}
if (index >= 0) {
this._cache.splice(index, 1);
} else {
console.warn(
`Resource ${resourceId || ""} of ${resourceType} was not found.`
);
}
this._queueResourceEvent("destroyed", resourceType, resource);
}
this._throttledNotifyWatchers();
}
_queueResourceEvent(callbackType, resourceType, update) {
for (const { resources, pendingEvents } of this._watchers) {
if (!resources.includes(resourceType)) {
continue;
}
if (pendingEvents.length) {
const lastEvent = pendingEvents[pendingEvents.length - 1];
if (lastEvent.callbackType == callbackType) {
lastEvent.updates.push(update);
continue;
}
}
pendingEvents.push({
callbackType,
updates: [update],
});
}
}
_notifyWatchers() {
for (const watcherEntry of this._watchers) {
const { onAvailable, onDestroyed, pendingEvents } = watcherEntry;
watcherEntry.pendingEvents = [];
for (const { callbackType, updates } of pendingEvents) {
try {
if (callbackType == "available") {
onAvailable(updates, { areExistingResources: false });
} else if (callbackType == "destroyed" && onDestroyed) {
onDestroyed(updates);
}
} catch (e) {
console.error(
"Exception while calling a RootResourceCommand",
callbackType,
"callback",
":",
e
);
}
}
}
}
_isValidResourceType(type) {
return this.ALL_TYPES.includes(type);
}
async _startListening(resourceType) {
if (this._listenedResources.has(resourceType)) {
return;
}
this._listenedResources.add(resourceType);
this._processingExistingResources.add(resourceType);
// For now, if the server doesn't support the resource type
// act as if we were listening, but do nothing.
// Calling watchResources/unwatchResources will work fine,
// but no resource will be notified.
if (this.rootFront.traits.resources?.[resourceType]) {
await this.rootFront.watchResources([resourceType]);
} else {
console.warn(
`Ignored watchRequest, resourceType "${resourceType}" not found in rootFront.traits.resources`
);
}
this._processingExistingResources.delete(resourceType);
}
async _forwardExistingResources(resourceTypes, onAvailable) {
const existingResources = this._cache.filter(resource =>
resourceTypes.includes(resource.resourceType)
);
if (existingResources.length) {
await onAvailable(existingResources, { areExistingResources: true });
}
}
_stopListening(resourceType) {
if (!this._listenedResources.has(resourceType)) {
throw new Error(
`Stopped listening for resource '${resourceType}' that isn't being listened to`
);
}
this._listenedResources.delete(resourceType);
this._cache = this._cache.filter(
cachedResource => cachedResource.resourceType !== resourceType
);
if (
!this.rootFront.isDestroyed() &&
this.rootFront.traits.resources?.[resourceType]
) {
this.rootFront.unwatchResources([resourceType]);
}
}
}
RootResourceCommand.TYPES = RootResourceCommand.prototype.TYPES = {
EXTENSIONS_BGSCRIPT_STATUS: "extensions-backgroundscript-status",
};
RootResourceCommand.ALL_TYPES = RootResourceCommand.prototype.ALL_TYPES =
Object.values(RootResourceCommand.TYPES);
module.exports = RootResourceCommand;