"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Connection = void 0;
const ws_1 = __importDefault(require("ws"));
const exception_js_1 = require("../exception.js");
const target_js_1 = require("./target.js");
class Connection {
    constructor(browser) {
        this.sessions = new Map();
        this.targetSessions = new Map();
        this.ws = null;
        this.awaitingCommands = new Map();
        this.nextCommandId = 1;
        this.bytesSent = 0;
        this.bytesReceived = 0;
        this.listeners = {
            onMessage: this.onMessage.bind(this),
            onClose: this.onClose.bind(this),
        };
        this.browser = browser;
        browser.on('global:Target.targetCreated', ev => this.onTargetCreated(ev));
        browser.on('global:Target.targetDestroyed', ev => this.onTargetDestroyed(ev));
        browser.on('global:Target.attachedToTarget', ev => this.onAttachedToTarget(ev));
        browser.on('global:Target.detachedFromTarget', ev => this.onDetachedFromTarget(ev));
        browser.on('global:Target.targetInfoChanged', ev => this.onTargetInfoChanged(ev));
    }
    isConnected() {
        return this.ws != null;
    }
    async connect(webSocketDebuggerUrl) {
        if (this.ws) {
            return;
        }
        await new Promise((resolve, reject) => {
            const ws = new ws_1.default(webSocketDebuggerUrl, {
                perMessageDeflate: false,
            });
            ws.on('open', () => {
                this.attachWebSocket(ws);
                resolve();
            });
            ws.on('error', reject);
        });
    }
    attachWebSocket(ws) {
        if (this.ws) {
            return;
        }
        ws.addListener('message', this.listeners.onMessage);
        ws.addListener('close', this.listeners.onClose);
        this.ws = ws;
        this.browser.emit('connect');
    }
    detachWebSocket() {
        const { ws } = this;
        if (!ws) {
            return;
        }
        ws.removeListener('message', this.listeners.onMessage);
        ws.removeListener('close', this.listeners.onClose);
        this.ws = null;
        this.bytesReceived = 0;
        this.bytesSent = 0;
        this.browser.emit('disconnect');
        this.rejectAll(cmd => {
            return new exception_js_1.Exception({
                name: 'CdpDisconnected',
                message: 'CDP: connection to browser endpoint lost',
                retry: true,
                details: { method: cmd.method, params: cmd.params },
            });
        });
        this.awaitingCommands.clear();
        this.sessions.clear();
        this.targetSessions.clear();
    }
    disconnect() {
        if (!this.ws) {
            return;
        }
        this.ws.close();
    }
    rejectAllForTarget(sessionId, errFn) {
        for (const handler of this.awaitingCommands.values()) {
            if (handler.sessionId === sessionId) {
                const err = errFn(handler);
                handler.reject(err);
            }
        }
    }
    rejectAll(errFn) {
        for (const handler of this.awaitingCommands.values()) {
            const err = errFn(handler);
            handler.reject(err);
        }
    }
    getTarget(sessionId) {
        if (!this.sessions.has(sessionId)) {
            throw new exception_js_1.Exception({
                name: 'CdpInvalidTarget',
                message: 'CDP: invalid target requested (most likely it is destroyed)',
                retry: true,
                details: {
                    sessionId,
                },
            });
        }
        return this.sessions.get(sessionId);
    }
    async refreshTargets() {
        const res = await this.send({ method: 'Target.getTargets', params: {} });
        const targets = res.targetInfos;
        const promises = targets.map(targetInfo => this.attachIfNeeded(targetInfo));
        await Promise.all(promises);
    }
    findTargetById(targetId) {
        const sessionId = this.targetSessions.get(targetId);
        const target = this.sessions.get(sessionId ?? '') ?? null;
        return target;
    }
    attachIfNeeded({ targetId, type }) {
        if (this.targetSessions.has(targetId) || type === 'background_page' || type === 'service_worker') {
            return;
        }
        this.sendAndForget({
            method: 'Target.attachToTarget',
            params: { targetId, flatten: true }
        });
    }
    addSession(sessionId, targetInfo) {
        const target = new target_js_1.Target(this.browser, sessionId, targetInfo);
        this.sessions.set(sessionId, target);
        this.targetSessions.set(targetInfo.targetId, sessionId);
        this.browser.emit('targetAttached', target);
    }
    removeSession(sessionId) {
        this.rejectAllForTarget(sessionId, cmd => {
            const { method, params } = cmd;
            return new exception_js_1.Exception({
                name: 'CdpTargetDetached',
                message: 'CDP: detached from target',
                retry: true,
                details: { method, params }
            });
        });
        const target = this.sessions.get(sessionId);
        if (target) {
            this.sessions.delete(sessionId);
            this.targetSessions.delete(target.targetId);
            this.browser.emit('targetDetached', target);
        }
    }
    sendAndForget(payload) {
        if (this.ws) {
            const { method, params, sessionId } = payload;
            const id = this.nextCommandId;
            this.nextCommandId += 1;
            const message = { id, method, params, sessionId };
            this.ws.send(JSON.stringify(message));
        }
    }
    async send(command) {
        const { method, params, sessionId, timeout = this.browser.config.cdpTimeout } = command;
        return new Promise((resolve, reject) => {
            if (!this.ws) {
                throw new exception_js_1.Exception({
                    name: 'CdpNotConnected',
                    message: 'CDP: not connected to browser endpoint',
                    retry: true,
                    details: { method, params },
                });
            }
            if (this.ws.readyState > 1) {
                this.onClose();
                throw new exception_js_1.Exception({
                    name: 'CdpDisconnected',
                    message: 'CDP: connection to browser endpoint lost',
                    retry: true,
                    details: { method, params },
                });
            }
            const { awaitingCommands } = this;
            const id = this.nextCommandId;
            this.nextCommandId += 1;
            const timer = setTimeout(() => {
                const err = new exception_js_1.Exception({
                    name: 'CdpTimeout',
                    message: 'CDP: timeout while waiting for command result',
                    retry: false,
                    details: { method, params },
                });
                handler.reject(err);
            }, timeout);
            const handler = {
                id,
                method,
                params,
                sessionId,
                resolve(result) {
                    awaitingCommands.delete(id);
                    clearTimeout(timer);
                    resolve(result);
                },
                reject(err) {
                    awaitingCommands.delete(id);
                    clearTimeout(timer);
                    reject(err);
                },
            };
            awaitingCommands.set(id, handler);
            const message = { id, method, params, sessionId };
            const payload = JSON.stringify(message);
            this.bytesSent += Buffer.byteLength(payload);
            this.ws.send(payload);
        });
    }
    attachedTargets() {
        return this.sessions.values();
    }
    onClose() {
        this.detachWebSocket();
    }
    onMessage(data) {
        this.bytesReceived += Buffer.byteLength(data);
        const message = JSON.parse(data);
        if (message.id) {
            const handler = this.awaitingCommands.get(message.id);
            if (!handler) {
                return;
            }
            if (message.error) {
                const { method, params } = handler;
                handler.reject(new exception_js_1.Exception({
                    name: 'CdpCommandFailed',
                    message: `CDP command failed: ${method}: ${message.error.message}`,
                    retry: true,
                    details: { method, params },
                }));
            }
            else {
                handler.resolve(message.result);
            }
        }
        else {
            const { method, params, sessionId } = message;
            const domain = method.replace(/\..*/, '');
            this.browser.emit('global:cdp', message);
            this.browser.emit('global:cdp.' + domain, message);
            this.browser.emit('global:' + method, params);
            if (sessionId) {
                const target = this.sessions.get(sessionId);
                if (target) {
                    target.emit('cdp', message);
                    target.emit('cdp.' + domain, message);
                    target.emit(method, params);
                }
            }
            else {
                this.browser.emit('cdp', message);
                this.browser.emit('cdp.' + domain, message);
                this.browser.emit(method, params);
            }
        }
    }
    onTargetCreated(params) {
        this.attachIfNeeded(params.targetInfo);
    }
    onTargetDestroyed(params) {
        const { targetId } = params;
        const target = this.findTargetById(targetId);
        if (target) {
            this.removeSession(target.sessionId);
        }
    }
    onAttachedToTarget(params) {
        const { sessionId, targetInfo } = params;
        if (!['iframe', 'page'].includes(targetInfo.type)) {
            this.send({
                method: 'Runtime.runIfWaitingForDebugger',
                params: {}
            }).catch(() => { });
            this.send({
                method: 'Target.detachFromTarget',
                params: {
                    sessionId,
                    targetId: targetInfo.targetId,
                }
            }).catch(() => { });
        }
        this.addSession(sessionId, targetInfo);
    }
    onDetachedFromTarget(params) {
        const { sessionId } = params;
        this.removeSession(sessionId);
    }
    onTargetInfoChanged(params) {
        const { targetId } = params.targetInfo;
        const target = this.findTargetById(targetId);
        if (target) {
            target.onInfoChanged(params.targetInfo);
        }
    }
}
exports.Connection = Connection;
//# sourceMappingURL=connection.js.map