@livekit/agents 1.0.27 → 1.0.30

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.
Files changed (100) hide show
  1. package/dist/connection_pool.cjs +242 -0
  2. package/dist/connection_pool.cjs.map +1 -0
  3. package/dist/connection_pool.d.cts +123 -0
  4. package/dist/connection_pool.d.ts +123 -0
  5. package/dist/connection_pool.d.ts.map +1 -0
  6. package/dist/connection_pool.js +218 -0
  7. package/dist/connection_pool.js.map +1 -0
  8. package/dist/connection_pool.test.cjs +256 -0
  9. package/dist/connection_pool.test.cjs.map +1 -0
  10. package/dist/connection_pool.test.js +255 -0
  11. package/dist/connection_pool.test.js.map +1 -0
  12. package/dist/index.cjs +2 -0
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +1 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/inference/tts.cjs +172 -56
  20. package/dist/inference/tts.cjs.map +1 -1
  21. package/dist/inference/tts.d.cts +3 -0
  22. package/dist/inference/tts.d.ts +3 -0
  23. package/dist/inference/tts.d.ts.map +1 -1
  24. package/dist/inference/tts.js +173 -57
  25. package/dist/inference/tts.js.map +1 -1
  26. package/dist/utils.cjs +20 -0
  27. package/dist/utils.cjs.map +1 -1
  28. package/dist/utils.d.cts +7 -0
  29. package/dist/utils.d.ts +7 -0
  30. package/dist/utils.d.ts.map +1 -1
  31. package/dist/utils.js +19 -0
  32. package/dist/utils.js.map +1 -1
  33. package/dist/voice/agent_activity.cjs +3 -1
  34. package/dist/voice/agent_activity.cjs.map +1 -1
  35. package/dist/voice/agent_activity.d.ts.map +1 -1
  36. package/dist/voice/agent_activity.js +3 -1
  37. package/dist/voice/agent_activity.js.map +1 -1
  38. package/dist/voice/agent_session.cjs +4 -1
  39. package/dist/voice/agent_session.cjs.map +1 -1
  40. package/dist/voice/agent_session.d.ts.map +1 -1
  41. package/dist/voice/agent_session.js +4 -1
  42. package/dist/voice/agent_session.js.map +1 -1
  43. package/dist/voice/avatar/datastream_io.cjs +1 -1
  44. package/dist/voice/avatar/datastream_io.cjs.map +1 -1
  45. package/dist/voice/avatar/datastream_io.js +1 -1
  46. package/dist/voice/avatar/datastream_io.js.map +1 -1
  47. package/dist/voice/background_audio.cjs +77 -37
  48. package/dist/voice/background_audio.cjs.map +1 -1
  49. package/dist/voice/background_audio.d.cts +10 -3
  50. package/dist/voice/background_audio.d.ts +10 -3
  51. package/dist/voice/background_audio.d.ts.map +1 -1
  52. package/dist/voice/background_audio.js +78 -37
  53. package/dist/voice/background_audio.js.map +1 -1
  54. package/dist/voice/index.cjs +1 -0
  55. package/dist/voice/index.cjs.map +1 -1
  56. package/dist/voice/index.d.cts +1 -0
  57. package/dist/voice/index.d.ts +1 -0
  58. package/dist/voice/index.d.ts.map +1 -1
  59. package/dist/voice/index.js +1 -0
  60. package/dist/voice/index.js.map +1 -1
  61. package/dist/voice/io.cjs +10 -1
  62. package/dist/voice/io.cjs.map +1 -1
  63. package/dist/voice/io.d.cts +18 -1
  64. package/dist/voice/io.d.ts +18 -1
  65. package/dist/voice/io.d.ts.map +1 -1
  66. package/dist/voice/io.js +10 -1
  67. package/dist/voice/io.js.map +1 -1
  68. package/dist/voice/recorder_io/recorder_io.cjs +1 -1
  69. package/dist/voice/recorder_io/recorder_io.cjs.map +1 -1
  70. package/dist/voice/recorder_io/recorder_io.js +1 -1
  71. package/dist/voice/recorder_io/recorder_io.js.map +1 -1
  72. package/dist/voice/room_io/_output.cjs +1 -1
  73. package/dist/voice/room_io/_output.cjs.map +1 -1
  74. package/dist/voice/room_io/_output.js +1 -1
  75. package/dist/voice/room_io/_output.js.map +1 -1
  76. package/dist/voice/transcription/synchronizer.cjs +1 -1
  77. package/dist/voice/transcription/synchronizer.cjs.map +1 -1
  78. package/dist/voice/transcription/synchronizer.js +1 -1
  79. package/dist/voice/transcription/synchronizer.js.map +1 -1
  80. package/dist/worker.cjs +4 -6
  81. package/dist/worker.cjs.map +1 -1
  82. package/dist/worker.d.ts.map +1 -1
  83. package/dist/worker.js +4 -6
  84. package/dist/worker.js.map +1 -1
  85. package/package.json +3 -3
  86. package/src/connection_pool.test.ts +346 -0
  87. package/src/connection_pool.ts +307 -0
  88. package/src/index.ts +1 -0
  89. package/src/inference/tts.ts +206 -63
  90. package/src/utils.ts +25 -0
  91. package/src/voice/agent_activity.ts +7 -1
  92. package/src/voice/agent_session.ts +4 -1
  93. package/src/voice/avatar/datastream_io.ts +1 -1
  94. package/src/voice/background_audio.ts +95 -55
  95. package/src/voice/index.ts +1 -0
  96. package/src/voice/io.ts +24 -0
  97. package/src/voice/recorder_io/recorder_io.ts +1 -1
  98. package/src/voice/room_io/_output.ts +1 -1
  99. package/src/voice/transcription/synchronizer.ts +1 -1
  100. package/src/worker.ts +4 -7
@@ -0,0 +1,346 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { ConnectionPool } from './connection_pool.js';
6
+
7
+ describe('ConnectionPool', () => {
8
+ const makeConnectCb = () => {
9
+ let n = 0;
10
+ return vi.fn(async (_timeout: number): Promise<string> => `conn_${++n}`);
11
+ };
12
+
13
+ describe('basic operations', () => {
14
+ it('should create and return a connection', async () => {
15
+ const connections: string[] = [];
16
+ const connectCb = vi.fn(async (_timeout: number): Promise<string> => {
17
+ const conn = `conn_${connections.length}`;
18
+ connections.push(conn);
19
+ return conn;
20
+ });
21
+ const closeCb = vi.fn(async (_conn: string) => {
22
+ // Mock close
23
+ });
24
+
25
+ const pool = new ConnectionPool<string>({
26
+ connectCb,
27
+ closeCb,
28
+ });
29
+
30
+ const conn = await pool.get();
31
+ expect(conn).toBe('conn_0');
32
+ expect(connectCb).toHaveBeenCalledTimes(1);
33
+
34
+ pool.put(conn);
35
+ const conn2 = await pool.get();
36
+ expect(conn2).toBe('conn_0'); // Should reuse
37
+ expect(connectCb).toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ it('should create new connection when none available', async () => {
41
+ const connectCb = makeConnectCb();
42
+ const closeCb = vi.fn(async (_conn: string) => {
43
+ // Mock close
44
+ });
45
+
46
+ const pool = new ConnectionPool<string>({
47
+ connectCb,
48
+ closeCb,
49
+ });
50
+
51
+ const conn1 = await pool.get();
52
+ pool.put(conn1);
53
+ const conn2 = await pool.get();
54
+ expect(conn1).toBe(conn2); // Should reuse
55
+ expect(connectCb).toHaveBeenCalledTimes(1);
56
+ });
57
+
58
+ it('should remove connection from pool', async () => {
59
+ const connectCb = makeConnectCb();
60
+ const closeCb = vi.fn(async (_conn: string) => {
61
+ // Mock close
62
+ });
63
+
64
+ const pool = new ConnectionPool<string>({
65
+ connectCb,
66
+ closeCb,
67
+ });
68
+
69
+ const conn = await pool.get();
70
+ pool.put(conn);
71
+ pool.remove(conn);
72
+
73
+ const conn2 = await pool.get();
74
+ expect(conn2).not.toBe(conn); // Should create new connection
75
+ expect(connectCb).toHaveBeenCalledTimes(2);
76
+ expect(closeCb).toHaveBeenCalledTimes(1);
77
+ });
78
+ });
79
+
80
+ describe('maxSessionDuration', () => {
81
+ it('should expire connections after maxSessionDuration', async () => {
82
+ const connectCb = makeConnectCb();
83
+ const closeCb = vi.fn(async (_conn: string) => {
84
+ // Mock close
85
+ });
86
+
87
+ const pool = new ConnectionPool<string>({
88
+ connectCb,
89
+ closeCb,
90
+ maxSessionDuration: 100, // 100ms
91
+ });
92
+
93
+ const conn1 = await pool.get();
94
+ pool.put(conn1);
95
+
96
+ // Wait for expiration
97
+ await new Promise((resolve) => setTimeout(resolve, 150));
98
+
99
+ const conn2 = await pool.get();
100
+ expect(conn2).not.toBe(conn1); // Should create new connection
101
+ expect(connectCb).toHaveBeenCalledTimes(2);
102
+ expect(closeCb).toHaveBeenCalledTimes(1);
103
+ });
104
+
105
+ it('should refresh connection timestamp when markRefreshedOnGet is true', async () => {
106
+ const connectCb = makeConnectCb();
107
+ const closeCb = vi.fn(async (_conn: string) => {
108
+ // Mock close
109
+ });
110
+
111
+ const pool = new ConnectionPool<string>({
112
+ connectCb,
113
+ closeCb,
114
+ maxSessionDuration: 200, // 200ms
115
+ markRefreshedOnGet: true,
116
+ });
117
+
118
+ const conn1 = await pool.get();
119
+ pool.put(conn1);
120
+
121
+ // Wait 100ms (less than expiration)
122
+ await new Promise((resolve) => setTimeout(resolve, 100));
123
+
124
+ // Get again - should refresh timestamp
125
+ const conn2 = await pool.get();
126
+ expect(conn2).toBe(conn1); // Should reuse
127
+ pool.put(conn2);
128
+
129
+ // Wait another 100ms (total 200ms, but refreshed at 100ms)
130
+ await new Promise((resolve) => setTimeout(resolve, 100));
131
+
132
+ // Should still be valid
133
+ const conn3 = await pool.get();
134
+ expect(conn3).toBe(conn1); // Should still reuse
135
+ expect(connectCb).toHaveBeenCalledTimes(1);
136
+ });
137
+ });
138
+
139
+ describe('withConnection', () => {
140
+ it('should return connection to pool on success', async () => {
141
+ const connectCb = makeConnectCb();
142
+ const closeCb = vi.fn(async (_conn: string) => {
143
+ // Mock close
144
+ });
145
+
146
+ const pool = new ConnectionPool<string>({
147
+ connectCb,
148
+ closeCb,
149
+ });
150
+
151
+ let capturedConn: string | undefined;
152
+ await pool.withConnection(async (conn) => {
153
+ capturedConn = conn;
154
+ return 'result';
155
+ });
156
+
157
+ // Connection should be returned to pool
158
+ const conn2 = await pool.get();
159
+ expect(conn2).toBe(capturedConn); // Should reuse
160
+ expect(connectCb).toHaveBeenCalledTimes(1);
161
+ });
162
+
163
+ it('should remove connection from pool on error', async () => {
164
+ const connectCb = makeConnectCb();
165
+ const closeCb = vi.fn(async (_conn: string) => {
166
+ // Mock close
167
+ });
168
+
169
+ const pool = new ConnectionPool<string>({
170
+ connectCb,
171
+ closeCb,
172
+ });
173
+
174
+ let capturedConn: string | undefined;
175
+ try {
176
+ await pool.withConnection(async (conn) => {
177
+ capturedConn = conn;
178
+ throw new Error('test error');
179
+ });
180
+ } catch (e) {
181
+ // Expected
182
+ }
183
+
184
+ // Connection should be removed from pool
185
+ const conn2 = await pool.get();
186
+ expect(conn2).not.toBe(capturedConn); // Should create new connection
187
+ expect(connectCb).toHaveBeenCalledTimes(2);
188
+ expect(closeCb).toHaveBeenCalledTimes(1);
189
+ });
190
+
191
+ it('should handle abort signal', async () => {
192
+ const connectCb = makeConnectCb();
193
+ const closeCb = vi.fn(async (_conn: string) => {
194
+ // Mock close
195
+ });
196
+
197
+ const pool = new ConnectionPool<string>({
198
+ connectCb,
199
+ closeCb,
200
+ });
201
+
202
+ const abortController = new AbortController();
203
+ let capturedConn: string | undefined;
204
+
205
+ const promise = pool.withConnection(
206
+ async (conn) => {
207
+ capturedConn = conn;
208
+ await new Promise((resolve) => setTimeout(resolve, 1000));
209
+ return 'result';
210
+ },
211
+ { signal: abortController.signal },
212
+ );
213
+
214
+ // Abort after a short delay
215
+ setTimeout(() => abortController.abort(), 10);
216
+
217
+ await expect(promise).rejects.toThrow();
218
+
219
+ // Connection should be removed from pool
220
+ const conn2 = await pool.get();
221
+ expect(conn2).not.toBe(capturedConn); // Should create new connection
222
+ expect(closeCb).toHaveBeenCalledTimes(1);
223
+ });
224
+ });
225
+
226
+ describe('prewarm', () => {
227
+ it('should create connection in background', async () => {
228
+ let n = 0;
229
+ const connectCb = vi.fn(async (_timeout: number): Promise<string> => {
230
+ await new Promise((resolve) => setTimeout(resolve, 50));
231
+ return `conn_${++n}`;
232
+ });
233
+ const closeCb = vi.fn(async (_conn: string) => {
234
+ // Mock close
235
+ });
236
+
237
+ const pool = new ConnectionPool<string>({
238
+ connectCb,
239
+ closeCb,
240
+ });
241
+
242
+ pool.prewarm();
243
+
244
+ // Wait for prewarm to complete
245
+ await new Promise((resolve) => setTimeout(resolve, 100));
246
+
247
+ const conn = await pool.get();
248
+ expect(conn).toBeDefined();
249
+ expect(connectCb).toHaveBeenCalledTimes(1);
250
+ });
251
+
252
+ it('should not prewarm if connections already exist', async () => {
253
+ const connectCb = makeConnectCb();
254
+ const closeCb = vi.fn(async (_conn: string) => {
255
+ // Mock close
256
+ });
257
+
258
+ const pool = new ConnectionPool<string>({
259
+ connectCb,
260
+ closeCb,
261
+ });
262
+
263
+ // Create a connection first
264
+ const conn1 = await pool.get();
265
+ pool.put(conn1);
266
+
267
+ pool.prewarm(); // Should not create new connection
268
+
269
+ const conn2 = await pool.get();
270
+ expect(conn2).toBe(conn1); // Should reuse existing
271
+ expect(connectCb).toHaveBeenCalledTimes(1);
272
+ });
273
+ });
274
+
275
+ describe('close', () => {
276
+ it('should close all connections', async () => {
277
+ const connectCb = makeConnectCb();
278
+ const closeCb = vi.fn(async (_conn: string) => {
279
+ // Mock close
280
+ });
281
+
282
+ const pool = new ConnectionPool<string>({
283
+ connectCb,
284
+ closeCb,
285
+ });
286
+
287
+ // Create two distinct connections by checking out both before returning either.
288
+ const conn1 = await pool.get();
289
+ const conn2 = await pool.get();
290
+ pool.put(conn1);
291
+ pool.put(conn2);
292
+
293
+ await pool.close();
294
+
295
+ expect(closeCb).toHaveBeenCalledTimes(2);
296
+ });
297
+
298
+ it('should invalidate all connections', async () => {
299
+ const connectCb = makeConnectCb();
300
+ const closeCb = vi.fn(async (_conn: string) => {
301
+ // Mock close
302
+ });
303
+
304
+ const pool = new ConnectionPool<string>({
305
+ connectCb,
306
+ closeCb,
307
+ });
308
+
309
+ // Create two distinct connections by checking out both before returning either.
310
+ const conn1 = await pool.get();
311
+ const conn2 = await pool.get();
312
+ pool.put(conn1);
313
+ pool.put(conn2);
314
+
315
+ pool.invalidate();
316
+ await pool.close(); // Drain to close
317
+
318
+ expect(closeCb).toHaveBeenCalledTimes(2);
319
+ });
320
+ });
321
+
322
+ describe('concurrent access', () => {
323
+ it('should handle concurrent get requests', async () => {
324
+ const connectCb = vi.fn(async (_timeout: number): Promise<string> => {
325
+ await new Promise((resolve) => setTimeout(resolve, 10));
326
+ return `conn_${Date.now()}_${Math.random()}`;
327
+ });
328
+ const closeCb = vi.fn(async (_conn: string) => {
329
+ // Mock close
330
+ });
331
+
332
+ const pool = new ConnectionPool<string>({
333
+ connectCb,
334
+ closeCb,
335
+ });
336
+
337
+ const promises = Array.from({ length: 5 }, () => pool.get());
338
+ const connections = await Promise.all(promises);
339
+
340
+ // All should be different connections
341
+ const uniqueConnections = new Set(connections);
342
+ expect(uniqueConnections.size).toBe(5);
343
+ expect(connectCb).toHaveBeenCalledTimes(5);
344
+ });
345
+ });
346
+ });
@@ -0,0 +1,307 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { Mutex } from '@livekit/mutex';
5
+ import { waitForAbort } from './utils.js';
6
+
7
+ /**
8
+ * Helper class to manage persistent connections like websockets.
9
+ */
10
+ export interface ConnectionPoolOptions<T> {
11
+ /**
12
+ * Maximum duration in milliseconds before forcing reconnection.
13
+ * If not set, connections will never expire based on duration.
14
+ */
15
+ maxSessionDuration?: number;
16
+
17
+ /**
18
+ * If true, the session will be marked as fresh when get() is called.
19
+ * Only used when maxSessionDuration is set.
20
+ */
21
+ markRefreshedOnGet?: boolean;
22
+
23
+ /**
24
+ * Async callback to create new connections.
25
+ * @param timeout - Connection timeout in milliseconds
26
+ * @returns A new connection object
27
+ */
28
+ connectCb: (timeout: number) => Promise<T>;
29
+
30
+ /**
31
+ * Optional async callback to close connections.
32
+ * @param conn - The connection to close
33
+ */
34
+ closeCb?: (conn: T) => Promise<void>;
35
+
36
+ /**
37
+ * Default connection timeout in milliseconds.
38
+ * Defaults to 10000 (10 seconds).
39
+ */
40
+ connectTimeout?: number;
41
+ }
42
+
43
+ /**
44
+ * Connection pool for managing persistent WebSocket connections.
45
+ *
46
+ * Reuses connections efficiently and automatically refreshes them after max duration.
47
+ * Prevents creating too many connections in a single conversation.
48
+ */
49
+ export class ConnectionPool<T> {
50
+ private readonly maxSessionDuration?: number;
51
+ private readonly markRefreshedOnGet: boolean;
52
+ private readonly connectCb: (timeout: number) => Promise<T>;
53
+ private readonly closeCb?: (conn: T) => Promise<void>;
54
+ private readonly connectTimeout: number;
55
+
56
+ // Track connections and their creation timestamps
57
+ private readonly connections: Map<T, number> = new Map();
58
+ // Available connections ready for reuse
59
+ private readonly available: Set<T> = new Set();
60
+ // Connections queued for closing
61
+ private readonly toClose: Set<T> = new Set();
62
+ // Mutex for connection operations
63
+ private readonly connectLock = new Mutex();
64
+ // Prewarm task reference
65
+ private prewarmController?: AbortController;
66
+
67
+ constructor(options: ConnectionPoolOptions<T>) {
68
+ this.maxSessionDuration = options.maxSessionDuration;
69
+ this.markRefreshedOnGet = options.markRefreshedOnGet ?? false;
70
+ this.connectCb = options.connectCb;
71
+ this.closeCb = options.closeCb;
72
+ this.connectTimeout = options.connectTimeout ?? 10_000;
73
+ }
74
+
75
+ /**
76
+ * Create a new connection.
77
+ *
78
+ * @param timeout - Connection timeout in milliseconds
79
+ * @returns The new connection object
80
+ * @throws If connectCb is not provided or connection fails
81
+ */
82
+ private async _connect(timeout: number): Promise<T> {
83
+ const connection = await this.connectCb(timeout);
84
+ this.connections.set(connection, Date.now());
85
+ return connection;
86
+ }
87
+
88
+ /**
89
+ * Drain and close all connections queued for closing.
90
+ */
91
+ private async _drainToClose(): Promise<void> {
92
+ const connectionsToClose = Array.from(this.toClose);
93
+ this.toClose.clear();
94
+
95
+ for (const conn of connectionsToClose) {
96
+ await this._maybeCloseConnection(conn);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Close a connection if closeCb is provided.
102
+ *
103
+ * @param conn - The connection to close
104
+ */
105
+ private async _maybeCloseConnection(conn: T): Promise<void> {
106
+ if (this.closeCb) {
107
+ await this.closeCb(conn);
108
+ }
109
+ }
110
+
111
+ private _abortError(): Error {
112
+ const error = new Error('The operation was aborted.');
113
+ error.name = 'AbortError';
114
+ return error;
115
+ }
116
+
117
+ /**
118
+ * Get an available connection or create a new one if needed.
119
+ *
120
+ * @param timeout - Connection timeout in milliseconds
121
+ * @returns An active connection object
122
+ */
123
+ async get(timeout?: number): Promise<T> {
124
+ const unlock = await this.connectLock.lock();
125
+ try {
126
+ await this._drainToClose();
127
+ const now = Date.now();
128
+
129
+ // Try to reuse an available connection that hasn't expired
130
+ while (this.available.size > 0) {
131
+ const conn = this.available.values().next().value as T;
132
+ this.available.delete(conn);
133
+
134
+ if (
135
+ this.maxSessionDuration === undefined ||
136
+ now - (this.connections.get(conn) ?? 0) <= this.maxSessionDuration
137
+ ) {
138
+ if (this.markRefreshedOnGet) {
139
+ this.connections.set(conn, now);
140
+ }
141
+ return conn;
142
+ }
143
+
144
+ // Connection expired; close it now so callers observing get() see it closed promptly.
145
+ // (Also makes tests deterministic: closeCb should have been called by the time get() resolves.)
146
+ if (this.connections.has(conn)) {
147
+ this.connections.delete(conn);
148
+ }
149
+ this.toClose.delete(conn);
150
+ await this._maybeCloseConnection(conn);
151
+ }
152
+
153
+ return await this._connect(timeout ?? this.connectTimeout);
154
+ } finally {
155
+ unlock();
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Mark a connection as available for reuse.
161
+ *
162
+ * If connection has been removed, it will not be added to the pool.
163
+ *
164
+ * @param conn - The connection to make available
165
+ */
166
+ put(conn: T): void {
167
+ if (this.connections.has(conn)) {
168
+ this.available.add(conn);
169
+ return;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Remove a specific connection from the pool.
175
+ *
176
+ * Marks the connection to be closed during the next drain cycle.
177
+ *
178
+ * @param conn - The connection to remove
179
+ */
180
+ remove(conn: T): void {
181
+ this.available.delete(conn);
182
+ if (this.connections.has(conn)) {
183
+ this.toClose.add(conn);
184
+ this.connections.delete(conn);
185
+ // Important for Node websockets: if we just "mark to close later" but remove listeners,
186
+ // the ws library can buffer incoming frames in memory. Close ASAP in background.
187
+ void (async () => {
188
+ const unlock = await this.connectLock.lock();
189
+ try {
190
+ if (!this.toClose.has(conn)) return;
191
+ await this._maybeCloseConnection(conn);
192
+ this.toClose.delete(conn);
193
+ } finally {
194
+ unlock();
195
+ }
196
+ })();
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Clear all existing connections.
202
+ *
203
+ * Marks all current connections to be closed during the next drain cycle.
204
+ */
205
+ invalidate(): void {
206
+ for (const conn of this.connections.keys()) {
207
+ this.toClose.add(conn);
208
+ }
209
+ this.connections.clear();
210
+ this.available.clear();
211
+ }
212
+
213
+ /**
214
+ * Initiate prewarming of the connection pool without blocking.
215
+ *
216
+ * This method starts a background task that creates a new connection if none exist.
217
+ * The task automatically cleans itself up when the connection pool is closed.
218
+ */
219
+ prewarm(): void {
220
+ if (this.prewarmController || this.connections.size > 0) {
221
+ return;
222
+ }
223
+
224
+ const controller = new AbortController();
225
+ this.prewarmController = controller;
226
+
227
+ // Start prewarm in background
228
+ this._prewarmImpl(controller.signal).catch(() => {
229
+ // Ignore errors during prewarm
230
+ });
231
+ }
232
+
233
+ private async _prewarmImpl(signal: AbortSignal): Promise<void> {
234
+ const unlock = await this.connectLock.lock();
235
+ try {
236
+ if (signal.aborted) {
237
+ return;
238
+ }
239
+
240
+ if (this.connections.size === 0) {
241
+ const conn = await this._connect(this.connectTimeout);
242
+ this.available.add(conn);
243
+ }
244
+ } finally {
245
+ unlock();
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Get a connection from the pool and automatically return it when done.
251
+ * Handles abort signals and ensures proper cleanup.
252
+ *
253
+ * @param fn - Function to execute with the connection
254
+ * @param options - Options including timeout and abort signal
255
+ * @returns The result of the function
256
+ */
257
+ async withConnection<R>(
258
+ fn: (conn: T) => Promise<R>,
259
+ options?: {
260
+ timeout?: number;
261
+ signal?: AbortSignal;
262
+ },
263
+ ): Promise<R> {
264
+ // Check if already aborted before getting connection
265
+ if (options?.signal?.aborted) {
266
+ throw this._abortError();
267
+ }
268
+
269
+ const conn = await this.get(options?.timeout);
270
+
271
+ const signal = options?.signal;
272
+
273
+ try {
274
+ const fnPromise = fn(conn);
275
+ const result = signal
276
+ ? await Promise.race([
277
+ fnPromise.then((value) => ({ type: 'result' as const, value })),
278
+ waitForAbort(signal).then(() => ({ type: 'abort' as const })),
279
+ ]).then((r) => {
280
+ if (r.type === 'abort') throw this._abortError();
281
+ return r.value;
282
+ })
283
+ : await fnPromise;
284
+ // Return connection to pool on success
285
+ this.put(conn);
286
+ return result;
287
+ } catch (error) {
288
+ // Remove connection from pool on error (don't return it)
289
+ this.remove(conn);
290
+ throw error;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Close all connections, draining any pending connection closures.
296
+ */
297
+ async close(): Promise<void> {
298
+ // Cancel prewarm task if running
299
+ if (this.prewarmController) {
300
+ this.prewarmController.abort();
301
+ this.prewarmController = undefined;
302
+ }
303
+
304
+ this.invalidate();
305
+ await this._drainToClose();
306
+ }
307
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ import * as voice from './voice/index.js';
23
23
 
24
24
  export * from './_exceptions.js';
25
25
  export * from './audio.js';
26
+ export * from './connection_pool.js';
26
27
  export * from './generator.js';
27
28
  export * from './inference_runner.js';
28
29
  export * from './job.js';