@absolutejs/voice 0.0.22-beta.656 → 0.0.22-beta.658

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.
@@ -335,48 +335,89 @@ var createMicrophoneCapture = (options) => {
335
335
  let sourceNode = null;
336
336
  let processorNode = null;
337
337
  let mediaStream = null;
338
- const start = async () => {
339
- if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
340
- throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
341
- }
342
- const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
343
- if (!AudioContextCtor) {
344
- throw new Error("Browser microphone capture requires AudioContext support.");
345
- }
346
- try {
347
- mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
348
- audio: {
349
- autoGainControl: true,
350
- channelCount: options.channelCount ?? 1,
351
- echoCancellation: true,
352
- noiseSuppression: true
338
+ let lifecycleGeneration = 0;
339
+ let startPromise = null;
340
+ const release = (resources) => {
341
+ resources.processorNode?.disconnect();
342
+ resources.sourceNode?.disconnect();
343
+ resources.mediaStream?.getTracks().forEach((track) => track.stop());
344
+ resources.audioContext?.close();
345
+ };
346
+ const start = () => {
347
+ if (mediaStream)
348
+ return Promise.resolve();
349
+ if (startPromise)
350
+ return startPromise;
351
+ const generation = ++lifecycleGeneration;
352
+ const operation = async () => {
353
+ if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
354
+ throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
355
+ }
356
+ const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
357
+ if (!AudioContextCtor) {
358
+ throw new Error("Browser microphone capture requires AudioContext support.");
359
+ }
360
+ let pendingAudioContext = null;
361
+ let pendingMediaStream = null;
362
+ let pendingProcessorNode = null;
363
+ let pendingSourceNode = null;
364
+ try {
365
+ pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
366
+ audio: {
367
+ autoGainControl: true,
368
+ channelCount: options.channelCount ?? 1,
369
+ echoCancellation: true,
370
+ noiseSuppression: true
371
+ }
372
+ });
373
+ if (generation !== lifecycleGeneration) {
374
+ throw new Error("Microphone capture stopped during startup.");
353
375
  }
354
- });
355
- audioContext = new AudioContextCtor;
356
- if (audioContext.state === "suspended") {
357
- await audioContext.resume();
376
+ pendingAudioContext = new AudioContextCtor;
377
+ if (pendingAudioContext.state === "suspended") {
378
+ await pendingAudioContext.resume();
379
+ }
380
+ if (generation !== lifecycleGeneration) {
381
+ throw new Error("Microphone capture stopped during startup.");
382
+ }
383
+ pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
384
+ pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
385
+ pendingProcessorNode.onaudioprocess = (event) => {
386
+ const channel = event.inputBuffer.getChannelData(0);
387
+ const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
388
+ const pcm = floatTo16BitPCM(downsampled);
389
+ options.onLevel?.(getPcmLevel(pcm));
390
+ options.onAudio(pcm);
391
+ };
392
+ pendingSourceNode.connect(pendingProcessorNode);
393
+ pendingProcessorNode.connect(pendingAudioContext.destination);
394
+ if (generation !== lifecycleGeneration) {
395
+ throw new Error("Microphone capture stopped during startup.");
396
+ }
397
+ audioContext = pendingAudioContext;
398
+ mediaStream = pendingMediaStream;
399
+ processorNode = pendingProcessorNode;
400
+ sourceNode = pendingSourceNode;
401
+ } catch (error) {
402
+ release({
403
+ audioContext: pendingAudioContext,
404
+ mediaStream: pendingMediaStream,
405
+ processorNode: pendingProcessorNode,
406
+ sourceNode: pendingSourceNode
407
+ });
408
+ throw error;
358
409
  }
359
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
360
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
361
- processorNode.onaudioprocess = (event) => {
362
- const channel = event.inputBuffer.getChannelData(0);
363
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
364
- const pcm = floatTo16BitPCM(downsampled);
365
- options.onLevel?.(getPcmLevel(pcm));
366
- options.onAudio(pcm);
367
- };
368
- sourceNode.connect(processorNode);
369
- processorNode.connect(audioContext.destination);
370
- } catch (error) {
371
- stop();
372
- throw error;
373
- }
410
+ };
411
+ const pending = operation().finally(() => {
412
+ if (startPromise === pending)
413
+ startPromise = null;
414
+ });
415
+ startPromise = pending;
416
+ return pending;
374
417
  };
375
418
  const stop = () => {
376
- processorNode?.disconnect();
377
- sourceNode?.disconnect();
378
- mediaStream?.getTracks().forEach((track) => track.stop());
379
- audioContext?.close();
419
+ lifecycleGeneration += 1;
420
+ release({ audioContext, mediaStream, processorNode, sourceNode });
380
421
  options.onLevel?.(0);
381
422
  audioContext = null;
382
423
  mediaStream = null;
@@ -588,6 +629,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
588
629
  var DEFAULT_PING_INTERVAL = 30000;
589
630
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
590
631
  var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
632
+ var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
591
633
  var MAX_PENDING_CONTROL_MESSAGES = 100;
592
634
  var RECONNECT_BASE_DELAY_MS = 500;
593
635
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
@@ -681,6 +723,7 @@ var createVoiceConnection = (path, options = {}) => {
681
723
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
682
724
  let lifecycleGeneration = 0;
683
725
  let disposed = false;
726
+ let reconnectPreparationAbort = null;
684
727
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
685
728
  const state = {
686
729
  isConnected: false,
@@ -768,12 +811,30 @@ var createVoiceConnection = (path, options = {}) => {
768
811
  return;
769
812
  }
770
813
  try {
771
- await options.prepareReconnect?.({
772
- attempt: state.reconnectAttempts,
773
- path,
774
- scenarioId: state.scenarioId,
775
- sessionId: state.sessionId
776
- });
814
+ if (options.prepareReconnect) {
815
+ const preparationAbort = new AbortController;
816
+ reconnectPreparationAbort = preparationAbort;
817
+ const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
818
+ try {
819
+ await Promise.race([
820
+ options.prepareReconnect({
821
+ attempt: state.reconnectAttempts,
822
+ path,
823
+ scenarioId: state.scenarioId,
824
+ sessionId: state.sessionId,
825
+ signal: preparationAbort.signal
826
+ }),
827
+ new Promise((_, reject) => {
828
+ preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
829
+ })
830
+ ]);
831
+ } finally {
832
+ clearTimeout(timeout);
833
+ if (reconnectPreparationAbort === preparationAbort) {
834
+ reconnectPreparationAbort = null;
835
+ }
836
+ }
837
+ }
777
838
  } catch (error) {
778
839
  if (disposed || lifecycleGeneration !== scheduledGeneration)
779
840
  return;
@@ -897,6 +958,8 @@ var createVoiceConnection = (path, options = {}) => {
897
958
  state.ws = ws;
898
959
  };
899
960
  const sendSerialized = (value) => {
961
+ if (disposed)
962
+ return;
900
963
  if (state.ws?.readyState === WS_OPEN) {
901
964
  state.ws.send(value);
902
965
  return;
@@ -951,6 +1014,8 @@ var createVoiceConnection = (path, options = {}) => {
951
1014
  };
952
1015
  const close = (reason = "client-close") => {
953
1016
  disposed = true;
1017
+ reconnectPreparationAbort?.abort();
1018
+ reconnectPreparationAbort = null;
954
1019
  lifecycleGeneration += 1;
955
1020
  clearTimers();
956
1021
  rejectPendingCallControls("Voice connection closed");
@@ -968,6 +1033,8 @@ var createVoiceConnection = (path, options = {}) => {
968
1033
  };
969
1034
  const disconnect = () => {
970
1035
  disposed = true;
1036
+ reconnectPreparationAbort?.abort();
1037
+ reconnectPreparationAbort = null;
971
1038
  lifecycleGeneration += 1;
972
1039
  clearTimers();
973
1040
  rejectPendingCallControls("Voice connection disconnected");
@@ -1219,7 +1286,10 @@ var createVoiceStream = (path, options = {}) => {
1219
1286
  getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
1220
1287
  }) : null;
1221
1288
  const subscribers = new Set;
1289
+ let disposed = false;
1222
1290
  const start = (input) => Promise.resolve().then(() => {
1291
+ if (disposed)
1292
+ return;
1223
1293
  if (!input?.sessionId && !input?.scenarioId) {
1224
1294
  return;
1225
1295
  }
@@ -1278,6 +1348,7 @@ var createVoiceStream = (path, options = {}) => {
1278
1348
  return connection.callControl(message);
1279
1349
  },
1280
1350
  close(reason) {
1351
+ disposed = true;
1281
1352
  unsubscribeConnection();
1282
1353
  browserMediaReporter?.close();
1283
1354
  connection.close(reason);
@@ -1285,6 +1356,7 @@ var createVoiceStream = (path, options = {}) => {
1285
1356
  notify();
1286
1357
  },
1287
1358
  disconnect() {
1359
+ disposed = true;
1288
1360
  unsubscribeConnection();
1289
1361
  browserMediaReporter?.close();
1290
1362
  connection.disconnect();
@@ -1688,6 +1760,9 @@ var createVoiceController = (path, options = {}) => {
1688
1760
  ...options.connection
1689
1761
  });
1690
1762
  let capture = null;
1763
+ let captureGeneration = 0;
1764
+ let disposed = false;
1765
+ let startRecordingPromise = null;
1691
1766
  let state = createInitialState2(stream);
1692
1767
  const subscribers = new Set;
1693
1768
  const notify = () => {
@@ -1744,6 +1819,7 @@ var createVoiceController = (path, options = {}) => {
1744
1819
  return capture;
1745
1820
  };
1746
1821
  const stopRecording = () => {
1822
+ captureGeneration += 1;
1747
1823
  capture?.stop();
1748
1824
  capture = null;
1749
1825
  state = {
@@ -1753,39 +1829,63 @@ var createVoiceController = (path, options = {}) => {
1753
1829
  notify();
1754
1830
  };
1755
1831
  const startRecording = async () => {
1832
+ if (disposed) {
1833
+ throw new Error("Voice controller is closed.");
1834
+ }
1756
1835
  if (state.isRecording) {
1757
1836
  return;
1758
1837
  }
1759
- try {
1760
- state = {
1761
- ...state,
1762
- recordingError: null
1763
- };
1764
- notify();
1765
- await ensureCapture().start();
1766
- state = {
1767
- ...state,
1768
- isRecording: true
1769
- };
1770
- notify();
1771
- } catch (error) {
1772
- capture?.stop();
1773
- capture = null;
1774
- state = {
1775
- ...state,
1776
- isRecording: false,
1777
- recordingError: error instanceof Error ? error.message : String(error)
1778
- };
1779
- notify();
1780
- throw error;
1781
- }
1838
+ if (startRecordingPromise)
1839
+ return startRecordingPromise;
1840
+ const generation = ++captureGeneration;
1841
+ const activeCapture = ensureCapture();
1842
+ const operation = (async () => {
1843
+ try {
1844
+ state = {
1845
+ ...state,
1846
+ recordingError: null
1847
+ };
1848
+ notify();
1849
+ await activeCapture.start();
1850
+ if (disposed || generation !== captureGeneration || capture !== activeCapture) {
1851
+ activeCapture.stop();
1852
+ return;
1853
+ }
1854
+ state = {
1855
+ ...state,
1856
+ isRecording: true
1857
+ };
1858
+ notify();
1859
+ } catch (error) {
1860
+ activeCapture.stop();
1861
+ if (generation !== captureGeneration || capture !== activeCapture) {
1862
+ throw error;
1863
+ }
1864
+ capture = null;
1865
+ state = {
1866
+ ...state,
1867
+ isRecording: false,
1868
+ recordingError: error instanceof Error ? error.message : String(error)
1869
+ };
1870
+ notify();
1871
+ throw error;
1872
+ }
1873
+ })();
1874
+ const pending = operation.finally(() => {
1875
+ if (startRecordingPromise === pending)
1876
+ startRecordingPromise = null;
1877
+ });
1878
+ startRecordingPromise = pending;
1879
+ return startRecordingPromise;
1782
1880
  };
1783
1881
  const close = (reason) => {
1882
+ disposed = true;
1784
1883
  unsubscribeStream();
1785
1884
  stopRecording();
1786
1885
  stream.close(reason);
1787
1886
  };
1788
1887
  const disconnect = () => {
1888
+ disposed = true;
1789
1889
  unsubscribeStream();
1790
1890
  stopRecording();
1791
1891
  stream.disconnect();
@@ -103,48 +103,89 @@ var createMicrophoneCapture = (options) => {
103
103
  let sourceNode = null;
104
104
  let processorNode = null;
105
105
  let mediaStream = null;
106
- const start = async () => {
107
- if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
108
- throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
109
- }
110
- const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
111
- if (!AudioContextCtor) {
112
- throw new Error("Browser microphone capture requires AudioContext support.");
113
- }
114
- try {
115
- mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
116
- audio: {
117
- autoGainControl: true,
118
- channelCount: options.channelCount ?? 1,
119
- echoCancellation: true,
120
- noiseSuppression: true
106
+ let lifecycleGeneration = 0;
107
+ let startPromise = null;
108
+ const release = (resources) => {
109
+ resources.processorNode?.disconnect();
110
+ resources.sourceNode?.disconnect();
111
+ resources.mediaStream?.getTracks().forEach((track) => track.stop());
112
+ resources.audioContext?.close();
113
+ };
114
+ const start = () => {
115
+ if (mediaStream)
116
+ return Promise.resolve();
117
+ if (startPromise)
118
+ return startPromise;
119
+ const generation = ++lifecycleGeneration;
120
+ const operation = async () => {
121
+ if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
122
+ throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
123
+ }
124
+ const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
125
+ if (!AudioContextCtor) {
126
+ throw new Error("Browser microphone capture requires AudioContext support.");
127
+ }
128
+ let pendingAudioContext = null;
129
+ let pendingMediaStream = null;
130
+ let pendingProcessorNode = null;
131
+ let pendingSourceNode = null;
132
+ try {
133
+ pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
134
+ audio: {
135
+ autoGainControl: true,
136
+ channelCount: options.channelCount ?? 1,
137
+ echoCancellation: true,
138
+ noiseSuppression: true
139
+ }
140
+ });
141
+ if (generation !== lifecycleGeneration) {
142
+ throw new Error("Microphone capture stopped during startup.");
121
143
  }
122
- });
123
- audioContext = new AudioContextCtor;
124
- if (audioContext.state === "suspended") {
125
- await audioContext.resume();
144
+ pendingAudioContext = new AudioContextCtor;
145
+ if (pendingAudioContext.state === "suspended") {
146
+ await pendingAudioContext.resume();
147
+ }
148
+ if (generation !== lifecycleGeneration) {
149
+ throw new Error("Microphone capture stopped during startup.");
150
+ }
151
+ pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
152
+ pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
153
+ pendingProcessorNode.onaudioprocess = (event) => {
154
+ const channel = event.inputBuffer.getChannelData(0);
155
+ const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
156
+ const pcm = floatTo16BitPCM(downsampled);
157
+ options.onLevel?.(getPcmLevel(pcm));
158
+ options.onAudio(pcm);
159
+ };
160
+ pendingSourceNode.connect(pendingProcessorNode);
161
+ pendingProcessorNode.connect(pendingAudioContext.destination);
162
+ if (generation !== lifecycleGeneration) {
163
+ throw new Error("Microphone capture stopped during startup.");
164
+ }
165
+ audioContext = pendingAudioContext;
166
+ mediaStream = pendingMediaStream;
167
+ processorNode = pendingProcessorNode;
168
+ sourceNode = pendingSourceNode;
169
+ } catch (error) {
170
+ release({
171
+ audioContext: pendingAudioContext,
172
+ mediaStream: pendingMediaStream,
173
+ processorNode: pendingProcessorNode,
174
+ sourceNode: pendingSourceNode
175
+ });
176
+ throw error;
126
177
  }
127
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
128
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
129
- processorNode.onaudioprocess = (event) => {
130
- const channel = event.inputBuffer.getChannelData(0);
131
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
132
- const pcm = floatTo16BitPCM(downsampled);
133
- options.onLevel?.(getPcmLevel(pcm));
134
- options.onAudio(pcm);
135
- };
136
- sourceNode.connect(processorNode);
137
- processorNode.connect(audioContext.destination);
138
- } catch (error) {
139
- stop();
140
- throw error;
141
- }
178
+ };
179
+ const pending = operation().finally(() => {
180
+ if (startPromise === pending)
181
+ startPromise = null;
182
+ });
183
+ startPromise = pending;
184
+ return pending;
142
185
  };
143
186
  const stop = () => {
144
- processorNode?.disconnect();
145
- sourceNode?.disconnect();
146
- mediaStream?.getTracks().forEach((track) => track.stop());
147
- audioContext?.close();
187
+ lifecycleGeneration += 1;
188
+ release({ audioContext, mediaStream, processorNode, sourceNode });
148
189
  options.onLevel?.(0);
149
190
  audioContext = null;
150
191
  mediaStream = null;
@@ -505,6 +546,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
505
546
  var DEFAULT_PING_INTERVAL = 30000;
506
547
  var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
507
548
  var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
549
+ var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
508
550
  var MAX_PENDING_CONTROL_MESSAGES = 100;
509
551
  var RECONNECT_BASE_DELAY_MS = 500;
510
552
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
@@ -598,6 +640,7 @@ var createVoiceConnection = (path, options = {}) => {
598
640
  const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
599
641
  let lifecycleGeneration = 0;
600
642
  let disposed = false;
643
+ let reconnectPreparationAbort = null;
601
644
  const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
602
645
  const state = {
603
646
  isConnected: false,
@@ -685,12 +728,30 @@ var createVoiceConnection = (path, options = {}) => {
685
728
  return;
686
729
  }
687
730
  try {
688
- await options.prepareReconnect?.({
689
- attempt: state.reconnectAttempts,
690
- path,
691
- scenarioId: state.scenarioId,
692
- sessionId: state.sessionId
693
- });
731
+ if (options.prepareReconnect) {
732
+ const preparationAbort = new AbortController;
733
+ reconnectPreparationAbort = preparationAbort;
734
+ const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
735
+ try {
736
+ await Promise.race([
737
+ options.prepareReconnect({
738
+ attempt: state.reconnectAttempts,
739
+ path,
740
+ scenarioId: state.scenarioId,
741
+ sessionId: state.sessionId,
742
+ signal: preparationAbort.signal
743
+ }),
744
+ new Promise((_, reject) => {
745
+ preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
746
+ })
747
+ ]);
748
+ } finally {
749
+ clearTimeout(timeout);
750
+ if (reconnectPreparationAbort === preparationAbort) {
751
+ reconnectPreparationAbort = null;
752
+ }
753
+ }
754
+ }
694
755
  } catch (error) {
695
756
  if (disposed || lifecycleGeneration !== scheduledGeneration)
696
757
  return;
@@ -814,6 +875,8 @@ var createVoiceConnection = (path, options = {}) => {
814
875
  state.ws = ws;
815
876
  };
816
877
  const sendSerialized = (value) => {
878
+ if (disposed)
879
+ return;
817
880
  if (state.ws?.readyState === WS_OPEN) {
818
881
  state.ws.send(value);
819
882
  return;
@@ -868,6 +931,8 @@ var createVoiceConnection = (path, options = {}) => {
868
931
  };
869
932
  const close = (reason = "client-close") => {
870
933
  disposed = true;
934
+ reconnectPreparationAbort?.abort();
935
+ reconnectPreparationAbort = null;
871
936
  lifecycleGeneration += 1;
872
937
  clearTimers();
873
938
  rejectPendingCallControls("Voice connection closed");
@@ -885,6 +950,8 @@ var createVoiceConnection = (path, options = {}) => {
885
950
  };
886
951
  const disconnect = () => {
887
952
  disposed = true;
953
+ reconnectPreparationAbort?.abort();
954
+ reconnectPreparationAbort = null;
888
955
  lifecycleGeneration += 1;
889
956
  clearTimers();
890
957
  rejectPendingCallControls("Voice connection disconnected");
@@ -1136,7 +1203,10 @@ var createVoiceStream = (path, options = {}) => {
1136
1203
  getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
1137
1204
  }) : null;
1138
1205
  const subscribers = new Set;
1206
+ let disposed = false;
1139
1207
  const start = (input) => Promise.resolve().then(() => {
1208
+ if (disposed)
1209
+ return;
1140
1210
  if (!input?.sessionId && !input?.scenarioId) {
1141
1211
  return;
1142
1212
  }
@@ -1195,6 +1265,7 @@ var createVoiceStream = (path, options = {}) => {
1195
1265
  return connection.callControl(message);
1196
1266
  },
1197
1267
  close(reason) {
1268
+ disposed = true;
1198
1269
  unsubscribeConnection();
1199
1270
  browserMediaReporter?.close();
1200
1271
  connection.close(reason);
@@ -1202,6 +1273,7 @@ var createVoiceStream = (path, options = {}) => {
1202
1273
  notify();
1203
1274
  },
1204
1275
  disconnect() {
1276
+ disposed = true;
1205
1277
  unsubscribeConnection();
1206
1278
  browserMediaReporter?.close();
1207
1279
  connection.disconnect();
@@ -1568,6 +1640,9 @@ var createVoiceController = (path, options = {}) => {
1568
1640
  ...options.connection
1569
1641
  });
1570
1642
  let capture = null;
1643
+ let captureGeneration = 0;
1644
+ let disposed = false;
1645
+ let startRecordingPromise = null;
1571
1646
  let state = createInitialState2(stream);
1572
1647
  const subscribers = new Set;
1573
1648
  const notify = () => {
@@ -1624,6 +1699,7 @@ var createVoiceController = (path, options = {}) => {
1624
1699
  return capture;
1625
1700
  };
1626
1701
  const stopRecording = () => {
1702
+ captureGeneration += 1;
1627
1703
  capture?.stop();
1628
1704
  capture = null;
1629
1705
  state = {
@@ -1633,39 +1709,63 @@ var createVoiceController = (path, options = {}) => {
1633
1709
  notify();
1634
1710
  };
1635
1711
  const startRecording = async () => {
1712
+ if (disposed) {
1713
+ throw new Error("Voice controller is closed.");
1714
+ }
1636
1715
  if (state.isRecording) {
1637
1716
  return;
1638
1717
  }
1639
- try {
1640
- state = {
1641
- ...state,
1642
- recordingError: null
1643
- };
1644
- notify();
1645
- await ensureCapture().start();
1646
- state = {
1647
- ...state,
1648
- isRecording: true
1649
- };
1650
- notify();
1651
- } catch (error) {
1652
- capture?.stop();
1653
- capture = null;
1654
- state = {
1655
- ...state,
1656
- isRecording: false,
1657
- recordingError: error instanceof Error ? error.message : String(error)
1658
- };
1659
- notify();
1660
- throw error;
1661
- }
1718
+ if (startRecordingPromise)
1719
+ return startRecordingPromise;
1720
+ const generation = ++captureGeneration;
1721
+ const activeCapture = ensureCapture();
1722
+ const operation = (async () => {
1723
+ try {
1724
+ state = {
1725
+ ...state,
1726
+ recordingError: null
1727
+ };
1728
+ notify();
1729
+ await activeCapture.start();
1730
+ if (disposed || generation !== captureGeneration || capture !== activeCapture) {
1731
+ activeCapture.stop();
1732
+ return;
1733
+ }
1734
+ state = {
1735
+ ...state,
1736
+ isRecording: true
1737
+ };
1738
+ notify();
1739
+ } catch (error) {
1740
+ activeCapture.stop();
1741
+ if (generation !== captureGeneration || capture !== activeCapture) {
1742
+ throw error;
1743
+ }
1744
+ capture = null;
1745
+ state = {
1746
+ ...state,
1747
+ isRecording: false,
1748
+ recordingError: error instanceof Error ? error.message : String(error)
1749
+ };
1750
+ notify();
1751
+ throw error;
1752
+ }
1753
+ })();
1754
+ const pending = operation.finally(() => {
1755
+ if (startRecordingPromise === pending)
1756
+ startRecordingPromise = null;
1757
+ });
1758
+ startRecordingPromise = pending;
1759
+ return startRecordingPromise;
1662
1760
  };
1663
1761
  const close = (reason) => {
1762
+ disposed = true;
1664
1763
  unsubscribeStream();
1665
1764
  stopRecording();
1666
1765
  stream.close(reason);
1667
1766
  };
1668
1767
  const disconnect = () => {
1768
+ disposed = true;
1669
1769
  unsubscribeStream();
1670
1770
  stopRecording();
1671
1771
  stream.disconnect();