@evolu/nodejs 3.0.0-next.2 → 3.0.0

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.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Node.js utilities for testing production bundles.
3
+ *
4
+ * These utilities use the dedicated `@evolu/nodejs/TestBundle` entry point so
5
+ * normal `@evolu/nodejs` imports do not evaluate the test toolchain, while
6
+ * bundle tests do not evaluate unrelated Evolu Node.js adapters.
7
+ *
8
+ * The {@link testBundle} function loads its optional Webpack and Vite peer
9
+ * dependencies only when called.
10
+ *
11
+ * @module
12
+ */
13
+ import { type PositiveDuration, type ReadonlyRecord } from "@evolu/common";
14
+ /** The measured sizes of one bundle. */
15
+ export interface TestBundleSize {
16
+ readonly rawSizeInBytes: number;
17
+ /** The Brotli-compressed size using quality 11. */
18
+ readonly brotliSizeInBytes: number;
19
+ }
20
+ /** Bundle sizes keyed by bundler name and version. */
21
+ export type TestBundleCaseResult = ReadonlyRecord<string, TestBundleSize>;
22
+ /** Case results keyed by case name for size snapshots. */
23
+ export type TestBundleResult = ReadonlyRecord<string, TestBundleCaseResult>;
24
+ /** The measured output supplied to {@link TestBundleCase.verify}. */
25
+ export interface TestBundle extends TestBundleSize {
26
+ /** The bundler name and version, for example `"vite@8.1.5"`. */
27
+ readonly bundler: string;
28
+ readonly code: string;
29
+ /** The copied artifact, or `null` when no output directory was requested. */
30
+ readonly outputPath: string | null;
31
+ }
32
+ /** One named bundle test case. */
33
+ export interface TestBundleCase {
34
+ /**
35
+ * A JavaScript or erasable-TypeScript entry.
36
+ *
37
+ * TypeScript dependencies are supported. TSX and TypeScript syntax that
38
+ * requires JavaScript generation are not supported by Webpack.
39
+ */
40
+ readonly entryPath: string;
41
+ /** Verifies the default export returned by each emitted bundle. */
42
+ readonly verify: (value: unknown, bundle: TestBundle) => void | Promise<void>;
43
+ }
44
+ /** Options for producing and verifying equivalent bundles. */
45
+ export interface TestBundleOptions {
46
+ /** Named cases bundled with every supported bundler. */
47
+ readonly cases: ReadonlyRecord<string, TestBundleCase>;
48
+ /**
49
+ * Package aliases resolved for every case and bundler. Targets must be
50
+ * absolute paths to modules using JavaScript syntax.
51
+ */
52
+ readonly aliases?: ReadonlyRecord<string, string>;
53
+ /**
54
+ * Copies emitted bundles into this directory for later inspection. Relative
55
+ * paths are resolved from the current working directory.
56
+ */
57
+ readonly outputDirectory?: string;
58
+ /** Maximum duration for producing each bundle. Defaults to `"30s"`. */
59
+ readonly bundlingTimeout?: PositiveDuration;
60
+ /** Maximum duration for executing each emitted bundle. Defaults to `"5s"`. */
61
+ readonly timeout?: PositiveDuration;
62
+ }
63
+ /**
64
+ * Bundles, executes, measures, and verifies named cases with every supported
65
+ * bundler.
66
+ *
67
+ * The fixture must default-export either a value, a promise, or a function that
68
+ * returns one. The resolved value must be structured-cloneable so it can cross
69
+ * the worker boundary. All asynchronous work started by the fixture must be
70
+ * awaited by that returned promise; detached work scheduled after it settles is
71
+ * outside the execution contract. Verification runs outside the measured
72
+ * bundle. Each emitted bundle runs in an isolated worker so evaluation errors,
73
+ * rejected promises, uncaught errors and unhandled rejections observed before
74
+ * completion, and timeouts fail the test without affecting the test process.
75
+ * Cases and bundlers form one flattened job list that runs concurrently,
76
+ * bounded by the CPU parallelism available to the process. If multiple jobs
77
+ * fail, all failures are reported together with their case and bundler names.
78
+ * The returned record contains only raw and Brotli byte counts grouped by case
79
+ * and versioned bundler name, so it can be compared directly with an inline
80
+ * snapshot.
81
+ *
82
+ * ### Example
83
+ *
84
+ * ```ts
85
+ * import { mkdtemp, rm, writeFile } from "node:fs/promises";
86
+ * import { tmpdir } from "node:os";
87
+ * import { join } from "node:path";
88
+ * import { testBundle } from "@evolu/nodejs/TestBundle";
89
+ *
90
+ * const directory = await mkdtemp(join(tmpdir(), "evolu-test-bundle-"));
91
+ * try {
92
+ * const entryPath = join(directory, "entry.ts");
93
+ * await writeFile(entryPath, "export default { answer: 42 };\n");
94
+ * const result = await testBundle({
95
+ * cases: {
96
+ * example: {
97
+ * entryPath,
98
+ * verify: (value) => {
99
+ * expect(value).toEqual({ answer: 42 });
100
+ * },
101
+ * },
102
+ * },
103
+ * });
104
+ * for (const size of Object.values(result.example)) {
105
+ * expect(size.brotliSizeInBytes).toBeGreaterThan(0);
106
+ * }
107
+ * } finally {
108
+ * await rm(directory, { recursive: true });
109
+ * }
110
+ * ```
111
+ */
112
+ export declare const testBundle: ({ cases, aliases, outputDirectory, bundlingTimeout, timeout: executionTimeout, }: TestBundleOptions) => Promise<TestBundleResult>;
113
+ //# sourceMappingURL=TestBundle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestBundle.d.ts","sourceRoot":"","sources":["../../src/TestBundle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAYL,KAAK,gBAAgB,EACrB,KAAK,cAAc,EAQpB,MAAM,eAAe,CAAC;AA4CvB,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,mDAAmD;IACnD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAED,sDAAsD;AACtD,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAE1E,0DAA0D;AAC1D,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;AAE5E,qEAAqE;AACrE,MAAM,WAAW,UAAW,SAAQ,cAAc;IAChD,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACvD;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,uEAAuE;IACvE,QAAQ,CAAC,eAAe,CAAC,EAAE,gBAAgB,CAAC;IAC5C,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,UAAU,qFAMpB,iBAAiB,KAAG,OAAO,CAAC,gBAAgB,CAgK9C,CAAC"}
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Node.js utilities for testing production bundles.
3
+ *
4
+ * These utilities use the dedicated `@evolu/nodejs/TestBundle` entry point so
5
+ * normal `@evolu/nodejs` imports do not evaluate the test toolchain, while
6
+ * bundle tests do not evaluate unrelated Evolu Node.js adapters.
7
+ *
8
+ * The {@link testBundle} function loads its optional Webpack and Vite peer
9
+ * dependencies only when called.
10
+ *
11
+ * @module
12
+ */
13
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
14
+ if (value !== null && value !== void 0) {
15
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
16
+ var dispose, inner;
17
+ if (async) {
18
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
19
+ dispose = value[Symbol.asyncDispose];
20
+ }
21
+ if (dispose === void 0) {
22
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
23
+ dispose = value[Symbol.dispose];
24
+ if (async) inner = dispose;
25
+ }
26
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
27
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
28
+ env.stack.push({ value: value, dispose: dispose, async: async });
29
+ }
30
+ else if (async) {
31
+ env.stack.push({ async: true });
32
+ }
33
+ return value;
34
+ };
35
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
36
+ return function (env) {
37
+ function fail(e) {
38
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
39
+ env.hasError = true;
40
+ }
41
+ var r, s = 0;
42
+ function next() {
43
+ while (r = env.stack.pop()) {
44
+ try {
45
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
46
+ if (r.dispose) {
47
+ var result = r.dispose.call(r.value);
48
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
49
+ }
50
+ else s |= 1;
51
+ }
52
+ catch (e) {
53
+ fail(e);
54
+ }
55
+ }
56
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
57
+ if (env.hasError) throw env.error;
58
+ }
59
+ return next();
60
+ };
61
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
62
+ var e = new Error(message);
63
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
64
+ });
65
+ import { allSettled, assert, assertType, instanceOf, createRun, durationToMillis, escapeRegExp, filterArray, isErr, mapArray, safelyStringifyUnknownValue, String as StringType, timeout, TimeoutError, tryAsync, } from "@evolu/common";
66
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
67
+ import { tmpdir } from "node:os";
68
+ import { dirname, isAbsolute, join, resolve } from "node:path";
69
+ import { pathToFileURL } from "node:url";
70
+ import { promisify } from "node:util";
71
+ import { isMainThread, parentPort, Worker, workerData, } from "node:worker_threads";
72
+ import { brotliCompressSync, constants as zlibConstants } from "node:zlib";
73
+ import { availableParallelism } from "./Platform.js";
74
+ /**
75
+ * Bundles, executes, measures, and verifies named cases with every supported
76
+ * bundler.
77
+ *
78
+ * The fixture must default-export either a value, a promise, or a function that
79
+ * returns one. The resolved value must be structured-cloneable so it can cross
80
+ * the worker boundary. All asynchronous work started by the fixture must be
81
+ * awaited by that returned promise; detached work scheduled after it settles is
82
+ * outside the execution contract. Verification runs outside the measured
83
+ * bundle. Each emitted bundle runs in an isolated worker so evaluation errors,
84
+ * rejected promises, uncaught errors and unhandled rejections observed before
85
+ * completion, and timeouts fail the test without affecting the test process.
86
+ * Cases and bundlers form one flattened job list that runs concurrently,
87
+ * bounded by the CPU parallelism available to the process. If multiple jobs
88
+ * fail, all failures are reported together with their case and bundler names.
89
+ * The returned record contains only raw and Brotli byte counts grouped by case
90
+ * and versioned bundler name, so it can be compared directly with an inline
91
+ * snapshot.
92
+ *
93
+ * ### Example
94
+ *
95
+ * ```ts
96
+ * import { mkdtemp, rm, writeFile } from "node:fs/promises";
97
+ * import { tmpdir } from "node:os";
98
+ * import { join } from "node:path";
99
+ * import { testBundle } from "@evolu/nodejs/TestBundle";
100
+ *
101
+ * const directory = await mkdtemp(join(tmpdir(), "evolu-test-bundle-"));
102
+ * try {
103
+ * const entryPath = join(directory, "entry.ts");
104
+ * await writeFile(entryPath, "export default { answer: 42 };\n");
105
+ * const result = await testBundle({
106
+ * cases: {
107
+ * example: {
108
+ * entryPath,
109
+ * verify: (value) => {
110
+ * expect(value).toEqual({ answer: 42 });
111
+ * },
112
+ * },
113
+ * },
114
+ * });
115
+ * for (const size of Object.values(result.example)) {
116
+ * expect(size.brotliSizeInBytes).toBeGreaterThan(0);
117
+ * }
118
+ * } finally {
119
+ * await rm(directory, { recursive: true });
120
+ * }
121
+ * ```
122
+ */
123
+ export const testBundle = async ({ cases, aliases = {}, outputDirectory, bundlingTimeout = "30s", timeout: executionTimeout = "5s", }) => {
124
+ const env_1 = { stack: [], error: void 0, hasError: false };
125
+ try {
126
+ const caseEntries = Object.entries(cases);
127
+ assert(caseEntries.length > 0, "Bundle tests require at least one case.");
128
+ for (const [name, path] of Object.entries(aliases)) {
129
+ assert(isAbsolute(path), `Bundle alias "${name}" target must be an absolute path.`);
130
+ }
131
+ const disposer = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
132
+ const temporaryDirectory = disposer.adopt(await mkdtemp(join(tmpdir(), "evolu-bundle-")), async (temporaryDirectory) => {
133
+ await rm(temporaryDirectory, { recursive: true, force: true });
134
+ });
135
+ const jobs = caseEntries.flatMap(([caseName, testCase]) => testBundlers.map((bundler) => ({ caseName, testCase, bundler })));
136
+ const run = __addDisposableResource(env_1, createRun(), true);
137
+ const results = await run.ok(allSettled(jobs, ({ caseName, testCase, bundler }, jobIndex) => async (run) => tryAsync(async () => {
138
+ const sourceEntryPath = resolve(testCase.entryPath);
139
+ const bundlerDirectory = join(temporaryDirectory, `${jobIndex}-${bundler.name}`);
140
+ await mkdir(bundlerDirectory, { recursive: true });
141
+ const bundlingResult = await run(timeout(runTestBundler(bundler.name, {
142
+ entryPath: sourceEntryPath,
143
+ outputDirectory: bundlerDirectory,
144
+ aliases,
145
+ }), bundlingTimeout));
146
+ if (!bundlingResult.ok) {
147
+ if (TimeoutError.is(bundlingResult.error)) {
148
+ throw new Error(`Bundle production timed out after ${durationToMillis(bundlingTimeout)} ms.`);
149
+ }
150
+ throw bundlingResult.error;
151
+ }
152
+ const output = bundlingResult.value;
153
+ const executablePath = join(bundlerDirectory, "bundle.mjs");
154
+ await writeFile(executablePath, output.code);
155
+ let persistedOutputPath = null;
156
+ if (outputDirectory) {
157
+ persistedOutputPath = join(outputDirectory, `${encodeURIComponent(caseName)}.${bundler.name}.mjs`);
158
+ await mkdir(outputDirectory, { recursive: true });
159
+ await writeFile(persistedOutputPath, output.code);
160
+ }
161
+ const bundle = {
162
+ bundler: `${bundler.name}@${output.version}`,
163
+ code: output.code,
164
+ outputPath: persistedOutputPath,
165
+ rawSizeInBytes: Buffer.byteLength(output.code),
166
+ brotliSizeInBytes: brotliCompressSync(output.code, {
167
+ params: {
168
+ [zlibConstants.BROTLI_PARAM_QUALITY]: 11,
169
+ },
170
+ }).byteLength,
171
+ };
172
+ const executionResult = await run(timeout(runTestBundle(executablePath), executionTimeout));
173
+ if (!executionResult.ok) {
174
+ if (TimeoutError.is(executionResult.error)) {
175
+ throw new Error(`Bundle execution timed out after ${durationToMillis(executionTimeout)} ms.`);
176
+ }
177
+ throw executionResult.error;
178
+ }
179
+ await testCase.verify(executionResult.value, bundle);
180
+ return { caseName, bundle };
181
+ }, (error) => {
182
+ run.signal.throwIfAborted();
183
+ return {
184
+ caseName,
185
+ bundler: bundler.name,
186
+ error,
187
+ };
188
+ }), { concurrency: availableParallelism() }));
189
+ const failures = filterArray(results, isErr);
190
+ if (failures.length > 0) {
191
+ const errors = mapArray(failures, ({ error: { caseName, bundler, error } }) => {
192
+ const message = error instanceof Error
193
+ ? error.message
194
+ : safelyStringifyUnknownValue(error);
195
+ return Object.assign(new Error(`${caseName} / ${bundler}: ${message}`, { cause: error }), { caseName, bundler });
196
+ });
197
+ throw new AggregateError(errors, [
198
+ "Bundle tests failed.",
199
+ ...mapArray(errors, (error) => `- ${error.message}`),
200
+ ].join("\n"));
201
+ }
202
+ const bundleEntriesByCase = new Map();
203
+ for (const [caseName] of caseEntries)
204
+ bundleEntriesByCase.set(caseName, []);
205
+ for (const result of results) {
206
+ assert(result.ok, "Expected every bundle test to succeed.");
207
+ const { caseName, bundle } = result.value;
208
+ const entries = bundleEntriesByCase.get(caseName);
209
+ assert(entries, `Missing bundle test case "${caseName}".`);
210
+ entries.push([
211
+ bundle.bundler,
212
+ {
213
+ rawSizeInBytes: bundle.rawSizeInBytes,
214
+ brotliSizeInBytes: bundle.brotliSizeInBytes,
215
+ },
216
+ ]);
217
+ }
218
+ return Object.fromEntries(Array.from(bundleEntriesByCase, ([caseName, entries]) => [
219
+ caseName,
220
+ Object.fromEntries(entries),
221
+ ]));
222
+ }
223
+ catch (e_1) {
224
+ env_1.error = e_1;
225
+ env_1.hasError = true;
226
+ }
227
+ finally {
228
+ const result_1 = __disposeResources(env_1);
229
+ if (result_1)
230
+ await result_1;
231
+ }
232
+ };
233
+ /** A Webpack production adapter for {@link testBundle}. */
234
+ const testWebpackBundler = {
235
+ name: "webpack",
236
+ bundle: async ({ entryPath, outputDirectory, aliases }) => {
237
+ const env_2 = { stack: [], error: void 0, hasError: false };
238
+ try {
239
+ const { default: webpack } = await import("webpack");
240
+ const filename = "webpack.js";
241
+ const disposer = __addDisposableResource(env_2, new AsyncDisposableStack(), true);
242
+ const compiler = disposer.adopt(webpack({
243
+ mode: "production",
244
+ target: ["web", "es2020"],
245
+ entry: entryPath,
246
+ experiments: { outputModule: true, typescript: true },
247
+ output: {
248
+ path: outputDirectory,
249
+ filename,
250
+ module: true,
251
+ library: { type: "module" },
252
+ },
253
+ resolve: {
254
+ alias: Object.fromEntries(Object.entries(aliases).map(([name, path]) => [`${name}$`, path])),
255
+ },
256
+ optimization: {
257
+ usedExports: true,
258
+ sideEffects: true,
259
+ minimize: true,
260
+ },
261
+ stats: "errors-only",
262
+ }), async (compiler) => {
263
+ await promisify(compiler.close.bind(compiler))();
264
+ });
265
+ const stats = await promisify(compiler.run.bind(compiler))();
266
+ if (stats?.hasErrors())
267
+ throw new Error(stats.toString("errors-only"));
268
+ return {
269
+ code: await readFile(join(outputDirectory, filename), "utf8"),
270
+ version: webpack.version,
271
+ };
272
+ }
273
+ catch (e_2) {
274
+ env_2.error = e_2;
275
+ env_2.hasError = true;
276
+ }
277
+ finally {
278
+ const result_2 = __disposeResources(env_2);
279
+ if (result_2)
280
+ await result_2;
281
+ }
282
+ },
283
+ };
284
+ /** A Vite production adapter for {@link testBundle}. */
285
+ const testViteBundler = {
286
+ name: "vite",
287
+ bundle: async ({ entryPath, aliases }) => {
288
+ const vite = await import("vite");
289
+ const output = await vite.build({
290
+ root: dirname(entryPath),
291
+ configFile: false,
292
+ envFile: false,
293
+ logLevel: "silent",
294
+ resolve: {
295
+ alias: Object.entries(aliases).map(([name, replacement]) => ({
296
+ find: new RegExp(`^${escapeRegExp(name)}$`),
297
+ replacement,
298
+ })),
299
+ },
300
+ build: {
301
+ write: false,
302
+ minify: true,
303
+ target: "es2020",
304
+ lib: {
305
+ entry: entryPath,
306
+ formats: ["es"],
307
+ fileName: () => "vite.js",
308
+ },
309
+ rolldownOptions: {
310
+ cwd: dirname(entryPath),
311
+ output: { codeSplitting: false, comments: false, minify: true },
312
+ },
313
+ },
314
+ });
315
+ assert(Array.isArray(output), "Vite did not return build outputs.");
316
+ const outputs = output;
317
+ assert(outputs.length === 1, "Vite did not return one build output.");
318
+ const viteOutput = outputs[0];
319
+ assert(viteOutput, "Vite did not return a build output.");
320
+ assert(viteOutput.output.length === 1, "Vite did not emit one JavaScript chunk.");
321
+ const chunk = viteOutput.output[0];
322
+ assert(chunk, "Vite did not emit a JavaScript chunk.");
323
+ assertType(StringType, chunk.code);
324
+ return {
325
+ code: chunk.code,
326
+ version: vite.version,
327
+ };
328
+ },
329
+ };
330
+ const testBundlers = [
331
+ testWebpackBundler,
332
+ testViteBundler,
333
+ ];
334
+ const testBundlerWorkerType = "evolu.test-bundler";
335
+ const isTestBundlerWorkerData = (value) => typeof value === "object" &&
336
+ value !== null &&
337
+ "type" in value &&
338
+ value.type === testBundlerWorkerType;
339
+ const runTestBundlerWorker = async ({ bundlerName, options, }) => {
340
+ try {
341
+ const bundler = testBundlers.find(({ name }) => name === bundlerName);
342
+ assert(bundler, `Unknown test bundler "${bundlerName}".`);
343
+ const value = await bundler.bundle(options);
344
+ parentPort?.postMessage({
345
+ ok: true,
346
+ value,
347
+ });
348
+ }
349
+ catch (error) {
350
+ const normalized = error instanceof Error ? error : new Error(String(error));
351
+ parentPort?.postMessage({
352
+ ok: false,
353
+ error: {
354
+ name: normalized.name,
355
+ message: normalized.message,
356
+ stack: normalized.stack ?? `${normalized.name}: ${normalized.message}`,
357
+ },
358
+ });
359
+ }
360
+ };
361
+ if (!isMainThread && isTestBundlerWorkerData(workerData)) {
362
+ void runTestBundlerWorker(workerData);
363
+ }
364
+ const runTestBundler = (bundlerName, options) => async (run) => tryAsync(async () => {
365
+ const env_3 = { stack: [], error: void 0, hasError: false };
366
+ try {
367
+ const worker = __addDisposableResource(env_3, new Worker(new URL(import.meta.url), {
368
+ execArgv: process.execArgv.filter((argument, index, execArgv) => argument !== "--input-type" &&
369
+ argument !== "--eval" &&
370
+ argument !== "-e" &&
371
+ argument !== "--print" &&
372
+ argument !== "-p" &&
373
+ !argument.startsWith("--input-type=") &&
374
+ !argument.startsWith("--eval=") &&
375
+ !argument.startsWith("--print=") &&
376
+ !["--input-type", "--eval", "-e", "--print", "-p"].includes(execArgv[index - 1] ?? "")),
377
+ workerData: {
378
+ type: testBundlerWorkerType,
379
+ bundlerName,
380
+ options,
381
+ },
382
+ }), true);
383
+ const _ = __addDisposableResource(env_3, run.onAbort(() => {
384
+ void worker.terminate();
385
+ }), false);
386
+ return await new Promise((resolve, reject) => {
387
+ worker.once("message", (message) => {
388
+ if (message.ok) {
389
+ resolve(message.value);
390
+ return;
391
+ }
392
+ reject(testBundleWorkerErrorToError(message.error));
393
+ });
394
+ worker.once("error", (error) => {
395
+ assertType(ErrorType, error);
396
+ reject(error);
397
+ });
398
+ worker.once("exit", (code) => {
399
+ reject(new Error(`Test bundler worker exited with code ${code} before returning a value.`));
400
+ });
401
+ });
402
+ }
403
+ catch (e_3) {
404
+ env_3.error = e_3;
405
+ env_3.hasError = true;
406
+ }
407
+ finally {
408
+ const result_3 = __disposeResources(env_3);
409
+ if (result_3)
410
+ await result_3;
411
+ }
412
+ }, (error) => {
413
+ run.signal.throwIfAborted();
414
+ assertType(ErrorType, error);
415
+ return error;
416
+ });
417
+ const ErrorType = /*#__PURE__*/ instanceOf(globalThis.Error);
418
+ // TODO: Replace the evaluated CommonJS worker with an ESM module worker.
419
+ const testBundleWorkerSource = String.raw `
420
+ const { parentPort, workerData } = require("node:worker_threads");
421
+
422
+ let completed = false;
423
+
424
+ const fail = (error) => {
425
+ if (completed) return;
426
+ completed = true;
427
+ const normalized = error instanceof Error ? error : new Error(String(error));
428
+ parentPort.postMessage({
429
+ ok: false,
430
+ error: {
431
+ name: normalized.name,
432
+ message: normalized.message,
433
+ stack: normalized.stack ?? normalized.name + ": " + normalized.message,
434
+ },
435
+ });
436
+ };
437
+
438
+ process.on("unhandledRejection", fail);
439
+
440
+ (async () => {
441
+ const module = await import(workerData);
442
+ if (!("default" in module)) {
443
+ throw new Error("A bundle test fixture must have a default export.");
444
+ }
445
+
446
+ const exported = module.default;
447
+ const value = await (typeof exported === "function" ? exported() : exported);
448
+
449
+ // Give unhandledRejection one event-loop turn to report detached rejections.
450
+ await new Promise((resolve) => setImmediate(resolve));
451
+ if (completed) return;
452
+ // Keep completed false until cloning succeeds so DataCloneError reaches fail.
453
+ parentPort.postMessage({ ok: true, value });
454
+ completed = true;
455
+ })().catch(fail);
456
+ `;
457
+ const testBundleWorkerErrorToError = (workerError) => {
458
+ const error = new Error(workerError.message);
459
+ error.name = workerError.name;
460
+ error.stack = workerError.stack;
461
+ return error;
462
+ };
463
+ const runTestBundle = (bundlePath) => async (run) => tryAsync(async () => {
464
+ const env_4 = { stack: [], error: void 0, hasError: false };
465
+ try {
466
+ const worker = __addDisposableResource(env_4, new Worker(testBundleWorkerSource, {
467
+ eval: true,
468
+ workerData: pathToFileURL(bundlePath).href,
469
+ }), true);
470
+ const _ = __addDisposableResource(env_4, run.onAbort(() => {
471
+ void worker.terminate();
472
+ }), false);
473
+ return await new Promise((resolve, reject) => {
474
+ worker.once("message", (workerMessage) => {
475
+ if (workerMessage.ok) {
476
+ resolve(workerMessage.value);
477
+ return;
478
+ }
479
+ reject(testBundleWorkerErrorToError(workerMessage.error));
480
+ });
481
+ worker.once("error", (error) => {
482
+ assertType(ErrorType, error);
483
+ reject(error);
484
+ });
485
+ worker.once("exit", (code) => {
486
+ reject(new Error(`Bundle worker exited with code ${code} before returning a value.`));
487
+ });
488
+ });
489
+ }
490
+ catch (e_4) {
491
+ env_4.error = e_4;
492
+ env_4.hasError = true;
493
+ }
494
+ finally {
495
+ const result_4 = __disposeResources(env_4);
496
+ if (result_4)
497
+ await result_4;
498
+ }
499
+ }, (error) => {
500
+ run.signal.throwIfAborted();
501
+ assertType(ErrorType, error);
502
+ return error;
503
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Node.js-specific time utilities.
3
+ *
4
+ * @module
5
+ */
6
+ import { type Brand, Millis, type Time } from "@evolu/common";
7
+ /** {@link Time} with Node.js high-resolution nanosecond readings. */
8
+ export interface NodejsTime extends Time {
9
+ /** Returns a monotonic high-resolution timestamp in nanoseconds. */
10
+ readonly hrtime: () => HrTime;
11
+ }
12
+ /** Monotonic high-resolution timestamp in nanoseconds. */
13
+ export type HrTime = bigint & Brand<"HrTime">;
14
+ /** Elapsed nanoseconds measured using {@link HrTime}. */
15
+ export type HrDuration = bigint & Brand<"HrDuration">;
16
+ /** Creates a {@link NodejsTime} using `process.hrtime.bigint()`. */
17
+ export declare const createNodejsTime: () => NodejsTime;
18
+ /**
19
+ * Returns the elapsed nanoseconds between two high-resolution timestamps.
20
+ *
21
+ * Throws if `end` precedes `start`.
22
+ */
23
+ export declare const hrDurationBetween: (start: HrTime, end: HrTime) => HrDuration;
24
+ /** Converts a high-resolution duration to the nearest millisecond. */
25
+ export declare const hrDurationToMillis: (duration: HrDuration) => Millis;
26
+ /** Converts milliseconds to an exact high-resolution duration. */
27
+ export declare const millisToHrDuration: (millis: Millis) => HrDuration;
28
+ //# sourceMappingURL=Time.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Time.d.ts","sourceRoot":"","sources":["../../src/Time.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAGL,KAAK,KAAK,EACV,MAAM,EACN,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAEvB,qEAAqE;AACrE,MAAM,WAAW,UAAW,SAAQ,IAAI;IACtC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;CAC/B;AAED,0DAA0D;AAC1D,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC;AAE9C,yDAAyD;AACzD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC;AAEtD,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,UAGlC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,UAAW,MAAM,OAAO,MAAM,KAAG,UAG9D,CAAC;AAEF,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,aAAc,UAAU,KAAG,MAKvD,CAAC;AAEJ,kEAAkE;AAClE,eAAO,MAAM,kBAAkB,WAAY,MAAM,KAAG,UACQ,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Node.js-specific time utilities.
3
+ *
4
+ * @module
5
+ */
6
+ import { assert, createTime, Millis, } from "@evolu/common";
7
+ /** Creates a {@link NodejsTime} using `process.hrtime.bigint()`. */
8
+ export const createNodejsTime = () => ({
9
+ ...createTime(),
10
+ hrtime: () => process.hrtime.bigint(),
11
+ });
12
+ /**
13
+ * Returns the elapsed nanoseconds between two high-resolution timestamps.
14
+ *
15
+ * Throws if `end` precedes `start`.
16
+ */
17
+ export const hrDurationBetween = (start, end) => {
18
+ assert(end >= start, "High-resolution end time must not precede start time");
19
+ return (end - start);
20
+ };
21
+ /** Converts a high-resolution duration to the nearest millisecond. */
22
+ export const hrDurationToMillis = (duration) => Millis.orThrow(Number((duration + nanosecondsPerMillisecond / 2n) / nanosecondsPerMillisecond));
23
+ /** Converts milliseconds to an exact high-resolution duration. */
24
+ export const millisToHrDuration = (millis) => (BigInt(millis) * nanosecondsPerMillisecond);
25
+ const nanosecondsPerMillisecond = 1000000n;
@@ -1 +1 @@
1
- {"version":3,"file":"WebSocket.d.ts","sourceRoot":"","sources":["../../src/WebSocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,MAAM,CAAC;AAEzE;;;GAGG;AACH,MAAM,WAAW,gCAAiC,SAAQ,eAAe;IACvE,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;CAC7B;AAED;;;GAGG;AACH,eAAO,MAAM,gCAAgC,GAC3C,MAAM,MAAM,EACZ,MAAM,MAAM,KACX,gCAWF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,+BAA+B,GAC1C,MAAM,MAAM,EACZ,MAAM,MAAM,KACX,OAAO,CAAC,eAAe,CAezB,CAAC"}
1
+ {"version":3,"file":"WebSocket.d.ts","sourceRoot":"","sources":["../../src/WebSocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,MAAM,CAAC;AAEzE;;;GAGG;AACH,MAAM,WAAW,gCAAiC,SAAQ,eAAe;IACvE,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;CAC7B;AAED;;;GAGG;AACH,eAAO,MAAM,gCAAgC,SACrC,MAAM,QACN,MAAM,KACX,gCAWF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,+BAA+B,SACpC,MAAM,QACN,MAAM,KACX,OAAO,CAAC,eAAe,CAezB,CAAC"}