488 lines
14 KiB
JavaScript
488 lines
14 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 { objectSpec } = require("resource://devtools/shared/specs/object.js");
|
|
const {
|
|
FrontClassWithSpec,
|
|
registerFront,
|
|
} = require("resource://devtools/shared/protocol.js");
|
|
const {
|
|
LongStringFront,
|
|
} = require("resource://devtools/client/fronts/string.js");
|
|
|
|
const SUPPORT_ENUM_ENTRIES_SET = new Set([
|
|
"CustomStateSet",
|
|
"FormData",
|
|
"Headers",
|
|
"HighlightRegistry",
|
|
"Map",
|
|
"MIDIInputMap",
|
|
"MIDIOutputMap",
|
|
"Set",
|
|
"Storage",
|
|
"URLSearchParams",
|
|
"WeakMap",
|
|
"WeakSet",
|
|
]);
|
|
|
|
/**
|
|
* A ObjectFront is used as a front end for the ObjectActor that is
|
|
* created on the server, hiding implementation details.
|
|
*/
|
|
class ObjectFront extends FrontClassWithSpec(objectSpec) {
|
|
constructor(conn = null, targetFront = null, parentFront = null, data) {
|
|
if (!parentFront) {
|
|
throw new Error("ObjectFront require a parent front");
|
|
}
|
|
|
|
super(conn, targetFront, parentFront);
|
|
|
|
this._grip = data;
|
|
this.actorID = this._grip.actor;
|
|
this.valid = true;
|
|
|
|
parentFront.manage(this);
|
|
}
|
|
|
|
form(data) {
|
|
this.actorID = data.actor;
|
|
this._grip = data;
|
|
}
|
|
|
|
skipDestroy() {
|
|
// Object fronts are simple fronts, they don't need to be cleaned up on
|
|
// toolbox destroy. `conn` is a DebuggerClient instance, check the
|
|
// `isToolboxDestroy` flag to skip the destroy.
|
|
return this.conn && this.conn.isToolboxDestroy;
|
|
}
|
|
|
|
getGrip() {
|
|
return this._grip;
|
|
}
|
|
|
|
get isFrozen() {
|
|
return this._grip.frozen;
|
|
}
|
|
|
|
get isSealed() {
|
|
return this._grip.sealed;
|
|
}
|
|
|
|
get isExtensible() {
|
|
return this._grip.extensible;
|
|
}
|
|
|
|
/**
|
|
* Request the prototype and own properties of the object.
|
|
*/
|
|
async getPrototypeAndProperties() {
|
|
const result = await super.prototypeAndProperties();
|
|
|
|
if (result.prototype) {
|
|
result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this);
|
|
}
|
|
|
|
// The result packet can have multiple properties that hold grips which we may need
|
|
// to turn into fronts.
|
|
const gripKeys = ["value", "getterValue", "get", "set"];
|
|
|
|
if (result.ownProperties) {
|
|
Object.entries(result.ownProperties).forEach(([key, descriptor]) => {
|
|
if (descriptor) {
|
|
for (const gripKey of gripKeys) {
|
|
if (descriptor.hasOwnProperty(gripKey)) {
|
|
result.ownProperties[key][gripKey] = getAdHocFrontOrPrimitiveGrip(
|
|
descriptor[gripKey],
|
|
this
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (result.safeGetterValues) {
|
|
Object.entries(result.safeGetterValues).forEach(([key, descriptor]) => {
|
|
if (descriptor) {
|
|
for (const gripKey of gripKeys) {
|
|
if (descriptor.hasOwnProperty(gripKey)) {
|
|
result.safeGetterValues[key][gripKey] =
|
|
getAdHocFrontOrPrimitiveGrip(descriptor[gripKey], this);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (result.ownSymbols) {
|
|
result.ownSymbols.forEach((descriptor, i, arr) => {
|
|
if (descriptor) {
|
|
for (const gripKey of gripKeys) {
|
|
if (descriptor.hasOwnProperty(gripKey)) {
|
|
arr[i][gripKey] = getAdHocFrontOrPrimitiveGrip(
|
|
descriptor[gripKey],
|
|
this
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Request a PropertyIteratorFront instance to ease listing
|
|
* properties for this object.
|
|
*
|
|
* @param options Object
|
|
* A dictionary object with various boolean attributes:
|
|
* - ignoreIndexedProperties Boolean
|
|
* If true, filters out Array items.
|
|
* e.g. properties names between `0` and `object.length`.
|
|
* - ignoreNonIndexedProperties Boolean
|
|
* If true, filters out items that aren't array items
|
|
* e.g. properties names that are not a number between `0`
|
|
* and `object.length`.
|
|
* - sort Boolean
|
|
* If true, the iterator will sort the properties by name
|
|
* before dispatching them.
|
|
*/
|
|
enumProperties(options) {
|
|
return super.enumProperties(options);
|
|
}
|
|
|
|
/**
|
|
* Request a PropertyIteratorFront instance to enumerate entries in a
|
|
* Map/Set-like object.
|
|
*/
|
|
enumEntries() {
|
|
if (!SUPPORT_ENUM_ENTRIES_SET.has(this._grip.class)) {
|
|
console.error(
|
|
`enumEntries can't be called for "${
|
|
this._grip.class
|
|
}" grips. Supported grips are: ${[...SUPPORT_ENUM_ENTRIES_SET].join(
|
|
", "
|
|
)}.`
|
|
);
|
|
return null;
|
|
}
|
|
return super.enumEntries();
|
|
}
|
|
|
|
/**
|
|
* Request a SymbolIteratorFront instance to enumerate symbols in an object.
|
|
*/
|
|
enumSymbols() {
|
|
if (this._grip.type !== "object") {
|
|
console.error("enumSymbols is only valid for objects grips.");
|
|
return null;
|
|
}
|
|
return super.enumSymbols();
|
|
}
|
|
|
|
/**
|
|
* Request the property descriptor of the object's specified property.
|
|
*
|
|
* @param name string The name of the requested property.
|
|
*/
|
|
getProperty(name) {
|
|
return super.property(name);
|
|
}
|
|
|
|
/**
|
|
* Request the value of the object's specified property.
|
|
*
|
|
* @param name string The name of the requested property.
|
|
* @param receiverId string|null The actorId of the receiver to be used for getters.
|
|
*/
|
|
async getPropertyValue(name, receiverId) {
|
|
const response = await super.propertyValue(name, receiverId);
|
|
|
|
if (response.value) {
|
|
const { value } = response;
|
|
if (value.return) {
|
|
response.value.return = getAdHocFrontOrPrimitiveGrip(
|
|
value.return,
|
|
this
|
|
);
|
|
}
|
|
|
|
if (value.throw) {
|
|
response.value.throw = getAdHocFrontOrPrimitiveGrip(value.throw, this);
|
|
}
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Get the body of a custom formatted object.
|
|
*/
|
|
async customFormatterBody() {
|
|
const result = await super.customFormatterBody();
|
|
|
|
if (!result?.customFormatterBody) {
|
|
return result;
|
|
}
|
|
|
|
const createFrontsInJsonMl = item => {
|
|
if (Array.isArray(item)) {
|
|
return item.map(i => createFrontsInJsonMl(i));
|
|
}
|
|
return getAdHocFrontOrPrimitiveGrip(item, this);
|
|
};
|
|
|
|
result.customFormatterBody = createFrontsInJsonMl(
|
|
result.customFormatterBody
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Request the prototype of the object.
|
|
*/
|
|
async getPrototype() {
|
|
const result = await super.prototype();
|
|
|
|
if (!result.prototype) {
|
|
return result;
|
|
}
|
|
|
|
result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Request the state of a promise.
|
|
*/
|
|
async getPromiseState() {
|
|
if (this._grip.class !== "Promise") {
|
|
console.error("getPromiseState is only valid for promise grips.");
|
|
return null;
|
|
}
|
|
|
|
let response, promiseState;
|
|
try {
|
|
response = await super.promiseState();
|
|
promiseState = response.promiseState;
|
|
} catch (error) {
|
|
// @backward-compat { version 85 } On older server, the promiseState request didn't
|
|
// didn't exist (bug 1552648). The promise state was directly included in the grip.
|
|
if (error.message.includes("unrecognizedPacketType")) {
|
|
promiseState = this._grip.promiseState;
|
|
response = { promiseState };
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const { value, reason } = promiseState;
|
|
|
|
if (value) {
|
|
promiseState.value = getAdHocFrontOrPrimitiveGrip(value, this);
|
|
}
|
|
|
|
if (reason) {
|
|
promiseState.reason = getAdHocFrontOrPrimitiveGrip(reason, this);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Request the target and handler internal slots of a proxy.
|
|
*/
|
|
async getProxySlots() {
|
|
if (this._grip.class !== "Proxy") {
|
|
console.error("getProxySlots is only valid for proxy grips.");
|
|
return null;
|
|
}
|
|
|
|
const response = await super.proxySlots();
|
|
const { proxyHandler, proxyTarget } = response;
|
|
|
|
if (proxyHandler) {
|
|
response.proxyHandler = getAdHocFrontOrPrimitiveGrip(proxyHandler, this);
|
|
}
|
|
|
|
if (proxyTarget) {
|
|
response.proxyTarget = getAdHocFrontOrPrimitiveGrip(proxyTarget, this);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
get isSyntaxError() {
|
|
return this._grip.preview && this._grip.preview.name == "SyntaxError";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When we are asking the server for the value of a given variable, we might get different
|
|
* type of objects:
|
|
* - a primitive (string, number, null, false, boolean)
|
|
* - a long string
|
|
* - an "object" (i.e. not primitive nor long string)
|
|
*
|
|
* Each of those type need a different front, or none:
|
|
* - a primitive does not allow further interaction with the server, so we don't need
|
|
* to have a dedicated front.
|
|
* - a long string needs a longStringFront to be able to retrieve the full string.
|
|
* - an object need an objectFront to retrieve properties, symbols and prototype.
|
|
*
|
|
* In the case an ObjectFront is created, we also check if the object has properties
|
|
* that should be turned into fronts as well.
|
|
*
|
|
* @param {String|Number|Object} options: The packet returned by the server.
|
|
* @param {Front} parentFront
|
|
*
|
|
* @returns {Number|String|Object|LongStringFront|ObjectFront}
|
|
*/
|
|
function getAdHocFrontOrPrimitiveGrip(packet, parentFront) {
|
|
// We only want to try to create a front when it makes sense, i.e when it has an
|
|
// actorID, unless:
|
|
// - it's a Symbol (See Bug 1600299)
|
|
// - it's a mapEntry (the preview.key and preview.value properties can hold actors)
|
|
// - it's a highlightRegistryEntry (the preview.value properties can hold actors)
|
|
// - or it is already a front (happens when we are using the legacy listeners in the ResourceCommand)
|
|
const isPacketAnObject = packet && typeof packet === "object";
|
|
const isFront = !!packet?.typeName;
|
|
if (
|
|
!isPacketAnObject ||
|
|
packet.type == "symbol" ||
|
|
(packet.type !== "mapEntry" &&
|
|
packet.type !== "highlightRegistryEntry" &&
|
|
!packet.actor) ||
|
|
isFront
|
|
) {
|
|
return packet;
|
|
}
|
|
|
|
const { conn } = parentFront;
|
|
// If the parent front is a target, consider it as the target to use for all objects
|
|
const targetFront = parentFront.isTargetFront
|
|
? parentFront
|
|
: parentFront.targetFront;
|
|
|
|
// We may have already created a front for this object actor since some actor (e.g. the
|
|
// thread actor) cache the object actors they create.
|
|
const existingFront = conn.getFrontByID(packet.actor);
|
|
if (existingFront) {
|
|
// This methods replicates Protocol.js logic when we receive an actor "form" (here `packet`):
|
|
// https://searchfox.org/mozilla-central/rev/aecbd5cdd28a09e11872bc829d9e6e4b943e6e49/devtools/shared/protocol/types.js#346
|
|
// We notify the Object Front about the new "form" so that it can update itself
|
|
// with latest data provided by the server.
|
|
// This will help ensure that the object previews get updated.
|
|
existingFront.form(packet);
|
|
|
|
// The `packet` may contain nested actor forms which should be converted into Fronts.
|
|
createChildFronts(existingFront, packet);
|
|
|
|
return existingFront;
|
|
}
|
|
|
|
const { type } = packet;
|
|
|
|
if (type === "longString") {
|
|
const longStringFront = new LongStringFront(conn, targetFront, parentFront);
|
|
longStringFront.form(packet);
|
|
parentFront.manage(longStringFront);
|
|
return longStringFront;
|
|
}
|
|
|
|
if (
|
|
(type === "mapEntry" || type === "highlightRegistryEntry") &&
|
|
packet.preview
|
|
) {
|
|
const { key, value } = packet.preview;
|
|
packet.preview.key = getAdHocFrontOrPrimitiveGrip(
|
|
key,
|
|
parentFront,
|
|
targetFront
|
|
);
|
|
packet.preview.value = getAdHocFrontOrPrimitiveGrip(
|
|
value,
|
|
parentFront,
|
|
targetFront
|
|
);
|
|
return packet;
|
|
}
|
|
|
|
const objectFront = new ObjectFront(conn, targetFront, parentFront, packet);
|
|
createChildFronts(objectFront, packet);
|
|
return objectFront;
|
|
}
|
|
|
|
/**
|
|
* Create child fronts of the passed object front given a packet. Those child fronts are
|
|
* usually mapping actors of the packet sub-properties (preview items, promise fullfilled
|
|
* values, …).
|
|
*
|
|
* @param {ObjectFront} objectFront
|
|
* @param {String|Number|Object} packet: The packet returned by the server
|
|
*/
|
|
function createChildFronts(objectFront, packet) {
|
|
if (packet.preview) {
|
|
const { message, entries } = packet.preview;
|
|
|
|
// The message could be a longString.
|
|
if (packet.preview.message) {
|
|
packet.preview.message = getAdHocFrontOrPrimitiveGrip(
|
|
message,
|
|
objectFront
|
|
);
|
|
}
|
|
|
|
// Handle Map/WeakMap preview entries (the preview might be directly used if has all the
|
|
// items needed, i.e. if the Map has less than 10 items).
|
|
if (entries && Array.isArray(entries)) {
|
|
packet.preview.entries = entries.map(([key, value]) => [
|
|
getAdHocFrontOrPrimitiveGrip(key, objectFront),
|
|
getAdHocFrontOrPrimitiveGrip(value, objectFront),
|
|
]);
|
|
}
|
|
}
|
|
|
|
if (packet && typeof packet.ownProperties === "object") {
|
|
for (const [name, descriptor] of Object.entries(packet.ownProperties)) {
|
|
// The descriptor can have multiple properties that hold grips which we may need
|
|
// to turn into fronts.
|
|
const gripKeys = ["value", "getterValue", "get", "set"];
|
|
for (const key of gripKeys) {
|
|
if (
|
|
descriptor &&
|
|
typeof descriptor === "object" &&
|
|
descriptor.hasOwnProperty(key)
|
|
) {
|
|
packet.ownProperties[name][key] = getAdHocFrontOrPrimitiveGrip(
|
|
descriptor[key],
|
|
objectFront
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle custom formatters
|
|
if (packet && packet.useCustomFormatter && Array.isArray(packet.header)) {
|
|
const createFrontsInJsonMl = item => {
|
|
if (Array.isArray(item)) {
|
|
return item.map(i => createFrontsInJsonMl(i));
|
|
}
|
|
return getAdHocFrontOrPrimitiveGrip(item, objectFront);
|
|
};
|
|
|
|
packet.header = createFrontsInJsonMl(packet.header);
|
|
}
|
|
}
|
|
|
|
registerFront(ObjectFront);
|
|
|
|
exports.ObjectFront = ObjectFront;
|
|
exports.getAdHocFrontOrPrimitiveGrip = getAdHocFrontOrPrimitiveGrip;
|