| "use strict"; |
| /* |
| * Copyright 2023 Google LLC. |
| * Copyright (c) Microsoft Corporation. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.ChannelProxy = 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"); |
| /** |
| * Used to send messages from realm to BiDi user. |
| */ |
| class ChannelProxy { |
| #properties; |
| #id = (0, uuid_js_1.uuidv4)(); |
| #logger; |
| constructor(channel, logger) { |
| this.#properties = channel; |
| this.#logger = logger; |
| } |
| /** |
| * Creates a channel proxy in the given realm, initialises listener and |
| * returns a handle to `sendMessage` delegate. |
| */ |
| async init(realm, eventManager) { |
| const channelHandle = await ChannelProxy.#createAndGetHandleInRealm(realm); |
| const sendMessageHandle = await ChannelProxy.#createSendMessageHandle(realm, channelHandle); |
| void this.#startListener(realm, channelHandle, eventManager); |
| return sendMessageHandle; |
| } |
| /** Gets a ChannelProxy from window and returns its handle. */ |
| async startListenerFromWindow(realm, eventManager) { |
| try { |
| const channelHandle = await this.#getHandleFromWindow(realm); |
| void this.#startListener(realm, channelHandle, eventManager); |
| } |
| catch (error) { |
| this.#logger?.(log_js_1.LogType.debugError, error); |
| } |
| } |
| /** |
| * Evaluation string which creates a ChannelProxy object on the client side. |
| */ |
| static #createChannelProxyEvalStr() { |
| const functionStr = String(() => { |
| const queue = []; |
| let queueNonEmptyResolver = null; |
| return { |
| /** |
| * Gets a promise, which is resolved as soon as a message occurs |
| * in the queue. |
| */ |
| async getMessage() { |
| const onMessage = queue.length > 0 |
| ? Promise.resolve() |
| : new Promise((resolve) => { |
| queueNonEmptyResolver = resolve; |
| }); |
| await onMessage; |
| return queue.shift(); |
| }, |
| /** |
| * Adds a message to the queue. |
| * Resolves the pending promise if needed. |
| */ |
| sendMessage(message) { |
| queue.push(message); |
| if (queueNonEmptyResolver !== null) { |
| queueNonEmptyResolver(); |
| queueNonEmptyResolver = null; |
| } |
| }, |
| }; |
| }); |
| return `(${functionStr})()`; |
| } |
| /** Creates a ChannelProxy in the given realm. */ |
| static async #createAndGetHandleInRealm(realm) { |
| const createChannelHandleResult = await realm.cdpClient.sendCommand('Runtime.evaluate', { |
| expression: this.#createChannelProxyEvalStr(), |
| contextId: realm.executionContextId, |
| serializationOptions: { |
| serialization: "idOnly" /* Protocol.Runtime.SerializationOptionsSerialization.IdOnly */, |
| }, |
| }); |
| if (createChannelHandleResult.exceptionDetails || |
| createChannelHandleResult.result.objectId === undefined) { |
| throw new Error(`Cannot create channel`); |
| } |
| return createChannelHandleResult.result.objectId; |
| } |
| /** Gets a handle to `sendMessage` delegate from the ChannelProxy handle. */ |
| static async #createSendMessageHandle(realm, channelHandle) { |
| const sendMessageArgResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', { |
| functionDeclaration: String((channelHandle) => { |
| return channelHandle.sendMessage; |
| }), |
| arguments: [{ objectId: channelHandle }], |
| executionContextId: realm.executionContextId, |
| serializationOptions: { |
| serialization: "idOnly" /* Protocol.Runtime.SerializationOptionsSerialization.IdOnly */, |
| }, |
| }); |
| // TODO: check for exceptionDetails. |
| return sendMessageArgResult.result.objectId; |
| } |
| /** Starts listening for the channel events of the provided ChannelProxy. */ |
| async #startListener(realm, channelHandle, eventManager) { |
| // noinspection InfiniteLoopJS |
| for (;;) { |
| try { |
| const message = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', { |
| functionDeclaration: String(async (channelHandle) => await channelHandle.getMessage()), |
| arguments: [ |
| { |
| objectId: channelHandle, |
| }, |
| ], |
| awaitPromise: true, |
| executionContextId: realm.executionContextId, |
| serializationOptions: { |
| serialization: "deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */, |
| maxDepth: this.#properties.serializationOptions?.maxObjectDepth ?? |
| undefined, |
| }, |
| }); |
| if (message.exceptionDetails) { |
| throw new Error('Runtime.callFunctionOn in ChannelProxy', { |
| cause: message.exceptionDetails, |
| }); |
| } |
| for (const browsingContext of realm.associatedBrowsingContexts) { |
| eventManager.registerEvent({ |
| type: 'event', |
| method: protocol_js_1.ChromiumBidi.Script.EventNames.Message, |
| params: { |
| channel: this.#properties.channel, |
| data: realm.cdpToBidiValue(message, this.#properties.ownership ?? "none" /* Script.ResultOwnership.None */), |
| source: realm.source, |
| }, |
| }, browsingContext.id); |
| } |
| } |
| catch (error) { |
| // If an error is thrown, then the channel is permanently broken, so we |
| // exit the loop. |
| this.#logger?.(log_js_1.LogType.debugError, error); |
| break; |
| } |
| } |
| } |
| /** |
| * Returns a handle of ChannelProxy from window's property which was set there |
| * by `getEvalInWindowStr`. If window property is not set yet, sets a promise |
| * resolver to the window property, so that `getEvalInWindowStr` can resolve |
| * the promise later on with the channel. |
| * This is needed because `getEvalInWindowStr` can be called before or |
| * after this method. |
| */ |
| async #getHandleFromWindow(realm) { |
| const channelHandleResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', { |
| functionDeclaration: String((id) => { |
| const w = window; |
| if (w[id] === undefined) { |
| // The channelProxy is not created yet. Create a promise, put the |
| // resolver to window property and return the promise. |
| // `getEvalInWindowStr` will resolve the promise later. |
| return new Promise((resolve) => (w[id] = resolve)); |
| } |
| // The channelProxy is already created by `getEvalInWindowStr` and |
| // is set into window property. Return it. |
| const channelProxy = w[id]; |
| delete w[id]; |
| return channelProxy; |
| }), |
| arguments: [{ value: this.#id }], |
| executionContextId: realm.executionContextId, |
| awaitPromise: true, |
| serializationOptions: { |
| serialization: "idOnly" /* Protocol.Runtime.SerializationOptionsSerialization.IdOnly */, |
| }, |
| }); |
| if (channelHandleResult.exceptionDetails !== undefined || |
| channelHandleResult.result.objectId === undefined) { |
| throw new Error(`ChannelHandle not found in window["${this.#id}"]`); |
| } |
| return channelHandleResult.result.objectId; |
| } |
| /** |
| * String to be evaluated to create a ProxyChannel and put it to window. |
| * Returns the delegate `sendMessage`. Used to provide an argument for preload |
| * script. Does the following: |
| * 1. Creates a ChannelProxy. |
| * 2. Puts the ChannelProxy to window['${this.#id}'] or resolves the promise |
| * by calling delegate stored in window['${this.#id}']. |
| * This is needed because `#getHandleFromWindow` can be called before or |
| * after this method. |
| * 3. Returns the delegate `sendMessage` of the created ChannelProxy. |
| */ |
| getEvalInWindowStr() { |
| const delegate = String((id, channelProxy) => { |
| const w = window; |
| if (w[id] === undefined) { |
| // `#getHandleFromWindow` is not initialized yet, and will get the |
| // channelProxy later. |
| w[id] = channelProxy; |
| } |
| else { |
| // `#getHandleFromWindow` is already set a delegate to window property |
| // and is waiting for it to be called with the channelProxy. |
| w[id](channelProxy); |
| delete w[id]; |
| } |
| return channelProxy.sendMessage; |
| }); |
| const channelProxyEval = ChannelProxy.#createChannelProxyEvalStr(); |
| return `(${delegate})('${this.#id}',${channelProxyEval})`; |
| } |
| } |
| exports.ChannelProxy = ChannelProxy; |
| //# sourceMappingURL=ChannelProxy.js.map |