Source code

Revision control

Copy as Markdown

Other Tools

/* 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`):
// 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;