flatsql 3.11.1 → 3.12.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 (181) hide show
  1. package/README.md +61 -795
  2. package/bin/flatsql.mjs +76 -0
  3. package/cpp/README.md +18 -0
  4. package/cpp/include/flatsql/flatsql.h +162 -0
  5. package/cpp/include/flatsql/flatsql_io.h +159 -0
  6. package/docs/API-3.12.md +114 -0
  7. package/package.json +28 -123
  8. package/wasm/flatsql.wasm +0 -0
  9. package/wasm/index.d.ts +61 -548
  10. package/wasm/index.js +280 -1281
  11. package/wasm/integrity.json +6 -26
  12. package/wasm/runtime/LICENSE.sdk.txt +190 -0
  13. package/wasm/runtime/NOTICE.txt +2 -0
  14. package/wasm/runtime/browser-guest.mjs +1 -0
  15. package/wasm/runtime/browser-host.mjs +1 -0
  16. package/wasm/runtime/node-guest.mjs +2 -0
  17. package/wasm/runtime/node-host.mjs +5 -0
  18. package/wasm/runtime/opfsIoWorker.mjs +1 -0
  19. package/wasm/runtime/wasiThreadBrowserWorker.mjs +1 -0
  20. package/dist/artifacts/index.d.ts +0 -3
  21. package/dist/artifacts/index.d.ts.map +0 -1
  22. package/dist/artifacts/index.js +0 -3
  23. package/dist/artifacts/index.js.map +0 -1
  24. package/dist/artifacts/transport.d.ts +0 -5
  25. package/dist/artifacts/transport.d.ts.map +0 -1
  26. package/dist/artifacts/transport.js +0 -45
  27. package/dist/artifacts/transport.js.map +0 -1
  28. package/dist/artifacts/types.d.ts +0 -34
  29. package/dist/artifacts/types.d.ts.map +0 -1
  30. package/dist/artifacts/types.js +0 -2
  31. package/dist/artifacts/types.js.map +0 -1
  32. package/dist/btree/btree.d.ts +0 -34
  33. package/dist/btree/btree.d.ts.map +0 -1
  34. package/dist/btree/btree.js +0 -234
  35. package/dist/btree/btree.js.map +0 -1
  36. package/dist/btree/index.d.ts +0 -3
  37. package/dist/btree/index.d.ts.map +0 -1
  38. package/dist/btree/index.js +0 -3
  39. package/dist/btree/index.js.map +0 -1
  40. package/dist/btree/types.d.ts +0 -36
  41. package/dist/btree/types.d.ts.map +0 -1
  42. package/dist/btree/types.js +0 -55
  43. package/dist/btree/types.js.map +0 -1
  44. package/dist/cluster/index.d.ts +0 -11
  45. package/dist/cluster/index.d.ts.map +0 -1
  46. package/dist/cluster/index.js +0 -59
  47. package/dist/cluster/index.js.map +0 -1
  48. package/dist/response/artifact.d.ts +0 -4
  49. package/dist/response/artifact.d.ts.map +0 -1
  50. package/dist/response/artifact.js +0 -102
  51. package/dist/response/artifact.js.map +0 -1
  52. package/dist/response/cache-key.d.ts +0 -6
  53. package/dist/response/cache-key.d.ts.map +0 -1
  54. package/dist/response/cache-key.js +0 -114
  55. package/dist/response/cache-key.js.map +0 -1
  56. package/dist/response/index.d.ts +0 -5
  57. package/dist/response/index.d.ts.map +0 -1
  58. package/dist/response/index.js +0 -5
  59. package/dist/response/index.js.map +0 -1
  60. package/dist/response/memory-cache.d.ts +0 -17
  61. package/dist/response/memory-cache.d.ts.map +0 -1
  62. package/dist/response/memory-cache.js +0 -76
  63. package/dist/response/memory-cache.js.map +0 -1
  64. package/dist/response/types.d.ts +0 -57
  65. package/dist/response/types.d.ts.map +0 -1
  66. package/dist/response/types.js +0 -2
  67. package/dist/response/types.js.map +0 -1
  68. package/dist/schema/index.d.ts +0 -3
  69. package/dist/schema/index.d.ts.map +0 -1
  70. package/dist/schema/index.js +0 -3
  71. package/dist/schema/index.js.map +0 -1
  72. package/dist/schema/parser.d.ts +0 -25
  73. package/dist/schema/parser.d.ts.map +0 -1
  74. package/dist/schema/parser.js +0 -474
  75. package/dist/schema/parser.js.map +0 -1
  76. package/dist/schema/types.d.ts +0 -33
  77. package/dist/schema/types.d.ts.map +0 -1
  78. package/dist/schema/types.js +0 -29
  79. package/dist/schema/types.js.map +0 -1
  80. package/dist/standalone/index.d.ts +0 -3
  81. package/dist/standalone/index.d.ts.map +0 -1
  82. package/dist/standalone/index.js +0 -3
  83. package/dist/standalone/index.js.map +0 -1
  84. package/dist/standalone/process-client.d.ts +0 -85
  85. package/dist/standalone/process-client.d.ts.map +0 -1
  86. package/dist/standalone/process-client.js +0 -537
  87. package/dist/standalone/process-client.js.map +0 -1
  88. package/dist/standalone/wasmedge-runner.d.ts +0 -19
  89. package/dist/standalone/wasmedge-runner.d.ts.map +0 -1
  90. package/dist/standalone/wasmedge-runner.js +0 -108
  91. package/dist/standalone/wasmedge-runner.js.map +0 -1
  92. package/dist/storage/index.d.ts +0 -3
  93. package/dist/storage/index.d.ts.map +0 -1
  94. package/dist/storage/index.js +0 -3
  95. package/dist/storage/index.js.map +0 -1
  96. package/dist/storage/stacked-flatbuffers.d.ts +0 -38
  97. package/dist/storage/stacked-flatbuffers.d.ts.map +0 -1
  98. package/dist/storage/stacked-flatbuffers.js +0 -253
  99. package/dist/storage/stacked-flatbuffers.js.map +0 -1
  100. package/dist/storage/types.d.ts +0 -23
  101. package/dist/storage/types.d.ts.map +0 -1
  102. package/dist/storage/types.js +0 -27
  103. package/dist/storage/types.js.map +0 -1
  104. package/dist/stress/aggregate.d.ts +0 -3
  105. package/dist/stress/aggregate.d.ts.map +0 -1
  106. package/dist/stress/aggregate.js +0 -61
  107. package/dist/stress/aggregate.js.map +0 -1
  108. package/dist/stress/docker-compose.d.ts +0 -17
  109. package/dist/stress/docker-compose.d.ts.map +0 -1
  110. package/dist/stress/docker-compose.js +0 -156
  111. package/dist/stress/docker-compose.js.map +0 -1
  112. package/dist/stress/index.d.ts +0 -10
  113. package/dist/stress/index.d.ts.map +0 -1
  114. package/dist/stress/index.js +0 -10
  115. package/dist/stress/index.js.map +0 -1
  116. package/dist/stress/metrics.d.ts +0 -3
  117. package/dist/stress/metrics.d.ts.map +0 -1
  118. package/dist/stress/metrics.js +0 -83
  119. package/dist/stress/metrics.js.map +0 -1
  120. package/dist/stress/native-publish-event-workload.d.ts +0 -3
  121. package/dist/stress/native-publish-event-workload.d.ts.map +0 -1
  122. package/dist/stress/native-publish-event-workload.js +0 -465
  123. package/dist/stress/native-publish-event-workload.js.map +0 -1
  124. package/dist/stress/production-gate.d.ts +0 -4
  125. package/dist/stress/production-gate.d.ts.map +0 -1
  126. package/dist/stress/production-gate.js +0 -12
  127. package/dist/stress/production-gate.js.map +0 -1
  128. package/dist/stress/report.d.ts +0 -3
  129. package/dist/stress/report.d.ts.map +0 -1
  130. package/dist/stress/report.js +0 -174
  131. package/dist/stress/report.js.map +0 -1
  132. package/dist/stress/sds-discovery.d.ts +0 -4
  133. package/dist/stress/sds-discovery.d.ts.map +0 -1
  134. package/dist/stress/sds-discovery.js +0 -40
  135. package/dist/stress/sds-discovery.js.map +0 -1
  136. package/dist/stress/smoke-runner.d.ts +0 -3
  137. package/dist/stress/smoke-runner.d.ts.map +0 -1
  138. package/dist/stress/smoke-runner.js +0 -64
  139. package/dist/stress/smoke-runner.js.map +0 -1
  140. package/dist/stress/types.d.ts +0 -219
  141. package/dist/stress/types.d.ts.map +0 -1
  142. package/dist/stress/types.js +0 -2
  143. package/dist/stress/types.js.map +0 -1
  144. package/dist/stress/workload.d.ts +0 -6
  145. package/dist/stress/workload.d.ts.map +0 -1
  146. package/dist/stress/workload.js +0 -144
  147. package/dist/stress/workload.js.map +0 -1
  148. package/docs/PARTITION-STORE-WASM.md +0 -338
  149. package/docs/PARTITION-STORE.md +0 -2380
  150. package/docs/agents/response-artifacts.md +0 -65
  151. package/docs/response-artifacts.md +0 -86
  152. package/scripts/stress-docker.sh +0 -279
  153. package/sdm/flatsql-spatial.wasm +0 -0
  154. package/sdm/index.d.ts +0 -88
  155. package/sdm/index.js +0 -253
  156. package/sdm/manifest.json +0 -402
  157. package/stress/docker/Dockerfile +0 -11
  158. package/stress/docker/docker-compose.yml +0 -51
  159. package/stress/sds/aggregate.mjs +0 -97
  160. package/stress/sds/run.mjs +0 -105
  161. package/stress/sds/write-compose.mjs +0 -96
  162. package/wasm/flatsql-client.js +0 -280
  163. package/wasm/flatsql-io.d.ts +0 -97
  164. package/wasm/flatsql-io.js +0 -625
  165. package/wasm/flatsql-p4-threads.wasm +0 -0
  166. package/wasm/flatsql-ps-threads.wasm +0 -0
  167. package/wasm/flatsql-wasi-noeh.wasm +0 -0
  168. package/wasm/flatsql-wasi.wasm +0 -0
  169. package/wasm/flatsql.js +0 -2
  170. package/wasm/flatsql.worker.js +0 -209
  171. package/wasm/native/flatsql_wasmedge_runner.cpp +0 -858
  172. package/wasm/ps-node-fault.d.mts +0 -23
  173. package/wasm/ps-node-fault.mjs +0 -381
  174. package/wasm/ps-node-guest.mjs +0 -173
  175. package/wasm/ps-node-host.d.mts +0 -95
  176. package/wasm/ps-node-host.mjs +0 -476
  177. package/wasm/ps-node-imports.mjs +0 -847
  178. package/wasm/ps.d.ts +0 -21
  179. package/wasm/ps.js +0 -75
  180. package/wasm/wasi.d.ts +0 -11
  181. package/wasm/wasi.js +0 -51
@@ -1,476 +0,0 @@
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
- const memory = new WebAssembly.Memory({ initial, maximum, shared: true });
169
- MAXIMUM_BYTES.set(memory, maximum * 65536);
170
- return memory;
171
- }
172
-
173
- // The byte size each memory may grow to (createMemory).
174
- const MAXIMUM_BYTES = new WeakMap();
175
-
176
- // V8 bounds-checks memory.fill/copy and atomics against each thread's cached
177
- // view of a shared memory's size, and another thread's memory.grow updates
178
- // that view only at this thread's next stack check. A guest thread that
179
- // fills a block carved from memory another thread has just grown traps
180
- // ("memory access out of bounds"). So the heap grows to the memory's maximum
181
- // before any guest thread starts, and never grows after (a partial pre-grow
182
- // only moves the failure). The heap stops 16 MiB short of 4 GiB: a segment
183
- // ending at 2^32 would wrap the guest allocator's pointer arithmetic.
184
- // WasmEdge (the SDN host) keeps one shared size and is not affected.
185
- function heapLimitBytes(memory, options) {
186
- const max = MAXIMUM_BYTES.get(memory) ?? memory.buffer.byteLength;
187
- const cap = options.heapBytes ?? Infinity;
188
- return Math.min(max, 2 ** 32 - 16 * 2 ** 20, memory.buffer.byteLength + cap);
189
- }
190
-
191
- /**
192
- * One guest process: its memory, its file system, its thread pool, and the
193
- * workers that run it. The supervisor starts pool workers when a spawn asks
194
- * for one and watches for a guest fault or exit.
195
- */
196
- class Supervisor {
197
- constructor(module, memory, cfg, options) {
198
- this.module = module;
199
- this.memory = memory;
200
- this.cfg = cfg;
201
- this.control = new Int32Array(cfg.control);
202
- this.pool = new Int32Array(cfg.pool);
203
- this.cap = this.pool[0];
204
- this.workers = new Set();
205
- this.listeners = new Set();
206
- this.stopped = false;
207
- this.poolErrors = [];
208
- // Memory size when the first guest thread was seen running (tick): any
209
- // growth after it is the precondition of the V8 stale-view trap above.
210
- this.memoryAtFirstThread = 0;
211
- const warm = Math.min(options.warmThreads ?? 16, this.cap);
212
- for (let i = 0; i < warm; i += 1) this.startPoolWorker();
213
- this.timer = setInterval(() => this.tick(), 2);
214
- }
215
-
216
- startPoolWorker() {
217
- const slot = Atomics.add(this.pool, POOL_CREATED, 1);
218
- if (slot >= this.cap) {
219
- Atomics.sub(this.pool, POOL_CREATED, 1);
220
- return;
221
- }
222
- const worker = new Worker(GUEST_URL, {
223
- workerData: { mode: "pool", slot, wasmModule: this.module, memory: this.memory, cfg: this.cfg },
224
- });
225
- worker.on("error", (error) => this.poolErrors.push(String(error?.stack ?? error)));
226
- worker.once("exit", () => this.workers.delete(worker));
227
- this.workers.add(worker);
228
- }
229
-
230
- tick() {
231
- if (this.stopped) return;
232
- const want = Atomics.exchange(this.pool, POOL_WANT, 0);
233
- for (let i = 0; i < want; i += 1) this.startPoolWorker();
234
- if (!this.memoryAtFirstThread && Atomics.load(this.pool, POOL_SPAWNED) > 0) {
235
- this.memoryAtFirstThread = this.memory.buffer.byteLength;
236
- }
237
- for (const listener of this.listeners) listener();
238
- }
239
-
240
- startGuest(mode) {
241
- const worker = new Worker(GUEST_URL, {
242
- workerData: { mode, wasmModule: this.module, memory: this.memory, cfg: this.cfg },
243
- });
244
- this.workers.add(worker);
245
- worker.once("exit", () => this.workers.delete(worker));
246
- return worker;
247
- }
248
-
249
- exited() {
250
- return Atomics.load(this.control, CONTROL_EXITED) !== 0;
251
- }
252
-
253
- faulted() {
254
- return Atomics.load(this.control, CONTROL_FAULT) !== 0;
255
- }
256
-
257
- report() {
258
- const c = this.control;
259
- const slots = Math.min(Atomics.load(c, CONTROL_SLOT), CONTROL_TIDS);
260
- const tids = new Set();
261
- for (let i = 0; i < slots; i += 1) tids.add(Atomics.load(c, CONTROL_HEADER + i));
262
- return {
263
- guestThreadsSpawned: Atomics.load(this.pool, POOL_SPAWNED),
264
- // Pool threads running at once, plus the guest's own thread.
265
- maxConcurrentGuestThreads: Atomics.load(this.pool, POOL_MAX_RUNNING) + 1,
266
- runningGuestThreads: Atomics.load(this.pool, POOL_RUNNING),
267
- spawnsDeclined: Atomics.load(this.pool, POOL_DECLINED),
268
- workerThreads: Atomics.load(c, CONTROL_STARTED),
269
- distinctWorkerThreadIds: tids.size,
270
- memoryBytes: this.memory.buffer.byteLength,
271
- // Bytes the memory grew after the first guest thread ran (sampled every
272
- // 2 ms). Under V8 that growth can trap a thread (heapLimitBytes).
273
- memoryGrownWhileThreadsRan: this.memoryAtFirstThread
274
- ? Math.max(0, this.memory.buffer.byteLength - this.memoryAtFirstThread)
275
- : 0,
276
- };
277
- }
278
-
279
- async stop() {
280
- this.stopped = true;
281
- clearInterval(this.timer);
282
- await Promise.all([...this.workers].map((w) => w.terminate().catch(() => {})));
283
- this.workers.clear();
284
- }
285
- }
286
-
287
- /**
288
- * Run a wasi-threads command (the ps test commands) to completion.
289
- *
290
- * @param {string|Uint8Array} wasm path or bytes
291
- * @param {object} [options]
292
- * @param {string[]} [options.args] argv after argv[0]
293
- * @param {Record<string,string>} [options.env] (TMPDIR defaults to "/tmp")
294
- * @param {string} [options.root] host directory the guest sees as "/" (WASI
295
- * and flatsql_io); default a private temporary directory, removed after
296
- * @param {boolean} [options.keepRoot] keep that temporary directory
297
- * @param {Array<{guest:string,host:string}>} [options.mounts] more preopens
298
- * @param {number} [options.maxThreads] pool cap (default 512)
299
- * @param {number} [options.warmThreads] workers started up front (default 16)
300
- * @param {number} [options.timeoutMs] stop the guest after this long
301
- * @param {number} [options.killAfterMs] kill the guest after this long (a
302
- * crash, not a failure: the result has killed: true)
303
- * @param {() => void} [options.beforeKill] runs just before a kill (the fault
304
- * overlay freezes here)
305
- * @param {object} [options.fault] a fault overlay (createFaultOverlay)
306
- * @param {number} [options.stdoutFd] guest stdout goes to this fd (default 1)
307
- * @param {{ randomSeed?: bigint }} [options.deterministic] seeded random_get
308
- * @returns {Promise<{ exitCode: number, killed: boolean, timedOut: boolean,
309
- * fault: boolean, error: string|null, elapsedMs: number, threads: object }>}
310
- */
311
- export async function runPsCommand(wasm, options = {}) {
312
- const { module, bytes } = await loadModule(wasm);
313
- const memory = createMemory(bytes ?? options.bytes, options);
314
- const argv0 = typeof wasm === "string" ? path.basename(wasm) : "command.wasm";
315
- const fsRoot = prepareRoot(options);
316
- const cfg = hostConfig({ ...options, args: [argv0, ...(options.args ?? [])] }, fsRoot.root);
317
- const started = performance.now();
318
- const sup = new Supervisor(module, memory, cfg, options);
319
- const guest = sup.startGuest("command");
320
- const result = await new Promise((resolve) => {
321
- let settled = false;
322
- const timers = [];
323
- const settle = (value) => {
324
- if (settled) return;
325
- settled = true;
326
- for (const t of timers) clearTimeout(t);
327
- sup.listeners.clear();
328
- resolve(value);
329
- };
330
- const base = { exitCode: 0, killed: false, timedOut: false, fault: false, error: null };
331
- guest.on("message", (msg) => {
332
- if (msg.t === "done") settle({ ...base, exitCode: msg.code, error: msg.error, fault: msg.error !== null });
333
- });
334
- guest.on("error", (error) => settle({ ...base, exitCode: 134, error: String(error?.stack ?? error), fault: true }));
335
- guest.on("exit", (code) => settle({ ...base, exitCode: code || 134, error: `guest worker exited (${code})`, fault: true }));
336
- // A pool thread that exits the process or traps: the guest's own thread
337
- // may be blocked in a join forever, so decide from here.
338
- sup.listeners.add(() => {
339
- if (sup.faulted()) {
340
- settle({
341
- ...base,
342
- exitCode: 134,
343
- error: `guest thread ${Atomics.load(sup.control, CONTROL_FAULT_TID)} trapped`,
344
- fault: true,
345
- });
346
- } else if (sup.exited()) {
347
- timers.push(setTimeout(() => settle({ ...base, exitCode: Atomics.load(sup.control, CONTROL_EXIT_CODE) }), 100));
348
- }
349
- });
350
- if (options.timeoutMs) {
351
- timers.push(setTimeout(() => settle({ ...base, exitCode: 124, timedOut: true, error: `timed out after ${options.timeoutMs} ms` }), options.timeoutMs));
352
- }
353
- if (options.killAfterMs !== undefined) {
354
- timers.push(setTimeout(() => {
355
- options.beforeKill?.();
356
- settle({ ...base, exitCode: 137, killed: true });
357
- }, options.killAfterMs));
358
- }
359
- });
360
- const threads = sup.report();
361
- await sup.stop();
362
- fsRoot.cleanup();
363
- return { ...result, elapsedMs: performance.now() - started, threads, root: fsRoot.root, poolErrors: sup.poolErrors };
364
- }
365
-
366
- /**
367
- * Instantiate flatsql-ps-threads.wasm as one instance (writer or reader) with
368
- * an exec thread of its own. Returns an RPC handle for the C ABI:
369
- * `call(fn, ...args)`, `write(bytes) -> ptr`, `read(ptr, len)`, `threads()`,
370
- * `close()`.
371
- */
372
- export async function createPsNodeInstance(wasm, options = {}) {
373
- const { module, bytes } = await loadModule(wasm);
374
- const memory = createMemory(bytes ?? options.bytes, options);
375
- const fsRoot = prepareRoot(options);
376
- const cfg = hostConfig(options, fsRoot.root);
377
- const sup = new Supervisor(module, memory, cfg, options);
378
- const worker = sup.startGuest("reactor");
379
- const pending = new Map();
380
- let nextId = 1;
381
- let failure = null;
382
- const fail = (error) => {
383
- failure = error;
384
- for (const p of pending.values()) p.reject(error);
385
- pending.clear();
386
- };
387
- sup.listeners.add(() => {
388
- if (!failure && sup.faulted()) fail(new Error(`guest thread ${Atomics.load(sup.control, CONTROL_FAULT_TID)} trapped`));
389
- });
390
- await new Promise((resolve, reject) => {
391
- worker.on("message", (msg) => {
392
- if (msg.t === "ready") resolve();
393
- else if (msg.t === "reply") {
394
- const p = pending.get(msg.id);
395
- pending.delete(msg.id);
396
- if (!p) return;
397
- if (msg.error) p.reject(new Error(msg.error));
398
- else p.resolve(msg.value);
399
- }
400
- });
401
- worker.on("error", (error) => {
402
- fail(error);
403
- reject(error);
404
- });
405
- });
406
- // Before any guest thread: the heap to the memory's maximum (heapLimitBytes).
407
- if (options.growHeap !== false) {
408
- await new Promise((resolve, reject) => {
409
- const id = nextId++;
410
- pending.set(id, { resolve, reject });
411
- worker.postMessage({ op: "growHeap", limitBytes: heapLimitBytes(memory, options), id });
412
- });
413
- }
414
- const rpc = (msg) => {
415
- if (failure) return Promise.reject(failure);
416
- const id = nextId++;
417
- return new Promise((resolve, reject) => {
418
- pending.set(id, { resolve, reject });
419
- worker.postMessage({ ...msg, id });
420
- });
421
- };
422
- return {
423
- memory,
424
- root: fsRoot.root,
425
- call: (fn, ...args) => rpc({ op: "call", fn, args }),
426
- write: (data) => rpc({ op: "write", bytes: data instanceof Uint8Array ? data : new Uint8Array(data) }),
427
- read: (ptr, len) => rpc({ op: "read", ptr, len }),
428
- threads: () => sup.report(),
429
- faulted: () => sup.faulted(),
430
- async close() {
431
- await sup.stop();
432
- fsRoot.cleanup();
433
- },
434
- };
435
- }
436
-
437
- // ---- CLI ---------------------------------------------------------------------
438
- const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
439
- if (invokedDirectly) {
440
- const [wasmPath, ...args] = process.argv.slice(2);
441
- if (!wasmPath) {
442
- process.stderr.write("usage: node wasm/ps-node-host.mjs <command.wasm> [args...]\n");
443
- process.exit(2);
444
- }
445
- const env = {};
446
- for (const [k, v] of Object.entries(process.env)) if (k.startsWith("FLATSQL_PS_") || k.startsWith("PS_")) env[k] = v;
447
- const timeoutMs = Number(process.env.FLATSQL_PS_TIMEOUT_MS ?? 0) || undefined;
448
- // The checkout is visible at its own path (the test commands read their
449
- // vectors from the build tree); everything else lives in a private root.
450
- const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
451
- const r = await runPsCommand(wasmPath, {
452
- args,
453
- env,
454
- timeoutMs,
455
- root: process.env.FLATSQL_PS_ROOT || undefined,
456
- keepRoot: process.env.FLATSQL_PS_KEEP_ROOT === "1",
457
- mounts: [{ guest: repo, host: repo }],
458
- });
459
- const t = r.threads;
460
- if (process.env.FLATSQL_PS_REPORT) {
461
- fs.writeFileSync(process.env.FLATSQL_PS_REPORT, `${JSON.stringify({ ...r, root: undefined })}\n`);
462
- }
463
- process.stderr.write(
464
- `ps-node-host: exit ${r.exitCode}${r.error ? ` (${r.error})` : ""}; guest threads spawned ${t.guestThreadsSpawned}, ` +
465
- `max concurrent ${t.maxConcurrentGuestThreads} on ${t.distinctWorkerThreadIds} distinct worker threads` +
466
- `${t.spawnsDeclined ? `, ${t.spawnsDeclined} spawns declined` : ""}; memory ${(t.memoryBytes / 2 ** 20).toFixed(0)} MiB; ` +
467
- `${(r.elapsedMs / 1000).toFixed(1)} s\n`,
468
- );
469
- if (t.memoryGrownWhileThreadsRan) {
470
- process.stderr.write(
471
- `ps-node-host: WARNING memory grew ${(t.memoryGrownWhileThreadsRan / 2 ** 20).toFixed(0)} MiB while guest ` +
472
- `threads ran (V8 can trap a thread on memory another thread grew; grow the heap before threads start)\n`,
473
- );
474
- }
475
- process.exit(r.exitCode);
476
- }