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

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 (119) hide show
  1. package/README.md +139 -12
  2. package/browser/chunks/index.browser-CUaPaKyS.js +10742 -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 +1894 -12851
  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.browser.d.ts +16 -0
  20. package/browser/workers/index.browser.d.ts.map +1 -0
  21. package/browser/workers/index.d.ts +4 -17
  22. package/browser/workers/index.d.ts.map +1 -1
  23. package/browser/workers/index.js +21 -0
  24. package/browser/workers/index.node.d.ts +12 -19
  25. package/browser/workers/index.node.d.ts.map +1 -1
  26. package/browser/workers/index.react-native.d.ts +28 -0
  27. package/browser/workers/index.react-native.d.ts.map +1 -0
  28. package/browser/workers/index.shared.d.ts +15 -0
  29. package/browser/workers/index.shared.d.ts.map +1 -0
  30. package/browser/workers/psbt-parallel.d.ts +2 -3
  31. package/browser/workers/psbt-parallel.d.ts.map +1 -1
  32. package/browser/workers/types.d.ts +17 -0
  33. package/browser/workers/types.d.ts.map +1 -1
  34. package/build/env.d.ts +13 -0
  35. package/build/env.d.ts.map +1 -0
  36. package/build/env.js +198 -0
  37. package/build/env.js.map +1 -0
  38. package/build/index.d.ts +2 -1
  39. package/build/index.d.ts.map +1 -1
  40. package/build/index.js +2 -1
  41. package/build/index.js.map +1 -1
  42. package/build/io/index.d.ts +0 -1
  43. package/build/io/index.d.ts.map +1 -1
  44. package/build/io/index.js +0 -2
  45. package/build/io/index.js.map +1 -1
  46. package/build/tsconfig.build.tsbuildinfo +1 -1
  47. package/build/types.d.ts.map +1 -1
  48. package/build/types.js +2 -16
  49. package/build/types.js.map +1 -1
  50. package/build/workers/WorkerSigningPool.d.ts +6 -0
  51. package/build/workers/WorkerSigningPool.d.ts.map +1 -1
  52. package/build/workers/WorkerSigningPool.js +8 -0
  53. package/build/workers/WorkerSigningPool.js.map +1 -1
  54. package/build/workers/WorkerSigningPool.node.d.ts +6 -0
  55. package/build/workers/WorkerSigningPool.node.d.ts.map +1 -1
  56. package/build/workers/WorkerSigningPool.node.js +9 -2
  57. package/build/workers/WorkerSigningPool.node.js.map +1 -1
  58. package/build/workers/WorkerSigningPool.sequential.d.ts +78 -0
  59. package/build/workers/WorkerSigningPool.sequential.d.ts.map +1 -0
  60. package/build/workers/WorkerSigningPool.sequential.js +160 -0
  61. package/build/workers/WorkerSigningPool.sequential.js.map +1 -0
  62. package/build/workers/WorkerSigningPool.worklet.d.ts +79 -0
  63. package/build/workers/WorkerSigningPool.worklet.d.ts.map +1 -0
  64. package/build/workers/WorkerSigningPool.worklet.js +388 -0
  65. package/build/workers/WorkerSigningPool.worklet.js.map +1 -0
  66. package/build/workers/index.browser.d.ts +24 -0
  67. package/build/workers/index.browser.d.ts.map +1 -0
  68. package/build/workers/index.browser.js +30 -0
  69. package/build/workers/index.browser.js.map +1 -0
  70. package/build/workers/index.d.ts +6 -18
  71. package/build/workers/index.d.ts.map +1 -1
  72. package/build/workers/index.js +12 -11
  73. package/build/workers/index.js.map +1 -1
  74. package/build/workers/index.node.d.ts +17 -3
  75. package/build/workers/index.node.d.ts.map +1 -1
  76. package/build/workers/index.node.js +23 -4
  77. package/build/workers/index.node.js.map +1 -1
  78. package/build/workers/index.react-native.d.ts +28 -0
  79. package/build/workers/index.react-native.d.ts.map +1 -0
  80. package/build/workers/index.react-native.js +67 -0
  81. package/build/workers/index.react-native.js.map +1 -0
  82. package/build/workers/index.shared.d.ts +15 -0
  83. package/build/workers/index.shared.d.ts.map +1 -0
  84. package/build/workers/index.shared.js +20 -0
  85. package/build/workers/index.shared.js.map +1 -0
  86. package/build/workers/psbt-parallel.d.ts +2 -3
  87. package/build/workers/psbt-parallel.d.ts.map +1 -1
  88. package/build/workers/psbt-parallel.js +4 -4
  89. package/build/workers/psbt-parallel.js.map +1 -1
  90. package/build/workers/types.d.ts +17 -0
  91. package/build/workers/types.d.ts.map +1 -1
  92. package/package.json +19 -1
  93. package/src/env.ts +237 -0
  94. package/src/index.ts +1 -2
  95. package/src/io/index.ts +0 -3
  96. package/src/types.ts +4 -27
  97. package/src/workers/WorkerSigningPool.node.ts +10 -2
  98. package/src/workers/WorkerSigningPool.sequential.ts +190 -0
  99. package/src/workers/WorkerSigningPool.ts +9 -0
  100. package/src/workers/WorkerSigningPool.worklet.ts +519 -0
  101. package/src/workers/index.browser.ts +34 -0
  102. package/src/workers/index.node.ts +28 -5
  103. package/src/workers/index.react-native.ts +110 -0
  104. package/src/workers/index.shared.ts +58 -0
  105. package/src/workers/index.ts +15 -64
  106. package/src/workers/psbt-parallel.ts +8 -8
  107. package/src/workers/types.ts +21 -0
  108. package/test/env.spec.ts +418 -0
  109. package/test/workers-pool.spec.ts +43 -0
  110. package/test/workers-sequential.spec.ts +669 -0
  111. package/test/workers-worklet.spec.ts +500 -0
  112. package/vite.config.browser.ts +31 -6
  113. package/browser/io/MemoryPool.d.ts +0 -220
  114. package/browser/io/MemoryPool.d.ts.map +0 -1
  115. package/build/io/MemoryPool.d.ts +0 -220
  116. package/build/io/MemoryPool.d.ts.map +0 -1
  117. package/build/io/MemoryPool.js +0 -309
  118. package/build/io/MemoryPool.js.map +0 -1
  119. package/src/io/MemoryPool.ts +0 -343
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared worker module exports.
3
+ *
4
+ * Contains all platform-agnostic exports used by browser, Node.js, and generic
5
+ * entry points. Platform-specific entry points re-export from this module and
6
+ * add their own `detectRuntime` and `createSigningPool` implementations.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ // Type exports
12
+ export {
13
+ SignatureType,
14
+ type SigningTaskMessage,
15
+ type BatchSigningMessage,
16
+ type BatchSigningTask,
17
+ type BatchSigningResultMessage,
18
+ type BatchSigningTaskResult,
19
+ type BatchSigningTaskError,
20
+ type WorkerInitMessage,
21
+ type WorkerShutdownMessage,
22
+ type WorkerMessage,
23
+ type SigningResultMessage,
24
+ type SigningErrorMessage,
25
+ type WorkerReadyMessage,
26
+ type WorkerShutdownAckMessage,
27
+ type WorkerResponse,
28
+ isSigningError,
29
+ isSigningResult,
30
+ isBatchResult,
31
+ isWorkerReady,
32
+ type WorkerEccLib,
33
+ type WorkerPoolConfig,
34
+ type SigningTask,
35
+ type ParallelSignerKeyPair,
36
+ type ParallelSigningResult,
37
+ type SigningPoolLike,
38
+ WorkerState,
39
+ type PooledWorker,
40
+ } from './types.js';
41
+
42
+ // Browser worker pool
43
+ export { WorkerSigningPool, getSigningPool } from './WorkerSigningPool.js';
44
+
45
+ // Worker code generation (for custom implementations)
46
+ export { generateWorkerCode, createWorkerBlobUrl, revokeWorkerBlobUrl } from './signing-worker.js';
47
+
48
+ // ECC bundle (for embedding in custom workers)
49
+ export { ECC_BUNDLE, ECC_BUNDLE_SIZE } from './ecc-bundle.js';
50
+
51
+ // PSBT parallel signing integration
52
+ export {
53
+ signPsbtParallel,
54
+ prepareSigningTasks,
55
+ applySignaturesToPsbt,
56
+ type ParallelSignOptions,
57
+ type PsbtParallelKeyPair,
58
+ } from './psbt-parallel.js';
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Worker-based parallel signing module.
2
+ * Worker-based parallel signing module (generic entry point).
3
3
  *
4
4
  * Provides secure parallel signature computation using worker threads.
5
5
  * Works in both Node.js (worker_threads) and browsers (Web Workers).
6
+ * Uses runtime detection to select the appropriate pool implementation.
6
7
  *
7
8
  * @example
8
9
  * ```typescript
@@ -46,62 +47,19 @@
46
47
  * @packageDocumentation
47
48
  */
48
49
 
49
- import type { WorkerPoolConfig, SigningTask, ParallelSignerKeyPair, ParallelSigningResult } from './types.js';
50
+ import type { WorkerPoolConfig, SigningPoolLike } from './types.js';
50
51
 
51
- // Type exports
52
- export {
53
- SignatureType,
54
- type SigningTaskMessage,
55
- type BatchSigningMessage,
56
- type BatchSigningTask,
57
- type BatchSigningResultMessage,
58
- type BatchSigningTaskResult,
59
- type BatchSigningTaskError,
60
- type WorkerInitMessage,
61
- type WorkerShutdownMessage,
62
- type WorkerMessage,
63
- type SigningResultMessage,
64
- type SigningErrorMessage,
65
- type WorkerReadyMessage,
66
- type WorkerShutdownAckMessage,
67
- type WorkerResponse,
68
- isSigningError,
69
- isSigningResult,
70
- isBatchResult,
71
- isWorkerReady,
72
- type WorkerEccLib,
73
- type WorkerPoolConfig,
74
- type SigningTask,
75
- type ParallelSignerKeyPair,
76
- type ParallelSigningResult,
77
- WorkerState,
78
- type PooledWorker,
79
- } from './types.js';
80
-
81
- // Browser worker pool
82
- export { WorkerSigningPool, getSigningPool } from './WorkerSigningPool.js';
83
-
84
- // Worker code generation (for custom implementations)
85
- export { generateWorkerCode, createWorkerBlobUrl, revokeWorkerBlobUrl } from './signing-worker.js';
86
-
87
- // ECC bundle (for embedding in custom workers)
88
- export { ECC_BUNDLE, ECC_BUNDLE_SIZE } from './ecc-bundle.js';
89
-
90
- // PSBT parallel signing integration
91
- export {
92
- signPsbtParallel,
93
- prepareSigningTasks,
94
- applySignaturesToPsbt,
95
- type ParallelSignOptions,
96
- type PsbtParallelKeyPair,
97
- } from './psbt-parallel.js';
52
+ export * from './index.shared.js';
98
53
 
99
54
  /**
100
55
  * Detects the runtime environment and returns the appropriate signing pool.
101
56
  *
102
57
  * @returns 'node' for Node.js, 'browser' for browsers, 'unknown' otherwise
103
58
  */
104
- export function detectRuntime(): 'node' | 'browser' | 'unknown' {
59
+ export function detectRuntime(): 'node' | 'browser' | 'react-native' | 'unknown' {
60
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
61
+ return 'react-native';
62
+ }
105
63
  if (typeof process !== 'undefined' && process.versions?.node) {
106
64
  return 'node';
107
65
  }
@@ -132,19 +90,7 @@ export function detectRuntime(): 'node' | 'browser' | 'unknown' {
132
90
  * await pool.shutdown();
133
91
  * ```
134
92
  */
135
- export async function createSigningPool(config?: WorkerPoolConfig): Promise<{
136
- signBatch: (
137
- tasks: readonly SigningTask[],
138
- keyPair: ParallelSignerKeyPair,
139
- ) => Promise<ParallelSigningResult>;
140
- preserveWorkers: () => void;
141
- releaseWorkers: () => void;
142
- shutdown: () => Promise<void>;
143
- workerCount: number;
144
- idleWorkerCount: number;
145
- busyWorkerCount: number;
146
- isPreservingWorkers: boolean;
147
- }> {
93
+ export async function createSigningPool(config?: WorkerPoolConfig): Promise<SigningPoolLike> {
148
94
  const runtime = detectRuntime();
149
95
 
150
96
  if (runtime === 'node') {
@@ -157,7 +103,12 @@ export async function createSigningPool(config?: WorkerPoolConfig): Promise<{
157
103
  const pool = WorkerSigningPool.getInstance(config);
158
104
  await pool.initialize();
159
105
  return pool;
106
+ } else if (runtime === 'react-native') {
107
+ const { SequentialSigningPool } = await import('./WorkerSigningPool.sequential.js');
108
+ const pool = SequentialSigningPool.getInstance(config);
109
+ await pool.initialize();
110
+ return pool;
160
111
  } else {
161
112
  throw new Error('Unsupported runtime for worker signing pool');
162
113
  }
163
- }
114
+ }
@@ -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,27 @@ 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
+ preserveWorkers(): void;
415
+ releaseWorkers(): void;
416
+ initialize(): Promise<void>;
417
+ shutdown(): Promise<void>;
418
+ readonly workerCount: number;
419
+ readonly idleWorkerCount: number;
420
+ readonly busyWorkerCount: number;
421
+ readonly isPreservingWorkers: boolean;
422
+ }
423
+
403
424
  /**
404
425
  * Internal worker wrapper for pool management.
405
426
  */
@@ -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
+ });