@absolutejs/voice 0.0.22-beta.655 → 0.0.22-beta.657

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vue/index.js CHANGED
@@ -10838,11 +10838,19 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
10838
10838
  var DEFAULT_PING_INTERVAL = 30000;
10839
10839
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
10840
10840
  var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
10841
+ var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
10841
10842
  var MAX_PENDING_CONTROL_MESSAGES = 100;
10842
10843
  var RECONNECT_BASE_DELAY_MS = 500;
10843
10844
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
10844
10845
  var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
10845
10846
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
10847
+
10848
+ class VoiceReconnectRejectedError extends Error {
10849
+ constructor(message = "Voice authorization could not be renewed.") {
10850
+ super(message);
10851
+ this.name = "VoiceReconnectRejectedError";
10852
+ }
10853
+ }
10846
10854
  var noop = () => {};
10847
10855
  var noopAsync = () => Promise.resolve();
10848
10856
  var noopUnsubscribe = () => noop;
@@ -10922,6 +10930,9 @@ var createVoiceConnection = (path, options = {}) => {
10922
10930
  const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
10923
10931
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
10924
10932
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
10933
+ let lifecycleGeneration = 0;
10934
+ let disposed = false;
10935
+ let reconnectPreparationAbort = null;
10925
10936
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
10926
10937
  const state = {
10927
10938
  isConnected: false,
@@ -10985,6 +10996,9 @@ var createVoiceConnection = (path, options = {}) => {
10985
10996
  pendingCallControls.clear();
10986
10997
  };
10987
10998
  const scheduleReconnect = () => {
10999
+ if (disposed)
11000
+ return;
11001
+ const scheduledGeneration = lifecycleGeneration;
10988
11002
  state.reconnectAttempts += 1;
10989
11003
  const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
10990
11004
  const nextAttemptAt = Date.now() + delayMs;
@@ -10999,18 +11013,44 @@ var createVoiceConnection = (path, options = {}) => {
10999
11013
  type: "connection"
11000
11014
  });
11001
11015
  state.reconnectTimeout = setTimeout(async () => {
11016
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
11017
+ return;
11002
11018
  if (state.reconnectAttempts > maxReconnectAttempts) {
11003
11019
  emitTerminalFailure("Voice connection could not be restored.");
11004
11020
  return;
11005
11021
  }
11006
11022
  try {
11007
- await options.prepareReconnect?.({
11008
- attempt: state.reconnectAttempts,
11009
- path,
11010
- scenarioId: state.scenarioId,
11011
- sessionId: state.sessionId
11012
- });
11013
- } catch {
11023
+ if (options.prepareReconnect) {
11024
+ const preparationAbort = new AbortController;
11025
+ reconnectPreparationAbort = preparationAbort;
11026
+ const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
11027
+ try {
11028
+ await Promise.race([
11029
+ options.prepareReconnect({
11030
+ attempt: state.reconnectAttempts,
11031
+ path,
11032
+ scenarioId: state.scenarioId,
11033
+ sessionId: state.sessionId,
11034
+ signal: preparationAbort.signal
11035
+ }),
11036
+ new Promise((_, reject) => {
11037
+ preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
11038
+ })
11039
+ ]);
11040
+ } finally {
11041
+ clearTimeout(timeout);
11042
+ if (reconnectPreparationAbort === preparationAbort) {
11043
+ reconnectPreparationAbort = null;
11044
+ }
11045
+ }
11046
+ }
11047
+ } catch (error) {
11048
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
11049
+ return;
11050
+ if (error instanceof VoiceReconnectRejectedError) {
11051
+ emitTerminalFailure(error.message);
11052
+ return;
11053
+ }
11014
11054
  if (state.reconnectAttempts < maxReconnectAttempts) {
11015
11055
  scheduleReconnect();
11016
11056
  } else {
@@ -11018,10 +11058,14 @@ var createVoiceConnection = (path, options = {}) => {
11018
11058
  }
11019
11059
  return;
11020
11060
  }
11061
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
11062
+ return;
11021
11063
  connect();
11022
11064
  }, delayMs);
11023
11065
  };
11024
11066
  const connect = () => {
11067
+ if (disposed)
11068
+ return;
11025
11069
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
11026
11070
  ws.binaryType = "arraybuffer";
11027
11071
  ws.onopen = () => {
@@ -11092,6 +11136,7 @@ var createVoiceConnection = (path, options = {}) => {
11092
11136
  state.ws = null;
11093
11137
  state.isConnected = false;
11094
11138
  clearTimers();
11139
+ rejectPendingCallControls("Voice connection interrupted");
11095
11140
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
11096
11141
  const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
11097
11142
  if (authorizationFailure) {
@@ -11122,6 +11167,8 @@ var createVoiceConnection = (path, options = {}) => {
11122
11167
  state.ws = ws;
11123
11168
  };
11124
11169
  const sendSerialized = (value) => {
11170
+ if (disposed)
11171
+ return;
11125
11172
  if (state.ws?.readyState === WS_OPEN) {
11126
11173
  state.ws.send(value);
11127
11174
  return;
@@ -11157,6 +11204,9 @@ var createVoiceConnection = (path, options = {}) => {
11157
11204
  send({ type: "end_turn" });
11158
11205
  };
11159
11206
  const callControl = (message) => {
11207
+ if (state.ws?.readyState !== WS_OPEN) {
11208
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
11209
+ }
11160
11210
  const requestId = crypto.randomUUID();
11161
11211
  const { promise, reject, resolve } = Promise.withResolvers();
11162
11212
  const timeout = setTimeout(() => {
@@ -11164,16 +11214,21 @@ var createVoiceConnection = (path, options = {}) => {
11164
11214
  reject(new Error(`${message.action} acknowledgement timed out`));
11165
11215
  }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
11166
11216
  pendingCallControls.set(requestId, { reject, resolve, timeout });
11167
- send({
11217
+ state.ws.send(JSON.stringify({
11168
11218
  ...message,
11169
11219
  requestId,
11170
11220
  type: "call_control"
11171
- });
11221
+ }));
11172
11222
  return promise;
11173
11223
  };
11174
11224
  const close = (reason = "client-close") => {
11225
+ disposed = true;
11226
+ reconnectPreparationAbort?.abort();
11227
+ reconnectPreparationAbort = null;
11228
+ lifecycleGeneration += 1;
11175
11229
  clearTimers();
11176
11230
  rejectPendingCallControls("Voice connection closed");
11231
+ state.pendingMessages.length = 0;
11177
11232
  if (state.ws) {
11178
11233
  const ws = state.ws;
11179
11234
  state.ws = null;
@@ -11186,8 +11241,13 @@ var createVoiceConnection = (path, options = {}) => {
11186
11241
  listeners.clear();
11187
11242
  };
11188
11243
  const disconnect = () => {
11244
+ disposed = true;
11245
+ reconnectPreparationAbort?.abort();
11246
+ reconnectPreparationAbort = null;
11247
+ lifecycleGeneration += 1;
11189
11248
  clearTimers();
11190
11249
  rejectPendingCallControls("Voice connection disconnected");
11250
+ state.pendingMessages.length = 0;
11191
11251
  if (state.ws) {
11192
11252
  const ws = state.ws;
11193
11253
  state.ws = null;
@@ -11435,7 +11495,10 @@ var createVoiceStream = (path, options = {}) => {
11435
11495
  getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
11436
11496
  }) : null;
11437
11497
  const subscribers = new Set;
11498
+ let disposed = false;
11438
11499
  const start = (input) => Promise.resolve().then(() => {
11500
+ if (disposed)
11501
+ return;
11439
11502
  if (!input?.sessionId && !input?.scenarioId) {
11440
11503
  return;
11441
11504
  }
@@ -11494,6 +11557,7 @@ var createVoiceStream = (path, options = {}) => {
11494
11557
  return connection.callControl(message);
11495
11558
  },
11496
11559
  close(reason) {
11560
+ disposed = true;
11497
11561
  unsubscribeConnection();
11498
11562
  browserMediaReporter?.close();
11499
11563
  connection.close(reason);
@@ -11501,6 +11565,7 @@ var createVoiceStream = (path, options = {}) => {
11501
11565
  notify();
11502
11566
  },
11503
11567
  disconnect() {
11568
+ disposed = true;
11504
11569
  unsubscribeConnection();
11505
11570
  browserMediaReporter?.close();
11506
11571
  connection.disconnect();
@@ -11719,48 +11784,89 @@ var createMicrophoneCapture = (options) => {
11719
11784
  let sourceNode = null;
11720
11785
  let processorNode = null;
11721
11786
  let mediaStream = null;
11722
- const start = async () => {
11723
- if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
11724
- throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
11725
- }
11726
- const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
11727
- if (!AudioContextCtor) {
11728
- throw new Error("Browser microphone capture requires AudioContext support.");
11729
- }
11730
- try {
11731
- mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
11732
- audio: {
11733
- autoGainControl: true,
11734
- channelCount: options.channelCount ?? 1,
11735
- echoCancellation: true,
11736
- noiseSuppression: true
11787
+ let lifecycleGeneration = 0;
11788
+ let startPromise = null;
11789
+ const release = (resources) => {
11790
+ resources.processorNode?.disconnect();
11791
+ resources.sourceNode?.disconnect();
11792
+ resources.mediaStream?.getTracks().forEach((track) => track.stop());
11793
+ resources.audioContext?.close();
11794
+ };
11795
+ const start = () => {
11796
+ if (mediaStream)
11797
+ return Promise.resolve();
11798
+ if (startPromise)
11799
+ return startPromise;
11800
+ const generation = ++lifecycleGeneration;
11801
+ const operation = async () => {
11802
+ if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
11803
+ throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
11804
+ }
11805
+ const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
11806
+ if (!AudioContextCtor) {
11807
+ throw new Error("Browser microphone capture requires AudioContext support.");
11808
+ }
11809
+ let pendingAudioContext = null;
11810
+ let pendingMediaStream = null;
11811
+ let pendingProcessorNode = null;
11812
+ let pendingSourceNode = null;
11813
+ try {
11814
+ pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
11815
+ audio: {
11816
+ autoGainControl: true,
11817
+ channelCount: options.channelCount ?? 1,
11818
+ echoCancellation: true,
11819
+ noiseSuppression: true
11820
+ }
11821
+ });
11822
+ if (generation !== lifecycleGeneration) {
11823
+ throw new Error("Microphone capture stopped during startup.");
11737
11824
  }
11738
- });
11739
- audioContext = new AudioContextCtor;
11740
- if (audioContext.state === "suspended") {
11741
- await audioContext.resume();
11825
+ pendingAudioContext = new AudioContextCtor;
11826
+ if (pendingAudioContext.state === "suspended") {
11827
+ await pendingAudioContext.resume();
11828
+ }
11829
+ if (generation !== lifecycleGeneration) {
11830
+ throw new Error("Microphone capture stopped during startup.");
11831
+ }
11832
+ pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
11833
+ pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
11834
+ pendingProcessorNode.onaudioprocess = (event) => {
11835
+ const channel = event.inputBuffer.getChannelData(0);
11836
+ const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
11837
+ const pcm = floatTo16BitPCM(downsampled);
11838
+ options.onLevel?.(getPcmLevel(pcm));
11839
+ options.onAudio(pcm);
11840
+ };
11841
+ pendingSourceNode.connect(pendingProcessorNode);
11842
+ pendingProcessorNode.connect(pendingAudioContext.destination);
11843
+ if (generation !== lifecycleGeneration) {
11844
+ throw new Error("Microphone capture stopped during startup.");
11845
+ }
11846
+ audioContext = pendingAudioContext;
11847
+ mediaStream = pendingMediaStream;
11848
+ processorNode = pendingProcessorNode;
11849
+ sourceNode = pendingSourceNode;
11850
+ } catch (error) {
11851
+ release({
11852
+ audioContext: pendingAudioContext,
11853
+ mediaStream: pendingMediaStream,
11854
+ processorNode: pendingProcessorNode,
11855
+ sourceNode: pendingSourceNode
11856
+ });
11857
+ throw error;
11742
11858
  }
11743
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
11744
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
11745
- processorNode.onaudioprocess = (event) => {
11746
- const channel = event.inputBuffer.getChannelData(0);
11747
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
11748
- const pcm = floatTo16BitPCM(downsampled);
11749
- options.onLevel?.(getPcmLevel(pcm));
11750
- options.onAudio(pcm);
11751
- };
11752
- sourceNode.connect(processorNode);
11753
- processorNode.connect(audioContext.destination);
11754
- } catch (error) {
11755
- stop();
11756
- throw error;
11757
- }
11859
+ };
11860
+ const pending = operation().finally(() => {
11861
+ if (startPromise === pending)
11862
+ startPromise = null;
11863
+ });
11864
+ startPromise = pending;
11865
+ return pending;
11758
11866
  };
11759
11867
  const stop = () => {
11760
- processorNode?.disconnect();
11761
- sourceNode?.disconnect();
11762
- mediaStream?.getTracks().forEach((track) => track.stop());
11763
- audioContext?.close();
11868
+ lifecycleGeneration += 1;
11869
+ release({ audioContext, mediaStream, processorNode, sourceNode });
11764
11870
  options.onLevel?.(0);
11765
11871
  audioContext = null;
11766
11872
  mediaStream = null;
@@ -12116,6 +12222,9 @@ var createVoiceController = (path, options = {}) => {
12116
12222
  ...options.connection
12117
12223
  });
12118
12224
  let capture = null;
12225
+ let captureGeneration = 0;
12226
+ let disposed = false;
12227
+ let startRecordingPromise = null;
12119
12228
  let state = createInitialState2(stream);
12120
12229
  const subscribers = new Set;
12121
12230
  const notify = () => {
@@ -12172,6 +12281,7 @@ var createVoiceController = (path, options = {}) => {
12172
12281
  return capture;
12173
12282
  };
12174
12283
  const stopRecording = () => {
12284
+ captureGeneration += 1;
12175
12285
  capture?.stop();
12176
12286
  capture = null;
12177
12287
  state = {
@@ -12181,39 +12291,63 @@ var createVoiceController = (path, options = {}) => {
12181
12291
  notify();
12182
12292
  };
12183
12293
  const startRecording = async () => {
12294
+ if (disposed) {
12295
+ throw new Error("Voice controller is closed.");
12296
+ }
12184
12297
  if (state.isRecording) {
12185
12298
  return;
12186
12299
  }
12187
- try {
12188
- state = {
12189
- ...state,
12190
- recordingError: null
12191
- };
12192
- notify();
12193
- await ensureCapture().start();
12194
- state = {
12195
- ...state,
12196
- isRecording: true
12197
- };
12198
- notify();
12199
- } catch (error) {
12200
- capture?.stop();
12201
- capture = null;
12202
- state = {
12203
- ...state,
12204
- isRecording: false,
12205
- recordingError: error instanceof Error ? error.message : String(error)
12206
- };
12207
- notify();
12208
- throw error;
12209
- }
12300
+ if (startRecordingPromise)
12301
+ return startRecordingPromise;
12302
+ const generation = ++captureGeneration;
12303
+ const activeCapture = ensureCapture();
12304
+ const operation = (async () => {
12305
+ try {
12306
+ state = {
12307
+ ...state,
12308
+ recordingError: null
12309
+ };
12310
+ notify();
12311
+ await activeCapture.start();
12312
+ if (disposed || generation !== captureGeneration || capture !== activeCapture) {
12313
+ activeCapture.stop();
12314
+ return;
12315
+ }
12316
+ state = {
12317
+ ...state,
12318
+ isRecording: true
12319
+ };
12320
+ notify();
12321
+ } catch (error) {
12322
+ activeCapture.stop();
12323
+ if (generation !== captureGeneration || capture !== activeCapture) {
12324
+ throw error;
12325
+ }
12326
+ capture = null;
12327
+ state = {
12328
+ ...state,
12329
+ isRecording: false,
12330
+ recordingError: error instanceof Error ? error.message : String(error)
12331
+ };
12332
+ notify();
12333
+ throw error;
12334
+ }
12335
+ })();
12336
+ const pending = operation.finally(() => {
12337
+ if (startRecordingPromise === pending)
12338
+ startRecordingPromise = null;
12339
+ });
12340
+ startRecordingPromise = pending;
12341
+ return startRecordingPromise;
12210
12342
  };
12211
12343
  const close = (reason) => {
12344
+ disposed = true;
12212
12345
  unsubscribeStream();
12213
12346
  stopRecording();
12214
12347
  stream.close(reason);
12215
12348
  };
12216
12349
  const disconnect = () => {
12350
+ disposed = true;
12217
12351
  unsubscribeStream();
12218
12352
  stopRecording();
12219
12353
  stream.disconnect();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.655",
3
+ "version": "0.0.22-beta.657",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",