When we process resources from the parent process, it is challenging to figure out what is the current top level window global. In web extensions, the top level window global can change over time if there is a background page or not. Differential Revision: https://phabricator.services.mozilla.com/D220668
1416 lines
53 KiB
JavaScript
1416 lines
53 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");
|
|
|
|
let gLastResourceId = 0;
|
|
|
|
function cacheKey(resourceType, resourceId) {
|
|
return `${resourceType}:${resourceId}`;
|
|
}
|
|
|
|
class ResourceCommand {
|
|
/**
|
|
* 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 object commands
|
|
* The commands object with all interfaces defined from devtools/shared/commands/
|
|
*/
|
|
constructor({ commands }) {
|
|
this.targetCommand = commands.targetCommand;
|
|
|
|
// Public attribute set by tests to disable throttling
|
|
this.throttlingDisabled = false;
|
|
|
|
this._onTargetAvailable = this._onTargetAvailable.bind(this);
|
|
this._onTargetDestroyed = this._onTargetDestroyed.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 = [];
|
|
|
|
// Set of watchers currently going through watchResources, only used to handle
|
|
// early calls to unwatchResources. Using a Set instead of an array for easier
|
|
// delete operations.
|
|
this._pendingWatchers = new Set();
|
|
|
|
// Caches for all resources by the order that the resource was taken.
|
|
this._cache = new Map();
|
|
this._listenedResources = new Set();
|
|
|
|
// 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._processingExistingResources = new Set();
|
|
|
|
// List of targetFront event listener unregistration functions keyed by target front.
|
|
// These are called when unwatching resources, so if a consumer starts watching resources again,
|
|
// we don't have listeners registered twice.
|
|
this._offTargetFrontListeners = new Map();
|
|
|
|
// Bug 1914386: We used to throttle the resource on client and should try to remove it entirely.
|
|
const throttleDelay = 0;
|
|
this._notifyWatchers = this._notifyWatchers.bind(this);
|
|
this._throttledNotifyWatchers = throttle(
|
|
this._notifyWatchers,
|
|
throttleDelay
|
|
);
|
|
}
|
|
|
|
get watcherFront() {
|
|
return this.targetCommand.watcherFront;
|
|
}
|
|
|
|
addResourceToCache(resource) {
|
|
const { resourceId, resourceType } = resource;
|
|
if (TRANSIENT_RESOURCE_TYPES.includes(resourceType)) {
|
|
return;
|
|
}
|
|
this._cache.set(cacheKey(resourceType, resourceId), resource);
|
|
}
|
|
|
|
/**
|
|
* Clear all the resources related to specifed resource types.
|
|
* Should also trigger clearing of the caches that exists on the related
|
|
* serverside resource watchers.
|
|
*
|
|
* @param {Array:string} resourceTypes
|
|
* A list of all the resource types whose
|
|
* resources shouled be cleared.
|
|
*/
|
|
async clearResources(resourceTypes) {
|
|
if (!Array.isArray(resourceTypes)) {
|
|
throw new Error("clearResources expects a list of resources types");
|
|
}
|
|
// Clear the cached resources of the type.
|
|
for (const [key, resource] of this._cache) {
|
|
if (resourceTypes.includes(resource.resourceType)) {
|
|
// NOTE: To anyone paranoid like me, yes it is okay to delete from a Map while iterating it.
|
|
this._cache.delete(key);
|
|
}
|
|
}
|
|
|
|
const resourcesToClear = resourceTypes.filter(resourceType =>
|
|
this.hasResourceCommandSupport(resourceType)
|
|
);
|
|
if (resourcesToClear.length) {
|
|
this.watcherFront.clearResources(resourcesToClear);
|
|
}
|
|
}
|
|
/**
|
|
* Return all specified resources cached in this watcher.
|
|
*
|
|
* @param {String} resourceType
|
|
* @return {Array} resources cached in this watcher
|
|
*/
|
|
getAllResources(resourceType) {
|
|
const result = [];
|
|
for (const resource of this._cache.values()) {
|
|
if (resource.resourceType === resourceType) {
|
|
result.push(resource);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 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.get(cacheKey(resourceType, 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 with an array of resources
|
|
* each time resource(s) are created.
|
|
* A second dictionary argument with `areExistingResources` boolean
|
|
* attribute helps knowing if that's live resources, or some coming
|
|
* from ResourceCommand cache.
|
|
* - {Function} onUpdated: This attribute is optional.
|
|
* Function which will be called with an array of updates resources
|
|
* each time resource(s) are updated.
|
|
* These resources were previously notified via onAvailable.
|
|
* - {Function} onDestroyed: This attribute is optional.
|
|
* Function which will be called with an array of deleted resources
|
|
* each time resource(s) are 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(
|
|
"ResourceCommand.watchResources expects an onAvailable function as argument"
|
|
);
|
|
}
|
|
|
|
for (const type of resources) {
|
|
if (!this._isValidResourceType(type)) {
|
|
throw new Error(
|
|
`ResourceCommand.watchResources invoked with an unknown type: "${type}"`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Copy the array in order to avoid the callsite to modify the list of watched resources by mutating the array.
|
|
// You have to call (un)watchResources to update the list of resources being watched!
|
|
resources = [...resources];
|
|
|
|
// Pending watchers are used in unwatchResources to remove watchers which
|
|
// are not fully registered yet. Store `onAvailable` which is the unique key
|
|
// for a watcher, as well as the resources array, so that unwatchResources
|
|
// can update the array if we stop watching a specific resource.
|
|
const pendingWatcher = {
|
|
resources,
|
|
onAvailable,
|
|
};
|
|
this._pendingWatchers.add(pendingWatcher);
|
|
|
|
// 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(
|
|
"resources-available-array",
|
|
this._onResourceAvailableArray.bind(this, {
|
|
watcherFront: this.watcherFront,
|
|
})
|
|
);
|
|
this.watcherFront.on(
|
|
"resources-updated-array",
|
|
this._onResourceUpdatedArray.bind(this, {
|
|
watcherFront: this.watcherFront,
|
|
})
|
|
);
|
|
this.watcherFront.on(
|
|
"resources-destroyed-array",
|
|
this._onResourceDestroyedArray.bind(this, {
|
|
watcherFront: this.watcherFront,
|
|
})
|
|
);
|
|
}
|
|
|
|
const promises = [];
|
|
for (const resource of resources) {
|
|
promises.push(this._startListening(resource));
|
|
}
|
|
await Promise.all(promises);
|
|
|
|
// 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 _forwardExistingResources 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();
|
|
|
|
// Update the _pendingWatchers set before adding the watcher to _watchers.
|
|
this._pendingWatchers.delete(pendingWatcher);
|
|
|
|
// If unwatchResources was called in the meantime, use pendingWatcher's
|
|
// resources to get the updated list of watched resources.
|
|
const watchedResources = pendingWatcher.resources;
|
|
|
|
// If no resource needs to be watched anymore, do not add an empty watcher
|
|
// to _watchers, and do not notify about cached resources.
|
|
if (!watchedResources.length) {
|
|
return;
|
|
}
|
|
|
|
// Register the watcher just after calling _startListening in order to avoid it being called
|
|
// for already existing resources, which will optionally be notified via _forwardExistingResources
|
|
this._watchers.push({
|
|
resources: watchedResources,
|
|
onAvailable,
|
|
onUpdated,
|
|
onDestroyed,
|
|
pendingEvents: [],
|
|
});
|
|
|
|
if (!ignoreExistingResources) {
|
|
await this._forwardExistingResources(watchedResources, 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(
|
|
"ResourceCommand.unwatchResources expects an onAvailable function as argument"
|
|
);
|
|
}
|
|
|
|
for (const type of resources) {
|
|
if (!this._isValidResourceType(type)) {
|
|
throw new Error(
|
|
`ResourceCommand.unwatchResources invoked with an unknown type: "${type}"`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Unregister the callbacks from the watchers registries.
|
|
// Check _watchers for the fully initialized watchers, as well as
|
|
// `_pendingWatchers` for new watchers still being created by `watchResources`
|
|
const allWatchers = [...this._watchers, ...this._pendingWatchers];
|
|
for (const watcherEntry of allWatchers) {
|
|
// 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;
|
|
});
|
|
|
|
// Stop listening to all resources for which we removed the last watcher
|
|
for (const resource of resources) {
|
|
const isResourceWatched = allWatchers.some(watcherEntry =>
|
|
watcherEntry.resources.includes(resource)
|
|
);
|
|
|
|
// Also check in _listenedResources as we may call unwatchResources
|
|
// for resources that we haven't started watching for.
|
|
if (!isResourceWatched && this._listenedResources.has(resource)) {
|
|
this._stopListening(resource);
|
|
}
|
|
}
|
|
|
|
// Stop watching for targets if we removed the last listener.
|
|
if (this._listenedResources.size == 0) {
|
|
this._unwatchAllTargets();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for a single resource of the provided resourceType.
|
|
*
|
|
* @param {String} resourceType
|
|
* One of ResourceCommand.TYPES, type of the expected resource.
|
|
* @param {Object} additional options
|
|
* - {Boolean} ignoreExistingResources: ignore existing resources or not.
|
|
* - {Function} predicate: if provided, will wait until a resource makes
|
|
* predicate(resource) return true.
|
|
* @return {Promise<Object>}
|
|
* Return a promise which resolves once we fully settle the resource listener.
|
|
* You should await for its resolution before doing the action which may fire
|
|
* your resource.
|
|
* This promise will expose an object with `onResource` attribute,
|
|
* itself being a promise, which will resolve once a matching resource is received.
|
|
*/
|
|
async waitForNextResource(
|
|
resourceType,
|
|
{ ignoreExistingResources = false, predicate } = {}
|
|
) {
|
|
// If no predicate was provided, convert to boolean to avoid resolving for
|
|
// empty `resources` arrays.
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* Check if there are any watchers for the specified resource.
|
|
*
|
|
* @param {String} resourceType
|
|
* One of ResourceCommand.TYPES
|
|
* @return {Boolean}
|
|
* If the resources type is beibg watched.
|
|
*/
|
|
isResourceWatched(resourceType) {
|
|
return this._listenedResources.has(resourceType);
|
|
}
|
|
|
|
/**
|
|
* 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 `TargetCommand.startListening`.
|
|
*/
|
|
async _watchAllTargets() {
|
|
if (!this._watchTargetsPromise) {
|
|
// If this is the very first listener registered, of all kind of resource types:
|
|
// * we want to start observing targets via TargetCommand
|
|
// * _onTargetAvailable will be called for each already existing targets and the next one to come
|
|
this._watchTargetsPromise = this.targetCommand.watchTargets({
|
|
types: this.targetCommand.ALL_TYPES,
|
|
onAvailable: this._onTargetAvailable,
|
|
onDestroyed: this._onTargetDestroyed,
|
|
});
|
|
}
|
|
return this._watchTargetsPromise;
|
|
}
|
|
|
|
_unwatchAllTargets() {
|
|
if (!this._watchTargetsPromise) {
|
|
return;
|
|
}
|
|
|
|
for (const offList of this._offTargetFrontListeners.values()) {
|
|
offList.forEach(off => off());
|
|
}
|
|
this._offTargetFrontListeners.clear();
|
|
|
|
this._watchTargetsPromise = null;
|
|
this.targetCommand.unwatchTargets({
|
|
types: this.targetCommand.ALL_TYPES,
|
|
onAvailable: this._onTargetAvailable,
|
|
onDestroyed: this._onTargetDestroyed,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* For a given resource type, start the legacy listeners for all already existing targets.
|
|
* Do that only if we have to. If this resourceType requires legacy listeners.
|
|
*/
|
|
async _startLegacyListenersForExistingTargets(resourceType) {
|
|
// If we were already listening to targets, we want to start the legacy listeners
|
|
// for all already existing targets.
|
|
//
|
|
// Only try instantiating the legacy listener, if this resource type:
|
|
// - has legacy listener implementation
|
|
// (new resource types may not be supported by old runtime and just not be received without breaking anything)
|
|
// - isn't supported by the server, or, the target type requires the a legacy listener implementation.
|
|
const shouldRunLegacyListeners =
|
|
resourceType in LegacyListeners &&
|
|
(!this.hasResourceCommandSupport(resourceType) ||
|
|
this._shouldRunLegacyListenerEvenWithWatcherSupport(resourceType));
|
|
if (shouldRunLegacyListeners) {
|
|
const promises = [];
|
|
const targets = this.targetCommand.getAllTargets(
|
|
this.targetCommand.ALL_TYPES
|
|
);
|
|
for (const targetFront of targets) {
|
|
// We disable warning in case we already registered the legacy listener for this target
|
|
// as this code may race with the call from onTargetAvailable if we end up having multiple
|
|
// calls to _startListening in parallel.
|
|
promises.push(
|
|
this._watchResourcesForTarget({
|
|
targetFront,
|
|
resourceType,
|
|
disableWarning: true,
|
|
})
|
|
);
|
|
}
|
|
await Promise.all(promises);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Method called by the TargetCommand for each already existing or target which has just been created.
|
|
*
|
|
* @param {Object} arg
|
|
* @param {Front} arg.targetFront
|
|
* The Front of the target that is available.
|
|
* This Front inherits from TargetMixin and is typically
|
|
* composed of a WindowGlobalTargetFront or ContentProcessTargetFront.
|
|
* @param {Boolean} arg.isTargetSwitching
|
|
* true when the new target was created because of a target switching.
|
|
*/
|
|
async _onTargetAvailable({ targetFront, isTargetSwitching }) {
|
|
const resources = [];
|
|
if (isTargetSwitching) {
|
|
// 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(ResourceCommand.TYPES)) {
|
|
// ...which has at least one listener...
|
|
if (!this._listenedResources.has(resourceType)) {
|
|
continue;
|
|
}
|
|
|
|
if (this._shouldRestartListenerOnTargetSwitching(resourceType)) {
|
|
this._stopListening(resourceType, {
|
|
bypassListenerCount: true,
|
|
});
|
|
resources.push(resourceType);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (targetFront.isDestroyed()) {
|
|
return;
|
|
}
|
|
|
|
// 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(ResourceCommand.TYPES)) {
|
|
// ...which has at least one listener...
|
|
if (!this._listenedResources.has(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 TargetCommand 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.
|
|
const offResourceAvailableArray = targetFront.on(
|
|
"resources-available-array",
|
|
this._onResourceAvailableArray.bind(this, { targetFront })
|
|
);
|
|
const offResourceUpdatedArray = targetFront.on(
|
|
"resources-updated-array",
|
|
this._onResourceUpdatedArray.bind(this, { targetFront })
|
|
);
|
|
const offResourceDestroyedArray = targetFront.on(
|
|
"resources-destroyed-array",
|
|
this._onResourceDestroyedArray.bind(this, { targetFront })
|
|
);
|
|
|
|
const offList = this._offTargetFrontListeners.get(targetFront) || [];
|
|
offList.push(
|
|
offResourceAvailableArray,
|
|
offResourceUpdatedArray,
|
|
offResourceDestroyedArray
|
|
);
|
|
|
|
if (isTargetSwitching) {
|
|
await Promise.all(
|
|
resources.map(resourceType =>
|
|
this._startListening(resourceType, {
|
|
bypassListenerCount: true,
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
// DOCUMENT_EVENT's will-navigate should replace target actor's will-navigate event,
|
|
// but only for targets provided by the watcher actor.
|
|
// Emit a fake DOCUMENT_EVENT's "will-navigate" out of target actor's will-navigate
|
|
// until watcher actor is supported by all descriptors (bug 1675763).
|
|
if (!this.targetCommand.hasTargetWatcherSupport()) {
|
|
const offWillNavigate = targetFront.on(
|
|
"will-navigate",
|
|
({ url, isFrameSwitching }) => {
|
|
targetFront.emit("resource-available-form", [
|
|
{
|
|
resourceType: this.TYPES.DOCUMENT_EVENT,
|
|
name: "will-navigate",
|
|
time: Date.now(), // will-navigate was not passing any timestamp
|
|
isFrameSwitching,
|
|
newURI: url,
|
|
},
|
|
]);
|
|
}
|
|
);
|
|
offList.push(offWillNavigate);
|
|
}
|
|
|
|
this._offTargetFrontListeners.set(targetFront, offList);
|
|
}
|
|
|
|
_shouldRestartListenerOnTargetSwitching(resourceType) {
|
|
// Note that we aren't using isServerTargetSwitchingEnabled, nor checking the
|
|
// server side target switching preference as we may have server side targets
|
|
// even when this is false/disabled.
|
|
// This will happen for bfcache navigations, even with server side targets disabled.
|
|
// `followWindowGlobalLifeCycle` will be false for the first top level target
|
|
// and only become true when doing a bfcache navigation.
|
|
// (only server side targets follow the WindowGlobal lifecycle)
|
|
// When server side targets are enabled, this will always be true.
|
|
const isServerSideTarget =
|
|
this.targetCommand.targetFront.targetForm.followWindowGlobalLifeCycle;
|
|
if (isServerSideTarget) {
|
|
// For top-level targets created from the server, only restart legacy
|
|
// listeners.
|
|
return !this.hasResourceCommandSupport(resourceType);
|
|
}
|
|
|
|
// For top-level targets created from the client we should always restart
|
|
// listeners.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Method called by the TargetCommand when a target has just been destroyed
|
|
* @param {Object} arg
|
|
* @param {Front} arg.targetFront
|
|
* The Front of the target that was destroyed
|
|
* @param {Boolean} arg.isModeSwitching
|
|
* true when this is called as the result of a change to the devtools.browsertoolbox.scope pref.
|
|
*/
|
|
_onTargetDestroyed({ targetFront, isModeSwitching }) {
|
|
// Clear the map of legacy listeners for this target.
|
|
this._existingLegacyListeners.set(targetFront, []);
|
|
this._offTargetFrontListeners.delete(targetFront);
|
|
|
|
// Purge the cache from any resource related to the destroyed target.
|
|
// Top level BrowsingContext target will be purge via DOCUMENT_EVENT will-navigate events.
|
|
// If we were to clean resources from target-destroyed, we will clear resources
|
|
// happening between will-navigate and target-destroyed. Typically the navigation request
|
|
// At the moment, isModeSwitching can only be true when targetFront.isTopLevel isn't true,
|
|
// so we don't need to add a specific check for isModeSwitching.
|
|
if (!targetFront.isTopLevel || !targetFront.isBrowsingContext) {
|
|
for (const [key, resource] of this._cache) {
|
|
if (resource.targetFront === targetFront) {
|
|
// NOTE: To anyone paranoid like me, yes it is okay to delete from a Map while iterating it.
|
|
this._cache.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Purge "available" pendingEvents for resources from the destroyed target when switching
|
|
// mode as we want to ignore those.
|
|
if (isModeSwitching) {
|
|
for (const watcherEntry of this._watchers) {
|
|
for (const pendingEvent of watcherEntry.pendingEvents) {
|
|
if (pendingEvent.callbackType == "available") {
|
|
pendingEvent.updates = pendingEvent.updates.filter(
|
|
update => update.targetFront !== targetFront
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async _onResourceAvailableArray({ targetFront, watcherFront }, array) {
|
|
let includesDocumentEventWillNavigate = false;
|
|
let includesDocumentEventDomLoading = false;
|
|
for (const [resourceType, resources] of array) {
|
|
const isAlreadyExistingResource =
|
|
this._processingExistingResources.has(resourceType);
|
|
const transformer = ResourceTransformers[resourceType];
|
|
|
|
for (let i = 0; i < resources.length; i++) {
|
|
let resource = resources[i];
|
|
if (!("resourceType" in resource)) {
|
|
resource.resourceType = resourceType;
|
|
}
|
|
|
|
if (watcherFront) {
|
|
targetFront = await this._getTargetForWatcherResource(resource);
|
|
// When we receive resources from the Watcher actor,
|
|
// there is no guarantee that the target front is fully initialized.
|
|
// The Target Front is initialized by the TargetCommand, by calling TargetFront.attachAndInitThread.
|
|
// We have to wait for its completion as resources watchers are expecting it to be completed.
|
|
//
|
|
// But when navigating, we may receive resources packets for a destroyed target.
|
|
// Or, in the context of the browser toolbox, they may not relate to any target.
|
|
if (targetFront) {
|
|
await targetFront.initialized;
|
|
}
|
|
}
|
|
|
|
// 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 (transformer) {
|
|
resource = transformer({
|
|
resource,
|
|
targetCommand: this.targetCommand,
|
|
targetFront,
|
|
watcherFront: this.watcherFront,
|
|
});
|
|
resources[i] = resource;
|
|
}
|
|
|
|
// isAlreadyExistingResource indicates that the resources already existed before
|
|
// the resource command started watching for this type of resource.
|
|
resource.isAlreadyExistingResource = isAlreadyExistingResource;
|
|
|
|
if (!resource.resourceId) {
|
|
resource.resourceId = `auto:${++gLastResourceId}`;
|
|
}
|
|
|
|
// Only consider top level document, and ignore remote iframes top document
|
|
let isWillNavigate = false;
|
|
if (resourceType == DOCUMENT_EVENT) {
|
|
isWillNavigate = resource.name === "will-navigate";
|
|
if (isWillNavigate && resource.targetFront.isTopLevel) {
|
|
includesDocumentEventWillNavigate = true;
|
|
this._onWillNavigate(resource.targetFront);
|
|
}
|
|
|
|
if (
|
|
resource.name === "dom-loading" &&
|
|
resource.targetFront.isTopLevel
|
|
) {
|
|
includesDocumentEventDomLoading = true;
|
|
}
|
|
}
|
|
|
|
// Avoid storing will-navigate resource and consider it as a transcient resource.
|
|
// We do that to prevent leaking this resource (and its target) on navigation.
|
|
// We do clear the cache in _onWillNavigate, that we call a few lines before this.
|
|
if (!isWillNavigate) {
|
|
this.addResourceToCache(resource);
|
|
}
|
|
}
|
|
|
|
this._queueResourceEvent("available", resourceType, resources);
|
|
}
|
|
|
|
// If we receive the DOCUMENT_EVENT for:
|
|
// - will-navigate
|
|
// - dom-loading + we're using the service worker legacy listener
|
|
// then flush immediately the resources to notify about the navigation sooner than later.
|
|
// (this is especially useful for tests, even if they should probably avoid depending on this...)
|
|
if (
|
|
includesDocumentEventWillNavigate ||
|
|
(includesDocumentEventDomLoading &&
|
|
!this.targetCommand.hasTargetWatcherSupport("service_worker")) ||
|
|
this.throttlingDisabled
|
|
) {
|
|
this._notifyWatchers();
|
|
} else {
|
|
this._throttledNotifyWatchers();
|
|
}
|
|
}
|
|
|
|
async _onResourceUpdatedArray(context, array) {
|
|
for (const [resourceType, resources] of array) {
|
|
for (const resource of resources) {
|
|
if (!("resourceType" in resource)) {
|
|
resource.resourceType = resourceType;
|
|
}
|
|
}
|
|
await this._onResourceUpdated(context, resources);
|
|
}
|
|
}
|
|
|
|
async _onResourceDestroyedArray(context, array) {
|
|
const resources = [];
|
|
for (const [resourceType, resourceIds] of array) {
|
|
for (const resourceId of resourceIds) {
|
|
resources.push({ resourceType, resourceId });
|
|
}
|
|
}
|
|
await this._onResourceDestroyed(context, resources);
|
|
}
|
|
|
|
/**
|
|
* Called every time a resource is updated in the remote target.
|
|
*
|
|
* Method called either by:
|
|
* - the backward compatibility code (LegacyListeners)
|
|
* - target actors RDP events
|
|
*
|
|
* @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<Object>} 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`);
|
|
}
|
|
|
|
// See _onResourceAvailableArray()
|
|
// We also need to wait for the related targetFront to be initialized
|
|
// otherwise we would notify about the update *before* it's available
|
|
// and the resource won't be in _cache.
|
|
if (watcherFront) {
|
|
targetFront = await this._getTargetForWatcherResource(update);
|
|
// When we receive the navigation request, the target front has already been
|
|
// destroyed, but this is fine. The cached resource has the reference to
|
|
// the (destroyed) target front and it is fully initialized.
|
|
if (targetFront) {
|
|
await targetFront.initialized;
|
|
}
|
|
}
|
|
|
|
const existingResource = this._cache.get(
|
|
cacheKey(resourceType, 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 every time a resource is destroyed in the remote target.
|
|
*
|
|
* @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<json/Front>} resources
|
|
* Depending on the resource Type, it can be an Array composed of
|
|
* either JSON objects or Fronts, which describes the resource.
|
|
*/
|
|
async _onResourceDestroyed({ targetFront }, resources) {
|
|
for (const resource of resources) {
|
|
const { resourceType, resourceId } = resource;
|
|
this._cache.delete(cacheKey(resourceType, resourceId));
|
|
if (!resource.targetFront) {
|
|
resource.targetFront = targetFront;
|
|
}
|
|
this._queueResourceEvent("destroyed", resourceType, [resource]);
|
|
}
|
|
this._throttledNotifyWatchers();
|
|
}
|
|
|
|
_queueResourceEvent(callbackType, resourceType, updates) {
|
|
for (const { resources, pendingEvents } of this._watchers) {
|
|
// This watcher doesn't listen to this type of resource
|
|
if (!resources.includes(resourceType)) {
|
|
continue;
|
|
}
|
|
// Avoid trying to coalesce with last pending event as mutating `updates` may have side effects
|
|
// with other watchers as this array is shared between all the watchers.
|
|
pendingEvents.push({
|
|
callbackType,
|
|
updates,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, { areExistingResources: false });
|
|
} else if (callbackType == "updated" && onUpdated) {
|
|
onUpdated(updates);
|
|
} else if (callbackType == "destroyed" && onDestroyed) {
|
|
onDestroyed(updates);
|
|
}
|
|
} catch (e) {
|
|
console.error(
|
|
"Exception while calling a ResourceCommand",
|
|
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, innerWindowId, resourceType } = resource;
|
|
|
|
// Some privileged resources aren't related to any BrowsingContext
|
|
// and so aren't bound to any Target Front.
|
|
// Server watchers should pass an explicit "-1" value in order to prevent
|
|
// silently ignoring an undefined browsingContextID attribute.
|
|
if (browsingContextID == -1) {
|
|
return this.targetCommand.targetFront;
|
|
}
|
|
|
|
if (innerWindowId && this.targetCommand.isServerTargetSwitchingEnabled()) {
|
|
return this.watcherFront.getWindowGlobalTargetByInnerWindowId(
|
|
innerWindowId
|
|
);
|
|
} else if (browsingContextID) {
|
|
return this.watcherFront.getWindowGlobalTarget(browsingContextID);
|
|
}
|
|
console.error(
|
|
`Resource of ${resourceType} is missing a browsingContextID or innerWindowId attribute`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
_onWillNavigate() {
|
|
// Special case for toolboxes debugging a document,
|
|
// purge the cache entirely when we start navigating to a new document.
|
|
// Other toolboxes and additional target for remote iframes or content process
|
|
// will be purge from onTargetDestroyed.
|
|
|
|
// NOTE: we could `clear` the cache here, but technically if anything is
|
|
// currently iterating over resources provided by getAllResources, that
|
|
// would interfere with their iteration. We just assign a new Map here to
|
|
// leave those iterators as is.
|
|
this._cache = new Map();
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
hasResourceCommandSupport(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.
|
|
*/
|
|
_hasResourceCommandSupportForTarget(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.targetCommand.hasTargetWatcherSupport(targetFront.targetType)) {
|
|
return false;
|
|
}
|
|
|
|
return this.hasResourceCommandSupport(resourceType);
|
|
}
|
|
|
|
_isValidResourceType(type) {
|
|
return this.ALL_TYPES.includes(type);
|
|
}
|
|
|
|
/**
|
|
* 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 ResourceCommand.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) {
|
|
if (this._listenedResources.has(resourceType)) {
|
|
return;
|
|
}
|
|
this._listenedResources.add(resourceType);
|
|
}
|
|
|
|
this._processingExistingResources.add(resourceType);
|
|
|
|
// Ensuring enabling listening to targets.
|
|
// This will be a no-op expect for the very first call to `_startListening`,
|
|
// where it is going to call `onTargetAvailable` for all already existing targets,
|
|
// as well as for those who will be created later.
|
|
//
|
|
// Do this *before* calling WatcherActor.watchResources in order to register "resource-available"
|
|
// listeners on targets before these events start being emitted.
|
|
await this._watchAllTargets(resourceType);
|
|
|
|
// When we are calling _startListening for the first time, _watchAllTargets
|
|
// will register legacylistener when it will call onTargetAvailable for all existing targets.
|
|
// But for any next calls to _startListening, _watchAllTargets will be a no-op,
|
|
// and nothing will start legacy listener for each already registered targets.
|
|
await this._startLegacyListenersForExistingTargets(resourceType);
|
|
|
|
// If the server supports the Watcher API and the Watcher supports
|
|
// this resource type, use this API
|
|
if (this.hasResourceCommandSupport(resourceType)) {
|
|
await this.watcherFront.watchResources([resourceType]);
|
|
}
|
|
this._processingExistingResources.delete(resourceType);
|
|
}
|
|
|
|
/**
|
|
* Return true if the resource should be watched via legacy listener,
|
|
* even when watcher supports this resource type.
|
|
*
|
|
* 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 never trigger
|
|
* the legacy listeners codepath for these resource types.
|
|
*
|
|
* If this isn't fixed soon, we may add other resources we want to see
|
|
* being fetched from these targets.
|
|
*/
|
|
_shouldRunLegacyListenerEvenWithWatcherSupport(resourceType) {
|
|
return WORKER_RESOURCE_TYPES.includes(resourceType);
|
|
}
|
|
|
|
async _forwardExistingResources(resourceTypes, onAvailable) {
|
|
const existingResources = [];
|
|
for (const resource of this._cache.values()) {
|
|
if (resourceTypes.includes(resource.resourceType)) {
|
|
existingResources.push(resource);
|
|
}
|
|
}
|
|
if (existingResources.length) {
|
|
await onAvailable(existingResources, { areExistingResources: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call backward compatibility code from `LegacyListeners` in order to listen for a given
|
|
* type of resource from a given target.
|
|
*/
|
|
async _watchResourcesForTarget({
|
|
targetFront,
|
|
resourceType,
|
|
disableWarning = false,
|
|
}) {
|
|
if (this._hasResourceCommandSupportForTarget(resourceType, targetFront)) {
|
|
// This resource / target pair should already be handled by the watcher,
|
|
// no need to start legacy listeners.
|
|
return;
|
|
}
|
|
|
|
// All workers target types are still not supported by the watcher
|
|
// so that we have to spawn legacy listener for all their resources.
|
|
// But some resources are irrelevant to workers, like network events.
|
|
// And we removed the related legacy listener as they are no longer used.
|
|
if (
|
|
targetFront.targetType.endsWith("worker") &&
|
|
!WORKER_RESOURCE_TYPES.includes(resourceType)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (targetFront.isDestroyed()) {
|
|
return;
|
|
}
|
|
|
|
const onAvailableArray = this._onResourceAvailableArray.bind(this, {
|
|
targetFront,
|
|
});
|
|
const onUpdatedArray = this._onResourceUpdatedArray.bind(this, {
|
|
targetFront,
|
|
});
|
|
const onDestroyedArray = this._onResourceDestroyedArray.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)) {
|
|
if (!disableWarning) {
|
|
console.warn(
|
|
`Already started legacy listener for ${resourceType} on ${targetFront.actorID}`
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
this._existingLegacyListeners.set(
|
|
targetFront,
|
|
legacyListeners.concat(resourceType)
|
|
);
|
|
|
|
try {
|
|
await LegacyListeners[resourceType]({
|
|
targetCommand: this.targetCommand,
|
|
targetFront,
|
|
onAvailableArray,
|
|
onDestroyedArray,
|
|
onUpdatedArray,
|
|
});
|
|
} catch (e) {
|
|
// Swallow the error to avoid breaking calls to watchResources which will
|
|
// loop on all existing targets to create legacy listeners.
|
|
// If a legacy listener fails to handle a target for some reason, we
|
|
// should still try to process other targets as much as possible.
|
|
// See Bug 1687645.
|
|
console.error(
|
|
`Failed to start [${resourceType}] legacy listener for target ${targetFront.actorID}`,
|
|
e
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
if (!this._listenedResources.has(resourceType)) {
|
|
throw new Error(
|
|
`Stopped listening for resource '${resourceType}' that isn't being listened to`
|
|
);
|
|
}
|
|
this._listenedResources.delete(resourceType);
|
|
}
|
|
|
|
// Clear the cached resources of the type.
|
|
for (const [key, resource] of this._cache) {
|
|
if (resource.resourceType == resourceType) {
|
|
// NOTE: To anyone paranoid like me, yes it is okay to delete from a Map while iterating it.
|
|
this._cache.delete(key);
|
|
}
|
|
}
|
|
|
|
// If the server supports the Watcher API and the Watcher supports
|
|
// this resource type, use this API
|
|
if (this.hasResourceCommandSupport(resourceType)) {
|
|
if (!this.watcherFront.isDestroyed()) {
|
|
this.watcherFront.unwatchResources([resourceType]);
|
|
}
|
|
|
|
const shouldRunLegacyListeners =
|
|
this._shouldRunLegacyListenerEvenWithWatcherSupport(resourceType);
|
|
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.targetCommand.getAllTargets(
|
|
this.targetCommand.ALL_TYPES
|
|
);
|
|
for (const target of targets) {
|
|
this._unwatchResourcesForTarget(target, resourceType);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Backward compatibility code, reverse of _watchResourcesForTarget.
|
|
*/
|
|
_unwatchResourcesForTarget(targetFront, resourceType) {
|
|
if (this._hasResourceCommandSupportForTarget(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);
|
|
}
|
|
}
|
|
}
|
|
|
|
const DOCUMENT_EVENT = "document-event";
|
|
ResourceCommand.TYPES = ResourceCommand.prototype.TYPES = {
|
|
CONSOLE_MESSAGE: "console-message",
|
|
CSS_CHANGE: "css-change",
|
|
CSS_MESSAGE: "css-message",
|
|
CSS_REGISTERED_PROPERTIES: "css-registered-properties",
|
|
ERROR_MESSAGE: "error-message",
|
|
PLATFORM_MESSAGE: "platform-message",
|
|
DOCUMENT_EVENT,
|
|
ROOT_NODE: "root-node",
|
|
STYLESHEET: "stylesheet",
|
|
NETWORK_EVENT: "network-event",
|
|
WEBSOCKET: "websocket",
|
|
COOKIE: "cookies",
|
|
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",
|
|
REFLOW: "reflow",
|
|
SOURCE: "source",
|
|
THREAD_STATE: "thread-state",
|
|
JSTRACER_TRACE: "jstracer-trace",
|
|
JSTRACER_STATE: "jstracer-state",
|
|
SERVER_SENT_EVENT: "server-sent-event",
|
|
LAST_PRIVATE_CONTEXT_EXIT: "last-private-context-exit",
|
|
};
|
|
ResourceCommand.ALL_TYPES = ResourceCommand.prototype.ALL_TYPES = Object.values(
|
|
ResourceCommand.TYPES
|
|
);
|
|
module.exports = ResourceCommand;
|
|
|
|
// This is the list of resource types supported by workers.
|
|
// We need such list to know when forcing to run the legacy listeners
|
|
// and when to avoid try to spawn some unsupported ones for workers.
|
|
const WORKER_RESOURCE_TYPES = [
|
|
ResourceCommand.TYPES.CONSOLE_MESSAGE,
|
|
ResourceCommand.TYPES.ERROR_MESSAGE,
|
|
ResourceCommand.TYPES.SOURCE,
|
|
ResourceCommand.TYPES.THREAD_STATE,
|
|
];
|
|
|
|
// List of resource types which aren't stored in the internal ResourceCommand cache.
|
|
// Only the first `watchResources()` call for a given resource type may receive already existing
|
|
// resources. All subsequent call to `watchResources()` for the same resource type will
|
|
// only receive future resource, and not the one already notified in the past.
|
|
// This is typically used for resources with very high throughput.
|
|
const TRANSIENT_RESOURCE_TYPES = [
|
|
ResourceCommand.TYPES.JSTRACER_TRACE,
|
|
ResourceCommand.TYPES.JSTRACER_STATE,
|
|
];
|
|
|
|
// 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 = {
|
|
async [ResourceCommand.TYPES.DOCUMENT_EVENT]({ 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 = ResourceCommand.TYPES.DOCUMENT_EVENT;
|
|
onAvailable([event]);
|
|
});
|
|
await webConsoleFront.startListeners(["DocumentEvents"]);
|
|
},
|
|
};
|
|
loader.lazyRequireGetter(
|
|
LegacyListeners,
|
|
ResourceCommand.TYPES.CONSOLE_MESSAGE,
|
|
"resource://devtools/shared/commands/resource/legacy-listeners/console-messages.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
LegacyListeners,
|
|
ResourceCommand.TYPES.CSS_CHANGE,
|
|
"resource://devtools/shared/commands/resource/legacy-listeners/css-changes.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
LegacyListeners,
|
|
ResourceCommand.TYPES.CSS_MESSAGE,
|
|
"resource://devtools/shared/commands/resource/legacy-listeners/css-messages.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
LegacyListeners,
|
|
ResourceCommand.TYPES.ERROR_MESSAGE,
|
|
"resource://devtools/shared/commands/resource/legacy-listeners/error-messages.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
LegacyListeners,
|
|
ResourceCommand.TYPES.PLATFORM_MESSAGE,
|
|
"resource://devtools/shared/commands/resource/legacy-listeners/platform-messages.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
LegacyListeners,
|
|
ResourceCommand.TYPES.ROOT_NODE,
|
|
"resource://devtools/shared/commands/resource/legacy-listeners/root-node.js"
|
|
);
|
|
|
|
loader.lazyRequireGetter(
|
|
LegacyListeners,
|
|
ResourceCommand.TYPES.SOURCE,
|
|
"resource://devtools/shared/commands/resource/legacy-listeners/source.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
LegacyListeners,
|
|
ResourceCommand.TYPES.THREAD_STATE,
|
|
"resource://devtools/shared/commands/resource/legacy-listeners/thread-states.js"
|
|
);
|
|
|
|
loader.lazyRequireGetter(
|
|
LegacyListeners,
|
|
ResourceCommand.TYPES.REFLOW,
|
|
"resource://devtools/shared/commands/resource/legacy-listeners/reflow.js"
|
|
);
|
|
|
|
// 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 = {};
|
|
|
|
loader.lazyRequireGetter(
|
|
ResourceTransformers,
|
|
ResourceCommand.TYPES.CONSOLE_MESSAGE,
|
|
"resource://devtools/shared/commands/resource/transformers/console-messages.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
ResourceTransformers,
|
|
ResourceCommand.TYPES.ERROR_MESSAGE,
|
|
"resource://devtools/shared/commands/resource/transformers/error-messages.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
ResourceTransformers,
|
|
ResourceCommand.TYPES.CACHE_STORAGE,
|
|
"resource://devtools/shared/commands/resource/transformers/storage-cache.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
ResourceTransformers,
|
|
ResourceCommand.TYPES.COOKIE,
|
|
"resource://devtools/shared/commands/resource/transformers/storage-cookie.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
ResourceTransformers,
|
|
ResourceCommand.TYPES.EXTENSION_STORAGE,
|
|
"resource://devtools/shared/commands/resource/transformers/storage-extension.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
ResourceTransformers,
|
|
ResourceCommand.TYPES.INDEXED_DB,
|
|
"resource://devtools/shared/commands/resource/transformers/storage-indexed-db.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
ResourceTransformers,
|
|
ResourceCommand.TYPES.LOCAL_STORAGE,
|
|
"resource://devtools/shared/commands/resource/transformers/storage-local-storage.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
ResourceTransformers,
|
|
ResourceCommand.TYPES.SESSION_STORAGE,
|
|
"resource://devtools/shared/commands/resource/transformers/storage-session-storage.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
ResourceTransformers,
|
|
ResourceCommand.TYPES.NETWORK_EVENT,
|
|
"resource://devtools/shared/commands/resource/transformers/network-events.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
ResourceTransformers,
|
|
ResourceCommand.TYPES.THREAD_STATE,
|
|
"resource://devtools/shared/commands/resource/transformers/thread-states.js"
|
|
);
|