@evolu/nodejs 3.0.0-next.3 → 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.
- package/dist/src/Cli.d.ts +29 -0
- package/dist/src/Cli.d.ts.map +1 -0
- package/dist/src/Cli.js +49 -0
- package/dist/src/Platform.d.ts +15 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +9 -0
- package/dist/src/Sqlite.js +2 -2
- package/dist/src/Task.d.ts +69 -24
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +148 -52
- package/dist/src/TestBundle.d.ts +113 -0
- package/dist/src/TestBundle.d.ts.map +1 -0
- package/dist/src/TestBundle.js +503 -0
- package/dist/src/Time.d.ts +28 -0
- package/dist/src/Time.d.ts.map +1 -0
- package/dist/src/Time.js +25 -0
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +7 -8
- package/dist/src/index.d.ts +13 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -1
- package/dist/src/local-first/Relay.d.ts +13 -20
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +36 -43
- package/package.json +30 -6
- package/src/Cli.ts +78 -0
- package/src/Platform.ts +20 -0
- package/src/Sqlite.ts +2 -2
- package/src/Task.ts +172 -63
- package/src/TestBundle.ts +674 -0
- package/src/Time.ts +55 -0
- package/src/Worker.ts +22 -20
- package/src/index.ts +14 -6
- package/src/local-first/Relay.ts +48 -53
|
@@ -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"}
|
package/dist/src/Time.js
ADDED
|
@@ -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,
|
|
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"}
|
package/dist/src/Worker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG9E,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,EAAE,
|
|
1
|
+
{"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG9E,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,EAAE,sBAsCpC,CAAC"}
|
package/dist/src/Worker.js
CHANGED
|
@@ -50,29 +50,29 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
-
import {
|
|
53
|
+
import { disposable } from "@evolu/common";
|
|
54
54
|
/** Creates a {@link BroadcastChannel} from a Node.js BroadcastChannel. */
|
|
55
55
|
export const createBroadcastChannel = (name) => {
|
|
56
56
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
57
57
|
try {
|
|
58
58
|
const nativeBroadcastChannel = new globalThis.BroadcastChannel(name);
|
|
59
59
|
const disposer = __addDisposableResource(env_1, new DisposableStack(), false);
|
|
60
|
+
let disposed = false;
|
|
60
61
|
disposer.defer(() => {
|
|
62
|
+
disposed = true;
|
|
61
63
|
nativeBroadcastChannel.onmessage = null;
|
|
62
64
|
nativeBroadcastChannel.close();
|
|
63
65
|
});
|
|
64
|
-
const disposables = disposer.move();
|
|
65
66
|
let onMessageHandler = null;
|
|
66
|
-
return {
|
|
67
|
+
return disposable({
|
|
67
68
|
postMessage: (message) => {
|
|
68
|
-
assertNotDisposed(disposables);
|
|
69
69
|
nativeBroadcastChannel.postMessage(message);
|
|
70
70
|
},
|
|
71
71
|
get onMessage() {
|
|
72
|
-
return
|
|
72
|
+
return disposed ? null : onMessageHandler;
|
|
73
73
|
},
|
|
74
74
|
set onMessage(fn) {
|
|
75
|
-
if (
|
|
75
|
+
if (disposed)
|
|
76
76
|
return;
|
|
77
77
|
onMessageHandler = fn;
|
|
78
78
|
nativeBroadcastChannel.onmessage = fn
|
|
@@ -81,8 +81,7 @@ export const createBroadcastChannel = (name) => {
|
|
|
81
81
|
}
|
|
82
82
|
: null;
|
|
83
83
|
},
|
|
84
|
-
|
|
85
|
-
};
|
|
84
|
+
}, disposer);
|
|
86
85
|
}
|
|
87
86
|
catch (e_1) {
|
|
88
87
|
env_1.error = e_1;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export * from "./
|
|
6
|
-
export * from "./
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* @mergeModuleWith <project>
|
|
4
|
+
*/
|
|
5
|
+
export * from "./Cli.ts";
|
|
6
|
+
export * from "./Crypto.ts";
|
|
7
|
+
export * from "./local-first/Relay.ts";
|
|
8
|
+
export * from "./Platform.ts";
|
|
9
|
+
export * from "./Sqlite.ts";
|
|
10
|
+
export * from "./Task.ts";
|
|
11
|
+
export * from "./Time.ts";
|
|
12
|
+
export * from "./WebSocket.ts";
|
|
13
|
+
export * from "./Worker.ts";
|
|
7
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
|
package/dist/src/index.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* @mergeModuleWith <project>
|
|
4
|
+
*/
|
|
5
|
+
export * from "./Cli.js";
|
|
2
6
|
export * from "./Crypto.js";
|
|
3
7
|
export * from "./local-first/Relay.js";
|
|
8
|
+
export * from "./Platform.js";
|
|
9
|
+
export * from "./Sqlite.js";
|
|
4
10
|
export * from "./Task.js";
|
|
11
|
+
export * from "./Time.js";
|
|
5
12
|
export * from "./WebSocket.js";
|
|
6
13
|
export * from "./Worker.js";
|