"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SocketServer = void 0;
const ws_1 = require("ws");
const types_1 = require("tdf-devtools-protocol/dist/types");
const aegis_1 = require("@/utils/aegis");
const enum_1 = require("@/@types/enum");
const db_1 = require("@/db");
const client_1 = require("@/client");
const debug_targets_1 = require("@/controller/debug-targets");
const pub_sub_manager_1 = require("@/controller/pub-sub-manager");
const log_1 = require("@/utils/log");
const pub_sub_channel_1 = require("@/utils/pub-sub-channel");
const url_1 = require("@/utils/url");
const debug_target_1 = require("@/utils/debug-target");
const config_1 = require("@/config");
const hmr_1 = require("@/controller/hmr");
const reload_adapter_1 = require("@/utils/reload-adapter");
const vue_devtools_1 = require("@/controller/vue-devtools");
const heartbeatInterval = 30000;
// 断开连接后不再发送调试指令，不会出现 id 混乱，所以 command id 可以 mock 一个
const mockCmdId = -100000;
const resumeCommands = [
    {
        id: mockCmdId,
        method: types_1.TdfCommand.TDFRuntimeResume,
        params: {},
        performance: aegis_1.createCDPPerformance(),
    },
    {
        id: mockCmdId - 1,
        method: types_1.ChromeCommand.DebuggerDisable,
        params: {},
        performance: aegis_1.createCDPPerformance(),
    },
    {
        id: mockCmdId - 2,
        method: types_1.ChromeCommand.RuntimeDisable,
        params: {},
        performance: aegis_1.createCDPPerformance(),
    },
];
const log = new log_1.Logger('socket-server', enum_1.WinstonColor.Cyan);
/**
 * ws 调试服务，支持远程调试（无线模式，使用 ws 通道）、本地调试（有线模式，使用 tunnel 数据通道）
 * 通过 redis pub/sub 实现多点部署时的消息分发，上下行消息根据 clientId 来建立 redis channel 进行分发
 * 本地安装 npm 包部署时（单点部署），不存储 redis，而是直接保存在内存中
 *
 * 设计方案： https://iwiki.woa.com/pages/viewpage.action?pageId=1222336167
 */
class SocketServer {
    constructor(server) {
        this.server = server;
    }
    /**
     * 开启调试服务
     */
    start() {
        const wss = new ws_1.Server({
            noServer: true,
            path: config_1.config.wsPath,
        });
        this.wss = wss;
        this.server.on('upgrade', this.onUpgrade.bind(this));
        wss.on('connection', this.onConnection.bind(this));
        wss.on('error', (e) => {
            var _a;
            log.error('wss error: %s', (_a = e) === null || _a === void 0 ? void 0 : _a.stack);
        });
        wss.on('upgrade', (response) => {
            log.info('wss upgrade: %j', response);
        });
        wss.on('close', () => {
            clearInterval(this.interval);
        });
        this.interval = setInterval(() => {
            this.wss.clients.forEach((client) => {
                if (client.isAlive === false) {
                    client.terminate();
                    return;
                }
                client.isAlive = false;
                client.ping(() => { });
            });
        }, heartbeatInterval);
    }
    /**
     * 关闭调试服务，并清理当前节点连接的调试对象缓存
     */
    async close() {
        await pub_sub_manager_1.cleanAllDebugTargets();
        this.wss.close(() => {
            log.warn('wss closed.');
        });
    }
    onUpgrade(req, socket, head) {
        log.info('onUpgrade, ws url: %s', req.url);
        const wsUrlParams = url_1.parseWsUrl(req.url);
        const reason = url_1.getWsInvalidReason(wsUrlParams);
        if (reason) {
            log.warn('onUpgrade error: %s', reason);
            return socket.destroy();
        }
        this.wss.handleUpgrade(req, socket, head, (ws) => {
            this.wss.emit('connection', ws, req);
        });
    }
    /**
     * ⚠️ 给 ws 添加事件监听前，不要执行异步操作，否则会遗漏消息
     * 事件监听后，再异步判断是否合法，不合法则关闭
     */
    async onConnection(ws, req) {
        const wsUrlParams = url_1.parseWsUrl(req.url);
        const host = (req.headers.host || req.headers.Host) || '';
        ws.isAlive = true;
        ws.on('pong', () => {
            ws.isAlive = true;
        });
        const { clientRole } = wsUrlParams;
        if ([enum_1.ClientRole.JSRuntime, enum_1.ClientRole.VueDevtools].includes(clientRole)) {
            vue_devtools_1.onVueClientConnection(ws, wsUrlParams);
            return;
        }
        if (clientRole === enum_1.ClientRole.HMRClient) {
            hmr_1.onHMRClientConnection(ws, wsUrlParams);
        }
        else if (clientRole === enum_1.ClientRole.HMRServer) {
            hmr_1.onHMRServerConnection(ws, wsUrlParams);
        }
        else if (clientRole === enum_1.ClientRole.Devtools) {
            this.onDevtoolsConnection(ws, wsUrlParams);
        }
        else {
            this.onAppConnection(ws, wsUrlParams, host);
        }
        if (clientRole === enum_1.ClientRole.Devtools) {
            const params = wsUrlParams;
            const exist = await this.checkDebugTargetExist(params);
            if (!exist) {
                const reason = `debugTarget not exist! ${params.clientId}`;
                log.warn(reason);
                ws.close(enum_1.WSCode.NoDebugTarget, reason);
            }
        }
    }
    async checkDebugTargetExist(wsUrlParams) {
        const { clientId, hash } = wsUrlParams;
        const debugTarget = await debug_targets_1.DebugTargetManager.findDebugTarget(clientId, hash);
        log.verbose('checkDebugTargetExist debug target: %j', debugTarget);
        return Boolean(debugTarget);
    }
    /**
     * pipe devtools ws message to redis pub/sub
     */
    onDevtoolsConnection(ws, wsUrlParams) {
        const { Subscriber, Publisher } = db_1.getDBOperator();
        const { extensionName, clientId } = wsUrlParams;
        const downwardChannelId = pub_sub_channel_1.createDownwardChannel(clientId, extensionName);
        const upwardChannelId = pub_sub_channel_1.createUpwardChannel(clientId, extensionName);
        const internalChannelId = pub_sub_channel_1.createInternalChannel(clientId, '');
        log.verbose('devtools connected, subscribe channel: %s, publish channel: %s', downwardChannelId, upwardChannelId);
        const downwardSubscriber = new Subscriber(downwardChannelId);
        // internal channel used to listen message between nodes, such as when app ws closed, notify devtools ws close
        const internalSubscriber = new Subscriber(internalChannelId);
        const internalPublisher = new Publisher(internalChannelId);
        const publisher = new Publisher(upwardChannelId);
        const downwardHandler = (msg) => {
            ws.send(msg);
            try {
                const msgStr = msg;
                const msgObj = JSON.parse(msgStr);
                const { ts: start } = msgObj;
                if (start) {
                    aegis_1.aegis.reportTime({
                        name: enum_1.ReportEvent.PubSub,
                        duration: Date.now() - start,
                        ext1: `${Math.ceil(msgStr.length / 1024)}KB`,
                        ext2: msgObj.method,
                    });
                }
            }
            catch (e) {
                log.error('%s channel message are invalid JSON, %s', downwardChannelId, msg);
            }
        };
        const internalHandler = (msg) => {
            if (msg === enum_1.InternalChannelEvent.AppWSClose) {
                log.warn('close devtools ws connection');
                ws.close(enum_1.WSCode.ClosePage, 'the target page is closed');
                internalSubscriber.unsubscribe(internalHandler);
                internalSubscriber.disconnect();
            }
        };
        downwardSubscriber.subscribe(downwardHandler);
        internalSubscriber.subscribe(internalHandler);
        internalPublisher.publish(enum_1.InternalChannelEvent.DevtoolsConnected);
        // for iOS, must invoke Debugger.disable before devtools frontend connected, otherwise couldn't
        // receive Debugger.scriptParsed event.
        // for Android, both way is okay
        resumeCommands.map(publisher.publish.bind(publisher));
        ws.on('message', (msg) => {
            publisher.publish(msg.toString());
        });
        ws.on('close', (code, reason) => {
            log.warn('devtools ws client close code %s, reason: %s, clientId: %s', code, reason, clientId);
            resumeCommands.map(publisher.publish.bind(publisher));
            // wait for publish finished
            process.nextTick(() => {
                publisher.disconnect();
                downwardSubscriber.unsubscribe(downwardHandler);
                downwardSubscriber.disconnect();
            });
        });
        ws.on('error', (e) => log.error('devtools ws client error: %j', e));
    }
    /**
     * app ws 连接，创建调试对象，并订阅上行调试消息
     */
    async onAppConnection(ws, wsUrlParams, host) {
        const { clientId, clientRole } = wsUrlParams;
        log.verbose('WSAppClient connected. %s', clientId);
        const platform = {
            [enum_1.ClientRole.Android]: enum_1.DevicePlatform.Android,
            [enum_1.ClientRole.IOS]: enum_1.DevicePlatform.IOS,
        }[clientRole];
        const useWS = client_1.appClientManager.shouldUseAppClientType(platform, "WSAppClient" /* WS */);
        if (!useWS)
            return log.warn('current env is %s, ignore ws connection', global.debugAppArgv.env);
        let debugTarget = debug_target_1.createTargetByWsUrlParams(wsUrlParams, host);
        // app ws 添加监听前可以执行异步操作，因为 app 建立连接后不会主动发送任何消息
        debugTarget = await debug_target_1.patchRefAndSave(debugTarget);
        process.nextTick(() => {
            pub_sub_manager_1.subscribeCommand(debugTarget, ws);
        });
        // when reload, iOS will create a new JSContext, so the debug protocol should resend
        //
        if (debugTarget.platform === enum_1.DevicePlatform.IOS)
            reload_adapter_1.publishReloadCommand(debugTarget);
        ws.on('close', (code, reason) => {
            log.warn('WSAppClient closed: %j, reason: %s, clientId: %s', code, reason, clientId);
            // when reload page, keep frontend open to debug lifecycle of created
            const closeDevtools = code === enum_1.WSCode.ClosePage;
            pub_sub_manager_1.cleanDebugTarget(clientId, closeDevtools);
        });
        ws.on('error', (e) => log.error('WSAppClient error %j', e));
    }
}
exports.SocketServer = SocketServer;
