space-data-module-sdk 0.8.18 → 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 (65) 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/flow/flowCompiler.js +34 -23
  50. package/src/host/browserModuleHarness.js +39 -35
  51. package/src/host/isomorphicLoader.js +13 -6
  52. package/src/host/wasiShim.js +57 -13
  53. package/src/host/wasiThreadBrowserWorker.mjs +1 -0
  54. package/src/host/wasiThreadHost.js +16 -1
  55. package/src/host/wasiThreadWorker.mjs +2 -1
  56. package/src/host/wasiThreadWorkerRuntime.js +2 -1
  57. package/src/testing/browserModuleHarness.js +2 -0
  58. package/src/testing/buildWasmEdgeRunner.js +147 -4
  59. package/src/testing/docker/wasmedge-parity.Dockerfile +23 -1
  60. package/src/testing/isomorphicHarness.js +5 -0
  61. package/src/testing/native/wasmedge-0.16.4-atomic-wait.patch +111 -0
  62. package/src/testing/native/wasmedge_wasi_threads_runner.c +345 -0
  63. package/src/testing/parityBrowserRunner.js +12 -4
  64. package/src/testing/parityHarness.js +11 -3
  65. 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.18",
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",
@@ -52,6 +52,8 @@ import { runWithEmceptionLock } from "../compiler/emceptionNode.js";
52
52
  import {
53
53
  assertPthreadArtifact,
54
54
  assertPthreadFlagsPresent,
55
+ assertSequentialArtifact,
56
+ assertSequentialFlagsAbsent,
55
57
  PTHREAD_FINAL_LINK_FLAGS,
56
58
  } from "../compiler/pthreadArtifactGuard.js";
57
59
  import { resolveWasiThreadsToolchain } from "../compiler/wasiThreadsToolchain.js";
@@ -96,12 +98,11 @@ const execFileAsync = promisify(execFile);
96
98
  const FLOW_THREAD_MODEL = Object.freeze({
97
99
  SINGLE_THREAD: "single-thread",
98
100
  WASI_THREADS: "wasi-threads",
101
+ WASI_SEQUENTIAL: "wasi-sequential",
99
102
  });
100
103
  const HISTORICAL_PTHREAD_THREAD_MODEL = "emscripten-pthreads";
101
- // A guest built by the clang wasi toolchain that provably never spawns a
102
- // thread. For LINKING purposes it is a wasi-threads object — same target, same
103
- // object format, same ABI — so it unions with wasi-threads rather than
104
- // conflicting with it. See normalizeGuestThreadModel.
104
+ // Sequential and threaded WASI objects can link together. Preserve the
105
+ // distinction until composition: an all-sequential flow must own its memory and not claim thread spawning.
105
106
  const WASI_SEQUENTIAL_THREAD_MODEL = "wasi-sequential";
106
107
 
107
108
  const RUNTIME_ABI_EXPORTS = [
@@ -762,18 +763,12 @@ function normalizeGuestThreadModel(value) {
762
763
  if (normalized === FLOW_THREAD_MODEL.SINGLE_THREAD) {
763
764
  return FLOW_THREAD_MODEL.SINGLE_THREAD;
764
765
  }
766
+ if (normalized === WASI_SEQUENTIAL_THREAD_MODEL) {
767
+ return FLOW_THREAD_MODEL.WASI_SEQUENTIAL;
768
+ }
765
769
  if (
766
770
  normalized === FLOW_THREAD_MODEL.WASI_THREADS ||
767
- normalized === HISTORICAL_PTHREAD_THREAD_MODEL ||
768
- // wasi-sequential collapses to wasi-threads HERE, at the linking layer,
769
- // and only here. The distinction is a property of a guest (may it spawn?),
770
- // not of the object format: both are clang wasm32-wasip1-threads objects
771
- // and link together cleanly. Keeping them separate at this layer would
772
- // make a flow of one sequential node fall through to the emception branch
773
- // and be linked by emcc — exactly the toolchain drift this model exists to
774
- // prevent. The per-guest guarantee is enforced where it belongs: on each
775
- // MODULE artifact, by assertSequentialArtifact.
776
- normalized === WASI_SEQUENTIAL_THREAD_MODEL
771
+ normalized === HISTORICAL_PTHREAD_THREAD_MODEL
777
772
  ) {
778
773
  return FLOW_THREAD_MODEL.WASI_THREADS;
779
774
  }
@@ -1941,6 +1936,9 @@ export function checkFlowProgram({ flow, dependencies = new Map() } = {}) {
1941
1936
  }
1942
1937
  }
1943
1938
 
1939
+ if (threadModels.has(FLOW_THREAD_MODEL.WASI_THREADS)) {
1940
+ threadModels.delete(FLOW_THREAD_MODEL.WASI_SEQUENTIAL);
1941
+ }
1944
1942
  let threadModel =
1945
1943
  threadModels.size === 1
1946
1944
  ? [...threadModels][0]
@@ -2245,6 +2243,12 @@ export function buildFlowModuleManifest({ flow, check, dependencies }) {
2245
2243
  name: flow.name ?? flow.programId,
2246
2244
  version: flow.version ?? "0.0.0",
2247
2245
  pluginFamily: "flow",
2246
+ ...(check.threadModel === FLOW_THREAD_MODEL.WASI_SEQUENTIAL
2247
+ ? { sequentialJustification: {
2248
+ kind: "caller-level-parallelism",
2249
+ detail: "Every linked guest is wasi-sequential; the flow dispatches them serially and delegates external concurrency to its host.",
2250
+ } }
2251
+ : {}),
2248
2252
  description:
2249
2253
  flow.description ??
2250
2254
  `Compiled SDN flow "${flow.programId}" (linked-direct monolithic artifact).`,
@@ -2941,6 +2945,7 @@ async function linkFlowArtifactWithWasiThreads({
2941
2945
  programSource,
2942
2946
  dependencyObjects,
2943
2947
  engineLinked = false,
2948
+ sequential = false,
2944
2949
  }) {
2945
2950
  const toolchain = resolveWasiThreadsToolchain();
2946
2951
  const workDir = await mkdtemp(
@@ -2975,9 +2980,8 @@ async function linkFlowArtifactWithWasiThreads({
2975
2980
  "-std=c++17",
2976
2981
  `-I${workDir}`,
2977
2982
  "-O3",
2978
- "-matomics",
2983
+ ...(sequential ? [] : ["-matomics", "-pthread"]),
2979
2984
  "-mbulk-memory",
2980
- "-pthread",
2981
2985
  "-fno-exceptions",
2982
2986
  "-DNDEBUG",
2983
2987
  ];
@@ -3010,16 +3014,20 @@ async function linkFlowArtifactWithWasiThreads({
3010
3014
  ...dependencyObjectPaths,
3011
3015
  "-O3",
3012
3016
  "-mexec-model=reactor",
3013
- ...PTHREAD_FINAL_LINK_FLAGS,
3017
+ ...(sequential
3018
+ ? ["-mbulk-memory", "-Wl,--initial-memory=16777216", "-Wl,--max-memory=2147483648"]
3019
+ : PTHREAD_FINAL_LINK_FLAGS),
3014
3020
  "-Wl,--export-memory",
3015
3021
  "-Wl,--allow-undefined",
3016
3022
  ...exportSymbols.map((symbol) => `-Wl,--export=${symbol}`),
3017
3023
  "-o",
3018
3024
  outputPath,
3019
3025
  ];
3020
- assertPthreadFlagsPresent(linkArgs, {
3021
- threadModel: HISTORICAL_PTHREAD_THREAD_MODEL,
3022
- });
3026
+ if (sequential) {
3027
+ assertSequentialFlagsAbsent(linkArgs, { threadModel: WASI_SEQUENTIAL_THREAD_MODEL });
3028
+ } else {
3029
+ assertPthreadFlagsPresent(linkArgs, { threadModel: HISTORICAL_PTHREAD_THREAD_MODEL });
3030
+ }
3023
3031
  await runWasiThreadsCompiler(toolchain.clangxx, linkArgs, workDir);
3024
3032
  return new Uint8Array(await readFile(outputPath));
3025
3033
  } finally {
@@ -3118,7 +3126,7 @@ export async function compileFlowProgram({
3118
3126
  });
3119
3127
 
3120
3128
  const linkFlowArtifact =
3121
- check.threadModel === FLOW_THREAD_MODEL.WASI_THREADS
3129
+ check.threadModel !== FLOW_THREAD_MODEL.SINGLE_THREAD
3122
3130
  ? linkFlowArtifactWithWasiThreads
3123
3131
  : linkFlowArtifactWithEmception;
3124
3132
  let wasmBytes = await linkFlowArtifact({
@@ -3129,6 +3137,7 @@ export async function compileFlowProgram({
3129
3137
  programSource,
3130
3138
  dependencyObjects: linkedDependencies.map((dependency) => dependency.guestLink.objectBytes),
3131
3139
  engineLinked: Boolean(check.engineLinkage),
3140
+ sequential: check.threadModel === FLOW_THREAD_MODEL.WASI_SEQUENTIAL,
3132
3141
  });
3133
3142
  wasmBytes = appendWasmCustomSection(
3134
3143
  wasmBytes,
@@ -3137,6 +3146,8 @@ export async function compileFlowProgram({
3137
3146
  );
3138
3147
  if (check.threadModel === FLOW_THREAD_MODEL.WASI_THREADS) {
3139
3148
  assertPthreadArtifact(wasmBytes, { source: sourceName });
3149
+ } else if (check.threadModel === FLOW_THREAD_MODEL.WASI_SEQUENTIAL) {
3150
+ assertSequentialArtifact(wasmBytes, { source: sourceName });
3140
3151
  }
3141
3152
 
3142
3153
  const report = await validateArtifactWithStandards({
@@ -3193,8 +3204,8 @@ export async function compileFlowProgram({
3193
3204
  name: flow.name ?? null,
3194
3205
  version: flow.version ?? null,
3195
3206
  compiler:
3196
- check.threadModel === FLOW_THREAD_MODEL.WASI_THREADS
3197
- ? "space-data-module flow compile (isomorphic, wasi-threads)"
3207
+ check.threadModel !== FLOW_THREAD_MODEL.SINGLE_THREAD
3208
+ ? `space-data-module flow compile (isomorphic, ${check.threadModel})`
3198
3209
  : "space-data-module flow compile (isomorphic, emception)",
3199
3210
  threadModel: check.threadModel,
3200
3211
  nodes: (flow.nodes ?? []).length,
@@ -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