@hahnpro/ms-speech-sdk
Version:
Microsoft Cognitive Services Speech SDK for JavaScript
283 lines (281 loc) • 14.6 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebsocketMessageAdapter = void 0;
// Node.JS specific web socket / browser support.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// import * as http from "http";
// import * as net from "net";
// import * as tls from "tls";
// import * as https from "https-proxy-agent";
// import Agent from "agent-base";
// import ws from "ws";
const HeaderNames_js_1 = require("../common.speech/HeaderNames.js");
const Exports_js_1 = require("../common/Exports.js");
class WebsocketMessageAdapter {
constructor(uri, connectionId, messageFormatter, proxyInfo, headers, enableCompression) {
if (!uri) {
throw new Exports_js_1.ArgumentNullError("uri");
}
if (!messageFormatter) {
throw new Exports_js_1.ArgumentNullError("messageFormatter");
}
// this.proxyInfo = proxyInfo;
this.privConnectionEvents = new Exports_js_1.EventSource();
this.privConnectionId = connectionId;
this.privMessageFormatter = messageFormatter;
this.privConnectionState = Exports_js_1.ConnectionState.None;
this.privUri = uri;
this.privHeaders = headers;
this.privEnableCompression = enableCompression;
// Add the connection ID to the headers
this.privHeaders[HeaderNames_js_1.HeaderNames.ConnectionId] = this.privConnectionId;
this.privHeaders.connectionId = this.privConnectionId;
this.privLastErrorReceived = "";
}
get state() {
return this.privConnectionState;
}
open() {
if (this.privConnectionState === Exports_js_1.ConnectionState.Disconnected) {
return Promise.reject(`Cannot open a connection that is in ${this.privConnectionState} state`);
}
if (this.privConnectionEstablishDeferral) {
return this.privConnectionEstablishDeferral.promise;
}
this.privConnectionEstablishDeferral = new Exports_js_1.Deferred();
this.privCertificateValidatedDeferral = new Exports_js_1.Deferred();
this.privConnectionState = Exports_js_1.ConnectionState.Connecting;
try {
// if (typeof WebSocket !== "undefined" && !WebsocketMessageAdapter.forceNpmWebSocket) {
// Browser handles cert checks.
this.privCertificateValidatedDeferral.resolve();
this.privWebsocketClient = new WebSocket(this.privUri);
// } else {
// // Workaround for https://github.com/microsoft/cognitive-services-speech-sdk-js/issues/465
// // Which is root caused by https://github.com/TooTallNate/node-agent-base/issues/61
// const uri = new URL(this.privUri);
// let protocol: string = uri.protocol;
// if (protocol?.toLocaleLowerCase() === "wss:") {
// protocol = "https:";
// } else if (protocol?.toLocaleLowerCase() === "ws:") {
// protocol = "http:";
// }
// const options: ws.ClientOptions = { headers: this.privHeaders, perMessageDeflate: this.privEnableCompression, followRedirects: protocol.toLocaleLowerCase() === "https:" };
// // The ocsp library will handle validation for us and fail the connection if needed.
// this.privCertificateValidatedDeferral.resolve();
// options.agent = this.getAgent();
// // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
// (options.agent as any).protocol = protocol;
// this.privWebsocketClient = new ws(this.privUri, options);
// this.privWebsocketClient.on("redirect", (redirectUrl: string): void => {
// const event: ConnectionRedirectEvent = new ConnectionRedirectEvent(this.privConnectionId, redirectUrl, this.privUri, `Getting redirect URL from endpoint ${this.privUri} with redirect URL '${redirectUrl}'`);
// Events.instance.onEvent(event);
// });
// }
this.privWebsocketClient.binaryType = "arraybuffer";
this.privReceivingMessageQueue = new Exports_js_1.Queue();
this.privDisconnectDeferral = new Exports_js_1.Deferred();
this.privSendMessageQueue = new Exports_js_1.Queue();
this.processSendQueue().catch((reason) => {
Exports_js_1.Events.instance.onEvent(new Exports_js_1.BackgroundEvent(reason));
});
}
catch (error) {
this.privConnectionEstablishDeferral.resolve(new Exports_js_1.ConnectionOpenResponse(500, error));
return this.privConnectionEstablishDeferral.promise;
}
this.onEvent(new Exports_js_1.ConnectionStartEvent(this.privConnectionId, this.privUri));
this.privWebsocketClient.onopen = () => {
this.privCertificateValidatedDeferral.promise.then(() => {
this.privConnectionState = Exports_js_1.ConnectionState.Connected;
this.onEvent(new Exports_js_1.ConnectionEstablishedEvent(this.privConnectionId));
this.privConnectionEstablishDeferral.resolve(new Exports_js_1.ConnectionOpenResponse(200, ""));
}, (error) => {
this.privConnectionEstablishDeferral.reject(error);
});
};
this.privWebsocketClient.onerror = (e) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
this.onEvent(new Exports_js_1.ConnectionErrorEvent(this.privConnectionId, e.message, e.type));
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
this.privLastErrorReceived = e.message;
};
this.privWebsocketClient.onclose = (e) => {
if (this.privConnectionState === Exports_js_1.ConnectionState.Connecting) {
this.privConnectionState = Exports_js_1.ConnectionState.Disconnected;
// this.onEvent(new ConnectionEstablishErrorEvent(this.connectionId, e.code, e.reason));
this.privConnectionEstablishDeferral.resolve(new Exports_js_1.ConnectionOpenResponse(e.code, e.reason + " " + this.privLastErrorReceived));
}
else {
this.privConnectionState = Exports_js_1.ConnectionState.Disconnected;
this.privWebsocketClient = null;
this.onEvent(new Exports_js_1.ConnectionClosedEvent(this.privConnectionId, e.code, e.reason));
}
this.onClose(e.code, e.reason).catch((reason) => {
Exports_js_1.Events.instance.onEvent(new Exports_js_1.BackgroundEvent(reason));
});
};
this.privWebsocketClient.onmessage = (e) => {
const networkReceivedTime = new Date().toISOString();
if (this.privConnectionState === Exports_js_1.ConnectionState.Connected) {
const deferred = new Exports_js_1.Deferred();
// let id = ++this.idCounter;
this.privReceivingMessageQueue.enqueueFromPromise(deferred.promise);
if (e.data instanceof ArrayBuffer) {
const rawMessage = new Exports_js_1.RawWebsocketMessage(Exports_js_1.MessageType.Binary, e.data);
this.privMessageFormatter
.toConnectionMessage(rawMessage)
.then((connectionMessage) => {
this.onEvent(new Exports_js_1.ConnectionMessageReceivedEvent(this.privConnectionId, networkReceivedTime, connectionMessage));
deferred.resolve(connectionMessage);
}, (error) => {
// TODO: Events for these ?
deferred.reject(`Invalid binary message format. Error: ${error}`);
});
}
else {
const rawMessage = new Exports_js_1.RawWebsocketMessage(Exports_js_1.MessageType.Text, e.data);
this.privMessageFormatter
.toConnectionMessage(rawMessage)
.then((connectionMessage) => {
this.onEvent(new Exports_js_1.ConnectionMessageReceivedEvent(this.privConnectionId, networkReceivedTime, connectionMessage));
deferred.resolve(connectionMessage);
}, (error) => {
// TODO: Events for these ?
deferred.reject(`Invalid text message format. Error: ${error}`);
});
}
}
};
return this.privConnectionEstablishDeferral.promise;
}
send(message) {
if (this.privConnectionState !== Exports_js_1.ConnectionState.Connected) {
return Promise.reject(`Cannot send on connection that is in ${Exports_js_1.ConnectionState[this.privConnectionState]} state`);
}
const messageSendStatusDeferral = new Exports_js_1.Deferred();
const messageSendDeferral = new Exports_js_1.Deferred();
this.privSendMessageQueue.enqueueFromPromise(messageSendDeferral.promise);
this.privMessageFormatter
.fromConnectionMessage(message)
.then((rawMessage) => {
messageSendDeferral.resolve({
Message: message,
RawWebsocketMessage: rawMessage,
sendStatusDeferral: messageSendStatusDeferral,
});
}, (error) => {
messageSendDeferral.reject(`Error formatting the message. ${error}`);
});
return messageSendStatusDeferral.promise;
}
read() {
if (this.privConnectionState !== Exports_js_1.ConnectionState.Connected) {
return Promise.reject(`Cannot read on connection that is in ${this.privConnectionState} state`);
}
return this.privReceivingMessageQueue.dequeue();
}
close(reason) {
if (this.privWebsocketClient) {
if (this.privConnectionState !== Exports_js_1.ConnectionState.Disconnected) {
this.privWebsocketClient.close(1000, reason ? reason : "Normal closure by client");
}
}
else {
return Promise.resolve();
}
return this.privDisconnectDeferral.promise;
}
get events() {
return this.privConnectionEvents;
}
sendRawMessage(sendItem) {
try {
// indicates we are draining the queue and it came with no message;
if (!sendItem) {
return Promise.resolve();
}
this.onEvent(new Exports_js_1.ConnectionMessageSentEvent(this.privConnectionId, new Date().toISOString(), sendItem.Message));
// add a check for the ws readystate in order to stop the red console error 'WebSocket is already in CLOSING or CLOSED state' appearing
if (this.isWebsocketOpen) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.privWebsocketClient.send(sendItem.RawWebsocketMessage.payload);
}
else {
return Promise.reject("websocket send error: Websocket not ready " + this.privConnectionId + " " + sendItem.Message.id + " " + new Error().stack);
}
return Promise.resolve();
}
catch (e) {
return Promise.reject(`websocket send error: ${e}`);
}
}
async onClose(code, reason) {
const closeReason = `Connection closed. ${code}: ${reason}`;
this.privConnectionState = Exports_js_1.ConnectionState.Disconnected;
this.privDisconnectDeferral.resolve();
await this.privReceivingMessageQueue.drainAndDispose(() => {
// TODO: Events for these ?
// Logger.instance.onEvent(new LoggingEvent(LogType.Warning, null, `Failed to process received message. Reason: ${closeReason}, Message: ${JSON.stringify(pendingReceiveItem)}`));
}, closeReason);
await this.privSendMessageQueue.drainAndDispose((pendingSendItem) => {
pendingSendItem.sendStatusDeferral.reject(closeReason);
}, closeReason);
}
async processSendQueue() {
while (true) {
const itemToSend = this.privSendMessageQueue.dequeue();
const sendItem = await itemToSend;
// indicates we are draining the queue and it came with no message;
if (!sendItem) {
return;
}
try {
await this.sendRawMessage(sendItem);
sendItem.sendStatusDeferral.resolve();
}
catch (sendError) {
sendItem.sendStatusDeferral.reject(sendError);
}
}
}
onEvent(event) {
this.privConnectionEvents.onEvent(event);
Exports_js_1.Events.instance.onEvent(event);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// private getAgent(): http.Agent {
// // eslint-disable-next-line @typescript-eslint/unbound-method
// const agent: { proxyInfo: ProxyInfo } = new Agent.Agent(this.createConnection) as unknown as { proxyInfo: ProxyInfo };
// // if (this.proxyInfo !== undefined &&
// // this.proxyInfo.HostName !== undefined &&
// // this.proxyInfo.Port > 0) {
// // agent.proxyInfo = this.proxyInfo;
// // }
// return agent as unknown as http.Agent;
// }
// private createConnection(request: Agent.ClientRequest, options: Agent.RequestOptions): Promise<net.Socket> {
// let socketPromise: Promise<net.Socket>;
// options = {
// ...options,
// ...{
// requestOCSP: true,
// servername: options.host
// }
// };
// if (!!options.secureEndpoint) {
// socketPromise = Promise.resolve(tls.connect(options));
// } else {
// socketPromise = Promise.resolve(net.connect(options));
// }
// return socketPromise;
// }
get isWebsocketOpen() {
return this.privWebsocketClient && this.privWebsocketClient.readyState === this.privWebsocketClient.OPEN;
}
}
exports.WebsocketMessageAdapter = WebsocketMessageAdapter;
WebsocketMessageAdapter.forceNpmWebSocket = false;
//# sourceMappingURL=WebsocketMessageAdapter.js.map