space-data-module-sdk 0.8.19 → 0.8.20

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 (64) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -2
  3. package/docs/assembly.js +190 -0
  4. package/docs/browser-wasmedge-isomorphic.html +6 -1
  5. package/docs/byo-wasm-quickstart.html +6 -1
  6. package/docs/conformance.html +6 -1
  7. package/docs/events-abi.html +6 -1
  8. package/docs/families/analytics.html +7 -2
  9. package/docs/families/attitude.html +7 -2
  10. package/docs/families/behavior.html +7 -2
  11. package/docs/families/breakup.html +7 -2
  12. package/docs/families/conjunction.html +7 -2
  13. package/docs/families/data-source.html +7 -2
  14. package/docs/families/effects.html +7 -2
  15. package/docs/families/environment.html +7 -2
  16. package/docs/families/estimation.html +7 -2
  17. package/docs/families/gnc.html +7 -2
  18. package/docs/families/maneuver.html +7 -2
  19. package/docs/families/obstruction.html +7 -2
  20. package/docs/families/propagator.html +7 -2
  21. package/docs/families/propulsion.html +7 -2
  22. package/docs/families/reentry.html +7 -2
  23. package/docs/families/rf.html +7 -2
  24. package/docs/families/scheduler.html +7 -2
  25. package/docs/families/sensor.html +7 -2
  26. package/docs/families/signature.html +7 -2
  27. package/docs/flatsql-host-contract.html +7 -2
  28. package/docs/flatsql-host-contract.md +2 -2
  29. package/docs/flatsql-streaming-standard.html +6 -1
  30. package/docs/gpu-module-abi.html +6 -1
  31. package/docs/index.html +21 -14
  32. package/docs/isomorphic-pthreads.html +19 -2
  33. package/docs/isomorphic-pthreads.md +59 -1
  34. package/docs/isomorphic-sdn-runtime-plan.html +6 -1
  35. package/docs/language-runtime-matrix.html +6 -1
  36. package/docs/llms.txt +1 -1
  37. package/docs/module-bundle-runtime-plan.html +6 -1
  38. package/docs/module-publication-standard.html +6 -1
  39. package/docs/node-red-default-node-parity.html +6 -1
  40. package/docs/protect-and-sign.html +6 -1
  41. package/docs/protocol-installation.html +6 -1
  42. package/docs/publication-submission.html +6 -1
  43. package/docs/secrets-capability.html +6 -1
  44. package/docs/styles.css +32 -0
  45. package/docs/testing-harness.html +6 -1
  46. package/docs/tri-runtime-parity-gate.html +6 -1
  47. package/docs/tri-runtime-parity.html +6 -1
  48. package/package.json +1 -1
  49. package/src/host/browserModuleHarness.js +39 -35
  50. package/src/host/isomorphicLoader.js +13 -6
  51. package/src/host/wasiShim.js +57 -13
  52. package/src/host/wasiThreadBrowserWorker.mjs +1 -0
  53. package/src/host/wasiThreadHost.js +16 -1
  54. package/src/host/wasiThreadWorker.mjs +2 -1
  55. package/src/host/wasiThreadWorkerRuntime.js +2 -1
  56. package/src/testing/browserModuleHarness.js +2 -0
  57. package/src/testing/buildWasmEdgeRunner.js +147 -4
  58. package/src/testing/docker/wasmedge-parity.Dockerfile +23 -1
  59. package/src/testing/isomorphicHarness.js +5 -0
  60. package/src/testing/native/wasmedge-0.16.4-atomic-wait.patch +111 -0
  61. package/src/testing/native/wasmedge_wasi_threads_runner.c +345 -0
  62. package/src/testing/parityBrowserRunner.js +12 -4
  63. package/src/testing/parityHarness.js +11 -3
  64. package/src/testing/parityLanes.js +29 -5
@@ -2,10 +2,12 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <script>try{var t=localStorage.getItem("sdn-theme");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}</script>
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
7
  <meta name="description" content="docs/tri-runtime-parity.md describes the parity harness: one" />
7
8
  <title>Tri-runtime parity gate — Space Data Module SDK</title>
8
9
  <link rel="stylesheet" href="./styles.css" />
10
+ <script src="./assembly.js" defer></script>
9
11
  <!-- SDN_CONSUMER_ASSETS_START -->
10
12
  <link rel="stylesheet" href="https://static.spacedatanetwork.org/assets/hd-wallet-ui/2.0.28/sdn-wallet-public-client.c3f68d1cfd88478f10d836a5e829d1dfc6a10157972cf0f7d4d319d0636f2cc4.css" integrity="sha384-fICuhN4I9xqOK1F5vGGzl26opuO+xQIvTZXMSV76lWTBiQ6AfzztMGLLDw8yRT8i" crossorigin="anonymous">
11
13
  <script defer src="https://static.spacedatanetwork.org/assets/hd-wallet-ui/2.0.28/sdn-wallet-public-client.f611e7e151a3b5c38384272f8894a4c6634f0a29bf925b0e02e0e27f8db0bfff.js" integrity="sha384-R+QqsKoWJIS7iWrtPmgJ49DVnb1hSGjZokupbLInJd1PLp8RwQhtF/hnaerc33ci" crossorigin="anonymous" data-sdn-wallet-public-client="v1"></script>
@@ -13,6 +15,7 @@
13
15
  <!-- SDN_CONSUMER_ASSETS_END -->
14
16
  </head>
15
17
  <body>
18
+ <canvas id="assembly" aria-hidden="true"></canvas>
16
19
  <a class="skip-link" href="#main">Skip to content</a>
17
20
  <sdn-stack-nav active="module-sdk"></sdn-stack-nav>
18
21
  <header class="site-header">
@@ -22,12 +25,13 @@
22
25
  <a href="./conformance.html">Conformance</a>
23
26
  <a href="https://github.com/DigitalArsenal/space-data-module-sdk">GitHub</a>
24
27
  <a class="stack-button" href="#stack">Stack</a>
28
+ <button class="theme-toggle" type="button" aria-label="Switch between light and dark theme" title="Light or dark theme"><svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg><svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg></button>
25
29
  </nav>
26
30
  </header>
27
31
  <div class="shell">
28
32
  <nav class="sidebar" id="sidebar" aria-label="Documentation">
29
33
  <h2>Start here</h2><ul>
30
- <li><a href="./index.html"><span>Harness family matrix</span></a></li>
34
+ <li><a href="./index.html"><span>Module types</span></a></li>
31
35
  <li><a href="./byo-wasm-quickstart.html"><span>BYO-wasm quickstart</span></a></li>
32
36
  <li><a href="./conformance.html"><span>Conformance kit</span></a></li>
33
37
  <li><a href="./protect-and-sign.html"><span>Protect and sign</span></a></li>
@@ -177,6 +181,7 @@ space-data-module parity-gate --gate-manifest ./parity/negative-control.json --e
177
181
  <footer class="site-footer">
178
182
  Space Data Module SDK — Apache-2.0. Every ABI page renders a Markdown
179
183
  source in <code>docs/</code>; edit the Markdown, never the HTML.
184
+ Contact <a href="mailto:tj@edgesource.com">tj@edgesource.com</a>.
180
185
  </footer>
181
186
  </body>
182
187
  </html>
@@ -2,10 +2,12 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <script>try{var t=localStorage.getItem("sdn-theme");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}</script>
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
7
  <meta name="description" content="ONE dist/isomorphic/module.wasm, THREE runtimes, IDENTICAL behavior:" />
7
8
  <title>Tri-runtime parity — Space Data Module SDK</title>
8
9
  <link rel="stylesheet" href="./styles.css" />
10
+ <script src="./assembly.js" defer></script>
9
11
  <!-- SDN_CONSUMER_ASSETS_START -->
10
12
  <link rel="stylesheet" href="https://static.spacedatanetwork.org/assets/hd-wallet-ui/2.0.28/sdn-wallet-public-client.c3f68d1cfd88478f10d836a5e829d1dfc6a10157972cf0f7d4d319d0636f2cc4.css" integrity="sha384-fICuhN4I9xqOK1F5vGGzl26opuO+xQIvTZXMSV76lWTBiQ6AfzztMGLLDw8yRT8i" crossorigin="anonymous">
11
13
  <script defer src="https://static.spacedatanetwork.org/assets/hd-wallet-ui/2.0.28/sdn-wallet-public-client.f611e7e151a3b5c38384272f8894a4c6634f0a29bf925b0e02e0e27f8db0bfff.js" integrity="sha384-R+QqsKoWJIS7iWrtPmgJ49DVnb1hSGjZokupbLInJd1PLp8RwQhtF/hnaerc33ci" crossorigin="anonymous" data-sdn-wallet-public-client="v1"></script>
@@ -13,6 +15,7 @@
13
15
  <!-- SDN_CONSUMER_ASSETS_END -->
14
16
  </head>
15
17
  <body>
18
+ <canvas id="assembly" aria-hidden="true"></canvas>
16
19
  <a class="skip-link" href="#main">Skip to content</a>
17
20
  <sdn-stack-nav active="module-sdk"></sdn-stack-nav>
18
21
  <header class="site-header">
@@ -22,12 +25,13 @@
22
25
  <a href="./conformance.html">Conformance</a>
23
26
  <a href="https://github.com/DigitalArsenal/space-data-module-sdk">GitHub</a>
24
27
  <a class="stack-button" href="#stack">Stack</a>
28
+ <button class="theme-toggle" type="button" aria-label="Switch between light and dark theme" title="Light or dark theme"><svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg><svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg></button>
25
29
  </nav>
26
30
  </header>
27
31
  <div class="shell">
28
32
  <nav class="sidebar" id="sidebar" aria-label="Documentation">
29
33
  <h2>Start here</h2><ul>
30
- <li><a href="./index.html"><span>Harness family matrix</span></a></li>
34
+ <li><a href="./index.html"><span>Module types</span></a></li>
31
35
  <li><a href="./byo-wasm-quickstart.html"><span>BYO-wasm quickstart</span></a></li>
32
36
  <li><a href="./conformance.html"><span>Conformance kit</span></a></li>
33
37
  <li><a href="./protect-and-sign.html"><span>Protect and sign</span></a></li>
@@ -198,6 +202,7 @@
198
202
  <footer class="site-footer">
199
203
  Space Data Module SDK — Apache-2.0. Every ABI page renders a Markdown
200
204
  source in <code>docs/</code>; edit the Markdown, never the HTML.
205
+ Contact <a href="mailto:tj@edgesource.com">tj@edgesource.com</a>.
201
206
  </footer>
202
207
  </body>
203
208
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.8.19",
3
+ "version": "0.8.20",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  * stdout for the response bytes.
21
21
  */
22
22
 
23
- import { createBrowserWasiShim, WasiExitError } from "./wasiShim.js";
23
+ import { createBrowserWasiShim, createSharedWasiProcess, WasiExitError } from "./wasiShim.js";
24
24
  import {
25
25
  createWasiThreadSpawn,
26
26
  isWasiThreadsModule,
@@ -307,7 +307,10 @@ async function instantiateBrowserModule(options = {}) {
307
307
  "Browser module harness memory must be a WebAssembly.Memory.",
308
308
  );
309
309
  }
310
+ const isThreaded = isWasiThreadsModule(options.wasmModule);
311
+ const processState = isThreaded ? createSharedWasiProcess(options) : null;
310
312
  const wasi = createBrowserWasiShim({
313
+ processState,
311
314
  args: options.args ?? [],
312
315
  env: options.env ?? {},
313
316
  stdinBytes: options.stdinBytes ?? new Uint8Array(),
@@ -323,7 +326,6 @@ async function instantiateBrowserModule(options = {}) {
323
326
  // `wasi.thread-spawn` over a SHARED imported memory. Such a module can only be
324
327
  // instantiated with shared memory, so force it here regardless of the caller's
325
328
  // sharedMemory flag; single-thread artifacts are unaffected.
326
- const isThreaded = isWasiThreadsModule(options.wasmModule);
327
329
  const memoryImports = moduleImports.filter((entry) => entry.kind === "memory");
328
330
  if (!providedMemory && memoryImports.length > 0) {
329
331
  providedMemory = createImportedMemory(
@@ -356,6 +358,7 @@ async function instantiateBrowserModule(options = {}) {
356
358
  threadHost = await createWasiThreadSpawn({
357
359
  wasmModule: options.wasmModule,
358
360
  memory: providedMemory,
361
+ processState,
359
362
  // Browser warm-pool sizing: cap pooled workers at how many guest threads
360
363
  // the module will actually ask for. Ignored by the Node lazy path.
361
364
  requestedThreads: options.maxThreads,
@@ -395,35 +398,28 @@ async function instantiateBrowserModule(options = {}) {
395
398
  Object.assign(importObject, bridge.imports);
396
399
  }
397
400
 
398
- instance = await WebAssembly.instantiate(options.wasmModule, importObject);
399
- memory = instance.exports.memory ?? providedMemory;
400
- if (memory) {
401
- if (
402
- options.sharedMemory === true &&
403
- !isSharedArrayBufferLike(memory.buffer)
404
- ) {
405
- throw new Error(
406
- "Browser module harness sharedMemory requires shared WebAssembly.Memory backing.",
407
- );
408
- }
409
- wasi.setMemory(memory);
410
- }
411
- if (instance.exports._initialize) {
412
- instance.exports._initialize();
413
- } else if (isThreaded && typeof instance.exports._start === "function") {
414
- // wasi-threads artifacts link the wasi command crt (exports `_start`), not a
415
- // reactor (`_initialize`). Run `_start` ONCE to execute global constructors
416
- // + WASI init before any direct invoke. The module declares no command
417
- // surface, so its main is an empty stub — `_start` runs ctors and proc_exit's
418
- // out; swallow that exit. Direct-invoke exports remain callable afterward
419
- // (the linear memory + globals persist).
420
- try {
421
- instance.exports._start();
422
- } catch (error) {
423
- if (!(error instanceof WasiExitError)) {
424
- throw error;
401
+ try {
402
+ instance = await WebAssembly.instantiate(options.wasmModule, importObject);
403
+ memory = instance.exports.memory ?? providedMemory;
404
+ if (memory) {
405
+ if (
406
+ options.sharedMemory === true &&
407
+ !isSharedArrayBufferLike(memory.buffer)
408
+ ) {
409
+ throw new Error(
410
+ "Browser module harness sharedMemory requires shared WebAssembly.Memory backing.",
411
+ );
425
412
  }
413
+ wasi.setMemory(memory);
414
+ }
415
+ if (instance.exports._initialize) {
416
+ instance.exports._initialize();
426
417
  }
418
+ // Command exports initialize their CRT on entry. Calling _start here would
419
+ // consume stdin before invocation; direct-only modules use _initialize.
420
+ } catch (error) {
421
+ await threadHost?.terminateAll();
422
+ throw error;
427
423
  }
428
424
 
429
425
  return {
@@ -561,8 +557,11 @@ export async function createBrowserModuleHarness(options = {}) {
561
557
  maxThreads: options.maxThreads,
562
558
  threadHostcallChannel: options.threadHostcallChannel,
563
559
  enableBrowserWasiThreads: options.enableBrowserWasiThreads,
560
+ wasiThreadWorkerBaseUrl: options.wasiThreadWorkerBaseUrl,
561
+ wasiThreadWorkerUrl: options.wasiThreadWorkerUrl,
564
562
  });
565
563
  const { instance, bridge, wasi, memory, threadHost } = activeContext;
564
+ let lastCommandContext = null;
566
565
  const allowRawInvoke = options.allowRawInvoke !== false;
567
566
 
568
567
  // --- Invoke helpers ---
@@ -805,6 +804,8 @@ export async function createBrowserModuleHarness(options = {}) {
805
804
  async function invokeCommandRaw(stdinBytes) {
806
805
  const commandContext = await instantiateBrowserModule({
807
806
  wasmModule,
807
+ // Command CRT startup consumes stdin and is not reentrant. Run it only
808
+ // below, after this fresh instance has received the actual request.
808
809
  host,
809
810
  hostcallDispatch: options.hostcallDispatch,
810
811
  args: options.args,
@@ -820,7 +821,10 @@ export async function createBrowserModuleHarness(options = {}) {
820
821
  maxThreads: options.maxThreads,
821
822
  threadHostcallChannel: options.threadHostcallChannel,
822
823
  enableBrowserWasiThreads: options.enableBrowserWasiThreads,
824
+ wasiThreadWorkerBaseUrl: options.wasiThreadWorkerBaseUrl,
825
+ wasiThreadWorkerUrl: options.wasiThreadWorkerUrl,
823
826
  });
827
+ lastCommandContext = commandContext;
824
828
  try {
825
829
  const commandExport = commandContext.instance.exports[DefaultInvokeExports.commandSymbol];
826
830
  if (typeof commandExport !== "function") {
@@ -833,6 +837,8 @@ export async function createBrowserModuleHarness(options = {}) {
833
837
  if (!(error instanceof WasiExitError) || error.code !== 0) {
834
838
  throw error;
835
839
  }
840
+ } finally {
841
+ await commandContext.threadHost?.terminateAll();
836
842
  }
837
843
  return commandContext.wasi.stdout;
838
844
  }
@@ -903,11 +909,9 @@ export async function createBrowserModuleHarness(options = {}) {
903
909
  return new Uint8Array(memory.buffer, ptr, size).slice();
904
910
  }
905
911
 
906
- function destroy() {
912
+ async function destroy() {
907
913
  wasi.flushOutput();
908
- if (threadHost) {
909
- void threadHost.terminateAll();
910
- }
914
+ await threadHost?.terminateAll();
911
915
  }
912
916
 
913
917
  return {
@@ -920,12 +924,12 @@ export async function createBrowserModuleHarness(options = {}) {
920
924
  module: wasmModule,
921
925
  host,
922
926
  bridge,
923
- wasi,
927
+ get wasi() { return lastCommandContext?.wasi ?? wasi; },
924
928
  memory,
925
929
  // Present only for isomorphic-pthreads (wasi-threads) artifacts: exposes the
926
930
  // spawn host so callers can observe real thread activity
927
931
  // (spawnCount/distinctOsThreadCount).
928
- threadHost,
932
+ get threadHost() { return lastCommandContext?.threadHost ?? threadHost; },
929
933
  callHost,
930
934
  invokeDirect,
931
935
  invoke,
@@ -65,12 +65,18 @@ async function createWasmEdgeCommandHarness(options = {}) {
65
65
  const loadableBytes = toLoadableWasmBytes(wasmBytes);
66
66
  const inspection = await inspectModule(loadableBytes);
67
67
 
68
- if (!inspection.exports.includes(DefaultInvokeExports.commandSymbol)) {
68
+ const threaded = inspection.imports.some((entry) =>
69
+ entry.module === "wasi" && entry.name === "thread-spawn");
70
+ const surface = inspection.exports.includes(DefaultInvokeExports.commandSymbol) ? "command" : "direct";
71
+ if (surface === "direct" && (!threaded || !inspection.exports.includes(DefaultInvokeExports.invokeSymbol))) {
69
72
  throw new Error(
70
73
  "Standalone WasmEdge loading requires a command-surface artifact with the _start export.",
71
74
  );
72
75
  }
73
76
 
77
+ const command = threaded
78
+ ? await (await import("../testing/buildWasmEdgeRunner.js")).resolveWasmEdgeWasiThreadsRunner(options)
79
+ : options.wasmEdgeRunnerBinary ?? options.wasmEdgeBinary ?? "wasmedge";
74
80
  let launchWasmPath = wasmPath;
75
81
  let tempArtifactDir = null;
76
82
  if (loadableBytes.byteLength !== wasmBytes.byteLength) {
@@ -84,9 +90,9 @@ async function createWasmEdgeCommandHarness(options = {}) {
84
90
  await writeFile(launchWasmPath, loadableBytes);
85
91
  }
86
92
 
87
- const command = options.wasmEdgeBinary ?? "wasmedge";
88
93
  const args = [
89
94
  ...(options.enableThreads === false ? [] : ["--enable-threads"]),
95
+ ...Object.entries(options.env ?? {}).flatMap(([key, value]) => ["--env", `${key}=${value}`]),
90
96
  launchWasmPath,
91
97
  ...(Array.isArray(options.args) ? options.args : []),
92
98
  ];
@@ -156,7 +162,7 @@ async function createWasmEdgeCommandHarness(options = {}) {
156
162
  runtime: {
157
163
  kind: "wasmedge",
158
164
  profile: inspection.profile,
159
- surface: "command",
165
+ surface,
160
166
  },
161
167
  launchPlan,
162
168
  invokeRaw,
@@ -225,8 +231,7 @@ export async function loadModule(options = {}) {
225
231
  // byte-level question.
226
232
  const needsCompiledModule =
227
233
  runtimeKind === "wasmedge" &&
228
- !runtimeHostRequested &&
229
- !options.wasmEdgeRunnerBinary;
234
+ !runtimeHostRequested;
230
235
  const needsBytes = Boolean(signaturePolicy) || runtimeKind === "wasmedge";
231
236
  let artifactBytes = null;
232
237
  let wasmModule = null;
@@ -264,9 +269,11 @@ export async function loadModule(options = {}) {
264
269
  });
265
270
  if (needsCompiledModule) {
266
271
  const inspection = await inspectModule(wasmModule);
272
+ const threaded = inspection.imports.some((entry) => entry.module === "wasi" && entry.name === "thread-spawn");
267
273
  if (
268
274
  (inspection.profile === "standalone" || inspection.profile === "module-host-abi") &&
269
- inspection.exports.includes("_start")
275
+ (inspection.exports.includes("_start") || (threaded && inspection.exports.includes("plugin_invoke_stream"))) &&
276
+ (!options.wasmEdgeRunnerBinary || options.surface === "command" || threaded)
270
277
  ) {
271
278
  return attachHostDispatch(
272
279
  await createWasmEdgeCommandHarness(options),
@@ -34,10 +34,34 @@ export class WasiExitError extends Error {
34
34
  }
35
35
  }
36
36
 
37
+ // One process owns its descriptors, even when libc executes main on a pthread.
38
+ // Offsets are atomically reserved so concurrent readers/writers never duplicate
39
+ // input or overwrite another thread's output. Overflow is an explicit WASI error.
40
+ export function createSharedWasiProcess({ args = [], env = {}, stdinBytes = [], maxOutputBytes = 16 * 1024 * 1024 } = {}) {
41
+ const bytes = new Uint8Array(stdinBytes);
42
+ const input = new Uint8Array(new SharedArrayBuffer(bytes.length));
43
+ input.set(bytes);
44
+ return {
45
+ args, env, input,
46
+ control: new Int32Array(new SharedArrayBuffer(5 * 4)),
47
+ stdout: new Uint8Array(new SharedArrayBuffer(maxOutputBytes)),
48
+ stderr: new Uint8Array(new SharedArrayBuffer(maxOutputBytes)),
49
+ };
50
+ }
51
+
52
+ function reserve(control, index, length, capacity) {
53
+ for (;;) {
54
+ const offset = Atomics.load(control, index);
55
+ if (length > capacity - offset) return -1;
56
+ if (Atomics.compareExchange(control, index, offset, offset + length) === offset) return offset;
57
+ }
58
+ }
59
+
37
60
  export function createBrowserWasiShim(options = {}) {
38
- const args = options.args ?? [];
39
- const env = options.env ?? {};
40
- const stdinBytes = new Uint8Array(options.stdinBytes ?? []);
61
+ const shared = options.processState;
62
+ const args = shared?.args ?? options.args ?? [];
63
+ const env = shared?.env ?? options.env ?? {};
64
+ const stdinBytes = shared?.input ?? new Uint8Array(options.stdinBytes ?? []);
41
65
  const logOutput = options.logOutput === true;
42
66
  const performanceApi = options.performance ?? globalThis.performance ?? {
43
67
  now: () => Date.now(),
@@ -100,7 +124,14 @@ export function createBrowserWasiShim(options = {}) {
100
124
  const base = iovsPtr + i * 8;
101
125
  const ptr = dv.getUint32(base, true);
102
126
  const len = dv.getUint32(base + 4, true);
103
- target.push(bytes.slice(ptr, ptr + len));
127
+ if (shared) {
128
+ const output = fd === 1 ? shared.stdout : shared.stderr;
129
+ const offset = reserve(shared.control, fd, len, output.length);
130
+ if (offset < 0) return 51; // NOSPC: never silently truncate command output.
131
+ output.set(bytes.subarray(ptr, ptr + len), offset);
132
+ } else {
133
+ target.push(bytes.slice(ptr, ptr + len));
134
+ }
104
135
  totalWritten += len;
105
136
  }
106
137
 
@@ -119,16 +150,25 @@ export function createBrowserWasiShim(options = {}) {
119
150
  let totalRead = 0;
120
151
 
121
152
  for (let i = 0; i < iovsLen; i += 1) {
122
- if (stdinOffset >= stdinBytes.length) {
123
- break;
124
- }
125
153
  const base = iovsPtr + i * 8;
126
154
  const ptr = dv.getUint32(base, true);
127
155
  const len = dv.getUint32(base + 4, true);
128
- const remaining = stdinBytes.length - stdinOffset;
129
- const count = Math.min(len, remaining);
130
- bytes.set(stdinBytes.subarray(stdinOffset, stdinOffset + count), ptr);
131
- stdinOffset += count;
156
+ let offset, count;
157
+ if (shared) {
158
+ do {
159
+ offset = Atomics.load(shared.control, 0);
160
+ count = Math.min(len, stdinBytes.length - offset);
161
+ } while (Atomics.compareExchange(shared.control, 0, offset, offset + count) !== offset);
162
+ } else {
163
+ offset = stdinOffset;
164
+ count = Math.min(len, stdinBytes.length - offset);
165
+ stdinOffset += count;
166
+ }
167
+ if (count === 0) {
168
+ if (offset >= stdinBytes.length) break;
169
+ continue; // An empty iovec does not mean end of file.
170
+ }
171
+ bytes.set(stdinBytes.subarray(offset, offset + count), ptr);
132
172
  totalRead += count;
133
173
  }
134
174
 
@@ -243,6 +283,10 @@ export function createBrowserWasiShim(options = {}) {
243
283
  }
244
284
 
245
285
  function proc_exit(code) {
286
+ if (shared) {
287
+ Atomics.store(shared.control, 4, code);
288
+ Atomics.store(shared.control, 3, 1);
289
+ }
246
290
  if (logOutput) {
247
291
  flushOutput();
248
292
  }
@@ -317,10 +361,10 @@ export function createBrowserWasiShim(options = {}) {
317
361
  getMemory,
318
362
  flushOutput,
319
363
  get stdout() {
320
- return concatChunks(stdoutChunks);
364
+ return shared ? shared.stdout.slice(0, Atomics.load(shared.control, 1)) : concatChunks(stdoutChunks);
321
365
  },
322
366
  get stderr() {
323
- return concatChunks(stderrChunks);
367
+ return shared ? shared.stderr.slice(0, Atomics.load(shared.control, 2)) : concatChunks(stderrChunks);
324
368
  },
325
369
  };
326
370
  }
@@ -16,6 +16,7 @@ self.onmessage = (event) => {
16
16
  wasmModule,
17
17
  memory,
18
18
  hostcallChannel,
19
+ processState: message.processState,
19
20
  });
20
21
  runtime.instantiate();
21
22
  self.postMessage({ t: "ready", ok: true });
@@ -202,7 +202,7 @@ const BROWSER_POOL_PROBE_TIMEOUT_MS = 1500;
202
202
  // surfacing) for the life of the pool.
203
203
  function armBrowserPool(
204
204
  created,
205
- { wasmModule, memory, hostcallChannel, timeoutMs, onExit },
205
+ { wasmModule, memory, hostcallChannel, processState, timeoutMs, onExit },
206
206
  ) {
207
207
  return new Promise((resolve) => {
208
208
  let remaining = created.length;
@@ -266,6 +266,7 @@ function armBrowserPool(
266
266
  wasmModule,
267
267
  memory,
268
268
  hostcallChannel: hostcallChannel ?? null,
269
+ processState,
269
270
  });
270
271
  }
271
272
  });
@@ -333,6 +334,7 @@ export async function createWasiThreadSpawn({
333
334
  memory,
334
335
  requestedThreads,
335
336
  hostcallChannel,
337
+ processState,
336
338
  requiresHostcalls = false,
337
339
  enableBrowserThreads,
338
340
  browserWorkerBaseUrl,
@@ -367,17 +369,29 @@ export async function createWasiThreadSpawn({
367
369
  );
368
370
  const NodeWorker = workerThreads.Worker;
369
371
  const nodeWorkerUrl = new URL("./wasiThreadWorker.mjs", import.meta.url);
372
+ // An --input-type parent runs eval/stdin, but this worker loads a file.
373
+ // Inheriting that flag makes the worker fail while the guest is blocked
374
+ // synchronously in pthread_join and cannot receive the error event.
375
+ const parentArgs = globalThis.process.execArgv;
376
+ const sourceFlags = ["--input-type", "--eval", "-e", "--print", "-p"];
377
+ const execArgv = parentArgs.some((arg) => arg.startsWith("--input-type"))
378
+ ? parentArgs.filter((arg, index, args) =>
379
+ !sourceFlags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)) &&
380
+ !(index > 0 && sourceFlags.includes(args[index - 1])))
381
+ : undefined;
370
382
 
371
383
  const threadSpawn = (startArg) => {
372
384
  const tid = (nextTid += 1);
373
385
  try {
374
386
  const worker = new NodeWorker(nodeWorkerUrl, {
387
+ execArgv,
375
388
  workerData: {
376
389
  wasmModule,
377
390
  memory,
378
391
  tid,
379
392
  startArg,
380
393
  hostcallChannel: hostcallChannel ?? null,
394
+ processState,
381
395
  },
382
396
  });
383
397
  // Node exposes the OS-thread id per Worker — distinct ids are direct
@@ -491,6 +505,7 @@ export async function createWasiThreadSpawn({
491
505
  wasmModule,
492
506
  memory,
493
507
  hostcallChannel,
508
+ processState,
494
509
  timeoutMs: BROWSER_POOL_PROBE_TIMEOUT_MS,
495
510
  onExit: returnWorkerToIdle,
496
511
  });
@@ -12,11 +12,12 @@ import { workerData } from "node:worker_threads";
12
12
 
13
13
  import { createWasiThreadWorkerRuntime } from "./wasiThreadWorkerRuntime.js";
14
14
 
15
- const { wasmModule, memory, tid, startArg, hostcallChannel } = workerData;
15
+ const { wasmModule, memory, tid, startArg, hostcallChannel, processState } = workerData;
16
16
  const runtime = createWasiThreadWorkerRuntime({
17
17
  wasmModule,
18
18
  memory,
19
19
  hostcallChannel,
20
+ processState,
20
21
  });
21
22
  const instance = runtime.instantiate();
22
23
 
@@ -58,8 +58,9 @@ export function createWasiThreadWorkerRuntime({
58
58
  wasmModule,
59
59
  memory,
60
60
  hostcallChannel,
61
+ processState,
61
62
  } = {}) {
62
- const wasi = createBrowserWasiShim({});
63
+ const wasi = createBrowserWasiShim({ processState });
63
64
  wasi.setMemory(memory);
64
65
  let instance = null;
65
66
  let hostcalls = null;
@@ -0,0 +1,2 @@
1
+ // Compatibility for consumers of the former source path. Browser-safe only.
2
+ export * from "../host/browserModuleHarness.js";