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
@@ -0,0 +1,345 @@
1
+ // WASI preview1 command host for the SDK parity lanes. WasmEdge 0.16.4's
2
+ // CLI enables atomics but does not implement wasi.thread-spawn. Like the SDN
3
+ // Go host, use a fresh instance per worker, shared memory, and ONE executor:
4
+ // atomic.wait/notify waiters belong to the executor in this runtime revision.
5
+ #define _POSIX_C_SOURCE 200809L
6
+ #include <wasmedge/wasmedge.h>
7
+ #include <pthread.h>
8
+ #include <stdbool.h>
9
+ #include <stdint.h>
10
+ #include <stdio.h>
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+ #include <time.h>
14
+
15
+ #define MAX_WORKERS 32
16
+
17
+ typedef struct {
18
+ WasmEdge_ModuleInstanceContext *instance;
19
+ WasmEdge_Async *async;
20
+ } Worker;
21
+
22
+ typedef struct {
23
+ WasmEdge_ExecutorContext *executor;
24
+ WasmEdge_StoreContext *store;
25
+ WasmEdge_ASTModuleContext *ast;
26
+ pthread_mutex_t mutex;
27
+ Worker workers[MAX_WORKERS];
28
+ int32_t next_tid;
29
+ bool closed;
30
+ bool failed;
31
+ bool exited;
32
+ } Threads;
33
+
34
+ static WasmEdge_String name(const char *s) {
35
+ return WasmEdge_StringWrap(s, (uint32_t)strlen(s));
36
+ }
37
+
38
+ static uint64_t monotonic_ms(void) {
39
+ struct timespec now;
40
+ clock_gettime(CLOCK_MONOTONIC, &now);
41
+ return (uint64_t)now.tv_sec * 1000 + (uint64_t)now.tv_nsec / 1000000;
42
+ }
43
+
44
+ static bool wait_until(WasmEdge_Async *call, uint64_t deadline) {
45
+ uint64_t now = monotonic_ms();
46
+ return WasmEdge_AsyncWaitFor(call, now < deadline ? deadline - now : 0);
47
+ }
48
+
49
+ static void require_result(const char *step, WasmEdge_Result result) {
50
+ if (!WasmEdge_ResultOK(result)) {
51
+ fprintf(stderr, "wasm trap: %s: %s\n", step,
52
+ WasmEdge_ResultGetMessage(result));
53
+ exit(1);
54
+ }
55
+ }
56
+
57
+ // Called with the mutex held. Only release instances after execution ends.
58
+ static void reap(Threads *g) {
59
+ for (size_t i = 0; i < MAX_WORKERS; ++i) {
60
+ Worker *w = &g->workers[i];
61
+ if (!w->async || !WasmEdge_AsyncWaitFor(w->async, 0)) continue;
62
+ WasmEdge_Result result = WasmEdge_AsyncGet(w->async, NULL, 0);
63
+ if (WasmEdge_ResultGetCode(result) == WasmEdge_ErrCode_Terminated) {
64
+ g->exited = true;
65
+ g->closed = true;
66
+ } else if (!WasmEdge_ResultOK(result)) {
67
+ fprintf(stderr, "wasm trap: wasi worker: %s\n",
68
+ WasmEdge_ResultGetMessage(result));
69
+ g->failed = true;
70
+ g->closed = true;
71
+ }
72
+ WasmEdge_AsyncDelete(w->async);
73
+ WasmEdge_ModuleInstanceDelete(w->instance);
74
+ *w = (Worker){0};
75
+ }
76
+ }
77
+
78
+ static WasmEdge_Result thread_spawn(void *data,
79
+ const WasmEdge_CallingFrameContext *frame, const WasmEdge_Value *in,
80
+ WasmEdge_Value *out) {
81
+ Threads *g = data;
82
+ out[0] = WasmEdge_ValueGenI32(-6); // WASI EAGAIN: caller may retry or run inline.
83
+ pthread_mutex_lock(&g->mutex);
84
+ if (g->closed) goto done;
85
+ reap(g);
86
+ if (g->closed || g->next_tid >= (1 << 29)) goto done;
87
+ Worker *w = NULL;
88
+ for (size_t i = 0; i < MAX_WORKERS; ++i) {
89
+ if (!g->workers[i].async) { w = &g->workers[i]; break; }
90
+ }
91
+ if (!w) goto done;
92
+ WasmEdge_Result result = WasmEdge_ExecutorInstantiate(
93
+ WasmEdge_CallingFrameGetExecutor(frame), &w->instance, g->store, g->ast);
94
+ if (!WasmEdge_ResultOK(result)) goto discard;
95
+ const WasmEdge_FunctionInstanceContext *entry =
96
+ WasmEdge_ModuleInstanceFindFunction(w->instance, name("wasi_thread_start"));
97
+ if (!entry) goto discard;
98
+ int32_t tid = g->next_tid++;
99
+ WasmEdge_Value args[] = {WasmEdge_ValueGenI32(tid), in[0]};
100
+ w->async = WasmEdge_ExecutorAsyncInvoke(WasmEdge_CallingFrameGetExecutor(frame), entry, args, 2);
101
+ if (!w->async) goto discard;
102
+ out[0] = WasmEdge_ValueGenI32(tid);
103
+ goto done;
104
+ discard:
105
+ if (w->instance) WasmEdge_ModuleInstanceDelete(w->instance);
106
+ *w = (Worker){0};
107
+ done:
108
+ pthread_mutex_unlock(&g->mutex);
109
+ return WasmEdge_Result_Success;
110
+ }
111
+
112
+ static WasmEdge_ModuleInstanceContext *memory_import(Threads *g) {
113
+ uint32_t count = WasmEdge_ASTModuleListImportsLength(g->ast);
114
+ const WasmEdge_ImportTypeContext **imports = calloc(count, sizeof(*imports));
115
+ if (count && !imports) exit(1);
116
+ WasmEdge_ASTModuleListImports(g->ast, imports, count);
117
+ WasmEdge_ModuleInstanceContext *env = NULL;
118
+ bool threaded = false;
119
+ for (uint32_t i = 0; i < count; ++i) {
120
+ WasmEdge_String mod = WasmEdge_ImportTypeGetModuleName(imports[i]);
121
+ WasmEdge_String field = WasmEdge_ImportTypeGetExternalName(imports[i]);
122
+ if (WasmEdge_StringIsEqual(mod, name("wasi")) &&
123
+ WasmEdge_StringIsEqual(field, name("thread-spawn"))) threaded = true;
124
+ if (WasmEdge_ImportTypeGetExternalType(imports[i]) !=
125
+ WasmEdge_ExternalType_Memory) continue;
126
+ if (env || !WasmEdge_StringIsEqual(mod, name("env")) ||
127
+ !WasmEdge_StringIsEqual(field, name("memory"))) {
128
+ fprintf(stderr, "unsupported imported memory: expected one env.memory\n");
129
+ exit(1);
130
+ }
131
+ const WasmEdge_MemoryTypeContext *type =
132
+ WasmEdge_ImportTypeGetMemoryType(g->ast, imports[i]);
133
+ WasmEdge_Limit limit = WasmEdge_MemoryTypeGetLimit(type);
134
+ if (!limit.Shared || !limit.HasMax) {
135
+ fprintf(stderr, "WASI threads require bounded shared memory\n");
136
+ exit(1);
137
+ }
138
+ env = WasmEdge_ModuleInstanceCreate(name("env"));
139
+ WasmEdge_MemoryInstanceContext *memory = WasmEdge_MemoryInstanceCreate(type);
140
+ if (!env || !memory) exit(1);
141
+ WasmEdge_ModuleInstanceAddMemory(env, name("memory"), memory);
142
+ require_result("register memory", WasmEdge_ExecutorRegisterImport(
143
+ g->executor, g->store, env));
144
+ }
145
+ free(imports);
146
+ if (threaded && !env) {
147
+ fprintf(stderr, "WASI threads require imported shared memory\n");
148
+ exit(1);
149
+ }
150
+ return env;
151
+ }
152
+
153
+ // Reactor artifacts use the same PIV request/response bytes as commands. Stage
154
+ // the request through the public allocator, then monitor the direct call with
155
+ // the same worker-group lifecycle as _start. No guest ABI is reimplemented.
156
+ static uint32_t stage_direct_request(Threads *g,
157
+ WasmEdge_ModuleInstanceContext *instance,
158
+ WasmEdge_MemoryInstanceContext *memory, WasmEdge_Value args[3]) {
159
+ size_t size = 0, capacity = 4096;
160
+ uint8_t *input = malloc(capacity);
161
+ if (!input) exit(1);
162
+ for (;;) {
163
+ size += fread(input + size, 1, capacity - size, stdin);
164
+ if (ferror(stdin)) { fprintf(stderr, "failed to read stdin\n"); exit(1); }
165
+ if (feof(stdin)) break;
166
+ if (capacity >= (1U << 30)) { fprintf(stderr, "request exceeds 1 GiB\n"); exit(1); }
167
+ capacity *= 2;
168
+ uint8_t *grown = realloc(input, capacity);
169
+ if (!grown) exit(1);
170
+ input = grown;
171
+ }
172
+ const WasmEdge_FunctionInstanceContext *init =
173
+ WasmEdge_ModuleInstanceFindFunction(instance, name("_initialize"));
174
+ if (init) require_result("initialize", WasmEdge_ExecutorInvoke(g->executor, init, NULL, 0, NULL, 0));
175
+ const WasmEdge_FunctionInstanceContext *alloc =
176
+ WasmEdge_ModuleInstanceFindFunction(instance, name("plugin_alloc"));
177
+ if (!alloc || !memory) { fprintf(stderr, "direct module requires plugin_alloc and memory\n"); exit(1); }
178
+ uint32_t padded = ((uint32_t)size + 7U) & ~7U;
179
+ WasmEdge_Value allocation_size = WasmEdge_ValueGenI32((int32_t)(padded + 4));
180
+ WasmEdge_Value pointer;
181
+ require_result("allocate", WasmEdge_ExecutorInvoke(g->executor, alloc, &allocation_size, 1, &pointer, 1));
182
+ uint32_t ptr = (uint32_t)WasmEdge_ValueGetI32(pointer);
183
+ if (!ptr || ptr % 8) { fprintf(stderr, "invalid direct allocation\n"); exit(1); }
184
+ uint32_t length_ptr = ptr + padded;
185
+ const uint8_t zero[4] = {0};
186
+ require_result("copy request", WasmEdge_MemoryInstanceSetData(memory, input, ptr, (uint32_t)size));
187
+ require_result("clear response length", WasmEdge_MemoryInstanceSetData(memory, zero, length_ptr, 4));
188
+ free(input);
189
+ args[0] = pointer;
190
+ args[1] = WasmEdge_ValueGenI32((int32_t)size);
191
+ args[2] = WasmEdge_ValueGenI32((int32_t)length_ptr);
192
+ return length_ptr;
193
+ }
194
+
195
+ static bool write_direct_response(WasmEdge_MemoryInstanceContext *memory,
196
+ uint32_t ptr, uint32_t length_ptr) {
197
+ uint8_t bytes[4];
198
+ require_result("read response length", WasmEdge_MemoryInstanceGetData(memory, bytes, length_ptr, 4));
199
+ uint32_t length = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 |
200
+ (uint32_t)bytes[2] << 16 | (uint32_t)bytes[3] << 24;
201
+ if (!ptr || ptr % 8 || !length) {
202
+ fprintf(stderr, "invalid direct response\n");
203
+ return false;
204
+ }
205
+ const uint8_t *data = WasmEdge_MemoryInstanceGetPointerConst(memory, ptr, length);
206
+ if (!data) { fprintf(stderr, "direct response is out of bounds\n"); return false; }
207
+ return fwrite(data, 1, length, stdout) == length;
208
+ }
209
+
210
+ int main(int argc, char **argv) {
211
+ if (strcmp(WasmEdge_VersionGet(), "0.16.4") != 0) {
212
+ fprintf(stderr, "WASI threads runner requires WasmEdge 0.16.4; linked %s\n", WasmEdge_VersionGet());
213
+ return 2;
214
+ }
215
+ if (argc == 2 && strcmp(argv[1], "--version") == 0) {
216
+ printf("wasmedge version %s\nsdm-wasi-threads-runner 1\n",
217
+ WasmEdge_VersionGet());
218
+ return 0;
219
+ }
220
+ // WasmEdge's default logger writes to stdout. Guest stdout is binary; all
221
+ // host diagnostics below are emitted to stderr instead.
222
+ WasmEdge_LogOff();
223
+ const char **envs = calloc((size_t)argc, sizeof(*envs));
224
+ if (!envs) return 1;
225
+ uint32_t env_count = 0;
226
+ bool stats = false;
227
+ int first = 1;
228
+ for (; first < argc; ++first) {
229
+ if (strcmp(argv[first], "--enable-threads") == 0) continue;
230
+ if (strcmp(argv[first], "--sdm-thread-stats") == 0) { stats = true; continue; }
231
+ if (strcmp(argv[first], "--env") == 0 && first + 1 < argc) {
232
+ envs[env_count++] = argv[++first];
233
+ continue;
234
+ }
235
+ break;
236
+ }
237
+ if (first >= argc || argv[first][0] == '-') {
238
+ fprintf(stderr, "usage: %s [--env NAME=VALUE] module.wasm [args...]\n", argv[0]);
239
+ free(envs);
240
+ return 2;
241
+ }
242
+ WasmEdge_ConfigureContext *conf = WasmEdge_ConfigureCreate();
243
+ WasmEdge_ConfigureAddProposal(conf, WasmEdge_Proposal_Threads);
244
+ WasmEdge_LoaderContext *loader = WasmEdge_LoaderCreate(conf);
245
+ WasmEdge_ValidatorContext *validator = WasmEdge_ValidatorCreate(conf);
246
+ Threads g = {.executor = WasmEdge_ExecutorCreate(conf, NULL),
247
+ .store = WasmEdge_StoreCreate(), .next_tid = 1};
248
+ if (pthread_mutex_init(&g.mutex, NULL) != 0) return 1;
249
+ require_result("parse", WasmEdge_LoaderParseFromFile(loader, &g.ast, argv[first]));
250
+ require_result("validate", WasmEdge_ValidatorValidate(validator, g.ast));
251
+ WasmEdge_ModuleInstanceContext *wasi = WasmEdge_ModuleInstanceCreateWASI(
252
+ (const char *const *)(argv + first), (uint32_t)(argc - first),
253
+ envs, env_count, NULL, 0);
254
+ free(envs);
255
+ if (!wasi) return 1;
256
+ require_result("register WASI", WasmEdge_ExecutorRegisterImport(
257
+ g.executor, g.store, wasi));
258
+ WasmEdge_ModuleInstanceContext *env = memory_import(&g);
259
+ WasmEdge_ModuleInstanceContext *threads = WasmEdge_ModuleInstanceCreate(name("wasi"));
260
+ WasmEdge_ValType i32 = WasmEdge_ValTypeGenI32();
261
+ WasmEdge_FunctionTypeContext *type = WasmEdge_FunctionTypeCreate(&i32, 1, &i32, 1);
262
+ WasmEdge_FunctionInstanceContext *spawn = WasmEdge_FunctionInstanceCreate(
263
+ type, thread_spawn, &g, 0);
264
+ WasmEdge_FunctionTypeDelete(type);
265
+ if (!threads || !spawn) return 1;
266
+ WasmEdge_ModuleInstanceAddFunction(threads, name("thread-spawn"), spawn);
267
+ require_result("register wasi threads", WasmEdge_ExecutorRegisterImport(
268
+ g.executor, g.store, threads));
269
+ WasmEdge_ModuleInstanceContext *instance = NULL;
270
+ require_result("instantiate", WasmEdge_ExecutorInstantiate(
271
+ g.executor, &instance, g.store, g.ast));
272
+ const WasmEdge_FunctionInstanceContext *start =
273
+ WasmEdge_ModuleInstanceFindFunction(instance, name("_start"));
274
+ bool direct = start == NULL;
275
+ WasmEdge_Value direct_args[3], response_pointer;
276
+ uint32_t response_length_ptr = 0;
277
+ WasmEdge_MemoryInstanceContext *memory = env
278
+ ? WasmEdge_ModuleInstanceFindMemory(env, name("memory"))
279
+ : WasmEdge_ModuleInstanceFindMemory(instance, name("memory"));
280
+ if (direct) {
281
+ start = WasmEdge_ModuleInstanceFindFunction(instance, name("plugin_invoke_stream"));
282
+ if (!start) { fprintf(stderr, "module has neither _start nor plugin_invoke_stream\n"); return 1; }
283
+ response_length_ptr = stage_direct_request(&g, instance, memory, direct_args);
284
+ }
285
+ WasmEdge_Async *main_call = WasmEdge_ExecutorAsyncInvoke(g.executor, start,
286
+ direct ? direct_args : NULL, direct ? 3 : 0);
287
+ if (!main_call) return 1;
288
+ for (;;) {
289
+ bool finished = WasmEdge_AsyncWaitFor(main_call, 1);
290
+ pthread_mutex_lock(&g.mutex);
291
+ reap(&g);
292
+ bool stop = finished || g.failed || g.exited;
293
+ if (stop) g.closed = true;
294
+ pthread_mutex_unlock(&g.mutex);
295
+ if (stop) break;
296
+ }
297
+ // Exit/trap in any thread ends the whole command. Cancel all executions
298
+ // before releasing shared memory; a stuck cancellation ends this process
299
+ // without freeing objects underneath live native workers.
300
+ uint64_t deadline = monotonic_ms() + 2000;
301
+ if (!WasmEdge_AsyncWaitFor(main_call, 0)) WasmEdge_AsyncCancel(main_call);
302
+ pthread_mutex_lock(&g.mutex);
303
+ for (size_t i = 0; i < MAX_WORKERS; ++i) {
304
+ if (g.workers[i].async) WasmEdge_AsyncCancel(g.workers[i].async);
305
+ }
306
+ pthread_mutex_unlock(&g.mutex);
307
+ if (!wait_until(main_call, deadline)) {
308
+ fprintf(stderr, "wasm trap: main cancellation timed out\n");
309
+ fflush(NULL);
310
+ _Exit(1);
311
+ }
312
+ WasmEdge_Result result = WasmEdge_AsyncGet(main_call,
313
+ direct ? &response_pointer : NULL, direct ? 1 : 0);
314
+ bool failed = g.failed || (!WasmEdge_ResultOK(result) && !g.exited);
315
+ if (failed) fprintf(stderr, "wasm trap: command: %s\n", WasmEdge_ResultGetMessage(result));
316
+ for (size_t i = 0; i < MAX_WORKERS; ++i) {
317
+ Worker *w = &g.workers[i];
318
+ if (!w->async) continue;
319
+ if (!wait_until(w->async, deadline)) {
320
+ fprintf(stderr, "wasm trap: worker cancellation timed out\n");
321
+ fflush(NULL);
322
+ _Exit(1);
323
+ }
324
+ WasmEdge_AsyncDelete(w->async);
325
+ WasmEdge_ModuleInstanceDelete(w->instance);
326
+ }
327
+ if (stats) fprintf(stderr, "sdm-wasi-threads: spawned=%d\n", g.next_tid - 1);
328
+ if (direct && !failed && !g.exited) {
329
+ failed = !write_direct_response(memory, (uint32_t)WasmEdge_ValueGetI32(response_pointer), response_length_ptr);
330
+ }
331
+ int exit_code = failed ? 1 : (int)WasmEdge_ModuleInstanceWASIGetExitCode(wasi);
332
+ WasmEdge_AsyncDelete(main_call);
333
+ WasmEdge_ModuleInstanceDelete(instance);
334
+ WasmEdge_ModuleInstanceDelete(threads);
335
+ if (env) WasmEdge_ModuleInstanceDelete(env);
336
+ WasmEdge_ModuleInstanceDelete(wasi);
337
+ WasmEdge_StoreDelete(g.store);
338
+ WasmEdge_ASTModuleDelete(g.ast);
339
+ WasmEdge_ExecutorDelete(g.executor);
340
+ WasmEdge_ValidatorDelete(validator);
341
+ WasmEdge_LoaderDelete(loader);
342
+ WasmEdge_ConfigureDelete(conf);
343
+ pthread_mutex_destroy(&g.mutex);
344
+ return exit_code;
345
+ }
@@ -91,6 +91,7 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
91
91
  let stdout = new Uint8Array(0);
92
92
  let stderr = new Uint8Array(0);
93
93
  let harness = null;
94
+ let spawnCount = null;
94
95
  try {
95
96
  // Plaintext hygiene: no per-case copy. createBrowserModuleHarness
96
97
  // compiles directly from a caller-supplied buffer without copying it
@@ -101,7 +102,10 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
101
102
  // would only add a fresh, unzeroed plaintext copy per run.
102
103
  harness = await createBrowserModuleHarness({
103
104
  wasmSource: moduleBytes,
104
- surface: "command",
105
+ surface: plan.surface ?? "command",
106
+ enableBrowserWasiThreads: true,
107
+ maxThreads: threadCount,
108
+ wasiThreadWorkerUrl: new URL("/wasi-thread-worker.js", globalThis.location.href).href,
105
109
  args: planCase.args,
106
110
  env: {
107
111
  ...planCase.env,
@@ -113,8 +117,11 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
113
117
  } catch (error) {
114
118
  ({ exitClass, exitDetail } = classifyBrowserError(error));
115
119
  } finally {
120
+ stderr = harness?.wasi?.stderr ?? stderr;
121
+ if (exitClass !== OK) stdout = harness?.wasi?.stdout ?? stdout;
122
+ spawnCount = harness?.threadHost?.spawnCount() ?? null;
116
123
  try {
117
- harness?.destroy?.();
124
+ await harness?.destroy?.();
118
125
  } catch {
119
126
  /* teardown must never mask the run result */
120
127
  }
@@ -124,6 +131,7 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
124
131
  threadCount,
125
132
  exitClass,
126
133
  exitDetail,
134
+ spawnCount,
127
135
  stdoutBase64: bytesToBase64(stdout),
128
136
  stderrBase64: bytesToBase64(stderr),
129
137
  });
@@ -144,8 +152,8 @@ async function postDone(body) {
144
152
  // Auto-run only inside a real browser page (the served runner). Node imports
145
153
  // of this module (tests, bundlers) never auto-run.
146
154
  if (
147
- typeof globalThis.document !== "undefined" &&
148
- typeof globalThis.window !== "undefined" &&
155
+ typeof globalThis.self !== "undefined" &&
156
+ typeof globalThis.location !== "undefined" &&
149
157
  globalThis.__SDM_PARITY_AUTORUN__ !== false
150
158
  ) {
151
159
  runParityPlanInBrowser({})
@@ -71,14 +71,18 @@ export function loadWasmEdgePin() {
71
71
  if (!repository) {
72
72
  throw new Error("wasmedgePin.json dockerImageRepository is required.");
73
73
  }
74
+ const runnerIdentity = createHash("sha256")
75
+ .update(readFileSync(path.join(__dirname, "native/wasmedge_wasi_threads_runner.c")))
76
+ .update(readFileSync(path.join(__dirname, "native/wasmedge-0.16.4-atomic-wait.patch")))
77
+ .update(readFileSync(path.join(__dirname, String(pin.dockerfile))))
78
+ .digest("hex").slice(0, 16);
74
79
  return Object.freeze({
75
80
  wasmedgeVersion: version,
76
81
  dockerImageRepository: repository,
77
82
  dockerImage: `${repository}:${version}`,
83
+ dockerRunnerImage: `${repository}:${version}-threads-${runnerIdentity}`,
78
84
  dockerfilePath: path.join(__dirname, String(pin.dockerfile)),
79
- dockerfileContextDir: path.dirname(
80
- path.join(__dirname, String(pin.dockerfile)),
81
- ),
85
+ dockerfileContextDir: __dirname,
82
86
  pinPath,
83
87
  });
84
88
  }
@@ -550,6 +554,9 @@ export async function runParityHarness(options = {}) {
550
554
  log: options.log ?? (() => {}),
551
555
  chromeBinary: options.chromeBinary,
552
556
  wasmedgeBinary: options.wasmedgeBinary,
557
+ wasmEdgeRunnerBinary: options.wasmEdgeRunnerBinary,
558
+ browserSurface: options.browserSurface ??
559
+ (WebAssembly.Module.exports(await WebAssembly.compile(loadableBytes)).some((entry) => entry.name === "_start") ? "command" : "direct"),
553
560
  dockerBinary: options.dockerBinary,
554
561
  dockerPlatform: options.dockerPlatform,
555
562
  autoBuildDockerImage: options.autoBuildDockerImage !== false,
@@ -609,6 +616,7 @@ export async function runParityHarness(options = {}) {
609
616
  threadCount: run.threadCount,
610
617
  exitClass: run.exitClass,
611
618
  exitDetail: run.exitDetail ?? null,
619
+ spawnCount: run.spawnCount ?? null,
612
620
  stdoutSha256: sha256Hex(toUint8Array(run.stdout) ?? new Uint8Array(0)),
613
621
  stdoutLength: (toUint8Array(run.stdout) ?? new Uint8Array(0)).length,
614
622
  })),
@@ -30,6 +30,8 @@ import { promisify } from "node:util";
30
30
 
31
31
  import { ExitClass, assertWasmEdgeVersionMatchesPin } from "./parityHarness.js";
32
32
  import { normalizeWasmEdgeOutcome } from "./wasmedgeOutput.js";
33
+ import { isWasiThreadsModule } from "../host/wasiThreadHost.js";
34
+ import { resolveWasmEdgeWasiThreadsRunner } from "./buildWasmEdgeRunner.js";
33
35
 
34
36
  const execFile = promisify(execFileCallback);
35
37
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -162,7 +164,10 @@ export async function resolveWasmEdgeBinary(context = {}) {
162
164
  }
163
165
 
164
166
  export async function runNativeWasmEdgeLane(context) {
165
- const binary = await resolveWasmEdgeBinary(context);
167
+ const threaded = isWasiThreadsModule(await WebAssembly.compile(context.loadableBytes ?? context.wasmBytes));
168
+ const binary = threaded
169
+ ? await resolveWasmEdgeWasiThreadsRunner(context)
170
+ : await resolveWasmEdgeBinary(context);
166
171
  let versionOutput;
167
172
  try {
168
173
  versionOutput = (await execFile(binary, ["--version"])).stdout;
@@ -185,7 +190,8 @@ export async function runNativeWasmEdgeLane(context) {
185
190
  for (const threadCount of planCase.threadCounts) {
186
191
  const outcome = await spawnWithStdin(
187
192
  binary,
188
- wasmedgeInvocationArgs(planCase, context.plan, threadCount),
193
+ [...(threaded ? ["--sdm-thread-stats"] : []),
194
+ ...wasmedgeInvocationArgs(planCase, context.plan, threadCount)],
189
195
  {
190
196
  cwd: workdir,
191
197
  env: { PATH: process.env.PATH ?? "" },
@@ -208,6 +214,7 @@ export async function runNativeWasmEdgeLane(context) {
208
214
  exitDetail,
209
215
  stdout: normalized.stdout,
210
216
  stderr: normalized.stderr,
217
+ spawnCount: threaded ? Number(/sdm-wasi-threads: spawned=(\d+)/.exec(normalized.diagnosticText)?.[1] ?? NaN) : null,
211
218
  stateFiles: null,
212
219
  });
213
220
  }
@@ -271,6 +278,8 @@ export async function ensureDockerParityImage(context) {
271
278
  }
272
279
 
273
280
  export async function runDockerWasmEdgeLane(context) {
281
+ const threaded = isWasiThreadsModule(await WebAssembly.compile(context.loadableBytes ?? context.wasmBytes));
282
+ if (threaded) context = { ...context, pin: { ...context.pin, dockerImage: context.pin.dockerRunnerImage } };
274
283
  const dockerBinary = context.dockerBinary ?? "docker";
275
284
  try {
276
285
  await execFile(dockerBinary, ["--version"]);
@@ -300,8 +309,10 @@ export async function runDockerWasmEdgeLane(context) {
300
309
  if (context.dockerPlatform) {
301
310
  dockerArgs.push("--platform", String(context.dockerPlatform));
302
311
  }
312
+ if (threaded) dockerArgs.push("--entrypoint", "/opt/wasmedge/bin/sdm-wasi-threads-runner");
303
313
  dockerArgs.push(
304
314
  image,
315
+ ...(threaded ? ["--sdm-thread-stats"] : []),
305
316
  ...wasmedgeInvocationArgs(planCase, context.plan, threadCount),
306
317
  );
307
318
  const outcome = await spawnWithStdin(dockerBinary, dockerArgs, {
@@ -323,6 +334,7 @@ export async function runDockerWasmEdgeLane(context) {
323
334
  exitDetail,
324
335
  stdout: normalized.stdout,
325
336
  stderr: normalized.stderr,
337
+ spawnCount: threaded ? Number(/sdm-wasi-threads: spawned=(\d+)/.exec(normalized.diagnosticText)?.[1] ?? NaN) : null,
326
338
  stateFiles: null,
327
339
  });
328
340
  }
@@ -373,11 +385,17 @@ async function buildBrowserRunnerBundle() {
373
385
  external: ["node:*", "hd-wallet-wasm"],
374
386
  logLevel: "silent",
375
387
  });
376
- return result.outputFiles[0].text;
388
+ const threadWorker = await esbuild.build({
389
+ entryPoints: [path.join(__dirname, "../host/wasiThreadBrowserWorker.mjs")],
390
+ bundle: true, write: false, format: "esm", platform: "browser", target: ["chrome110"],
391
+ external: ["node:*", "hd-wallet-wasm"], logLevel: "silent",
392
+ });
393
+ return { runner: result.outputFiles[0].text, threadWorker: threadWorker.outputFiles[0].text };
377
394
  }
378
395
 
379
396
  function browserPlanPayload(context) {
380
397
  return JSON.stringify({
398
+ surface: context.browserSurface ?? "command",
381
399
  threadEnvVar: context.plan.threadEnvVar,
382
400
  cases: context.plan.cases.map((planCase) => ({
383
401
  id: planCase.id,
@@ -393,7 +411,7 @@ const RUNNER_HTML = `<!doctype html>
393
411
  <html>
394
412
  <head><meta charset="utf-8"><title>sdm parity runner</title></head>
395
413
  <body><pre id="status">parity runner booting…</pre>
396
- <script type="module" src="/runner.js"></script></body>
414
+ <script type="module">new Worker("/runner.js", { type: "module" });</script></body>
397
415
  </html>`;
398
416
 
399
417
  export async function runBrowserLane(context) {
@@ -447,7 +465,12 @@ export async function runBrowserLane(context) {
447
465
  ...securityHeaders,
448
466
  "Content-Type": "text/javascript; charset=utf-8",
449
467
  });
450
- response.end(runnerBundle);
468
+ response.end(runnerBundle.runner);
469
+ return;
470
+ }
471
+ if (url.pathname === "/wasi-thread-worker.js") {
472
+ response.writeHead(200, { ...securityHeaders, "Content-Type": "text/javascript; charset=utf-8" });
473
+ response.end(runnerBundle.threadWorker);
451
474
  return;
452
475
  }
453
476
  if (url.pathname === "/plan") {
@@ -522,6 +545,7 @@ export async function runBrowserLane(context) {
522
545
  threadCount: Number(run.threadCount),
523
546
  exitClass: String(run.exitClass),
524
547
  exitDetail: run.exitDetail ?? null,
548
+ spawnCount: run.spawnCount ?? null,
525
549
  stdout: new Uint8Array(Buffer.from(String(run.stdoutBase64 ?? ""), "base64")),
526
550
  stderr: new Uint8Array(Buffer.from(String(run.stderrBase64 ?? ""), "base64")),
527
551
  stateFiles: null,