378 lines
11 KiB
JavaScript
378 lines
11 KiB
JavaScript
/* 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._onResourceAvailableArray = this._onResourceAvailableArray.bind(this);
|
|
this._onResourceDestroyedArray = this._onResourceDestroyedArray.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(
|
|
"resources-available-array",
|
|
this._onResourceAvailableArray
|
|
);
|
|
this.rootFront.on(
|
|
"resources-destroyed-array",
|
|
this._onResourceDestroyedArray
|
|
);
|
|
}
|
|
|
|
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 _onResourceAvailableArray(array) {
|
|
for (const [resourceType, resources] of array) {
|
|
for (const resource of resources) {
|
|
if (!("resourceType" in resource)) {
|
|
resource.resourceType = resourceType;
|
|
}
|
|
}
|
|
this._onResourceAvailable(resources);
|
|
}
|
|
}
|
|
|
|
async _onResourceDestroyedArray(context, array) {
|
|
for (const [resourceType, resources] of array) {
|
|
for (const resource of resources) {
|
|
if (!("resourceType" in resource)) {
|
|
resource.resourceType = resourceType;
|
|
}
|
|
}
|
|
this._onResourceDestroyed(resources);
|
|
}
|
|
}
|
|
|
|
_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();
|
|
}
|
|
|
|
_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;
|