"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Realm = void 0; const protocol_js_1 = require("../../../protocol/protocol.js"); const log_js_1 = require("../../../utils/log.js"); const uuid_js_1 = require("../../../utils/uuid.js"); const ChannelProxy_js_1 = require("./ChannelProxy.js"); class Realm { #cdpClient; #eventManager; #executionContextId; #logger; #origin; #realmId; #realmStorage; constructor(cdpClient, eventManager, executionContextId, logger, origin, realmId, realmStorage) { this.#cdpClient = cdpClient; this.#eventManager = eventManager; this.#executionContextId = executionContextId; this.#logger = logger; this.#origin = origin; this.#realmId = realmId; this.#realmStorage = realmStorage; this.#realmStorage.addRealm(this); } cdpToBidiValue(cdpValue, resultOwnership) { const bidiValue = this.serializeForBiDi(cdpValue.result.deepSerializedValue, new Map()); if (cdpValue.result.objectId) { const objectId = cdpValue.result.objectId; if (resultOwnership === "root" /* Script.ResultOwnership.Root */) { // Extend BiDi value with `handle` based on required `resultOwnership` // and CDP response but not on the actual BiDi type. bidiValue.handle = objectId; // Remember all the handles sent to client. this.#realmStorage.knownHandlesToRealmMap.set(objectId, this.realmId); } else { // No need to await for the object to be released. void this.#releaseObject(objectId).catch((error) => this.#logger?.(log_js_1.LogType.debugError, error)); } } return bidiValue; } /** * Relies on the CDP to implement proper BiDi serialization, except: * * CDP integer property `backendNodeId` is replaced with `sharedId` of * `{documentId}_element_{backendNodeId}`; * * CDP integer property `weakLocalObjectReference` is replaced with UUID `internalId` * using unique-per serialization `internalIdMap`. * * CDP type `platformobject` is replaced with `object`. * @param deepSerializedValue - CDP value to be converted to BiDi. * @param internalIdMap - Map from CDP integer `weakLocalObjectReference` to BiDi UUID * `internalId`. */ serializeForBiDi(deepSerializedValue, internalIdMap) { if (Object.hasOwn(deepSerializedValue, 'weakLocalObjectReference')) { const weakLocalObjectReference = deepSerializedValue.weakLocalObjectReference; if (!internalIdMap.has(weakLocalObjectReference)) { internalIdMap.set(weakLocalObjectReference, (0, uuid_js_1.uuidv4)()); } deepSerializedValue.internalId = internalIdMap.get(weakLocalObjectReference); delete deepSerializedValue['weakLocalObjectReference']; } // Platform object is a special case. It should have only `{type: object}` // without `value` field. if (deepSerializedValue.type === 'platformobject') { return { type: 'object' }; } const bidiValue = deepSerializedValue.value; if (bidiValue === undefined) { return deepSerializedValue; } // Recursively update the nested values. if (['array', 'set', 'htmlcollection', 'nodelist'].includes(deepSerializedValue.type)) { for (const i in bidiValue) { bidiValue[i] = this.serializeForBiDi(bidiValue[i], internalIdMap); } } if (['object', 'map'].includes(deepSerializedValue.type)) { for (const i in bidiValue) { bidiValue[i] = [ this.serializeForBiDi(bidiValue[i][0], internalIdMap), this.serializeForBiDi(bidiValue[i][1], internalIdMap), ]; } } return deepSerializedValue; } get realmId() { return this.#realmId; } get executionContextId() { return this.#executionContextId; } get origin() { return this.#origin; } get source() { return { realm: this.realmId, }; } get cdpClient() { return this.#cdpClient; } get baseInfo() { return { realm: this.realmId, origin: this.origin, }; } async evaluate(expression, awaitPromise, resultOwnership = "none" /* Script.ResultOwnership.None */, serializationOptions = {}, userActivation = false, includeCommandLineApi = false) { const cdpEvaluateResult = await this.cdpClient.sendCommand('Runtime.evaluate', { contextId: this.executionContextId, expression, awaitPromise, serializationOptions: Realm.#getSerializationOptions("deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */, serializationOptions), userGesture: userActivation, includeCommandLineAPI: includeCommandLineApi, }); if (cdpEvaluateResult.exceptionDetails) { return await this.#getExceptionResult(cdpEvaluateResult.exceptionDetails, 0, resultOwnership); } return { realm: this.realmId, result: this.cdpToBidiValue(cdpEvaluateResult, resultOwnership), type: 'success', }; } #registerEvent(event) { if (this.associatedBrowsingContexts.length === 0) { this.#eventManager.registerEvent(event, null); } else { for (const browsingContext of this.associatedBrowsingContexts) { this.#eventManager.registerEvent(event, browsingContext.id); } } } initialize() { this.#registerEvent({ type: 'event', method: protocol_js_1.ChromiumBidi.Script.EventNames.RealmCreated, params: this.realmInfo, }); } /** * Serializes a given CDP object into BiDi, keeping references in the * target's `globalThis`. */ async serializeCdpObject(cdpRemoteObject, resultOwnership) { const argument = Realm.#cdpRemoteObjectToCallArgument(cdpRemoteObject); const cdpValue = await this.cdpClient.sendCommand('Runtime.callFunctionOn', { functionDeclaration: String((remoteObject) => remoteObject), awaitPromise: false, arguments: [argument], serializationOptions: { serialization: "deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */, }, executionContextId: this.executionContextId, }); return this.cdpToBidiValue(cdpValue, resultOwnership); } static #cdpRemoteObjectToCallArgument(cdpRemoteObject) { if (cdpRemoteObject.objectId !== undefined) { return { objectId: cdpRemoteObject.objectId }; } if (cdpRemoteObject.unserializableValue !== undefined) { return { unserializableValue: cdpRemoteObject.unserializableValue }; } return { value: cdpRemoteObject.value }; } /** * Gets the string representation of an object. This is equivalent to * calling `toString()` on the object value. */ async stringifyObject(cdpRemoteObject) { const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', { functionDeclaration: String((remoteObject) => String(remoteObject)), awaitPromise: false, arguments: [cdpRemoteObject], returnByValue: true, executionContextId: this.executionContextId, }); return result.value; } async #flattenKeyValuePairs(mappingLocalValue) { const keyValueArray = await Promise.all(mappingLocalValue.map(async ([key, value]) => { let keyArg; if (typeof key === 'string') { // Key is a string. keyArg = { value: key }; } else { // Key is a serialized value. keyArg = await this.deserializeForCdp(key); } const valueArg = await this.deserializeForCdp(value); return [keyArg, valueArg]; })); return keyValueArray.flat(); } async #flattenValueList(listLocalValue) { return await Promise.all(listLocalValue.map((localValue) => this.deserializeForCdp(localValue))); } async #serializeCdpExceptionDetails(cdpExceptionDetails, lineOffset, resultOwnership) { const callFrames = cdpExceptionDetails.stackTrace?.callFrames.map((frame) => ({ url: frame.url, functionName: frame.functionName, lineNumber: frame.lineNumber - lineOffset, columnNumber: frame.columnNumber, })) ?? []; // Exception should always be there. const exception = cdpExceptionDetails.exception; return { exception: await this.serializeCdpObject(exception, resultOwnership), columnNumber: cdpExceptionDetails.columnNumber, lineNumber: cdpExceptionDetails.lineNumber - lineOffset, stackTrace: { callFrames, }, text: (await this.stringifyObject(exception)) || cdpExceptionDetails.text, }; } async callFunction(functionDeclaration, awaitPromise, thisLocalValue = { type: 'undefined', }, argumentsLocalValues = [], resultOwnership = "none" /* Script.ResultOwnership.None */, serializationOptions = {}, userActivation = false) { const callFunctionAndSerializeScript = `(...args) => { function callFunction(f, args) { const deserializedThis = args.shift(); const deserializedArgs = args; return f.apply(deserializedThis, deserializedArgs); } return callFunction(( ${functionDeclaration} ), args); }`; const thisAndArgumentsList = [ await this.deserializeForCdp(thisLocalValue), ...(await Promise.all(argumentsLocalValues.map(async (argumentLocalValue) => await this.deserializeForCdp(argumentLocalValue)))), ]; let cdpCallFunctionResult; try { cdpCallFunctionResult = await this.cdpClient.sendCommand('Runtime.callFunctionOn', { functionDeclaration: callFunctionAndSerializeScript, awaitPromise, arguments: thisAndArgumentsList, serializationOptions: Realm.#getSerializationOptions("deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */, serializationOptions), executionContextId: this.executionContextId, userGesture: userActivation, }); } catch (error) { // Heuristic to determine if the problem is in the argument. // The check can be done on the `deserialization` step, but this approach // helps to save round-trips. if (error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ && [ 'Could not find object with given id', 'Argument should belong to the same JavaScript world as target object', 'Invalid remote object id', ].includes(error.message)) { throw new protocol_js_1.NoSuchHandleException('Handle was not found.'); } throw error; } if (cdpCallFunctionResult.exceptionDetails) { return await this.#getExceptionResult(cdpCallFunctionResult.exceptionDetails, 1, resultOwnership); } return { type: 'success', result: this.cdpToBidiValue(cdpCallFunctionResult, resultOwnership), realm: this.realmId, }; } async deserializeForCdp(localValue) { if ('handle' in localValue && localValue.handle) { return { objectId: localValue.handle }; // We tried to find a handle value but failed // This allows us to have exhaustive switch on `localValue.type` } else if ('handle' in localValue || 'sharedId' in localValue) { throw new protocol_js_1.NoSuchHandleException('Handle was not found.'); } switch (localValue.type) { case 'undefined': return { unserializableValue: 'undefined' }; case 'null': return { unserializableValue: 'null' }; case 'string': return { value: localValue.value }; case 'number': if (localValue.value === 'NaN') { return { unserializableValue: 'NaN' }; } else if (localValue.value === '-0') { return { unserializableValue: '-0' }; } else if (localValue.value === 'Infinity') { return { unserializableValue: 'Infinity' }; } else if (localValue.value === '-Infinity') { return { unserializableValue: '-Infinity' }; } return { value: localValue.value, }; case 'boolean': return { value: Boolean(localValue.value) }; case 'bigint': return { unserializableValue: `BigInt(${JSON.stringify(localValue.value)})`, }; case 'date': return { unserializableValue: `new Date(Date.parse(${JSON.stringify(localValue.value)}))`, }; case 'regexp': return { unserializableValue: `new RegExp(${JSON.stringify(localValue.value.pattern)}, ${JSON.stringify(localValue.value.flags)})`, }; case 'map': { // TODO: If none of the nested keys and values has a remote // reference, serialize to `unserializableValue` without CDP roundtrip. const keyValueArray = await this.#flattenKeyValuePairs(localValue.value); const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', { functionDeclaration: String((...args) => { const result = new Map(); for (let i = 0; i < args.length; i += 2) { result.set(args[i], args[i + 1]); } return result; }), awaitPromise: false, arguments: keyValueArray, returnByValue: false, executionContextId: this.executionContextId, }); // TODO(#375): Release `result.objectId` after using. return { objectId: result.objectId }; } case 'object': { // TODO: If none of the nested keys and values has a remote // reference, serialize to `unserializableValue` without CDP roundtrip. const keyValueArray = await this.#flattenKeyValuePairs(localValue.value); const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', { functionDeclaration: String((...args) => { const result = {}; for (let i = 0; i < args.length; i += 2) { // Key should be either `string`, `number`, or `symbol`. const key = args[i]; result[key] = args[i + 1]; } return result; }), awaitPromise: false, arguments: keyValueArray, returnByValue: false, executionContextId: this.executionContextId, }); // TODO(#375): Release `result.objectId` after using. return { objectId: result.objectId }; } case 'array': { // TODO: If none of the nested items has a remote reference, // serialize to `unserializableValue` without CDP roundtrip. const args = await this.#flattenValueList(localValue.value); const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', { functionDeclaration: String((...args) => args), awaitPromise: false, arguments: args, returnByValue: false, executionContextId: this.executionContextId, }); // TODO(#375): Release `result.objectId` after using. return { objectId: result.objectId }; } case 'set': { // TODO: if none of the nested items has a remote reference, // serialize to `unserializableValue` without CDP roundtrip. const args = await this.#flattenValueList(localValue.value); const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', { functionDeclaration: String((...args) => new Set(args)), awaitPromise: false, arguments: args, returnByValue: false, executionContextId: this.executionContextId, }); // TODO(#375): Release `result.objectId` after using. return { objectId: result.objectId }; } case 'channel': { const channelProxy = new ChannelProxy_js_1.ChannelProxy(localValue.value, this.#logger); const channelProxySendMessageHandle = await channelProxy.init(this, this.#eventManager); return { objectId: channelProxySendMessageHandle }; } // TODO(#375): Dispose of nested objects. } // Intentionally outside to handle unknown types throw new Error(`Value ${JSON.stringify(localValue)} is not deserializable.`); } async #getExceptionResult(exceptionDetails, lineOffset, resultOwnership) { return { exceptionDetails: await this.#serializeCdpExceptionDetails(exceptionDetails, lineOffset, resultOwnership), realm: this.realmId, type: 'exception', }; } static #getSerializationOptions(serialization, serializationOptions) { return { serialization, additionalParameters: Realm.#getAdditionalSerializationParameters(serializationOptions), ...Realm.#getMaxObjectDepth(serializationOptions), }; } static #getAdditionalSerializationParameters(serializationOptions) { const additionalParameters = {}; if (serializationOptions.maxDomDepth !== undefined) { additionalParameters['maxNodeDepth'] = serializationOptions.maxDomDepth === null ? 1000 : serializationOptions.maxDomDepth; } if (serializationOptions.includeShadowTree !== undefined) { additionalParameters['includeShadowTree'] = serializationOptions.includeShadowTree; } return additionalParameters; } static #getMaxObjectDepth(serializationOptions) { return serializationOptions.maxObjectDepth === undefined || serializationOptions.maxObjectDepth === null ? {} : { maxDepth: serializationOptions.maxObjectDepth }; } async #releaseObject(handle) { try { await this.cdpClient.sendCommand('Runtime.releaseObject', { objectId: handle, }); } catch (error) { // Heuristic to determine if the problem is in the unknown handler. // Ignore the error if so. if (!(error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ && error.message === 'Invalid remote object id')) { throw error; } } } async disown(handle) { // Disowning an object from different realm does nothing. if (this.#realmStorage.knownHandlesToRealmMap.get(handle) !== this.realmId) { return; } await this.#releaseObject(handle); this.#realmStorage.knownHandlesToRealmMap.delete(handle); } dispose() { this.#registerEvent({ type: 'event', method: protocol_js_1.ChromiumBidi.Script.EventNames.RealmDestroyed, params: { realm: this.realmId, }, }); } } exports.Realm = Realm; //# sourceMappingURL=Realm.js.map