@speechly/browser-client
Advanced tools
Comparing version 1.2.0 to 1.3.0
@@ -27,9 +27,8 @@ import { ClientOptions, StateChangeCallback, SegmentChangeCallback, TentativeTranscriptCallback, TranscriptCallback, TentativeEntitiesCallback, EntityCallback, IntentCallback } from './types'; | ||
private readonly activeContexts; | ||
private readonly reconnectAttemptCount; | ||
private readonly reconnectMinDelay; | ||
private readonly maxReconnectAttemptCount; | ||
private readonly contextStopDelay; | ||
private connectAttempt; | ||
private stoppedContextIdPromise?; | ||
private initializeMicrophonePromise?; | ||
private readonly initializeApiClientPromise; | ||
private resolveInitialization?; | ||
private connectPromise; | ||
private initializePromise; | ||
private resolveStopContext?; | ||
@@ -40,2 +39,3 @@ private readonly deviceId; | ||
private state; | ||
private readonly apiUrl; | ||
private stateChangeCb; | ||
@@ -50,6 +50,11 @@ private segmentChangeCb; | ||
constructor(options: ClientOptions); | ||
private getReconnectDelayMs; | ||
private sleep; | ||
/** | ||
* Esteblish websocket connection | ||
* Connect to Speechly backend. | ||
* This function will be called by initialize if not manually called earlier. | ||
* Calling connect() immediately after constructor and setting callbacks allows | ||
* prewarming the connection, resulting in less noticeable waits for the user. | ||
*/ | ||
private connect; | ||
connect(): Promise<void>; | ||
/** | ||
@@ -129,2 +134,3 @@ * Initializes the client, by initializing the microphone and establishing connection to the API. | ||
private readonly handleWebsocketClosure; | ||
private reconnect; | ||
private setState; | ||
@@ -131,0 +137,0 @@ /** |
@@ -14,2 +14,6 @@ import { Microphone } from '../microphone'; | ||
/** | ||
* Connect to Speechly upon creating the client instance. Defaults to true. | ||
*/ | ||
connect?: boolean; | ||
/** | ||
* The unique identifier of a project in the dashboard. | ||
@@ -19,2 +23,3 @@ */ | ||
/** | ||
* @deprecated | ||
* The language which is used by the app. | ||
@@ -21,0 +26,0 @@ */ |
@@ -33,2 +33,3 @@ /** | ||
Opened = "WEBSOCKET_OPEN", | ||
Closed = "WEBSOCKET_CLOSED", | ||
SourceSampleRateSetSuccess = "SOURSE_SAMPLE_RATE_SET_SUCCESS", | ||
@@ -134,3 +135,7 @@ Started = "started", | ||
*/ | ||
export declare type CloseCallback = (err: Error) => void; | ||
export declare type CloseCallback = (err: { | ||
code: number; | ||
reason: string; | ||
wasClean: boolean; | ||
}) => void; | ||
/** | ||
@@ -137,0 +142,0 @@ * The interface for a client for Speechly SLU WebSocket API. |
@@ -1,2 +0,2 @@ | ||
declare const _default: "/**\n * Known WebSocket response types.\n * @public\n */\nvar WebsocketResponseType;\n(function (WebsocketResponseType) {\n WebsocketResponseType[\"Opened\"] = \"WEBSOCKET_OPEN\";\n WebsocketResponseType[\"SourceSampleRateSetSuccess\"] = \"SOURSE_SAMPLE_RATE_SET_SUCCESS\";\n WebsocketResponseType[\"Started\"] = \"started\";\n WebsocketResponseType[\"Stopped\"] = \"stopped\";\n})(WebsocketResponseType || (WebsocketResponseType = {}));\nvar CONTROL = {\n WRITE_INDEX: 0,\n FRAMES_AVAILABLE: 1,\n LOCK: 2\n};\nvar WebsocketClient = /** @class */ (function () {\n function WebsocketClient(ctx) {\n var _this = this;\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n this.shouldResendLastFramesSent = false;\n this.buffer = new Float32Array(0);\n this.lastFramesSent = new Int16Array(0); // to re-send after switch context\n this.debug = false;\n this.initialized = false;\n this.onWebsocketClose = function (event) {\n _this.websocket = undefined;\n _this.connect(0);\n };\n this.onWebsocketOpen = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket opened');\n }\n if (_this.isContextStarted && !_this.isStartContextConfirmed) {\n _this.send(_this.outbox);\n }\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_OPEN' });\n };\n this.onWebsocketError = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket error');\n }\n _this.closeWebsocket();\n };\n this.onWebsocketMessage = function (event) {\n var response;\n try {\n response = JSON.parse(event.data);\n }\n catch (e) {\n console.error('[SpeechlyClient] Error parsing response from the server:', e);\n return;\n }\n if (response.type === WebsocketResponseType.Started) {\n _this.isStartContextConfirmed = true;\n if (_this.shouldResendLastFramesSent) {\n _this.resendLastFrames();\n _this.shouldResendLastFramesSent = false;\n }\n }\n _this.workerCtx.postMessage(response);\n };\n this.workerCtx = ctx;\n }\n WebsocketClient.prototype.init = function (apiUrl, authToken, targetSampleRate, debug) {\n if (this.initialized) {\n console.log('[SpeechlyClient]', 'already initialized');\n return;\n }\n this.debug = debug;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'initialize worker');\n }\n this.apiUrl = apiUrl;\n this.authToken = authToken;\n this.targetSampleRate = targetSampleRate;\n this.initialized = true;\n this.connect(0);\n };\n WebsocketClient.prototype.setSourceSampleRate = function (sourceSampleRate) {\n this.sourceSampleRate = sourceSampleRate;\n this.resampleRatio = this.sourceSampleRate / this.targetSampleRate;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'resampleRatio', this.resampleRatio);\n }\n if (this.resampleRatio > 1) {\n this.filter = generateFilter(this.sourceSampleRate, this.targetSampleRate, 127);\n }\n this.workerCtx.postMessage({ type: 'SOURSE_SAMPLE_RATE_SET_SUCCESS' });\n if (isNaN(this.resampleRatio)) {\n throw Error(\"resampleRatio is NaN source rate is \".concat(this.sourceSampleRate, \" and target rate is \").concat(this.targetSampleRate));\n }\n };\n WebsocketClient.prototype.setSharedArrayBuffers = function (controlSAB, dataSAB) {\n this.controlSAB = new Int32Array(controlSAB);\n this.dataSAB = new Float32Array(dataSAB);\n var audioHandleInterval = this.dataSAB.length / 32; // ms\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Audio handle interval', audioHandleInterval, 'ms');\n }\n setInterval(this.sendAudioFromSAB.bind(this), audioHandleInterval);\n };\n WebsocketClient.prototype.connect = function (timeout) {\n if (timeout === void 0) { timeout = 1000; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connect in ', timeout / 1000, 'sec');\n }\n setTimeout(this.initializeWebsocket.bind(this), timeout);\n };\n WebsocketClient.prototype.initializeWebsocket = function () {\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connecting to ', this.apiUrl);\n }\n this.websocket = new WebSocket(this.apiUrl, this.authToken);\n this.websocket.addEventListener('open', this.onWebsocketOpen);\n this.websocket.addEventListener('message', this.onWebsocketMessage);\n this.websocket.addEventListener('error', this.onWebsocketError);\n this.websocket.addEventListener('close', this.onWebsocketClose);\n };\n WebsocketClient.prototype.isOpen = function () {\n return this.websocket !== undefined && this.websocket.readyState === this.websocket.OPEN;\n };\n WebsocketClient.prototype.resendLastFrames = function () {\n if (this.lastFramesSent.length > 0) {\n this.send(this.lastFramesSent);\n this.lastFramesSent = new Int16Array(0);\n }\n };\n WebsocketClient.prototype.sendAudio = function (audioChunk) {\n if (!this.isContextStarted) {\n return;\n }\n if (audioChunk.length > 0) {\n if (this.resampleRatio > 1) {\n // Downsampling\n this.send(this.downsample(audioChunk));\n }\n else {\n this.send(float32ToInt16(audioChunk));\n }\n }\n };\n WebsocketClient.prototype.sendAudioFromSAB = function () {\n if (!this.isContextStarted) {\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n return;\n }\n if (this.controlSAB == undefined) {\n return;\n }\n var framesAvailable = this.controlSAB[CONTROL.FRAMES_AVAILABLE];\n var lock = this.controlSAB[CONTROL.LOCK];\n if (lock == 0 && framesAvailable > 0) {\n var data = this.dataSAB.subarray(0, framesAvailable);\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n if (data.length > 0) {\n var frames_1;\n if (this.resampleRatio > 1) {\n frames_1 = this.downsample(data);\n }\n else {\n frames_1 = float32ToInt16(data);\n }\n this.send(frames_1);\n // 16000 per second, 1000 in 100 ms\n // save last 250 ms\n if (this.lastFramesSent.length > 1024 * 4) {\n this.lastFramesSent = frames_1;\n }\n else {\n var concat = new Int16Array(this.lastFramesSent.length + frames_1.length);\n concat.set(this.lastFramesSent);\n concat.set(frames_1, this.lastFramesSent.length);\n this.lastFramesSent = concat;\n }\n }\n }\n };\n WebsocketClient.prototype.startContext = function (appId) {\n if (this.isContextStarted) {\n console.log('Cant start context: it has been already started');\n return;\n }\n this.isContextStarted = true;\n this.isStartContextConfirmed = false;\n if (appId !== undefined) {\n this.outbox = JSON.stringify({ event: 'start', appId: appId });\n }\n else {\n this.outbox = JSON.stringify({ event: 'start' });\n }\n this.send(this.outbox);\n };\n WebsocketClient.prototype.stopContext = function () {\n if (this.websocket == undefined) {\n throw Error('Cant start context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant stop context: it is not started');\n return;\n }\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n };\n WebsocketClient.prototype.switchContext = function (newAppId) {\n if (this.websocket == undefined) {\n throw Error('Cant switch context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant switch context: it is not started');\n return;\n }\n if (newAppId == undefined) {\n console.log('Cant switch context: new app id is undefined');\n return;\n }\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n this.shouldResendLastFramesSent = true;\n this.send(JSON.stringify({ event: 'start', appId: newAppId }));\n };\n WebsocketClient.prototype.closeWebsocket = function () {\n if (this.websocket == null) {\n throw Error('Websocket is not open');\n }\n this.websocket.removeEventListener('open', this.onWebsocketOpen);\n this.websocket.removeEventListener('message', this.onWebsocketMessage);\n this.websocket.removeEventListener('error', this.onWebsocketError);\n this.websocket.removeEventListener('close', this.onWebsocketClose);\n this.websocket.close();\n };\n WebsocketClient.prototype.downsample = function (input) {\n var inputBuffer = new Float32Array(this.buffer.length + input.length);\n inputBuffer.set(this.buffer, 0);\n inputBuffer.set(input, this.buffer.length);\n var outputLength = Math.ceil((inputBuffer.length - this.filter.length) / this.resampleRatio);\n var outputBuffer = new Int16Array(outputLength);\n for (var i = 0; i < outputLength; i++) {\n var offset = Math.round(this.resampleRatio * i);\n var val = 0.0;\n for (var j = 0; j < this.filter.length; j++) {\n val += inputBuffer[offset + j] * this.filter[j];\n }\n outputBuffer[i] = val * (val < 0 ? 0x8000 : 0x7fff);\n }\n var remainingOffset = Math.round(this.resampleRatio * outputLength);\n if (remainingOffset < inputBuffer.length) {\n this.buffer = inputBuffer.subarray(remainingOffset);\n }\n else {\n this.buffer = new Float32Array(0);\n }\n return outputBuffer;\n };\n WebsocketClient.prototype.send = function (data) {\n if (!this.isOpen()) {\n throw Error('Cant send data: websocket is inactive');\n }\n try {\n this.websocket.send(data);\n }\n catch (error) {\n console.log('[SpeechlyClient]', 'Server connection error', error);\n }\n };\n return WebsocketClient;\n}());\nvar ctx = self;\nvar websocketClient = new WebsocketClient(ctx);\nctx.onmessage = function (e) {\n switch (e.data.type) {\n case 'INIT':\n websocketClient.init(e.data.apiUrl, e.data.authToken, e.data.targetSampleRate, e.data.debug);\n break;\n case 'SET_SOURSE_SAMPLE_RATE':\n websocketClient.setSourceSampleRate(e.data.sourceSampleRate);\n break;\n case 'SET_SHARED_ARRAY_BUFFERS':\n websocketClient.setSharedArrayBuffers(e.data.controlSAB, e.data.dataSAB);\n break;\n case 'CLOSE':\n websocketClient.closeWebsocket();\n break;\n case 'START_CONTEXT':\n websocketClient.startContext(e.data.appId);\n break;\n case 'SWITCH_CONTEXT':\n websocketClient.switchContext(e.data.appId);\n break;\n case 'STOP_CONTEXT':\n websocketClient.stopContext();\n break;\n case 'AUDIO':\n websocketClient.sendAudio(e.data.payload);\n break;\n default:\n console.log('WORKER', e);\n }\n};\nfunction float32ToInt16(buffer) {\n var buf = new Int16Array(buffer.length);\n for (var l = 0; l < buffer.length; l++) {\n buf[l] = buffer[l] * (buffer[l] < 0 ? 0x8000 : 0x7fff);\n }\n return buf;\n}\nfunction generateFilter(sourceSampleRate, targetSampleRate, length) {\n if (length % 2 === 0) {\n throw Error('Filter length must be odd');\n }\n var cutoff = targetSampleRate / 2;\n var filter = new Float32Array(length);\n var sum = 0;\n for (var i = 0; i < length; i++) {\n var x = sinc(((2 * cutoff) / sourceSampleRate) * (i - (length - 1) / 2));\n sum += x;\n filter[i] = x;\n }\n for (var i = 0; i < length; i++) {\n filter[i] = filter[i] / sum;\n }\n return filter;\n}\nfunction sinc(x) {\n if (x === 0.0) {\n return 1.0;\n }\n var piX = Math.PI * x;\n return Math.sin(piX) / piX;\n}\n"; | ||
declare const _default: "/**\n * Known WebSocket response types.\n * @public\n */\nvar WebsocketResponseType;\n(function (WebsocketResponseType) {\n WebsocketResponseType[\"Opened\"] = \"WEBSOCKET_OPEN\";\n WebsocketResponseType[\"SourceSampleRateSetSuccess\"] = \"SOURSE_SAMPLE_RATE_SET_SUCCESS\";\n WebsocketResponseType[\"Started\"] = \"started\";\n WebsocketResponseType[\"Stopped\"] = \"stopped\";\n})(WebsocketResponseType || (WebsocketResponseType = {}));\nvar CONTROL = {\n WRITE_INDEX: 0,\n FRAMES_AVAILABLE: 1,\n LOCK: 2\n};\nvar WebsocketClient = /** @class */ (function () {\n function WebsocketClient(ctx) {\n var _this = this;\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n this.shouldResendLastFramesSent = false;\n this.buffer = new Float32Array(0);\n this.lastFramesSent = new Int16Array(0); // to re-send after switch context\n this.debug = false;\n this.initialized = false;\n // WebSocket's close handler, called e.g. when\n // - normal close (code 1000)\n // - network unreachable or unable to (re)connect (code 1006)\n // List of CloseEvent.code values: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code\n this.onWebsocketClose = function (event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'onWebsocketClose');\n }\n _this.websocket.removeEventListener('open', _this.onWebsocketOpen);\n _this.websocket.removeEventListener('message', _this.onWebsocketMessage);\n _this.websocket.removeEventListener('error', _this.onWebsocketError);\n _this.websocket.removeEventListener('close', _this.onWebsocketClose);\n _this.websocket = undefined;\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_CLOSED', code: event.code, reason: event.reason, wasClean: event.wasClean });\n };\n this.onWebsocketOpen = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket opened');\n }\n if (_this.isContextStarted && !_this.isStartContextConfirmed) {\n _this.send(_this.outbox);\n }\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_OPEN' });\n };\n this.onWebsocketError = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket error');\n }\n };\n this.onWebsocketMessage = function (event) {\n var response;\n try {\n response = JSON.parse(event.data);\n }\n catch (e) {\n console.error('[SpeechlyClient] Error parsing response from the server:', e);\n return;\n }\n if (response.type === WebsocketResponseType.Started) {\n _this.isStartContextConfirmed = true;\n if (_this.shouldResendLastFramesSent) {\n _this.resendLastFrames();\n _this.shouldResendLastFramesSent = false;\n }\n }\n _this.workerCtx.postMessage(response);\n };\n this.workerCtx = ctx;\n }\n WebsocketClient.prototype.init = function (apiUrl, authToken, targetSampleRate, debug) {\n this.debug = debug;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'initialize worker');\n }\n this.apiUrl = apiUrl;\n this.authToken = authToken;\n this.targetSampleRate = targetSampleRate;\n this.initialized = true;\n this.connect(0);\n };\n WebsocketClient.prototype.setSourceSampleRate = function (sourceSampleRate) {\n this.sourceSampleRate = sourceSampleRate;\n this.resampleRatio = this.sourceSampleRate / this.targetSampleRate;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'resampleRatio', this.resampleRatio);\n }\n if (this.resampleRatio > 1) {\n this.filter = generateFilter(this.sourceSampleRate, this.targetSampleRate, 127);\n }\n this.workerCtx.postMessage({ type: 'SOURSE_SAMPLE_RATE_SET_SUCCESS' });\n if (isNaN(this.resampleRatio)) {\n throw Error(\"resampleRatio is NaN source rate is \".concat(this.sourceSampleRate, \" and target rate is \").concat(this.targetSampleRate));\n }\n };\n WebsocketClient.prototype.setSharedArrayBuffers = function (controlSAB, dataSAB) {\n this.controlSAB = new Int32Array(controlSAB);\n this.dataSAB = new Float32Array(dataSAB);\n var audioHandleInterval = this.dataSAB.length / 32; // ms\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Audio handle interval', audioHandleInterval, 'ms');\n }\n setInterval(this.sendAudioFromSAB.bind(this), audioHandleInterval);\n };\n WebsocketClient.prototype.connect = function (timeout) {\n if (timeout === void 0) { timeout = 1000; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connect in ', timeout / 1000, 'sec');\n }\n setTimeout(this.initializeWebsocket.bind(this), timeout);\n };\n WebsocketClient.prototype.initializeWebsocket = function () {\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connecting to ', this.apiUrl);\n }\n this.websocket = new WebSocket(this.apiUrl, this.authToken);\n this.websocket.addEventListener('open', this.onWebsocketOpen);\n this.websocket.addEventListener('message', this.onWebsocketMessage);\n this.websocket.addEventListener('error', this.onWebsocketError);\n this.websocket.addEventListener('close', this.onWebsocketClose);\n };\n WebsocketClient.prototype.isOpen = function () {\n return this.websocket !== undefined && this.websocket.readyState === this.websocket.OPEN;\n };\n WebsocketClient.prototype.resendLastFrames = function () {\n if (this.lastFramesSent.length > 0) {\n this.send(this.lastFramesSent);\n this.lastFramesSent = new Int16Array(0);\n }\n };\n WebsocketClient.prototype.sendAudio = function (audioChunk) {\n if (!this.isContextStarted) {\n return;\n }\n if (audioChunk.length > 0) {\n if (this.resampleRatio > 1) {\n // Downsampling\n this.send(this.downsample(audioChunk));\n }\n else {\n this.send(float32ToInt16(audioChunk));\n }\n }\n };\n WebsocketClient.prototype.sendAudioFromSAB = function () {\n if (!this.isContextStarted) {\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n return;\n }\n if (this.controlSAB == undefined) {\n return;\n }\n var framesAvailable = this.controlSAB[CONTROL.FRAMES_AVAILABLE];\n var lock = this.controlSAB[CONTROL.LOCK];\n if (lock == 0 && framesAvailable > 0) {\n var data = this.dataSAB.subarray(0, framesAvailable);\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n if (data.length > 0) {\n var frames_1;\n if (this.resampleRatio > 1) {\n frames_1 = this.downsample(data);\n }\n else {\n frames_1 = float32ToInt16(data);\n }\n this.send(frames_1);\n // 16000 per second, 1000 in 100 ms\n // save last 250 ms\n if (this.lastFramesSent.length > 1024 * 4) {\n this.lastFramesSent = frames_1;\n }\n else {\n var concat = new Int16Array(this.lastFramesSent.length + frames_1.length);\n concat.set(this.lastFramesSent);\n concat.set(frames_1, this.lastFramesSent.length);\n this.lastFramesSent = concat;\n }\n }\n }\n };\n WebsocketClient.prototype.startContext = function (appId) {\n if (this.isContextStarted) {\n console.log('Cant start context: it has been already started');\n return;\n }\n this.isContextStarted = true;\n this.isStartContextConfirmed = false;\n if (appId !== undefined) {\n this.outbox = JSON.stringify({ event: 'start', appId: appId });\n }\n else {\n this.outbox = JSON.stringify({ event: 'start' });\n }\n this.send(this.outbox);\n };\n WebsocketClient.prototype.stopContext = function () {\n if (!this.websocket) {\n throw Error('Cant start context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant stop context: it is not started');\n return;\n }\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n };\n WebsocketClient.prototype.switchContext = function (newAppId) {\n if (!this.websocket) {\n throw Error('Cant switch context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant switch context: it is not started');\n return;\n }\n if (newAppId == undefined) {\n console.log('Cant switch context: new app id is undefined');\n return;\n }\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n this.shouldResendLastFramesSent = true;\n this.send(JSON.stringify({ event: 'start', appId: newAppId }));\n };\n WebsocketClient.prototype.closeWebsocket = function (websocketCode, reason) {\n if (websocketCode === void 0) { websocketCode = 1005; }\n if (reason === void 0) { reason = \"No Status Received\"; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Websocket closing');\n }\n if (!this.websocket) {\n throw Error('Websocket is not open');\n }\n this.websocket.close(websocketCode, reason);\n };\n WebsocketClient.prototype.downsample = function (input) {\n var inputBuffer = new Float32Array(this.buffer.length + input.length);\n inputBuffer.set(this.buffer, 0);\n inputBuffer.set(input, this.buffer.length);\n var outputLength = Math.ceil((inputBuffer.length - this.filter.length) / this.resampleRatio);\n var outputBuffer = new Int16Array(outputLength);\n for (var i = 0; i < outputLength; i++) {\n var offset = Math.round(this.resampleRatio * i);\n var val = 0.0;\n for (var j = 0; j < this.filter.length; j++) {\n val += inputBuffer[offset + j] * this.filter[j];\n }\n outputBuffer[i] = val * (val < 0 ? 0x8000 : 0x7fff);\n }\n var remainingOffset = Math.round(this.resampleRatio * outputLength);\n if (remainingOffset < inputBuffer.length) {\n this.buffer = inputBuffer.subarray(remainingOffset);\n }\n else {\n this.buffer = new Float32Array(0);\n }\n return outputBuffer;\n };\n WebsocketClient.prototype.send = function (data) {\n if (!this.isOpen()) {\n throw Error('Cant send data: websocket is inactive');\n }\n try {\n this.websocket.send(data);\n }\n catch (error) {\n console.log('[SpeechlyClient]', 'Server connection error', error);\n }\n };\n return WebsocketClient;\n}());\nvar ctx = self;\nvar websocketClient = new WebsocketClient(ctx);\nctx.onmessage = function (e) {\n switch (e.data.type) {\n case 'INIT':\n websocketClient.init(e.data.apiUrl, e.data.authToken, e.data.targetSampleRate, e.data.debug);\n break;\n case 'SET_SOURSE_SAMPLE_RATE':\n websocketClient.setSourceSampleRate(e.data.sourceSampleRate);\n break;\n case 'SET_SHARED_ARRAY_BUFFERS':\n websocketClient.setSharedArrayBuffers(e.data.controlSAB, e.data.dataSAB);\n break;\n case 'CLOSE':\n websocketClient.closeWebsocket(1000, \"Close requested by client\");\n break;\n case 'START_CONTEXT':\n websocketClient.startContext(e.data.appId);\n break;\n case 'SWITCH_CONTEXT':\n websocketClient.switchContext(e.data.appId);\n break;\n case 'STOP_CONTEXT':\n websocketClient.stopContext();\n break;\n case 'AUDIO':\n websocketClient.sendAudio(e.data.payload);\n break;\n default:\n console.log('WORKER', e);\n }\n};\nfunction float32ToInt16(buffer) {\n var buf = new Int16Array(buffer.length);\n for (var l = 0; l < buffer.length; l++) {\n buf[l] = buffer[l] * (buffer[l] < 0 ? 0x8000 : 0x7fff);\n }\n return buf;\n}\nfunction generateFilter(sourceSampleRate, targetSampleRate, length) {\n if (length % 2 === 0) {\n throw Error('Filter length must be odd');\n }\n var cutoff = targetSampleRate / 2;\n var filter = new Float32Array(length);\n var sum = 0;\n for (var i = 0; i < length; i++) {\n var x = sinc(((2 * cutoff) / sourceSampleRate) * (i - (length - 1) / 2));\n sum += x;\n filter[i] = x;\n }\n for (var i = 0; i < length; i++) {\n filter[i] = filter[i] / sum;\n }\n return filter;\n}\nfunction sinc(x) {\n if (x === 0.0) {\n return 1.0;\n }\n var piX = Math.PI * x;\n return Math.sin(piX) / piX;\n}\n"; | ||
export default _default; |
{ | ||
"name": "@speechly/browser-client", | ||
"version": "1.2.0", | ||
"version": "1.3.0", | ||
"description": "Browser client for Speechly API", | ||
@@ -27,12 +27,9 @@ "keywords": [ | ||
"build:watch": "rm -rf ./dist/ && pnpm run buildworker && pnpx rollup -c --silent", | ||
"buildworker": "npx tsc ./worker/worker.ts && cat ./worker/templateOpen > ./src/websocket/worker.ts && cat ./worker/worker.js >> ./src/websocket/worker.ts && cat ./worker/templateEnd >> ./src/websocket/worker.ts", | ||
"check": "pnpm run build && npx api-extractor run --verbose", | ||
"docs": "pnpm run prepdist && npx typedoc --readme none --includeDeclarations --excludeExternals --excludeNotExported --excludePrivate --excludeProtected --out ./docs/ --plugin typedoc-plugin-markdown ./dist/index.d.ts", | ||
"buildworker": "pnpx tsc ./worker/worker.ts && cat ./worker/templateOpen > ./src/websocket/worker.ts && cat ./worker/worker.js >> ./src/websocket/worker.ts && cat ./worker/templateEnd >> ./src/websocket/worker.ts", | ||
"check": "pnpm run build && pnpx api-extractor run --verbose", | ||
"docs": "rimraf docs && pnpx typedoc --readme none --excludeExternals --excludePrivate --excludeProtected --out ./docs/ --entryPointStrategy expand --sort required-first --disableSources ./src/", | ||
"getdeps": "pnpm install --force --frozen-lockfile", | ||
"lint": "npx eslint --cache --max-warnings 0 'src/**/*.{ts,tsx}'", | ||
"precommit": "npx prettier --write src/**/*.ts && pnpm run build && npx api-extractor run --local && pnpm run docs", | ||
"prepdist": "node ./config/prepare_dist.js", | ||
"prerelease": "pnpm run check && pnpm run prepdist", | ||
"test": "npx jest --config ./config/jest.config.js", | ||
"watch": "rm -rf ./dist/ && mkdir dist && pnpm run prepdist && npx tsc-watch" | ||
"lint": "pnpx eslint --cache --max-warnings 0 'src/**/*.{ts,tsx}'", | ||
"precommit": "pnpx prettier --write src/**/*.ts && pnpm run build && pnpx api-extractor run --local && pnpm run docs", | ||
"test": "pnpx jest --config ./config/jest.config.js" | ||
}, | ||
@@ -55,3 +52,2 @@ "repository": { | ||
"base-64": "^0.1.0", | ||
"locale-code": "^2.0.2", | ||
"uuid": "^8.0.0" | ||
@@ -86,5 +82,5 @@ }, | ||
"tslib": "~2.3.1", | ||
"typedoc": "^0.21.5", | ||
"typedoc-plugin-markdown": "^3.10.4", | ||
"typescript": "^4.3.5" | ||
"typescript": "^4.3.5", | ||
"typedoc": "^0.22.6", | ||
"typedoc-plugin-markdown": "^3.11.3" | ||
}, | ||
@@ -91,0 +87,0 @@ "publishConfig": { |
@@ -1,2 +0,1 @@ | ||
import localeCode from 'locale-code' | ||
import { v4 as uuidv4 } from 'uuid' | ||
@@ -48,3 +47,2 @@ | ||
const defaultLoginUrl = 'https://api.speechly.com/login' | ||
const defaultLanguage = 'en-US' | ||
@@ -78,9 +76,9 @@ declare global { | ||
private readonly activeContexts = new Map<string, Map<number, SegmentState>>() | ||
private readonly reconnectAttemptCount = 5 | ||
private readonly reconnectMinDelay = 1000 | ||
private readonly maxReconnectAttemptCount = 10 | ||
private readonly contextStopDelay = 250 | ||
private connectAttempt: number = 0 | ||
private stoppedContextIdPromise?: Promise<string> | ||
private initializeMicrophonePromise?: Promise<void> | ||
private readonly initializeApiClientPromise: Promise<void> | ||
private resolveInitialization?: (value?: void) => void | ||
private connectPromise: Promise<void> | null | ||
private initializePromise: Promise<void> | null | ||
private resolveStopContext?: (value?: unknown) => void | ||
@@ -91,2 +89,3 @@ private readonly deviceId: string | ||
private state: ClientState = ClientState.Disconnected | ||
private readonly apiUrl: string | ||
@@ -119,7 +118,2 @@ private stateChangeCb: StateChangeCallback = () => {} | ||
const language = options.language ?? defaultLanguage | ||
if (!(localeCode.validate(language) || (localeCode.validateLanguageCode(`${language.substring(0, 2)}-XX`) && /^..-\d\d\d$/.test(language)))) { | ||
throw Error(`[SpeechlyClient] Invalid language "${language}"`) | ||
} | ||
this.debug = options.debug ?? false | ||
@@ -130,4 +124,4 @@ this.logSegments = options.logSegments ?? false | ||
this.projectId = options.projectId ?? undefined | ||
const apiUrl = generateWsUrl(options.apiUrl ?? defaultApiUrl, language, options.sampleRate ?? DefaultSampleRate) | ||
this.apiClient = options.apiClient ?? new WebWorkerController() | ||
this.apiUrl = generateWsUrl(options.apiUrl ?? defaultApiUrl, options.sampleRate ?? DefaultSampleRate) | ||
@@ -140,26 +134,3 @@ if (this.appId !== undefined && this.projectId !== undefined) { | ||
this.deviceId = this.storage.getOrSet(deviceIdStorageKey, uuidv4) | ||
const storedToken = this.storage.get(authTokenKey) | ||
// 2. Fetch auth token. It doesn't matter if it's not present. | ||
this.initializeApiClientPromise = new Promise(resolve => { | ||
this.resolveInitialization = resolve | ||
}) | ||
if (storedToken == null || !validateToken(storedToken, this.projectId, this.appId, this.deviceId)) { | ||
fetchToken(this.loginUrl, this.projectId, this.appId, this.deviceId) | ||
.then(token => { | ||
this.authToken = token | ||
// Cache the auth token in local storage for future use. | ||
this.storage.set(authTokenKey, this.authToken) | ||
this.connect(apiUrl) | ||
}) | ||
.catch(err => { | ||
this.setState(ClientState.Failed) | ||
throw err | ||
}) | ||
} else { | ||
this.authToken = storedToken | ||
this.connect(apiUrl) | ||
} | ||
if (window.AudioContext !== undefined) { | ||
@@ -173,27 +144,62 @@ this.isWebkit = false | ||
this.microphone = options.microphone ?? new BrowserMicrophone(this.isWebkit, this.sampleRate, this.apiClient, this.debug) | ||
this.microphone = | ||
options.microphone ?? new BrowserMicrophone(this.isWebkit, this.sampleRate, this.apiClient, this.debug) | ||
this.apiClient.onResponse(this.handleWebsocketResponse) | ||
this.apiClient.onClose(this.handleWebsocketClosure) | ||
this.connectPromise = null | ||
this.initializePromise = null | ||
window.SpeechlyClient = this | ||
if (options.connect !== false) { | ||
// eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
this.connect() | ||
} | ||
} | ||
private getReconnectDelayMs(attempt: number): number { | ||
return 2 ** attempt * 100 | ||
} | ||
private async sleep(ms: number): Promise<void> { | ||
return new Promise(resolve => setTimeout(resolve, ms)) | ||
} | ||
/** | ||
* Esteblish websocket connection | ||
* Connect to Speechly backend. | ||
* This function will be called by initialize if not manually called earlier. | ||
* Calling connect() immediately after constructor and setting callbacks allows | ||
* prewarming the connection, resulting in less noticeable waits for the user. | ||
*/ | ||
private connect(apiUrl: string): void { | ||
if (this.authToken != null) { | ||
this.apiClient.initialize( | ||
apiUrl, | ||
this.authToken, | ||
this.sampleRate, | ||
this.debug, | ||
).then(() => { | ||
if (this.resolveInitialization != null) { | ||
this.resolveInitialization() | ||
public async connect(): Promise<void> { | ||
if (this.connectPromise === null) { | ||
this.connectPromise = (async () => { | ||
await this.sleep(this.getReconnectDelayMs(this.connectAttempt++)) | ||
// Get auth token from cache or renew it | ||
const storedToken = this.storage.get(authTokenKey) | ||
if (storedToken == null || !validateToken(storedToken, this.projectId, this.appId, this.deviceId)) { | ||
try { | ||
this.authToken = await fetchToken(this.loginUrl, this.projectId, this.appId, this.deviceId) | ||
// Cache the auth token in local storage for future use. | ||
this.storage.set(authTokenKey, this.authToken) | ||
} catch (err) { | ||
this.setState(ClientState.Failed) | ||
throw err | ||
} | ||
} else { | ||
this.authToken = storedToken | ||
} | ||
}).catch(err => { | ||
throw err | ||
}) | ||
// Establish websocket connection | ||
try { | ||
await this.apiClient.initialize(this.apiUrl, this.authToken, this.sampleRate, this.debug) | ||
} catch (err) { | ||
this.setState(ClientState.Failed) | ||
throw err | ||
} | ||
})() | ||
} | ||
await this.connectPromise | ||
} | ||
@@ -211,77 +217,73 @@ | ||
async initialize(): Promise<void> { | ||
await this.initializeApiClientPromise | ||
if (this.state !== ClientState.Disconnected) { | ||
throw Error('Cannot initialize client - client is not in Disconnected state') | ||
} | ||
// Ensure we're connected. Returns immediately if we are | ||
await this.connect() | ||
this.setState(ClientState.Connecting) | ||
if (this.initializePromise === null) { | ||
this.initializePromise = (async () => { | ||
this.setState(ClientState.Connecting) | ||
try { | ||
if (this.isWebkit) { | ||
if (window.webkitAudioContext !== undefined) { | ||
// eslint-disable-next-line new-cap | ||
this.audioContext = new window.webkitAudioContext() | ||
} | ||
} else { | ||
const opts: AudioContextOptions = {} | ||
if (this.nativeResamplingSupported) { | ||
opts.sampleRate = this.sampleRate | ||
} | ||
try { | ||
// 1. Initialise the storage and fetch deviceId (or generate new one and store it). | ||
// await this.storage.initialize() | ||
// this.deviceId = await this.storage.getOrSet(deviceIdStorageKey, uuidv4) | ||
this.audioContext = new window.AudioContext(opts) | ||
} | ||
// 2. Initialise the microphone stack. | ||
if (this.isWebkit) { | ||
if (window.webkitAudioContext !== undefined) { | ||
// eslint-disable-next-line new-cap | ||
this.audioContext = new window.webkitAudioContext() | ||
} | ||
} else { | ||
const opts: AudioContextOptions = {} | ||
if (this.nativeResamplingSupported) { | ||
opts.sampleRate = this.sampleRate | ||
} | ||
const mediaStreamConstraints: MediaStreamConstraints = { | ||
video: false, | ||
} | ||
this.audioContext = new window.AudioContext(opts) | ||
} | ||
if (this.nativeResamplingSupported || this.autoGainControl) { | ||
mediaStreamConstraints.audio = { | ||
sampleRate: this.sampleRate, | ||
// @ts-ignore | ||
autoGainControl: this.autoGainControl, | ||
} | ||
} else { | ||
mediaStreamConstraints.audio = true | ||
} | ||
const mediaStreamConstraints: MediaStreamConstraints = { | ||
video: false, | ||
} | ||
if (this.audioContext != null) { | ||
// Start audio context if we are dealing with a WebKit browser. | ||
// | ||
// WebKit browsers (e.g. Safari) require to resume the context first, | ||
// before obtaining user media by calling `mediaDevices.getUserMedia`. | ||
// | ||
// If done in a different order, the audio context will resume successfully, | ||
// but will emit empty audio buffers. | ||
if (this.isWebkit) { | ||
await this.audioContext.resume() | ||
} | ||
// 3. Initialise websocket. | ||
await this.apiClient.setSourceSampleRate(this.audioContext.sampleRate) | ||
await this.microphone.initialize(this.audioContext, mediaStreamConstraints) | ||
} else { | ||
throw ErrDeviceNotSupported | ||
} | ||
} catch (err) { | ||
switch (err) { | ||
case ErrDeviceNotSupported: | ||
this.setState(ClientState.NoBrowserSupport) | ||
break | ||
case ErrNoAudioConsent: | ||
this.setState(ClientState.NoAudioConsent) | ||
break | ||
default: | ||
this.setState(ClientState.Failed) | ||
} | ||
if (this.nativeResamplingSupported || this.autoGainControl) { | ||
mediaStreamConstraints.audio = { | ||
sampleRate: this.sampleRate, | ||
// @ts-ignore | ||
autoGainControl: this.autoGainControl, | ||
throw err | ||
} | ||
} else { | ||
mediaStreamConstraints.audio = true | ||
} | ||
if (this.audioContext != null) { | ||
// Start audio context if we are dealing with a WebKit browser. | ||
// | ||
// WebKit browsers (e.g. Safari) require to resume the context first, | ||
// before obtaining user media by calling `mediaDevices.getUserMedia`. | ||
// | ||
// If done in a different order, the audio context will resume successfully, | ||
// but will emit empty audio buffers. | ||
if (this.isWebkit) { | ||
await this.audioContext.resume() | ||
} | ||
// 3. Initialise websocket. | ||
await this.apiClient.setSourceSampleRate(this.audioContext.sampleRate) | ||
this.initializeMicrophonePromise = this.microphone.initialize(this.audioContext, mediaStreamConstraints) | ||
await this.initializeMicrophonePromise | ||
} else { | ||
throw ErrDeviceNotSupported | ||
} | ||
} catch (err) { | ||
switch (err) { | ||
case ErrDeviceNotSupported: | ||
this.setState(ClientState.NoBrowserSupport) | ||
break | ||
case ErrNoAudioConsent: | ||
this.setState(ClientState.NoAudioConsent) | ||
break | ||
default: | ||
this.setState(ClientState.Failed) | ||
} | ||
throw err | ||
this.setState(ClientState.Connected) | ||
})() | ||
} | ||
this.setState(ClientState.Connected) | ||
await this.initializePromise | ||
} | ||
@@ -310,2 +312,4 @@ | ||
this.activeContexts.clear() | ||
this.connectPromise = null | ||
this.initializePromise = null | ||
this.setState(ClientState.Disconnected) | ||
@@ -336,2 +340,5 @@ | ||
async startContext(appId?: string): Promise<string> { | ||
// Ensure we're initialized; returns immediately if we are | ||
await this.initialize() | ||
if (this.resolveStopContext != null) { | ||
@@ -572,18 +579,34 @@ this.resolveStopContext() | ||
private readonly handleWebsocketClosure = (err: Error): void => { | ||
private readonly handleWebsocketClosure = (err: { code: number, reason: string, wasClean: boolean }): void => { | ||
if (err.code === 1000) { | ||
if (this.debug) { | ||
console.log('[SpeechlyClient]', 'Websocket closed', err) | ||
} | ||
} else { | ||
if (this.debug) { | ||
console.error('[SpeechlyClient]', 'Websocket closed due to error', err) | ||
} | ||
// If for some reason deviceId is missing, there's nothing else we can do but fail completely. | ||
if (this.deviceId === undefined) { | ||
this.setState(ClientState.Failed) | ||
return | ||
} | ||
this.reconnect() | ||
} | ||
} | ||
private reconnect(): void { | ||
if (this.debug) { | ||
console.error('[SpeechlyClient]', 'Server connection closed', err) | ||
console.log('[SpeechlyClient]', 'Reconnecting...', this.connectAttempt) | ||
} | ||
// If for some reason deviceId is missing, there's nothing else we can do but fail completely. | ||
if (this.deviceId === undefined) { | ||
if (this.state !== ClientState.Failed && this.connectAttempt < this.maxReconnectAttemptCount) { | ||
this.connectPromise = null | ||
// eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
this.connect() | ||
} else { | ||
console.error('[SpeechlyClient] Maximum reconnect count reached, giving up.') | ||
this.setState(ClientState.Failed) | ||
return | ||
} | ||
// Make sure we don't have concurrent reconnection procedures or attempt to reconnect from a failed state. | ||
if (this.state === ClientState.Connecting || this.state === ClientState.Failed) { | ||
return | ||
} | ||
this.setState(ClientState.Connecting) | ||
} | ||
@@ -612,5 +635,4 @@ | ||
function generateWsUrl(baseUrl: string, languageCode: string, sampleRate: number): string { | ||
function generateWsUrl(baseUrl: string, sampleRate: number): string { | ||
const params = new URLSearchParams() | ||
params.append('languageCode', languageCode) | ||
params.append('sampleRate', sampleRate.toString()) | ||
@@ -617,0 +639,0 @@ |
@@ -16,2 +16,7 @@ import { Microphone } from '../microphone' | ||
/** | ||
* Connect to Speechly upon creating the client instance. Defaults to true. | ||
*/ | ||
connect?: boolean | ||
/** | ||
* The unique identifier of a project in the dashboard. | ||
@@ -22,2 +27,3 @@ */ | ||
/** | ||
* @deprecated | ||
* The language which is used by the app. | ||
@@ -24,0 +30,0 @@ */ |
@@ -37,2 +37,3 @@ /** | ||
Opened = 'WEBSOCKET_OPEN', | ||
Closed = 'WEBSOCKET_CLOSED', | ||
SourceSampleRateSetSuccess = 'SOURSE_SAMPLE_RATE_SET_SUCCESS', | ||
@@ -152,3 +153,3 @@ Started = 'started', | ||
*/ | ||
export type CloseCallback = (err: Error) => void | ||
export type CloseCallback = (err: {code: number, reason: string, wasClean: boolean}) => void | ||
@@ -155,0 +156,0 @@ /** |
@@ -127,2 +127,9 @@ import { APIClient, ResponseCallback, CloseCallback, WebsocketResponse, WebsocketResponseType } from './types' | ||
break | ||
case WebsocketResponseType.Closed: | ||
this.onCloseCb({ | ||
code: event.data.code, | ||
reason: event.data.reason, | ||
wasClean: event.data.wasClean, | ||
}) | ||
break | ||
case WebsocketResponseType.SourceSampleRateSetSuccess: | ||
@@ -129,0 +136,0 @@ if (this.resolveSourceSampleRateSet != null) { |
@@ -27,5 +27,16 @@ export default `/** | ||
this.initialized = false; | ||
// WebSocket's close handler, called e.g. when | ||
// - normal close (code 1000) | ||
// - network unreachable or unable to (re)connect (code 1006) | ||
// List of CloseEvent.code values: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code | ||
this.onWebsocketClose = function (event) { | ||
if (_this.debug) { | ||
console.log('[SpeechlyClient]', 'onWebsocketClose'); | ||
} | ||
_this.websocket.removeEventListener('open', _this.onWebsocketOpen); | ||
_this.websocket.removeEventListener('message', _this.onWebsocketMessage); | ||
_this.websocket.removeEventListener('error', _this.onWebsocketError); | ||
_this.websocket.removeEventListener('close', _this.onWebsocketClose); | ||
_this.websocket = undefined; | ||
_this.connect(0); | ||
_this.workerCtx.postMessage({ type: 'WEBSOCKET_CLOSED', code: event.code, reason: event.reason, wasClean: event.wasClean }); | ||
}; | ||
@@ -45,3 +56,2 @@ this.onWebsocketOpen = function (_event) { | ||
} | ||
_this.closeWebsocket(); | ||
}; | ||
@@ -69,6 +79,2 @@ this.onWebsocketMessage = function (event) { | ||
WebsocketClient.prototype.init = function (apiUrl, authToken, targetSampleRate, debug) { | ||
if (this.initialized) { | ||
console.log('[SpeechlyClient]', 'already initialized'); | ||
return; | ||
} | ||
this.debug = debug; | ||
@@ -201,3 +207,3 @@ if (this.debug) { | ||
WebsocketClient.prototype.stopContext = function () { | ||
if (this.websocket == undefined) { | ||
if (!this.websocket) { | ||
throw Error('Cant start context: websocket is undefined'); | ||
@@ -215,3 +221,3 @@ } | ||
WebsocketClient.prototype.switchContext = function (newAppId) { | ||
if (this.websocket == undefined) { | ||
if (!this.websocket) { | ||
throw Error('Cant switch context: websocket is undefined'); | ||
@@ -233,11 +239,12 @@ } | ||
}; | ||
WebsocketClient.prototype.closeWebsocket = function () { | ||
if (this.websocket == null) { | ||
WebsocketClient.prototype.closeWebsocket = function (websocketCode, reason) { | ||
if (websocketCode === void 0) { websocketCode = 1005; } | ||
if (reason === void 0) { reason = "No Status Received"; } | ||
if (this.debug) { | ||
console.log('[SpeechlyClient]', 'Websocket closing'); | ||
} | ||
if (!this.websocket) { | ||
throw Error('Websocket is not open'); | ||
} | ||
this.websocket.removeEventListener('open', this.onWebsocketOpen); | ||
this.websocket.removeEventListener('message', this.onWebsocketMessage); | ||
this.websocket.removeEventListener('error', this.onWebsocketError); | ||
this.websocket.removeEventListener('close', this.onWebsocketClose); | ||
this.websocket.close(); | ||
this.websocket.close(websocketCode, reason); | ||
}; | ||
@@ -294,3 +301,3 @@ WebsocketClient.prototype.downsample = function (input) { | ||
case 'CLOSE': | ||
websocketClient.closeWebsocket(); | ||
websocketClient.closeWebsocket(1000, "Close requested by client"); | ||
break; | ||
@@ -297,0 +304,0 @@ case 'START_CONTEXT': |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
2
2
347265
5147
- Removedlocale-code@^2.0.2
- Removediso-3166-1-alpha-2@1.0.2(transitive)
- Removediso-639-1@2.1.15(transitive)
- Removediso-639-1-zh@2.0.4(transitive)
- Removedlocale-code@2.0.2(transitive)
- Removedmout@1.2.4(transitive)