@absolutejs/voice 0.0.22-beta.656 → 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.
@@ -11442,6 +11442,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
11442
11442
  var DEFAULT_PING_INTERVAL = 30000;
11443
11443
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
11444
11444
  var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
11445
+ var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
11445
11446
  var MAX_PENDING_CONTROL_MESSAGES = 100;
11446
11447
  var RECONNECT_BASE_DELAY_MS = 500;
11447
11448
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
@@ -11535,6 +11536,7 @@ var createVoiceConnection = (path, options = {}) => {
11535
11536
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
11536
11537
  let lifecycleGeneration = 0;
11537
11538
  let disposed = false;
11539
+ let reconnectPreparationAbort = null;
11538
11540
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
11539
11541
  const state = {
11540
11542
  isConnected: false,
@@ -11622,12 +11624,30 @@ var createVoiceConnection = (path, options = {}) => {
11622
11624
  return;
11623
11625
  }
11624
11626
  try {
11625
- await options.prepareReconnect?.({
11626
- attempt: state.reconnectAttempts,
11627
- path,
11628
- scenarioId: state.scenarioId,
11629
- sessionId: state.sessionId
11630
- });
11627
+ if (options.prepareReconnect) {
11628
+ const preparationAbort = new AbortController;
11629
+ reconnectPreparationAbort = preparationAbort;
11630
+ const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
11631
+ try {
11632
+ await Promise.race([
11633
+ options.prepareReconnect({
11634
+ attempt: state.reconnectAttempts,
11635
+ path,
11636
+ scenarioId: state.scenarioId,
11637
+ sessionId: state.sessionId,
11638
+ signal: preparationAbort.signal
11639
+ }),
11640
+ new Promise((_, reject) => {
11641
+ preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
11642
+ })
11643
+ ]);
11644
+ } finally {
11645
+ clearTimeout(timeout);
11646
+ if (reconnectPreparationAbort === preparationAbort) {
11647
+ reconnectPreparationAbort = null;
11648
+ }
11649
+ }
11650
+ }
11631
11651
  } catch (error) {
11632
11652
  if (disposed || lifecycleGeneration !== scheduledGeneration)
11633
11653
  return;
@@ -11751,6 +11771,8 @@ var createVoiceConnection = (path, options = {}) => {
11751
11771
  state.ws = ws;
11752
11772
  };
11753
11773
  const sendSerialized = (value) => {
11774
+ if (disposed)
11775
+ return;
11754
11776
  if (state.ws?.readyState === WS_OPEN) {
11755
11777
  state.ws.send(value);
11756
11778
  return;
@@ -11805,6 +11827,8 @@ var createVoiceConnection = (path, options = {}) => {
11805
11827
  };
11806
11828
  const close = (reason = "client-close") => {
11807
11829
  disposed = true;
11830
+ reconnectPreparationAbort?.abort();
11831
+ reconnectPreparationAbort = null;
11808
11832
  lifecycleGeneration += 1;
11809
11833
  clearTimers();
11810
11834
  rejectPendingCallControls("Voice connection closed");
@@ -11822,6 +11846,8 @@ var createVoiceConnection = (path, options = {}) => {
11822
11846
  };
11823
11847
  const disconnect = () => {
11824
11848
  disposed = true;
11849
+ reconnectPreparationAbort?.abort();
11850
+ reconnectPreparationAbort = null;
11825
11851
  lifecycleGeneration += 1;
11826
11852
  clearTimers();
11827
11853
  rejectPendingCallControls("Voice connection disconnected");
@@ -12073,7 +12099,10 @@ var createVoiceStream = (path, options = {}) => {
12073
12099
  getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
12074
12100
  }) : null;
12075
12101
  const subscribers = new Set;
12102
+ let disposed = false;
12076
12103
  const start = (input) => Promise.resolve().then(() => {
12104
+ if (disposed)
12105
+ return;
12077
12106
  if (!input?.sessionId && !input?.scenarioId) {
12078
12107
  return;
12079
12108
  }
@@ -12132,6 +12161,7 @@ var createVoiceStream = (path, options = {}) => {
12132
12161
  return connection.callControl(message);
12133
12162
  },
12134
12163
  close(reason) {
12164
+ disposed = true;
12135
12165
  unsubscribeConnection();
12136
12166
  browserMediaReporter?.close();
12137
12167
  connection.close(reason);
@@ -12139,6 +12169,7 @@ var createVoiceStream = (path, options = {}) => {
12139
12169
  notify();
12140
12170
  },
12141
12171
  disconnect() {
12172
+ disposed = true;
12142
12173
  unsubscribeConnection();
12143
12174
  browserMediaReporter?.close();
12144
12175
  connection.disconnect();
@@ -12336,48 +12367,89 @@ var createMicrophoneCapture = (options) => {
12336
12367
  let sourceNode = null;
12337
12368
  let processorNode = null;
12338
12369
  let mediaStream = null;
12339
- const start = async () => {
12340
- if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
12341
- throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
12342
- }
12343
- const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
12344
- if (!AudioContextCtor) {
12345
- throw new Error("Browser microphone capture requires AudioContext support.");
12346
- }
12347
- try {
12348
- mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
12349
- audio: {
12350
- autoGainControl: true,
12351
- channelCount: options.channelCount ?? 1,
12352
- echoCancellation: true,
12353
- noiseSuppression: true
12370
+ let lifecycleGeneration = 0;
12371
+ let startPromise = null;
12372
+ const release = (resources) => {
12373
+ resources.processorNode?.disconnect();
12374
+ resources.sourceNode?.disconnect();
12375
+ resources.mediaStream?.getTracks().forEach((track) => track.stop());
12376
+ resources.audioContext?.close();
12377
+ };
12378
+ const start = () => {
12379
+ if (mediaStream)
12380
+ return Promise.resolve();
12381
+ if (startPromise)
12382
+ return startPromise;
12383
+ const generation = ++lifecycleGeneration;
12384
+ const operation = async () => {
12385
+ if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
12386
+ throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
12387
+ }
12388
+ const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
12389
+ if (!AudioContextCtor) {
12390
+ throw new Error("Browser microphone capture requires AudioContext support.");
12391
+ }
12392
+ let pendingAudioContext = null;
12393
+ let pendingMediaStream = null;
12394
+ let pendingProcessorNode = null;
12395
+ let pendingSourceNode = null;
12396
+ try {
12397
+ pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
12398
+ audio: {
12399
+ autoGainControl: true,
12400
+ channelCount: options.channelCount ?? 1,
12401
+ echoCancellation: true,
12402
+ noiseSuppression: true
12403
+ }
12404
+ });
12405
+ if (generation !== lifecycleGeneration) {
12406
+ throw new Error("Microphone capture stopped during startup.");
12354
12407
  }
12355
- });
12356
- audioContext = new AudioContextCtor;
12357
- if (audioContext.state === "suspended") {
12358
- await audioContext.resume();
12408
+ pendingAudioContext = new AudioContextCtor;
12409
+ if (pendingAudioContext.state === "suspended") {
12410
+ await pendingAudioContext.resume();
12411
+ }
12412
+ if (generation !== lifecycleGeneration) {
12413
+ throw new Error("Microphone capture stopped during startup.");
12414
+ }
12415
+ pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
12416
+ pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
12417
+ pendingProcessorNode.onaudioprocess = (event) => {
12418
+ const channel = event.inputBuffer.getChannelData(0);
12419
+ const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
12420
+ const pcm = floatTo16BitPCM(downsampled);
12421
+ options.onLevel?.(getPcmLevel(pcm));
12422
+ options.onAudio(pcm);
12423
+ };
12424
+ pendingSourceNode.connect(pendingProcessorNode);
12425
+ pendingProcessorNode.connect(pendingAudioContext.destination);
12426
+ if (generation !== lifecycleGeneration) {
12427
+ throw new Error("Microphone capture stopped during startup.");
12428
+ }
12429
+ audioContext = pendingAudioContext;
12430
+ mediaStream = pendingMediaStream;
12431
+ processorNode = pendingProcessorNode;
12432
+ sourceNode = pendingSourceNode;
12433
+ } catch (error) {
12434
+ release({
12435
+ audioContext: pendingAudioContext,
12436
+ mediaStream: pendingMediaStream,
12437
+ processorNode: pendingProcessorNode,
12438
+ sourceNode: pendingSourceNode
12439
+ });
12440
+ throw error;
12359
12441
  }
12360
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
12361
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
12362
- processorNode.onaudioprocess = (event) => {
12363
- const channel = event.inputBuffer.getChannelData(0);
12364
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
12365
- const pcm = floatTo16BitPCM(downsampled);
12366
- options.onLevel?.(getPcmLevel(pcm));
12367
- options.onAudio(pcm);
12368
- };
12369
- sourceNode.connect(processorNode);
12370
- processorNode.connect(audioContext.destination);
12371
- } catch (error) {
12372
- stop();
12373
- throw error;
12374
- }
12442
+ };
12443
+ const pending = operation().finally(() => {
12444
+ if (startPromise === pending)
12445
+ startPromise = null;
12446
+ });
12447
+ startPromise = pending;
12448
+ return pending;
12375
12449
  };
12376
12450
  const stop = () => {
12377
- processorNode?.disconnect();
12378
- sourceNode?.disconnect();
12379
- mediaStream?.getTracks().forEach((track) => track.stop());
12380
- audioContext?.close();
12451
+ lifecycleGeneration += 1;
12452
+ release({ audioContext, mediaStream, processorNode, sourceNode });
12381
12453
  options.onLevel?.(0);
12382
12454
  audioContext = null;
12383
12455
  mediaStream = null;
@@ -12733,6 +12805,9 @@ var createVoiceController = (path, options = {}) => {
12733
12805
  ...options.connection
12734
12806
  });
12735
12807
  let capture = null;
12808
+ let captureGeneration = 0;
12809
+ let disposed = false;
12810
+ let startRecordingPromise = null;
12736
12811
  let state = createInitialState2(stream);
12737
12812
  const subscribers = new Set;
12738
12813
  const notify = () => {
@@ -12789,6 +12864,7 @@ var createVoiceController = (path, options = {}) => {
12789
12864
  return capture;
12790
12865
  };
12791
12866
  const stopRecording = () => {
12867
+ captureGeneration += 1;
12792
12868
  capture?.stop();
12793
12869
  capture = null;
12794
12870
  state = {
@@ -12798,39 +12874,63 @@ var createVoiceController = (path, options = {}) => {
12798
12874
  notify();
12799
12875
  };
12800
12876
  const startRecording = async () => {
12877
+ if (disposed) {
12878
+ throw new Error("Voice controller is closed.");
12879
+ }
12801
12880
  if (state.isRecording) {
12802
12881
  return;
12803
12882
  }
12804
- try {
12805
- state = {
12806
- ...state,
12807
- recordingError: null
12808
- };
12809
- notify();
12810
- await ensureCapture().start();
12811
- state = {
12812
- ...state,
12813
- isRecording: true
12814
- };
12815
- notify();
12816
- } catch (error) {
12817
- capture?.stop();
12818
- capture = null;
12819
- state = {
12820
- ...state,
12821
- isRecording: false,
12822
- recordingError: error instanceof Error ? error.message : String(error)
12823
- };
12824
- notify();
12825
- throw error;
12826
- }
12883
+ if (startRecordingPromise)
12884
+ return startRecordingPromise;
12885
+ const generation = ++captureGeneration;
12886
+ const activeCapture = ensureCapture();
12887
+ const operation = (async () => {
12888
+ try {
12889
+ state = {
12890
+ ...state,
12891
+ recordingError: null
12892
+ };
12893
+ notify();
12894
+ await activeCapture.start();
12895
+ if (disposed || generation !== captureGeneration || capture !== activeCapture) {
12896
+ activeCapture.stop();
12897
+ return;
12898
+ }
12899
+ state = {
12900
+ ...state,
12901
+ isRecording: true
12902
+ };
12903
+ notify();
12904
+ } catch (error) {
12905
+ activeCapture.stop();
12906
+ if (generation !== captureGeneration || capture !== activeCapture) {
12907
+ throw error;
12908
+ }
12909
+ capture = null;
12910
+ state = {
12911
+ ...state,
12912
+ isRecording: false,
12913
+ recordingError: error instanceof Error ? error.message : String(error)
12914
+ };
12915
+ notify();
12916
+ throw error;
12917
+ }
12918
+ })();
12919
+ const pending = operation.finally(() => {
12920
+ if (startRecordingPromise === pending)
12921
+ startRecordingPromise = null;
12922
+ });
12923
+ startRecordingPromise = pending;
12924
+ return startRecordingPromise;
12827
12925
  };
12828
12926
  const close = (reason) => {
12927
+ disposed = true;
12829
12928
  unsubscribeStream();
12830
12929
  stopRecording();
12831
12930
  stream.close(reason);
12832
12931
  };
12833
12932
  const disconnect = () => {
12933
+ disposed = true;
12834
12934
  unsubscribeStream();
12835
12935
  stopRecording();
12836
12936
  stream.disconnect();
@@ -517,48 +517,89 @@ var createMicrophoneCapture = (options) => {
517
517
  let sourceNode = null;
518
518
  let processorNode = null;
519
519
  let mediaStream = null;
520
- const start = async () => {
521
- if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
522
- throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
523
- }
524
- const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
525
- if (!AudioContextCtor) {
526
- throw new Error("Browser microphone capture requires AudioContext support.");
527
- }
528
- try {
529
- mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
530
- audio: {
531
- autoGainControl: true,
532
- channelCount: options.channelCount ?? 1,
533
- echoCancellation: true,
534
- noiseSuppression: true
520
+ let lifecycleGeneration = 0;
521
+ let startPromise = null;
522
+ const release = (resources) => {
523
+ resources.processorNode?.disconnect();
524
+ resources.sourceNode?.disconnect();
525
+ resources.mediaStream?.getTracks().forEach((track) => track.stop());
526
+ resources.audioContext?.close();
527
+ };
528
+ const start = () => {
529
+ if (mediaStream)
530
+ return Promise.resolve();
531
+ if (startPromise)
532
+ return startPromise;
533
+ const generation = ++lifecycleGeneration;
534
+ const operation = async () => {
535
+ if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
536
+ throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
537
+ }
538
+ const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
539
+ if (!AudioContextCtor) {
540
+ throw new Error("Browser microphone capture requires AudioContext support.");
541
+ }
542
+ let pendingAudioContext = null;
543
+ let pendingMediaStream = null;
544
+ let pendingProcessorNode = null;
545
+ let pendingSourceNode = null;
546
+ try {
547
+ pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
548
+ audio: {
549
+ autoGainControl: true,
550
+ channelCount: options.channelCount ?? 1,
551
+ echoCancellation: true,
552
+ noiseSuppression: true
553
+ }
554
+ });
555
+ if (generation !== lifecycleGeneration) {
556
+ throw new Error("Microphone capture stopped during startup.");
535
557
  }
536
- });
537
- audioContext = new AudioContextCtor;
538
- if (audioContext.state === "suspended") {
539
- await audioContext.resume();
558
+ pendingAudioContext = new AudioContextCtor;
559
+ if (pendingAudioContext.state === "suspended") {
560
+ await pendingAudioContext.resume();
561
+ }
562
+ if (generation !== lifecycleGeneration) {
563
+ throw new Error("Microphone capture stopped during startup.");
564
+ }
565
+ pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
566
+ pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
567
+ pendingProcessorNode.onaudioprocess = (event) => {
568
+ const channel = event.inputBuffer.getChannelData(0);
569
+ const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
570
+ const pcm = floatTo16BitPCM(downsampled);
571
+ options.onLevel?.(getPcmLevel(pcm));
572
+ options.onAudio(pcm);
573
+ };
574
+ pendingSourceNode.connect(pendingProcessorNode);
575
+ pendingProcessorNode.connect(pendingAudioContext.destination);
576
+ if (generation !== lifecycleGeneration) {
577
+ throw new Error("Microphone capture stopped during startup.");
578
+ }
579
+ audioContext = pendingAudioContext;
580
+ mediaStream = pendingMediaStream;
581
+ processorNode = pendingProcessorNode;
582
+ sourceNode = pendingSourceNode;
583
+ } catch (error) {
584
+ release({
585
+ audioContext: pendingAudioContext,
586
+ mediaStream: pendingMediaStream,
587
+ processorNode: pendingProcessorNode,
588
+ sourceNode: pendingSourceNode
589
+ });
590
+ throw error;
540
591
  }
541
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
542
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
543
- processorNode.onaudioprocess = (event) => {
544
- const channel = event.inputBuffer.getChannelData(0);
545
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
546
- const pcm = floatTo16BitPCM(downsampled);
547
- options.onLevel?.(getPcmLevel(pcm));
548
- options.onAudio(pcm);
549
- };
550
- sourceNode.connect(processorNode);
551
- processorNode.connect(audioContext.destination);
552
- } catch (error) {
553
- stop();
554
- throw error;
555
- }
592
+ };
593
+ const pending = operation().finally(() => {
594
+ if (startPromise === pending)
595
+ startPromise = null;
596
+ });
597
+ startPromise = pending;
598
+ return pending;
556
599
  };
557
600
  const stop = () => {
558
- processorNode?.disconnect();
559
- sourceNode?.disconnect();
560
- mediaStream?.getTracks().forEach((track) => track.stop());
561
- audioContext?.close();
601
+ lifecycleGeneration += 1;
602
+ release({ audioContext, mediaStream, processorNode, sourceNode });
562
603
  options.onLevel?.(0);
563
604
  audioContext = null;
564
605
  mediaStream = null;
@@ -770,6 +811,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
770
811
  var DEFAULT_PING_INTERVAL = 30000;
771
812
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
772
813
  var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
814
+ var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
773
815
  var MAX_PENDING_CONTROL_MESSAGES = 100;
774
816
  var RECONNECT_BASE_DELAY_MS = 500;
775
817
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
@@ -863,6 +905,7 @@ var createVoiceConnection = (path, options = {}) => {
863
905
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
864
906
  let lifecycleGeneration = 0;
865
907
  let disposed = false;
908
+ let reconnectPreparationAbort = null;
866
909
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
867
910
  const state = {
868
911
  isConnected: false,
@@ -950,12 +993,30 @@ var createVoiceConnection = (path, options = {}) => {
950
993
  return;
951
994
  }
952
995
  try {
953
- await options.prepareReconnect?.({
954
- attempt: state.reconnectAttempts,
955
- path,
956
- scenarioId: state.scenarioId,
957
- sessionId: state.sessionId
958
- });
996
+ if (options.prepareReconnect) {
997
+ const preparationAbort = new AbortController;
998
+ reconnectPreparationAbort = preparationAbort;
999
+ const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
1000
+ try {
1001
+ await Promise.race([
1002
+ options.prepareReconnect({
1003
+ attempt: state.reconnectAttempts,
1004
+ path,
1005
+ scenarioId: state.scenarioId,
1006
+ sessionId: state.sessionId,
1007
+ signal: preparationAbort.signal
1008
+ }),
1009
+ new Promise((_, reject) => {
1010
+ preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
1011
+ })
1012
+ ]);
1013
+ } finally {
1014
+ clearTimeout(timeout);
1015
+ if (reconnectPreparationAbort === preparationAbort) {
1016
+ reconnectPreparationAbort = null;
1017
+ }
1018
+ }
1019
+ }
959
1020
  } catch (error) {
960
1021
  if (disposed || lifecycleGeneration !== scheduledGeneration)
961
1022
  return;
@@ -1079,6 +1140,8 @@ var createVoiceConnection = (path, options = {}) => {
1079
1140
  state.ws = ws;
1080
1141
  };
1081
1142
  const sendSerialized = (value) => {
1143
+ if (disposed)
1144
+ return;
1082
1145
  if (state.ws?.readyState === WS_OPEN) {
1083
1146
  state.ws.send(value);
1084
1147
  return;
@@ -1133,6 +1196,8 @@ var createVoiceConnection = (path, options = {}) => {
1133
1196
  };
1134
1197
  const close = (reason = "client-close") => {
1135
1198
  disposed = true;
1199
+ reconnectPreparationAbort?.abort();
1200
+ reconnectPreparationAbort = null;
1136
1201
  lifecycleGeneration += 1;
1137
1202
  clearTimers();
1138
1203
  rejectPendingCallControls("Voice connection closed");
@@ -1150,6 +1215,8 @@ var createVoiceConnection = (path, options = {}) => {
1150
1215
  };
1151
1216
  const disconnect = () => {
1152
1217
  disposed = true;
1218
+ reconnectPreparationAbort?.abort();
1219
+ reconnectPreparationAbort = null;
1153
1220
  lifecycleGeneration += 1;
1154
1221
  clearTimers();
1155
1222
  rejectPendingCallControls("Voice connection disconnected");
@@ -1401,7 +1468,10 @@ var createVoiceStream = (path, options = {}) => {
1401
1468
  getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
1402
1469
  }) : null;
1403
1470
  const subscribers = new Set;
1471
+ let disposed = false;
1404
1472
  const start = (input) => Promise.resolve().then(() => {
1473
+ if (disposed)
1474
+ return;
1405
1475
  if (!input?.sessionId && !input?.scenarioId) {
1406
1476
  return;
1407
1477
  }
@@ -1460,6 +1530,7 @@ var createVoiceStream = (path, options = {}) => {
1460
1530
  return connection.callControl(message);
1461
1531
  },
1462
1532
  close(reason) {
1533
+ disposed = true;
1463
1534
  unsubscribeConnection();
1464
1535
  browserMediaReporter?.close();
1465
1536
  connection.close(reason);
@@ -1467,6 +1538,7 @@ var createVoiceStream = (path, options = {}) => {
1467
1538
  notify();
1468
1539
  },
1469
1540
  disconnect() {
1541
+ disposed = true;
1470
1542
  unsubscribeConnection();
1471
1543
  browserMediaReporter?.close();
1472
1544
  connection.disconnect();
@@ -1870,6 +1942,9 @@ var createVoiceController = (path, options = {}) => {
1870
1942
  ...options.connection
1871
1943
  });
1872
1944
  let capture = null;
1945
+ let captureGeneration = 0;
1946
+ let disposed = false;
1947
+ let startRecordingPromise = null;
1873
1948
  let state = createInitialState2(stream);
1874
1949
  const subscribers = new Set;
1875
1950
  const notify = () => {
@@ -1926,6 +2001,7 @@ var createVoiceController = (path, options = {}) => {
1926
2001
  return capture;
1927
2002
  };
1928
2003
  const stopRecording = () => {
2004
+ captureGeneration += 1;
1929
2005
  capture?.stop();
1930
2006
  capture = null;
1931
2007
  state = {
@@ -1935,39 +2011,63 @@ var createVoiceController = (path, options = {}) => {
1935
2011
  notify();
1936
2012
  };
1937
2013
  const startRecording = async () => {
2014
+ if (disposed) {
2015
+ throw new Error("Voice controller is closed.");
2016
+ }
1938
2017
  if (state.isRecording) {
1939
2018
  return;
1940
2019
  }
1941
- try {
1942
- state = {
1943
- ...state,
1944
- recordingError: null
1945
- };
1946
- notify();
1947
- await ensureCapture().start();
1948
- state = {
1949
- ...state,
1950
- isRecording: true
1951
- };
1952
- notify();
1953
- } catch (error) {
1954
- capture?.stop();
1955
- capture = null;
1956
- state = {
1957
- ...state,
1958
- isRecording: false,
1959
- recordingError: error instanceof Error ? error.message : String(error)
1960
- };
1961
- notify();
1962
- throw error;
1963
- }
2020
+ if (startRecordingPromise)
2021
+ return startRecordingPromise;
2022
+ const generation = ++captureGeneration;
2023
+ const activeCapture = ensureCapture();
2024
+ const operation = (async () => {
2025
+ try {
2026
+ state = {
2027
+ ...state,
2028
+ recordingError: null
2029
+ };
2030
+ notify();
2031
+ await activeCapture.start();
2032
+ if (disposed || generation !== captureGeneration || capture !== activeCapture) {
2033
+ activeCapture.stop();
2034
+ return;
2035
+ }
2036
+ state = {
2037
+ ...state,
2038
+ isRecording: true
2039
+ };
2040
+ notify();
2041
+ } catch (error) {
2042
+ activeCapture.stop();
2043
+ if (generation !== captureGeneration || capture !== activeCapture) {
2044
+ throw error;
2045
+ }
2046
+ capture = null;
2047
+ state = {
2048
+ ...state,
2049
+ isRecording: false,
2050
+ recordingError: error instanceof Error ? error.message : String(error)
2051
+ };
2052
+ notify();
2053
+ throw error;
2054
+ }
2055
+ })();
2056
+ const pending = operation.finally(() => {
2057
+ if (startRecordingPromise === pending)
2058
+ startRecordingPromise = null;
2059
+ });
2060
+ startRecordingPromise = pending;
2061
+ return startRecordingPromise;
1964
2062
  };
1965
2063
  const close = (reason) => {
2064
+ disposed = true;
1966
2065
  unsubscribeStream();
1967
2066
  stopRecording();
1968
2067
  stream.close(reason);
1969
2068
  };
1970
2069
  const disconnect = () => {
2070
+ disposed = true;
1971
2071
  unsubscribeStream();
1972
2072
  stopRecording();
1973
2073
  stream.disconnect();