"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 uuid_1 = require("../../../utils/uuid");
/**
 * Used to send messages from realm to BiDi user.
 */
class ChannelProxy {
    #properties;
    #id = (0, uuid_1.uuidv4)();
    constructor(channel) {
        if (![0, null, undefined].includes(channel.serializationOptions?.maxDomDepth)) {
            throw new Error('serializationOptions.maxDomDepth other than 0 or null is not supported');
        }
        if (![undefined, 'none'].includes(channel.serializationOptions?.includeShadowTree)) {
            throw new Error('serializationOptions.includeShadowTree other than "none" is not supported');
        }
        this.#properties = channel;
    }
    /**
     * 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) {
        const channelHandle = await this.#getHandleFromWindow(realm);
        void this.#startListener(realm, channelHandle, eventManager);
    }
    /**
     * 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',
            },
        });
        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',
            },
        });
        // TODO: check for exceptionDetails.
        return sendMessageArgResult.result.objectId;
    }
    /** Starts listening for the channel events of the provided ChannelProxy. */
    async #startListener(realm, channelHandle, eventManager) {
        // TODO(#294): Remove this loop after the realm is destroyed.
        // Rely on the CDP throwing exception in such a case.
        // noinspection InfiniteLoopJS
        for (;;) {
            const message = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
                functionDeclaration: String(async (channelHandle) => channelHandle.getMessage()),
                arguments: [
                    {
                        objectId: channelHandle,
                    },
                ],
                awaitPromise: true,
                executionContextId: realm.executionContextId,
                serializationOptions: {
                    serialization: 'deep',
                    ...(this.#properties.serializationOptions?.maxObjectDepth ===
                        undefined ||
                        this.#properties.serializationOptions.maxObjectDepth === null
                        ? {}
                        : {
                            maxDepth: this.#properties.serializationOptions.maxObjectDepth,
                        }),
                },
            });
            if (message.exceptionDetails) {
                // TODO: add logging.
                // TODO: check if a error should be thrown.
                return;
            }
            eventManager.registerEvent({
                method: protocol_js_1.Script.EventNames.MessageEvent,
                params: {
                    channel: this.#properties.channel,
                    data: realm.cdpToBidiValue(message, this.#properties.ownership ?? 'none'),
                    source: {
                        realm: realm.realmId,
                        context: realm.browsingContextId,
                    },
                },
            }, realm.browsingContextId);
        }
    }
    /**
     * 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',
            },
        });
        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