{"version":3,"sources":["src/common.browser/WebsocketMessageAdapter.ts"],"names":[],"mappings":"AAIA,OAAO,EAMH,eAAe,EACf,iBAAiB,EAGjB,sBAAsB,EAEtB,eAAe,EAGf,WAAW,EACX,0BAA0B,EAI7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAYxC,qBAAa,uBAAuB;IAChC,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,oBAAoB,CAA6B;IACzD,OAAO,CAAC,mBAAmB,CAAiB;IAE5C,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;IACxB,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,WAAW,CAA6B;IAChD,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,CAAC;KAAE,EACnC,iBAAiB,EAAE,OAAO;IAyB9B,IAAW,KAAK,IAAI,eAAe,CAElC;IAEM,IAAI,QAAO,OAAO,CAAC,sBAAsB,CAAC,CA6GhD;IAEM,IAAI,YAAa,iBAAiB,KAAG,OAAO,CAAC,IAAI,CAAC,CAuBxD;IAEM,IAAI,QAAO,OAAO,CAAC,iBAAiB,CAAC,CAM3C;IAEM,KAAK,YAAa,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC,CAU9C;IAED,IAAW,MAAM,IAAI,WAAW,CAAC,eAAe,CAAC,CAEhD;IAED,OAAO,CAAC,cAAc,CAoBrB;YAEa,OAAO;YAcP,gBAAgB;IAkB9B,OAAO,CAAC,OAAO,CAGd;IAED,OAAO,KAAK,eAAe,GAE1B;CAEJ","file":"WebsocketMessageAdapter.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\r\n// Licensed under the MIT license.\r\n\r\nimport { HeaderNames } from \"../common.speech/HeaderNames\";\r\nimport {\r\n    ArgumentNullError,\r\n    BackgroundEvent,\r\n    ConnectionClosedEvent,\r\n    ConnectionErrorEvent,\r\n    ConnectionEstablishedEvent,\r\n    ConnectionEvent,\r\n    ConnectionMessage,\r\n    ConnectionMessageReceivedEvent,\r\n    ConnectionMessageSentEvent,\r\n    ConnectionOpenResponse,\r\n    ConnectionStartEvent,\r\n    ConnectionState,\r\n    Deferred,\r\n    Events,\r\n    EventSource,\r\n    IWebsocketMessageFormatter,\r\n    MessageType,\r\n    Queue,\r\n    RawWebsocketMessage,\r\n} from \"../common/Exports\";\r\nimport { ProxyInfo } from \"./ProxyInfo\";\r\n\r\n// Node.JS specific web socket / browser support.\r\nimport ws from \"ws\";\r\nimport { CertCheckAgent } from \"./CertChecks\";\r\n\r\ninterface ISendItem {\r\n    Message: ConnectionMessage;\r\n    RawWebsocketMessage: RawWebsocketMessage;\r\n    sendStatusDeferral: Deferred<void>;\r\n}\r\n\r\nexport class WebsocketMessageAdapter {\r\n    private privConnectionState: ConnectionState;\r\n    private privMessageFormatter: IWebsocketMessageFormatter;\r\n    private privWebsocketClient: WebSocket | ws;\r\n\r\n    private privSendMessageQueue: Queue<ISendItem>;\r\n    private privReceivingMessageQueue: Queue<ConnectionMessage>;\r\n    private privConnectionEstablishDeferral: Deferred<ConnectionOpenResponse>;\r\n    private privCertificateValidatedDeferral: Deferred<void>;\r\n    private privDisconnectDeferral: Deferred<void>;\r\n    private privConnectionEvents: EventSource<ConnectionEvent>;\r\n    private privConnectionId: string;\r\n    private privUri: string;\r\n    private proxyInfo: ProxyInfo;\r\n    private privHeaders: { [key: string]: string; };\r\n    private privLastErrorReceived: string;\r\n    private privEnableCompression: boolean;\r\n\r\n    public static forceNpmWebSocket: boolean = false;\r\n\r\n    public constructor(\r\n        uri: string,\r\n        connectionId: string,\r\n        messageFormatter: IWebsocketMessageFormatter,\r\n        proxyInfo: ProxyInfo,\r\n        headers: { [key: string]: string; },\r\n        enableCompression: boolean) {\r\n\r\n        if (!uri) {\r\n            throw new ArgumentNullError(\"uri\");\r\n        }\r\n\r\n        if (!messageFormatter) {\r\n            throw new ArgumentNullError(\"messageFormatter\");\r\n        }\r\n\r\n        this.proxyInfo = proxyInfo;\r\n        this.privConnectionEvents = new EventSource<ConnectionEvent>();\r\n        this.privConnectionId = connectionId;\r\n        this.privMessageFormatter = messageFormatter;\r\n        this.privConnectionState = ConnectionState.None;\r\n        this.privUri = uri;\r\n        this.privHeaders = headers;\r\n        this.privEnableCompression = enableCompression;\r\n\r\n        // Add the connection ID to the headers\r\n        this.privHeaders[HeaderNames.ConnectionId] = this.privConnectionId;\r\n\r\n        this.privLastErrorReceived = \"\";\r\n    }\r\n\r\n    public get state(): ConnectionState {\r\n        return this.privConnectionState;\r\n    }\r\n\r\n    public open = (): Promise<ConnectionOpenResponse> => {\r\n        if (this.privConnectionState === ConnectionState.Disconnected) {\r\n            return Promise.reject<ConnectionOpenResponse>(`Cannot open a connection that is in ${this.privConnectionState} state`);\r\n        }\r\n\r\n        if (this.privConnectionEstablishDeferral) {\r\n            return this.privConnectionEstablishDeferral.promise;\r\n        }\r\n\r\n        this.privConnectionEstablishDeferral = new Deferred<ConnectionOpenResponse>();\r\n        this.privCertificateValidatedDeferral = new Deferred<void>();\r\n\r\n        this.privConnectionState = ConnectionState.Connecting;\r\n\r\n        try {\r\n\r\n            if (typeof WebSocket !== \"undefined\" && !WebsocketMessageAdapter.forceNpmWebSocket) {\r\n                // Browser handles cert checks.\r\n                this.privCertificateValidatedDeferral.resolve();\r\n\r\n                this.privWebsocketClient = new WebSocket(this.privUri);\r\n            } else {\r\n                const options: ws.ClientOptions = { headers: this.privHeaders, perMessageDeflate: this.privEnableCompression };\r\n                // The ocsp library will handle validation for us and fail the connection if needed.\r\n                this.privCertificateValidatedDeferral.resolve();\r\n                const checkAgent: CertCheckAgent = new CertCheckAgent(this.proxyInfo);\r\n\r\n                options.agent = checkAgent.GetAgent();\r\n                this.privWebsocketClient = new ws(this.privUri, options);\r\n            }\r\n\r\n            this.privWebsocketClient.binaryType = \"arraybuffer\";\r\n            this.privReceivingMessageQueue = new Queue<ConnectionMessage>();\r\n            this.privDisconnectDeferral = new Deferred<void>();\r\n            this.privSendMessageQueue = new Queue<ISendItem>();\r\n            this.processSendQueue().catch((reason: string): void => {\r\n                Events.instance.onEvent(new BackgroundEvent(reason));\r\n            });\r\n        } catch (error) {\r\n            this.privConnectionEstablishDeferral.resolve(new ConnectionOpenResponse(500, error));\r\n            return this.privConnectionEstablishDeferral.promise;\r\n        }\r\n\r\n        this.onEvent(new ConnectionStartEvent(this.privConnectionId, this.privUri));\r\n\r\n        this.privWebsocketClient.onopen = (e: { target: WebSocket | ws }) => {\r\n            this.privCertificateValidatedDeferral.promise.then((): void => {\r\n                this.privConnectionState = ConnectionState.Connected;\r\n                this.onEvent(new ConnectionEstablishedEvent(this.privConnectionId));\r\n                this.privConnectionEstablishDeferral.resolve(new ConnectionOpenResponse(200, \"\"));\r\n            }, (error: string): void => {\r\n                this.privConnectionEstablishDeferral.reject(error);\r\n            });\r\n        };\r\n\r\n        this.privWebsocketClient.onerror = (e: { error: any; message: string; type: string; target: WebSocket | ws }) => {\r\n            this.onEvent(new ConnectionErrorEvent(this.privConnectionId, e.message, e.type));\r\n            this.privLastErrorReceived = e.message;\r\n        };\r\n\r\n        this.privWebsocketClient.onclose = (e: { wasClean: boolean; code: number; reason: string; target: WebSocket | ws }) => {\r\n            if (this.privConnectionState === ConnectionState.Connecting) {\r\n                this.privConnectionState = ConnectionState.Disconnected;\r\n                // this.onEvent(new ConnectionEstablishErrorEvent(this.connectionId, e.code, e.reason));\r\n                this.privConnectionEstablishDeferral.resolve(new ConnectionOpenResponse(e.code, e.reason + \" \" + this.privLastErrorReceived));\r\n            } else {\r\n                this.privConnectionState = ConnectionState.Disconnected;\r\n                this.privWebsocketClient = null;\r\n                this.onEvent(new ConnectionClosedEvent(this.privConnectionId, e.code, e.reason));\r\n            }\r\n\r\n            this.onClose(e.code, e.reason).catch((reason: string): void => {\r\n                Events.instance.onEvent(new BackgroundEvent(reason));\r\n            });\r\n        };\r\n\r\n        this.privWebsocketClient.onmessage = (e: { data: ws.Data; type: string; target: WebSocket | ws }) => {\r\n            const networkReceivedTime = new Date().toISOString();\r\n            if (this.privConnectionState === ConnectionState.Connected) {\r\n                const deferred = new Deferred<ConnectionMessage>();\r\n                // let id = ++this.idCounter;\r\n                this.privReceivingMessageQueue.enqueueFromPromise(deferred.promise);\r\n                if (e.data instanceof ArrayBuffer) {\r\n                    const rawMessage = new RawWebsocketMessage(MessageType.Binary, e.data);\r\n                    this.privMessageFormatter\r\n                        .toConnectionMessage(rawMessage)\r\n                        .then((connectionMessage: ConnectionMessage) => {\r\n                            this.onEvent(new ConnectionMessageReceivedEvent(this.privConnectionId, networkReceivedTime, connectionMessage));\r\n                            deferred.resolve(connectionMessage);\r\n                        }, (error: string) => {\r\n                            // TODO: Events for these ?\r\n                            deferred.reject(`Invalid binary message format. Error: ${error}`);\r\n                        });\r\n                } else {\r\n                    const rawMessage = new RawWebsocketMessage(MessageType.Text, e.data);\r\n                    this.privMessageFormatter\r\n                        .toConnectionMessage(rawMessage)\r\n                        .then((connectionMessage: ConnectionMessage) => {\r\n                            this.onEvent(new ConnectionMessageReceivedEvent(this.privConnectionId, networkReceivedTime, connectionMessage));\r\n                            deferred.resolve(connectionMessage);\r\n                        }, (error: string) => {\r\n                            // TODO: Events for these ?\r\n                            deferred.reject(`Invalid text message format. Error: ${error}`);\r\n                        });\r\n                }\r\n            }\r\n        };\r\n\r\n        return this.privConnectionEstablishDeferral.promise;\r\n    }\r\n\r\n    public send = (message: ConnectionMessage): Promise<void> => {\r\n        if (this.privConnectionState !== ConnectionState.Connected) {\r\n            return Promise.reject(`Cannot send on connection that is in ${ConnectionState[this.privConnectionState]} state`);\r\n        }\r\n\r\n        const messageSendStatusDeferral = new Deferred<void>();\r\n        const messageSendDeferral = new Deferred<ISendItem>();\r\n\r\n        this.privSendMessageQueue.enqueueFromPromise(messageSendDeferral.promise);\r\n\r\n        this.privMessageFormatter\r\n            .fromConnectionMessage(message)\r\n            .then((rawMessage: RawWebsocketMessage) => {\r\n                messageSendDeferral.resolve({\r\n                    Message: message,\r\n                    RawWebsocketMessage: rawMessage,\r\n                    sendStatusDeferral: messageSendStatusDeferral,\r\n                });\r\n            }, (error: string) => {\r\n                messageSendDeferral.reject(`Error formatting the message. ${error}`);\r\n            });\r\n\r\n        return messageSendStatusDeferral.promise;\r\n    }\r\n\r\n    public read = (): Promise<ConnectionMessage> => {\r\n        if (this.privConnectionState !== ConnectionState.Connected) {\r\n            return Promise.reject<ConnectionMessage>(`Cannot read on connection that is in ${this.privConnectionState} state`);\r\n        }\r\n\r\n        return this.privReceivingMessageQueue.dequeue();\r\n    }\r\n\r\n    public close = (reason?: string): Promise<void> => {\r\n        if (this.privWebsocketClient) {\r\n            if (this.privConnectionState !== ConnectionState.Disconnected) {\r\n                this.privWebsocketClient.close(1000, reason ? reason : \"Normal closure by client\");\r\n            }\r\n        } else {\r\n            return Promise.resolve();\r\n        }\r\n\r\n        return this.privDisconnectDeferral.promise;\r\n    }\r\n\r\n    public get events(): EventSource<ConnectionEvent> {\r\n        return this.privConnectionEvents;\r\n    }\r\n\r\n    private sendRawMessage = (sendItem: ISendItem): Promise<void> => {\r\n        try {\r\n            // indicates we are draining the queue and it came with no message;\r\n            if (!sendItem) {\r\n                return Promise.resolve();\r\n            }\r\n\r\n            this.onEvent(new ConnectionMessageSentEvent(this.privConnectionId, new Date().toISOString(), sendItem.Message));\r\n\r\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\r\n            if (this.isWebsocketOpen) {\r\n                this.privWebsocketClient.send(sendItem.RawWebsocketMessage.payload);\r\n            } else {\r\n                return Promise.reject(\"websocket send error: Websocket not ready \" + this.privConnectionId + \" \" + sendItem.Message.id + \" \" + new Error().stack);\r\n            }\r\n            return Promise.resolve();\r\n\r\n        } catch (e) {\r\n            return Promise.reject(`websocket send error: ${e}`);\r\n        }\r\n    }\r\n\r\n    private async onClose(code: number, reason: string): Promise<void> {\r\n        const closeReason = `Connection closed. ${code}: ${reason}`;\r\n        this.privConnectionState = ConnectionState.Disconnected;\r\n        this.privDisconnectDeferral.resolve();\r\n        await this.privReceivingMessageQueue.drainAndDispose((pendingReceiveItem: ConnectionMessage) => {\r\n            // TODO: Events for these ?\r\n            // Logger.instance.onEvent(new LoggingEvent(LogType.Warning, null, `Failed to process received message. Reason: ${closeReason}, Message: ${JSON.stringify(pendingReceiveItem)}`));\r\n        }, closeReason);\r\n\r\n        await this.privSendMessageQueue.drainAndDispose((pendingSendItem: ISendItem) => {\r\n            pendingSendItem.sendStatusDeferral.reject(closeReason);\r\n        }, closeReason);\r\n    }\r\n\r\n    private async processSendQueue(): Promise<void> {\r\n        while (true) {\r\n            const itemToSend: Promise<ISendItem> = this.privSendMessageQueue.dequeue();\r\n            const sendItem: ISendItem = await itemToSend;\r\n            // indicates we are draining the queue and it came with no message;\r\n            if (!sendItem) {\r\n                return;\r\n            }\r\n\r\n            try {\r\n                await this.sendRawMessage(sendItem);\r\n                sendItem.sendStatusDeferral.resolve();\r\n            } catch (sendError) {\r\n                sendItem.sendStatusDeferral.reject(sendError);\r\n            }\r\n        }\r\n    }\r\n\r\n    private onEvent = (event: ConnectionEvent): void => {\r\n        this.privConnectionEvents.onEvent(event);\r\n        Events.instance.onEvent(event);\r\n    }\r\n\r\n    private get isWebsocketOpen(): boolean {\r\n        return this.privWebsocketClient && this.privWebsocketClient.readyState === this.privWebsocketClient.OPEN;\r\n    }\r\n\r\n}\r\n"]}