{"version":3,"sources":["src/common.browser/WebsocketMessageAdapter.ts"],"names":[],"mappings":"AAaA,OAAO,EAMH,eAAe,EACf,iBAAiB,EAGjB,sBAAsB,EAGtB,eAAe,EAGf,WAAW,EACX,0BAA0B,EAI7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAQ3C,qBAAa,uBAAuB;IAChC,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,oBAAoB,CAA6B;IACzD,OAAO,CAAC,mBAAmB,CAAY;IAEvC,OAAO,CAAC,oBAAoB,CAAmB;IAC/C,OAAO,CAAC,yBAAyB,CAA2B;IAC5D,OAAO,CAAC,+BAA+B,CAAmC;IAC1E,OAAO,CAAC,gCAAgC,CAAiB;IACzD,OAAO,CAAC,sBAAsB,CAAiB;IAC/C,OAAO,CAAC,oBAAoB,CAA+B;IAC3D,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,OAAO,CAAS;IAExB,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,qBAAqB,CAAU;IAEvC,OAAc,iBAAiB,EAAE,OAAO,CAAS;gBAG7C,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,0BAA0B,EAC5C,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,EAClC,iBAAiB,EAAE,OAAO;IA0B9B,IAAW,KAAK,IAAI,eAAe,CAElC;IAEM,IAAI,IAAI,OAAO,CAAC,sBAAsB,CAAC;IAkIvC,IAAI,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB/C,IAAI,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAQlC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY5C,IAAW,MAAM,IAAI,WAAW,CAAC,eAAe,CAAC,CAEhD;IAED,OAAO,CAAC,cAAc;YAuBR,OAAO;YAcP,gBAAgB;IAkB9B,OAAO,CAAC,OAAO;IAuCf,OAAO,KAAK,eAAe,GAE1B;CAEJ","file":"WebsocketMessageAdapter.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\n// Licensed under the MIT license.\n\n// Node.JS specific web socket / browser support.\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// import * as http from \"http\";\n// import * as net from \"net\";\n// import * as tls from \"tls\";\n// import * as https from \"https-proxy-agent\";\n// import Agent from \"agent-base\";\n\n// import ws from \"ws\";\nimport { HeaderNames } from \"../common.speech/HeaderNames.js\";\nimport {\n    ArgumentNullError,\n    BackgroundEvent,\n    ConnectionClosedEvent,\n    ConnectionErrorEvent,\n    ConnectionEstablishedEvent,\n    ConnectionEvent,\n    ConnectionMessage,\n    ConnectionMessageReceivedEvent,\n    ConnectionMessageSentEvent,\n    ConnectionOpenResponse,\n    ConnectionRedirectEvent,\n    ConnectionStartEvent,\n    ConnectionState,\n    Deferred,\n    Events,\n    EventSource,\n    IWebsocketMessageFormatter,\n    MessageType,\n    Queue,\n    RawWebsocketMessage,\n} from \"../common/Exports.js\";\nimport { ProxyInfo } from \"./ProxyInfo.js\";\n\ninterface ISendItem {\n    Message: ConnectionMessage;\n    RawWebsocketMessage: RawWebsocketMessage;\n    sendStatusDeferral: Deferred<void>;\n}\n\nexport class WebsocketMessageAdapter {\n    private privConnectionState: ConnectionState;\n    private privMessageFormatter: IWebsocketMessageFormatter;\n    private privWebsocketClient: WebSocket;\n\n    private privSendMessageQueue: Queue<ISendItem>;\n    private privReceivingMessageQueue: Queue<ConnectionMessage>;\n    private privConnectionEstablishDeferral: Deferred<ConnectionOpenResponse>;\n    private privCertificateValidatedDeferral: Deferred<void>;\n    private privDisconnectDeferral: Deferred<void>;\n    private privConnectionEvents: EventSource<ConnectionEvent>;\n    private privConnectionId: string;\n    private privUri: string;\n    // private proxyInfo: ProxyInfo;\n    private privHeaders: { [key: string]: string };\n    private privLastErrorReceived: string;\n    private privEnableCompression: boolean;\n\n    public static forceNpmWebSocket: boolean = false;\n\n    public constructor(\n        uri: string,\n        connectionId: string,\n        messageFormatter: IWebsocketMessageFormatter,\n        proxyInfo: ProxyInfo,\n        headers: { [key: string]: string },\n        enableCompression: boolean) {\n\n        if (!uri) {\n            throw new ArgumentNullError(\"uri\");\n        }\n\n        if (!messageFormatter) {\n            throw new ArgumentNullError(\"messageFormatter\");\n        }\n\n        // this.proxyInfo = proxyInfo;\n        this.privConnectionEvents = new EventSource<ConnectionEvent>();\n        this.privConnectionId = connectionId;\n        this.privMessageFormatter = messageFormatter;\n        this.privConnectionState = ConnectionState.None;\n        this.privUri = uri;\n        this.privHeaders = headers;\n        this.privEnableCompression = enableCompression;\n\n        // Add the connection ID to the headers\n        this.privHeaders[HeaderNames.ConnectionId] = this.privConnectionId;\n        this.privHeaders.connectionId = this.privConnectionId;\n\n        this.privLastErrorReceived = \"\";\n    }\n\n    public get state(): ConnectionState {\n        return this.privConnectionState;\n    }\n\n    public open(): Promise<ConnectionOpenResponse> {\n        if (this.privConnectionState === ConnectionState.Disconnected) {\n            return Promise.reject<ConnectionOpenResponse>(`Cannot open a connection that is in ${this.privConnectionState} state`);\n        }\n\n        if (this.privConnectionEstablishDeferral) {\n            return this.privConnectionEstablishDeferral.promise;\n        }\n\n        this.privConnectionEstablishDeferral = new Deferred<ConnectionOpenResponse>();\n        this.privCertificateValidatedDeferral = new Deferred<void>();\n\n        this.privConnectionState = ConnectionState.Connecting;\n\n        try {\n\n            // if (typeof WebSocket !== \"undefined\" && !WebsocketMessageAdapter.forceNpmWebSocket) {\n                // Browser handles cert checks.\n                this.privCertificateValidatedDeferral.resolve();\n\n                this.privWebsocketClient = new WebSocket(this.privUri);\n            // } else {\n            //     // Workaround for https://github.com/microsoft/cognitive-services-speech-sdk-js/issues/465\n            //     // Which is root caused by https://github.com/TooTallNate/node-agent-base/issues/61\n            //     const uri = new URL(this.privUri);\n            //     let protocol: string = uri.protocol;\n\n            //     if (protocol?.toLocaleLowerCase() === \"wss:\") {\n            //         protocol = \"https:\";\n            //     } else if (protocol?.toLocaleLowerCase() === \"ws:\") {\n            //         protocol = \"http:\";\n            //     }\n\n            //     const options: ws.ClientOptions = { headers: this.privHeaders, perMessageDeflate: this.privEnableCompression, followRedirects: protocol.toLocaleLowerCase() === \"https:\" };\n            //     // The ocsp library will handle validation for us and fail the connection if needed.\n            //     this.privCertificateValidatedDeferral.resolve();\n\n            //     options.agent = this.getAgent();\n\n            //     // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n            //     (options.agent as any).protocol = protocol;\n            //     this.privWebsocketClient = new ws(this.privUri, options);\n            //     this.privWebsocketClient.on(\"redirect\", (redirectUrl: string): void => {\n            //         const event: ConnectionRedirectEvent = new ConnectionRedirectEvent(this.privConnectionId, redirectUrl, this.privUri, `Getting redirect URL from endpoint ${this.privUri} with redirect URL '${redirectUrl}'`);\n            //         Events.instance.onEvent(event);\n            //     });\n            // }\n\n            this.privWebsocketClient.binaryType = \"arraybuffer\";\n            this.privReceivingMessageQueue = new Queue<ConnectionMessage>();\n            this.privDisconnectDeferral = new Deferred<void>();\n            this.privSendMessageQueue = new Queue<ISendItem>();\n            this.processSendQueue().catch((reason: string): void => {\n                Events.instance.onEvent(new BackgroundEvent(reason));\n            });\n        } catch (error) {\n            this.privConnectionEstablishDeferral.resolve(new ConnectionOpenResponse(500, error as string));\n            return this.privConnectionEstablishDeferral.promise;\n        }\n\n        this.onEvent(new ConnectionStartEvent(this.privConnectionId, this.privUri));\n\n        this.privWebsocketClient.onopen = (): void => {\n            this.privCertificateValidatedDeferral.promise.then((): void => {\n                this.privConnectionState = ConnectionState.Connected;\n                this.onEvent(new ConnectionEstablishedEvent(this.privConnectionId));\n                this.privConnectionEstablishDeferral.resolve(new ConnectionOpenResponse(200, \"\"));\n            }, (error: string): void => {\n                this.privConnectionEstablishDeferral.reject(error);\n            });\n        };\n\n        this.privWebsocketClient.onerror = (e: any): void => {\n            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument\n            this.onEvent(new ConnectionErrorEvent(this.privConnectionId, e.message, e.type));\n            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment\n            this.privLastErrorReceived = e.message;\n        };\n\n        this.privWebsocketClient.onclose = (e: { wasClean: boolean; code: number; reason: string; target: any }): void => {\n            if (this.privConnectionState === ConnectionState.Connecting) {\n                this.privConnectionState = ConnectionState.Disconnected;\n                // this.onEvent(new ConnectionEstablishErrorEvent(this.connectionId, e.code, e.reason));\n                this.privConnectionEstablishDeferral.resolve(new ConnectionOpenResponse(e.code, e.reason + \" \" + this.privLastErrorReceived));\n            } else {\n                this.privConnectionState = ConnectionState.Disconnected;\n                this.privWebsocketClient = null;\n                this.onEvent(new ConnectionClosedEvent(this.privConnectionId, e.code, e.reason));\n            }\n\n            this.onClose(e.code, e.reason).catch((reason: string): void => {\n                Events.instance.onEvent(new BackgroundEvent(reason));\n            });\n        };\n\n        this.privWebsocketClient.onmessage = (e: { data: any; type: string; target: any }): void => {\n            const networkReceivedTime = new Date().toISOString();\n            if (this.privConnectionState === ConnectionState.Connected) {\n                const deferred = new Deferred<ConnectionMessage>();\n                // let id = ++this.idCounter;\n                this.privReceivingMessageQueue.enqueueFromPromise(deferred.promise);\n                if (e.data instanceof ArrayBuffer) {\n                    const rawMessage = new RawWebsocketMessage(MessageType.Binary, e.data);\n                    this.privMessageFormatter\n                        .toConnectionMessage(rawMessage)\n                        .then((connectionMessage: ConnectionMessage): void => {\n                            this.onEvent(new ConnectionMessageReceivedEvent(this.privConnectionId, networkReceivedTime, connectionMessage));\n                            deferred.resolve(connectionMessage);\n                        }, (error: string): void => {\n                            // TODO: Events for these ?\n                            deferred.reject(`Invalid binary message format. Error: ${error}`);\n                        });\n                } else {\n                    const rawMessage = new RawWebsocketMessage(MessageType.Text, e.data);\n                    this.privMessageFormatter\n                        .toConnectionMessage(rawMessage)\n                        .then((connectionMessage: ConnectionMessage): void => {\n                            this.onEvent(new ConnectionMessageReceivedEvent(this.privConnectionId, networkReceivedTime, connectionMessage));\n                            deferred.resolve(connectionMessage);\n                        }, (error: string): void => {\n                            // TODO: Events for these ?\n                            deferred.reject(`Invalid text message format. Error: ${error}`);\n                        });\n                }\n            }\n        };\n\n        return this.privConnectionEstablishDeferral.promise;\n    }\n\n    public send(message: ConnectionMessage): Promise<void> {\n        if (this.privConnectionState !== ConnectionState.Connected) {\n            return Promise.reject(`Cannot send on connection that is in ${ConnectionState[this.privConnectionState]} state`);\n        }\n\n        const messageSendStatusDeferral = new Deferred<void>();\n        const messageSendDeferral = new Deferred<ISendItem>();\n\n        this.privSendMessageQueue.enqueueFromPromise(messageSendDeferral.promise);\n\n        this.privMessageFormatter\n            .fromConnectionMessage(message)\n            .then((rawMessage: RawWebsocketMessage): void => {\n                messageSendDeferral.resolve({\n                    Message: message,\n                    RawWebsocketMessage: rawMessage,\n                    sendStatusDeferral: messageSendStatusDeferral,\n                });\n            }, (error: string): void => {\n                messageSendDeferral.reject(`Error formatting the message. ${error}`);\n            });\n\n        return messageSendStatusDeferral.promise;\n    }\n\n    public read(): Promise<ConnectionMessage> {\n        if (this.privConnectionState !== ConnectionState.Connected) {\n            return Promise.reject<ConnectionMessage>(`Cannot read on connection that is in ${this.privConnectionState} state`);\n        }\n\n        return this.privReceivingMessageQueue.dequeue();\n    }\n\n    public close(reason?: string): Promise<void> {\n        if (this.privWebsocketClient) {\n            if (this.privConnectionState !== ConnectionState.Disconnected) {\n                this.privWebsocketClient.close(1000, reason ? reason : \"Normal closure by client\");\n            }\n        } else {\n            return Promise.resolve();\n        }\n\n        return this.privDisconnectDeferral.promise;\n    }\n\n    public get events(): EventSource<ConnectionEvent> {\n        return this.privConnectionEvents;\n    }\n\n    private sendRawMessage(sendItem: ISendItem): Promise<void> {\n        try {\n            // indicates we are draining the queue and it came with no message;\n            if (!sendItem) {\n                return Promise.resolve();\n            }\n\n            this.onEvent(new ConnectionMessageSentEvent(this.privConnectionId, new Date().toISOString(), sendItem.Message));\n\n            // add a check for the ws readystate in order to stop the red console error 'WebSocket is already in CLOSING or CLOSED state' appearing\n            if (this.isWebsocketOpen) {\n                // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n                this.privWebsocketClient.send(sendItem.RawWebsocketMessage.payload);\n            } else {\n                return Promise.reject(\"websocket send error: Websocket not ready \" + this.privConnectionId + \" \" + sendItem.Message.id + \" \" + new Error().stack);\n            }\n            return Promise.resolve();\n\n        } catch (e) {\n            return Promise.reject(`websocket send error: ${e as string}`);\n        }\n    }\n\n    private async onClose(code: number, reason: string): Promise<void> {\n        const closeReason = `Connection closed. ${code}: ${reason}`;\n        this.privConnectionState = ConnectionState.Disconnected;\n        this.privDisconnectDeferral.resolve();\n        await this.privReceivingMessageQueue.drainAndDispose((): void => {\n            // TODO: Events for these ?\n            // Logger.instance.onEvent(new LoggingEvent(LogType.Warning, null, `Failed to process received message. Reason: ${closeReason}, Message: ${JSON.stringify(pendingReceiveItem)}`));\n        }, closeReason);\n\n        await this.privSendMessageQueue.drainAndDispose((pendingSendItem: ISendItem): void => {\n            pendingSendItem.sendStatusDeferral.reject(closeReason);\n        }, closeReason);\n    }\n\n    private async processSendQueue(): Promise<void> {\n        while (true) {\n            const itemToSend: Promise<ISendItem> = this.privSendMessageQueue.dequeue();\n            const sendItem: ISendItem = await itemToSend;\n            // indicates we are draining the queue and it came with no message;\n            if (!sendItem) {\n                return;\n            }\n\n            try {\n                await this.sendRawMessage(sendItem);\n                sendItem.sendStatusDeferral.resolve();\n            } catch (sendError) {\n                sendItem.sendStatusDeferral.reject(sendError as string);\n            }\n        }\n    }\n\n    private onEvent(event: ConnectionEvent): void {\n        this.privConnectionEvents.onEvent(event);\n        Events.instance.onEvent(event);\n    }\n\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    // private getAgent(): http.Agent {\n    //     // eslint-disable-next-line @typescript-eslint/unbound-method\n    //     const agent: { proxyInfo: ProxyInfo } = new Agent.Agent(this.createConnection) as unknown as { proxyInfo: ProxyInfo };\n\n    //     // if (this.proxyInfo !== undefined &&\n    //     //     this.proxyInfo.HostName !== undefined &&\n    //     //     this.proxyInfo.Port > 0) {\n    //     //     agent.proxyInfo = this.proxyInfo;\n    //     // }\n\n    //     return agent as unknown as http.Agent;\n    // }\n\n    // private createConnection(request: Agent.ClientRequest, options: Agent.RequestOptions): Promise<net.Socket> {\n    //     let socketPromise: Promise<net.Socket>;\n\n    //     options = {\n    //         ...options,\n    //         ...{\n    //             requestOCSP: true,\n    //             servername: options.host\n    //         }\n    //     };\n\n    //     if (!!options.secureEndpoint) {\n    //         socketPromise = Promise.resolve(tls.connect(options));\n    //     } else {\n    //         socketPromise = Promise.resolve(net.connect(options));\n    //     }\n\n    //     return socketPromise;\n    // }\n\n    private get isWebsocketOpen(): boolean {\n        return this.privWebsocketClient && this.privWebsocketClient.readyState === this.privWebsocketClient.OPEN;\n    }\n\n}\n"]}