flatsql 2.0.3 → 3.1.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.
Files changed (84) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +54 -64
  3. package/dist/artifacts/index.d.ts +1 -3
  4. package/dist/artifacts/index.d.ts.map +1 -1
  5. package/dist/artifacts/index.js +1 -3
  6. package/dist/artifacts/index.js.map +1 -1
  7. package/dist/response/artifact.d.ts +1 -2
  8. package/dist/response/artifact.d.ts.map +1 -1
  9. package/dist/response/artifact.js.map +1 -1
  10. package/dist/response/types.d.ts +6 -0
  11. package/dist/response/types.d.ts.map +1 -1
  12. package/dist/stress/index.d.ts +0 -1
  13. package/dist/stress/index.d.ts.map +1 -1
  14. package/dist/stress/index.js +0 -1
  15. package/dist/stress/index.js.map +1 -1
  16. package/dist/stress/native-publish-event-workload.d.ts.map +1 -1
  17. package/dist/stress/native-publish-event-workload.js +14 -4
  18. package/dist/stress/native-publish-event-workload.js.map +1 -1
  19. package/dist/stress/smoke-runner.d.ts.map +1 -1
  20. package/dist/stress/smoke-runner.js +6 -5
  21. package/dist/stress/smoke-runner.js.map +1 -1
  22. package/docs/PARTITION-STORE-WASM.md +262 -0
  23. package/docs/PARTITION-STORE.md +980 -0
  24. package/package.json +61 -22
  25. package/wasm/flatsql-io.js +4 -1
  26. package/wasm/flatsql-ps-threads.wasm +0 -0
  27. package/wasm/flatsql-wasi-noeh.wasm +0 -0
  28. package/wasm/flatsql-wasi.wasm +0 -0
  29. package/wasm/flatsql.js +1 -1
  30. package/wasm/flatsql.wasm +0 -0
  31. package/wasm/index.d.ts +2 -0
  32. package/wasm/index.js +16 -1
  33. package/wasm/integrity.json +28 -0
  34. package/wasm/ps-node-fault.d.mts +23 -0
  35. package/wasm/ps-node-fault.mjs +381 -0
  36. package/wasm/ps-node-guest.mjs +156 -0
  37. package/wasm/ps-node-host.d.mts +86 -0
  38. package/wasm/ps-node-host.mjs +431 -0
  39. package/wasm/ps-node-imports.mjs +847 -0
  40. package/wasm/ps.d.ts +21 -0
  41. package/wasm/ps.js +75 -0
  42. package/dist/artifacts/builder.d.ts +0 -31
  43. package/dist/artifacts/builder.d.ts.map +0 -1
  44. package/dist/artifacts/builder.js +0 -514
  45. package/dist/artifacts/builder.js.map +0 -1
  46. package/dist/artifacts/demo-extractors.d.ts +0 -15
  47. package/dist/artifacts/demo-extractors.d.ts.map +0 -1
  48. package/dist/artifacts/demo-extractors.js +0 -238
  49. package/dist/artifacts/demo-extractors.js.map +0 -1
  50. package/dist/artifacts/standalone-builder.d.ts +0 -62
  51. package/dist/artifacts/standalone-builder.d.ts.map +0 -1
  52. package/dist/artifacts/standalone-builder.js +0 -144
  53. package/dist/artifacts/standalone-builder.js.map +0 -1
  54. package/dist/artifacts/worker-client.d.ts +0 -32
  55. package/dist/artifacts/worker-client.d.ts.map +0 -1
  56. package/dist/artifacts/worker-client.js +0 -415
  57. package/dist/artifacts/worker-client.js.map +0 -1
  58. package/dist/core/database.d.ts +0 -86
  59. package/dist/core/database.d.ts.map +0 -1
  60. package/dist/core/database.js +0 -900
  61. package/dist/core/database.js.map +0 -1
  62. package/dist/core/index.d.ts +0 -3
  63. package/dist/core/index.d.ts.map +0 -1
  64. package/dist/core/index.js +0 -3
  65. package/dist/core/index.js.map +0 -1
  66. package/dist/core/table-store.d.ts +0 -30
  67. package/dist/core/table-store.d.ts.map +0 -1
  68. package/dist/core/table-store.js +0 -187
  69. package/dist/core/table-store.js.map +0 -1
  70. package/dist/flatbuffer-accessor.d.ts +0 -35
  71. package/dist/flatbuffer-accessor.d.ts.map +0 -1
  72. package/dist/flatbuffer-accessor.js +0 -114
  73. package/dist/flatbuffer-accessor.js.map +0 -1
  74. package/dist/index.d.ts +0 -15
  75. package/dist/index.d.ts.map +0 -1
  76. package/dist/index.js +0 -22
  77. package/dist/index.js.map +0 -1
  78. package/dist/stress/publish-event-workload.d.ts +0 -3
  79. package/dist/stress/publish-event-workload.d.ts.map +0 -1
  80. package/dist/stress/publish-event-workload.js +0 -467
  81. package/dist/stress/publish-event-workload.js.map +0 -1
  82. package/wasm/flatsql-artifact.worker.js +0 -847
  83. package/wasm/standalone.d.ts +0 -122
  84. package/wasm/standalone.js +0 -914
@@ -0,0 +1,431 @@
1
+ // FlatSQL partition store: the Node wasi-threads host (design §5.6, T4;
2
+ // docs/PARTITION-STORE-WASM.md).
3
+ //
4
+ // Runs flatsql-ps-threads.wasm (a reactor) and the wasm test commands on
5
+ // Node 24+ with real threads:
6
+ // - the guest's own thread is a worker (ps-node-guest.mjs); this thread
7
+ // supervises and is never blocked by the guest;
8
+ // - wasi.thread-spawn claims a pre-started worker from a pool this thread
9
+ // keeps (grown on demand up to a cap); workers are reused, so a guest that
10
+ // starts and stops engines thousands of times never exhausts it, and any
11
+ // guest thread may spawn;
12
+ // - env.flatsql_io_* is space-data-module-sdk's Node sync-fs provider over a
13
+ // shared virtual-handle table; the fault overlay (ps-node-fault.mjs) is
14
+ // interposed on every syscall when `fault` is given;
15
+ // - WASI preview1 is this host's own (ps-node-imports.mjs): shared
16
+ // descriptors, process-wide clocks, preopens.
17
+ //
18
+ // Requires the optional peer dependency space-data-module-sdk >= 0.8.24.
19
+ //
20
+ // node wasm/ps-node-host.mjs <command.wasm> [args...]
21
+ // runs a wasm command in a private root (FLATSQL_PS_ROOT to choose it,
22
+ // FLATSQL_PS_KEEP_ROOT=1 to keep it), with the checkout mounted at its own
23
+ // path and the FLATSQL_PS_* / PS_* environment; FLATSQL_PS_TIMEOUT_MS
24
+ // bounds the run. Prints the thread report to stderr (and writes it as
25
+ // JSON to FLATSQL_PS_REPORT when set).
26
+
27
+ import fs from "node:fs";
28
+ import os from "node:os";
29
+ import path from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+ import { Worker } from "node:worker_threads";
32
+
33
+ import { createNodeSyncFsIoTable } from "space-data-module-sdk/host/flatsql-io/node";
34
+
35
+ import {
36
+ CONTROL_EXITED,
37
+ CONTROL_EXIT_CODE,
38
+ CONTROL_FAULT,
39
+ CONTROL_FAULT_TID,
40
+ CONTROL_HEADER,
41
+ CONTROL_SLOT,
42
+ CONTROL_STARTED,
43
+ CONTROL_TIDS,
44
+ POOL_CREATED,
45
+ POOL_DECLINED,
46
+ POOL_MAX_RUNNING,
47
+ POOL_RUNNING,
48
+ POOL_SPAWNED,
49
+ POOL_WANT,
50
+ createControlBlock,
51
+ createThreadPool,
52
+ createWasiFdTable,
53
+ } from "./ps-node-imports.mjs";
54
+
55
+ const GUEST_URL = new URL("./ps-node-guest.mjs", import.meta.url);
56
+
57
+ /** The shared memory limits a module imports (env.memory), in pages. */
58
+ export function importedMemoryLimits(bytes) {
59
+ const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
60
+ let at = 8;
61
+ const leb = () => {
62
+ let result = 0;
63
+ let shift = 0;
64
+ for (;;) {
65
+ const b = u8[at++];
66
+ result += (b & 0x7f) * 2 ** shift;
67
+ shift += 7;
68
+ if (!(b & 0x80)) return result;
69
+ }
70
+ };
71
+ const name = () => {
72
+ const n = leb();
73
+ const s = Buffer.from(u8.subarray(at, at + n)).toString("utf8");
74
+ at += n;
75
+ return s;
76
+ };
77
+ while (at < u8.length) {
78
+ const id = u8[at++];
79
+ const size = leb();
80
+ const end = at + size;
81
+ if (id === 2) {
82
+ const count = leb();
83
+ for (let i = 0; i < count; i += 1) {
84
+ const mod = name();
85
+ const field = name();
86
+ const kind = u8[at++];
87
+ if (kind === 0) leb();
88
+ else if (kind === 1) {
89
+ at += 1;
90
+ const flags = leb();
91
+ leb();
92
+ if (flags & 1) leb();
93
+ } else if (kind === 2) {
94
+ const flags = leb();
95
+ const initial = leb();
96
+ const maximum = flags & 1 ? leb() : undefined;
97
+ return { module: mod, name: field, initial, maximum, shared: (flags & 2) !== 0 };
98
+ } else if (kind === 3) {
99
+ at += 2;
100
+ } else {
101
+ throw new Error(`unknown import kind ${kind}`);
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+ at = end;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ // The guest's file system: `root` (a host directory) is the guest's "/" for
112
+ // WASI and the flatsql_io root; `mounts` preopen further host directories at
113
+ // guest paths. With no root a private temporary directory is created and
114
+ // removed afterwards.
115
+ function prepareRoot(options) {
116
+ if (options.root) {
117
+ fs.mkdirSync(options.root, { recursive: true });
118
+ return { root: path.resolve(options.root), cleanup: () => {} };
119
+ }
120
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "flatsql-ps-node-"));
121
+ return {
122
+ root,
123
+ cleanup: () => {
124
+ if (!options.keepRoot) fs.rmSync(root, { recursive: true, force: true });
125
+ },
126
+ };
127
+ }
128
+
129
+ function hostConfig(options, root) {
130
+ fs.mkdirSync(path.join(root, "tmp"), { recursive: true });
131
+ const preopens = [{ guest: "/", host: root }, ...(options.mounts ?? [])];
132
+ let deterministic = null;
133
+ if (options.deterministic) {
134
+ const randomState = new SharedArrayBuffer(8);
135
+ new BigUint64Array(randomState)[0] = BigInt(options.deterministic.randomSeed ?? 0x9e3779b97f4a7c15n) | 1n;
136
+ deterministic = { randomSeed: String(options.deterministic.randomSeed ?? 0), randomState };
137
+ }
138
+ return {
139
+ args: options.args ?? [],
140
+ env: { TMPDIR: "/tmp", ...(options.env ?? {}) },
141
+ fdTable: createWasiFdTable({ preopens }),
142
+ control: createControlBlock(),
143
+ pool: createThreadPool(options.maxThreads ?? 512),
144
+ spawnWaitMs: options.spawnWaitMs ?? 60000,
145
+ ioRoot: root,
146
+ ioTable: createNodeSyncFsIoTable({ maxHandles: options.maxHandles ?? 16384 }),
147
+ instanceId: options.instanceId ?? 0,
148
+ fault: options.fault?.buffer ?? null,
149
+ stdoutFd: options.stdoutFd,
150
+ stderrFd: options.stderrFd,
151
+ deterministic,
152
+ };
153
+ }
154
+
155
+ async function loadModule(source) {
156
+ if (source instanceof WebAssembly.Module) return { module: source, bytes: null };
157
+ const bytes = typeof source === "string" ? fs.readFileSync(source) : new Uint8Array(source);
158
+ return { module: await WebAssembly.compile(bytes), bytes };
159
+ }
160
+
161
+ function createMemory(bytes, options) {
162
+ const limits = importedMemoryLimits(bytes);
163
+ if (!limits || limits.module !== "env" || limits.name !== "memory" || !limits.shared) {
164
+ throw new Error("the module must import a shared env.memory");
165
+ }
166
+ const initial = Math.max(limits.initial, options.initialPages ?? 0);
167
+ const maximum = Math.min(limits.maximum ?? 65536, options.maximumPages ?? 65536);
168
+ return new WebAssembly.Memory({ initial, maximum, shared: true });
169
+ }
170
+
171
+ /**
172
+ * One guest process: its memory, its file system, its thread pool, and the
173
+ * workers that run it. The supervisor starts pool workers when a spawn asks
174
+ * for one and watches for a guest fault or exit.
175
+ */
176
+ class Supervisor {
177
+ constructor(module, memory, cfg, options) {
178
+ this.module = module;
179
+ this.memory = memory;
180
+ this.cfg = cfg;
181
+ this.control = new Int32Array(cfg.control);
182
+ this.pool = new Int32Array(cfg.pool);
183
+ this.cap = this.pool[0];
184
+ this.workers = new Set();
185
+ this.listeners = new Set();
186
+ this.stopped = false;
187
+ this.poolErrors = [];
188
+ const warm = Math.min(options.warmThreads ?? 16, this.cap);
189
+ for (let i = 0; i < warm; i += 1) this.startPoolWorker();
190
+ this.timer = setInterval(() => this.tick(), 2);
191
+ }
192
+
193
+ startPoolWorker() {
194
+ const slot = Atomics.add(this.pool, POOL_CREATED, 1);
195
+ if (slot >= this.cap) {
196
+ Atomics.sub(this.pool, POOL_CREATED, 1);
197
+ return;
198
+ }
199
+ const worker = new Worker(GUEST_URL, {
200
+ workerData: { mode: "pool", slot, wasmModule: this.module, memory: this.memory, cfg: this.cfg },
201
+ });
202
+ worker.on("error", (error) => this.poolErrors.push(String(error?.stack ?? error)));
203
+ worker.once("exit", () => this.workers.delete(worker));
204
+ this.workers.add(worker);
205
+ }
206
+
207
+ tick() {
208
+ if (this.stopped) return;
209
+ const want = Atomics.exchange(this.pool, POOL_WANT, 0);
210
+ for (let i = 0; i < want; i += 1) this.startPoolWorker();
211
+ for (const listener of this.listeners) listener();
212
+ }
213
+
214
+ startGuest(mode) {
215
+ const worker = new Worker(GUEST_URL, {
216
+ workerData: { mode, wasmModule: this.module, memory: this.memory, cfg: this.cfg },
217
+ });
218
+ this.workers.add(worker);
219
+ worker.once("exit", () => this.workers.delete(worker));
220
+ return worker;
221
+ }
222
+
223
+ exited() {
224
+ return Atomics.load(this.control, CONTROL_EXITED) !== 0;
225
+ }
226
+
227
+ faulted() {
228
+ return Atomics.load(this.control, CONTROL_FAULT) !== 0;
229
+ }
230
+
231
+ report() {
232
+ const c = this.control;
233
+ const slots = Math.min(Atomics.load(c, CONTROL_SLOT), CONTROL_TIDS);
234
+ const tids = new Set();
235
+ for (let i = 0; i < slots; i += 1) tids.add(Atomics.load(c, CONTROL_HEADER + i));
236
+ return {
237
+ guestThreadsSpawned: Atomics.load(this.pool, POOL_SPAWNED),
238
+ // Pool threads running at once, plus the guest's own thread.
239
+ maxConcurrentGuestThreads: Atomics.load(this.pool, POOL_MAX_RUNNING) + 1,
240
+ runningGuestThreads: Atomics.load(this.pool, POOL_RUNNING),
241
+ spawnsDeclined: Atomics.load(this.pool, POOL_DECLINED),
242
+ workerThreads: Atomics.load(c, CONTROL_STARTED),
243
+ distinctWorkerThreadIds: tids.size,
244
+ memoryBytes: this.memory.buffer.byteLength,
245
+ };
246
+ }
247
+
248
+ async stop() {
249
+ this.stopped = true;
250
+ clearInterval(this.timer);
251
+ await Promise.all([...this.workers].map((w) => w.terminate().catch(() => {})));
252
+ this.workers.clear();
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Run a wasi-threads command (the ps test commands) to completion.
258
+ *
259
+ * @param {string|Uint8Array} wasm path or bytes
260
+ * @param {object} [options]
261
+ * @param {string[]} [options.args] argv after argv[0]
262
+ * @param {Record<string,string>} [options.env] (TMPDIR defaults to "/tmp")
263
+ * @param {string} [options.root] host directory the guest sees as "/" (WASI
264
+ * and flatsql_io); default a private temporary directory, removed after
265
+ * @param {boolean} [options.keepRoot] keep that temporary directory
266
+ * @param {Array<{guest:string,host:string}>} [options.mounts] more preopens
267
+ * @param {number} [options.maxThreads] pool cap (default 512)
268
+ * @param {number} [options.warmThreads] workers started up front (default 16)
269
+ * @param {number} [options.timeoutMs] stop the guest after this long
270
+ * @param {number} [options.killAfterMs] kill the guest after this long (a
271
+ * crash, not a failure: the result has killed: true)
272
+ * @param {() => void} [options.beforeKill] runs just before a kill (the fault
273
+ * overlay freezes here)
274
+ * @param {object} [options.fault] a fault overlay (createFaultOverlay)
275
+ * @param {number} [options.stdoutFd] guest stdout goes to this fd (default 1)
276
+ * @param {{ randomSeed?: bigint }} [options.deterministic] seeded random_get
277
+ * @returns {Promise<{ exitCode: number, killed: boolean, timedOut: boolean,
278
+ * fault: boolean, error: string|null, elapsedMs: number, threads: object }>}
279
+ */
280
+ export async function runPsCommand(wasm, options = {}) {
281
+ const { module, bytes } = await loadModule(wasm);
282
+ const memory = createMemory(bytes ?? options.bytes, options);
283
+ const argv0 = typeof wasm === "string" ? path.basename(wasm) : "command.wasm";
284
+ const fsRoot = prepareRoot(options);
285
+ const cfg = hostConfig({ ...options, args: [argv0, ...(options.args ?? [])] }, fsRoot.root);
286
+ const started = performance.now();
287
+ const sup = new Supervisor(module, memory, cfg, options);
288
+ const guest = sup.startGuest("command");
289
+ const result = await new Promise((resolve) => {
290
+ let settled = false;
291
+ const timers = [];
292
+ const settle = (value) => {
293
+ if (settled) return;
294
+ settled = true;
295
+ for (const t of timers) clearTimeout(t);
296
+ sup.listeners.clear();
297
+ resolve(value);
298
+ };
299
+ const base = { exitCode: 0, killed: false, timedOut: false, fault: false, error: null };
300
+ guest.on("message", (msg) => {
301
+ if (msg.t === "done") settle({ ...base, exitCode: msg.code, error: msg.error, fault: msg.error !== null });
302
+ });
303
+ guest.on("error", (error) => settle({ ...base, exitCode: 134, error: String(error?.stack ?? error), fault: true }));
304
+ guest.on("exit", (code) => settle({ ...base, exitCode: code || 134, error: `guest worker exited (${code})`, fault: true }));
305
+ // A pool thread that exits the process or traps: the guest's own thread
306
+ // may be blocked in a join forever, so decide from here.
307
+ sup.listeners.add(() => {
308
+ if (sup.faulted()) {
309
+ settle({
310
+ ...base,
311
+ exitCode: 134,
312
+ error: `guest thread ${Atomics.load(sup.control, CONTROL_FAULT_TID)} trapped`,
313
+ fault: true,
314
+ });
315
+ } else if (sup.exited()) {
316
+ timers.push(setTimeout(() => settle({ ...base, exitCode: Atomics.load(sup.control, CONTROL_EXIT_CODE) }), 100));
317
+ }
318
+ });
319
+ if (options.timeoutMs) {
320
+ timers.push(setTimeout(() => settle({ ...base, exitCode: 124, timedOut: true, error: `timed out after ${options.timeoutMs} ms` }), options.timeoutMs));
321
+ }
322
+ if (options.killAfterMs !== undefined) {
323
+ timers.push(setTimeout(() => {
324
+ options.beforeKill?.();
325
+ settle({ ...base, exitCode: 137, killed: true });
326
+ }, options.killAfterMs));
327
+ }
328
+ });
329
+ const threads = sup.report();
330
+ await sup.stop();
331
+ fsRoot.cleanup();
332
+ return { ...result, elapsedMs: performance.now() - started, threads, root: fsRoot.root, poolErrors: sup.poolErrors };
333
+ }
334
+
335
+ /**
336
+ * Instantiate flatsql-ps-threads.wasm as one instance (writer or reader) with
337
+ * an exec thread of its own. Returns an RPC handle for the C ABI:
338
+ * `call(fn, ...args)`, `write(bytes) -> ptr`, `read(ptr, len)`, `threads()`,
339
+ * `close()`.
340
+ */
341
+ export async function createPsNodeInstance(wasm, options = {}) {
342
+ const { module, bytes } = await loadModule(wasm);
343
+ const memory = createMemory(bytes ?? options.bytes, options);
344
+ const fsRoot = prepareRoot(options);
345
+ const cfg = hostConfig(options, fsRoot.root);
346
+ const sup = new Supervisor(module, memory, cfg, options);
347
+ const worker = sup.startGuest("reactor");
348
+ const pending = new Map();
349
+ let nextId = 1;
350
+ let failure = null;
351
+ const fail = (error) => {
352
+ failure = error;
353
+ for (const p of pending.values()) p.reject(error);
354
+ pending.clear();
355
+ };
356
+ sup.listeners.add(() => {
357
+ if (!failure && sup.faulted()) fail(new Error(`guest thread ${Atomics.load(sup.control, CONTROL_FAULT_TID)} trapped`));
358
+ });
359
+ await new Promise((resolve, reject) => {
360
+ worker.on("message", (msg) => {
361
+ if (msg.t === "ready") resolve();
362
+ else if (msg.t === "reply") {
363
+ const p = pending.get(msg.id);
364
+ pending.delete(msg.id);
365
+ if (!p) return;
366
+ if (msg.error) p.reject(new Error(msg.error));
367
+ else p.resolve(msg.value);
368
+ }
369
+ });
370
+ worker.on("error", (error) => {
371
+ fail(error);
372
+ reject(error);
373
+ });
374
+ });
375
+ const rpc = (msg) => {
376
+ if (failure) return Promise.reject(failure);
377
+ const id = nextId++;
378
+ return new Promise((resolve, reject) => {
379
+ pending.set(id, { resolve, reject });
380
+ worker.postMessage({ ...msg, id });
381
+ });
382
+ };
383
+ return {
384
+ memory,
385
+ root: fsRoot.root,
386
+ call: (fn, ...args) => rpc({ op: "call", fn, args }),
387
+ write: (data) => rpc({ op: "write", bytes: data instanceof Uint8Array ? data : new Uint8Array(data) }),
388
+ read: (ptr, len) => rpc({ op: "read", ptr, len }),
389
+ threads: () => sup.report(),
390
+ faulted: () => sup.faulted(),
391
+ async close() {
392
+ await sup.stop();
393
+ fsRoot.cleanup();
394
+ },
395
+ };
396
+ }
397
+
398
+ // ---- CLI ---------------------------------------------------------------------
399
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
400
+ if (invokedDirectly) {
401
+ const [wasmPath, ...args] = process.argv.slice(2);
402
+ if (!wasmPath) {
403
+ process.stderr.write("usage: node wasm/ps-node-host.mjs <command.wasm> [args...]\n");
404
+ process.exit(2);
405
+ }
406
+ const env = {};
407
+ for (const [k, v] of Object.entries(process.env)) if (k.startsWith("FLATSQL_PS_") || k.startsWith("PS_")) env[k] = v;
408
+ const timeoutMs = Number(process.env.FLATSQL_PS_TIMEOUT_MS ?? 0) || undefined;
409
+ // The checkout is visible at its own path (the test commands read their
410
+ // vectors from the build tree); everything else lives in a private root.
411
+ const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
412
+ const r = await runPsCommand(wasmPath, {
413
+ args,
414
+ env,
415
+ timeoutMs,
416
+ root: process.env.FLATSQL_PS_ROOT || undefined,
417
+ keepRoot: process.env.FLATSQL_PS_KEEP_ROOT === "1",
418
+ mounts: [{ guest: repo, host: repo }],
419
+ });
420
+ const t = r.threads;
421
+ if (process.env.FLATSQL_PS_REPORT) {
422
+ fs.writeFileSync(process.env.FLATSQL_PS_REPORT, `${JSON.stringify({ ...r, root: undefined })}\n`);
423
+ }
424
+ process.stderr.write(
425
+ `ps-node-host: exit ${r.exitCode}${r.error ? ` (${r.error})` : ""}; guest threads spawned ${t.guestThreadsSpawned}, ` +
426
+ `max concurrent ${t.maxConcurrentGuestThreads} on ${t.distinctWorkerThreadIds} distinct worker threads` +
427
+ `${t.spawnsDeclined ? `, ${t.spawnsDeclined} spawns declined` : ""}; memory ${(t.memoryBytes / 2 ** 20).toFixed(0)} MiB; ` +
428
+ `${(r.elapsedMs / 1000).toFixed(1)} s\n`,
429
+ );
430
+ process.exit(r.exitCode);
431
+ }