chrome-devtools-frontend
Version:
Chrome DevTools UI
1,165 lines (991 loc) • 38.6 kB
text/typescript
// Copyright 2009 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js';
import * as Protocol from '../../generated/protocol.js';
import type {DOMPinnedWebIDLProp, DOMPinnedWebIDLType} from '../common/JavaScriptMetaData.js';
import type {DebuggerModel, FunctionDetails} from './DebuggerModel.js';
import type {RuntimeModel} from './RuntimeModel.js';
/** This cannot be an interface due to "instanceof RemoteObject" checks in the code. **/
export abstract class RemoteObject {
static fromLocalObject(value: unknown): RemoteObject {
return new LocalJSONObject(value);
}
static type(remoteObject: RemoteObject): string {
if (remoteObject === null) {
return 'null';
}
const type = typeof remoteObject;
if (type !== 'object' && type !== 'function') {
return type;
}
return remoteObject.type;
}
static isNullOrUndefined(remoteObject?: RemoteObject): boolean {
if (remoteObject === undefined) {
return true;
}
switch (remoteObject.type) {
case Protocol.Runtime.RemoteObjectType.Object:
return remoteObject.subtype === Protocol.Runtime.RemoteObjectSubtype.Null;
case Protocol.Runtime.RemoteObjectType.Undefined:
return true;
default:
return false;
}
}
static arrayNameFromDescription(description: string): string {
return description.replace(descriptionLengthParenRegex, '').replace(descriptionLengthSquareRegex, '');
}
static arrayLength(object: RemoteObject|Protocol.Runtime.RemoteObject|Protocol.Runtime.ObjectPreview): number {
if (object.subtype !== 'array' && object.subtype !== 'typedarray') {
return 0;
}
// Array lengths in V8-generated descriptions switched from square brackets to parentheses.
// Both formats are checked in case the front end is dealing with an old version of V8.
const parenMatches = object.description?.match(descriptionLengthParenRegex);
const squareMatches = object.description?.match(descriptionLengthSquareRegex);
return parenMatches ? parseInt(parenMatches[1], 10) : (squareMatches ? parseInt(squareMatches[1], 10) : 0);
}
static arrayBufferByteLength(object: RemoteObject|Protocol.Runtime.RemoteObject|Protocol.Runtime.ObjectPreview):
number {
if (object.subtype !== 'arraybuffer') {
return 0;
}
const matches = object.description?.match(descriptionLengthParenRegex);
return matches ? parseInt(matches[1], 10) : 0;
}
static unserializableDescription(object: unknown): string|null {
if (typeof object === 'number') {
const description = String(object);
if (object === 0 && 1 / object < 0) {
return UnserializableNumber.NEGATIVE_ZERO;
}
if (description === UnserializableNumber.NAN || description === UnserializableNumber.INFINITY ||
description === UnserializableNumber.NEGATIVE_INFINITY) {
return description;
}
}
if (typeof object === 'bigint') {
return object + 'n';
}
return null;
}
static toCallArgument(object: string|number|bigint|boolean|RemoteObject|Protocol.Runtime.RemoteObject|null|undefined):
Protocol.Runtime.CallArgument {
const type = typeof object;
if (type === 'undefined') {
return {};
}
const unserializableDescription = RemoteObject.unserializableDescription(object);
if (type === 'number') {
if (unserializableDescription !== null) {
return {unserializableValue: unserializableDescription};
}
return {value: object};
}
if (type === 'bigint') {
return {unserializableValue: unserializableDescription ?? undefined};
}
if (type === 'string' || type === 'boolean') {
return {value: object};
}
if (!object) {
return {value: null};
}
// The unserializableValue is a function on RemoteObject's and a simple property on
// Protocol.Runtime.RemoteObject's.
const objectAsProtocolRemoteObject = (object as Protocol.Runtime.RemoteObject);
if (object instanceof RemoteObject) {
const unserializableValue = object.unserializableValue();
if (unserializableValue !== undefined) {
return {unserializableValue};
}
} else if (objectAsProtocolRemoteObject.unserializableValue !== undefined) {
return {unserializableValue: objectAsProtocolRemoteObject.unserializableValue};
}
if (typeof objectAsProtocolRemoteObject.objectId !== 'undefined') {
return {objectId: objectAsProtocolRemoteObject.objectId};
}
return {value: objectAsProtocolRemoteObject.value};
}
static async loadFromObjectPerProto(object: RemoteObject, generatePreview: boolean, nonIndexedPropertiesOnly = false):
Promise<GetPropertiesResult> {
const result = await Promise.all([
object.getAllProperties(true /* accessorPropertiesOnly */, generatePreview, nonIndexedPropertiesOnly),
object.getOwnProperties(generatePreview, nonIndexedPropertiesOnly),
]);
const accessorProperties = result[0].properties;
const ownProperties = result[1].properties;
const internalProperties = result[1].internalProperties;
if (!ownProperties || !accessorProperties) {
return {properties: null, internalProperties: null};
}
const propertiesMap = new Map<string, RemoteObjectProperty>();
const propertySymbols = [];
for (let i = 0; i < accessorProperties.length; i++) {
const property = accessorProperties[i];
if (property.symbol) {
propertySymbols.push(property);
} else if (property.isOwn || property.name !== '__proto__') {
// TODO(crbug/1076820): Eventually we should move away from
// showing accessor #properties directly on the receiver.
propertiesMap.set(property.name, property);
}
}
for (let i = 0; i < ownProperties.length; i++) {
const property = ownProperties[i];
if (property.isAccessorProperty()) {
continue;
}
if (property.private || property.symbol) {
propertySymbols.push(property);
} else {
propertiesMap.set(property.name, property);
}
}
return {
properties: [...propertiesMap.values()].concat(propertySymbols),
internalProperties: internalProperties ? internalProperties : null,
};
}
customPreview(): Protocol.Runtime.CustomPreview|null {
return null;
}
abstract get objectId(): Protocol.Runtime.RemoteObjectId|undefined;
abstract get type(): string;
abstract get subtype(): string|undefined;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
abstract get value(): any;
abstract get description(): string|undefined;
abstract set description(description: string|undefined);
abstract get hasChildren(): boolean;
abstract arrayLength(): number;
abstract getOwnProperties(generatePreview: boolean, nonIndexedPropertiesOnly?: boolean): Promise<GetPropertiesResult>;
abstract getAllProperties(
accessorPropertiesOnly: boolean, generatePreview: boolean,
nonIndexedPropertiesOnly?: boolean): Promise<GetPropertiesResult>;
unserializableValue(): string|undefined {
throw new Error('Not implemented');
}
get preview(): Protocol.Runtime.ObjectPreview|undefined {
return undefined;
}
get className(): string|null {
return null;
}
callFunction<T, U>(_functionDeclaration: (this: U, ...args: any[]) => T, _args?: Protocol.Runtime.CallArgument[]):
Promise<CallFunctionResult> {
throw new Error('Not implemented');
}
callFunctionJSON<T, U>(
_functionDeclaration: (this: U, ...args: any[]) => T,
_args: Protocol.Runtime.CallArgument[]|undefined): Promise<T|null> {
throw new Error('Not implemented');
}
arrayBufferByteLength(): number {
throw new Error('Not implemented');
}
deleteProperty(_name: Protocol.Runtime.CallArgument): Promise<string|undefined> {
throw new Error('Not implemented');
}
setPropertyValue(_name: string|Protocol.Runtime.CallArgument, _value: string): Promise<string|undefined> {
throw new Error('Not implemented');
}
release(): void {
}
debuggerModel(): DebuggerModel {
throw new Error('DebuggerModel-less object');
}
runtimeModel(): RuntimeModel {
throw new Error('RuntimeModel-less object');
}
isNode(): boolean {
return false;
}
/**
* Checks whether this object can be inspected with the Linear memory inspector.
* @returns `true` if this object can be inspected with the Linear memory inspector.
*/
isLinearMemoryInspectable(): boolean {
return false;
}
webIdl?: RemoteObjectWebIdlTypeMetadata;
}
export class RemoteObjectImpl extends RemoteObject {
#runtimeModel: RuntimeModel;
readonly #runtimeAgent: ProtocolProxyApi.RuntimeApi;
readonly #type: string;
readonly #subtype: string|undefined;
#objectId: Protocol.Runtime.RemoteObjectId|undefined;
#description: string|undefined;
#hasChildren: boolean;
readonly #preview: Protocol.Runtime.ObjectPreview|undefined;
readonly #unserializableValue: string|undefined;
readonly #value: typeof RemoteObject.prototype.value;
readonly #customPreview: Protocol.Runtime.CustomPreview|null;
readonly #className: string|null;
constructor(
runtimeModel: RuntimeModel,
objectId: Protocol.Runtime.RemoteObjectId|undefined,
type: string,
subtype: string|undefined,
value: typeof RemoteObject.prototype.value,
unserializableValue?: string,
description?: string,
preview?: Protocol.Runtime.ObjectPreview,
customPreview?: Protocol.Runtime.CustomPreview,
className?: string,
) {
super();
this.#runtimeModel = runtimeModel;
this.#runtimeAgent = runtimeModel.target().runtimeAgent();
this.#type = type;
this.#subtype = subtype;
if (objectId) {
// handle
this.#objectId = objectId;
this.#description = description;
this.#hasChildren = (type !== 'symbol');
this.#preview = preview;
} else {
this.#description = description;
if (!this.description && unserializableValue) {
this.#description = unserializableValue;
}
if (!this.#description && (typeof value !== 'object' || value === null)) {
this.#description = String(value);
}
this.#hasChildren = false;
if (typeof unserializableValue === 'string') {
this.#unserializableValue = unserializableValue;
if (unserializableValue === UnserializableNumber.INFINITY ||
unserializableValue === UnserializableNumber.NEGATIVE_INFINITY ||
unserializableValue === UnserializableNumber.NEGATIVE_ZERO ||
unserializableValue === UnserializableNumber.NAN) {
this.#value = Number(unserializableValue);
} else if (type === 'bigint' && unserializableValue.endsWith('n')) {
this.#value = BigInt(unserializableValue.substring(0, unserializableValue.length - 1));
} else {
this.#value = unserializableValue;
}
} else {
this.#value = value;
}
}
this.#customPreview = customPreview || null;
this.#className = typeof className === 'string' ? className : null;
}
override customPreview(): Protocol.Runtime.CustomPreview|null {
return this.#customPreview;
}
override get objectId(): Protocol.Runtime.RemoteObjectId|undefined {
return this.#objectId;
}
override get type(): string {
return this.#type;
}
override get subtype(): string|undefined {
return this.#subtype;
}
override get value(): typeof RemoteObject.prototype.value {
return this.#value;
}
override unserializableValue(): string|undefined {
return this.#unserializableValue;
}
override get description(): string|undefined {
return this.#description;
}
override set description(description: string|undefined) {
this.#description = description;
}
override get hasChildren(): boolean {
return this.#hasChildren;
}
override get preview(): Protocol.Runtime.ObjectPreview|undefined {
return this.#preview;
}
override get className(): string|null {
return this.#className;
}
override getOwnProperties(generatePreview: boolean, nonIndexedPropertiesOnly = false): Promise<GetPropertiesResult> {
return this.doGetProperties(true, false, nonIndexedPropertiesOnly, generatePreview);
}
override getAllProperties(
accessorPropertiesOnly: boolean, generatePreview: boolean,
nonIndexedPropertiesOnly = false): Promise<GetPropertiesResult> {
return this.doGetProperties(false, accessorPropertiesOnly, nonIndexedPropertiesOnly, generatePreview);
}
async createRemoteObject(object: Protocol.Runtime.RemoteObject): Promise<RemoteObject> {
return this.#runtimeModel.createRemoteObject(object);
}
async doGetProperties(
ownProperties: boolean, accessorPropertiesOnly: boolean, nonIndexedPropertiesOnly: boolean,
generatePreview: boolean): Promise<GetPropertiesResult> {
if (!this.#objectId) {
return {properties: null, internalProperties: null};
}
const response = await this.#runtimeAgent.invoke_getProperties({
objectId: this.#objectId,
ownProperties,
accessorPropertiesOnly,
nonIndexedPropertiesOnly,
generatePreview,
});
if (response.getError()) {
return {properties: null, internalProperties: null};
}
if (response.exceptionDetails) {
this.#runtimeModel.exceptionThrown(Date.now(), response.exceptionDetails);
return {properties: null, internalProperties: null};
}
const {result: properties = [], internalProperties = [], privateProperties = []} = response;
const result = [];
for (const property of properties) {
const propertyValue = property.value ? await this.createRemoteObject(property.value) : null;
const propertySymbol = property.symbol ? this.#runtimeModel.createRemoteObject(property.symbol) : null;
const remoteProperty = new RemoteObjectProperty(
property.name, propertyValue, Boolean(property.enumerable), Boolean(property.writable),
Boolean(property.isOwn), Boolean(property.wasThrown), propertySymbol);
if (typeof property.value === 'undefined') {
if (property.get && property.get.type !== 'undefined') {
remoteProperty.getter = this.#runtimeModel.createRemoteObject(property.get);
}
if (property.set && property.set.type !== 'undefined') {
remoteProperty.setter = this.#runtimeModel.createRemoteObject(property.set);
}
}
result.push(remoteProperty);
}
for (const property of privateProperties) {
const propertyValue = property.value ? this.#runtimeModel.createRemoteObject(property.value) : null;
const remoteProperty = new RemoteObjectProperty(
property.name, propertyValue, true, true, true, false, undefined, false, undefined, true);
if (typeof property.value === 'undefined') {
if (property.get && property.get.type !== 'undefined') {
remoteProperty.getter = this.#runtimeModel.createRemoteObject(property.get);
}
if (property.set && property.set.type !== 'undefined') {
remoteProperty.setter = this.#runtimeModel.createRemoteObject(property.set);
}
}
result.push(remoteProperty);
}
const internalPropertiesResult = [];
for (const property of internalProperties) {
if (!property.value) {
continue;
}
const propertyValue = this.#runtimeModel.createRemoteObject(property.value);
internalPropertiesResult.push(
new RemoteObjectProperty(property.name, propertyValue, true, false, undefined, undefined, undefined, true));
}
return {properties: result, internalProperties: internalPropertiesResult};
}
override async setPropertyValue(name: string|Protocol.Runtime.CallArgument, value: string):
Promise<string|undefined> {
if (!this.#objectId) {
return 'Can’t set a property of non-object.';
}
const response = await this.#runtimeAgent.invoke_evaluate({expression: value, silent: true});
if (response.getError() || response.exceptionDetails) {
return response.getError() ||
(response.result.type !== 'string' ? response.result.description : response.result.value as string);
}
if (typeof name === 'string') {
name = RemoteObject.toCallArgument(name);
}
const resultPromise = this.doSetObjectPropertyValue(response.result, name);
if (response.result.objectId) {
void this.#runtimeAgent.invoke_releaseObject({objectId: response.result.objectId});
}
return await resultPromise;
}
async doSetObjectPropertyValue(result: Protocol.Runtime.RemoteObject, name: Protocol.Runtime.CallArgument):
Promise<string|undefined> {
// This assignment may be for a regular (data) property, and for an accessor property (with getter/setter).
// Note the sensitive matter about accessor property: the property may be physically defined in some proto object,
// but logically it is bound to the object in question. JavaScript passes this object to getters/setters, not the object
// where property was defined; so do we.
const setPropertyValueFunction = 'function(a, b) { this[a] = b; }';
const argv = [name, RemoteObject.toCallArgument(result)];
const response = await this.#runtimeAgent.invoke_callFunctionOn({
objectId: this.#objectId,
functionDeclaration: setPropertyValueFunction,
arguments: argv,
silent: true,
});
const error = response.getError();
return error || response.exceptionDetails ? error || response.result.description : undefined;
}
override async deleteProperty(name: Protocol.Runtime.CallArgument): Promise<string|undefined> {
if (!this.#objectId) {
return 'Can’t delete a property of non-object.';
}
const deletePropertyFunction = 'function(a) { delete this[a]; return !(a in this); }';
const response = await this.#runtimeAgent.invoke_callFunctionOn({
objectId: this.#objectId,
functionDeclaration: deletePropertyFunction,
arguments: [name],
silent: true,
});
if (response.getError() || response.exceptionDetails) {
return response.getError() || response.result.description;
}
if (!response.result.value) {
return 'Failed to delete property.';
}
return undefined;
}
override async callFunction<T, U>(
functionDeclaration: (this: U, ...args: any[]) => T,
args?: Protocol.Runtime.CallArgument[]): Promise<CallFunctionResult> {
const response = await this.#runtimeAgent.invoke_callFunctionOn({
objectId: this.#objectId,
functionDeclaration: functionDeclaration.toString(),
arguments: args,
silent: true,
});
if (response.getError()) {
return {object: null, wasThrown: false};
}
// TODO: release exceptionDetails object
return {
object: this.#runtimeModel.createRemoteObject(response.result),
wasThrown: Boolean(response.exceptionDetails),
};
}
override async callFunctionJSON<T, U>(
functionDeclaration: (this: U, ...args: any[]) => T,
args: Protocol.Runtime.CallArgument[]|undefined): Promise<T|null> {
const response = await this.#runtimeAgent.invoke_callFunctionOn({
objectId: this.#objectId,
functionDeclaration: functionDeclaration.toString(),
arguments: args,
silent: true,
returnByValue: true,
});
if (response.getError() || response.exceptionDetails) {
return null;
}
return response.result.value;
}
override release(): void {
if (!this.#objectId) {
return;
}
void this.#runtimeAgent.invoke_releaseObject({objectId: this.#objectId});
}
override arrayLength(): number {
return RemoteObject.arrayLength(this);
}
override arrayBufferByteLength(): number {
return RemoteObject.arrayBufferByteLength(this);
}
override debuggerModel(): DebuggerModel {
return this.#runtimeModel.debuggerModel();
}
override runtimeModel(): RuntimeModel {
return this.#runtimeModel;
}
override isNode(): boolean {
return Boolean(this.#objectId) && this.type === 'object' && this.subtype === 'node';
}
override isLinearMemoryInspectable(): boolean {
return this.type === 'object' && this.subtype !== undefined &&
['webassemblymemory', 'typedarray', 'dataview', 'arraybuffer'].includes(this.subtype);
}
}
export class ScopeRemoteObject extends RemoteObjectImpl {
readonly #scopeRef: ScopeRef;
#savedScopeProperties: RemoteObjectProperty[]|undefined;
constructor(
runtimeModel: RuntimeModel, objectId: Protocol.Runtime.RemoteObjectId|undefined, scopeRef: ScopeRef, type: string,
subtype: string|undefined, value: typeof RemoteObjectImpl.prototype.value, unserializableValue?: string,
description?: string, preview?: Protocol.Runtime.ObjectPreview) {
super(runtimeModel, objectId, type, subtype, value, unserializableValue, description, preview);
this.#scopeRef = scopeRef;
this.#savedScopeProperties = undefined;
}
override async doGetProperties(ownProperties: boolean, accessorPropertiesOnly: boolean, _generatePreview: boolean):
Promise<GetPropertiesResult> {
if (accessorPropertiesOnly) {
return {properties: [], internalProperties: []};
}
if (this.#savedScopeProperties) {
// No need to reload scope variables, as the remote object never
// changes its #properties. If variable is updated, the #properties
// array is patched locally.
return {properties: this.#savedScopeProperties.slice(), internalProperties: null};
}
const allProperties = await super.doGetProperties(
ownProperties, accessorPropertiesOnly, false /* nonIndexedPropertiesOnly */, true /* generatePreview */);
if (Array.isArray(allProperties.properties)) {
this.#savedScopeProperties = allProperties.properties.slice();
}
return allProperties;
}
override async doSetObjectPropertyValue(
result: Protocol.Runtime.RemoteObject, argumentName: Protocol.Runtime.CallArgument): Promise<string|undefined> {
const name = (argumentName.value as string);
const error = await this.debuggerModel().setVariableValue(
this.#scopeRef.number, name, RemoteObject.toCallArgument(result), this.#scopeRef.callFrameId);
if (error) {
return error;
}
if (this.#savedScopeProperties) {
for (const property of this.#savedScopeProperties) {
if (property.name === name) {
property.value = this.runtimeModel().createRemoteObject(result);
}
}
}
return;
}
}
export class ScopeRef {
readonly number: number;
readonly callFrameId: Protocol.Debugger.CallFrameId;
constructor(number: number, callFrameId: Protocol.Debugger.CallFrameId) {
this.number = number;
this.callFrameId = callFrameId;
}
}
export class RemoteObjectProperty {
name: string;
value?: RemoteObject;
enumerable: boolean;
writable: boolean;
isOwn: boolean;
wasThrown: boolean;
symbol: RemoteObject|undefined;
synthetic: boolean;
syntheticSetter: ((arg0: string) => Promise<RemoteObject|null>)|undefined;
private: boolean;
getter: RemoteObject|undefined;
setter: RemoteObject|undefined;
webIdl?: RemoteObjectWebIdlPropertyMetadata;
constructor(
name: string, value: RemoteObject|null, enumerable?: boolean, writable?: boolean, isOwn?: boolean,
wasThrown?: boolean, symbol?: RemoteObject|null, synthetic?: boolean,
syntheticSetter?: ((arg0: string) => Promise<RemoteObject|null>), isPrivate?: boolean) {
this.name = name;
this.value = value !== null ? value : undefined;
this.enumerable = typeof enumerable !== 'undefined' ? enumerable : true;
const isNonSyntheticOrSyntheticWritable = !synthetic || Boolean(syntheticSetter);
this.writable = typeof writable !== 'undefined' ? writable : isNonSyntheticOrSyntheticWritable;
this.isOwn = Boolean(isOwn);
this.wasThrown = Boolean(wasThrown);
if (symbol) {
this.symbol = symbol;
}
this.synthetic = Boolean(synthetic);
if (syntheticSetter) {
this.syntheticSetter = syntheticSetter;
}
this.private = Boolean(isPrivate);
}
async setSyntheticValue(expression: string): Promise<boolean> {
if (!this.syntheticSetter) {
return false;
}
const result = await this.syntheticSetter(expression);
if (result) {
this.value = result;
}
return Boolean(result);
}
isAccessorProperty(): boolean {
return Boolean(this.getter || this.setter);
}
match({includeNullOrUndefinedValues, regex}: {includeNullOrUndefinedValues: boolean, regex: RegExp|null}): boolean {
if (regex !== null) {
if (!regex.test(this.name) && !regex.test(this.value?.description ?? '')) {
return false;
}
}
if (!includeNullOrUndefinedValues) {
if (!this.isAccessorProperty() && RemoteObject.isNullOrUndefined(this.value)) {
return false;
}
}
return true;
}
cloneWithNewName(newName: string): RemoteObjectProperty {
const property = new RemoteObjectProperty(
newName, this.value ?? null, this.enumerable, this.writable, this.isOwn, this.wasThrown, this.symbol,
this.synthetic, this.syntheticSetter, this.private);
property.getter = this.getter;
property.setter = this.setter;
return property;
}
}
// Below is a wrapper around a local object that implements the RemoteObject interface,
// which can be used by the UI code (primarily ObjectPropertiesSection).
// Note that only JSON-compliant objects are currently supported, as there's no provision
// for traversing prototypes, extracting class names via constructor, handling #properties
// or functions.
export class LocalJSONObject extends RemoteObject {
#value: typeof RemoteObject.prototype.value;
#cachedDescription!: string;
#cachedChildren!: RemoteObjectProperty[];
constructor(value: typeof RemoteObject.prototype.value) {
super();
this.#value = value;
}
override get objectId(): Protocol.Runtime.RemoteObjectId|undefined {
return undefined;
}
override get value(): typeof RemoteObject.prototype.value {
return this.#value;
}
override unserializableValue(): string|undefined {
const unserializableDescription = RemoteObject.unserializableDescription(this.#value);
return unserializableDescription || undefined;
}
override get description(): string {
if (this.#cachedDescription) {
return this.#cachedDescription;
}
function formatArrayItem(this: LocalJSONObject, property: RemoteObjectProperty): string {
return this.formatValue(property.value || null);
}
function formatObjectItem(this: LocalJSONObject, property: RemoteObjectProperty): string {
let name: string = property.name;
if (/^\s|\s$|^$|\n/.test(name)) {
name = '"' + name.replace(/\n/g, '\u21B5') + '"';
}
return name + ': ' + this.formatValue(property.value || null);
}
if (this.type === 'object') {
switch (this.subtype) {
case 'array':
this.#cachedDescription = this.concatenate('[', ']', formatArrayItem.bind(this));
break;
case 'date':
this.#cachedDescription = String(this.#value);
break;
case 'null':
this.#cachedDescription = 'null';
break;
default:
this.#cachedDescription = this.concatenate('{', '}', formatObjectItem.bind(this));
}
} else {
this.#cachedDescription = String(this.#value);
}
return this.#cachedDescription;
}
private formatValue(value: RemoteObject|null): string {
if (!value) {
return 'undefined';
}
const description = value.description || '';
if (value.type === 'string') {
return '"' + description.replace(/\n/g, '\u21B5') + '"';
}
return description;
}
private concatenate(prefix: string, suffix: string, formatProperty: (arg0: RemoteObjectProperty) => string): string {
const previewChars = 100;
let buffer = prefix;
const children = this.children();
for (let i = 0; i < children.length; ++i) {
const itemDescription = formatProperty(children[i]);
if (buffer.length + itemDescription.length > previewChars) {
buffer += ',…';
break;
}
if (i) {
buffer += ', ';
}
buffer += itemDescription;
}
buffer += suffix;
return buffer;
}
override get type(): string {
return typeof this.#value;
}
override get subtype(): string|undefined {
if (this.#value === null) {
return 'null';
}
if (Array.isArray(this.#value)) {
return 'array';
}
if (this.#value instanceof Date) {
return 'date';
}
if (this.#value instanceof Error) {
return 'error';
}
return undefined;
}
override get hasChildren(): boolean {
if ((typeof this.#value !== 'object') || (this.#value === null)) {
return false;
}
return Boolean(Object.keys(this.#value).length);
}
override async getOwnProperties(_generatePreview: boolean, nonIndexedPropertiesOnly = false):
Promise<GetPropertiesResult> {
function isArrayIndex(name: string): boolean {
const index = Number(name) >>> 0;
return String(index) === name;
}
let properties = this.children();
if (nonIndexedPropertiesOnly) {
properties = properties.filter(property => !isArrayIndex(property.name));
}
return {properties, internalProperties: null};
}
override async getAllProperties(
accessorPropertiesOnly: boolean, generatePreview: boolean,
nonIndexedPropertiesOnly = false): Promise<GetPropertiesResult> {
if (accessorPropertiesOnly) {
return {properties: [], internalProperties: null};
}
return await this.getOwnProperties(generatePreview, nonIndexedPropertiesOnly);
}
private children(): RemoteObjectProperty[] {
if (!this.hasChildren) {
return [];
}
if (!this.#cachedChildren) {
this.#cachedChildren = Object.entries(this.#value).map(([name, value]) => {
return new RemoteObjectProperty(
name, value instanceof RemoteObject ? value : RemoteObject.fromLocalObject(value));
});
}
return this.#cachedChildren;
}
override arrayLength(): number {
return Array.isArray(this.#value) ? this.#value.length : 0;
}
override async callFunction<T, U>(
functionDeclaration: (this: U, ...args: any[]) => T,
args?: Protocol.Runtime.CallArgument[]): Promise<CallFunctionResult> {
const target = this.#value as U;
const rawArgs = args ? args.map(arg => arg.value) : [];
let result;
let wasThrown = false;
try {
result = functionDeclaration.apply(target, rawArgs);
} catch {
wasThrown = true;
}
const object = RemoteObject.fromLocalObject(result);
return {object, wasThrown};
}
override async callFunctionJSON<T, U>(
functionDeclaration: (this: U, ...args: any[]) => T,
args: Protocol.Runtime.CallArgument[]|undefined): Promise<T|null> {
const target = this.#value as U;
const rawArgs = args ? args.map(arg => arg.value) : [];
let result;
try {
result = functionDeclaration.apply(target, rawArgs);
} catch {
result = null;
}
return result;
}
}
export class RemoteArrayBuffer {
readonly #object: RemoteObject;
constructor(object: RemoteObject) {
if (object.type !== 'object' || object.subtype !== 'arraybuffer') {
throw new Error('Object is not an arraybuffer');
}
this.#object = object;
}
byteLength(): number {
return this.#object.arrayBufferByteLength();
}
async bytes(start = 0, end = this.byteLength()): Promise<number[]|null> {
if (start < 0 || start >= this.byteLength()) {
throw new RangeError('start is out of range');
}
if (end < start || end > this.byteLength()) {
throw new RangeError('end is out of range');
}
return await this.#object.callFunctionJSON(bytes, [{value: start}, {value: end - start}]);
function bytes(this: ArrayBuffer, offset: number, length: number): number[] {
return [...new Uint8Array(this, offset, length)];
}
}
object(): RemoteObject {
return this.#object;
}
}
export class RemoteArray {
readonly #object: RemoteObject;
constructor(object: RemoteObject) {
this.#object = object;
}
static objectAsArray(object: RemoteObject|null): RemoteArray {
if (object?.type !== 'object' || (object.subtype !== 'array' && object.subtype !== 'typedarray')) {
throw new Error('Object is empty or not an array');
}
return new RemoteArray(object);
}
static async createFromRemoteObjects(objects: RemoteObject[]): Promise<RemoteArray> {
if (!objects.length) {
throw new Error('Input array is empty');
}
const result = await objects[0].callFunction(createArray, objects.map(RemoteObject.toCallArgument));
if (result.wasThrown || !result.object) {
throw new Error('Call function throws exceptions or returns empty value');
}
return RemoteArray.objectAsArray(result.object);
function createArray<T>(...args: T[]): T[] {
return args;
}
}
async at(index: number): Promise<RemoteObject> {
if (index < 0 || index > this.#object.arrayLength()) {
throw new Error('Out of range');
}
const result = await this.#object.callFunction<unknown, unknown[]>(at, [RemoteObject.toCallArgument(index)]);
if (result.wasThrown || !result.object) {
throw new Error('Exception in callFunction or result value is empty');
}
return result.object;
function at<T>(this: T[], index: number): T {
return this[index];
}
}
length(): number {
return this.#object.arrayLength();
}
map<T>(func: (arg0: RemoteObject) => Promise<T>): Promise<T[]> {
const promises = [];
for (let i = 0; i < this.length(); ++i) {
promises.push(this.at(i).then(func));
}
return Promise.all(promises);
}
object(): RemoteObject {
return this.#object;
}
}
export class RemoteFunction {
readonly #object: RemoteObject;
constructor(object: RemoteObject) {
this.#object = object;
}
static objectAsFunction(object: RemoteObject): RemoteFunction {
if (object.type !== 'function') {
throw new Error('Object is empty or not a function');
}
return new RemoteFunction(object);
}
async targetFunction(): Promise<RemoteObject> {
const ownProperties = await this.#object.getOwnProperties(false /* generatePreview */);
const targetFunction = ownProperties.internalProperties?.find(({name}) => name === '[[TargetFunction]]');
return targetFunction?.value ?? this.#object;
}
async targetFunctionDetails(): Promise<FunctionDetails|null> {
const targetFunction = await this.targetFunction();
const functionDetails = await targetFunction.debuggerModel().functionDetailsPromise(targetFunction);
if (this.#object !== targetFunction) {
targetFunction.release();
}
return functionDetails;
}
}
export class RemoteError {
readonly #object: RemoteObject;
#exceptionDetails?: Promise<Protocol.Runtime.ExceptionDetails|undefined>;
#cause?: Promise<RemoteObject|undefined>;
private constructor(object: RemoteObject) {
this.#object = object;
}
static objectAsError(object: RemoteObject): RemoteError {
if (object.subtype !== 'error') {
throw new Error(`Object of type ${object.subtype} is not an error`);
}
return new RemoteError(object);
}
get errorStack(): string {
return this.#object.description ?? '';
}
exceptionDetails(): Promise<Protocol.Runtime.ExceptionDetails|undefined> {
if (!this.#exceptionDetails) {
this.#exceptionDetails = this.#lookupExceptionDetails();
}
return this.#exceptionDetails;
}
#lookupExceptionDetails(): Promise<Protocol.Runtime.ExceptionDetails|undefined> {
if (this.#object.objectId) {
return this.#object.runtimeModel().getExceptionDetails(this.#object.objectId);
}
return Promise.resolve(undefined);
}
cause(): Promise<RemoteObject|undefined> {
if (!this.#cause) {
this.#cause = this.#lookupCause();
}
return this.#cause;
}
async #lookupCause(): Promise<RemoteObject|undefined> {
const allProperties =
await this.#object.getAllProperties(false /* accessorPropertiesOnly */, false /* generatePreview */);
const cause = allProperties.properties?.find(prop => prop.name === 'cause');
return cause?.value;
}
}
const descriptionLengthParenRegex = /\(([0-9]+)\)/;
const descriptionLengthSquareRegex = /\[([0-9]+)\]/;
const enum UnserializableNumber {
NEGATIVE_ZERO = ('-0'),
NAN = ('NaN'),
INFINITY = ('Infinity'),
NEGATIVE_INFINITY = ('-Infinity'),
}
export interface CallFunctionResult {
object: RemoteObject|null;
wasThrown?: boolean;
}
export interface GetPropertiesResult {
properties: RemoteObjectProperty[]|null;
internalProperties: RemoteObjectProperty[]|null;
}
export interface RemoteObjectWebIdlTypeMetadata {
info: DOMPinnedWebIDLType;
state: Map<string, string>;
}
export interface RemoteObjectWebIdlPropertyMetadata {
info: DOMPinnedWebIDLProp;
applicable?: boolean;
}
/**
* Pair of a linear memory inspectable {@link RemoteObject} and an optional
* expression, which identifies the variable holding the object in the
* current scope or the name of the field holding the object.
*
* This data structure is used to reveal an object in the Linear Memory
* Inspector panel.
*/
export class LinearMemoryInspectable {
/** The linear memory inspectable {@link RemoteObject}. */
readonly object: RemoteObject;
/** The name of the variable or the field holding the `object`. */
readonly expression: string|undefined;
/**
* Wrap `object` and `expression` into a reveable structure.
*
* @param object A linear memory inspectable {@link RemoteObject}.
* @param expression An optional name of the field or variable holding the `object`.
*/
constructor(object: RemoteObject, expression?: string) {
if (!object.isLinearMemoryInspectable()) {
throw new Error('object must be linear memory inspectable');
}
this.object = object;
this.expression = expression;
}
}