@btc-vision/bitcoin 7.0.0-alpha.3 → 7.0.0-alpha.4

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 (96) hide show
  1. package/README.md +139 -12
  2. package/browser/chunks/WorkerSigningPool.sequential-DHha7j0b.js +113 -0
  3. package/browser/env.d.ts +13 -0
  4. package/browser/env.d.ts.map +1 -0
  5. package/browser/index.d.ts +1 -1
  6. package/browser/index.d.ts.map +1 -1
  7. package/browser/index.js +872 -1105
  8. package/browser/io/index.d.ts +0 -1
  9. package/browser/io/index.d.ts.map +1 -1
  10. package/browser/types.d.ts.map +1 -1
  11. package/browser/workers/WorkerSigningPool.d.ts +6 -0
  12. package/browser/workers/WorkerSigningPool.d.ts.map +1 -1
  13. package/browser/workers/WorkerSigningPool.node.d.ts +6 -0
  14. package/browser/workers/WorkerSigningPool.node.d.ts.map +1 -1
  15. package/browser/workers/WorkerSigningPool.sequential.d.ts +69 -0
  16. package/browser/workers/WorkerSigningPool.sequential.d.ts.map +1 -0
  17. package/browser/workers/WorkerSigningPool.worklet.d.ts +64 -0
  18. package/browser/workers/WorkerSigningPool.worklet.d.ts.map +1 -0
  19. package/browser/workers/index.d.ts +2 -2
  20. package/browser/workers/index.d.ts.map +1 -1
  21. package/browser/workers/index.react-native.d.ts +28 -0
  22. package/browser/workers/index.react-native.d.ts.map +1 -0
  23. package/browser/workers/psbt-parallel.d.ts +2 -3
  24. package/browser/workers/psbt-parallel.d.ts.map +1 -1
  25. package/browser/workers/types.d.ts +12 -0
  26. package/browser/workers/types.d.ts.map +1 -1
  27. package/build/env.d.ts +13 -0
  28. package/build/env.d.ts.map +1 -0
  29. package/build/env.js +198 -0
  30. package/build/env.js.map +1 -0
  31. package/build/index.d.ts +2 -1
  32. package/build/index.d.ts.map +1 -1
  33. package/build/index.js +2 -1
  34. package/build/index.js.map +1 -1
  35. package/build/io/index.d.ts +0 -1
  36. package/build/io/index.d.ts.map +1 -1
  37. package/build/io/index.js +0 -2
  38. package/build/io/index.js.map +1 -1
  39. package/build/tsconfig.build.tsbuildinfo +1 -1
  40. package/build/types.d.ts.map +1 -1
  41. package/build/types.js +2 -16
  42. package/build/types.js.map +1 -1
  43. package/build/workers/WorkerSigningPool.d.ts +6 -0
  44. package/build/workers/WorkerSigningPool.d.ts.map +1 -1
  45. package/build/workers/WorkerSigningPool.js +8 -0
  46. package/build/workers/WorkerSigningPool.js.map +1 -1
  47. package/build/workers/WorkerSigningPool.node.d.ts +6 -0
  48. package/build/workers/WorkerSigningPool.node.d.ts.map +1 -1
  49. package/build/workers/WorkerSigningPool.node.js +9 -2
  50. package/build/workers/WorkerSigningPool.node.js.map +1 -1
  51. package/build/workers/WorkerSigningPool.sequential.d.ts +78 -0
  52. package/build/workers/WorkerSigningPool.sequential.d.ts.map +1 -0
  53. package/build/workers/WorkerSigningPool.sequential.js +160 -0
  54. package/build/workers/WorkerSigningPool.sequential.js.map +1 -0
  55. package/build/workers/WorkerSigningPool.worklet.d.ts +79 -0
  56. package/build/workers/WorkerSigningPool.worklet.d.ts.map +1 -0
  57. package/build/workers/WorkerSigningPool.worklet.js +388 -0
  58. package/build/workers/WorkerSigningPool.worklet.js.map +1 -0
  59. package/build/workers/index.d.ts +2 -2
  60. package/build/workers/index.d.ts.map +1 -1
  61. package/build/workers/index.js +9 -0
  62. package/build/workers/index.js.map +1 -1
  63. package/build/workers/index.react-native.d.ts +28 -0
  64. package/build/workers/index.react-native.d.ts.map +1 -0
  65. package/build/workers/index.react-native.js +67 -0
  66. package/build/workers/index.react-native.js.map +1 -0
  67. package/build/workers/psbt-parallel.d.ts +2 -3
  68. package/build/workers/psbt-parallel.d.ts.map +1 -1
  69. package/build/workers/psbt-parallel.js +4 -4
  70. package/build/workers/psbt-parallel.js.map +1 -1
  71. package/build/workers/types.d.ts +12 -0
  72. package/build/workers/types.d.ts.map +1 -1
  73. package/package.json +11 -1
  74. package/src/env.ts +237 -0
  75. package/src/index.ts +1 -2
  76. package/src/io/index.ts +0 -3
  77. package/src/types.ts +4 -27
  78. package/src/workers/WorkerSigningPool.node.ts +10 -2
  79. package/src/workers/WorkerSigningPool.sequential.ts +190 -0
  80. package/src/workers/WorkerSigningPool.ts +9 -0
  81. package/src/workers/WorkerSigningPool.worklet.ts +519 -0
  82. package/src/workers/index.react-native.ts +110 -0
  83. package/src/workers/index.ts +10 -1
  84. package/src/workers/psbt-parallel.ts +8 -8
  85. package/src/workers/types.ts +16 -0
  86. package/test/env.spec.ts +418 -0
  87. package/test/workers-pool.spec.ts +43 -0
  88. package/test/workers-sequential.spec.ts +669 -0
  89. package/test/workers-worklet.spec.ts +500 -0
  90. package/browser/io/MemoryPool.d.ts +0 -220
  91. package/browser/io/MemoryPool.d.ts.map +0 -1
  92. package/build/io/MemoryPool.d.ts +0 -220
  93. package/build/io/MemoryPool.d.ts.map +0 -1
  94. package/build/io/MemoryPool.js +0 -309
  95. package/build/io/MemoryPool.js.map +0 -1
  96. package/src/io/MemoryPool.ts +0 -343
@@ -32,9 +32,8 @@ import type { PsbtInput, TapKeySig, TapScriptSig } from 'bip174';
32
32
  import type { PublicKey } from '../types.js';
33
33
  import type { Psbt } from '../psbt.js';
34
34
  import { Transaction } from '../transaction.js';
35
- import type { ParallelSignerKeyPair, ParallelSigningResult, SigningTask, WorkerPoolConfig, } from './types.js';
35
+ import type { ParallelSignerKeyPair, ParallelSigningResult, SigningPoolLike, SigningTask, WorkerPoolConfig, } from './types.js';
36
36
  import { SignatureType } from './types.js';
37
- import { WorkerSigningPool } from './WorkerSigningPool.js';
38
37
  import { toXOnly } from '../pubkey.js';
39
38
  import { isTaprootInput, serializeTaprootSignature } from '../psbt/bip371.js';
40
39
  import * as bscript from '../script.js';
@@ -108,17 +107,18 @@ export interface PsbtParallelKeyPair extends ParallelSignerKeyPair {
108
107
  export async function signPsbtParallel(
109
108
  psbt: Psbt,
110
109
  keyPair: PsbtParallelKeyPair,
111
- poolOrConfig?: WorkerSigningPool | WorkerPoolConfig,
110
+ poolOrConfig?: SigningPoolLike | WorkerPoolConfig,
112
111
  options: ParallelSignOptions = {},
113
112
  ): Promise<ParallelSigningResult> {
114
113
  // Get or create pool
115
- let pool: WorkerSigningPool;
114
+ let pool: SigningPoolLike;
116
115
  let shouldShutdown = false;
117
116
 
118
- if (poolOrConfig instanceof WorkerSigningPool) {
117
+ if (poolOrConfig && 'signBatch' in poolOrConfig) {
119
118
  pool = poolOrConfig;
120
119
  } else {
121
- pool = WorkerSigningPool.getInstance(poolOrConfig);
120
+ const { WorkerSigningPool } = await import('./WorkerSigningPool.js');
121
+ pool = WorkerSigningPool.getInstance(poolOrConfig as WorkerPoolConfig | undefined);
122
122
  if (!pool.isPreservingWorkers) {
123
123
  shouldShutdown = true;
124
124
  }
@@ -310,12 +310,12 @@ export function applySignaturesToPsbt(
310
310
  } else {
311
311
  // ECDSA signature
312
312
  const encodedSig = bscript.signature.encode(
313
- Buffer.from(sigResult.signature),
313
+ sigResult.signature,
314
314
  input.sighashType ?? Transaction.SIGHASH_ALL,
315
315
  );
316
316
  const partialSig = [
317
317
  {
318
- pubkey: Buffer.from(pubkey),
318
+ pubkey: Uint8Array.from(pubkey),
319
319
  signature: encodedSig,
320
320
  },
321
321
  ];
@@ -400,6 +400,22 @@ export const WorkerState = {
400
400
 
401
401
  export type WorkerState = (typeof WorkerState)[keyof typeof WorkerState];
402
402
 
403
+ /**
404
+ * Minimum contract for any signing pool implementation.
405
+ *
406
+ * Implemented by WorkerSigningPool (browser), NodeWorkerSigningPool (Node.js),
407
+ * and SequentialSigningPool (React Native / fallback).
408
+ */
409
+ export interface SigningPoolLike {
410
+ signBatch(
411
+ tasks: readonly SigningTask[],
412
+ keyPair: ParallelSignerKeyPair,
413
+ ): Promise<ParallelSigningResult>;
414
+ initialize(): Promise<void>;
415
+ shutdown(): Promise<void>;
416
+ readonly isPreservingWorkers: boolean;
417
+ }
418
+
403
419
  /**
404
420
  * Internal worker wrapper for pool management.
405
421
  */
@@ -0,0 +1,418 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ describe('Runtime capability check — hard requirements', () => {
4
+ beforeEach(() => {
5
+ vi.resetModules();
6
+ });
7
+
8
+ afterEach(() => {
9
+ vi.unstubAllGlobals();
10
+ vi.restoreAllMocks();
11
+ });
12
+
13
+ it('should pass in a capable environment', async () => {
14
+ await expect(import('../src/env.js')).resolves.not.toThrow();
15
+ });
16
+
17
+ it('should throw when BigInt is missing', async () => {
18
+ vi.stubGlobal('BigInt', undefined);
19
+
20
+ await expect(import('../src/env.js')).rejects.toThrow('unsupported runtime');
21
+ await expect(import('../src/env.js')).rejects.toThrow('BigInt');
22
+ });
23
+
24
+ it('should throw when Uint8Array is missing', async () => {
25
+ vi.stubGlobal('Uint8Array', undefined);
26
+
27
+ await expect(import('../src/env.js')).rejects.toThrow('unsupported runtime');
28
+ await expect(import('../src/env.js')).rejects.toThrow('Uint8Array');
29
+ });
30
+
31
+ it('should throw when DataView.getBigInt64 is missing', async () => {
32
+ const orig = DataView.prototype.getBigInt64;
33
+ (DataView.prototype as Record<string, unknown>)['getBigInt64'] = undefined;
34
+
35
+ try {
36
+ await import('../src/env.js');
37
+ expect.unreachable('should have thrown');
38
+ } catch (err) {
39
+ expect((err as Error).message).toContain('getBigInt64');
40
+ } finally {
41
+ DataView.prototype.getBigInt64 = orig;
42
+ }
43
+ });
44
+
45
+ it('should report all missing capabilities at once', async () => {
46
+ vi.stubGlobal('BigInt', undefined);
47
+ vi.stubGlobal('Uint8Array', undefined);
48
+
49
+ try {
50
+ await import('../src/env.js');
51
+ expect.unreachable('should have thrown');
52
+ } catch (err) {
53
+ const msg = (err as Error).message;
54
+ expect(msg).toContain('BigInt');
55
+ expect(msg).toContain('Uint8Array');
56
+ expect(msg).toContain('cannot be polyfilled');
57
+ }
58
+ });
59
+ });
60
+
61
+ describe('Polyfill — TextEncoder / TextDecoder', () => {
62
+ beforeEach(() => {
63
+ vi.resetModules();
64
+ });
65
+
66
+ afterEach(() => {
67
+ vi.unstubAllGlobals();
68
+ vi.restoreAllMocks();
69
+ });
70
+
71
+ it('should polyfill TextEncoder when missing', async () => {
72
+ vi.stubGlobal('TextEncoder', undefined);
73
+
74
+ await import('../src/env.js');
75
+
76
+ const encoder = new globalThis.TextEncoder();
77
+ const result = encoder.encode('hello');
78
+ expect(result).toBeInstanceOf(Uint8Array);
79
+ expect(Array.from(result)).toEqual([0x68, 0x65, 0x6c, 0x6c, 0x6f]);
80
+ });
81
+
82
+ it('should encode multi-byte UTF-8 correctly', async () => {
83
+ vi.stubGlobal('TextEncoder', undefined);
84
+
85
+ await import('../src/env.js');
86
+
87
+ const encoder = new globalThis.TextEncoder();
88
+ // 2-byte: é (U+00E9)
89
+ const twoB = encoder.encode('é');
90
+ expect(Array.from(twoB)).toEqual([0xc3, 0xa9]);
91
+
92
+ // 3-byte: € (U+20AC)
93
+ const threeB = encoder.encode('€');
94
+ expect(Array.from(threeB)).toEqual([0xe2, 0x82, 0xac]);
95
+ });
96
+
97
+ it('should encode surrogate pairs (4-byte UTF-8)', async () => {
98
+ vi.stubGlobal('TextEncoder', undefined);
99
+
100
+ await import('../src/env.js');
101
+
102
+ const encoder = new globalThis.TextEncoder();
103
+ // U+1F600 (😀) = F0 9F 98 80
104
+ const emoji = encoder.encode('😀');
105
+ expect(Array.from(emoji)).toEqual([0xf0, 0x9f, 0x98, 0x80]);
106
+ });
107
+
108
+ it('should polyfill TextDecoder when missing', async () => {
109
+ vi.stubGlobal('TextDecoder', undefined);
110
+
111
+ await import('../src/env.js');
112
+
113
+ const decoder = new globalThis.TextDecoder();
114
+ const result = decoder.decode(new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f]));
115
+ expect(result).toBe('hello');
116
+ });
117
+
118
+ it('should decode multi-byte UTF-8 correctly', async () => {
119
+ vi.stubGlobal('TextDecoder', undefined);
120
+
121
+ await import('../src/env.js');
122
+
123
+ const decoder = new globalThis.TextDecoder();
124
+ expect(decoder.decode(new Uint8Array([0xc3, 0xa9]))).toBe('é');
125
+ expect(decoder.decode(new Uint8Array([0xe2, 0x82, 0xac]))).toBe('€');
126
+ });
127
+
128
+ it('should decode surrogate pairs (4-byte UTF-8)', async () => {
129
+ vi.stubGlobal('TextDecoder', undefined);
130
+
131
+ await import('../src/env.js');
132
+
133
+ const decoder = new globalThis.TextDecoder();
134
+ expect(decoder.decode(new Uint8Array([0xf0, 0x9f, 0x98, 0x80]))).toBe('😀');
135
+ });
136
+
137
+ it('should round-trip encode/decode', async () => {
138
+ vi.stubGlobal('TextEncoder', undefined);
139
+ vi.stubGlobal('TextDecoder', undefined);
140
+
141
+ await import('../src/env.js');
142
+
143
+ const input = 'Hello, 世界! 🚀';
144
+ const encoder = new globalThis.TextEncoder();
145
+ const decoder = new globalThis.TextDecoder();
146
+ expect(decoder.decode(encoder.encode(input))).toBe(input);
147
+ });
148
+
149
+ it('should not replace native TextEncoder', async () => {
150
+ const NativeTextEncoder = globalThis.TextEncoder;
151
+
152
+ await import('../src/env.js');
153
+
154
+ expect(globalThis.TextEncoder).toBe(NativeTextEncoder);
155
+ });
156
+ });
157
+
158
+ describe('Polyfill — Map', () => {
159
+ beforeEach(() => {
160
+ vi.resetModules();
161
+ });
162
+
163
+ afterEach(() => {
164
+ vi.unstubAllGlobals();
165
+ vi.restoreAllMocks();
166
+ });
167
+
168
+ it('should polyfill Map when missing', async () => {
169
+ vi.stubGlobal('Map', undefined);
170
+
171
+ await import('../src/env.js');
172
+
173
+ const map = new globalThis.Map<string, number>();
174
+ map.set('a', 1);
175
+ map.set('b', 2);
176
+ expect(map.size).toBe(2);
177
+ expect(map.get('a')).toBe(1);
178
+ expect(map.has('b')).toBe(true);
179
+ expect(map.has('c')).toBe(false);
180
+ });
181
+
182
+ it('should support delete and clear', async () => {
183
+ vi.stubGlobal('Map', undefined);
184
+
185
+ await import('../src/env.js');
186
+
187
+ const map = new globalThis.Map<string, number>();
188
+ map.set('x', 10);
189
+ map.set('y', 20);
190
+ expect(map.delete('x')).toBe(true);
191
+ expect(map.delete('x')).toBe(false);
192
+ expect(map.size).toBe(1);
193
+
194
+ map.clear();
195
+ expect(map.size).toBe(0);
196
+ });
197
+
198
+ it('should support iteration', async () => {
199
+ vi.stubGlobal('Map', undefined);
200
+
201
+ await import('../src/env.js');
202
+
203
+ const map = new globalThis.Map<string, number>([
204
+ ['a', 1],
205
+ ['b', 2],
206
+ ]);
207
+
208
+ const keys = [...map.keys()];
209
+ const values = [...map.values()];
210
+ const entries = [...map.entries()];
211
+
212
+ expect(keys).toEqual(['a', 'b']);
213
+ expect(values).toEqual([1, 2]);
214
+ expect(entries).toEqual([
215
+ ['a', 1],
216
+ ['b', 2],
217
+ ]);
218
+ });
219
+
220
+ it('should support forEach', async () => {
221
+ vi.stubGlobal('Map', undefined);
222
+
223
+ await import('../src/env.js');
224
+
225
+ const map = new globalThis.Map([['k', 'v']]);
226
+ const collected: string[] = [];
227
+ map.forEach((val, key) => collected.push(`${key}=${val}`));
228
+ expect(collected).toEqual(['k=v']);
229
+ });
230
+
231
+ it('should overwrite existing keys', async () => {
232
+ vi.stubGlobal('Map', undefined);
233
+
234
+ await import('../src/env.js');
235
+
236
+ const map = new globalThis.Map<string, number>();
237
+ map.set('a', 1);
238
+ map.set('a', 2);
239
+ expect(map.size).toBe(1);
240
+ expect(map.get('a')).toBe(2);
241
+ });
242
+
243
+ it('should not replace native Map', async () => {
244
+ const NativeMap = globalThis.Map;
245
+
246
+ await import('../src/env.js');
247
+
248
+ expect(globalThis.Map).toBe(NativeMap);
249
+ });
250
+ });
251
+
252
+ describe('Polyfill — Promise.allSettled', () => {
253
+ beforeEach(() => {
254
+ vi.resetModules();
255
+ });
256
+
257
+ afterEach(() => {
258
+ vi.unstubAllGlobals();
259
+ vi.restoreAllMocks();
260
+ });
261
+
262
+ it('should polyfill Promise.allSettled when missing', async () => {
263
+ const orig = Promise.allSettled;
264
+ (Promise as Record<string, unknown>)['allSettled'] = undefined;
265
+
266
+ try {
267
+ await import('../src/env.js');
268
+
269
+ const results = await Promise.allSettled([
270
+ Promise.resolve(1),
271
+ Promise.reject(new Error('fail')),
272
+ Promise.resolve(3),
273
+ ]);
274
+
275
+ expect(results).toHaveLength(3);
276
+ expect(results[0]).toEqual({ status: 'fulfilled', value: 1 });
277
+ expect(results[1]!.status).toBe('rejected');
278
+ expect((results[1] as PromiseRejectedResult).reason).toBeInstanceOf(Error);
279
+ expect(results[2]).toEqual({ status: 'fulfilled', value: 3 });
280
+ } finally {
281
+ Promise.allSettled = orig;
282
+ }
283
+ });
284
+
285
+ it('should not replace native Promise.allSettled', async () => {
286
+ const native = Promise.allSettled;
287
+
288
+ await import('../src/env.js');
289
+
290
+ expect(Promise.allSettled).toBe(native);
291
+ });
292
+ });
293
+
294
+ describe('Polyfill — structuredClone', () => {
295
+ beforeEach(() => {
296
+ vi.resetModules();
297
+ });
298
+
299
+ afterEach(() => {
300
+ vi.unstubAllGlobals();
301
+ vi.restoreAllMocks();
302
+ });
303
+
304
+ it('should polyfill structuredClone when missing', async () => {
305
+ vi.stubGlobal('structuredClone', undefined);
306
+
307
+ await import('../src/env.js');
308
+
309
+ const original = { a: 1, b: { c: 2 } };
310
+ const cloned = globalThis.structuredClone(original);
311
+
312
+ expect(cloned).toEqual(original);
313
+ expect(cloned).not.toBe(original);
314
+ expect(cloned.b).not.toBe(original.b);
315
+ });
316
+
317
+ it('should deep clone nested objects', async () => {
318
+ vi.stubGlobal('structuredClone', undefined);
319
+
320
+ await import('../src/env.js');
321
+
322
+ const original = { x: [1, 2, { y: 3 }] };
323
+ const cloned = globalThis.structuredClone(original);
324
+
325
+ cloned.x[0] = 99;
326
+ (cloned.x[2] as { y: number }).y = 99;
327
+
328
+ expect(original.x[0]).toBe(1);
329
+ expect((original.x[2] as { y: number }).y).toBe(3);
330
+ });
331
+
332
+ it('should handle primitives and null', async () => {
333
+ vi.stubGlobal('structuredClone', undefined);
334
+
335
+ await import('../src/env.js');
336
+
337
+ expect(globalThis.structuredClone(42)).toBe(42);
338
+ expect(globalThis.structuredClone('hello')).toBe('hello');
339
+ expect(globalThis.structuredClone(true)).toBe(true);
340
+ expect(globalThis.structuredClone(null)).toBe(null);
341
+ });
342
+
343
+ it('should not replace native structuredClone', async () => {
344
+ const native = globalThis.structuredClone;
345
+
346
+ await import('../src/env.js');
347
+
348
+ expect(globalThis.structuredClone).toBe(native);
349
+ });
350
+ });
351
+
352
+ describe('Polyfill — Symbol.dispose / Symbol.asyncDispose', () => {
353
+ beforeEach(() => {
354
+ vi.resetModules();
355
+ });
356
+
357
+ afterEach(() => {
358
+ vi.unstubAllGlobals();
359
+ vi.restoreAllMocks();
360
+ });
361
+
362
+ it('should ensure Symbol.dispose is defined after import', async () => {
363
+ await import('../src/env.js');
364
+ expect(typeof Symbol.dispose).toBe('symbol');
365
+ });
366
+
367
+ it('should ensure Symbol.asyncDispose is defined after import', async () => {
368
+ await import('../src/env.js');
369
+ expect(typeof Symbol.asyncDispose).toBe('symbol');
370
+ });
371
+
372
+ it('should polyfill dispose on a target that lacks it', () => {
373
+ const target: Record<string, symbol | undefined> = { dispose: undefined };
374
+ if (typeof target['dispose'] === 'undefined') {
375
+ target['dispose'] = Symbol.for('Symbol.dispose');
376
+ }
377
+ expect(target['dispose']).toBe(Symbol.for('Symbol.dispose'));
378
+ });
379
+
380
+ it('should polyfill asyncDispose on a target that lacks it', () => {
381
+ const target: Record<string, symbol | undefined> = { asyncDispose: undefined };
382
+ if (typeof target['asyncDispose'] === 'undefined') {
383
+ target['asyncDispose'] = Symbol.for('Symbol.asyncDispose');
384
+ }
385
+ expect(target['asyncDispose']).toBe(Symbol.for('Symbol.asyncDispose'));
386
+ });
387
+
388
+ it('should not overwrite an existing dispose symbol', () => {
389
+ const existing = Symbol('existing');
390
+ const target: Record<string, symbol> = { dispose: existing };
391
+ if (typeof target['dispose'] === 'undefined') {
392
+ target['dispose'] = Symbol.for('Symbol.dispose');
393
+ }
394
+ expect(target['dispose']).toBe(existing);
395
+ });
396
+
397
+ it('should not overwrite an existing asyncDispose symbol', () => {
398
+ const existing = Symbol('existing');
399
+ const target: Record<string, symbol> = { asyncDispose: existing };
400
+ if (typeof target['asyncDispose'] === 'undefined') {
401
+ target['asyncDispose'] = Symbol.for('Symbol.asyncDispose');
402
+ }
403
+ expect(target['asyncDispose']).toBe(existing);
404
+ });
405
+
406
+ it('should produce a usable symbol for async method dispatch', async () => {
407
+ const sym = Symbol.for('Symbol.asyncDispose');
408
+ const obj = {
409
+ disposed: false,
410
+ async [sym]() {
411
+ this.disposed = true;
412
+ },
413
+ };
414
+
415
+ await obj[sym]();
416
+ expect(obj.disposed).toBe(true);
417
+ });
418
+ });
@@ -606,6 +606,49 @@ describe('WorkerSigningPool', () => {
606
606
  });
607
607
  });
608
608
 
609
+ describe('Symbol.asyncDispose', () => {
610
+ it('should have Symbol.asyncDispose method', () => {
611
+ const pool = WorkerSigningPool.getInstance({ workerCount: 1 });
612
+ expect(typeof pool[Symbol.asyncDispose]).toBe('function');
613
+ });
614
+
615
+ it('should terminate workers when disposed', async () => {
616
+ const pool = WorkerSigningPool.getInstance({ workerCount: 2 });
617
+ await pool.initialize();
618
+ expect(pool.workerCount).toBe(2);
619
+
620
+ await pool[Symbol.asyncDispose]();
621
+ expect(pool.workerCount).toBe(0);
622
+ });
623
+
624
+ it('should be safe to call after shutdown', async () => {
625
+ const pool = WorkerSigningPool.getInstance({ workerCount: 2 });
626
+ await pool.initialize();
627
+
628
+ await pool.shutdown();
629
+ await pool[Symbol.asyncDispose](); // Should not throw
630
+
631
+ expect(pool.workerCount).toBe(0);
632
+ });
633
+
634
+ it('should clean up with await using', async () => {
635
+ WorkerSigningPool.resetInstance();
636
+
637
+ let poolRef: InstanceType<typeof WorkerSigningPool> | undefined;
638
+
639
+ // Scoped block — pool is disposed when the block exits
640
+ {
641
+ await using pool = WorkerSigningPool.getInstance({ workerCount: 2 });
642
+ await pool.initialize();
643
+ expect(pool.workerCount).toBe(2);
644
+ poolRef = pool;
645
+ }
646
+
647
+ // After scope exit, dispose has been called
648
+ expect(poolRef!.workerCount).toBe(0);
649
+ });
650
+ });
651
+
609
652
  describe('Worker Pool Without Preservation', () => {
610
653
  it('should terminate workers after batch when not preserving', async () => {
611
654
  const pool = WorkerSigningPool.getInstance({ workerCount: 2 });