space-data-module-sdk 0.8.20 → 0.8.22

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 (35) hide show
  1. package/docs/isomorphic-pthreads.html +100 -0
  2. package/docs/isomorphic-pthreads.md +335 -0
  3. package/package.json +7 -2
  4. package/src/browser.js +6 -0
  5. package/src/compiler/invokeGlue.js +38 -0
  6. package/src/flow/flatsqlLinkShim.js +295 -0
  7. package/src/flow/index.d.ts +31 -0
  8. package/src/flow/index.js +8 -0
  9. package/src/host/browserCapabilityProbe.js +251 -0
  10. package/src/host/browserModuleHarness.js +24 -5
  11. package/src/host/flatsqlIo.js +14 -0
  12. package/src/host/flatsqlIoConformance.js +349 -0
  13. package/src/host/flatsqlIoContract.js +240 -0
  14. package/src/host/flatsqlIoImports.js +335 -0
  15. package/src/host/flatsqlIoMemoryBackend.js +178 -0
  16. package/src/host/flatsqlIoServer.js +960 -0
  17. package/src/host/flatsqlIoWorkers.js +323 -0
  18. package/src/host/hostWorkerBundleSources.js +11 -0
  19. package/src/host/hostWorkerBundles.js +79 -0
  20. package/src/host/index.js +9 -0
  21. package/src/host/isomorphicLoader.js +10 -2
  22. package/src/host/nodeSyncFsIo.js +633 -0
  23. package/src/host/opfsIoBackend.js +280 -0
  24. package/src/host/opfsIoWorker.mjs +223 -0
  25. package/src/host/sabIoChannel.js +848 -0
  26. package/src/host/sabIoMirror.js +306 -0
  27. package/src/host/wasiThreadBrowserWorker.mjs +21 -11
  28. package/src/host/wasiThreadHost.js +261 -43
  29. package/src/host/wasiThreadWorker.mjs +46 -8
  30. package/src/host/wasiThreadWorkerRuntime.js +54 -0
  31. package/src/index.d.ts +349 -0
  32. package/src/runtime/constants.js +6 -0
  33. package/src/testing/native/wasmedge_wasi_threads_runner.c +13 -5
  34. package/src/testing/parityHarness.js +7 -1
  35. package/src/testing/parityLanes.js +19 -0
@@ -302,6 +302,43 @@ function renderMethodDescriptors(methods) {
302
302
  .join("\n");
303
303
  }
304
304
 
305
+ // Direct-surface initializer for command-model artifacts (wasi toolchains).
306
+ //
307
+ // A command artifact links crt1-command: its _start sets up the main-thread
308
+ // descriptor (__wasi_init_tp, threads sysroot) and runs the global
309
+ // constructors, then main. A host that serves the direct surface never enters
310
+ // _start, so without this entry namespace-scope C++ objects stay zero-filled
311
+ // and the main thread has no pthread descriptor. It is exported under the
312
+ // conventional __wasm_call_ctors name that direct hosts call when a module has
313
+ // no reactor _initialize. It runs once per instance and is a no-op on an
314
+ // instance whose constructors have already run.
315
+ const COMMAND_DIRECT_INITIALIZER_SOURCE = `#if defined(__wasi__)
316
+ extern "C" void __wasm_call_ctors(void);
317
+ extern "C" __attribute__((weak)) void __wasi_init_tp(void);
318
+
319
+ namespace {
320
+ // volatile: the optimizer would otherwise evaluate the marking constructor at
321
+ // compile time and emit the flag as already set.
322
+ volatile int g_sdm_constructors_ran = 0;
323
+
324
+ __attribute__((constructor)) void MarkConstructorsRan() {
325
+ __atomic_store_n(&g_sdm_constructors_ran, 1, __ATOMIC_SEQ_CST);
326
+ }
327
+ } // namespace
328
+
329
+ extern "C" __attribute__((export_name("__wasm_call_ctors")))
330
+ void sdm_command_direct_initialize(void) {
331
+ if (__atomic_exchange_n(&g_sdm_constructors_ran, 1, __ATOMIC_SEQ_CST) != 0) {
332
+ return;
333
+ }
334
+ if (__wasi_init_tp) {
335
+ __wasi_init_tp();
336
+ }
337
+ __wasm_call_ctors();
338
+ }
339
+ #endif
340
+ `;
341
+
305
342
  export function generateInvokeSupportSource({ manifest = {}, includeCommandMain = true } = {}) {
306
343
  const methods = Array.isArray(manifest.methods) ? manifest.methods : [];
307
344
  return `#include <algorithm>
@@ -1812,6 +1849,7 @@ extern "C" uint32_t plugin_invoke_stream(
1812
1849
  return response_ptr;
1813
1850
  }
1814
1851
 
1852
+ ${includeCommandMain ? COMMAND_DIRECT_INITIALIZER_SOURCE : ""}
1815
1853
  ${includeCommandMain
1816
1854
  ? `int main(int argc, char **argv) {
1817
1855
  const char *shortcut_method = nullptr;
@@ -122,6 +122,23 @@ const OP = {
122
122
  i64_mul: 0x7e,
123
123
  i64_xor: 0x85,
124
124
  void: 0x40,
125
+ unreachable: 0x00,
126
+ drop: 0x1a,
127
+ i32_eq: 0x46,
128
+ i32_ne: 0x47,
129
+ i32_gt_s: 0x4a,
130
+ i32_ge_s: 0x4e,
131
+ i64_eqz: 0x50,
132
+ };
133
+
134
+ // Threads-proposal opcodes (0xFE prefix) used by shim v2.
135
+ const ATOMIC = {
136
+ notify: [0xfe, 0x00],
137
+ wait32: [0xfe, 0x01],
138
+ i32_load: [0xfe, 0x10],
139
+ i32_store: [0xfe, 0x17],
140
+ i32_add: [0xfe, 0x1e],
141
+ i32_cmpxchg: [0xfe, 0x48],
125
142
  };
126
143
 
127
144
  const FNV_OFFSET_BASIS = 1469598103934665603n; // deployed decision-gate basis
@@ -302,3 +319,281 @@ export function readEngineRefEntry(view, base) {
302
319
  used: view.getUint32(base + 36, true),
303
320
  };
304
321
  }
322
+
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // Shim v2 (FlatSQL partition store T9, design §18 T9 and §20): a mailbox over
326
+ // IMPORTED LANE MEMORY.
327
+ //
328
+ // In the partition store a linked flow no longer calls engine functions: the
329
+ // engine is a set of multithreaded instances, and a query runs on a reader
330
+ // lane thread that serves a mailbox in the reader instance's shared memory.
331
+ // v2 imports that shared memory and gives the flow the mailbox protocol as
332
+ // direct in-wasm calls:
333
+ //
334
+ // mb_submit(mailbox, op, req_ptr, req_len) -> seq | -1 (mailbox busy)
335
+ // mb_poll(mailbox, seq) -> 1 when complete, else 0
336
+ // mb_wait(mailbox, seq, poll_ns, max_polls) -> the lane's status, or
337
+ // FLATSQL_LINK_PENDING (-110) after
338
+ // max_polls bounded waits
339
+ // mb_release(mailbox, seq) -> 0, or -1 when not complete
340
+ // mb_cancel(mailbox, seq) -> 0 (the lane polls the cancel word)
341
+ // load32_acquire(addr) / store32_release(addr, value)
342
+ // plus v1's peek8/peek32/peek64/poke8/poke32/fnv1a64/count_frames over the
343
+ // lane memory.
344
+ //
345
+ // POLLING-BOUNDED WAITS, NO RELIANCE ON CROSS-EXECUTOR NOTIFY. WasmEdge keeps
346
+ // atomic waiters per executor, and the flow and the reader instance run on
347
+ // different executors, so a lane's notify may never reach the flow's wait.
348
+ // mb_wait therefore never waits unboundedly: each wait is `poll_ns` long and
349
+ // every wake (notified or timed out) re-reads the mailbox, so a lost notify
350
+ // costs at most one poll interval and can never lose a completion. poll_ns = 0
351
+ // polls without executing memory.atomic.wait at all (contexts that may not
352
+ // block, such as a browser main thread). The shim still notifies the lane's
353
+ // doorbell on submit and cancel; that is only an accelerator.
354
+ //
355
+ // Mailbox (64 bytes, 8-aligned, in lane memory; little-endian):
356
+ // +0 state (IDLE 0, SUBMITTED 1, CLAIMED 2, DONE 3, SUBMITTING 4)
357
+ // +4 seq +8 done_seq +12 op +16 req_ptr +20 req_len
358
+ // +24 status +28 resp_ptr +32 resp_len +36 doorbell +40 cancel
359
+ // +44 flags +48 generation (u64) +56 reserved
360
+ // Client: IDLE -> SUBMITTING -> SUBMITTED (publishes the fields), waits for
361
+ // DONE with done_seq == seq, reads status/resp, then DONE -> IDLE (release).
362
+ // Lane: waits (bounded) on the doorbell, CAS SUBMITTED -> CLAIMED, runs the
363
+ // request, writes status/resp_ptr/resp_len/generation, stores done_seq = seq,
364
+ // stores DONE, and notifies the state word. A lane checks `cancel == seq`
365
+ // while it runs.
366
+
367
+ export const FLATSQL_LINK_MAILBOX_BYTES = 64;
368
+ export const FLATSQL_LINK_MAILBOX = Object.freeze({
369
+ STATE: 0,
370
+ SEQ: 4,
371
+ DONE_SEQ: 8,
372
+ OP: 12,
373
+ REQ_PTR: 16,
374
+ REQ_LEN: 20,
375
+ STATUS: 24,
376
+ RESP_PTR: 28,
377
+ RESP_LEN: 32,
378
+ DOORBELL: 36,
379
+ CANCEL: 40,
380
+ FLAGS: 44,
381
+ GENERATION: 48,
382
+ });
383
+ export const FLATSQL_LINK_STATE = Object.freeze({
384
+ IDLE: 0,
385
+ SUBMITTED: 1,
386
+ CLAIMED: 2,
387
+ DONE: 3,
388
+ SUBMITTING: 4,
389
+ });
390
+ /** mb_wait's "not complete yet" result: retryable, the request stays live. */
391
+ export const FLATSQL_LINK_PENDING = -110;
392
+ /** mb_submit's result when the mailbox holds another request. */
393
+ export const FLATSQL_LINK_BUSY = -1;
394
+
395
+ /** Assemble the v2 shim bytes (deterministic). */
396
+ export function buildFlatsqlLinkShimV2Wasm() {
397
+ const types = [
398
+ [0x60, ...vec([[I32]]), ...vec([[I32]])], // t0: (i32)->i32
399
+ [0x60, ...vec([[I32]]), ...vec([[I64]])], // t1: (i32)->i64
400
+ [0x60, ...vec([[I32], [I32]]), ...vec([])], // t2: (i32,i32)->()
401
+ [0x60, ...vec([[I32], [I32]]), ...vec([[I64]])], // t3: (i32,i32)->i64
402
+ [0x60, ...vec([[I32], [I32]]), ...vec([[I32]])], // t4: (i32,i32)->i32
403
+ [0x60, ...vec([[I32], [I32], [I32], [I32]]), ...vec([[I32]])], // t5: submit
404
+ [0x60, ...vec([[I32], [I32], [I64], [I32]]), ...vec([[I32]])], // t6: wait
405
+ ];
406
+
407
+ // (import "flatsql" "memory" (memory 0 65536 shared))
408
+ const imports = [
409
+ [
410
+ ...lebU(7), ...utf8("flatsql"),
411
+ ...lebU(6), ...utf8("memory"),
412
+ 0x02,
413
+ 0x03, ...lebU(0), ...lebU(65536),
414
+ ],
415
+ ];
416
+
417
+ const names = [
418
+ ["peek8", 0], ["peek32", 0], ["peek64", 1], ["poke8", 2], ["poke32", 2],
419
+ ["fnv1a64", 3], ["count_frames", 4],
420
+ ["mb_submit", 5], ["mb_poll", 4], ["mb_wait", 6], ["mb_release", 4],
421
+ ["mb_cancel", 4], ["load32_acquire", 0], ["store32_release", 2],
422
+ ];
423
+ const funcTypes = names.map(([, type]) => type);
424
+ const exports = names.map(([name], index) => [
425
+ ...lebU(name.length), ...utf8(name), 0x00, ...lebU(index),
426
+ ]);
427
+
428
+ const m = FLATSQL_LINK_MAILBOX;
429
+ const S = FLATSQL_LINK_STATE;
430
+ const aload = (offset) => [...ATOMIC.i32_load, ...lebU(2), ...lebU(offset)];
431
+ const astore = (offset) => [...ATOMIC.i32_store, ...lebU(2), ...lebU(offset)];
432
+ const store = (offset) => [OP.i32_store, ...lebU(2), ...lebU(offset)];
433
+ const load = (offset) => [OP.i32_load, ...lebU(2), ...lebU(offset)];
434
+
435
+ const peek8 = func([], [OP.local_get, 0, OP.i32_load8_u, ...memarg(0)]);
436
+ const peek32 = func([], [OP.local_get, 0, OP.i32_load, ...memarg(0)]);
437
+ const peek64 = func([], [OP.local_get, 0, OP.i64_load, ...memarg(0)]);
438
+ const poke8 = func([], [OP.local_get, 0, OP.local_get, 1, OP.i32_store8, ...memarg(0)]);
439
+ const poke32 = func([], [OP.local_get, 0, OP.local_get, 1, OP.i32_store, ...memarg(0)]);
440
+ const v1 = buildFlatsqlLinkShimWasm();
441
+ // fnv1a64 and count_frames are byte-for-byte v1's bodies (same algorithm,
442
+ // same code); lift them from the v1 code section.
443
+ const v1Bodies = extractCodeBodies(v1);
444
+ const fnv = v1Bodies[5];
445
+ const countFrames = v1Bodies[6];
446
+
447
+ // mb_submit(mb, op, req_ptr, req_len) -> seq | -1 ; local 4 = seq
448
+ const submit = func(
449
+ [[1, I32]],
450
+ [
451
+ OP.local_get, 0, OP.i32_const, S.IDLE, OP.i32_const, S.SUBMITTING,
452
+ ...ATOMIC.i32_cmpxchg, ...lebU(2), ...lebU(m.STATE),
453
+ OP.if, OP.void, OP.i32_const, ...lebS32(FLATSQL_LINK_BUSY), OP.return, OP.end,
454
+ OP.local_get, 0, ...aload(m.SEQ), OP.i32_const, 1, OP.i32_add, OP.local_set, 4,
455
+ OP.local_get, 4, OP.i32_eqz, OP.if, OP.void, OP.i32_const, 1, OP.local_set, 4, OP.end,
456
+ OP.local_get, 0, OP.local_get, 1, ...store(m.OP),
457
+ OP.local_get, 0, OP.local_get, 2, ...store(m.REQ_PTR),
458
+ OP.local_get, 0, OP.local_get, 3, ...store(m.REQ_LEN),
459
+ OP.local_get, 0, OP.i32_const, 0, ...store(m.STATUS),
460
+ OP.local_get, 0, OP.i32_const, 0, ...store(m.RESP_PTR),
461
+ OP.local_get, 0, OP.i32_const, 0, ...store(m.RESP_LEN),
462
+ OP.local_get, 0, OP.i32_const, 0, ...astore(m.CANCEL),
463
+ OP.local_get, 0, OP.local_get, 4, ...astore(m.SEQ),
464
+ OP.local_get, 0, OP.i32_const, S.SUBMITTED, ...astore(m.STATE),
465
+ OP.local_get, 0, OP.i32_const, 1, ...ATOMIC.i32_add, ...lebU(2), ...lebU(m.DOORBELL), OP.drop,
466
+ OP.local_get, 0, OP.i32_const, 1, ...ATOMIC.notify, ...lebU(2), ...lebU(m.DOORBELL), OP.drop,
467
+ OP.local_get, 4,
468
+ ],
469
+ );
470
+
471
+ // mb_poll(mb, seq) -> 1 | 0
472
+ const poll = func(
473
+ [],
474
+ [
475
+ OP.local_get, 0, ...aload(m.STATE), OP.i32_const, S.DONE, OP.i32_eq,
476
+ OP.local_get, 0, ...aload(m.DONE_SEQ), OP.local_get, 1, OP.i32_eq,
477
+ OP.i32_and,
478
+ ],
479
+ );
480
+
481
+ // mb_wait(mb, seq, poll_ns, max_polls) -> status | PENDING
482
+ // locals: 4 polls, 5 state
483
+ const wait = func(
484
+ [[2, I32]],
485
+ [
486
+ OP.loop, OP.void,
487
+ OP.local_get, 0, ...aload(m.STATE), OP.local_set, 5,
488
+ OP.local_get, 5, OP.i32_const, S.DONE, OP.i32_eq,
489
+ OP.local_get, 0, ...aload(m.DONE_SEQ), OP.local_get, 1, OP.i32_eq,
490
+ OP.i32_and,
491
+ OP.if, OP.void, OP.local_get, 0, ...load(m.STATUS), OP.return, OP.end,
492
+ OP.local_get, 3, OP.i32_const, 0, OP.i32_gt_s,
493
+ OP.local_get, 4, OP.local_get, 3, OP.i32_ge_s,
494
+ OP.i32_and,
495
+ OP.if, OP.void, OP.i32_const, ...lebS32(FLATSQL_LINK_PENDING), OP.return, OP.end,
496
+ OP.local_get, 4, OP.i32_const, 1, OP.i32_add, OP.local_set, 4,
497
+ OP.local_get, 2, OP.i64_eqz, OP.i32_eqz,
498
+ OP.if, OP.void,
499
+ OP.local_get, 0, OP.local_get, 5, OP.local_get, 2,
500
+ ...ATOMIC.wait32, ...lebU(2), ...lebU(m.STATE), OP.drop,
501
+ OP.end,
502
+ OP.br, 0,
503
+ OP.end,
504
+ OP.unreachable,
505
+ ],
506
+ );
507
+
508
+ // mb_release(mb, seq) -> 0 | -1
509
+ const release = func(
510
+ [],
511
+ [
512
+ OP.local_get, 0, ...aload(m.DONE_SEQ), OP.local_get, 1, OP.i32_ne,
513
+ OP.if, OP.void, OP.i32_const, ...lebS32(-1), OP.return, OP.end,
514
+ OP.local_get, 0, OP.i32_const, S.DONE, OP.i32_const, S.IDLE,
515
+ ...ATOMIC.i32_cmpxchg, ...lebU(2), ...lebU(m.STATE),
516
+ OP.i32_const, S.DONE, OP.i32_eq,
517
+ OP.if, I32, OP.i32_const, 0, 0x05 /* else */, OP.i32_const, ...lebS32(-1), OP.end,
518
+ ],
519
+ );
520
+
521
+ // mb_cancel(mb, seq) -> 0
522
+ const cancel = func(
523
+ [],
524
+ [
525
+ OP.local_get, 0, OP.local_get, 1, ...astore(m.CANCEL),
526
+ OP.local_get, 0, OP.i32_const, 1, ...ATOMIC.i32_add, ...lebU(2), ...lebU(m.DOORBELL), OP.drop,
527
+ OP.local_get, 0, OP.i32_const, 1, ...ATOMIC.notify, ...lebU(2), ...lebU(m.DOORBELL), OP.drop,
528
+ OP.i32_const, 0,
529
+ ],
530
+ );
531
+
532
+ const loadAcquire = func([], [OP.local_get, 0, ...aload(0)]);
533
+ const storeRelease = func([], [OP.local_get, 0, OP.local_get, 1, ...astore(0)]);
534
+
535
+ const bytes = [
536
+ 0x00, 0x61, 0x73, 0x6d,
537
+ 0x01, 0x00, 0x00, 0x00,
538
+ ...section(1, vec(types)),
539
+ ...section(2, vec(imports)),
540
+ ...section(3, vec(funcTypes.map((t) => lebU(t)))),
541
+ ...section(7, vec(exports)),
542
+ ...section(
543
+ 10,
544
+ vec([
545
+ peek8, peek32, peek64, poke8, poke32, fnv, countFrames,
546
+ submit, poll, wait, release, cancel, loadAcquire, storeRelease,
547
+ ]),
548
+ ),
549
+ ];
550
+ return new Uint8Array(bytes);
551
+ }
552
+
553
+ function readLebU(bytes, offset) {
554
+ let result = 0;
555
+ let shift = 0;
556
+ let at = offset;
557
+ for (;;) {
558
+ const byte = bytes[at];
559
+ at += 1;
560
+ result |= (byte & 0x7f) << shift;
561
+ if ((byte & 0x80) === 0) return [result >>> 0, at];
562
+ shift += 7;
563
+ }
564
+ }
565
+
566
+ // The code section's function bodies, each as its size-prefixed byte array.
567
+ function extractCodeBodies(wasm) {
568
+ let at = 8;
569
+ while (at < wasm.length) {
570
+ const id = wasm[at];
571
+ const [size, payload] = readLebU(wasm, at + 1);
572
+ if (id === 10) {
573
+ let [count, cursor] = readLebU(wasm, payload);
574
+ const bodies = [];
575
+ for (let i = 0; i < count; i += 1) {
576
+ const [bodySize, bodyStart] = readLebU(wasm, cursor);
577
+ bodies.push(Array.from(wasm.subarray(cursor, bodyStart + bodySize)));
578
+ cursor = bodyStart + bodySize;
579
+ }
580
+ return bodies;
581
+ }
582
+ at = payload + size;
583
+ }
584
+ throw new Error("no code section");
585
+ }
586
+
587
+ /** The v2 shim module bytes (assembled once at import). */
588
+ export const FLATSQL_LINK_SHIM_V2_WASM = buildFlatsqlLinkShimV2Wasm();
589
+
590
+ /**
591
+ * Instantiate shim v2 against a lane's shared memory (the reader instance's
592
+ * `WebAssembly.Memory`, shared).
593
+ */
594
+ export async function instantiateFlatsqlLinkShimV2(laneMemory) {
595
+ const { instance } = await WebAssembly.instantiate(FLATSQL_LINK_SHIM_V2_WASM.slice().buffer, {
596
+ [FLATSQL_ENGINE_IMPORT_MODULE]: { memory: laneMemory },
597
+ });
598
+ return instance;
599
+ }
@@ -103,6 +103,37 @@ export function instantiateFlatsqlLinkShim(
103
103
  export function isEngineBodyRefToken(
104
104
  token: bigint | number | string,
105
105
  ): boolean;
106
+ /** Link shim v2: a mailbox over imported (shared) lane memory. */
107
+ export const FLATSQL_LINK_SHIM_V2_WASM: Uint8Array;
108
+ export function buildFlatsqlLinkShimV2Wasm(): Uint8Array;
109
+ export function instantiateFlatsqlLinkShimV2(
110
+ laneMemory: WebAssembly.Memory,
111
+ ): Promise<WebAssembly.Instance>;
112
+ export const FLATSQL_LINK_MAILBOX_BYTES: number;
113
+ export const FLATSQL_LINK_MAILBOX: Readonly<{
114
+ STATE: number;
115
+ SEQ: number;
116
+ DONE_SEQ: number;
117
+ OP: number;
118
+ REQ_PTR: number;
119
+ REQ_LEN: number;
120
+ STATUS: number;
121
+ RESP_PTR: number;
122
+ RESP_LEN: number;
123
+ DOORBELL: number;
124
+ CANCEL: number;
125
+ FLAGS: number;
126
+ GENERATION: number;
127
+ }>;
128
+ export const FLATSQL_LINK_STATE: Readonly<{
129
+ IDLE: number;
130
+ SUBMITTED: number;
131
+ CLAIMED: number;
132
+ DONE: number;
133
+ SUBMITTING: number;
134
+ }>;
135
+ export const FLATSQL_LINK_PENDING: number;
136
+ export const FLATSQL_LINK_BUSY: number;
106
137
  export function readEngineRefEntry(
107
138
  view: DataView,
108
139
  base: number,
package/src/flow/index.js CHANGED
@@ -25,6 +25,14 @@ export {
25
25
  instantiateFlatsqlLinkShim,
26
26
  isEngineBodyRefToken,
27
27
  readEngineRefEntry,
28
+ FLATSQL_LINK_SHIM_V2_WASM,
29
+ buildFlatsqlLinkShimV2Wasm,
30
+ instantiateFlatsqlLinkShimV2,
31
+ FLATSQL_LINK_MAILBOX,
32
+ FLATSQL_LINK_MAILBOX_BYTES,
33
+ FLATSQL_LINK_STATE,
34
+ FLATSQL_LINK_PENDING,
35
+ FLATSQL_LINK_BUSY,
28
36
  } from "./flatsqlLinkShim.js";
29
37
  // The flow compiler (flow check/compile) is node-only (emception + fs) and
30
38
  // ships on the dedicated "./flow/compiler" subpath to keep this barrel
@@ -0,0 +1,251 @@
1
+ // Browser capability matrix probe (docs/architecture/flatsql-partition-store.md
2
+ // §18 T9 #4, §20 "browser matrix", A13, A37, §22.4-6).
3
+ //
4
+ // The local partition store needs, in the context that runs it:
5
+ // - cross-origin isolation and SharedArrayBuffer (ruling 6);
6
+ // - OPFS with FileSystemSyncAccessHandle in a worker (A13 gate: a successful
7
+ // createSyncAccessHandle probe);
8
+ // - SHARED sync-handle modes ("readwrite-unsafe"): a reader I/O worker must be
9
+ // able to read files the writer I/O worker holds (A7). The owner ruled that
10
+ // a browser without them gets no local store and reads from the network
11
+ // (§22.4-6);
12
+ // and it adapts to:
13
+ // - Atomics.waitAsync (else the MessagePort/BroadcastChannel doorbell);
14
+ // - SharedArrayBuffer views in sync-handle read/write (else a scratch copy).
15
+ //
16
+ // The worker half is a self-contained function, serialized into a blob: worker
17
+ // so it runs under the dashboard's `worker-src 'self' blob:` CSP (A39).
18
+
19
+ /**
20
+ * Worker-side probe. Self-contained on purpose: it is stringified into a blob
21
+ * worker, so it may not reference anything outside its own body.
22
+ */
23
+ export async function probeWorkerCapabilities() {
24
+ const result = {
25
+ context: "worker",
26
+ crossOriginIsolated: self.crossOriginIsolated === true,
27
+ sharedArrayBuffer: typeof SharedArrayBuffer === "function",
28
+ atomicsWaitAsync: typeof Atomics.waitAsync === "function",
29
+ atomicsWait: false,
30
+ broadcastChannel: typeof BroadcastChannel === "function",
31
+ webLocks: !!(self.navigator && self.navigator.locks),
32
+ hardwareConcurrency: self.navigator ? self.navigator.hardwareConcurrency ?? null : null,
33
+ opfs: false,
34
+ opfsError: null,
35
+ syncAccessHandle: false,
36
+ syncAccessHandleError: null,
37
+ sharedHandleModes: false,
38
+ sharedHandleModesDetail: null,
39
+ secondHandleError: null,
40
+ sharedViewRead: false,
41
+ sharedViewWrite: false,
42
+ wasmSharedMemoryView: false,
43
+ removeWhileOpenError: null,
44
+ nestedBlobWorker: null,
45
+ };
46
+ try {
47
+ const word = new Int32Array(new SharedArrayBuffer(4));
48
+ result.atomicsWait = Atomics.wait(word, 0, 1, 0) === "not-equal";
49
+ } catch (error) {
50
+ result.atomicsWait = false;
51
+ }
52
+ let root = null;
53
+ try {
54
+ root = await navigator.storage.getDirectory();
55
+ result.opfs = true;
56
+ } catch (error) {
57
+ result.opfsError = `${error && error.name}: ${error && error.message}`;
58
+ }
59
+ if (root) {
60
+ const dir = await root.getDirectoryHandle(".sdm-capability-probe", { create: true });
61
+ let handle = null;
62
+ try {
63
+ const file = await dir.getFileHandle("probe.bin", { create: true });
64
+ handle = await file.createSyncAccessHandle();
65
+ result.syncAccessHandle = true;
66
+ try {
67
+ const second = await file.createSyncAccessHandle();
68
+ second.close();
69
+ result.secondHandleError = null;
70
+ } catch (error) {
71
+ result.secondHandleError = error && error.name;
72
+ }
73
+ const shared = new Uint8Array(new SharedArrayBuffer(64));
74
+ shared.fill(0x3c);
75
+ try {
76
+ result.sharedViewWrite = handle.write(shared, { at: 0 }) === 64;
77
+ } catch (error) {
78
+ result.sharedViewWrite = false;
79
+ }
80
+ try {
81
+ const back = new Uint8Array(new SharedArrayBuffer(64));
82
+ result.sharedViewRead = handle.read(back, { at: 0 }) === 64 && back[63] === 0x3c;
83
+ } catch (error) {
84
+ result.sharedViewRead = false;
85
+ }
86
+ try {
87
+ const memory = new WebAssembly.Memory({ initial: 1, maximum: 2, shared: true });
88
+ const view = new Uint8Array(memory.buffer, 128, 64);
89
+ result.wasmSharedMemoryView = handle.read(view, { at: 0 }) === 64 && view[0] === 0x3c;
90
+ } catch (error) {
91
+ result.wasmSharedMemoryView = false;
92
+ }
93
+ try {
94
+ await dir.removeEntry("probe.bin");
95
+ result.removeWhileOpenError = null;
96
+ } catch (error) {
97
+ result.removeWhileOpenError = error && error.name;
98
+ }
99
+ } catch (error) {
100
+ result.syncAccessHandleError = `${error && error.name}: ${error && error.message}`;
101
+ } finally {
102
+ try {
103
+ if (handle) handle.close();
104
+ } catch (error) {
105
+ // ignore
106
+ }
107
+ }
108
+ let first = null;
109
+ let second = null;
110
+ try {
111
+ const file = await dir.getFileHandle("modes.bin", { create: true });
112
+ first = await file.createSyncAccessHandle({ mode: "readwrite-unsafe" });
113
+ if (first.mode !== "readwrite-unsafe") {
114
+ result.sharedHandleModesDetail = "mode option ignored";
115
+ } else {
116
+ second = await file.createSyncAccessHandle({ mode: "readwrite-unsafe" });
117
+ result.sharedHandleModes = true;
118
+ result.sharedHandleModesDetail = "two readwrite-unsafe handles";
119
+ }
120
+ } catch (error) {
121
+ result.sharedHandleModesDetail = `${error && error.name}: ${error && error.message}`;
122
+ } finally {
123
+ try {
124
+ if (second) second.close();
125
+ } catch (error) {
126
+ // ignore
127
+ }
128
+ try {
129
+ if (first) first.close();
130
+ } catch (error) {
131
+ // ignore
132
+ }
133
+ }
134
+ try {
135
+ await root.removeEntry(".sdm-capability-probe", { recursive: true });
136
+ } catch (error) {
137
+ // ignore
138
+ }
139
+ }
140
+ try {
141
+ const url = URL.createObjectURL(new Blob(["self.postMessage(1)"], { type: "text/javascript" }));
142
+ const nested = new Worker(url);
143
+ result.nestedBlobWorker = await new Promise((resolve) => {
144
+ const timer = setTimeout(() => resolve(false), 3000);
145
+ nested.onmessage = () => {
146
+ clearTimeout(timer);
147
+ resolve(true);
148
+ };
149
+ nested.onerror = () => {
150
+ clearTimeout(timer);
151
+ resolve(false);
152
+ };
153
+ });
154
+ nested.terminate();
155
+ URL.revokeObjectURL(url);
156
+ } catch (error) {
157
+ result.nestedBlobWorker = false;
158
+ }
159
+ return result;
160
+ }
161
+
162
+ /** The worker probe as a classic script source (for a blob: URL). */
163
+ export function workerCapabilityProbeSource() {
164
+ return `(${probeWorkerCapabilities.toString()})().then((r) => self.postMessage(r), (e) => self.postMessage({ error: String(e && e.message || e) }));`;
165
+ }
166
+
167
+ function pageCapabilities() {
168
+ const nav = typeof navigator !== "undefined" ? navigator : {};
169
+ let sharedWasmMemory = false;
170
+ try {
171
+ sharedWasmMemory =
172
+ new WebAssembly.Memory({ initial: 1, maximum: 1, shared: true }).buffer instanceof
173
+ SharedArrayBuffer;
174
+ } catch {
175
+ sharedWasmMemory = false;
176
+ }
177
+ return {
178
+ context: typeof window !== "undefined" ? "window" : "worker",
179
+ crossOriginIsolated: globalThis.crossOriginIsolated === true,
180
+ sharedArrayBuffer: typeof SharedArrayBuffer === "function",
181
+ sharedWasmMemory,
182
+ atomicsWaitAsync: typeof Atomics.waitAsync === "function",
183
+ broadcastChannel: typeof BroadcastChannel === "function",
184
+ webLocks: !!nav.locks,
185
+ opfsApi: !!(nav.storage && nav.storage.getDirectory),
186
+ storagePersist: !!(nav.storage && nav.storage.persist),
187
+ storageEstimate: !!(nav.storage && nav.storage.estimate),
188
+ hardwareConcurrency: nav.hardwareConcurrency ?? null,
189
+ deviceMemory: nav.deviceMemory ?? null,
190
+ userAgent: nav.userAgent ?? null,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Probe the current context and (from a window or worker) a blob: worker.
196
+ * Resolves to `{ page, worker, localStore }`; `localStore` is the gate
197
+ * decision with its reasons.
198
+ */
199
+ export async function probeBrowserCapabilities({ timeoutMs = 10_000 } = {}) {
200
+ const page = pageCapabilities();
201
+ let worker = null;
202
+ if (typeof Worker === "function" && typeof Blob === "function") {
203
+ try {
204
+ const url = URL.createObjectURL(
205
+ new Blob([workerCapabilityProbeSource()], { type: "text/javascript" }),
206
+ );
207
+ const probe = new Worker(url);
208
+ worker = await new Promise((resolve) => {
209
+ const timer = setTimeout(() => resolve({ error: "probe timed out" }), timeoutMs);
210
+ probe.onmessage = (event) => {
211
+ clearTimeout(timer);
212
+ resolve(event.data);
213
+ };
214
+ probe.onerror = (event) => {
215
+ clearTimeout(timer);
216
+ resolve({ error: String(event && event.message) });
217
+ };
218
+ });
219
+ probe.terminate();
220
+ URL.revokeObjectURL(url);
221
+ } catch (error) {
222
+ worker = { error: String(error && error.message) };
223
+ }
224
+ }
225
+ return { page, worker, localStore: flatsqlLocalStoreGate({ page, worker }) };
226
+ }
227
+
228
+ /**
229
+ * The local-store gate (ruling 6, A13, §22.4-6): all of cross-origin
230
+ * isolation, shared memory, OPFS sync access handles in a worker, and shared
231
+ * sync-handle modes. Anything missing means no local store; reads go to the
232
+ * network. Returns `{ supported, missing: string[], doorbell, transfer }`.
233
+ */
234
+ export function flatsqlLocalStoreGate({ page, worker }) {
235
+ const missing = [];
236
+ if (!page?.crossOriginIsolated) missing.push("cross-origin isolation");
237
+ if (!page?.sharedArrayBuffer || !page?.sharedWasmMemory) missing.push("shared memory");
238
+ if (!worker || worker.error) missing.push("worker probe");
239
+ else {
240
+ if (!worker.opfs) missing.push("OPFS");
241
+ if (!worker.syncAccessHandle) missing.push("sync access handles");
242
+ if (!worker.sharedHandleModes) missing.push("shared sync-handle modes");
243
+ if (!worker.atomicsWait) missing.push("Atomics.wait in workers");
244
+ }
245
+ return {
246
+ supported: missing.length === 0,
247
+ missing,
248
+ doorbell: worker?.atomicsWaitAsync ? "atomics" : "message",
249
+ transfer: worker?.sharedViewRead && worker?.sharedViewWrite ? "direct" : "scratch",
250
+ };
251
+ }