@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.
- package/README.md +139 -12
- package/browser/chunks/index.browser-CUaPaKyS.js +10742 -0
- package/browser/env.d.ts +13 -0
- package/browser/env.d.ts.map +1 -0
- package/browser/index.d.ts +1 -1
- package/browser/index.d.ts.map +1 -1
- package/browser/index.js +1894 -12851
- package/browser/io/index.d.ts +0 -1
- package/browser/io/index.d.ts.map +1 -1
- package/browser/types.d.ts.map +1 -1
- package/browser/workers/WorkerSigningPool.d.ts +6 -0
- package/browser/workers/WorkerSigningPool.d.ts.map +1 -1
- package/browser/workers/WorkerSigningPool.node.d.ts +6 -0
- package/browser/workers/WorkerSigningPool.node.d.ts.map +1 -1
- package/browser/workers/WorkerSigningPool.sequential.d.ts +69 -0
- package/browser/workers/WorkerSigningPool.sequential.d.ts.map +1 -0
- package/browser/workers/WorkerSigningPool.worklet.d.ts +64 -0
- package/browser/workers/WorkerSigningPool.worklet.d.ts.map +1 -0
- package/browser/workers/index.browser.d.ts +16 -0
- package/browser/workers/index.browser.d.ts.map +1 -0
- package/browser/workers/index.d.ts +4 -17
- package/browser/workers/index.d.ts.map +1 -1
- package/browser/workers/index.js +21 -0
- package/browser/workers/index.node.d.ts +12 -19
- package/browser/workers/index.node.d.ts.map +1 -1
- package/browser/workers/index.react-native.d.ts +28 -0
- package/browser/workers/index.react-native.d.ts.map +1 -0
- package/browser/workers/index.shared.d.ts +15 -0
- package/browser/workers/index.shared.d.ts.map +1 -0
- package/browser/workers/psbt-parallel.d.ts +2 -3
- package/browser/workers/psbt-parallel.d.ts.map +1 -1
- package/browser/workers/types.d.ts +17 -0
- package/browser/workers/types.d.ts.map +1 -1
- package/build/env.d.ts +13 -0
- package/build/env.d.ts.map +1 -0
- package/build/env.js +198 -0
- package/build/env.js.map +1 -0
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -1
- package/build/index.js.map +1 -1
- package/build/io/index.d.ts +0 -1
- package/build/io/index.d.ts.map +1 -1
- package/build/io/index.js +0 -2
- package/build/io/index.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/build/types.d.ts.map +1 -1
- package/build/types.js +2 -16
- package/build/types.js.map +1 -1
- package/build/workers/WorkerSigningPool.d.ts +6 -0
- package/build/workers/WorkerSigningPool.d.ts.map +1 -1
- package/build/workers/WorkerSigningPool.js +8 -0
- package/build/workers/WorkerSigningPool.js.map +1 -1
- package/build/workers/WorkerSigningPool.node.d.ts +6 -0
- package/build/workers/WorkerSigningPool.node.d.ts.map +1 -1
- package/build/workers/WorkerSigningPool.node.js +9 -2
- package/build/workers/WorkerSigningPool.node.js.map +1 -1
- package/build/workers/WorkerSigningPool.sequential.d.ts +78 -0
- package/build/workers/WorkerSigningPool.sequential.d.ts.map +1 -0
- package/build/workers/WorkerSigningPool.sequential.js +160 -0
- package/build/workers/WorkerSigningPool.sequential.js.map +1 -0
- package/build/workers/WorkerSigningPool.worklet.d.ts +79 -0
- package/build/workers/WorkerSigningPool.worklet.d.ts.map +1 -0
- package/build/workers/WorkerSigningPool.worklet.js +388 -0
- package/build/workers/WorkerSigningPool.worklet.js.map +1 -0
- package/build/workers/index.browser.d.ts +24 -0
- package/build/workers/index.browser.d.ts.map +1 -0
- package/build/workers/index.browser.js +30 -0
- package/build/workers/index.browser.js.map +1 -0
- package/build/workers/index.d.ts +6 -18
- package/build/workers/index.d.ts.map +1 -1
- package/build/workers/index.js +12 -11
- package/build/workers/index.js.map +1 -1
- package/build/workers/index.node.d.ts +17 -3
- package/build/workers/index.node.d.ts.map +1 -1
- package/build/workers/index.node.js +23 -4
- package/build/workers/index.node.js.map +1 -1
- package/build/workers/index.react-native.d.ts +28 -0
- package/build/workers/index.react-native.d.ts.map +1 -0
- package/build/workers/index.react-native.js +67 -0
- package/build/workers/index.react-native.js.map +1 -0
- package/build/workers/index.shared.d.ts +15 -0
- package/build/workers/index.shared.d.ts.map +1 -0
- package/build/workers/index.shared.js +20 -0
- package/build/workers/index.shared.js.map +1 -0
- package/build/workers/psbt-parallel.d.ts +2 -3
- package/build/workers/psbt-parallel.d.ts.map +1 -1
- package/build/workers/psbt-parallel.js +4 -4
- package/build/workers/psbt-parallel.js.map +1 -1
- package/build/workers/types.d.ts +17 -0
- package/build/workers/types.d.ts.map +1 -1
- package/package.json +19 -1
- package/src/env.ts +237 -0
- package/src/index.ts +1 -2
- package/src/io/index.ts +0 -3
- package/src/types.ts +4 -27
- package/src/workers/WorkerSigningPool.node.ts +10 -2
- package/src/workers/WorkerSigningPool.sequential.ts +190 -0
- package/src/workers/WorkerSigningPool.ts +9 -0
- package/src/workers/WorkerSigningPool.worklet.ts +519 -0
- package/src/workers/index.browser.ts +34 -0
- package/src/workers/index.node.ts +28 -5
- package/src/workers/index.react-native.ts +110 -0
- package/src/workers/index.shared.ts +58 -0
- package/src/workers/index.ts +15 -64
- package/src/workers/psbt-parallel.ts +8 -8
- package/src/workers/types.ts +21 -0
- package/test/env.spec.ts +418 -0
- package/test/workers-pool.spec.ts +43 -0
- package/test/workers-sequential.spec.ts +669 -0
- package/test/workers-worklet.spec.ts +500 -0
- package/vite.config.browser.ts +31 -6
- package/browser/io/MemoryPool.d.ts +0 -220
- package/browser/io/MemoryPool.d.ts.map +0 -1
- package/build/io/MemoryPool.d.ts +0 -220
- package/build/io/MemoryPool.d.ts.map +0 -1
- package/build/io/MemoryPool.js +0 -309
- package/build/io/MemoryPool.js.map +0 -1
- 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';
|
package/src/workers/index.ts
CHANGED
|
@@ -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,
|
|
50
|
+
import type { WorkerPoolConfig, SigningPoolLike } from './types.js';
|
|
50
51
|
|
|
51
|
-
|
|
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?:
|
|
110
|
+
poolOrConfig?: SigningPoolLike | WorkerPoolConfig,
|
|
112
111
|
options: ParallelSignOptions = {},
|
|
113
112
|
): Promise<ParallelSigningResult> {
|
|
114
113
|
// Get or create pool
|
|
115
|
-
let pool:
|
|
114
|
+
let pool: SigningPoolLike;
|
|
116
115
|
let shouldShutdown = false;
|
|
117
116
|
|
|
118
|
-
if (poolOrConfig
|
|
117
|
+
if (poolOrConfig && 'signBatch' in poolOrConfig) {
|
|
119
118
|
pool = poolOrConfig;
|
|
120
119
|
} else {
|
|
121
|
-
|
|
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
|
-
|
|
313
|
+
sigResult.signature,
|
|
314
314
|
input.sighashType ?? Transaction.SIGHASH_ALL,
|
|
315
315
|
);
|
|
316
316
|
const partialSig = [
|
|
317
317
|
{
|
|
318
|
-
pubkey:
|
|
318
|
+
pubkey: Uint8Array.from(pubkey),
|
|
319
319
|
signature: encodedSig,
|
|
320
320
|
},
|
|
321
321
|
];
|
package/src/workers/types.ts
CHANGED
|
@@ -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
|
*/
|
package/test/env.spec.ts
ADDED
|
@@ -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
|
+
});
|