1415 lines
53 KiB
JavaScript
1415 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"
|
|
);
|