/* 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("devtools/shared/throttle"); class ResourceWatcher { /** * This class helps retrieving existing and listening to resources. * A resource is something that: * - the target you are debugging exposes * - can be created as early as the process/worker/page starts loading * - can already exist, or will be created later on * - doesn't require any user data to be fetched, only a type/category * * @param {TargetList} targetList * A TargetList instance, which helps communicating to the backend * in order to iterate and listen over the requested resources. */ constructor(targetList) { this.targetList = targetList; this._onTargetAvailable = this._onTargetAvailable.bind(this); this._onTargetDestroyed = this._onTargetDestroyed.bind(this); this._onResourceAvailable = this._onResourceAvailable.bind(this); this._onResourceDestroyed = this._onResourceDestroyed.bind(this); // Array of all the currently registered watchers, which contains object with attributes: // - {String} resources: list of all resource watched by this one watcher // - {Function} onAvailable: watcher's function to call when a new resource is available // - {Function} onUpdated: watcher's function to call when a resource has been updated // - {Function} onDestroyed: watcher's function to call when a resource is destroyed this._watchers = []; // Cache for all resources by the order that the resource was taken. this._cache = []; this._listenerCount = new Map(); // WeakMap used to avoid starting a legacy listener twice for the same // target + resource-type pair. Legacy listener creation can be subject to // race conditions. // Maps a target front to an array of resource types. this._existingLegacyListeners = new WeakMap(); this._notifyWatchers = this._notifyWatchers.bind(this); this._throttledNotifyWatchers = throttle(this._notifyWatchers, 100); } get watcherFront() { return this.targetList.watcherFront; } /** * Return all specified resources cached in this watcher. * * @param {String} resourceType * @return {Array} resources cached in this watcher */ getAllResources(resourceType) { return this._cache.filter(r => r.resourceType === resourceType); } /** * Return the specified resource cached in this watcher. * * @param {String} resourceType * @param {String} resourceId * @return {Object} resource cached in this watcher */ getResourceById(resourceType, resourceId) { return this._cache.find( r => r.resourceType === resourceType && r.resourceId === resourceId ); } /** * Request to start retrieving all already existing instances of given * type of resources and also start watching for the one to be created after. * * @param {Array:string} resources * List of all resources which should be fetched and observed. * @param {Object} options * - {Function} onAvailable: This attribute is mandatory. * Function which will be called once per existing * resource and each time a resource is created. * - {Function} onUpdated: This attribute is optional. * Function which will be called each time a resource, * previously notified via onAvailable is updated. * - {Function} onDestroyed: This attribute is optional. * Function which will be called each time a resource in * the remote target is destroyed. * - {boolean} ignoreExistingResources: * This attribute is optional. Default value is false. * If set to true, onAvailable won't be called with * existing resources. */ async watchResources(resources, options) { const { onAvailable, onUpdated, onDestroyed, ignoreExistingResources = false, } = options; if (typeof onAvailable !== "function") { throw new Error( "ResourceWatcher.watchResources expects an onAvailable function as argument" ); } // Bug 1675763: Watcher actor is not available in all situations yet. if (!this._listenerRegistered && this.watcherFront) { this._listenerRegistered = true; // Resources watched from the parent process will be emitted on the Watcher Actor. // So that we also have to listen for this event on it, in addition to all targets. this.watcherFront.on( "resource-available-form", this._onResourceAvailable.bind(this, { watcherFront: this.watcherFront, }) ); this.watcherFront.on( "resource-updated-form", this._onResourceUpdated.bind(this, { watcherFront: this.watcherFront }) ); this.watcherFront.on( "resource-destroyed-form", this._onResourceDestroyed.bind(this, { watcherFront: this.watcherFront, }) ); } // First ensuring enabling listening to targets. // This will call onTargetAvailable for all already existing targets, // as well as for the one created later. // Do this *before* calling _startListening in order to register // "resource-available" listener before requesting for the resources in _startListening. await this._watchAllTargets(); for (const resource of resources) { // If we are registering the first listener, so start listening from the server about // this one resource. if (!this._hasListenerForResource(resource)) { await this._startListening(resource); } } // The resource cache is immediately filled when receiving the sources, but they are // emitted with a delay due to throttling. Since the cache can contain resources that // will soon be emitted, we have to flush it before adding the new listeners. // Otherwise forwardCacheResources might emit resources that will also be emitted by // the next `_notifyWatchers` call done when calling `_startListening`, which will pull the // "already existing" resources. this._notifyWatchers(); // Register the watcher just after calling _startListening in order to avoid it being called // for already existing resources, which will optionally be notified via _forwardCachedResources this._watchers.push({ resources, onAvailable, onUpdated, onDestroyed, pendingEvents: [], }); if (!ignoreExistingResources) { await this._forwardCachedResources(resources, onAvailable); } } /** * Stop watching for given type of resources. * See `watchResources` for the arguments as both methods receive the same. * Note that `onUpdated` and `onDestroyed` attributes of `options` aren't used here. * Only `onAvailable` attribute is looked up and we unregister all the other registered callbacks * when a matching available callback is found. */ unwatchResources(resources, options) { const { onAvailable } = options; if (typeof onAvailable !== "function") { throw new Error( "ResourceWatcher.unwatchResources expects an onAvailable function as argument" ); } const watchedResources = []; for (const resource of resources) { if (this._hasListenerForResource(resource)) { watchedResources.push(resource); } } // Unregister the callbacks from the _watchers registry for (const watcherEntry of this._watchers) { // onAvailable is the only mandatory argument which ends up being used to match // the right watcher entry. if (watcherEntry.onAvailable == onAvailable) { // Remove all resources that we stop watching. We may still watch for some others. watcherEntry.resources = watcherEntry.resources.filter(resourceType => { return !resources.includes(resourceType); }); } } this._watchers = this._watchers.filter(entry => { // Remove entries entirely if it isn't watching for any resource type return entry.resources.length > 0; }); // Stop listening to all resources that no longer have any watcher callback for (const resource of watchedResources) { if (!this._hasListenerForResource(resource)) { this._stopListening(resource); } } // Stop watching for targets if we removed the last listener. let listeners = 0; for (const count of this._listenerCount.values()) { listeners += count; } if (listeners <= 0) { this._unwatchAllTargets(); } } /** * Start watching for all already existing and future targets. * * We are using ALL_TYPES, but this won't force listening to all types. * It will only listen for types which are defined by `TargetList.startListening`. */ async _watchAllTargets() { if (!this._watchTargetsPromise) { this._watchTargetsPromise = this.targetList.watchTargets( this.targetList.ALL_TYPES, this._onTargetAvailable, this._onTargetDestroyed ); } return this._watchTargetsPromise; } _unwatchAllTargets() { if (!this._watchTargetsPromise) { return; } this._watchTargetsPromise = null; this.targetList.unwatchTargets( this.targetList.ALL_TYPES, this._onTargetAvailable, this._onTargetDestroyed ); } /** * Method called by the TargetList for each already existing or target which has just been created. * * @param {Front} targetFront * The Front of the target that is available. * This Front inherits from TargetMixin and is typically * composed of a BrowsingContextTargetFront or ContentProcessTargetFront. */ async _onTargetAvailable({ targetFront, isTargetSwitching }) { const resources = []; if (isTargetSwitching) { this._onWillNavigate(targetFront); // WatcherActor currently only watches additional frame targets and // explicitely ignores top level one that may be created when navigating // to a new process. // In order to keep working resources that are being watched via the // Watcher actor, we have to unregister and re-register the resource // types. This will force calling `Resources.watchResources` on the new top // level target. for (const resourceType of Object.values(ResourceWatcher.TYPES)) { // ...which has at least one listener... if (!this._listenerCount.get(resourceType)) { continue; } await this._stopListening(resourceType, { bypassListenerCount: true }); resources.push(resourceType); } } if (targetFront.isDestroyed()) { return; } targetFront.on("will-navigate", () => this._onWillNavigate(targetFront)); // If we are target switching, we already stop & start listening to all the // currently monitored resources. if (!isTargetSwitching) { // For each resource type... for (const resourceType of Object.values(ResourceWatcher.TYPES)) { // ...which has at least one listener... if (!this._listenerCount.get(resourceType)) { continue; } // ...request existing resource and new one to come from this one target // *but* only do that for backward compat, where we don't have the watcher API // (See bug 1626647) await this._watchResourcesForTarget(targetFront, resourceType); } } // Compared to the TargetList and Watcher.watchTargets, // We do call Watcher.watchResources, but the events are fired on the target. // That's because the Watcher runs in the parent process/main thread, while resources // are available from the target's process/thread. targetFront.on( "resource-available-form", this._onResourceAvailable.bind(this, { targetFront }) ); targetFront.on( "resource-updated-form", this._onResourceUpdated.bind(this, { targetFront }) ); targetFront.on( "resource-destroyed-form", this._onResourceDestroyed.bind(this, { targetFront }) ); if (isTargetSwitching) { for (const resourceType of resources) { await this._startListening(resourceType, { bypassListenerCount: true }); } } } /** * Method called by the TargetList when a target has just been destroyed * See _onTargetAvailable for arguments, they are the same. */ _onTargetDestroyed({ targetFront }) { // Clear the map of legacy listeners for this target. this._existingLegacyListeners.set(targetFront, []); //TODO: Is there a point in doing anything else? // // We could remove the available/destroyed event, but as the target is destroyed // its listeners will be destroyed anyway. } /** * Method called either by: * - the backward compatibility code (LegacyListeners) * - target actors RDP events * whenever an already existing resource is being listed or when a new one * has been created. * * @param {Object} source * A dictionary object with only one of these two attributes: * - targetFront: a Target Front, if the resource is watched from the target process or thread * - watcherFront: a Watcher Front, if the resource is watched from the parent process * @param {Array} resources * Depending on the resource Type, it can be an Array composed of either JSON objects or Fronts, * which describes the resource. */ async _onResourceAvailable({ targetFront, watcherFront }, resources) { for (let resource of resources) { const { resourceType } = resource; if (watcherFront) { targetFront = await this._getTargetForWatcherResource(resource); if (!targetFront) { continue; } } // Put the targetFront on the resource for easy retrieval. // (Resources from the legacy listeners may already have the attribute set) if (!resource.targetFront) { resource.targetFront = targetFront; } if (ResourceTransformers[resourceType]) { resource = ResourceTransformers[resourceType]({ resource, targetList: this.targetList, targetFront, watcherFront: this.watcherFront, }); } this._queueResourceEvent("available", resourceType, resource); this._cache.push(resource); } this._throttledNotifyWatchers(); } /** * Method called either by: * - the backward compatibility code (LegacyListeners) * - target actors RDP events * Called everytime a resource is updated in the remote target. * * @param {Object} source * Please see _onResourceAvailable for this parameter. * @param {Array} updates * Depending on the listener. * * Among the element in the array, the following attributes are given special handling. * - resourceType {String}: * The type of resource to be updated. * - resourceId {String}: * The id of resource to be updated. * - resourceUpdates {Object}: * If resourceUpdates is in the element, a cached resource specified by resourceType * and resourceId is updated by Object.assign(cachedResource, resourceUpdates). * - nestedResourceUpdates {Object}: * If `nestedResourceUpdates` is passed, update one nested attribute with a new value * This allows updating one attribute of an object stored in a resource's attribute, * as well as adding new elements to arrays. * `path` is an array mentioning all nested attribute to walk through. * `value` is the new nested attribute value to set. * * And also, the element is passed to the listener as it is as “update” object. * So if we don't want to update a cached resource but have information want to * pass on to the listener, can pass it on using attributes other than the ones * listed above. * For example, if the element consists of like * "{ resourceType:… resourceId:…, testValue: “test”, }”, * the listener can receive the value as follows. * * onResourceUpdate({ update }) { * console.log(update.testValue); // “test” should be displayed * } */ async _onResourceUpdated({ targetFront, watcherFront }, updates) { for (const update of updates) { const { resourceType, resourceId, resourceUpdates, nestedResourceUpdates, } = update; if (!resourceId) { console.warn(`Expected resource ${resourceType} to have a resourceId`); } const existingResource = this._cache.find( cachedResource => cachedResource.resourceType === resourceType && cachedResource.resourceId === resourceId ); if (!existingResource) { continue; } if (resourceUpdates) { Object.assign(existingResource, resourceUpdates); } if (nestedResourceUpdates) { for (const { path, value } of nestedResourceUpdates) { let target = existingResource; for (let i = 0; i < path.length - 1; i++) { target = target[path[i]]; } target[path[path.length - 1]] = value; } } this._queueResourceEvent("updated", resourceType, { resource: existingResource, update, }); } this._throttledNotifyWatchers(); } /** * Called everytime a resource is destroyed in the remote target. * See _onResourceAvailable for the argument description. */ async _onResourceDestroyed({ targetFront, watcherFront }, 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(); } /** * Check if there is at least one listener registered for the given resource type. * * @param {String} resourceType * Watched resource type */ _hasListenerForResource(resourceType) { return this._watchers.some(({ resources }) => { return resources.includes(resourceType); }); } _queueResourceEvent(callbackType, resourceType, update) { for (const { resources, pendingEvents } of this._watchers) { // This watcher doesn't listen to this type of resource if (!resources.includes(resourceType)) { continue; } // If we receive a new event of the same type, accumulate the new update in the last event if (pendingEvents.length > 0) { const lastEvent = pendingEvents[pendingEvents.length - 1]; if (lastEvent.callbackType == callbackType) { lastEvent.updates.push(update); continue; } } // Otherwise, pile up a new event, which will force calling watcher // callback a new time pendingEvents.push({ callbackType, updates: [update], }); } } /** * Flush the pending event and notify all the currently registered watchers * about all the available, updated and destroyed events that have been accumulated in * `_watchers`'s `pendingEvents` arrays. */ _notifyWatchers() { for (const watcherEntry of this._watchers) { const { onAvailable, onUpdated, onDestroyed, pendingEvents, } = watcherEntry; // Immediately clear the buffer in order to avoid possible races, where an event listener // would end up somehow adding a new throttled resource watcherEntry.pendingEvents = []; for (const { callbackType, updates } of pendingEvents) { try { if (callbackType == "available") { onAvailable(updates); } else if (callbackType == "updated" && onUpdated) { onUpdated(updates); } else if (callbackType == "destroyed" && onDestroyed) { onDestroyed(updates); } } catch (e) { console.error( "Exception while calling a ResourceWatcher", callbackType, "callback", ":", e ); } } } } // Compute the target front if the resource comes from the Watcher Actor. // (`targetFront` will be null as the watcher is in the parent process // and targets are in distinct processes) _getTargetForWatcherResource(resource) { const { browsingContextID, resourceType } = resource; // Resource emitted from the Watcher Actor should all have a // browsingContextID attribute if (!browsingContextID) { console.error( `Resource of ${resourceType} is missing a browsingContextID attribute` ); return null; } return this.watcherFront.getBrowsingContextTarget(browsingContextID); } _onWillNavigate(targetFront) { if (targetFront.isTopLevel) { this._cache = []; return; } this._cache = this._cache.filter( cachedResource => cachedResource.targetFront !== targetFront ); } /** * Tells if the server supports listening to the given resource type * via the watcher actor's watchResources method. * * @return {Boolean} True, if the server supports this type. */ hasResourceWatcherSupport(resourceType) { return this.watcherFront?.traits?.resources?.[resourceType]; } /** * Tells if the server supports listening to the given resource type * via the watcher actor's watchResources method, and that, for a specific * target. * * @return {Boolean} True, if the server supports this type. */ _hasResourceWatcherSupportForTarget(resourceType, targetFront) { // First check if the watcher supports this target type. // If it doesn't, no resource type can be listened via the Watcher actor for this target. if (!this.targetList.hasTargetWatcherSupport(targetFront.targetType)) { return false; } return this.hasResourceWatcherSupport(resourceType); } /** * Start listening for a given type of resource. * For backward compatibility code, we register the legacy listeners on * each individual target * * @param {String} resourceType * One string of ResourceWatcher.TYPES, which designates the types of resources * to be listened. * @param {Object} * - {Boolean} bypassListenerCount * Pass true to avoid checking/updating the listenersCount map. * Exclusively used when target switching, to stop & start listening * to all resources. */ async _startListening(resourceType, { bypassListenerCount = false } = {}) { if (!bypassListenerCount) { let listeners = this._listenerCount.get(resourceType) || 0; listeners++; this._listenerCount.set(resourceType, listeners); if (listeners > 1) { return; } } // If the server supports the Watcher API and the Watcher supports // this resource type, use this API if (this.hasResourceWatcherSupport(resourceType)) { await this.watcherFront.watchResources([resourceType]); // Bug 1678385: In order to support watching for JS Source resource // for service workers and parent process workers, which aren't supported yet // by the watcher actor, we do not bail out here and allow to execute // the legacy listener for these targets. // Once bug 1608848 is fixed, we can remove this and always return. // If this isn't fixed soon, we may add other resources we want to see // being fetched from these targets. const shouldRunLegacyListeners = resourceType == ResourceWatcher.TYPES.SOURCE; if (!shouldRunLegacyListeners) { return; } } // Otherwise, fallback on backward compat mode and use LegacyListeners. // If this is the first listener for this type of resource, // we should go through all the existing targets as onTargetAvailable // has already been called for these existing targets. const promises = []; const targets = this.targetList.getAllTargets(this.targetList.ALL_TYPES); for (const target of targets) { promises.push(this._watchResourcesForTarget(target, resourceType)); } await Promise.all(promises); } async _forwardCachedResources(resourceTypes, onAvailable) { const cachedResources = this._cache.filter(resource => resourceTypes.includes(resource.resourceType) ); if (cachedResources.length > 0) { await onAvailable(cachedResources); } } /** * Call backward compatibility code from `LegacyListeners` in order to listen for a given * type of resource from a given target. */ _watchResourcesForTarget(targetFront, resourceType) { if (this._hasResourceWatcherSupportForTarget(resourceType, targetFront)) { // This resource / target pair should already be handled by the watcher, // no need to start legacy listeners. return Promise.resolve(); } if (targetFront.isDestroyed()) { return Promise.resolve(); } const onAvailable = this._onResourceAvailable.bind(this, { targetFront }); const onUpdated = this._onResourceUpdated.bind(this, { targetFront }); const onDestroyed = this._onResourceDestroyed.bind(this, { targetFront }); if (!(resourceType in LegacyListeners)) { throw new Error(`Missing legacy listener for ${resourceType}`); } const legacyListeners = this._existingLegacyListeners.get(targetFront) || []; if (legacyListeners.includes(resourceType)) { console.error( `Already started legacy listener for ${resourceType} on ${targetFront.actorID}` ); return Promise.resolve(); } this._existingLegacyListeners.set( targetFront, legacyListeners.concat(resourceType) ); return LegacyListeners[resourceType]({ targetList: this.targetList, targetFront, onAvailable, onDestroyed, onUpdated, }); } /** * Reverse of _startListening. Stop listening for a given type of resource. * For backward compatibility, we unregister from each individual target. * * See _startListening for parameters description. */ _stopListening(resourceType, { bypassListenerCount = false } = {}) { if (!bypassListenerCount) { let listeners = this._listenerCount.get(resourceType); if (!listeners || listeners <= 0) { throw new Error( `Stopped listening for resource '${resourceType}' that isn't being listened to` ); } listeners--; this._listenerCount.set(resourceType, listeners); if (listeners > 0) { return; } } // Clear the cached resources of the type. this._cache = this._cache.filter( cachedResource => cachedResource.resourceType !== resourceType ); // If the server supports the Watcher API and the Watcher supports // this resource type, use this API if (this.hasResourceWatcherSupport(resourceType)) { if (!this.watcherFront.isDestroyed()) { this.watcherFront.unwatchResources([resourceType]); } // See comment in `_startListening` const shouldRunLegacyListeners = resourceType == ResourceWatcher.TYPES.SOURCE; if (!shouldRunLegacyListeners) { return; } } // Otherwise, fallback on backward compat mode and use LegacyListeners. // If this was the last listener, we should stop watching these events from the actors // and the actors should stop watching things from the platform const targets = this.targetList.getAllTargets(this.targetList.ALL_TYPES); for (const target of targets) { this._unwatchResourcesForTarget(target, resourceType); } } /** * Backward compatibility code, reverse of _watchResourcesForTarget. */ _unwatchResourcesForTarget(targetFront, resourceType) { if (this._hasResourceWatcherSupportForTarget(resourceType, targetFront)) { // This resource / target pair should already be handled by the watcher, // no need to stop legacy listeners. } // Is there really a point in: // - unregistering `onAvailable` RDP event callbacks from target-scoped actors? // - calling `stopListeners()` as we are most likely closing the toolbox and destroying everything? // // It is important to keep this method synchronous and do as less as possible // in the case of toolbox destroy. // // We are aware of one case where that might be useful. // When a panel is disabled via the options panel, after it has been opened. // Would that justify doing this? Is there another usecase? // XXX: This is most likely only needed to avoid growing the Map infinitely. // Unless in the "disabled panel" use case mentioned in the comment above, // we should not see the same target actorID again. const listeners = this._existingLegacyListeners.get(targetFront); if (listeners && listeners.includes(resourceType)) { const remainingListeners = listeners.filter(l => l !== resourceType); this._existingLegacyListeners.set(targetFront, remainingListeners); } } } ResourceWatcher.TYPES = ResourceWatcher.prototype.TYPES = { CONSOLE_MESSAGE: "console-message", CSS_CHANGE: "css-change", CSS_MESSAGE: "css-message", ERROR_MESSAGE: "error-message", PLATFORM_MESSAGE: "platform-message", DOCUMENT_EVENT: "document-event", ROOT_NODE: "root-node", STYLESHEET: "stylesheet", NETWORK_EVENT: "network-event", WEBSOCKET: "websocket", COOKIE: "cookie", LOCAL_STORAGE: "local-storage", SESSION_STORAGE: "session-storage", CACHE_STORAGE: "Cache", EXTENSION_STORAGE: "extension-storage", INDEXED_DB: "indexed-db", NETWORK_EVENT_STACKTRACE: "network-event-stacktrace", SOURCE: "source", }; module.exports = { ResourceWatcher, TYPES: ResourceWatcher.TYPES }; // Backward compat code for each type of resource. // Each section added here should eventually be removed once the equivalent server // code is implement in Firefox, in its release channel. const LegacyListeners = { [ResourceWatcher.TYPES .CONSOLE_MESSAGE]: require("devtools/shared/resources/legacy-listeners/console-messages"), [ResourceWatcher.TYPES .CSS_CHANGE]: require("devtools/shared/resources/legacy-listeners/css-changes"), [ResourceWatcher.TYPES .CSS_MESSAGE]: require("devtools/shared/resources/legacy-listeners/css-messages"), [ResourceWatcher.TYPES .ERROR_MESSAGE]: require("devtools/shared/resources/legacy-listeners/error-messages"), [ResourceWatcher.TYPES .PLATFORM_MESSAGE]: require("devtools/shared/resources/legacy-listeners/platform-messages"), async [ResourceWatcher.TYPES.DOCUMENT_EVENT]({ targetList, targetFront, onAvailable, }) { // DocumentEventsListener of webconsole handles only top level document. if (!targetFront.isTopLevel) { return; } const webConsoleFront = await targetFront.getFront("console"); webConsoleFront.on("documentEvent", event => { event.resourceType = ResourceWatcher.TYPES.DOCUMENT_EVENT; onAvailable([event]); }); await webConsoleFront.startListeners(["DocumentEvents"]); }, [ResourceWatcher.TYPES .ROOT_NODE]: require("devtools/shared/resources/legacy-listeners/root-node"), [ResourceWatcher.TYPES .STYLESHEET]: require("devtools/shared/resources/legacy-listeners/stylesheet"), [ResourceWatcher.TYPES .NETWORK_EVENT]: require("devtools/shared/resources/legacy-listeners/network-events"), [ResourceWatcher.TYPES .WEBSOCKET]: require("devtools/shared/resources/legacy-listeners/websocket"), [ResourceWatcher.TYPES .COOKIE]: require("devtools/shared/resources/legacy-listeners/cookie"), [ResourceWatcher.TYPES .LOCAL_STORAGE]: require("devtools/shared/resources/legacy-listeners/local-storage"), [ResourceWatcher.TYPES .SESSION_STORAGE]: require("devtools/shared/resources/legacy-listeners/session-storage"), [ResourceWatcher.TYPES .CACHE_STORAGE]: require("devtools/shared/resources/legacy-listeners/cache-storage"), [ResourceWatcher.TYPES .EXTENSION_STORAGE]: require("devtools/shared/resources/legacy-listeners/extension-storage"), [ResourceWatcher.TYPES .INDEXED_DB]: require("devtools/shared/resources/legacy-listeners/indexed-db"), [ResourceWatcher.TYPES .NETWORK_EVENT_STACKTRACE]: require("devtools/shared/resources/legacy-listeners/network-event-stacktraces"), [ResourceWatcher.TYPES .SOURCE]: require("devtools/shared/resources/legacy-listeners/source"), }; // Optional transformers for each type of resource. // Each module added here should be a function that will receive the resource, the target, … // and perform some transformation on the resource before it will be emitted. // This is a good place to handle backward compatibility and manual resource marshalling. const ResourceTransformers = { [ResourceWatcher.TYPES .CONSOLE_MESSAGE]: require("devtools/shared/resources/transformers/console-messages"), [ResourceWatcher.TYPES .ERROR_MESSAGE]: require("devtools/shared/resources/transformers/error-messages"), [ResourceWatcher.TYPES .LOCAL_STORAGE]: require("devtools/shared/resources/transformers/storage-local-storage.js"), [ResourceWatcher.TYPES .SESSION_STORAGE]: require("devtools/shared/resources/transformers/storage-session-storage.js"), [ResourceWatcher.TYPES .NETWORK_EVENT]: require("devtools/shared/resources/transformers/network-events"), };