summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/root-resource/root-resource-command.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/commands/root-resource/root-resource-command.js')
-rw-r--r--devtools/shared/commands/root-resource/root-resource-command.js348
1 files changed, 348 insertions, 0 deletions
diff --git a/devtools/shared/commands/root-resource/root-resource-command.js b/devtools/shared/commands/root-resource/root-resource-command.js
new file mode 100644
index 0000000000..1071d1bcb1
--- /dev/null
+++ b/devtools/shared/commands/root-resource/root-resource-command.js
@@ -0,0 +1,348 @@
+/* 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;