Source: session/ChannelSessionTransportHost.js

'use strict';

const { randomUUID } = require('node:crypto');
const { openOriginalChannelTransport } = require('./ChannelSessionTransport');

/**
 * Borrows a transport in an existing Puppeteer realm selected by the channel
 * owner. Never creates a page, worker, browser, login or WhatsApp socket.
 * assertOwned is mandatory and must fence the caller's original realm/session.
 * The owner signals navigation, worker loss and lease revocation via signal.
 * This is an internal transport API, not a call engine or readiness claim.
 */
async function borrowOriginalChannelTransport(realm, signal, assertOwned) {
    const fail = () => new Error('wwebjs_original_transport_unavailable');
    if (
        typeof realm?.evaluateHandle !== 'function' ||
        !(signal instanceof AbortSignal) ||
        typeof assertOwned !== 'function'
    )
        throw fail();

    const lifetime = new AbortController();
    let handle;
    let stopped = false;
    let disposing = Promise.resolve();
    const dispose = (value) => {
        if (!value) return;
        disposing = Promise.resolve()
            .then(() => value.evaluate((source) => source.close()))
            .catch(() => {})
            .finally(() => value.dispose().catch(() => {}));
    };
    const close = () => {
        if (stopped) return;
        stopped = true;
        signal.removeEventListener('abort', close);
        lifetime.abort();
        dispose(handle);
    };
    const check = () => {
        try {
            if (stopped || signal.aborted) throw fail();
            assertOwned();
        } catch {
            close();
            throw fail();
        }
    };
    signal.addEventListener('abort', close, { once: true });
    try {
        check();
        // Exactly once. A late handle is retired in its own original realm;
        // acquisition is never retried in another page or replacement worker.
        handle = await realm.evaluateHandle(openOriginalChannelTransport);
        if (stopped) dispose(handle);
        check();
        const generation = await handle.evaluate((source) => {
            source.check();
            return source.generation;
        });
        check();
        if (
            typeof generation !== 'string' ||
            !/^[0-9.]+:[1-9][0-9]*$/.test(generation)
        )
            throw fail();

        // One pending socket event, not an HTTP loop or a status poll.
        void handle.evaluate((source) => source.closed()).then(close, close);

        const run = async (method, args, cancellation, operationId) => {
            if (!(cancellation instanceof AbortSignal)) throw fail();
            check();
            if (cancellation.aborted) throw fail();
            const combined = AbortSignal.any([lifetime.signal, cancellation]);
            let abort;
            const cancelled = new Promise((_, reject) => {
                abort = () => {
                    // Cancelling an ACK/event wait does not cancel or repeat a
                    // socket write already dispatched to the original realm.
                    if (operationId)
                        void handle
                            .evaluate(
                                (source, id) => source.cancel(id),
                                operationId,
                            )
                            .catch(close);
                    reject(fail());
                };
                combined.addEventListener('abort', abort, { once: true });
            });
            try {
                check();
                if (combined.aborted) throw fail();
                const operation = handle.evaluate(
                    (source, name, input) => {
                        source.check();
                        return source[name](...input);
                    },
                    method,
                    args,
                );
                const result = await Promise.race([operation, cancelled]);
                check();
                if (combined.aborted) throw fail();
                return result;
            } catch {
                throw fail();
            } finally {
                combined.removeEventListener('abort', abort);
            }
        };
        return Object.freeze({
            generation,
            signal: lifetime.signal,
            check,
            close,
            disposed: () => disposing,
            send: (node, cancellation) => run('send', [node], cancellation),
            request: (node, id, cancellation) =>
                run('request', [node, id], cancellation, id),
            next: (cancellation) => {
                const id = randomUUID();
                return run('next', [id], cancellation, id);
            },
        });
    } catch {
        close();
        throw fail();
    }
}

module.exports = { borrowOriginalChannelTransport };