@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.
package/dist/index.js CHANGED
@@ -39835,6 +39835,7 @@ var createVoiceMonitorRunnerRoutes = (options) => {
39835
39835
  };
39836
39836
 
39837
39837
  // src/core/plugin.ts
39838
+ var socketIdentity = (ws) => ws.raw && typeof ws.raw === "object" ? ws.raw : ws;
39838
39839
  var resolveQueryScenario = (query) => {
39839
39840
  if (typeof query?.scenarioId === "string" && query.scenarioId.trim()) {
39840
39841
  return query.scenarioId.trim();
@@ -39946,14 +39947,15 @@ var parseClientMessage = (raw) => {
39946
39947
  };
39947
39948
  var resolveSessionId3 = (runtime, ws) => {
39948
39949
  const query = ws.data && typeof ws.data === "object" && "query" in ws.data ? ws.data.query : undefined;
39949
- const existing = runtime.socketSessions.get(ws);
39950
+ const identity = socketIdentity(ws);
39951
+ const existing = runtime.socketSessions.get(identity);
39950
39952
  const providedSessionId = typeof query?.sessionId === "string" && query.sessionId.trim() ? query.sessionId.trim() : existing?.sessionId ?? createId();
39951
39953
  const scenarioId = resolveQueryScenario(query) ?? existing?.scenarioId ?? null;
39952
39954
  const resolved = {
39953
39955
  scenarioId,
39954
39956
  sessionId: providedSessionId
39955
39957
  };
39956
- runtime.socketSessions.set(ws, resolved);
39958
+ runtime.socketSessions.set(identity, resolved);
39957
39959
  return resolved;
39958
39960
  };
39959
39961
  var resolveSocketQuery = (ws) => ws.data && typeof ws.data === "object" && ("query" in ws.data) ? ws.data.query ?? {} : {};
@@ -40158,30 +40160,48 @@ var voice = (config) => {
40158
40160
  socketSessions: new WeakMap
40159
40161
  };
40160
40162
  const authorizedSockets = new WeakSet;
40161
- const authorizeSocket = async (ws, sessionId, scenarioId) => {
40162
- if (!config.authorizeConnection) {
40163
- authorizedSockets.add(ws);
40163
+ const pendingSocketAuthorizations = new WeakMap;
40164
+ const authorizeSocket = (ws, sessionId, scenarioId) => {
40165
+ const identity = socketIdentity(ws);
40166
+ const existing = pendingSocketAuthorizations.get(identity);
40167
+ if (existing)
40168
+ return existing;
40169
+ const authorization = (async () => {
40170
+ if (!config.authorizeConnection) {
40171
+ authorizedSockets.add(identity);
40172
+ return true;
40173
+ }
40174
+ let authorized = false;
40175
+ try {
40176
+ authorized = await config.authorizeConnection({
40177
+ context: ws.data,
40178
+ headers: resolveSocketHeaders(ws),
40179
+ path: config.path,
40180
+ query: resolveSocketQuery(ws),
40181
+ scenarioId,
40182
+ sessionId
40183
+ });
40184
+ } catch (error) {
40185
+ runtime.logger.warn?.(`[voice] connection authorization failed for session "${sessionId}": ${error instanceof Error ? error.message : String(error)}`);
40186
+ }
40187
+ if (!authorized) {
40188
+ ws.close(4401, "unauthorized");
40189
+ return false;
40190
+ }
40191
+ authorizedSockets.add(identity);
40164
40192
  return true;
40165
- }
40166
- let authorized = false;
40167
- try {
40168
- authorized = await config.authorizeConnection({
40169
- context: ws.data,
40170
- headers: resolveSocketHeaders(ws),
40171
- path: config.path,
40172
- query: resolveSocketQuery(ws),
40173
- scenarioId,
40174
- sessionId
40175
- });
40176
- } catch (error) {
40177
- runtime.logger.warn?.(`[voice] connection authorization failed for session "${sessionId}": ${error instanceof Error ? error.message : String(error)}`);
40178
- }
40179
- if (!authorized) {
40180
- ws.close(4401, "unauthorized");
40193
+ })();
40194
+ pendingSocketAuthorizations.set(identity, authorization);
40195
+ return authorization;
40196
+ };
40197
+ const waitForSocketAuthorization = async (ws) => {
40198
+ const identity = socketIdentity(ws);
40199
+ if (authorizedSockets.has(identity))
40200
+ return true;
40201
+ const pending = pendingSocketAuthorizations.get(identity);
40202
+ if (!pending)
40181
40203
  return false;
40182
- }
40183
- authorizedSockets.add(ws);
40184
- return true;
40204
+ return pending;
40185
40205
  };
40186
40206
  const { monitor } = config;
40187
40207
  const registerMonitorSession = (sessionId, handle) => {
@@ -40491,8 +40511,9 @@ var voice = (config) => {
40491
40511
  };
40492
40512
  return new Elysia70({ name: "absolutejs-voice" }).ws(config.path, {
40493
40513
  close: async (ws, code, reason) => {
40494
- const socketState = runtime.socketSessions.get(ws);
40495
- if (!socketState || runtime.activeSockets.get(socketState.sessionId) !== ws) {
40514
+ const identity = socketIdentity(ws);
40515
+ const socketState = runtime.socketSessions.get(identity);
40516
+ if (!socketState || runtime.activeSockets.get(socketState.sessionId) !== identity) {
40496
40517
  return;
40497
40518
  }
40498
40519
  runtime.activeSockets.delete(socketState.sessionId);
@@ -40509,7 +40530,7 @@ var voice = (config) => {
40509
40530
  }
40510
40531
  },
40511
40532
  message: async (ws, raw) => {
40512
- if (!authorizedSockets.has(ws)) {
40533
+ if (!await waitForSocketAuthorization(ws)) {
40513
40534
  ws.close(4401, "unauthorized");
40514
40535
  return;
40515
40536
  }
@@ -40595,8 +40616,8 @@ var voice = (config) => {
40595
40616
  deregisterMonitorSession(sessionState.sessionId, "session-switch");
40596
40617
  }
40597
40618
  sessionState.sessionId = message.sessionId;
40598
- runtime.activeSockets.set(message.sessionId, ws);
40599
- runtime.socketSessions.set(ws, {
40619
+ runtime.activeSockets.set(message.sessionId, socketIdentity(ws));
40620
+ runtime.socketSessions.set(socketIdentity(ws), {
40600
40621
  ...sessionState,
40601
40622
  sessionId: message.sessionId,
40602
40623
  scenarioId: sessionState.scenarioId
@@ -40604,7 +40625,7 @@ var voice = (config) => {
40604
40625
  }
40605
40626
  if (message.type === "start" && message.scenarioId) {
40606
40627
  sessionState.scenarioId = message.scenarioId;
40607
- runtime.socketSessions.set(ws, {
40628
+ runtime.socketSessions.set(socketIdentity(ws), {
40608
40629
  ...sessionState,
40609
40630
  scenarioId: message.scenarioId
40610
40631
  });
@@ -40624,7 +40645,7 @@ var voice = (config) => {
40624
40645
  return;
40625
40646
  }
40626
40647
  const existing = runtime.activeSessions.get(sessionState.sessionId);
40627
- runtime.activeSockets.set(sessionState.sessionId, ws);
40648
+ runtime.activeSockets.set(sessionState.sessionId, socketIdentity(ws));
40628
40649
  if (existing) {
40629
40650
  await existing.disconnect({
40630
40651
  code: 1006,
@@ -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();