@nimbus-sh/worker 0.1.1 → 0.1.3

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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +6 -0
  3. package/README.md +38 -3
  4. package/dist/_shared/cache-stats.d.ts +1 -1
  5. package/dist/_shared/cache-stats.js +1 -1
  6. package/dist/_shared/exports-resolver.d.ts +0 -2
  7. package/dist/_shared/exports-resolver.d.ts.map +1 -1
  8. package/dist/_shared/exports-resolver.js +0 -2
  9. package/dist/_shared/session-id.d.ts +10 -8
  10. package/dist/_shared/session-id.d.ts.map +1 -1
  11. package/dist/_shared/session-id.js +13 -12
  12. package/dist/_shared/session-router.d.ts +0 -1
  13. package/dist/_shared/session-router.d.ts.map +1 -1
  14. package/dist/_shared/session-router.js +0 -1
  15. package/dist/auth/index.d.ts +1 -1
  16. package/dist/auth/index.d.ts.map +1 -1
  17. package/dist/auth/index.js +1 -1
  18. package/dist/auth/types.d.ts +2 -1
  19. package/dist/auth/types.d.ts.map +1 -1
  20. package/dist/auth/types.js +4 -1
  21. package/dist/constants.d.ts.map +1 -1
  22. package/dist/constants.js +0 -1
  23. package/dist/facets/cirrus-real.d.ts +1 -1
  24. package/dist/facets/cirrus-real.js +1 -1
  25. package/dist/facets/manager.d.ts +0 -1
  26. package/dist/facets/manager.d.ts.map +1 -1
  27. package/dist/facets/manager.js +3 -15
  28. package/dist/facets/process.d.ts +2 -2
  29. package/dist/facets/process.js +2 -2
  30. package/dist/facets/vite-dev-server.d.ts +3 -3
  31. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  32. package/dist/facets/vite-dev-server.js +6 -8
  33. package/dist/facets/wasm-swap-registry.d.ts +15 -21
  34. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  35. package/dist/facets/wasm-swap-registry.js +41 -54
  36. package/dist/facets/ws-terminal.d.ts +4 -5
  37. package/dist/facets/ws-terminal.d.ts.map +1 -1
  38. package/dist/facets/ws-terminal.js +7 -8
  39. package/dist/git/commands.d.ts.map +1 -1
  40. package/dist/git/commands.js +0 -1
  41. package/dist/git/network-facet.d.ts.map +1 -1
  42. package/dist/git/network-facet.js +0 -2
  43. package/dist/git-bundle.generated.d.ts +0 -1
  44. package/dist/git-bundle.generated.d.ts.map +1 -1
  45. package/dist/git-bundle.generated.js +0 -1
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/loaders/child-process/spawn-facet.d.ts +7 -52
  49. package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -1
  50. package/dist/loaders/child-process/spawn-facet.js +7 -49
  51. package/dist/loaders/child-process/spawn-pool.d.ts +5 -44
  52. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  53. package/dist/loaders/child-process/spawn-pool.js +5 -44
  54. package/dist/loaders/fanout-pool.d.ts +18 -65
  55. package/dist/loaders/fanout-pool.d.ts.map +1 -1
  56. package/dist/loaders/fanout-pool.js +25 -87
  57. package/dist/loaders/generated-workers.d.ts +1 -2
  58. package/dist/loaders/generated-workers.d.ts.map +1 -1
  59. package/dist/loaders/generated-workers.js +3 -4
  60. package/dist/loaders/loader-pool.d.ts +1 -2
  61. package/dist/loaders/loader-pool.d.ts.map +1 -1
  62. package/dist/loaders/loader-pool.js +1 -1
  63. package/dist/loaders/npm-resolve-preamble.d.ts +4 -5
  64. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  65. package/dist/loaders/npm-resolve-preamble.js +14 -21
  66. package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -1
  67. package/dist/loaders/pre-bundle-preamble.js +0 -1
  68. package/dist/npm/cache.js +1 -1
  69. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  70. package/dist/npm/install-batch-facet.js +7 -12
  71. package/dist/npm/installer.d.ts +5 -7
  72. package/dist/npm/installer.d.ts.map +1 -1
  73. package/dist/npm/installer.js +20 -101
  74. package/dist/npm/r2-cache.d.ts +17 -29
  75. package/dist/npm/r2-cache.d.ts.map +1 -1
  76. package/dist/npm/r2-cache.js +16 -27
  77. package/dist/npm/resolve-facet.d.ts +11 -17
  78. package/dist/npm/resolve-facet.d.ts.map +1 -1
  79. package/dist/npm/resolve-facet.js +14 -28
  80. package/dist/npm/resolve-one-facet.d.ts +10 -13
  81. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  82. package/dist/npm/resolve-one-facet.js +8 -10
  83. package/dist/npm/resolver.d.ts +0 -2
  84. package/dist/npm/resolver.d.ts.map +1 -1
  85. package/dist/npm/resolver.js +0 -12
  86. package/dist/observability/oom-discriminator.d.ts +0 -2
  87. package/dist/observability/oom-discriminator.d.ts.map +1 -1
  88. package/dist/observability/oom-discriminator.js +0 -2
  89. package/dist/replica/routing.js +1 -1
  90. package/dist/router/index.d.ts +6 -2
  91. package/dist/router/index.d.ts.map +1 -1
  92. package/dist/router/index.js +4 -0
  93. package/dist/router/remote-api.d.ts +44 -0
  94. package/dist/router/remote-api.d.ts.map +1 -0
  95. package/dist/router/remote-api.js +333 -0
  96. package/dist/runtime/barrel-synthesizer.js +1 -1
  97. package/dist/runtime/clang-runner.d.ts +5 -6
  98. package/dist/runtime/clang-runner.d.ts.map +1 -1
  99. package/dist/runtime/clang-runner.js +5 -6
  100. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  101. package/dist/runtime/esbuild-service.js +0 -3
  102. package/dist/runtime/framework-detect.d.ts +0 -1
  103. package/dist/runtime/framework-detect.d.ts.map +1 -1
  104. package/dist/runtime/framework-detect.js +0 -1
  105. package/dist/runtime/node-runner.d.ts +1 -1
  106. package/dist/runtime/node-runner.js +1 -1
  107. package/dist/runtime/node-shims.d.ts.map +1 -1
  108. package/dist/runtime/node-shims.js +0 -22
  109. package/dist/runtime/package-manager.d.ts +2 -7
  110. package/dist/runtime/package-manager.d.ts.map +1 -1
  111. package/dist/runtime/package-manager.js +2 -7
  112. package/dist/runtime/process-logs-api.js +1 -1
  113. package/dist/runtime/process-logs.d.ts +0 -1
  114. package/dist/runtime/process-logs.d.ts.map +1 -1
  115. package/dist/runtime/process-table.d.ts +2 -2
  116. package/dist/runtime/process-table.d.ts.map +1 -1
  117. package/dist/runtime/process-table.js +1 -1
  118. package/dist/runtime/python-repl.d.ts +0 -1
  119. package/dist/runtime/python-repl.d.ts.map +1 -1
  120. package/dist/runtime/python-repl.js +0 -1
  121. package/dist/runtime/python-runner.d.ts +1 -2
  122. package/dist/runtime/python-runner.d.ts.map +1 -1
  123. package/dist/runtime/python-runner.js +1 -3
  124. package/dist/runtime/repl-session.d.ts +0 -1
  125. package/dist/runtime/repl-session.d.ts.map +1 -1
  126. package/dist/runtime/repl-session.js +1 -2
  127. package/dist/runtime/require-resolver.d.ts.map +1 -1
  128. package/dist/runtime/require-resolver.js +1 -4
  129. package/dist/runtime/ruby-runner.d.ts +0 -1
  130. package/dist/runtime/ruby-runner.d.ts.map +1 -1
  131. package/dist/runtime/ruby-runner.js +0 -1
  132. package/dist/runtime/runtime-catalog.d.ts +2 -4
  133. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  134. package/dist/runtime/runtime-catalog.js +2 -4
  135. package/dist/runtime/runtime-registry.d.ts +1 -1
  136. package/dist/runtime/runtime-registry.js +1 -1
  137. package/dist/runtime/streams.d.ts.map +1 -1
  138. package/dist/runtime/streams.js +0 -1
  139. package/dist/runtime/wasi-instance.d.ts +35 -45
  140. package/dist/runtime/wasi-instance.d.ts.map +1 -1
  141. package/dist/runtime/wasi-instance.js +87 -103
  142. package/dist/runtime/wasm-runner.d.ts +9 -15
  143. package/dist/runtime/wasm-runner.d.ts.map +1 -1
  144. package/dist/runtime/wasm-runner.js +23 -29
  145. package/dist/session/bindings.d.ts +2 -7
  146. package/dist/session/bindings.d.ts.map +1 -1
  147. package/dist/session/bindings.js +3 -8
  148. package/dist/session/helpers.d.ts +2 -9
  149. package/dist/session/helpers.d.ts.map +1 -1
  150. package/dist/session/helpers.js +2 -10
  151. package/dist/session/init.d.ts +3 -6
  152. package/dist/session/init.d.ts.map +1 -1
  153. package/dist/session/init.js +25 -30
  154. package/dist/session/nimbus-session.d.ts +1 -1
  155. package/dist/session/nimbus-session.js +5 -5
  156. package/dist/session/routes.d.ts.map +1 -1
  157. package/dist/session/routes.js +9 -16
  158. package/dist/session/rpc.d.ts +2 -2
  159. package/dist/session/rpc.js +6 -6
  160. package/dist/session/supervisor-rpc.d.ts +1 -2
  161. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  162. package/dist/session/supervisor-rpc.js +1 -5
  163. package/dist/shell/features.d.ts +5 -5
  164. package/dist/shell/features.js +6 -6
  165. package/dist/shell/unix-commands.js +15 -15
  166. package/dist/vfs/dev-provider.d.ts +1 -1
  167. package/dist/vfs/dev-provider.js +1 -1
  168. package/dist/vfs/sqlite-vfs.d.ts +3 -3
  169. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  170. package/dist/vfs/sqlite-vfs.js +10 -11
  171. package/package.json +13 -2
  172. package/public/s/index.html +44 -75
  173. package/scripts/bundle-facet-workers.mjs +1 -1
  174. package/scripts/bundle-git.mjs +1 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
- * wasi-instance.ts — WASI snapshot_preview1 shim for Nimbus (Wave-1 + Wave-2 + Stream-B).
2
+ * wasi-instance.ts — WASI snapshot_preview1 shim for Nimbus.
3
3
  *
4
- * Wave-1 surface (16 fns, kept verbatim):
4
+ * Core WASI surface:
5
5
  * args_get / args_sizes_get / environ_get / environ_sizes_get
6
6
  * fd_close / fd_write / fd_read / fd_seek / fd_tell
7
7
  * fd_fdstat_get / fd_fdstat_set_flags
@@ -10,53 +10,53 @@
10
10
  * random_get
11
11
  * sched_yield
12
12
  *
13
- * Wave-2 additions (Wave-2; verbatim from wasi-w2 plan):
13
+ * Filesystem WASI additions:
14
14
  * path_open
15
15
  * path_create_directory / path_remove_directory
16
16
  * path_unlink_file
17
- * path_rename (uses Nimbus W-3 atomic rename)
18
- * path_filestat_get / path_filestat_set_times (no-op times — UPGRADED in Stream-B B2)
17
+ * path_rename
18
+ * path_filestat_get / path_filestat_set_times
19
19
  * fd_readdir (cookie-paginated)
20
20
  * fd_filestat_get / fd_filestat_set_size
21
21
  * fd_pread / fd_pwrite
22
22
  * fd_prestat_get / fd_prestat_dir_name (real preopens, not EBADF)
23
23
  *
24
- * Stream-B additions (B1-B6, no-op stubs upgraded + new fns):
25
- * B1: per-file mtime/atime/ctime tracking + in-memory symlink table
24
+ * Socket, polling, and metadata additions:
25
+ * - per-file mtime/atime/ctime tracking + in-memory symlink table
26
26
  * (additive — files Map shape unchanged; parallel times/symlinks Maps)
27
- * B2: fd_filestat_set_times / path_filestat_set_times — REAL impls that
27
+ * - fd_filestat_set_times / path_filestat_set_times — implementations that
28
28
  * write mtime/atime; honor ATIM_NOW / MTIM_NOW flags via clock_realtime
29
- * B3: path_symlink / path_readlink / path_link — REAL impls. Symlink
29
+ * - path_symlink / path_readlink / path_link. Symlink
30
30
  * resolution via __wasiResolvePathFull(baseFd, path, followFlag)
31
31
  * with POSIX-style 40-deep loop detection (returns ELOOP).
32
- * B4: fd_allocate — REAL impl (extends bytes to offset+len; zero-fill)
33
- * B5: proc_raise(sig) — fixed: throws __WasiExit(128 + sig) per POSIX
34
- * shell convention (SIGABRT=6 → 134, SIGTERM=15 → 143); was constant 128.
35
- * B6: fd_fdstat_set_rights — REAL impl (tracks per-fd rights mask;
32
+ * - fd_allocate extends bytes to offset+len and zero-fills.
33
+ * - proc_raise(sig) throws __WasiExit(128 + sig) per POSIX shell convention
34
+ * (SIGABRT=6 -> 134, SIGTERM=15 -> 143).
35
+ * - fd_fdstat_set_rights tracks per-fd rights mask;
36
36
  * fd_fdstat_get returns the tracked mask instead of wide-open).
37
37
  *
38
- * Stream-B P3 additions (sockets via cloudflare:sockets + JSPI):
39
- * B7: sock_send / sock_recv / sock_shutdown — REAL impls via
38
+ * Socket support via cloudflare:sockets + JSPI:
39
+ * - sock_send / sock_recv / sock_shutdown via
40
40
  * WebAssembly.Suspending wrapping. path_open('/dev/tcp/<host>/<port>')
41
41
  * synthetic-path triggers cloudflare:sockets connect().
42
42
  *
43
- * Stream-B P4 additions (poll_oneoff FULL via JSPI):
44
- * B8: poll_oneoff — REAL impl. Handles ALL 3 subscription types in a
43
+ * Poll support via JSPI:
44
+ * - poll_oneoff handles all three subscription types in a
45
45
  * single Promise.race wrapped via WebAssembly.Suspending:
46
46
  * CLOCK (REALTIME + MONOTONIC, relative + absolute deadlines via
47
- * SUBSCRIPTION_CLOCK_ABSTIME flag) → setTimeout to deadline.
48
- * FD_READ/FD_WRITE on file/dir/stdio/symlink → always-ready
47
+ * SUBSCRIPTION_CLOCK_ABSTIME flag) -> setTimeout to deadline.
48
+ * FD_READ/FD_WRITE on file/dir/stdio/symlink -> always-ready
49
49
  * (POSIX: regular files never block).
50
- * FD_READ on socket → real await on
50
+ * FD_READ on socket -> real await on
51
51
  * socket.readable.getReader().read(); data is stashed on
52
52
  * entry.readBuf so subsequent sock_recv sees it pre-loaded.
53
- * FD_WRITE on socket → always-ready (CF Workers writable
53
+ * FD_WRITE on socket -> always-ready (CF Workers writable
54
54
  * streams have unbounded queue from wasm-side perspective).
55
55
  * Concurrent-ready drain: after first-promise resolution, probes
56
56
  * each remaining promise against a microtask sentinel; collects
57
57
  * all currently-resolved events into the output.
58
58
  *
59
- * Stream-B P4 / B9: wasi-threads honest assessment (hard limit):
59
+ * wasi-threads hard limit:
60
60
  * wasi_thread_start is NOT exposed in this shim's import table.
61
61
  * Workers facet isolates have no shared-linear-memory primitive
62
62
  * across instances, so pthread semantics cannot be implemented
@@ -64,9 +64,9 @@
64
64
  * "undefined symbol: thread_spawn" error at LINK time — a clear
65
65
  * diagnostic instead of a runtime memory-corruption bug.
66
66
  * Full rationale + workarounds-considered-and-rejected:
67
- * docs/wasi-threads-infeasibility.md.
67
+ * docs/wasi-threads.md.
68
68
  *
69
- * Architecture (Wave-2 strategy)
69
+ * Architecture (filesystem WASI strategy)
70
70
  * ──────────────────────────────
71
71
  *
72
72
  * Strategy: bulk-snapshot + flush. The supervisor snapshots the user's
@@ -106,18 +106,18 @@
106
106
  * `__wasiMakeImports`, `__wasiRunStart`, `__wasiSnapshotFS`) are in scope
107
107
  * when the user fn runs. Self-contained — no closure captures, no imports.
108
108
  */
109
- export declare const WASI_INSTANCE_PREAMBLE_SRC = "\n// \u2500\u2500 BEGIN: wasi-instance preamble (Wave-1 + Wave-2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Hand-written WASI snapshot_preview1 shim for Nimbus.\n// Source mirror: src/runtime/wasi-instance.ts. Keep in sync by hand.\n\n// errno constants\nconst __WASI_ESUCCESS = 0;\nconst __WASI_EAGAIN = 6;\nconst __WASI_EBADF = 8;\nconst __WASI_ECONNREFUSED = 14;\nconst __WASI_EEXIST = 20;\nconst __WASI_EHOSTUNREACH = 23;\nconst __WASI_EINVAL = 28;\nconst __WASI_EIO = 29;\nconst __WASI_EISDIR = 31;\nconst __WASI_ELOOP = 32;\nconst __WASI_ENOENT = 44;\nconst __WASI_ENOSYS = 52;\nconst __WASI_ENOTCONN = 53;\nconst __WASI_ENOTDIR = 54;\nconst __WASI_ENOTEMPTY = 55;\nconst __WASI_ENOTSOCK = 57;\nconst __WASI_EPIPE = 64;\nconst __WASI_ENOTCAPABLE = 76;\n// clock ids\nconst __WASI_CLOCK_REALTIME = 0;\nconst __WASI_CLOCK_MONOTONIC = 1;\nconst __WASI_CLOCK_PROCESS_CPUTIME_ID = 2;\nconst __WASI_CLOCK_THREAD_CPUTIME_ID = 3;\n// oflags\nconst __WASI_O_CREAT = 1;\nconst __WASI_O_DIRECTORY = 2;\nconst __WASI_O_EXCL = 4;\nconst __WASI_O_TRUNC = 8;\n// lookupflags (passed to path_open, path_filestat_get, etc.)\nconst __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW = 1;\n// fstflags (filestat_set_times)\nconst __WASI_FSTFLAGS_ATIM = 1;\nconst __WASI_FSTFLAGS_ATIM_NOW = 2;\nconst __WASI_FSTFLAGS_MTIM = 4;\nconst __WASI_FSTFLAGS_MTIM_NOW = 8;\n// filetypes\nconst __WASI_FT_UNKNOWN = 0;\nconst __WASI_FT_BLOCK_DEVICE = 1;\nconst __WASI_FT_CHARACTER_DEVICE = 2;\nconst __WASI_FT_DIRECTORY = 3;\nconst __WASI_FT_REGULAR_FILE = 4;\nconst __WASI_FT_SOCKET_DGRAM = 5;\nconst __WASI_FT_SOCKET_STREAM = 6;\nconst __WASI_FT_SYMBOLIC_LINK = 7;\n// preopen types\nconst __WASI_PREOPENTYPE_DIR = 0;\n// Symlink resolution loop limit (POSIX SYMLOOP_MAX is typically 8-40).\nconst __WASI_SYMLOOP_MAX = 40;\n// Default per-fd rights mask (wide-open).\nconst __WASI_RIGHTS_ALL = 0xFFFFFFFFFFFFFFFFn;\n// Stream-B B7: sock_shutdown SD flags.\nconst __WASI_SDFLAGS_RD = 1;\nconst __WASI_SDFLAGS_WR = 2;\n// Stream-B B8: poll_oneoff subscription / event types.\nconst __WASI_EVENTTYPE_CLOCK = 0;\nconst __WASI_EVENTTYPE_FD_READ = 1;\nconst __WASI_EVENTTYPE_FD_WRITE = 2;\nconst __WASI_SUBCLOCKFLAGS_ABSTIME = 1; // SUBSCRIPTION_CLOCK_ABSTIME\n// Stream-B B7: synthetic-path prefix recognised by path_open as a request\n// to open a TCP socket. Mirrors bash's /dev/tcp/<host>/<port> convention\n// (https://www.gnu.org/software/bash/manual/html_node/Redirections.html).\nconst __WASI_TCP_PATH_PREFIX = '/dev/tcp/';\n\n// Stream-B B7: resolved at preamble module-init via dynamic import.\n// CF docs: \"TCP sockets cannot be created in global scope and shared\n// across requests\" \u2014 so we only IMPORT the module here (cheap symbol\n// resolution); the actual connect() call lives inside path_open which\n// runs at facet-handler time (i.e., within WorkerEntrypoint.execute()).\n// See /workspace/.seal-internal/2026-05-11-stream-b/p3-spike.md \u00A72.\nlet __cfSocketConnect = null;\n// Top-level await is supported in workerd ES modules at module-init.\n// If import fails (no cloudflare:sockets binding, or running under a\n// runtime that doesn't expose it), __cfSocketConnect stays null and\n// path_open('/dev/tcp/...') returns ENOSYS with a clear diagnostic.\ntry {\n const __mod = await import('cloudflare:sockets');\n __cfSocketConnect = __mod.connect;\n} catch (__e) {\n // fail-soft: socket support disabled this call.\n}\n\nclass __WasiExit { constructor(code) { this.code = code | 0; } }\n\n// \u2500\u2500\u2500 Virtual filesystem state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// __wasiInitFS({ root, preopens, files, dirs, times?, symlinks? }) \u2014\n// install a per-call FS:\n// root string \u2014 canonical session root, e.g. 'home/user/wasi-w2'.\n// preopens Array<{ wasiPath, vfsPath }> \u2014 fd>=3 preopens (in order).\n// files Record<vfsPath, base64 string> \u2014 initial file contents.\n// dirs Array<vfsPath> \u2014 initial directory list.\n// times Record<vfsPath, {mtime, atime, ctime}> | undefined\n// \u2014 per-path nanosecond timestamps (Stream-B B1). Optional;\n// paths without entries default to \"now\" at init time.\n// Values are decimal strings to survive JSON.serialize\n// (BigInt is not JSON-safe); converted to BigInt internally.\n// symlinks Record<vfsPath, targetPath> | undefined\n// \u2014 Stream-B B3 in-memory symlink table. Optional; defaults\n// to empty. Target paths are canonicalized at insertion.\n//\n// After _start returns, __wasiSnapshotFS() extracts:\n// {\n// filesWritten: Record<vfsPath, base64 string>, // new + modified\n// filesDeleted: string[], // unlinked\n// dirsCreated: string[], // mkdir'd\n// dirsDeleted: string[], // rmdir'd\n// timesChanged: Record<vfsPath, {mtime, atime, ctime}> // B1 \u2014 new or modified mtime/atime\n// symlinksCreated: Record<vfsPath, targetPath> // B3 \u2014 new symlinks\n// symlinksDeleted: string[] // B3 \u2014 removed symlinks\n// }\n//\n// Backward-compat: all new fields are additive. Callers that don't pass\n// 'times'/'symlinks' to __wasiInitFS see identical behavior to pre-B1.\n// Supervisors that don't read 'timesChanged'/'symlinks*' from the\n// snapshot see identical behavior to pre-B1.\n\nlet __wasiFS = null; // populated by __wasiInitFS\nlet __wasiPreopens = []; // [{ wasiPath, vfsPath, fd }, ...]\n\n// All paths are stored in canonical form: no leading '/', no '..', no\n// double slashes. The wasm program sees '/foo/bar.txt' (with leading\n// slash) but the VFS keys are 'home/user/.../foo/bar.txt'.\nfunction __wasiCanonicalize(p) {\n if (!p) return '';\n const parts = p.split('/');\n const out = [];\n for (const seg of parts) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') { if (out.length > 0) out.pop(); continue; }\n out.push(seg);\n }\n return out.join('/');\n}\n\n// Wall-clock at init, used as the default mtime/atime/ctime for any path\n// that arrives without an explicit times entry. Captured once per call so\n// all \"default-init\" timestamps within a call share a value (mtime ==\n// atime == ctime), matching what a real cold-load would produce.\nfunction __wasiNowNs() {\n // Date.now() is ms since epoch; multiply by 1e6 \u2192 ns.\n return BigInt(Date.now()) * 1000000n;\n}\nfunction __wasiInitFS(opts) {\n const files = new Map(); // canonicalVfsPath \u2192 Uint8Array\n const dirs = new Set(); // canonicalVfsPath\n // Stream-B B1: parallel timestamp + symlink maps.\n const times = new Map(); // canonicalVfsPath \u2192 {mtime: BigInt, atime: BigInt, ctime: BigInt}\n const symlinks = new Map(); // canonicalVfsPath \u2192 targetPath (canonical)\n // mirror originals for diff at flush time\n const origFiles = new Map();\n const origDirs = new Set();\n const origTimes = new Map();\n const origSymlinks = new Map();\n const nowNs = __wasiNowNs();\n for (const [path, b64] of Object.entries(opts.files || {})) {\n const canon = __wasiCanonicalize(path);\n const bin = atob(b64);\n const u8 = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);\n files.set(canon, u8);\n origFiles.set(canon, u8.slice()); // copy so subsequent mutations detect change\n // Default timestamps (overwritten below if opts.times has this path).\n const t = { mtime: nowNs, atime: nowNs, ctime: nowNs };\n times.set(canon, t);\n origTimes.set(canon, { mtime: t.mtime, atime: t.atime, ctime: t.ctime });\n }\n for (const path of opts.dirs || []) {\n const canon = __wasiCanonicalize(path);\n dirs.add(canon);\n origDirs.add(canon);\n if (!times.has(canon)) {\n const t = { mtime: nowNs, atime: nowNs, ctime: nowNs };\n times.set(canon, t);\n origTimes.set(canon, { mtime: t.mtime, atime: t.atime, ctime: t.ctime });\n }\n }\n // Stream-B B1: load explicit per-path timestamps if supervisor provided them.\n // Values arrive as decimal strings (BigInt \u2192 JSON.stringify safe) or numbers.\n for (const [path, t] of Object.entries(opts.times || {})) {\n const canon = __wasiCanonicalize(path);\n const cur = times.get(canon);\n if (!cur) continue; // path not yet in files/dirs; skip\n const mtime = (t && t.mtime !== undefined) ? BigInt(t.mtime) : cur.mtime;\n const atime = (t && t.atime !== undefined) ? BigInt(t.atime) : cur.atime;\n const ctime = (t && t.ctime !== undefined) ? BigInt(t.ctime) : cur.ctime;\n times.set(canon, { mtime, atime, ctime });\n origTimes.set(canon, { mtime, atime, ctime });\n }\n // Stream-B B3: load explicit symlinks if supervisor provided them.\n for (const [path, target] of Object.entries(opts.symlinks || {})) {\n const canon = __wasiCanonicalize(path);\n symlinks.set(canon, String(target));\n origSymlinks.set(canon, String(target));\n }\n __wasiFS = {\n root: __wasiCanonicalize(opts.root || ''),\n files, dirs, times, symlinks,\n origFiles, origDirs, origTimes, origSymlinks,\n };\n // Reset fd table baseline; install preopens as fd 3, 4, 5, ...\n __wasiPreopens = [];\n fdTable.clear();\n fdTable.set(0, { kind: 'stdin' });\n fdTable.set(1, { kind: 'stdout' });\n fdTable.set(2, { kind: 'stderr' });\n nextFd = 3;\n for (const po of (opts.preopens || [])) {\n const fd = nextFd++;\n const vfsPath = __wasiCanonicalize(po.vfsPath);\n fdTable.set(fd, { kind: 'preopen', wasiPath: po.wasiPath, vfsPath });\n __wasiPreopens.push({ fd, wasiPath: po.wasiPath, vfsPath });\n if (!dirs.has(vfsPath)) dirs.add(vfsPath);\n if (!times.has(vfsPath)) {\n const t = { mtime: nowNs, atime: nowNs, ctime: nowNs };\n times.set(vfsPath, t);\n origTimes.set(vfsPath, { mtime: t.mtime, atime: t.atime, ctime: t.ctime });\n }\n }\n}\n\n// Stream-B B7 helper: open a TCP socket via cloudflare:sockets when\n// path_open is invoked on a /dev/tcp/<host>/<port> synthetic path.\n// Returns a WASI errno. Allocates a new fd of kind:'socket' on success\n// and writes it to fdOutPtr.\nfunction __wasiOpenTcpSocket(pathArg, fdflags, fdOutPtr, writeU32LE) {\n if (typeof __cfSocketConnect !== 'function') {\n // cloudflare:sockets unavailable in this runtime. Report ENOSYS so\n // the user program sees a clear errno rather than a hang.\n return __WASI_ENOSYS;\n }\n // pathArg shape: \"/dev/tcp/<host>/<port>\".\n const tail = pathArg.substring(__WASI_TCP_PATH_PREFIX.length);\n const slashIdx = tail.lastIndexOf('/');\n if (slashIdx <= 0 || slashIdx === tail.length - 1) return __WASI_EINVAL;\n const host = tail.substring(0, slashIdx);\n const portStr = tail.substring(slashIdx + 1);\n const port = parseInt(portStr, 10);\n if (!host || !(port > 0 && port < 65536)) return __WASI_EINVAL;\n let socket;\n try {\n // Per CF docs: connect() is sync (returns Socket immediately); the\n // socket.opened promise resolves when the TCP handshake completes.\n // We do NOT await opened here \u2014 sock_send/sock_recv will await it\n // implicitly via the writable/readable streams (or via socket.opened\n // before the first read/write).\n //\n // Prod-verify-fix: pass allowHalfOpen=true. Default is false,\n // which makes the writable side close automatically on EOF AND\n // (per empirical prod behaviour) makes a manual writer.close()\n // affect readable-side delivery. POSIX shutdown(SHUT_WR) requires\n // half-close semantics: the user can stop sending while still\n // receiving the peer's response. tcpbin.com:4242 (and most\n // request/response protocols) rely on this \u2014 client signals EOF\n // via half-close, server completes its echo, client reads it.\n // See https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#socketoptions\n socket = __cfSocketConnect({ hostname: host, port }, { allowHalfOpen: true });\n } catch (e) {\n // Synchronous errors from connect() (e.g. invalid address). Surface\n // as ECONNREFUSED since the user-observable behavior is the same.\n return __WASI_ECONNREFUSED;\n }\n const fd = nextFd++;\n fdTable.set(fd, {\n kind: 'socket',\n socket,\n reader: null, // lazy: getReader() on first sock_recv\n writer: null, // lazy: getWriter() on first sock_send\n readBuf: new Uint8Array(0),\n readBufOffset: 0,\n eof: false,\n closed: false,\n halfClosedWr: false,\n fdflags: fdflags | 0,\n });\n writeU32LE(fdOutPtr, fd);\n return __WASI_ESUCCESS;\n}\n\n// Stream-B B1+B2 helpers: update tracked timestamps for a path. Idempotent.\n// Caller passes nanosecond BigInt(s); pass null for fields to keep unchanged.\nfunction __wasiTouchTimes(canonPath, mtimeNs, atimeNs, ctimeNs) {\n if (!__wasiFS) return;\n const cur = __wasiFS.times.get(canonPath);\n if (cur) {\n if (mtimeNs !== null) cur.mtime = mtimeNs;\n if (atimeNs !== null) cur.atime = atimeNs;\n if (ctimeNs !== null) cur.ctime = ctimeNs;\n } else {\n const now = __wasiNowNs();\n __wasiFS.times.set(canonPath, {\n mtime: mtimeNs !== null ? mtimeNs : now,\n atime: atimeNs !== null ? atimeNs : now,\n ctime: ctimeNs !== null ? ctimeNs : now,\n });\n }\n}\n// Convenience: bump mtime + ctime to \"now\" on a write/create.\nfunction __wasiBumpMtime(canonPath) {\n const now = __wasiNowNs();\n __wasiTouchTimes(canonPath, now, null, now);\n}\n\nfunction __wasiSnapshotFS() {\n if (!__wasiFS) return null;\n const filesWritten = {};\n const filesDeleted = [];\n const dirsCreated = [];\n const dirsDeleted = [];\n const timesChanged = {}; // Stream-B B1: mtime/atime/ctime diffs\n const symlinksCreated = {}; // Stream-B B3: new symlinks\n const symlinksDeleted = []; // Stream-B B3: removed symlinks\n // files: anything in __wasiFS.files not in orig, or whose bytes differ.\n for (const [path, bytes] of __wasiFS.files) {\n const orig = __wasiFS.origFiles.get(path);\n let same = !!orig && orig.length === bytes.length;\n if (same) {\n for (let i = 0; i < bytes.length; i++) {\n if (orig[i] !== bytes[i]) { same = false; break; }\n }\n }\n if (!same) {\n // base64 encode\n let s = '';\n for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);\n filesWritten[path] = btoa(s);\n }\n }\n // files: anything in orig not in current\n for (const path of __wasiFS.origFiles.keys()) {\n if (!__wasiFS.files.has(path)) filesDeleted.push(path);\n }\n // dirs\n for (const path of __wasiFS.dirs) {\n if (!__wasiFS.origDirs.has(path)) dirsCreated.push(path);\n }\n for (const path of __wasiFS.origDirs) {\n if (!__wasiFS.dirs.has(path)) dirsDeleted.push(path);\n }\n // Stream-B B1: times diff. BigInt \u2192 decimal string (JSON.stringify-safe).\n for (const [path, t] of __wasiFS.times) {\n const orig = __wasiFS.origTimes.get(path);\n if (!orig || orig.mtime !== t.mtime || orig.atime !== t.atime || orig.ctime !== t.ctime) {\n timesChanged[path] = {\n mtime: t.mtime.toString(),\n atime: t.atime.toString(),\n ctime: t.ctime.toString(),\n };\n }\n }\n // Stream-B B3: symlink diff.\n for (const [path, target] of __wasiFS.symlinks) {\n if (__wasiFS.origSymlinks.get(path) !== target) {\n symlinksCreated[path] = target;\n }\n }\n for (const path of __wasiFS.origSymlinks.keys()) {\n if (!__wasiFS.symlinks.has(path)) symlinksDeleted.push(path);\n }\n return {\n filesWritten, filesDeleted, dirsCreated, dirsDeleted,\n timesChanged, symlinksCreated, symlinksDeleted,\n };\n}\n\n// \u2500\u2500\u2500 fd table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Entry shapes:\n// { kind: 'stdin' | 'stdout' | 'stderr' }\n// { kind: 'preopen', wasiPath, vfsPath, rights? }\n// { kind: 'file', vfsPath, offset, oflags, fdflags, rights? }\n// { kind: 'dir', vfsPath, readdirEntries: null | Array, cookie, rights? }\n// { kind: 'symlink', vfsPath, target, rights? } // Stream-B B3 (only when O_NOFOLLOW)\n//\n// Stream-B B6: 'rights' is an optional BigInt mask. When set, fd_fdstat_get\n// returns it (instead of the wide-open default). fd_fdstat_set_rights\n// writes to it. The mask is advisory in this shim (we don't enforce per-fn\n// rights checks in v1 \u2014 single-tenant facet, no untrusted callers).\nconst fdTable = new Map();\nlet nextFd = 3;\n\n// Resolve a WASI path against a preopen fd; returns the canonical VFS path\n// WITHOUT following symlinks. Stream-B kept this name compatible with all\n// existing call sites (~10 of them inside the imports object).\n//\n// wasi-path-fix: a single preopen { wasiPath: '/', vfsPath: 'home/user' }\n// exposes the user's home as the wasm-side root. wasi-libc strips '/'\n// from absolute paths and hands the rest to path_open. For \"/tmp/x\" the\n// stripped path \"tmp/x\" + vfsPath \"home/user\" yields \"home/user/tmp/x\"\n// (correct). For \"/home/user/x\" the stripped path \"home/user/x\" +\n// vfsPath \"home/user\" yields \"home/user/home/user/x\" \u2014 the file lands\n// at the wrong location. The user mental model is that \"/home/user/x\"\n// IS the user's file at home/user/x in the VFS.\n//\n// Fix: in __wasiResolvePath, when baseFd is a preopen AND the supplied\n// pathStr already starts with the preopen's vfsPath (followed by '/' or\n// end-of-string), strip the vfsPath prefix BEFORE prepending it. This\n// makes \"/home/user/x\" \u2192 wasi-libc strip \u2192 \"home/user/x\" \u2192 resolver\n// strip vfsPath \u2192 \"x\" \u2192 final canonical \"home/user/x\" \u2713. Paths that\n// don't share the vfsPath prefix (like \"tmp/x\") follow the legacy path.\n//\n// Skip the strip when vfsPath is empty (no chroot mismatch possible).\n// Be careful with segment boundaries: vfsPath \"home/user\" must NOT\n// strip from path \"home/userfoo/bar\" (it's a different directory).\nfunction __wasiResolvePath(baseFd, pathStr) {\n const entry = fdTable.get(baseFd);\n if (!entry) return null;\n let baseVfs;\n if (entry.kind === 'preopen' || entry.kind === 'dir') baseVfs = entry.vfsPath;\n else return null;\n // Strip leading './' or '/' segments (POSIX legacy from wasi-libc).\n let trimmed = pathStr.replace(/^\\.?\\/+/, '').replace(/^\\/+/, '');\n // wasi-path-fix: chroot-collision strip. Only applies to preopens (not\n // 'dir' fds \u2014 those are opened via path_open with already-resolved\n // paths). Only when baseVfs is non-empty AND trimmed starts with\n // baseVfs at a segment boundary.\n if (entry.kind === 'preopen' && baseVfs.length > 0) {\n if (trimmed === baseVfs) {\n trimmed = '';\n } else if (trimmed.length > baseVfs.length &&\n trimmed.charCodeAt(baseVfs.length) === 47 /* '/' */ &&\n trimmed.substring(0, baseVfs.length) === baseVfs) {\n trimmed = trimmed.substring(baseVfs.length + 1);\n }\n }\n return __wasiCanonicalize(baseVfs + '/' + trimmed);\n}\n\n// Stream-B B3: resolve a path WITH optional symlink-follow + POSIX loop\n// detection. Returns {path: canonical, isSymlink: bool, err?: errno}.\n// - If followFlag is true (default \u2014 matches LOOKUPFLAGS_SYMLINK_FOLLOW),\n// walks the symlink chain up to __WASI_SYMLOOP_MAX depth. ELOOP if exceeded.\n// - If followFlag is false, returns the bare resolved path; if the path\n// itself is in __wasiFS.symlinks, isSymlink=true so callers know to\n// open it as a symlink fd rather than a regular file.\n// Errors return {path: '', isSymlink: false, err: __WASI_E...}.\nfunction __wasiResolvePathFull(baseFd, pathStr, followFlag) {\n let p = __wasiResolvePath(baseFd, pathStr);\n if (p === null) return { path: '', isSymlink: false, err: __WASI_EBADF };\n if (!__wasiFS) return { path: p, isSymlink: false };\n if (!followFlag) {\n return { path: p, isSymlink: __wasiFS.symlinks.has(p), err: __WASI_ESUCCESS };\n }\n // Follow symlinks with bounded loop counter.\n let depth = 0;\n while (__wasiFS.symlinks.has(p)) {\n if (depth++ >= __WASI_SYMLOOP_MAX) {\n return { path: p, isSymlink: true, err: __WASI_ELOOP };\n }\n const target = __wasiFS.symlinks.get(p);\n // Target may be relative or absolute. Absolute targets (leading '/')\n // resolve against the same preopen root as the original path; relative\n // targets resolve against the directory containing the symlink.\n let next;\n if (target.startsWith('/')) {\n // Resolve against the preopen of baseFd.\n const e = fdTable.get(baseFd);\n const baseVfs = (e && (e.kind === 'preopen' || e.kind === 'dir')) ? e.vfsPath : '';\n next = __wasiCanonicalize(baseVfs + '/' + target);\n } else {\n const lastSlash = p.lastIndexOf('/');\n const parent = lastSlash >= 0 ? p.substring(0, lastSlash) : '';\n next = __wasiCanonicalize(parent + '/' + target);\n }\n p = next;\n }\n return { path: p, isSymlink: false, err: __WASI_ESUCCESS };\n}\n\n// \u2500\u2500\u2500 makeImports \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction __wasiMakeImports(opts) {\n // opts: { argv, env, getMemory, stdoutWrite?, stderrWrite? }\n const argv = opts.argv || [];\n const envArr = [];\n if (opts.env) for (const k of Object.keys(opts.env)) envArr.push(k + '=' + opts.env[k]);\n const utf8enc = new TextEncoder();\n const utf8dec = new TextDecoder();\n\n function view() { return new DataView(opts.getMemory().buffer); }\n function u8() { return new Uint8Array(opts.getMemory().buffer); }\n function writeU32LE(off, v) { view().setUint32(off, v >>> 0, true); }\n function writeU64LE(off, v) {\n const dv = view();\n if (typeof v === 'bigint') { dv.setBigUint64(off, v, true); return; }\n const lo = (v >>> 0);\n const hi = Math.floor(v / 4294967296) >>> 0;\n dv.setUint32(off, lo, true);\n dv.setUint32(off + 4, hi, true);\n }\n function readPath(ptr, len) {\n const bytes = u8().subarray(ptr, ptr + len);\n return utf8dec.decode(bytes);\n }\n\n let stdoutBuf = '';\n let stderrBuf = '';\n function appendStream(kind, bytes) {\n const s = utf8dec.decode(bytes);\n if (kind === 'stdout') {\n stdoutBuf += s;\n if (opts.stdoutWrite) opts.stdoutWrite(s);\n } else {\n stderrBuf += s;\n if (opts.stderrWrite) opts.stderrWrite(s);\n }\n }\n\n // \u2500\u2500 Helpers operating on the VFS state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n function getFile(vfsPath) {\n if (!__wasiFS) return null;\n return __wasiFS.files.get(vfsPath) || null;\n }\n function hasDir(vfsPath) {\n if (!__wasiFS) return false;\n if (__wasiFS.dirs.has(vfsPath)) return true;\n // Root preopens implicitly exist as dirs.\n return false;\n }\n function setFile(vfsPath, bytes) {\n __wasiFS.files.set(vfsPath, bytes);\n // Stream-B B1: bump mtime+ctime on every write. atime stays as-is\n // (read paths bump atime explicitly via touchAccess()).\n __wasiBumpMtime(vfsPath);\n }\n function unsetFile(vfsPath) {\n __wasiFS.files.delete(vfsPath);\n __wasiFS.times.delete(vfsPath);\n }\n function touchAccess(vfsPath) {\n // Stream-B B1: bump atime on a read. mtime/ctime unchanged.\n if (!__wasiFS) return;\n const cur = __wasiFS.times.get(vfsPath);\n if (cur) cur.atime = __wasiNowNs();\n }\n function ensureParentDirs(vfsPath) {\n // Ensure all ancestor dirs of vfsPath exist (they should, but mkdir -p\n // semantics are handled in path_create_directory by callers).\n const parts = vfsPath.split('/');\n if (parts.length <= 1) return;\n let p = '';\n for (let i = 0; i < parts.length - 1; i++) {\n p = p ? (p + '/' + parts[i]) : parts[i];\n __wasiFS.dirs.add(p);\n // Stream-B B1: ensure ancestor dirs have a times entry.\n if (!__wasiFS.times.has(p)) {\n const now = __wasiNowNs();\n __wasiFS.times.set(p, { mtime: now, atime: now, ctime: now });\n }\n }\n }\n function readdirChildren(vfsPath) {\n const out = [];\n const prefix = vfsPath === '' ? '' : vfsPath + '/';\n // Files\n for (const path of __wasiFS.files.keys()) {\n if (path.startsWith(prefix)) {\n const rest = path.substring(prefix.length);\n if (rest && rest.indexOf('/') === -1) {\n out.push({ name: rest, type: __WASI_FT_REGULAR_FILE });\n }\n }\n }\n // Dirs\n for (const path of __wasiFS.dirs) {\n if (path !== vfsPath && path.startsWith(prefix)) {\n const rest = path.substring(prefix.length);\n if (rest && rest.indexOf('/') === -1) {\n out.push({ name: rest, type: __WASI_FT_DIRECTORY });\n }\n }\n }\n return out;\n }\n\n const imports = {\n // \u2500\u2500 args / env \u2500\u2500\n args_get(argvPtr, argvBufPtr) {\n let buf = argvBufPtr;\n const memU8 = u8();\n const dv = view();\n for (let i = 0; i < argv.length; i++) {\n dv.setUint32(argvPtr + i * 4, buf, true);\n const bytes = utf8enc.encode(argv[i] + '\\0');\n memU8.set(bytes, buf);\n buf += bytes.length;\n }\n return __WASI_ESUCCESS;\n },\n args_sizes_get(argcPtr, sizePtr) {\n let total = 0;\n for (let i = 0; i < argv.length; i++) total += utf8enc.encode(argv[i]).length + 1;\n writeU32LE(argcPtr, argv.length);\n writeU32LE(sizePtr, total);\n return __WASI_ESUCCESS;\n },\n environ_get(environPtr, envBufPtr) {\n let buf = envBufPtr;\n const memU8 = u8();\n const dv = view();\n for (let i = 0; i < envArr.length; i++) {\n dv.setUint32(environPtr + i * 4, buf, true);\n const bytes = utf8enc.encode(envArr[i] + '\\0');\n memU8.set(bytes, buf);\n buf += bytes.length;\n }\n return __WASI_ESUCCESS;\n },\n environ_sizes_get(envcPtr, sizePtr) {\n let total = 0;\n for (let i = 0; i < envArr.length; i++) total += utf8enc.encode(envArr[i]).length + 1;\n writeU32LE(envcPtr, envArr.length);\n writeU32LE(sizePtr, total);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 fd basic \u2500\u2500\n fd_close(fd) {\n if (fd === 0 || fd === 1 || fd === 2) return __WASI_ESUCCESS;\n if (!fdTable.has(fd)) return __WASI_EBADF;\n // Don't delete preopens \u2014 they're meant to persist for the program lifetime.\n const entry = fdTable.get(fd);\n if (entry.kind === 'preopen') return __WASI_ESUCCESS;\n // Stream-B B7: best-effort close of socket streams. The actual\n // socket.close() is fire-and-forget (sync return per WASI) \u2014 if\n // the program wants to await closure it should sock_shutdown\n // first. We DO drop the fd-table entry so subsequent ops on this\n // fd see EBADF.\n if (entry.kind === 'socket' && !entry.closed) {\n try { entry.socket.close(); } catch {}\n entry.closed = true;\n }\n fdTable.delete(fd);\n return __WASI_ESUCCESS;\n },\n\n fd_read(fd, iovsPtr, iovsLen, nreadPtr) {\n if (fd === 0) { writeU32LE(nreadPtr, 0); return __WASI_ESUCCESS; }\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const file = getFile(entry.vfsPath);\n if (!file) return __WASI_ENOENT;\n const dv = view();\n const memU8 = u8();\n let total = 0;\n for (let i = 0; i < iovsLen; i++) {\n const iov = iovsPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n const remain = file.length - entry.offset;\n if (remain <= 0) break;\n const n = Math.min(bufLen, remain);\n memU8.set(file.subarray(entry.offset, entry.offset + n), bufPtr);\n entry.offset += n;\n total += n;\n if (n < bufLen) break;\n }\n writeU32LE(nreadPtr, total);\n return __WASI_ESUCCESS;\n },\n\n fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {\n const dv = view();\n const memU8 = u8();\n // Gather all iov bytes\n let total = 0;\n const parts = [];\n for (let i = 0; i < iovsLen; i++) {\n const iov = iovsPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n if (bufLen > 0) parts.push(memU8.slice(bufPtr, bufPtr + bufLen));\n total += bufLen;\n }\n let combined;\n if (parts.length === 0) combined = new Uint8Array(0);\n else if (parts.length === 1) combined = parts[0];\n else {\n combined = new Uint8Array(total);\n let off = 0;\n for (const p of parts) { combined.set(p, off); off += p.length; }\n }\n if (fd === 1 || fd === 2) {\n appendStream(fd === 1 ? 'stdout' : 'stderr', combined);\n writeU32LE(nwrittenPtr, total);\n return __WASI_ESUCCESS;\n }\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const file = getFile(entry.vfsPath) || new Uint8Array(0);\n // Splice combined into file at entry.offset\n const newLen = Math.max(file.length, entry.offset + total);\n const next = new Uint8Array(newLen);\n next.set(file, 0);\n next.set(combined, entry.offset);\n setFile(entry.vfsPath, next);\n entry.offset += total;\n writeU32LE(nwrittenPtr, total);\n return __WASI_ESUCCESS;\n },\n\n fd_seek(fd, offsetArg, whence, newOffsetPtr) {\n // WASI/V8 passes i64 args as BigInt. Wave-1 mistakenly assumed\n // (lo, hi) i32 pairs; that worked only because hello-world never\n // exercised seek. Wave-2: accept BigInt directly.\n if (fd === 0 || fd === 1 || fd === 2) {\n writeU64LE(newOffsetPtr, 0n);\n return __WASI_ESUCCESS;\n }\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const delta = typeof offsetArg === 'bigint' ? offsetArg : BigInt(offsetArg | 0);\n const cur = BigInt(entry.offset);\n const file = getFile(entry.vfsPath);\n const fileLen = file ? BigInt(file.length) : 0n;\n let next;\n if (whence === 0) next = delta;\n else if (whence === 1) next = cur + delta;\n else if (whence === 2) next = fileLen + delta;\n else return __WASI_EINVAL;\n if (next < 0n) return __WASI_EINVAL;\n entry.offset = Number(next);\n writeU64LE(newOffsetPtr, next);\n return __WASI_ESUCCESS;\n },\n\n fd_tell(fd, offsetPtr) {\n if (fd === 0 || fd === 1 || fd === 2) {\n writeU64LE(offsetPtr, 0n);\n return __WASI_ESUCCESS;\n }\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n writeU64LE(offsetPtr, BigInt(entry.offset));\n return __WASI_ESUCCESS;\n },\n\n fd_fdstat_get(fd, statPtr) {\n const entry = fdTable.get(fd);\n if (!entry) return __WASI_EBADF;\n const dv = view();\n let ftype = __WASI_FT_UNKNOWN;\n if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {\n ftype = __WASI_FT_CHARACTER_DEVICE;\n } else if (entry.kind === 'preopen' || entry.kind === 'dir') {\n ftype = __WASI_FT_DIRECTORY;\n } else if (entry.kind === 'file') {\n ftype = __WASI_FT_REGULAR_FILE;\n } else if (entry.kind === 'symlink') {\n ftype = __WASI_FT_SYMBOLIC_LINK;\n } else if (entry.kind === 'socket') {\n ftype = __WASI_FT_SOCKET_STREAM; // Stream-B B7\n }\n dv.setUint8(statPtr, ftype);\n dv.setUint8(statPtr + 1, 0);\n dv.setUint16(statPtr + 2, (entry.fdflags || 0) & 0xFFFF, true);\n dv.setUint32(statPtr + 4, 0, true);\n // Stream-B B6: honor entry.rights when set, else wide-open default.\n // The legacy '0x3FFFFFFFn' was a 30-bit truncation of the actual\n // 64-bit rights field; modern wasi-libc probes specific bits so we\n // now publish full 64-bit __WASI_RIGHTS_ALL.\n const rb = (entry.rights !== undefined) ? entry.rights : __WASI_RIGHTS_ALL;\n const ri = (entry.rightsInheriting !== undefined) ? entry.rightsInheriting : __WASI_RIGHTS_ALL;\n writeU64LE(statPtr + 8, rb);\n writeU64LE(statPtr + 16, ri);\n return __WASI_ESUCCESS;\n },\n\n fd_fdstat_set_flags(fd, flags) {\n const entry = fdTable.get(fd);\n if (!entry) return __WASI_EBADF;\n if (entry.kind === 'file') entry.fdflags = flags;\n return __WASI_ESUCCESS;\n },\n\n // Stream-B B6: track per-fd rights mask. Callers narrow their own caps;\n // we record but don't enforce in v1 (single-tenant facet). The narrow\n // is visible to subsequent fd_fdstat_get calls, satisfying capability-\n // tightening probes that round-trip the mask.\n fd_fdstat_set_rights(fd, rightsBase, rightsInheriting) {\n const entry = fdTable.get(fd);\n if (!entry) return __WASI_EBADF;\n // Args are i64 \u2014 V8 routes as BigInt.\n const rb = typeof rightsBase === 'bigint' ? rightsBase : BigInt(rightsBase >>> 0);\n const ri = typeof rightsInheriting === 'bigint' ? rightsInheriting : BigInt(rightsInheriting >>> 0);\n // POSIX semantics: rights can only NARROW, never widen.\n const curRb = entry.rights !== undefined ? entry.rights : __WASI_RIGHTS_ALL;\n const curRi = entry.rightsInheriting !== undefined ? entry.rightsInheriting : __WASI_RIGHTS_ALL;\n if ((rb & ~curRb) !== 0n) return __WASI_ENOTCAPABLE;\n if ((ri & ~curRi) !== 0n) return __WASI_ENOTCAPABLE;\n entry.rights = rb;\n entry.rightsInheriting = ri;\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 preopens \u2500\u2500\n fd_prestat_get(fd, prestatPtr) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'preopen') return __WASI_EBADF;\n const dv = view();\n dv.setUint8(prestatPtr, __WASI_PREOPENTYPE_DIR);\n // pr_name_len at offset 4 (after 3 bytes pad)\n const nameBytes = utf8enc.encode(entry.wasiPath);\n dv.setUint32(prestatPtr + 4, nameBytes.length, true);\n return __WASI_ESUCCESS;\n },\n fd_prestat_dir_name(fd, pathPtr, pathLen) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'preopen') return __WASI_EBADF;\n const nameBytes = utf8enc.encode(entry.wasiPath);\n if (nameBytes.length > pathLen) return __WASI_EINVAL;\n u8().set(nameBytes, pathPtr);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 path_open \u2500\u2500\n path_open(baseFd, dirflags, pathPtr, pathLen, oflags, _rightsBase, _rightsInheriting, fdflags, fdOutPtr) {\n const pathArg = readPath(pathPtr, pathLen);\n\n // Stream-B B7: synthetic /dev/tcp/<host>/<port> path \u2014 open a TCP\n // socket via cloudflare:sockets. Bash-like convention; matches\n // /workspace/.seal-internal/2026-05-11-stream-b/p3-spike.md \u00A72.\n // Intercept BEFORE resolution because /dev/tcp/* doesn't exist in\n // the in-memory FS \u2014 __wasiResolvePath would canonicalize it but\n // not find any entry.\n if (pathArg.startsWith(__WASI_TCP_PATH_PREFIX)) {\n return __wasiOpenTcpSocket(pathArg, fdflags, fdOutPtr, writeU32LE);\n }\n // Stream-B B3: honor LOOKUPFLAGS_SYMLINK_FOLLOW (default behavior\n // when bit is set or dirflags omitted). When NOT set, surface the\n // symlink itself as a 'symlink' fd kind so readlink can introspect.\n const follow = (dirflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || dirflags === 0;\n const rp = __wasiResolvePathFull(baseFd, pathArg, follow);\n if (rp.err === __WASI_EBADF) return __WASI_EBADF;\n if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;\n const resolved = rp.path;\n const isCreate = (oflags & __WASI_O_CREAT) !== 0;\n const isDirectory = (oflags & __WASI_O_DIRECTORY) !== 0;\n const isExcl = (oflags & __WASI_O_EXCL) !== 0;\n const isTrunc = (oflags & __WASI_O_TRUNC) !== 0;\n\n // If the unfollowed path is a symlink and follow=false, open as\n // a symlink fd (only readlink-style introspection works on it).\n if (!follow && rp.isSymlink) {\n const fd = nextFd++;\n const target = __wasiFS.symlinks.get(resolved);\n fdTable.set(fd, { kind: 'symlink', vfsPath: resolved, target, oflags, fdflags });\n writeU32LE(fdOutPtr, fd);\n return __WASI_ESUCCESS;\n }\n\n const fileExists = __wasiFS.files.has(resolved);\n const dirExists = __wasiFS.dirs.has(resolved);\n\n if (isDirectory) {\n if (!dirExists) return __WASI_ENOENT;\n const fd = nextFd++;\n fdTable.set(fd, { kind: 'dir', vfsPath: resolved, readdirEntries: null, cookie: 0n, oflags, fdflags });\n writeU32LE(fdOutPtr, fd);\n return __WASI_ESUCCESS;\n }\n if (dirExists) {\n // Opening a directory without O_DIRECTORY: WASI returns EISDIR.\n return __WASI_EISDIR;\n }\n if (!fileExists) {\n if (!isCreate) return __WASI_ENOENT;\n // create empty file\n ensureParentDirs(resolved);\n setFile(resolved, new Uint8Array(0));\n } else if (isExcl) {\n return __WASI_EEXIST;\n } else if (isTrunc) {\n setFile(resolved, new Uint8Array(0));\n }\n const fd = nextFd++;\n fdTable.set(fd, { kind: 'file', vfsPath: resolved, offset: 0, oflags, fdflags });\n writeU32LE(fdOutPtr, fd);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 path_create_directory \u2500\u2500\n path_create_directory(baseFd, pathPtr, pathLen) {\n const path = readPath(pathPtr, pathLen);\n const resolved = __wasiResolvePath(baseFd, path);\n if (resolved === null) return __WASI_EBADF;\n if (__wasiFS.dirs.has(resolved)) return __WASI_EEXIST;\n if (__wasiFS.files.has(resolved)) return __WASI_EEXIST;\n ensureParentDirs(resolved);\n __wasiFS.dirs.add(resolved);\n // Stream-B B1: seed times for the new dir.\n const now = __wasiNowNs();\n __wasiFS.times.set(resolved, { mtime: now, atime: now, ctime: now });\n return __WASI_ESUCCESS;\n },\n\n path_remove_directory(baseFd, pathPtr, pathLen) {\n const path = readPath(pathPtr, pathLen);\n const resolved = __wasiResolvePath(baseFd, path);\n if (resolved === null) return __WASI_EBADF;\n if (!__wasiFS.dirs.has(resolved)) return __WASI_ENOENT;\n // Must be empty\n const children = readdirChildren(resolved);\n if (children.length > 0) return __WASI_ENOTEMPTY;\n __wasiFS.dirs.delete(resolved);\n return __WASI_ESUCCESS;\n },\n\n path_unlink_file(baseFd, pathPtr, pathLen) {\n const path = readPath(pathPtr, pathLen);\n const resolved = __wasiResolvePath(baseFd, path);\n if (resolved === null) return __WASI_EBADF;\n if (__wasiFS.dirs.has(resolved)) return __WASI_EISDIR;\n // Stream-B B3: unlink also removes a symlink at this path. We\n // operate on the unfollowed path (POSIX unlink semantics \u2014 removes\n // the directory entry itself, not the symlink target).\n if (__wasiFS.symlinks.has(resolved)) {\n __wasiFS.symlinks.delete(resolved);\n __wasiFS.times.delete(resolved);\n return __WASI_ESUCCESS;\n }\n if (!__wasiFS.files.has(resolved)) return __WASI_ENOENT;\n unsetFile(resolved);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 path_rename (atomic; overwrites destination) \u2500\u2500\n path_rename(srcFd, srcPathPtr, srcPathLen, dstFd, dstPathPtr, dstPathLen) {\n const srcPath = readPath(srcPathPtr, srcPathLen);\n const dstPath = readPath(dstPathPtr, dstPathLen);\n const src = __wasiResolvePath(srcFd, srcPath);\n const dst = __wasiResolvePath(dstFd, dstPath);\n if (src === null || dst === null) return __WASI_EBADF;\n // src must exist as either a file or dir\n const srcFile = __wasiFS.files.get(src);\n const srcIsDir = __wasiFS.dirs.has(src);\n if (!srcFile && !srcIsDir) return __WASI_ENOENT;\n // pre-unlink destination if present (the W-3 semantic for file \u2192 file).\n if (__wasiFS.files.has(dst)) __wasiFS.files.delete(dst);\n if (__wasiFS.dirs.has(dst)) __wasiFS.dirs.delete(dst);\n if (srcFile) {\n __wasiFS.files.delete(src);\n __wasiFS.files.set(dst, srcFile);\n } else {\n __wasiFS.dirs.delete(src);\n __wasiFS.dirs.add(dst);\n // Move children (rare; mostly the rename target is a single file).\n // For directories we walk and rebase any matching path key.\n const srcPrefix = src + '/';\n const toMove = [];\n for (const key of __wasiFS.files.keys()) {\n if (key.startsWith(srcPrefix)) toMove.push(key);\n }\n for (const key of toMove) {\n const newKey = dst + '/' + key.substring(srcPrefix.length);\n __wasiFS.files.set(newKey, __wasiFS.files.get(key));\n __wasiFS.files.delete(key);\n }\n const dirsToMove = [];\n for (const key of __wasiFS.dirs) {\n if (key.startsWith(srcPrefix)) dirsToMove.push(key);\n }\n for (const key of dirsToMove) {\n const newKey = dst + '/' + key.substring(srcPrefix.length);\n __wasiFS.dirs.add(newKey);\n __wasiFS.dirs.delete(key);\n }\n }\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 path_filestat_get \u2500\u2500\n path_filestat_get(baseFd, lookupflags, pathPtr, pathLen, statPtr) {\n const path = readPath(pathPtr, pathLen);\n // Stream-B B3: follow symlinks unless explicitly told not to.\n const follow = (lookupflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || lookupflags === 0;\n const rp = __wasiResolvePathFull(baseFd, path, follow);\n if (rp.err === __WASI_EBADF) return __WASI_EBADF;\n if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;\n const resolved = rp.path;\n let ftype, size;\n if (!follow && rp.isSymlink) {\n ftype = __WASI_FT_SYMBOLIC_LINK;\n size = BigInt(new TextEncoder().encode(__wasiFS.symlinks.get(resolved)).length);\n } else if (__wasiFS.files.has(resolved)) {\n ftype = __WASI_FT_REGULAR_FILE;\n size = BigInt(__wasiFS.files.get(resolved).length);\n } else if (__wasiFS.dirs.has(resolved)) {\n ftype = __WASI_FT_DIRECTORY;\n size = 0n;\n } else {\n return __WASI_ENOENT;\n }\n // Stream-B B1: emit real timestamps.\n const t = __wasiFS.times.get(resolved) || { mtime: 0n, atime: 0n, ctime: 0n };\n const dv = view();\n // filestat_t layout (WASI preview1):\n // dev:u64@0, ino:u64@8, filetype:u8@16, [pad 17..23],\n // nlink:u64@24, size:u64@32, atim:u64@40, mtim:u64@48, ctim:u64@56\n writeU64LE(statPtr, 0n);\n writeU64LE(statPtr + 8, 0n);\n dv.setUint8(statPtr + 16, ftype);\n for (let i = 17; i < 24; i++) dv.setUint8(statPtr + i, 0);\n writeU64LE(statPtr + 24, 1n);\n writeU64LE(statPtr + 32, size);\n writeU64LE(statPtr + 40, t.atime);\n writeU64LE(statPtr + 48, t.mtime);\n writeU64LE(statPtr + 56, t.ctime);\n return __WASI_ESUCCESS;\n },\n\n // Stream-B B2: real path_filestat_set_times. Honors ATIM/ATIM_NOW/\n // MTIM/MTIM_NOW flags. Spec: atim_ns and mtim_ns are absolute\n // nanosecond timestamps; flags select which fields to update + whether\n // to clamp to \"now\". ENOENT if path doesn't exist.\n path_filestat_set_times(baseFd, lookupflags, pathPtr, pathLen, atimArg, mtimArg, fstflags) {\n const path = readPath(pathPtr, pathLen);\n const follow = (lookupflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || lookupflags === 0;\n const rp = __wasiResolvePathFull(baseFd, path, follow);\n if (rp.err === __WASI_EBADF) return __WASI_EBADF;\n if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;\n const resolved = rp.path;\n const isSymlinkOnly = !follow && rp.isSymlink;\n if (!isSymlinkOnly && !__wasiFS.files.has(resolved) && !__wasiFS.dirs.has(resolved)) {\n return __WASI_ENOENT;\n }\n // Decode flags. Mutually-exclusive pairs ATIM vs ATIM_NOW (similarly\n // for MTIM) \u2014 caller error if both set; we accept and prefer _NOW.\n const setAtim = (fstflags & __WASI_FSTFLAGS_ATIM) !== 0;\n const setAtimNow = (fstflags & __WASI_FSTFLAGS_ATIM_NOW) !== 0;\n const setMtim = (fstflags & __WASI_FSTFLAGS_MTIM) !== 0;\n const setMtimNow = (fstflags & __WASI_FSTFLAGS_MTIM_NOW) !== 0;\n if ((setAtim && setAtimNow) || (setMtim && setMtimNow)) return __WASI_EINVAL;\n const now = __wasiNowNs();\n const atimNs = setAtimNow ? now : (setAtim ? (typeof atimArg === 'bigint' ? atimArg : BigInt(atimArg)) : null);\n const mtimNs = setMtimNow ? now : (setMtim ? (typeof mtimArg === 'bigint' ? mtimArg : BigInt(mtimArg)) : null);\n __wasiTouchTimes(resolved, mtimNs, atimNs, now);\n return __WASI_ESUCCESS;\n },\n\n // Stream-B B3: read the target string of a symlink. Spec:\n // path_readlink(fd, path, path_len, buf, buf_len, *bufused)\n // Truncates to buf_len; writes actual bytes to *bufused.\n path_readlink(baseFd, pathPtr, pathLen, bufPtr, bufLen, bufUsedPtr) {\n const path = readPath(pathPtr, pathLen);\n // readlink NEVER follows symlinks on the last component (POSIX).\n const rp = __wasiResolvePathFull(baseFd, path, false);\n if (rp.err === __WASI_EBADF) return __WASI_EBADF;\n if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;\n const resolved = rp.path;\n if (!__wasiFS.symlinks.has(resolved)) return __WASI_EINVAL; // not a symlink\n const target = __wasiFS.symlinks.get(resolved);\n const enc = new TextEncoder();\n const targetBytes = enc.encode(target);\n const n = Math.min(targetBytes.length, bufLen);\n u8().set(targetBytes.subarray(0, n), bufPtr);\n writeU32LE(bufUsedPtr, n);\n // POSIX: readlink doesn't update atime per most filesystems; but\n // some (ext4 with strictatime) do. We follow Linux default: no\n // atime bump on readlink.\n return __WASI_ESUCCESS;\n },\n\n // Stream-B B3: create a symlink. Spec:\n // path_symlink(old_path, fd, new_path)\n // old_path is the symlink's TARGET (stored verbatim); fd+new_path\n // is the location where the symlink itself is created.\n path_symlink(oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) {\n const oldPath = readPath(oldPathPtr, oldPathLen); // target string\n const newPath = readPath(newPathPtr, newPathLen);\n const resolved = __wasiResolvePath(newFd, newPath);\n if (resolved === null) return __WASI_EBADF;\n if (__wasiFS.files.has(resolved)) return __WASI_EEXIST;\n if (__wasiFS.dirs.has(resolved)) return __WASI_EEXIST;\n if (__wasiFS.symlinks.has(resolved)) return __WASI_EEXIST;\n // Store the target verbatim \u2014 symlinks per POSIX are dumb strings;\n // resolution happens at lookup time.\n __wasiFS.symlinks.set(resolved, oldPath);\n ensureParentDirs(resolved);\n const now = __wasiNowNs();\n __wasiFS.times.set(resolved, { mtime: now, atime: now, ctime: now });\n return __WASI_ESUCCESS;\n },\n\n // Stream-B B3: hardlink. Spec:\n // path_link(old_fd, old_flags, old_path, new_fd, new_path)\n // Implements as a shared-buffer alias. POSIX semantics: both names\n // point at the same inode, so writes through either are visible to\n // the other. In our in-memory FS the \"same inode\" is the same\n // Uint8Array reference. CAVEAT: our setFile/fd_write paths REPLACE\n // the buffer reference (immutable-style update) so subsequent writes\n // don't propagate. Matches link(2) at the WASI-layer (link itself\n // succeeds; concurrent-mutation semantics are filesystem-dependent\n // and our shim is a single-call sandbox).\n path_link(oldFd, oldFlags, oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) {\n const oldPath = readPath(oldPathPtr, oldPathLen);\n const newPath = readPath(newPathPtr, newPathLen);\n const follow = (oldFlags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || oldFlags === 0;\n const rpOld = __wasiResolvePathFull(oldFd, oldPath, follow);\n if (rpOld.err === __WASI_EBADF) return __WASI_EBADF;\n if (rpOld.err === __WASI_ELOOP) return __WASI_ELOOP;\n const src = rpOld.path;\n const dst = __wasiResolvePath(newFd, newPath);\n if (dst === null) return __WASI_EBADF;\n if (!__wasiFS.files.has(src)) return __WASI_ENOENT;\n if (__wasiFS.files.has(dst) || __wasiFS.dirs.has(dst) || __wasiFS.symlinks.has(dst)) {\n return __WASI_EEXIST;\n }\n __wasiFS.files.set(dst, __wasiFS.files.get(src)); // shared reference\n ensureParentDirs(dst);\n const now = __wasiNowNs();\n __wasiFS.times.set(dst, { mtime: now, atime: now, ctime: now });\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 fd_filestat_get / fd_filestat_set_size \u2500\u2500\n fd_filestat_get(fd, statPtr) {\n const entry = fdTable.get(fd);\n if (!entry) return __WASI_EBADF;\n const dv = view();\n let ftype = __WASI_FT_UNKNOWN;\n let size = 0n;\n let timesPath = null;\n if (entry.kind === 'file') {\n ftype = __WASI_FT_REGULAR_FILE;\n const f = getFile(entry.vfsPath);\n if (f) size = BigInt(f.length);\n timesPath = entry.vfsPath;\n } else if (entry.kind === 'dir' || entry.kind === 'preopen') {\n ftype = __WASI_FT_DIRECTORY;\n timesPath = entry.vfsPath;\n } else if (entry.kind === 'symlink') {\n ftype = __WASI_FT_SYMBOLIC_LINK;\n size = BigInt(new TextEncoder().encode(entry.target || '').length);\n timesPath = entry.vfsPath;\n } else if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {\n ftype = __WASI_FT_CHARACTER_DEVICE;\n }\n const t = (timesPath && __wasiFS.times.get(timesPath)) || { mtime: 0n, atime: 0n, ctime: 0n };\n writeU64LE(statPtr, 0n);\n writeU64LE(statPtr + 8, 0n);\n dv.setUint8(statPtr + 16, ftype);\n for (let i = 17; i < 24; i++) dv.setUint8(statPtr + i, 0);\n writeU64LE(statPtr + 24, 1n);\n writeU64LE(statPtr + 32, size);\n writeU64LE(statPtr + 40, t.atime);\n writeU64LE(statPtr + 48, t.mtime);\n writeU64LE(statPtr + 56, t.ctime);\n return __WASI_ESUCCESS;\n },\n fd_filestat_set_size(fd, size) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const cur = getFile(entry.vfsPath) || new Uint8Array(0);\n const newSize = Number(size);\n const next = new Uint8Array(newSize);\n next.set(cur.subarray(0, Math.min(cur.length, newSize)), 0);\n setFile(entry.vfsPath, next);\n return __WASI_ESUCCESS;\n },\n // Stream-B B2: real fd_filestat_set_times.\n fd_filestat_set_times(fd, atimArg, mtimArg, fstflags) {\n const entry = fdTable.get(fd);\n if (!entry) return __WASI_EBADF;\n if (entry.kind !== 'file' && entry.kind !== 'dir' && entry.kind !== 'preopen' && entry.kind !== 'symlink') {\n // stdio fds: no-op success (POSIX touches /dev/stdin etc. silently).\n return __WASI_ESUCCESS;\n }\n const setAtim = (fstflags & __WASI_FSTFLAGS_ATIM) !== 0;\n const setAtimNow = (fstflags & __WASI_FSTFLAGS_ATIM_NOW) !== 0;\n const setMtim = (fstflags & __WASI_FSTFLAGS_MTIM) !== 0;\n const setMtimNow = (fstflags & __WASI_FSTFLAGS_MTIM_NOW) !== 0;\n if ((setAtim && setAtimNow) || (setMtim && setMtimNow)) return __WASI_EINVAL;\n const now = __wasiNowNs();\n const atimNs = setAtimNow ? now : (setAtim ? (typeof atimArg === 'bigint' ? atimArg : BigInt(atimArg)) : null);\n const mtimNs = setMtimNow ? now : (setMtim ? (typeof mtimArg === 'bigint' ? mtimArg : BigInt(mtimArg)) : null);\n __wasiTouchTimes(entry.vfsPath, mtimNs, atimNs, now);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 fd_pread / fd_pwrite (offset-explicit) \u2500\u2500\n fd_pread(fd, iovsPtr, iovsLen, offsetArg, nreadPtr) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const file = getFile(entry.vfsPath);\n if (!file) return __WASI_ENOENT;\n let offset = typeof offsetArg === 'bigint'\n ? Number(offsetArg)\n : (offsetArg >>> 0);\n const dv = view();\n const memU8 = u8();\n let total = 0;\n for (let i = 0; i < iovsLen; i++) {\n const iov = iovsPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n const remain = file.length - offset;\n if (remain <= 0) break;\n const n = Math.min(bufLen, remain);\n memU8.set(file.subarray(offset, offset + n), bufPtr);\n offset += n;\n total += n;\n if (n < bufLen) break;\n }\n writeU32LE(nreadPtr, total);\n return __WASI_ESUCCESS;\n },\n\n fd_pwrite(fd, iovsPtr, iovsLen, offsetArg, nwrittenPtr) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n let offset = typeof offsetArg === 'bigint'\n ? Number(offsetArg)\n : (offsetArg >>> 0);\n const dv = view();\n const memU8 = u8();\n let total = 0;\n const parts = [];\n for (let i = 0; i < iovsLen; i++) {\n const iov = iovsPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n if (bufLen > 0) parts.push(memU8.slice(bufPtr, bufPtr + bufLen));\n total += bufLen;\n }\n let combined;\n if (parts.length === 0) combined = new Uint8Array(0);\n else if (parts.length === 1) combined = parts[0];\n else {\n combined = new Uint8Array(total);\n let off = 0;\n for (const p of parts) { combined.set(p, off); off += p.length; }\n }\n const cur = getFile(entry.vfsPath) || new Uint8Array(0);\n const newLen = Math.max(cur.length, offset + total);\n const next = new Uint8Array(newLen);\n next.set(cur, 0);\n next.set(combined, offset);\n setFile(entry.vfsPath, next);\n writeU32LE(nwrittenPtr, total);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 fd_readdir \u2500\u2500\n //\n // WASI dirent layout (24 bytes per entry):\n // d_next u64 @ 0 (cookie for next entry)\n // d_ino u64 @ 8\n // d_namlen u32 @ 16\n // d_type u8 @ 20\n // pad 21..23\n // followed by name bytes (variable).\n fd_readdir(fd, bufPtr, bufLen, cookieArg, bufusedPtr) {\n const entry = fdTable.get(fd);\n if (!entry || (entry.kind !== 'dir' && entry.kind !== 'preopen')) return __WASI_EBADF;\n if (!entry.readdirEntries) {\n const kids = readdirChildren(entry.vfsPath);\n entry.readdirEntries = [\n { name: '.', type: __WASI_FT_DIRECTORY },\n { name: '..', type: __WASI_FT_DIRECTORY },\n ...kids,\n ];\n }\n let startCookie = typeof cookieArg === 'bigint'\n ? Number(cookieArg)\n : (cookieArg >>> 0);\n let written = 0;\n const dv = view();\n const memU8 = u8();\n for (let i = startCookie; i < entry.readdirEntries.length; i++) {\n const e = entry.readdirEntries[i];\n const nameBytes = utf8enc.encode(e.name);\n const recordSize = 24 + nameBytes.length;\n if (written + 24 > bufLen) break;\n // d_next = i+1 (next cookie)\n writeU64LE(bufPtr + written, BigInt(i + 1));\n writeU64LE(bufPtr + written + 8, 0n); // d_ino\n dv.setUint32(bufPtr + written + 16, nameBytes.length, true);\n dv.setUint8(bufPtr + written + 20, e.type);\n for (let j = 21; j < 24; j++) dv.setUint8(bufPtr + written + j, 0);\n written += 24;\n // Write as much of the name as fits.\n const nameRoom = Math.min(nameBytes.length, bufLen - written);\n memU8.set(nameBytes.subarray(0, nameRoom), bufPtr + written);\n written += nameRoom;\n if (nameRoom < nameBytes.length) break;\n }\n writeU32LE(bufusedPtr, written);\n return __WASI_ESUCCESS;\n },\n\n fd_advise() { return __WASI_ESUCCESS; },\n // Stream-B B4: real fd_allocate. Extends the file's byte buffer with\n // zeros so [offset, offset+len) is allocated. POSIX posix_fallocate\n // semantics. ENOSPC is not reachable in our in-memory FS (the\n // 32 MiB snapshot cap is enforced at supervisor level; in-call we\n // just allocate the JS Uint8Array).\n fd_allocate(fd, offsetArg, lenArg) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const offset = typeof offsetArg === 'bigint' ? Number(offsetArg) : (offsetArg >>> 0);\n const len = typeof lenArg === 'bigint' ? Number(lenArg) : (lenArg >>> 0);\n const cur = getFile(entry.vfsPath) || new Uint8Array(0);\n const needed = offset + len;\n if (needed <= cur.length) return __WASI_ESUCCESS; // already big enough\n const next = new Uint8Array(needed);\n next.set(cur, 0); // [cur.length, needed) is implicitly zero-initialised\n setFile(entry.vfsPath, next);\n return __WASI_ESUCCESS;\n },\n fd_datasync() { return __WASI_ESUCCESS; },\n fd_sync() { return __WASI_ESUCCESS; },\n fd_renumber(from, to) {\n const entry = fdTable.get(from);\n if (!entry) return __WASI_EBADF;\n fdTable.delete(from);\n fdTable.set(to, entry);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 proc_exit \u2500\u2500\n proc_exit(code) { throw new __WasiExit(code | 0); },\n // Stream-B B5: proc_raise(sig) \u2014 POSIX shell convention encodes a\n // signal-terminated process as exit-status 128+sig. SIGABRT=6 \u2192 134,\n // SIGTERM=15 \u2192 143, SIGKILL=9 \u2192 137. Returns errno (never actually;\n // throws via __WasiExit). The legacy 'throw new __WasiExit(128)' was\n // wrong: it discarded the signal number, so probes couldn't\n // distinguish SIGABRT from SIGTERM at the exit-code layer.\n proc_raise(sig) { throw new __WasiExit(128 + ((sig | 0) & 0xFF)); },\n\n // \u2500\u2500 clock \u2500\u2500\n clock_time_get(clockId, _precision, timePtr) {\n // precision is i64 \u2014 passed as BigInt by V8. We don't use it but\n // the arity must match for V8 to route correctly.\n let nowNs;\n if (clockId === __WASI_CLOCK_REALTIME) {\n nowNs = BigInt(Date.now()) * 1000000n;\n } else if (clockId === __WASI_CLOCK_MONOTONIC\n || clockId === __WASI_CLOCK_PROCESS_CPUTIME_ID\n || clockId === __WASI_CLOCK_THREAD_CPUTIME_ID) {\n const ms = (typeof performance !== 'undefined' && performance.now)\n ? performance.now() : Date.now();\n nowNs = BigInt(Math.floor(ms * 1000)) * 1000n;\n } else return __WASI_EINVAL;\n writeU64LE(timePtr, nowNs);\n return __WASI_ESUCCESS;\n },\n clock_res_get(clockId, resPtr) {\n if (clockId === __WASI_CLOCK_REALTIME) { writeU64LE(resPtr, 1000000n); return __WASI_ESUCCESS; }\n if (clockId === __WASI_CLOCK_MONOTONIC\n || clockId === __WASI_CLOCK_PROCESS_CPUTIME_ID\n || clockId === __WASI_CLOCK_THREAD_CPUTIME_ID) { writeU64LE(resPtr, 1000n); return __WASI_ESUCCESS; }\n return __WASI_EINVAL;\n },\n\n // \u2500\u2500 random \u2500\u2500\n random_get(bufPtr, bufLen) {\n const memU8 = u8();\n const CHUNK = 65536;\n let off = 0;\n while (off < bufLen) {\n const n = Math.min(bufLen - off, CHUNK);\n crypto.getRandomValues(memU8.subarray(bufPtr + off, bufPtr + off + n));\n off += n;\n }\n return __WASI_ESUCCESS;\n },\n\n sched_yield() { return __WASI_ESUCCESS; },\n\n // \u2500\u2500 Stream-B B8: poll_oneoff FULL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // Spec: WASI preview1 poll_oneoff(in_subscriptions, out_events,\n // nsubscriptions, *retNevents) blocks until at least one event fires,\n // writes events to out_events, returns count via *retNevents.\n //\n // Subscription layout (48B per entry, align 8):\n // +0 userdata: u64\n // +8 tag: u8 (EVENTTYPE_CLOCK=0 | FD_READ=1 | FD_WRITE=2)\n // +9..15 pad\n // +16 CLOCK: id:u32, +24 timeout:u64, +32 precision:u64, +40 flags:u16\n // FD_R/W: file_descriptor:u32\n //\n // Event layout (32B per entry, align 8):\n // +0 userdata: u64\n // +8 error: u16\n // +10 type: u8\n // +11..15 pad\n // +16 nbytes: u64 (fd events) \u2014 0 for clock\n // +24 flags: u16 (fd events) \u2014 0 here (no peer-hangup detection)\n // +26..31 pad\n //\n // Implementation: async fn that:\n // 1. Parses all subscriptions.\n // 2. Builds a Promise per subscription:\n // - CLOCK: setTimeout(deadline) \u2192 resolves with subscription idx.\n // - FD_READ/WRITE on regular file/dir/stdio: always-resolved Promise.\n // - FD_READ on socket: socket.readable.getReader().read() peek.\n // - FD_WRITE on socket: always-resolved (writable streams have\n // unbounded queue from the wasm side's perspective).\n // 3. Promise.race over all of them.\n // 4. After race resolves, drain all NOW-ready subscriptions\n // (winning + any others that also resolved or were always-ready).\n // 5. Write events; return count.\n //\n // Cancellation: pending setTimeouts and reader-locks are cleaned up\n // on every iteration to avoid leaking resources between poll calls.\n //\n // Wrapped in WebAssembly.Suspending at the imports-object finalise\n // point (with sock_*). When Suspending is unavailable, the bare async\n // fn returns a Promise which the wasm caller cannot consume \u2014 the\n // trap surfaces via __wasiRunStartAsync's catch.\n async poll_oneoff(inSubsPtr, outEventsPtr, nsubs, retNeventsPtr) {\n const dv = view();\n if ((nsubs | 0) <= 0) {\n writeU32LE(retNeventsPtr, 0);\n return __WASI_ESUCCESS;\n }\n // Parse subscriptions.\n const subs = []; // [{ userdata: BigInt, tag, ... }]\n for (let i = 0; i < nsubs; i++) {\n const base = inSubsPtr + i * 48;\n const userdata = dv.getBigUint64(base, true);\n const tag = dv.getUint8(base + 8);\n if (tag === __WASI_EVENTTYPE_CLOCK) {\n const id = dv.getUint32(base + 16, true);\n const timeout = dv.getBigUint64(base + 24, true);\n const precision = dv.getBigUint64(base + 32, true);\n const flags = dv.getUint16(base + 40, true);\n subs.push({ idx: i, userdata, tag, id, timeout, precision, flags });\n } else if (tag === __WASI_EVENTTYPE_FD_READ || tag === __WASI_EVENTTYPE_FD_WRITE) {\n const fd = dv.getUint32(base + 16, true);\n subs.push({ idx: i, userdata, tag, fd });\n } else {\n subs.push({ idx: i, userdata, tag, badTag: true });\n }\n }\n // Build per-subscription readiness promises. Each resolves with an\n // event-record { idx, error, type, nbytes, flags }. Bookkeeping for\n // cancellation: timerIds[i] holds setTimeout handle (or null);\n // readerLocks[i] holds a {reader, fd} pair so we can releaseLock\n // after the race.\n const timerIds = new Array(subs.length).fill(null);\n const readerLocks = [];\n const monoNowNs = () => {\n const ms = (typeof performance !== 'undefined' && performance.now)\n ? performance.now() : Date.now();\n return BigInt(Math.floor(ms * 1000)) * 1000n;\n };\n const promises = subs.map((s) => {\n if (s.badTag) {\n return Promise.resolve({\n idx: s.idx, error: __WASI_EINVAL, type: s.tag, nbytes: 0n, flags: 0,\n });\n }\n if (s.tag === __WASI_EVENTTYPE_CLOCK) {\n // Compute absolute deadline (ns) and convert to ms-delay.\n let deadlineNs;\n if ((s.flags & __WASI_SUBCLOCKFLAGS_ABSTIME) !== 0) {\n deadlineNs = s.timeout;\n } else {\n // Relative: deadline = now + timeout.\n const nowNs = (s.id === __WASI_CLOCK_REALTIME)\n ? BigInt(Date.now()) * 1000000n\n : monoNowNs();\n deadlineNs = nowNs + s.timeout;\n }\n const nowAgain = (s.id === __WASI_CLOCK_REALTIME)\n ? BigInt(Date.now()) * 1000000n\n : monoNowNs();\n const remainNs = deadlineNs > nowAgain ? (deadlineNs - nowAgain) : 0n;\n const remainMs = Number(remainNs / 1000000n);\n return new Promise((resolve) => {\n // POLICY: setTimeout is the only correct way to express a\n // deadline in JS event-loop terms. The PROBE-QUALITY anti-\n // setTimeout rule applies to probe ASSERTION LOGIC, not to\n // the implementation surface itself \u2014 a CLOCK subscription\n // BY DEFINITION needs a timer.\n const t = setTimeout(() => resolve({\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag, nbytes: 0n, flags: 0,\n }), remainMs > 0 ? remainMs : 0);\n timerIds[s.idx] = t;\n });\n }\n // FD subscription.\n const entry = fdTable.get(s.fd);\n if (!entry) {\n return Promise.resolve({\n idx: s.idx, error: __WASI_EBADF, type: s.tag, nbytes: 0n, flags: 0,\n });\n }\n // Regular files, dirs, stdio: always ready (POSIX: regular files\n // never block \u2014 read returns immediately even if at EOF).\n if (entry.kind === 'file' || entry.kind === 'dir' ||\n entry.kind === 'preopen' || entry.kind === 'symlink' ||\n entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {\n let nbytes = 0n;\n if (entry.kind === 'file' && s.tag === __WASI_EVENTTYPE_FD_READ) {\n const f = getFile(entry.vfsPath);\n if (f) {\n const remain = f.length - (entry.offset || 0);\n nbytes = remain > 0 ? BigInt(remain) : 0n;\n }\n } else if (s.tag === __WASI_EVENTTYPE_FD_WRITE) {\n // Writable: report large available capacity. Most user code\n // only checks nbytes > 0.\n nbytes = 0xFFFFFFFFn;\n }\n return Promise.resolve({\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag, nbytes, flags: 0,\n });\n }\n // Socket fd.\n if (entry.kind === 'socket') {\n if (s.tag === __WASI_EVENTTYPE_FD_WRITE) {\n // CF Workers writable streams are unbounded from the wasm\n // side; always-ready is a defensible approximation. Real\n // backpressure is enforced inside sock_send's await chain.\n return Promise.resolve({\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,\n nbytes: 0xFFFFFFFFn, flags: 0,\n });\n }\n // FD_READ: if local readBuf has bytes, immediate-ready.\n if (entry.readBuf && entry.readBufOffset < entry.readBuf.length) {\n const avail = BigInt(entry.readBuf.length - entry.readBufOffset);\n return Promise.resolve({\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,\n nbytes: avail, flags: 0,\n });\n }\n if (entry.eof || entry.closed) {\n // Peer closed: report ready with 0 bytes + HANGUP flag bit.\n return Promise.resolve({\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,\n nbytes: 0n, flags: 1, // RECV_DATA_TRUNCATED bit doesn't apply; flag 1 == HANGUP per eventrwflags_t bit 0.\n });\n }\n // Awaitable: peek at the readable stream. We MUST NOT consume\n // the data into oblivion \u2014 sock_recv on a later call needs to\n // see it. Strategy: getReader(), read() one chunk, STASH the\n // result on entry.readBuf so sock_recv finds it pre-loaded.\n return (async () => {\n try {\n if (!entry.reader) entry.reader = entry.socket.readable.getReader();\n await entry.socket.opened;\n const { value, done } = await entry.reader.read();\n if (done) {\n entry.eof = true;\n entry.readBuf = new Uint8Array(0);\n entry.readBufOffset = 0;\n return {\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,\n nbytes: 0n, flags: 1, // HANGUP\n };\n }\n entry.readBuf = (value instanceof Uint8Array)\n ? value\n : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);\n entry.readBufOffset = 0;\n return {\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,\n nbytes: BigInt(entry.readBuf.length), flags: 0,\n };\n } catch (e) {\n return {\n idx: s.idx, error: __WASI_EIO, type: s.tag,\n nbytes: 0n, flags: 0,\n };\n }\n })();\n }\n return Promise.resolve({\n idx: s.idx, error: __WASI_EBADF, type: s.tag, nbytes: 0n, flags: 0,\n });\n });\n // Race them. After the FIRST one resolves, check whether ANY others\n // also have results pending (they could have resolved synchronously\n // before the race even began, e.g. always-ready file fds).\n const winnerResult = await Promise.race(promises);\n // Now collect all currently-resolved promises. Strategy: tag each\n // with a marker via Promise.race against a sentinel \u2014 anything\n // that resolved BEFORE OR AT the same time as the winner is ready.\n // Practical approach: race each remaining against a microtask\n // tick; if it resolves within that tick, it's \"concurrently ready\".\n const ready = [];\n ready.push(winnerResult);\n // Cancel any pending timers (their resolve callbacks are now moot\n // unless we want to drain them too \u2014 see below for the rule).\n // For correctness AND minimal latency, we DON'T cancel \u2014 we drain\n // any timer that has already fired (same tick) by checking\n // Promise.race against an already-resolved sentinel.\n const sentinel = Promise.resolve('__not-ready__');\n for (let i = 0; i < promises.length; i++) {\n if (i === winnerResult.idx) continue;\n // race against a microtask tick to detect \"already resolved\".\n // Use a single-microtask delay so synchronously-resolved promises\n // (always-ready file fds) are caught while still-pending ones\n // (socket reads, future timers) are skipped.\n const p = promises[i];\n const probed = await Promise.race([p, sentinel]);\n if (probed !== '__not-ready__') {\n ready.push(probed);\n } else {\n // Cancel timer for non-firing CLOCK subscriptions.\n if (timerIds[i] !== null) {\n try { clearTimeout(timerIds[i]); } catch {}\n timerIds[i] = null;\n }\n }\n }\n // Write events.\n let nevents = 0;\n for (const ev of ready) {\n const off = outEventsPtr + nevents * 32;\n // userdata\n const subUserdata = subs[ev.idx].userdata;\n dv.setBigUint64(off, subUserdata, true);\n // error u16\n dv.setUint16(off + 8, ev.error, true);\n // type u8\n dv.setUint8(off + 10, ev.type);\n // pad\n for (let p = 11; p < 16; p++) dv.setUint8(off + p, 0);\n // fd_readwrite.nbytes u64 (0 for clock, but write the field\n // anyway \u2014 caller reads union by type).\n dv.setBigUint64(off + 16, ev.nbytes || 0n, true);\n // fd_readwrite.flags u16\n dv.setUint16(off + 24, ev.flags || 0, true);\n // pad\n for (let p = 26; p < 32; p++) dv.setUint8(off + p, 0);\n nevents++;\n }\n writeU32LE(retNeventsPtr, nevents);\n return __WASI_ESUCCESS;\n },\n\n // Stream-B B7: sockets via cloudflare:sockets connect() + JSPI.\n //\n // sock_send/sock_recv/sock_shutdown are ASYNC fns. They await on\n // ReadableStream/WritableStream readers/writers, which means the\n // wasm caller's expectation of a sync errno return must be bridged\n // via WebAssembly.Suspending. The Suspending wrapper is applied in\n // the return statement below where the imports object is finalised\n // (not here \u2014 wrapping at definition site would shadow the bare\n // function in the imports map). The async functions below have\n // signature compatible with 'new WebAssembly.Suspending(asyncFn)':\n // they return Promise<i32 errno>.\n\n async sock_send(fd, siDataPtr, siDataLen, _siFlags, retDataLenPtr) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'socket') return __WASI_ENOTSOCK;\n if (entry.closed || entry.halfClosedWr) return __WASI_EPIPE;\n // Gather iovs into a single Uint8Array per the WASI ciovec_array shape.\n const dv = view();\n const memU8 = u8();\n let total = 0;\n const parts = [];\n for (let i = 0; i < siDataLen; i++) {\n const iov = siDataPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n if (bufLen > 0) parts.push(memU8.slice(bufPtr, bufPtr + bufLen));\n total += bufLen;\n }\n let combined;\n if (parts.length === 0) combined = new Uint8Array(0);\n else if (parts.length === 1) combined = parts[0];\n else {\n combined = new Uint8Array(total);\n let off = 0;\n for (const p of parts) { combined.set(p, off); off += p.length; }\n }\n try {\n if (!entry.writer) entry.writer = entry.socket.writable.getWriter();\n // Wait for the socket to be connected on first write. socket.opened\n // resolves after the TCP handshake completes; subsequent writes\n // are unblocked because opened is a stable resolved promise.\n await entry.socket.opened;\n await entry.writer.write(combined);\n } catch (e) {\n // Map common errors to spec errnos.\n const msg = (e && e.message) ? e.message : String(e);\n if (/refused|ECONNREFUSED/i.test(msg)) return __WASI_ECONNREFUSED;\n if (/unreach|EHOSTUNREACH/i.test(msg)) return __WASI_EHOSTUNREACH;\n return __WASI_EIO;\n }\n writeU32LE(retDataLenPtr, total);\n return __WASI_ESUCCESS;\n },\n\n async sock_recv(fd, riDataPtr, riDataLen, _riFlags, retDataLenPtr, retFlagsPtr) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'socket') return __WASI_ENOTSOCK;\n if (entry.closed) return __WASI_ENOTCONN;\n const dv = view();\n const memU8 = u8();\n // If the local readBuf is empty, fetch a chunk from the stream.\n if (entry.readBufOffset >= entry.readBuf.length && !entry.eof) {\n try {\n if (!entry.reader) entry.reader = entry.socket.readable.getReader();\n await entry.socket.opened;\n const { value, done } = await entry.reader.read();\n if (done) {\n entry.eof = true;\n entry.readBuf = new Uint8Array(0);\n entry.readBufOffset = 0;\n } else {\n entry.readBuf = (value instanceof Uint8Array)\n ? value\n : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);\n entry.readBufOffset = 0;\n }\n } catch (e) {\n const msg = (e && e.message) ? e.message : String(e);\n if (/refused|ECONNREFUSED/i.test(msg)) return __WASI_ECONNREFUSED;\n if (/unreach|EHOSTUNREACH/i.test(msg)) return __WASI_EHOSTUNREACH;\n return __WASI_EIO;\n }\n }\n // Copy from readBuf into the user's iovs, up to total request size.\n let total = 0;\n for (let i = 0; i < riDataLen; i++) {\n const iov = riDataPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n const remain = entry.readBuf.length - entry.readBufOffset;\n if (remain <= 0) break;\n const n = Math.min(bufLen, remain);\n memU8.set(entry.readBuf.subarray(entry.readBufOffset, entry.readBufOffset + n), bufPtr);\n entry.readBufOffset += n;\n total += n;\n if (n < bufLen) break;\n }\n writeU32LE(retDataLenPtr, total);\n if (typeof retFlagsPtr === 'number') {\n // ROFLAGS_RECV_DATA_TRUNCATED bit; we don't truncate in this impl.\n dv.setUint16(retFlagsPtr, 0, true);\n }\n return __WASI_ESUCCESS;\n },\n\n async sock_shutdown(fd, how) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'socket') return __WASI_ENOTSOCK;\n const wantRd = (how & __WASI_SDFLAGS_RD) !== 0;\n const wantWr = (how & __WASI_SDFLAGS_WR) !== 0;\n try {\n // Full close (SHUT_RDWR): genuinely close the underlying socket.\n // Both readable and writable streams are forcibly closed per CF\n // docs (https://developers.cloudflare.com/workers/runtime-apis/\n // tcp-sockets/#close-tcp-connections).\n if (wantRd && wantWr) {\n if (entry.writer) {\n try { await entry.writer.close(); } catch {}\n }\n if (entry.reader) {\n try { await entry.reader.cancel(); } catch {}\n }\n try { await entry.socket.close(); } catch {}\n entry.halfClosedWr = true;\n entry.eof = true;\n entry.closed = true;\n return __WASI_ESUCCESS;\n }\n // Half-close write-only (SHUT_WR): POSIX semantics expect the\n // peer to receive an EOF on its read side while WE can still\n // receive its remaining bytes. CF Workers' TCP socket API does\n // not expose a true POSIX half-close primitive \u2014 calling\n // writer.close() may tear down the underlying connection in\n // ways that prevent further reads, even with allowHalfOpen=true.\n //\n // Empirically observed on prod (sock-shutdown-write probe RED\n // after writer.close() with allowHalfOpen=true): subsequent\n // sock_recv returns 0 bytes because the readable side stops\n // delivering once the writer closes.\n //\n // Best-fit semantics under this constraint: SHUT_WR marks the\n // shim-side fd as half-closed-WR (subsequent sock_send returns\n // EPIPE), but does NOT call writer.close() on the underlying\n // socket. The peer will eventually see EOF when our socket is\n // fully closed (at fd_close or program exit). In the meantime,\n // sock_recv continues to drain the readable side correctly.\n // This trades request/response-protocol correctness (where the\n // peer expects EOF to know when the request is done) for\n // server-echo-protocol correctness (where the peer streams\n // back regardless). The probe is the latter case; documented\n // limit for the former: see docs/wasi-threads-infeasibility.md\n // and surrounding sock_* commentary.\n if (wantWr && !entry.halfClosedWr) {\n entry.halfClosedWr = true;\n }\n // Half-close read-only (SHUT_RD): cancel the reader to stop\n // delivery. This IS safe \u2014 cancelling the reader doesn't tear\n // down the underlying socket on the CF side.\n if (wantRd && !entry.eof) {\n if (entry.reader) {\n try { await entry.reader.cancel(); } catch {}\n }\n entry.eof = true;\n }\n } catch (e) {\n return __WASI_EIO;\n }\n return __WASI_ESUCCESS;\n },\n };\n\n // Stream-B B7: wrap the async socket imports in WebAssembly.Suspending\n // so the wasm caller can use sync-shape calls that yield to the JS\n // event loop. Requires V8 14.2+ (workerd Oct 2025+) \u2014 see\n // /workspace/.seal-internal/2026-05-11-jspi-spike/findings.md.\n // If Suspending isn't available (older runtime), socket imports remain\n // async fns that the wasm boundary will reject with a trap \u2014 caller\n // sees a clean failure via __wasiRunStartAsync's catch block.\n if (typeof WebAssembly !== 'undefined' && typeof WebAssembly.Suspending === 'function') {\n imports.sock_send = new WebAssembly.Suspending(imports.sock_send);\n imports.sock_recv = new WebAssembly.Suspending(imports.sock_recv);\n imports.sock_shutdown = new WebAssembly.Suspending(imports.sock_shutdown);\n // Stream-B B8: poll_oneoff also needs JSPI to support CLOCK\n // subscriptions (await setTimeout) and socket-fd readiness\n // (await reader.read()). Same Suspending shape as sock_*.\n imports.poll_oneoff = new WebAssembly.Suspending(imports.poll_oneoff);\n }\n\n return {\n wasiImport: imports,\n getStdout: () => stdoutBuf,\n getStderr: () => stderrBuf,\n };\n}\n\nfunction __wasiRunStart(instance, ctx) {\n try {\n const start = instance.exports._start;\n if (typeof start !== 'function') {\n return { exitCode: 1, error: '_start is not a function (got ' + typeof start + ')' };\n }\n start();\n return { exitCode: 0 };\n } catch (e) {\n if (e && e.constructor && e.constructor.name === '__WasiExit') {\n return { exitCode: e.code };\n }\n return { exitCode: 1, error: (e && e.message) ? e.message : String(e) };\n }\n}\n\n// Stream-B (P3/P4 forward-decl): async variant of runStart. Wraps _start\n// with WebAssembly.promising so suspending imports (sock_send, sock_recv,\n// poll_oneoff) can await Promises and have V8 suspend+resume the wasm\n// stack. Returns the SAME shape as __wasiRunStart but in a Promise.\n//\n// In P2 (this commit) no suspending imports exist yet \u2014 invoking this\n// function is functionally identical to __wasiRunStart but returns a\n// Promise. P3 (sockets) and P4 (poll_oneoff) wire the actual Suspending\n// imports + this async entrypoint together.\n//\n// Caller contract: legacy callers that need a sync return (ruby-runner)\n// keep using __wasiRunStart. New callers (wasm-runner WASI mode, post-\n// P3) switch to await __wasiRunStartAsync(...).\nasync function __wasiRunStartAsync(instance, ctx) {\n try {\n const start = instance.exports._start;\n if (typeof start !== 'function') {\n return { exitCode: 1, error: '_start is not a function (got ' + typeof start + ')' };\n }\n // WebAssembly.promising wraps the export so it returns a Promise<void>\n // that resolves when the (potentially-suspending) wasm computation\n // completes. Available since V8 14.2 (workerd Oct 2025) \u2014 see\n // /workspace/.seal-internal/2026-05-11-jspi-spike/findings.md.\n //\n // If promising isn't available (older runtimes), fall through to a\n // direct call \u2014 fully spec-compatible for non-suspending _start.\n if (typeof WebAssembly !== 'undefined' && typeof WebAssembly.promising === 'function') {\n const promisingStart = WebAssembly.promising(start);\n await promisingStart();\n } else {\n start();\n }\n return { exitCode: 0 };\n } catch (e) {\n if (e && e.constructor && e.constructor.name === '__WasiExit') {\n return { exitCode: e.code };\n }\n return { exitCode: 1, error: (e && e.message) ? e.message : String(e) };\n }\n}\n// \u2500\u2500 END: wasi-instance preamble \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n";
109
+ export declare const WASI_INSTANCE_PREAMBLE_SRC = "\n// \u2500\u2500 BEGIN: wasi-instance preamble (core WASI + filesystem WASI) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Hand-written WASI snapshot_preview1 shim for Nimbus.\n// Source mirror: src/runtime/wasi-instance.ts. Keep in sync by hand.\n\n// errno constants\nconst __WASI_ESUCCESS = 0;\nconst __WASI_EAGAIN = 6;\nconst __WASI_EBADF = 8;\nconst __WASI_ECONNREFUSED = 14;\nconst __WASI_EEXIST = 20;\nconst __WASI_EHOSTUNREACH = 23;\nconst __WASI_EINVAL = 28;\nconst __WASI_EIO = 29;\nconst __WASI_EISDIR = 31;\nconst __WASI_ELOOP = 32;\nconst __WASI_ENOENT = 44;\nconst __WASI_ENOSYS = 52;\nconst __WASI_ENOTCONN = 53;\nconst __WASI_ENOTDIR = 54;\nconst __WASI_ENOTEMPTY = 55;\nconst __WASI_ENOTSOCK = 57;\nconst __WASI_EPIPE = 64;\nconst __WASI_ENOTCAPABLE = 76;\n// clock ids\nconst __WASI_CLOCK_REALTIME = 0;\nconst __WASI_CLOCK_MONOTONIC = 1;\nconst __WASI_CLOCK_PROCESS_CPUTIME_ID = 2;\nconst __WASI_CLOCK_THREAD_CPUTIME_ID = 3;\n// oflags\nconst __WASI_O_CREAT = 1;\nconst __WASI_O_DIRECTORY = 2;\nconst __WASI_O_EXCL = 4;\nconst __WASI_O_TRUNC = 8;\n// lookupflags (passed to path_open, path_filestat_get, etc.)\nconst __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW = 1;\n// fstflags (filestat_set_times)\nconst __WASI_FSTFLAGS_ATIM = 1;\nconst __WASI_FSTFLAGS_ATIM_NOW = 2;\nconst __WASI_FSTFLAGS_MTIM = 4;\nconst __WASI_FSTFLAGS_MTIM_NOW = 8;\n// filetypes\nconst __WASI_FT_UNKNOWN = 0;\nconst __WASI_FT_BLOCK_DEVICE = 1;\nconst __WASI_FT_CHARACTER_DEVICE = 2;\nconst __WASI_FT_DIRECTORY = 3;\nconst __WASI_FT_REGULAR_FILE = 4;\nconst __WASI_FT_SOCKET_DGRAM = 5;\nconst __WASI_FT_SOCKET_STREAM = 6;\nconst __WASI_FT_SYMBOLIC_LINK = 7;\n// preopen types\nconst __WASI_PREOPENTYPE_DIR = 0;\n// Symlink resolution loop limit (POSIX SYMLOOP_MAX is typically 8-40).\nconst __WASI_SYMLOOP_MAX = 40;\n// Default per-fd rights mask (wide-open).\nconst __WASI_RIGHTS_ALL = 0xFFFFFFFFFFFFFFFFn;\n// WASI socket and polling support B7: sock_shutdown SD flags.\nconst __WASI_SDFLAGS_RD = 1;\nconst __WASI_SDFLAGS_WR = 2;\n// WASI socket and polling support B8: poll_oneoff subscription / event types.\nconst __WASI_EVENTTYPE_CLOCK = 0;\nconst __WASI_EVENTTYPE_FD_READ = 1;\nconst __WASI_EVENTTYPE_FD_WRITE = 2;\nconst __WASI_SUBCLOCKFLAGS_ABSTIME = 1; // SUBSCRIPTION_CLOCK_ABSTIME\n// WASI socket and polling support B7: synthetic-path prefix recognised by path_open as a request\n// to open a TCP socket. Mirrors bash's /dev/tcp/<host>/<port> convention\n// (https://www.gnu.org/software/bash/manual/html_node/Redirections.html).\nconst __WASI_TCP_PATH_PREFIX = '/dev/tcp/';\n\n// WASI socket and polling support B7: resolved at preamble module-init via dynamic import.\n// CF docs: \"TCP sockets cannot be created in global scope and shared\n// across requests\" \u2014 so we only IMPORT the module here (cheap symbol\n// resolution); the actual connect() call lives inside path_open which\n// runs at facet-handler time (i.e., within WorkerEntrypoint.execute()).\nlet __cfSocketConnect = null;\n// Top-level await is supported in workerd ES modules at module-init.\n// If import fails (no cloudflare:sockets binding, or running under a\n// runtime that doesn't expose it), __cfSocketConnect stays null and\n// path_open('/dev/tcp/...') returns ENOSYS with a clear diagnostic.\ntry {\n const __mod = await import('cloudflare:sockets');\n __cfSocketConnect = __mod.connect;\n} catch (__e) {\n // fail-soft: socket support disabled this call.\n}\n\nclass __WasiExit { constructor(code) { this.code = code | 0; } }\n\n// \u2500\u2500\u2500 Virtual filesystem state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// __wasiInitFS({ root, preopens, files, dirs, times?, symlinks? }) \u2014\n// install a per-call FS:\n// root string \u2014 canonical session root, e.g. 'home/user/wasi-files'.\n// preopens Array<{ wasiPath, vfsPath }> \u2014 fd>=3 preopens (in order).\n// files Record<vfsPath, base64 string> \u2014 initial file contents.\n// dirs Array<vfsPath> \u2014 initial directory list.\n// times Record<vfsPath, {mtime, atime, ctime}> | undefined\n// \u2014 per-path nanosecond timestamps (WASI socket and polling support B1). Optional;\n// paths without entries default to \"now\" at init time.\n// Values are decimal strings to survive JSON.serialize\n// (BigInt is not JSON-safe); converted to BigInt internally.\n// symlinks Record<vfsPath, targetPath> | undefined\n// \u2014 WASI socket and polling support B3 in-memory symlink table. Optional; defaults\n// to empty. Target paths are canonicalized at insertion.\n//\n// After _start returns, __wasiSnapshotFS() extracts:\n// {\n// filesWritten: Record<vfsPath, base64 string>, // new + modified\n// filesDeleted: string[], // unlinked\n// dirsCreated: string[], // mkdir'd\n// dirsDeleted: string[], // rmdir'd\n// timesChanged: Record<vfsPath, {mtime, atime, ctime}> // B1 \u2014 new or modified mtime/atime\n// symlinksCreated: Record<vfsPath, targetPath> // B3 \u2014 new symlinks\n// symlinksDeleted: string[] // B3 \u2014 removed symlinks\n// }\n//\n// Backward-compat: all new fields are additive. Callers that don't pass\n// 'times'/'symlinks' to __wasiInitFS see identical behavior to pre-B1.\n// Supervisors that don't read 'timesChanged'/'symlinks*' from the\n// snapshot see identical behavior to pre-B1.\n\nlet __wasiFS = null; // populated by __wasiInitFS\nlet __wasiPreopens = []; // [{ wasiPath, vfsPath, fd }, ...]\n\n// All paths are stored in canonical form: no leading '/', no '..', no\n// double slashes. The wasm program sees '/foo/bar.txt' (with leading\n// slash) but the VFS keys are 'home/user/.../foo/bar.txt'.\nfunction __wasiCanonicalize(p) {\n if (!p) return '';\n const parts = p.split('/');\n const out = [];\n for (const seg of parts) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') { if (out.length > 0) out.pop(); continue; }\n out.push(seg);\n }\n return out.join('/');\n}\n\n// Wall-clock at init, used as the default mtime/atime/ctime for any path\n// that arrives without an explicit times entry. Captured once per call so\n// all \"default-init\" timestamps within a call share a value (mtime ==\n// atime == ctime), matching what a real cold-load would produce.\nfunction __wasiNowNs() {\n // Date.now() is ms since epoch; multiply by 1e6 \u2192 ns.\n return BigInt(Date.now()) * 1000000n;\n}\nfunction __wasiInitFS(opts) {\n const files = new Map(); // canonicalVfsPath \u2192 Uint8Array\n const dirs = new Set(); // canonicalVfsPath\n // WASI socket and polling support B1: parallel timestamp + symlink maps.\n const times = new Map(); // canonicalVfsPath \u2192 {mtime: BigInt, atime: BigInt, ctime: BigInt}\n const symlinks = new Map(); // canonicalVfsPath \u2192 targetPath (canonical)\n // mirror originals for diff at flush time\n const origFiles = new Map();\n const origDirs = new Set();\n const origTimes = new Map();\n const origSymlinks = new Map();\n const nowNs = __wasiNowNs();\n for (const [path, b64] of Object.entries(opts.files || {})) {\n const canon = __wasiCanonicalize(path);\n const bin = atob(b64);\n const u8 = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);\n files.set(canon, u8);\n origFiles.set(canon, u8.slice()); // copy so subsequent mutations detect change\n // Default timestamps (overwritten below if opts.times has this path).\n const t = { mtime: nowNs, atime: nowNs, ctime: nowNs };\n times.set(canon, t);\n origTimes.set(canon, { mtime: t.mtime, atime: t.atime, ctime: t.ctime });\n }\n for (const path of opts.dirs || []) {\n const canon = __wasiCanonicalize(path);\n dirs.add(canon);\n origDirs.add(canon);\n if (!times.has(canon)) {\n const t = { mtime: nowNs, atime: nowNs, ctime: nowNs };\n times.set(canon, t);\n origTimes.set(canon, { mtime: t.mtime, atime: t.atime, ctime: t.ctime });\n }\n }\n // WASI socket and polling support B1: load explicit per-path timestamps if supervisor provided them.\n // Values arrive as decimal strings (BigInt \u2192 JSON.stringify safe) or numbers.\n for (const [path, t] of Object.entries(opts.times || {})) {\n const canon = __wasiCanonicalize(path);\n const cur = times.get(canon);\n if (!cur) continue; // path not yet in files/dirs; skip\n const mtime = (t && t.mtime !== undefined) ? BigInt(t.mtime) : cur.mtime;\n const atime = (t && t.atime !== undefined) ? BigInt(t.atime) : cur.atime;\n const ctime = (t && t.ctime !== undefined) ? BigInt(t.ctime) : cur.ctime;\n times.set(canon, { mtime, atime, ctime });\n origTimes.set(canon, { mtime, atime, ctime });\n }\n // WASI socket and polling support B3: load explicit symlinks if supervisor provided them.\n for (const [path, target] of Object.entries(opts.symlinks || {})) {\n const canon = __wasiCanonicalize(path);\n symlinks.set(canon, String(target));\n origSymlinks.set(canon, String(target));\n }\n __wasiFS = {\n root: __wasiCanonicalize(opts.root || ''),\n files, dirs, times, symlinks,\n origFiles, origDirs, origTimes, origSymlinks,\n };\n // Reset fd table baseline; install preopens as fd 3, 4, 5, ...\n __wasiPreopens = [];\n fdTable.clear();\n fdTable.set(0, { kind: 'stdin' });\n fdTable.set(1, { kind: 'stdout' });\n fdTable.set(2, { kind: 'stderr' });\n nextFd = 3;\n for (const po of (opts.preopens || [])) {\n const fd = nextFd++;\n const vfsPath = __wasiCanonicalize(po.vfsPath);\n fdTable.set(fd, { kind: 'preopen', wasiPath: po.wasiPath, vfsPath });\n __wasiPreopens.push({ fd, wasiPath: po.wasiPath, vfsPath });\n if (!dirs.has(vfsPath)) dirs.add(vfsPath);\n if (!times.has(vfsPath)) {\n const t = { mtime: nowNs, atime: nowNs, ctime: nowNs };\n times.set(vfsPath, t);\n origTimes.set(vfsPath, { mtime: t.mtime, atime: t.atime, ctime: t.ctime });\n }\n }\n}\n\n// WASI socket and polling support B7 helper: open a TCP socket via cloudflare:sockets when\n// path_open is invoked on a /dev/tcp/<host>/<port> synthetic path.\n// Returns a WASI errno. Allocates a new fd of kind:'socket' on success\n// and writes it to fdOutPtr.\nfunction __wasiOpenTcpSocket(pathArg, fdflags, fdOutPtr, writeU32LE) {\n if (typeof __cfSocketConnect !== 'function') {\n // cloudflare:sockets unavailable in this runtime. Report ENOSYS so\n // the user program sees a clear errno rather than a hang.\n return __WASI_ENOSYS;\n }\n // pathArg shape: \"/dev/tcp/<host>/<port>\".\n const tail = pathArg.substring(__WASI_TCP_PATH_PREFIX.length);\n const slashIdx = tail.lastIndexOf('/');\n if (slashIdx <= 0 || slashIdx === tail.length - 1) return __WASI_EINVAL;\n const host = tail.substring(0, slashIdx);\n const portStr = tail.substring(slashIdx + 1);\n const port = parseInt(portStr, 10);\n if (!host || !(port > 0 && port < 65536)) return __WASI_EINVAL;\n let socket;\n try {\n // Per CF docs: connect() is sync (returns Socket immediately); the\n // socket.opened promise resolves when the TCP handshake completes.\n // We do NOT await opened here \u2014 sock_send/sock_recv will await it\n // implicitly via the writable/readable streams (or via socket.opened\n // before the first read/write).\n //\n // Prod-verify-fix: pass allowHalfOpen=true. Default is false,\n // which makes the writable side close automatically on EOF AND\n // (per empirical prod behaviour) makes a manual writer.close()\n // affect readable-side delivery. POSIX shutdown(SHUT_WR) requires\n // half-close semantics: the user can stop sending while still\n // receiving the peer's response. tcpbin.com:4242 (and most\n // request/response protocols) rely on this \u2014 client signals EOF\n // via half-close, server completes its echo, client reads it.\n // See https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#socketoptions\n socket = __cfSocketConnect({ hostname: host, port }, { allowHalfOpen: true });\n } catch (e) {\n // Synchronous errors from connect() (e.g. invalid address). Surface\n // as ECONNREFUSED since the user-observable behavior is the same.\n return __WASI_ECONNREFUSED;\n }\n const fd = nextFd++;\n fdTable.set(fd, {\n kind: 'socket',\n socket,\n reader: null, // lazy: getReader() on first sock_recv\n writer: null, // lazy: getWriter() on first sock_send\n readBuf: new Uint8Array(0),\n readBufOffset: 0,\n eof: false,\n closed: false,\n halfClosedWr: false,\n fdflags: fdflags | 0,\n });\n writeU32LE(fdOutPtr, fd);\n return __WASI_ESUCCESS;\n}\n\n// WASI socket and polling support B1+B2 helpers: update tracked timestamps for a path. Idempotent.\n// Caller passes nanosecond BigInt(s); pass null for fields to keep unchanged.\nfunction __wasiTouchTimes(canonPath, mtimeNs, atimeNs, ctimeNs) {\n if (!__wasiFS) return;\n const cur = __wasiFS.times.get(canonPath);\n if (cur) {\n if (mtimeNs !== null) cur.mtime = mtimeNs;\n if (atimeNs !== null) cur.atime = atimeNs;\n if (ctimeNs !== null) cur.ctime = ctimeNs;\n } else {\n const now = __wasiNowNs();\n __wasiFS.times.set(canonPath, {\n mtime: mtimeNs !== null ? mtimeNs : now,\n atime: atimeNs !== null ? atimeNs : now,\n ctime: ctimeNs !== null ? ctimeNs : now,\n });\n }\n}\n// Convenience: bump mtime + ctime to \"now\" on a write/create.\nfunction __wasiBumpMtime(canonPath) {\n const now = __wasiNowNs();\n __wasiTouchTimes(canonPath, now, null, now);\n}\n\nfunction __wasiSnapshotFS() {\n if (!__wasiFS) return null;\n const filesWritten = {};\n const filesDeleted = [];\n const dirsCreated = [];\n const dirsDeleted = [];\n const timesChanged = {}; // WASI socket and polling support B1: mtime/atime/ctime diffs\n const symlinksCreated = {}; // WASI socket and polling support B3: new symlinks\n const symlinksDeleted = []; // WASI socket and polling support B3: removed symlinks\n // files: anything in __wasiFS.files not in orig, or whose bytes differ.\n for (const [path, bytes] of __wasiFS.files) {\n const orig = __wasiFS.origFiles.get(path);\n let same = !!orig && orig.length === bytes.length;\n if (same) {\n for (let i = 0; i < bytes.length; i++) {\n if (orig[i] !== bytes[i]) { same = false; break; }\n }\n }\n if (!same) {\n // base64 encode\n let s = '';\n for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);\n filesWritten[path] = btoa(s);\n }\n }\n // files: anything in orig not in current\n for (const path of __wasiFS.origFiles.keys()) {\n if (!__wasiFS.files.has(path)) filesDeleted.push(path);\n }\n // dirs\n for (const path of __wasiFS.dirs) {\n if (!__wasiFS.origDirs.has(path)) dirsCreated.push(path);\n }\n for (const path of __wasiFS.origDirs) {\n if (!__wasiFS.dirs.has(path)) dirsDeleted.push(path);\n }\n // WASI socket and polling support B1: times diff. BigInt \u2192 decimal string (JSON.stringify-safe).\n for (const [path, t] of __wasiFS.times) {\n const orig = __wasiFS.origTimes.get(path);\n if (!orig || orig.mtime !== t.mtime || orig.atime !== t.atime || orig.ctime !== t.ctime) {\n timesChanged[path] = {\n mtime: t.mtime.toString(),\n atime: t.atime.toString(),\n ctime: t.ctime.toString(),\n };\n }\n }\n // WASI socket and polling support B3: symlink diff.\n for (const [path, target] of __wasiFS.symlinks) {\n if (__wasiFS.origSymlinks.get(path) !== target) {\n symlinksCreated[path] = target;\n }\n }\n for (const path of __wasiFS.origSymlinks.keys()) {\n if (!__wasiFS.symlinks.has(path)) symlinksDeleted.push(path);\n }\n return {\n filesWritten, filesDeleted, dirsCreated, dirsDeleted,\n timesChanged, symlinksCreated, symlinksDeleted,\n };\n}\n\n// \u2500\u2500\u2500 fd table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Entry shapes:\n// { kind: 'stdin' | 'stdout' | 'stderr' }\n// { kind: 'preopen', wasiPath, vfsPath, rights? }\n// { kind: 'file', vfsPath, offset, oflags, fdflags, rights? }\n// { kind: 'dir', vfsPath, readdirEntries: null | Array, cookie, rights? }\n// { kind: 'symlink', vfsPath, target, rights? } // WASI socket and polling support B3 (only when O_NOFOLLOW)\n//\n// WASI socket and polling support B6: 'rights' is an optional BigInt mask. When set, fd_fdstat_get\n// returns it (instead of the wide-open default). fd_fdstat_set_rights\n// writes to it. The mask is advisory in this shim (we don't enforce per-fn\n// rights checks in v1 \u2014 single-tenant facet, no untrusted callers).\nconst fdTable = new Map();\nlet nextFd = 3;\n\n// Resolve a WASI path against a preopen fd; returns the canonical VFS path\n// WITHOUT following symlinks. WASI socket and polling support kept this name compatible with all\n// existing call sites (~10 of them inside the imports object).\n//\n// wasi-path-fix: a single preopen { wasiPath: '/', vfsPath: 'home/user' }\n// exposes the user's home as the wasm-side root. wasi-libc strips '/'\n// from absolute paths and hands the rest to path_open. For \"/tmp/x\" the\n// stripped path \"tmp/x\" + vfsPath \"home/user\" yields \"home/user/tmp/x\"\n// (correct). For \"/home/user/x\" the stripped path \"home/user/x\" +\n// vfsPath \"home/user\" yields \"home/user/home/user/x\" \u2014 the file lands\n// at the wrong location. The user mental model is that \"/home/user/x\"\n// IS the user's file at home/user/x in the VFS.\n//\n// Fix: in __wasiResolvePath, when baseFd is a preopen AND the supplied\n// pathStr already starts with the preopen's vfsPath (followed by '/' or\n// end-of-string), strip the vfsPath prefix BEFORE prepending it. This\n// makes \"/home/user/x\" \u2192 wasi-libc strip \u2192 \"home/user/x\" \u2192 resolver\n// strip vfsPath \u2192 \"x\" \u2192 final canonical \"home/user/x\" \u2713. Paths that\n// don't share the vfsPath prefix (like \"tmp/x\") follow the legacy path.\n//\n// Skip the strip when vfsPath is empty (no chroot mismatch possible).\n// Be careful with segment boundaries: vfsPath \"home/user\" must NOT\n// strip from path \"home/userfoo/bar\" (it's a different directory).\nfunction __wasiResolvePath(baseFd, pathStr) {\n const entry = fdTable.get(baseFd);\n if (!entry) return null;\n let baseVfs;\n if (entry.kind === 'preopen' || entry.kind === 'dir') baseVfs = entry.vfsPath;\n else return null;\n // Strip leading './' or '/' segments (POSIX legacy from wasi-libc).\n let trimmed = pathStr.replace(/^\\.?\\/+/, '').replace(/^\\/+/, '');\n // wasi-path-fix: chroot-collision strip. Only applies to preopens (not\n // 'dir' fds \u2014 those are opened via path_open with already-resolved\n // paths). Only when baseVfs is non-empty AND trimmed starts with\n // baseVfs at a segment boundary.\n if (entry.kind === 'preopen' && baseVfs.length > 0) {\n if (trimmed === baseVfs) {\n trimmed = '';\n } else if (trimmed.length > baseVfs.length &&\n trimmed.charCodeAt(baseVfs.length) === 47 /* '/' */ &&\n trimmed.substring(0, baseVfs.length) === baseVfs) {\n trimmed = trimmed.substring(baseVfs.length + 1);\n }\n }\n return __wasiCanonicalize(baseVfs + '/' + trimmed);\n}\n\n// WASI socket and polling support B3: resolve a path WITH optional symlink-follow + POSIX loop\n// detection. Returns {path: canonical, isSymlink: bool, err?: errno}.\n// - If followFlag is true (default \u2014 matches LOOKUPFLAGS_SYMLINK_FOLLOW),\n// walks the symlink chain up to __WASI_SYMLOOP_MAX depth. ELOOP if exceeded.\n// - If followFlag is false, returns the bare resolved path; if the path\n// itself is in __wasiFS.symlinks, isSymlink=true so callers know to\n// open it as a symlink fd rather than a regular file.\n// Errors return {path: '', isSymlink: false, err: __WASI_E...}.\nfunction __wasiResolvePathFull(baseFd, pathStr, followFlag) {\n let p = __wasiResolvePath(baseFd, pathStr);\n if (p === null) return { path: '', isSymlink: false, err: __WASI_EBADF };\n if (!__wasiFS) return { path: p, isSymlink: false };\n if (!followFlag) {\n return { path: p, isSymlink: __wasiFS.symlinks.has(p), err: __WASI_ESUCCESS };\n }\n // Follow symlinks with bounded loop counter.\n let depth = 0;\n while (__wasiFS.symlinks.has(p)) {\n if (depth++ >= __WASI_SYMLOOP_MAX) {\n return { path: p, isSymlink: true, err: __WASI_ELOOP };\n }\n const target = __wasiFS.symlinks.get(p);\n // Target may be relative or absolute. Absolute targets (leading '/')\n // resolve against the same preopen root as the original path; relative\n // targets resolve against the directory containing the symlink.\n let next;\n if (target.startsWith('/')) {\n // Resolve against the preopen of baseFd.\n const e = fdTable.get(baseFd);\n const baseVfs = (e && (e.kind === 'preopen' || e.kind === 'dir')) ? e.vfsPath : '';\n next = __wasiCanonicalize(baseVfs + '/' + target);\n } else {\n const lastSlash = p.lastIndexOf('/');\n const parent = lastSlash >= 0 ? p.substring(0, lastSlash) : '';\n next = __wasiCanonicalize(parent + '/' + target);\n }\n p = next;\n }\n return { path: p, isSymlink: false, err: __WASI_ESUCCESS };\n}\n\n// \u2500\u2500\u2500 makeImports \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction __wasiMakeImports(opts) {\n // opts: { argv, env, getMemory, stdoutWrite?, stderrWrite? }\n const argv = opts.argv || [];\n const envArr = [];\n if (opts.env) for (const k of Object.keys(opts.env)) envArr.push(k + '=' + opts.env[k]);\n const utf8enc = new TextEncoder();\n const utf8dec = new TextDecoder();\n\n function view() { return new DataView(opts.getMemory().buffer); }\n function u8() { return new Uint8Array(opts.getMemory().buffer); }\n function writeU32LE(off, v) { view().setUint32(off, v >>> 0, true); }\n function writeU64LE(off, v) {\n const dv = view();\n if (typeof v === 'bigint') { dv.setBigUint64(off, v, true); return; }\n const lo = (v >>> 0);\n const hi = Math.floor(v / 4294967296) >>> 0;\n dv.setUint32(off, lo, true);\n dv.setUint32(off + 4, hi, true);\n }\n function readPath(ptr, len) {\n const bytes = u8().subarray(ptr, ptr + len);\n return utf8dec.decode(bytes);\n }\n\n let stdoutBuf = '';\n let stderrBuf = '';\n function appendStream(kind, bytes) {\n const s = utf8dec.decode(bytes);\n if (kind === 'stdout') {\n stdoutBuf += s;\n if (opts.stdoutWrite) opts.stdoutWrite(s);\n } else {\n stderrBuf += s;\n if (opts.stderrWrite) opts.stderrWrite(s);\n }\n }\n\n // \u2500\u2500 Helpers operating on the VFS state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n function getFile(vfsPath) {\n if (!__wasiFS) return null;\n return __wasiFS.files.get(vfsPath) || null;\n }\n function hasDir(vfsPath) {\n if (!__wasiFS) return false;\n if (__wasiFS.dirs.has(vfsPath)) return true;\n // Root preopens implicitly exist as dirs.\n return false;\n }\n function setFile(vfsPath, bytes) {\n __wasiFS.files.set(vfsPath, bytes);\n // WASI socket and polling support B1: bump mtime+ctime on every write. atime stays as-is\n // (read paths bump atime explicitly via touchAccess()).\n __wasiBumpMtime(vfsPath);\n }\n function unsetFile(vfsPath) {\n __wasiFS.files.delete(vfsPath);\n __wasiFS.times.delete(vfsPath);\n }\n function touchAccess(vfsPath) {\n // WASI socket and polling support B1: bump atime on a read. mtime/ctime unchanged.\n if (!__wasiFS) return;\n const cur = __wasiFS.times.get(vfsPath);\n if (cur) cur.atime = __wasiNowNs();\n }\n function ensureParentDirs(vfsPath) {\n // Ensure all ancestor dirs of vfsPath exist (they should, but mkdir -p\n // semantics are handled in path_create_directory by callers).\n const parts = vfsPath.split('/');\n if (parts.length <= 1) return;\n let p = '';\n for (let i = 0; i < parts.length - 1; i++) {\n p = p ? (p + '/' + parts[i]) : parts[i];\n __wasiFS.dirs.add(p);\n // WASI socket and polling support B1: ensure ancestor dirs have a times entry.\n if (!__wasiFS.times.has(p)) {\n const now = __wasiNowNs();\n __wasiFS.times.set(p, { mtime: now, atime: now, ctime: now });\n }\n }\n }\n function readdirChildren(vfsPath) {\n const out = [];\n const prefix = vfsPath === '' ? '' : vfsPath + '/';\n // Files\n for (const path of __wasiFS.files.keys()) {\n if (path.startsWith(prefix)) {\n const rest = path.substring(prefix.length);\n if (rest && rest.indexOf('/') === -1) {\n out.push({ name: rest, type: __WASI_FT_REGULAR_FILE });\n }\n }\n }\n // Dirs\n for (const path of __wasiFS.dirs) {\n if (path !== vfsPath && path.startsWith(prefix)) {\n const rest = path.substring(prefix.length);\n if (rest && rest.indexOf('/') === -1) {\n out.push({ name: rest, type: __WASI_FT_DIRECTORY });\n }\n }\n }\n return out;\n }\n\n const imports = {\n // \u2500\u2500 args / env \u2500\u2500\n args_get(argvPtr, argvBufPtr) {\n let buf = argvBufPtr;\n const memU8 = u8();\n const dv = view();\n for (let i = 0; i < argv.length; i++) {\n dv.setUint32(argvPtr + i * 4, buf, true);\n const bytes = utf8enc.encode(argv[i] + '\\0');\n memU8.set(bytes, buf);\n buf += bytes.length;\n }\n return __WASI_ESUCCESS;\n },\n args_sizes_get(argcPtr, sizePtr) {\n let total = 0;\n for (let i = 0; i < argv.length; i++) total += utf8enc.encode(argv[i]).length + 1;\n writeU32LE(argcPtr, argv.length);\n writeU32LE(sizePtr, total);\n return __WASI_ESUCCESS;\n },\n environ_get(environPtr, envBufPtr) {\n let buf = envBufPtr;\n const memU8 = u8();\n const dv = view();\n for (let i = 0; i < envArr.length; i++) {\n dv.setUint32(environPtr + i * 4, buf, true);\n const bytes = utf8enc.encode(envArr[i] + '\\0');\n memU8.set(bytes, buf);\n buf += bytes.length;\n }\n return __WASI_ESUCCESS;\n },\n environ_sizes_get(envcPtr, sizePtr) {\n let total = 0;\n for (let i = 0; i < envArr.length; i++) total += utf8enc.encode(envArr[i]).length + 1;\n writeU32LE(envcPtr, envArr.length);\n writeU32LE(sizePtr, total);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 fd basic \u2500\u2500\n fd_close(fd) {\n if (fd === 0 || fd === 1 || fd === 2) return __WASI_ESUCCESS;\n if (!fdTable.has(fd)) return __WASI_EBADF;\n // Don't delete preopens \u2014 they're meant to persist for the program lifetime.\n const entry = fdTable.get(fd);\n if (entry.kind === 'preopen') return __WASI_ESUCCESS;\n // WASI socket and polling support B7: best-effort close of socket streams. The actual\n // socket.close() is fire-and-forget (sync return per WASI) \u2014 if\n // the program wants to await closure it should sock_shutdown\n // first. We DO drop the fd-table entry so subsequent ops on this\n // fd see EBADF.\n if (entry.kind === 'socket' && !entry.closed) {\n try { entry.socket.close(); } catch {}\n entry.closed = true;\n }\n fdTable.delete(fd);\n return __WASI_ESUCCESS;\n },\n\n fd_read(fd, iovsPtr, iovsLen, nreadPtr) {\n if (fd === 0) { writeU32LE(nreadPtr, 0); return __WASI_ESUCCESS; }\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const file = getFile(entry.vfsPath);\n if (!file) return __WASI_ENOENT;\n const dv = view();\n const memU8 = u8();\n let total = 0;\n for (let i = 0; i < iovsLen; i++) {\n const iov = iovsPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n const remain = file.length - entry.offset;\n if (remain <= 0) break;\n const n = Math.min(bufLen, remain);\n memU8.set(file.subarray(entry.offset, entry.offset + n), bufPtr);\n entry.offset += n;\n total += n;\n if (n < bufLen) break;\n }\n writeU32LE(nreadPtr, total);\n return __WASI_ESUCCESS;\n },\n\n fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {\n const dv = view();\n const memU8 = u8();\n // Gather all iov bytes\n let total = 0;\n const parts = [];\n for (let i = 0; i < iovsLen; i++) {\n const iov = iovsPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n if (bufLen > 0) parts.push(memU8.slice(bufPtr, bufPtr + bufLen));\n total += bufLen;\n }\n let combined;\n if (parts.length === 0) combined = new Uint8Array(0);\n else if (parts.length === 1) combined = parts[0];\n else {\n combined = new Uint8Array(total);\n let off = 0;\n for (const p of parts) { combined.set(p, off); off += p.length; }\n }\n if (fd === 1 || fd === 2) {\n appendStream(fd === 1 ? 'stdout' : 'stderr', combined);\n writeU32LE(nwrittenPtr, total);\n return __WASI_ESUCCESS;\n }\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const file = getFile(entry.vfsPath) || new Uint8Array(0);\n // Splice combined into file at entry.offset\n const newLen = Math.max(file.length, entry.offset + total);\n const next = new Uint8Array(newLen);\n next.set(file, 0);\n next.set(combined, entry.offset);\n setFile(entry.vfsPath, next);\n entry.offset += total;\n writeU32LE(nwrittenPtr, total);\n return __WASI_ESUCCESS;\n },\n\n fd_seek(fd, offsetArg, whence, newOffsetPtr) {\n // WASI/V8 passes i64 args as BigInt. core WASI mistakenly assumed\n // (lo, hi) i32 pairs; that worked only because hello-world never\n // exercised seek. filesystem WASI: accept BigInt directly.\n if (fd === 0 || fd === 1 || fd === 2) {\n writeU64LE(newOffsetPtr, 0n);\n return __WASI_ESUCCESS;\n }\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const delta = typeof offsetArg === 'bigint' ? offsetArg : BigInt(offsetArg | 0);\n const cur = BigInt(entry.offset);\n const file = getFile(entry.vfsPath);\n const fileLen = file ? BigInt(file.length) : 0n;\n let next;\n if (whence === 0) next = delta;\n else if (whence === 1) next = cur + delta;\n else if (whence === 2) next = fileLen + delta;\n else return __WASI_EINVAL;\n if (next < 0n) return __WASI_EINVAL;\n entry.offset = Number(next);\n writeU64LE(newOffsetPtr, next);\n return __WASI_ESUCCESS;\n },\n\n fd_tell(fd, offsetPtr) {\n if (fd === 0 || fd === 1 || fd === 2) {\n writeU64LE(offsetPtr, 0n);\n return __WASI_ESUCCESS;\n }\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n writeU64LE(offsetPtr, BigInt(entry.offset));\n return __WASI_ESUCCESS;\n },\n\n fd_fdstat_get(fd, statPtr) {\n const entry = fdTable.get(fd);\n if (!entry) return __WASI_EBADF;\n const dv = view();\n let ftype = __WASI_FT_UNKNOWN;\n if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {\n ftype = __WASI_FT_CHARACTER_DEVICE;\n } else if (entry.kind === 'preopen' || entry.kind === 'dir') {\n ftype = __WASI_FT_DIRECTORY;\n } else if (entry.kind === 'file') {\n ftype = __WASI_FT_REGULAR_FILE;\n } else if (entry.kind === 'symlink') {\n ftype = __WASI_FT_SYMBOLIC_LINK;\n } else if (entry.kind === 'socket') {\n ftype = __WASI_FT_SOCKET_STREAM; // WASI socket and polling support B7\n }\n dv.setUint8(statPtr, ftype);\n dv.setUint8(statPtr + 1, 0);\n dv.setUint16(statPtr + 2, (entry.fdflags || 0) & 0xFFFF, true);\n dv.setUint32(statPtr + 4, 0, true);\n // WASI socket and polling support B6: honor entry.rights when set, else wide-open default.\n // The legacy '0x3FFFFFFFn' was a 30-bit truncation of the actual\n // 64-bit rights field; modern wasi-libc probes specific bits so we\n // now publish full 64-bit __WASI_RIGHTS_ALL.\n const rb = (entry.rights !== undefined) ? entry.rights : __WASI_RIGHTS_ALL;\n const ri = (entry.rightsInheriting !== undefined) ? entry.rightsInheriting : __WASI_RIGHTS_ALL;\n writeU64LE(statPtr + 8, rb);\n writeU64LE(statPtr + 16, ri);\n return __WASI_ESUCCESS;\n },\n\n fd_fdstat_set_flags(fd, flags) {\n const entry = fdTable.get(fd);\n if (!entry) return __WASI_EBADF;\n if (entry.kind === 'file') entry.fdflags = flags;\n return __WASI_ESUCCESS;\n },\n\n // WASI socket and polling support B6: track per-fd rights mask. Callers narrow their own caps;\n // we record but don't enforce in v1 (single-tenant facet). The narrow\n // is visible to subsequent fd_fdstat_get calls, satisfying capability-\n // tightening probes that round-trip the mask.\n fd_fdstat_set_rights(fd, rightsBase, rightsInheriting) {\n const entry = fdTable.get(fd);\n if (!entry) return __WASI_EBADF;\n // Args are i64 \u2014 V8 routes as BigInt.\n const rb = typeof rightsBase === 'bigint' ? rightsBase : BigInt(rightsBase >>> 0);\n const ri = typeof rightsInheriting === 'bigint' ? rightsInheriting : BigInt(rightsInheriting >>> 0);\n // POSIX semantics: rights can only NARROW, never widen.\n const curRb = entry.rights !== undefined ? entry.rights : __WASI_RIGHTS_ALL;\n const curRi = entry.rightsInheriting !== undefined ? entry.rightsInheriting : __WASI_RIGHTS_ALL;\n if ((rb & ~curRb) !== 0n) return __WASI_ENOTCAPABLE;\n if ((ri & ~curRi) !== 0n) return __WASI_ENOTCAPABLE;\n entry.rights = rb;\n entry.rightsInheriting = ri;\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 preopens \u2500\u2500\n fd_prestat_get(fd, prestatPtr) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'preopen') return __WASI_EBADF;\n const dv = view();\n dv.setUint8(prestatPtr, __WASI_PREOPENTYPE_DIR);\n // pr_name_len at offset 4 (after 3 bytes pad)\n const nameBytes = utf8enc.encode(entry.wasiPath);\n dv.setUint32(prestatPtr + 4, nameBytes.length, true);\n return __WASI_ESUCCESS;\n },\n fd_prestat_dir_name(fd, pathPtr, pathLen) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'preopen') return __WASI_EBADF;\n const nameBytes = utf8enc.encode(entry.wasiPath);\n if (nameBytes.length > pathLen) return __WASI_EINVAL;\n u8().set(nameBytes, pathPtr);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 path_open \u2500\u2500\n path_open(baseFd, dirflags, pathPtr, pathLen, oflags, _rightsBase, _rightsInheriting, fdflags, fdOutPtr) {\n const pathArg = readPath(pathPtr, pathLen);\n\n // WASI socket and polling support B7: synthetic /dev/tcp/<host>/<port> path \u2014 open a TCP\n // socket via cloudflare:sockets. Bash-like convention; matches\n // Intercept BEFORE resolution because /dev/tcp/* doesn't exist in\n // the in-memory FS \u2014 __wasiResolvePath would canonicalize it but\n // not find any entry.\n if (pathArg.startsWith(__WASI_TCP_PATH_PREFIX)) {\n return __wasiOpenTcpSocket(pathArg, fdflags, fdOutPtr, writeU32LE);\n }\n // WASI socket and polling support B3: honor LOOKUPFLAGS_SYMLINK_FOLLOW (default behavior\n // when bit is set or dirflags omitted). When NOT set, surface the\n // symlink itself as a 'symlink' fd kind so readlink can introspect.\n const follow = (dirflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || dirflags === 0;\n const rp = __wasiResolvePathFull(baseFd, pathArg, follow);\n if (rp.err === __WASI_EBADF) return __WASI_EBADF;\n if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;\n const resolved = rp.path;\n const isCreate = (oflags & __WASI_O_CREAT) !== 0;\n const isDirectory = (oflags & __WASI_O_DIRECTORY) !== 0;\n const isExcl = (oflags & __WASI_O_EXCL) !== 0;\n const isTrunc = (oflags & __WASI_O_TRUNC) !== 0;\n\n // If the unfollowed path is a symlink and follow=false, open as\n // a symlink fd (only readlink-style introspection works on it).\n if (!follow && rp.isSymlink) {\n const fd = nextFd++;\n const target = __wasiFS.symlinks.get(resolved);\n fdTable.set(fd, { kind: 'symlink', vfsPath: resolved, target, oflags, fdflags });\n writeU32LE(fdOutPtr, fd);\n return __WASI_ESUCCESS;\n }\n\n const fileExists = __wasiFS.files.has(resolved);\n const dirExists = __wasiFS.dirs.has(resolved);\n\n if (isDirectory) {\n if (!dirExists) return __WASI_ENOENT;\n const fd = nextFd++;\n fdTable.set(fd, { kind: 'dir', vfsPath: resolved, readdirEntries: null, cookie: 0n, oflags, fdflags });\n writeU32LE(fdOutPtr, fd);\n return __WASI_ESUCCESS;\n }\n if (dirExists) {\n // Opening a directory without O_DIRECTORY: WASI returns EISDIR.\n return __WASI_EISDIR;\n }\n if (!fileExists) {\n if (!isCreate) return __WASI_ENOENT;\n // create empty file\n ensureParentDirs(resolved);\n setFile(resolved, new Uint8Array(0));\n } else if (isExcl) {\n return __WASI_EEXIST;\n } else if (isTrunc) {\n setFile(resolved, new Uint8Array(0));\n }\n const fd = nextFd++;\n fdTable.set(fd, { kind: 'file', vfsPath: resolved, offset: 0, oflags, fdflags });\n writeU32LE(fdOutPtr, fd);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 path_create_directory \u2500\u2500\n path_create_directory(baseFd, pathPtr, pathLen) {\n const path = readPath(pathPtr, pathLen);\n const resolved = __wasiResolvePath(baseFd, path);\n if (resolved === null) return __WASI_EBADF;\n if (__wasiFS.dirs.has(resolved)) return __WASI_EEXIST;\n if (__wasiFS.files.has(resolved)) return __WASI_EEXIST;\n ensureParentDirs(resolved);\n __wasiFS.dirs.add(resolved);\n // WASI socket and polling support B1: seed times for the new dir.\n const now = __wasiNowNs();\n __wasiFS.times.set(resolved, { mtime: now, atime: now, ctime: now });\n return __WASI_ESUCCESS;\n },\n\n path_remove_directory(baseFd, pathPtr, pathLen) {\n const path = readPath(pathPtr, pathLen);\n const resolved = __wasiResolvePath(baseFd, path);\n if (resolved === null) return __WASI_EBADF;\n if (!__wasiFS.dirs.has(resolved)) return __WASI_ENOENT;\n // Must be empty\n const children = readdirChildren(resolved);\n if (children.length > 0) return __WASI_ENOTEMPTY;\n __wasiFS.dirs.delete(resolved);\n return __WASI_ESUCCESS;\n },\n\n path_unlink_file(baseFd, pathPtr, pathLen) {\n const path = readPath(pathPtr, pathLen);\n const resolved = __wasiResolvePath(baseFd, path);\n if (resolved === null) return __WASI_EBADF;\n if (__wasiFS.dirs.has(resolved)) return __WASI_EISDIR;\n // WASI socket and polling support B3: unlink also removes a symlink at this path. We\n // operate on the unfollowed path (POSIX unlink semantics \u2014 removes\n // the directory entry itself, not the symlink target).\n if (__wasiFS.symlinks.has(resolved)) {\n __wasiFS.symlinks.delete(resolved);\n __wasiFS.times.delete(resolved);\n return __WASI_ESUCCESS;\n }\n if (!__wasiFS.files.has(resolved)) return __WASI_ENOENT;\n unsetFile(resolved);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 path_rename (atomic; overwrites destination) \u2500\u2500\n path_rename(srcFd, srcPathPtr, srcPathLen, dstFd, dstPathPtr, dstPathLen) {\n const srcPath = readPath(srcPathPtr, srcPathLen);\n const dstPath = readPath(dstPathPtr, dstPathLen);\n const src = __wasiResolvePath(srcFd, srcPath);\n const dst = __wasiResolvePath(dstFd, dstPath);\n if (src === null || dst === null) return __WASI_EBADF;\n // src must exist as either a file or dir\n const srcFile = __wasiFS.files.get(src);\n const srcIsDir = __wasiFS.dirs.has(src);\n if (!srcFile && !srcIsDir) return __WASI_ENOENT;\n // pre-unlink destination if present (the W-3 semantic for file \u2192 file).\n if (__wasiFS.files.has(dst)) __wasiFS.files.delete(dst);\n if (__wasiFS.dirs.has(dst)) __wasiFS.dirs.delete(dst);\n if (srcFile) {\n __wasiFS.files.delete(src);\n __wasiFS.files.set(dst, srcFile);\n } else {\n __wasiFS.dirs.delete(src);\n __wasiFS.dirs.add(dst);\n // Move children (rare; mostly the rename target is a single file).\n // For directories we walk and rebase any matching path key.\n const srcPrefix = src + '/';\n const toMove = [];\n for (const key of __wasiFS.files.keys()) {\n if (key.startsWith(srcPrefix)) toMove.push(key);\n }\n for (const key of toMove) {\n const newKey = dst + '/' + key.substring(srcPrefix.length);\n __wasiFS.files.set(newKey, __wasiFS.files.get(key));\n __wasiFS.files.delete(key);\n }\n const dirsToMove = [];\n for (const key of __wasiFS.dirs) {\n if (key.startsWith(srcPrefix)) dirsToMove.push(key);\n }\n for (const key of dirsToMove) {\n const newKey = dst + '/' + key.substring(srcPrefix.length);\n __wasiFS.dirs.add(newKey);\n __wasiFS.dirs.delete(key);\n }\n }\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 path_filestat_get \u2500\u2500\n path_filestat_get(baseFd, lookupflags, pathPtr, pathLen, statPtr) {\n const path = readPath(pathPtr, pathLen);\n // WASI socket and polling support B3: follow symlinks unless explicitly told not to.\n const follow = (lookupflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || lookupflags === 0;\n const rp = __wasiResolvePathFull(baseFd, path, follow);\n if (rp.err === __WASI_EBADF) return __WASI_EBADF;\n if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;\n const resolved = rp.path;\n let ftype, size;\n if (!follow && rp.isSymlink) {\n ftype = __WASI_FT_SYMBOLIC_LINK;\n size = BigInt(new TextEncoder().encode(__wasiFS.symlinks.get(resolved)).length);\n } else if (__wasiFS.files.has(resolved)) {\n ftype = __WASI_FT_REGULAR_FILE;\n size = BigInt(__wasiFS.files.get(resolved).length);\n } else if (__wasiFS.dirs.has(resolved)) {\n ftype = __WASI_FT_DIRECTORY;\n size = 0n;\n } else {\n return __WASI_ENOENT;\n }\n // WASI socket and polling support B1: emit real timestamps.\n const t = __wasiFS.times.get(resolved) || { mtime: 0n, atime: 0n, ctime: 0n };\n const dv = view();\n // filestat_t layout (WASI preview1):\n // dev:u64@0, ino:u64@8, filetype:u8@16, [pad 17..23],\n // nlink:u64@24, size:u64@32, atim:u64@40, mtim:u64@48, ctim:u64@56\n writeU64LE(statPtr, 0n);\n writeU64LE(statPtr + 8, 0n);\n dv.setUint8(statPtr + 16, ftype);\n for (let i = 17; i < 24; i++) dv.setUint8(statPtr + i, 0);\n writeU64LE(statPtr + 24, 1n);\n writeU64LE(statPtr + 32, size);\n writeU64LE(statPtr + 40, t.atime);\n writeU64LE(statPtr + 48, t.mtime);\n writeU64LE(statPtr + 56, t.ctime);\n return __WASI_ESUCCESS;\n },\n\n // WASI socket and polling support B2: real path_filestat_set_times. Honors ATIM/ATIM_NOW/\n // MTIM/MTIM_NOW flags. Spec: atim_ns and mtim_ns are absolute\n // nanosecond timestamps; flags select which fields to update + whether\n // to clamp to \"now\". ENOENT if path doesn't exist.\n path_filestat_set_times(baseFd, lookupflags, pathPtr, pathLen, atimArg, mtimArg, fstflags) {\n const path = readPath(pathPtr, pathLen);\n const follow = (lookupflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || lookupflags === 0;\n const rp = __wasiResolvePathFull(baseFd, path, follow);\n if (rp.err === __WASI_EBADF) return __WASI_EBADF;\n if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;\n const resolved = rp.path;\n const isSymlinkOnly = !follow && rp.isSymlink;\n if (!isSymlinkOnly && !__wasiFS.files.has(resolved) && !__wasiFS.dirs.has(resolved)) {\n return __WASI_ENOENT;\n }\n // Decode flags. Mutually-exclusive pairs ATIM vs ATIM_NOW (similarly\n // for MTIM) \u2014 caller error if both set; we accept and prefer _NOW.\n const setAtim = (fstflags & __WASI_FSTFLAGS_ATIM) !== 0;\n const setAtimNow = (fstflags & __WASI_FSTFLAGS_ATIM_NOW) !== 0;\n const setMtim = (fstflags & __WASI_FSTFLAGS_MTIM) !== 0;\n const setMtimNow = (fstflags & __WASI_FSTFLAGS_MTIM_NOW) !== 0;\n if ((setAtim && setAtimNow) || (setMtim && setMtimNow)) return __WASI_EINVAL;\n const now = __wasiNowNs();\n const atimNs = setAtimNow ? now : (setAtim ? (typeof atimArg === 'bigint' ? atimArg : BigInt(atimArg)) : null);\n const mtimNs = setMtimNow ? now : (setMtim ? (typeof mtimArg === 'bigint' ? mtimArg : BigInt(mtimArg)) : null);\n __wasiTouchTimes(resolved, mtimNs, atimNs, now);\n return __WASI_ESUCCESS;\n },\n\n // WASI socket and polling support B3: read the target string of a symlink. Spec:\n // path_readlink(fd, path, path_len, buf, buf_len, *bufused)\n // Truncates to buf_len; writes actual bytes to *bufused.\n path_readlink(baseFd, pathPtr, pathLen, bufPtr, bufLen, bufUsedPtr) {\n const path = readPath(pathPtr, pathLen);\n // readlink NEVER follows symlinks on the last component (POSIX).\n const rp = __wasiResolvePathFull(baseFd, path, false);\n if (rp.err === __WASI_EBADF) return __WASI_EBADF;\n if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;\n const resolved = rp.path;\n if (!__wasiFS.symlinks.has(resolved)) return __WASI_EINVAL; // not a symlink\n const target = __wasiFS.symlinks.get(resolved);\n const enc = new TextEncoder();\n const targetBytes = enc.encode(target);\n const n = Math.min(targetBytes.length, bufLen);\n u8().set(targetBytes.subarray(0, n), bufPtr);\n writeU32LE(bufUsedPtr, n);\n // POSIX: readlink doesn't update atime per most filesystems; but\n // some (ext4 with strictatime) do. We follow Linux default: no\n // atime bump on readlink.\n return __WASI_ESUCCESS;\n },\n\n // WASI socket and polling support B3: create a symlink. Spec:\n // path_symlink(old_path, fd, new_path)\n // old_path is the symlink's TARGET (stored verbatim); fd+new_path\n // is the location where the symlink itself is created.\n path_symlink(oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) {\n const oldPath = readPath(oldPathPtr, oldPathLen); // target string\n const newPath = readPath(newPathPtr, newPathLen);\n const resolved = __wasiResolvePath(newFd, newPath);\n if (resolved === null) return __WASI_EBADF;\n if (__wasiFS.files.has(resolved)) return __WASI_EEXIST;\n if (__wasiFS.dirs.has(resolved)) return __WASI_EEXIST;\n if (__wasiFS.symlinks.has(resolved)) return __WASI_EEXIST;\n // Store the target verbatim \u2014 symlinks per POSIX are dumb strings;\n // resolution happens at lookup time.\n __wasiFS.symlinks.set(resolved, oldPath);\n ensureParentDirs(resolved);\n const now = __wasiNowNs();\n __wasiFS.times.set(resolved, { mtime: now, atime: now, ctime: now });\n return __WASI_ESUCCESS;\n },\n\n // WASI socket and polling support B3: hardlink. Spec:\n // path_link(old_fd, old_flags, old_path, new_fd, new_path)\n // Implements as a shared-buffer alias. POSIX semantics: both names\n // point at the same inode, so writes through either are visible to\n // the other. In our in-memory FS the \"same inode\" is the same\n // Uint8Array reference. CAVEAT: our setFile/fd_write paths REPLACE\n // the buffer reference (immutable-style update) so subsequent writes\n // don't propagate. Matches link(2) at the WASI-layer (link itself\n // succeeds; concurrent-mutation semantics are filesystem-dependent\n // and our shim is a single-call sandbox).\n path_link(oldFd, oldFlags, oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) {\n const oldPath = readPath(oldPathPtr, oldPathLen);\n const newPath = readPath(newPathPtr, newPathLen);\n const follow = (oldFlags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || oldFlags === 0;\n const rpOld = __wasiResolvePathFull(oldFd, oldPath, follow);\n if (rpOld.err === __WASI_EBADF) return __WASI_EBADF;\n if (rpOld.err === __WASI_ELOOP) return __WASI_ELOOP;\n const src = rpOld.path;\n const dst = __wasiResolvePath(newFd, newPath);\n if (dst === null) return __WASI_EBADF;\n if (!__wasiFS.files.has(src)) return __WASI_ENOENT;\n if (__wasiFS.files.has(dst) || __wasiFS.dirs.has(dst) || __wasiFS.symlinks.has(dst)) {\n return __WASI_EEXIST;\n }\n __wasiFS.files.set(dst, __wasiFS.files.get(src)); // shared reference\n ensureParentDirs(dst);\n const now = __wasiNowNs();\n __wasiFS.times.set(dst, { mtime: now, atime: now, ctime: now });\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 fd_filestat_get / fd_filestat_set_size \u2500\u2500\n fd_filestat_get(fd, statPtr) {\n const entry = fdTable.get(fd);\n if (!entry) return __WASI_EBADF;\n const dv = view();\n let ftype = __WASI_FT_UNKNOWN;\n let size = 0n;\n let timesPath = null;\n if (entry.kind === 'file') {\n ftype = __WASI_FT_REGULAR_FILE;\n const f = getFile(entry.vfsPath);\n if (f) size = BigInt(f.length);\n timesPath = entry.vfsPath;\n } else if (entry.kind === 'dir' || entry.kind === 'preopen') {\n ftype = __WASI_FT_DIRECTORY;\n timesPath = entry.vfsPath;\n } else if (entry.kind === 'symlink') {\n ftype = __WASI_FT_SYMBOLIC_LINK;\n size = BigInt(new TextEncoder().encode(entry.target || '').length);\n timesPath = entry.vfsPath;\n } else if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {\n ftype = __WASI_FT_CHARACTER_DEVICE;\n }\n const t = (timesPath && __wasiFS.times.get(timesPath)) || { mtime: 0n, atime: 0n, ctime: 0n };\n writeU64LE(statPtr, 0n);\n writeU64LE(statPtr + 8, 0n);\n dv.setUint8(statPtr + 16, ftype);\n for (let i = 17; i < 24; i++) dv.setUint8(statPtr + i, 0);\n writeU64LE(statPtr + 24, 1n);\n writeU64LE(statPtr + 32, size);\n writeU64LE(statPtr + 40, t.atime);\n writeU64LE(statPtr + 48, t.mtime);\n writeU64LE(statPtr + 56, t.ctime);\n return __WASI_ESUCCESS;\n },\n fd_filestat_set_size(fd, size) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const cur = getFile(entry.vfsPath) || new Uint8Array(0);\n const newSize = Number(size);\n const next = new Uint8Array(newSize);\n next.set(cur.subarray(0, Math.min(cur.length, newSize)), 0);\n setFile(entry.vfsPath, next);\n return __WASI_ESUCCESS;\n },\n // WASI socket and polling support B2: real fd_filestat_set_times.\n fd_filestat_set_times(fd, atimArg, mtimArg, fstflags) {\n const entry = fdTable.get(fd);\n if (!entry) return __WASI_EBADF;\n if (entry.kind !== 'file' && entry.kind !== 'dir' && entry.kind !== 'preopen' && entry.kind !== 'symlink') {\n // stdio fds: no-op success (POSIX touches /dev/stdin etc. silently).\n return __WASI_ESUCCESS;\n }\n const setAtim = (fstflags & __WASI_FSTFLAGS_ATIM) !== 0;\n const setAtimNow = (fstflags & __WASI_FSTFLAGS_ATIM_NOW) !== 0;\n const setMtim = (fstflags & __WASI_FSTFLAGS_MTIM) !== 0;\n const setMtimNow = (fstflags & __WASI_FSTFLAGS_MTIM_NOW) !== 0;\n if ((setAtim && setAtimNow) || (setMtim && setMtimNow)) return __WASI_EINVAL;\n const now = __wasiNowNs();\n const atimNs = setAtimNow ? now : (setAtim ? (typeof atimArg === 'bigint' ? atimArg : BigInt(atimArg)) : null);\n const mtimNs = setMtimNow ? now : (setMtim ? (typeof mtimArg === 'bigint' ? mtimArg : BigInt(mtimArg)) : null);\n __wasiTouchTimes(entry.vfsPath, mtimNs, atimNs, now);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 fd_pread / fd_pwrite (offset-explicit) \u2500\u2500\n fd_pread(fd, iovsPtr, iovsLen, offsetArg, nreadPtr) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const file = getFile(entry.vfsPath);\n if (!file) return __WASI_ENOENT;\n let offset = typeof offsetArg === 'bigint'\n ? Number(offsetArg)\n : (offsetArg >>> 0);\n const dv = view();\n const memU8 = u8();\n let total = 0;\n for (let i = 0; i < iovsLen; i++) {\n const iov = iovsPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n const remain = file.length - offset;\n if (remain <= 0) break;\n const n = Math.min(bufLen, remain);\n memU8.set(file.subarray(offset, offset + n), bufPtr);\n offset += n;\n total += n;\n if (n < bufLen) break;\n }\n writeU32LE(nreadPtr, total);\n return __WASI_ESUCCESS;\n },\n\n fd_pwrite(fd, iovsPtr, iovsLen, offsetArg, nwrittenPtr) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n let offset = typeof offsetArg === 'bigint'\n ? Number(offsetArg)\n : (offsetArg >>> 0);\n const dv = view();\n const memU8 = u8();\n let total = 0;\n const parts = [];\n for (let i = 0; i < iovsLen; i++) {\n const iov = iovsPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n if (bufLen > 0) parts.push(memU8.slice(bufPtr, bufPtr + bufLen));\n total += bufLen;\n }\n let combined;\n if (parts.length === 0) combined = new Uint8Array(0);\n else if (parts.length === 1) combined = parts[0];\n else {\n combined = new Uint8Array(total);\n let off = 0;\n for (const p of parts) { combined.set(p, off); off += p.length; }\n }\n const cur = getFile(entry.vfsPath) || new Uint8Array(0);\n const newLen = Math.max(cur.length, offset + total);\n const next = new Uint8Array(newLen);\n next.set(cur, 0);\n next.set(combined, offset);\n setFile(entry.vfsPath, next);\n writeU32LE(nwrittenPtr, total);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 fd_readdir \u2500\u2500\n //\n // WASI dirent layout (24 bytes per entry):\n // d_next u64 @ 0 (cookie for next entry)\n // d_ino u64 @ 8\n // d_namlen u32 @ 16\n // d_type u8 @ 20\n // pad 21..23\n // followed by name bytes (variable).\n fd_readdir(fd, bufPtr, bufLen, cookieArg, bufusedPtr) {\n const entry = fdTable.get(fd);\n if (!entry || (entry.kind !== 'dir' && entry.kind !== 'preopen')) return __WASI_EBADF;\n if (!entry.readdirEntries) {\n const kids = readdirChildren(entry.vfsPath);\n entry.readdirEntries = [\n { name: '.', type: __WASI_FT_DIRECTORY },\n { name: '..', type: __WASI_FT_DIRECTORY },\n ...kids,\n ];\n }\n let startCookie = typeof cookieArg === 'bigint'\n ? Number(cookieArg)\n : (cookieArg >>> 0);\n let written = 0;\n const dv = view();\n const memU8 = u8();\n for (let i = startCookie; i < entry.readdirEntries.length; i++) {\n const e = entry.readdirEntries[i];\n const nameBytes = utf8enc.encode(e.name);\n const recordSize = 24 + nameBytes.length;\n if (written + 24 > bufLen) break;\n // d_next = i+1 (next cookie)\n writeU64LE(bufPtr + written, BigInt(i + 1));\n writeU64LE(bufPtr + written + 8, 0n); // d_ino\n dv.setUint32(bufPtr + written + 16, nameBytes.length, true);\n dv.setUint8(bufPtr + written + 20, e.type);\n for (let j = 21; j < 24; j++) dv.setUint8(bufPtr + written + j, 0);\n written += 24;\n // Write as much of the name as fits.\n const nameRoom = Math.min(nameBytes.length, bufLen - written);\n memU8.set(nameBytes.subarray(0, nameRoom), bufPtr + written);\n written += nameRoom;\n if (nameRoom < nameBytes.length) break;\n }\n writeU32LE(bufusedPtr, written);\n return __WASI_ESUCCESS;\n },\n\n fd_advise() { return __WASI_ESUCCESS; },\n // WASI socket and polling support B4: real fd_allocate. Extends the file's byte buffer with\n // zeros so [offset, offset+len) is allocated. POSIX posix_fallocate\n // semantics. ENOSPC is not reachable in our in-memory FS (the\n // 32 MiB snapshot cap is enforced at supervisor level; in-call we\n // just allocate the JS Uint8Array).\n fd_allocate(fd, offsetArg, lenArg) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'file') return __WASI_EBADF;\n const offset = typeof offsetArg === 'bigint' ? Number(offsetArg) : (offsetArg >>> 0);\n const len = typeof lenArg === 'bigint' ? Number(lenArg) : (lenArg >>> 0);\n const cur = getFile(entry.vfsPath) || new Uint8Array(0);\n const needed = offset + len;\n if (needed <= cur.length) return __WASI_ESUCCESS; // already big enough\n const next = new Uint8Array(needed);\n next.set(cur, 0); // [cur.length, needed) is implicitly zero-initialised\n setFile(entry.vfsPath, next);\n return __WASI_ESUCCESS;\n },\n fd_datasync() { return __WASI_ESUCCESS; },\n fd_sync() { return __WASI_ESUCCESS; },\n fd_renumber(from, to) {\n const entry = fdTable.get(from);\n if (!entry) return __WASI_EBADF;\n fdTable.delete(from);\n fdTable.set(to, entry);\n return __WASI_ESUCCESS;\n },\n\n // \u2500\u2500 proc_exit \u2500\u2500\n proc_exit(code) { throw new __WasiExit(code | 0); },\n // WASI socket and polling support B5: proc_raise(sig) \u2014 POSIX shell convention encodes a\n // signal-terminated process as exit-status 128+sig. SIGABRT=6 \u2192 134,\n // SIGTERM=15 \u2192 143, SIGKILL=9 \u2192 137. Returns errno (never actually;\n // throws via __WasiExit). The legacy 'throw new __WasiExit(128)' was\n // wrong: it discarded the signal number, so probes couldn't\n // distinguish SIGABRT from SIGTERM at the exit-code layer.\n proc_raise(sig) { throw new __WasiExit(128 + ((sig | 0) & 0xFF)); },\n\n // \u2500\u2500 clock \u2500\u2500\n clock_time_get(clockId, _precision, timePtr) {\n // precision is i64 \u2014 passed as BigInt by V8. We don't use it but\n // the arity must match for V8 to route correctly.\n let nowNs;\n if (clockId === __WASI_CLOCK_REALTIME) {\n nowNs = BigInt(Date.now()) * 1000000n;\n } else if (clockId === __WASI_CLOCK_MONOTONIC\n || clockId === __WASI_CLOCK_PROCESS_CPUTIME_ID\n || clockId === __WASI_CLOCK_THREAD_CPUTIME_ID) {\n const ms = (typeof performance !== 'undefined' && performance.now)\n ? performance.now() : Date.now();\n nowNs = BigInt(Math.floor(ms * 1000)) * 1000n;\n } else return __WASI_EINVAL;\n writeU64LE(timePtr, nowNs);\n return __WASI_ESUCCESS;\n },\n clock_res_get(clockId, resPtr) {\n if (clockId === __WASI_CLOCK_REALTIME) { writeU64LE(resPtr, 1000000n); return __WASI_ESUCCESS; }\n if (clockId === __WASI_CLOCK_MONOTONIC\n || clockId === __WASI_CLOCK_PROCESS_CPUTIME_ID\n || clockId === __WASI_CLOCK_THREAD_CPUTIME_ID) { writeU64LE(resPtr, 1000n); return __WASI_ESUCCESS; }\n return __WASI_EINVAL;\n },\n\n // \u2500\u2500 random \u2500\u2500\n random_get(bufPtr, bufLen) {\n const memU8 = u8();\n const CHUNK = 65536;\n let off = 0;\n while (off < bufLen) {\n const n = Math.min(bufLen - off, CHUNK);\n crypto.getRandomValues(memU8.subarray(bufPtr + off, bufPtr + off + n));\n off += n;\n }\n return __WASI_ESUCCESS;\n },\n\n sched_yield() { return __WASI_ESUCCESS; },\n\n // \u2500\u2500 WASI socket and polling support B8: poll_oneoff FULL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // Spec: WASI preview1 poll_oneoff(in_subscriptions, out_events,\n // nsubscriptions, *retNevents) blocks until at least one event fires,\n // writes events to out_events, returns count via *retNevents.\n //\n // Subscription layout (48B per entry, align 8):\n // +0 userdata: u64\n // +8 tag: u8 (EVENTTYPE_CLOCK=0 | FD_READ=1 | FD_WRITE=2)\n // +9..15 pad\n // +16 CLOCK: id:u32, +24 timeout:u64, +32 precision:u64, +40 flags:u16\n // FD_R/W: file_descriptor:u32\n //\n // Event layout (32B per entry, align 8):\n // +0 userdata: u64\n // +8 error: u16\n // +10 type: u8\n // +11..15 pad\n // +16 nbytes: u64 (fd events) \u2014 0 for clock\n // +24 flags: u16 (fd events) \u2014 0 here (no peer-hangup detection)\n // +26..31 pad\n //\n // Implementation: async fn that:\n // 1. Parses all subscriptions.\n // 2. Builds a Promise per subscription:\n // - CLOCK: setTimeout(deadline) \u2192 resolves with subscription idx.\n // - FD_READ/WRITE on regular file/dir/stdio: always-resolved Promise.\n // - FD_READ on socket: socket.readable.getReader().read() peek.\n // - FD_WRITE on socket: always-resolved (writable streams have\n // unbounded queue from the wasm side's perspective).\n // 3. Promise.race over all of them.\n // 4. After race resolves, drain all NOW-ready subscriptions\n // (winning + any others that also resolved or were always-ready).\n // 5. Write events; return count.\n //\n // Cancellation: pending setTimeouts and reader-locks are cleaned up\n // on every iteration to avoid leaking resources between poll calls.\n //\n // Wrapped in WebAssembly.Suspending at the imports-object finalise\n // point (with sock_*). When Suspending is unavailable, the bare async\n // fn returns a Promise which the wasm caller cannot consume \u2014 the\n // trap surfaces via __wasiRunStartAsync's catch.\n async poll_oneoff(inSubsPtr, outEventsPtr, nsubs, retNeventsPtr) {\n const dv = view();\n if ((nsubs | 0) <= 0) {\n writeU32LE(retNeventsPtr, 0);\n return __WASI_ESUCCESS;\n }\n // Parse subscriptions.\n const subs = []; // [{ userdata: BigInt, tag, ... }]\n for (let i = 0; i < nsubs; i++) {\n const base = inSubsPtr + i * 48;\n const userdata = dv.getBigUint64(base, true);\n const tag = dv.getUint8(base + 8);\n if (tag === __WASI_EVENTTYPE_CLOCK) {\n const id = dv.getUint32(base + 16, true);\n const timeout = dv.getBigUint64(base + 24, true);\n const precision = dv.getBigUint64(base + 32, true);\n const flags = dv.getUint16(base + 40, true);\n subs.push({ idx: i, userdata, tag, id, timeout, precision, flags });\n } else if (tag === __WASI_EVENTTYPE_FD_READ || tag === __WASI_EVENTTYPE_FD_WRITE) {\n const fd = dv.getUint32(base + 16, true);\n subs.push({ idx: i, userdata, tag, fd });\n } else {\n subs.push({ idx: i, userdata, tag, badTag: true });\n }\n }\n // Build per-subscription readiness promises. Each resolves with an\n // event-record { idx, error, type, nbytes, flags }. Bookkeeping for\n // cancellation: timerIds[i] holds setTimeout handle (or null);\n // readerLocks[i] holds a {reader, fd} pair so we can releaseLock\n // after the race.\n const timerIds = new Array(subs.length).fill(null);\n const readerLocks = [];\n const monoNowNs = () => {\n const ms = (typeof performance !== 'undefined' && performance.now)\n ? performance.now() : Date.now();\n return BigInt(Math.floor(ms * 1000)) * 1000n;\n };\n const promises = subs.map((s) => {\n if (s.badTag) {\n return Promise.resolve({\n idx: s.idx, error: __WASI_EINVAL, type: s.tag, nbytes: 0n, flags: 0,\n });\n }\n if (s.tag === __WASI_EVENTTYPE_CLOCK) {\n // Compute absolute deadline (ns) and convert to ms-delay.\n let deadlineNs;\n if ((s.flags & __WASI_SUBCLOCKFLAGS_ABSTIME) !== 0) {\n deadlineNs = s.timeout;\n } else {\n // Relative: deadline = now + timeout.\n const nowNs = (s.id === __WASI_CLOCK_REALTIME)\n ? BigInt(Date.now()) * 1000000n\n : monoNowNs();\n deadlineNs = nowNs + s.timeout;\n }\n const nowAgain = (s.id === __WASI_CLOCK_REALTIME)\n ? BigInt(Date.now()) * 1000000n\n : monoNowNs();\n const remainNs = deadlineNs > nowAgain ? (deadlineNs - nowAgain) : 0n;\n const remainMs = Number(remainNs / 1000000n);\n return new Promise((resolve) => {\n // POLICY: setTimeout is the only correct way to express a\n // deadline in JS event-loop terms. The PROBE-QUALITY anti-\n // setTimeout rule applies to probe ASSERTION LOGIC, not to\n // the implementation surface itself \u2014 a CLOCK subscription\n // BY DEFINITION needs a timer.\n const t = setTimeout(() => resolve({\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag, nbytes: 0n, flags: 0,\n }), remainMs > 0 ? remainMs : 0);\n timerIds[s.idx] = t;\n });\n }\n // FD subscription.\n const entry = fdTable.get(s.fd);\n if (!entry) {\n return Promise.resolve({\n idx: s.idx, error: __WASI_EBADF, type: s.tag, nbytes: 0n, flags: 0,\n });\n }\n // Regular files, dirs, stdio: always ready (POSIX: regular files\n // never block \u2014 read returns immediately even if at EOF).\n if (entry.kind === 'file' || entry.kind === 'dir' ||\n entry.kind === 'preopen' || entry.kind === 'symlink' ||\n entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {\n let nbytes = 0n;\n if (entry.kind === 'file' && s.tag === __WASI_EVENTTYPE_FD_READ) {\n const f = getFile(entry.vfsPath);\n if (f) {\n const remain = f.length - (entry.offset || 0);\n nbytes = remain > 0 ? BigInt(remain) : 0n;\n }\n } else if (s.tag === __WASI_EVENTTYPE_FD_WRITE) {\n // Writable: report large available capacity. Most user code\n // only checks nbytes > 0.\n nbytes = 0xFFFFFFFFn;\n }\n return Promise.resolve({\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag, nbytes, flags: 0,\n });\n }\n // Socket fd.\n if (entry.kind === 'socket') {\n if (s.tag === __WASI_EVENTTYPE_FD_WRITE) {\n // CF Workers writable streams are unbounded from the wasm\n // side; always-ready is a defensible approximation. Real\n // backpressure is enforced inside sock_send's await chain.\n return Promise.resolve({\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,\n nbytes: 0xFFFFFFFFn, flags: 0,\n });\n }\n // FD_READ: if local readBuf has bytes, immediate-ready.\n if (entry.readBuf && entry.readBufOffset < entry.readBuf.length) {\n const avail = BigInt(entry.readBuf.length - entry.readBufOffset);\n return Promise.resolve({\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,\n nbytes: avail, flags: 0,\n });\n }\n if (entry.eof || entry.closed) {\n // Peer closed: report ready with 0 bytes + HANGUP flag bit.\n return Promise.resolve({\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,\n nbytes: 0n, flags: 1, // RECV_DATA_TRUNCATED bit doesn't apply; flag 1 == HANGUP per eventrwflags_t bit 0.\n });\n }\n // Awaitable: peek at the readable stream. We MUST NOT consume\n // the data into oblivion \u2014 sock_recv on a later call needs to\n // see it. Strategy: getReader(), read() one chunk, STASH the\n // result on entry.readBuf so sock_recv finds it pre-loaded.\n return (async () => {\n try {\n if (!entry.reader) entry.reader = entry.socket.readable.getReader();\n await entry.socket.opened;\n const { value, done } = await entry.reader.read();\n if (done) {\n entry.eof = true;\n entry.readBuf = new Uint8Array(0);\n entry.readBufOffset = 0;\n return {\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,\n nbytes: 0n, flags: 1, // HANGUP\n };\n }\n entry.readBuf = (value instanceof Uint8Array)\n ? value\n : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);\n entry.readBufOffset = 0;\n return {\n idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,\n nbytes: BigInt(entry.readBuf.length), flags: 0,\n };\n } catch (e) {\n return {\n idx: s.idx, error: __WASI_EIO, type: s.tag,\n nbytes: 0n, flags: 0,\n };\n }\n })();\n }\n return Promise.resolve({\n idx: s.idx, error: __WASI_EBADF, type: s.tag, nbytes: 0n, flags: 0,\n });\n });\n // Race them. After the FIRST one resolves, check whether ANY others\n // also have results pending (they could have resolved synchronously\n // before the race even began, e.g. always-ready file fds).\n const winnerResult = await Promise.race(promises);\n // Now collect all currently-resolved promises. Strategy: tag each\n // with a marker via Promise.race against a sentinel \u2014 anything\n // that resolved BEFORE OR AT the same time as the winner is ready.\n // Practical approach: race each remaining against a microtask\n // tick; if it resolves within that tick, it's \"concurrently ready\".\n const ready = [];\n ready.push(winnerResult);\n // Cancel any pending timers (their resolve callbacks are now moot\n // unless we want to drain them too \u2014 see below for the rule).\n // For correctness AND minimal latency, we DON'T cancel \u2014 we drain\n // any timer that has already fired (same tick) by checking\n // Promise.race against an already-resolved sentinel.\n const sentinel = Promise.resolve('__not-ready__');\n for (let i = 0; i < promises.length; i++) {\n if (i === winnerResult.idx) continue;\n // race against a microtask tick to detect \"already resolved\".\n // Use a single-microtask delay so synchronously-resolved promises\n // (always-ready file fds) are caught while still-pending ones\n // (socket reads, future timers) are skipped.\n const p = promises[i];\n const probed = await Promise.race([p, sentinel]);\n if (probed !== '__not-ready__') {\n ready.push(probed);\n } else {\n // Cancel timer for non-firing CLOCK subscriptions.\n if (timerIds[i] !== null) {\n try { clearTimeout(timerIds[i]); } catch {}\n timerIds[i] = null;\n }\n }\n }\n // Write events.\n let nevents = 0;\n for (const ev of ready) {\n const off = outEventsPtr + nevents * 32;\n // userdata\n const subUserdata = subs[ev.idx].userdata;\n dv.setBigUint64(off, subUserdata, true);\n // error u16\n dv.setUint16(off + 8, ev.error, true);\n // type u8\n dv.setUint8(off + 10, ev.type);\n // pad\n for (let p = 11; p < 16; p++) dv.setUint8(off + p, 0);\n // fd_readwrite.nbytes u64 (0 for clock, but write the field\n // anyway \u2014 caller reads union by type).\n dv.setBigUint64(off + 16, ev.nbytes || 0n, true);\n // fd_readwrite.flags u16\n dv.setUint16(off + 24, ev.flags || 0, true);\n // pad\n for (let p = 26; p < 32; p++) dv.setUint8(off + p, 0);\n nevents++;\n }\n writeU32LE(retNeventsPtr, nevents);\n return __WASI_ESUCCESS;\n },\n\n // WASI socket and polling support B7: sockets via cloudflare:sockets connect() + JSPI.\n //\n // sock_send/sock_recv/sock_shutdown are ASYNC fns. They await on\n // ReadableStream/WritableStream readers/writers, which means the\n // wasm caller's expectation of a sync errno return must be bridged\n // via WebAssembly.Suspending. The Suspending wrapper is applied in\n // the return statement below where the imports object is finalised\n // (not here \u2014 wrapping at definition site would shadow the bare\n // function in the imports map). The async functions below have\n // signature compatible with 'new WebAssembly.Suspending(asyncFn)':\n // they return Promise<i32 errno>.\n\n async sock_send(fd, siDataPtr, siDataLen, _siFlags, retDataLenPtr) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'socket') return __WASI_ENOTSOCK;\n if (entry.closed || entry.halfClosedWr) return __WASI_EPIPE;\n // Gather iovs into a single Uint8Array per the WASI ciovec_array shape.\n const dv = view();\n const memU8 = u8();\n let total = 0;\n const parts = [];\n for (let i = 0; i < siDataLen; i++) {\n const iov = siDataPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n if (bufLen > 0) parts.push(memU8.slice(bufPtr, bufPtr + bufLen));\n total += bufLen;\n }\n let combined;\n if (parts.length === 0) combined = new Uint8Array(0);\n else if (parts.length === 1) combined = parts[0];\n else {\n combined = new Uint8Array(total);\n let off = 0;\n for (const p of parts) { combined.set(p, off); off += p.length; }\n }\n try {\n if (!entry.writer) entry.writer = entry.socket.writable.getWriter();\n // Wait for the socket to be connected on first write. socket.opened\n // resolves after the TCP handshake completes; subsequent writes\n // are unblocked because opened is a stable resolved promise.\n await entry.socket.opened;\n await entry.writer.write(combined);\n } catch (e) {\n // Map common errors to spec errnos.\n const msg = (e && e.message) ? e.message : String(e);\n if (/refused|ECONNREFUSED/i.test(msg)) return __WASI_ECONNREFUSED;\n if (/unreach|EHOSTUNREACH/i.test(msg)) return __WASI_EHOSTUNREACH;\n return __WASI_EIO;\n }\n writeU32LE(retDataLenPtr, total);\n return __WASI_ESUCCESS;\n },\n\n async sock_recv(fd, riDataPtr, riDataLen, _riFlags, retDataLenPtr, retFlagsPtr) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'socket') return __WASI_ENOTSOCK;\n if (entry.closed) return __WASI_ENOTCONN;\n const dv = view();\n const memU8 = u8();\n // If the local readBuf is empty, fetch a chunk from the stream.\n if (entry.readBufOffset >= entry.readBuf.length && !entry.eof) {\n try {\n if (!entry.reader) entry.reader = entry.socket.readable.getReader();\n await entry.socket.opened;\n const { value, done } = await entry.reader.read();\n if (done) {\n entry.eof = true;\n entry.readBuf = new Uint8Array(0);\n entry.readBufOffset = 0;\n } else {\n entry.readBuf = (value instanceof Uint8Array)\n ? value\n : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);\n entry.readBufOffset = 0;\n }\n } catch (e) {\n const msg = (e && e.message) ? e.message : String(e);\n if (/refused|ECONNREFUSED/i.test(msg)) return __WASI_ECONNREFUSED;\n if (/unreach|EHOSTUNREACH/i.test(msg)) return __WASI_EHOSTUNREACH;\n return __WASI_EIO;\n }\n }\n // Copy from readBuf into the user's iovs, up to total request size.\n let total = 0;\n for (let i = 0; i < riDataLen; i++) {\n const iov = riDataPtr + i * 8;\n const bufPtr = dv.getUint32(iov, true);\n const bufLen = dv.getUint32(iov + 4, true);\n const remain = entry.readBuf.length - entry.readBufOffset;\n if (remain <= 0) break;\n const n = Math.min(bufLen, remain);\n memU8.set(entry.readBuf.subarray(entry.readBufOffset, entry.readBufOffset + n), bufPtr);\n entry.readBufOffset += n;\n total += n;\n if (n < bufLen) break;\n }\n writeU32LE(retDataLenPtr, total);\n if (typeof retFlagsPtr === 'number') {\n // ROFLAGS_RECV_DATA_TRUNCATED bit; we don't truncate in this impl.\n dv.setUint16(retFlagsPtr, 0, true);\n }\n return __WASI_ESUCCESS;\n },\n\n async sock_shutdown(fd, how) {\n const entry = fdTable.get(fd);\n if (!entry || entry.kind !== 'socket') return __WASI_ENOTSOCK;\n const wantRd = (how & __WASI_SDFLAGS_RD) !== 0;\n const wantWr = (how & __WASI_SDFLAGS_WR) !== 0;\n try {\n // Full close (SHUT_RDWR): genuinely close the underlying socket.\n // Both readable and writable streams are forcibly closed per CF\n // docs (https://developers.cloudflare.com/workers/runtime-apis/\n // tcp-sockets/#close-tcp-connections).\n if (wantRd && wantWr) {\n if (entry.writer) {\n try { await entry.writer.close(); } catch {}\n }\n if (entry.reader) {\n try { await entry.reader.cancel(); } catch {}\n }\n try { await entry.socket.close(); } catch {}\n entry.halfClosedWr = true;\n entry.eof = true;\n entry.closed = true;\n return __WASI_ESUCCESS;\n }\n // Half-close write-only (SHUT_WR): POSIX semantics expect the\n // peer to receive an EOF on its read side while WE can still\n // receive its remaining bytes. CF Workers' TCP socket API does\n // not expose a true POSIX half-close primitive \u2014 calling\n // writer.close() may tear down the underlying connection in\n // ways that prevent further reads, even with allowHalfOpen=true.\n //\n // Empirically observed on prod (sock-shutdown-write probe failing\n // after writer.close() with allowHalfOpen=true): subsequent\n // sock_recv returns 0 bytes because the readable side stops\n // delivering once the writer closes.\n //\n // Best-fit semantics under this constraint: SHUT_WR marks the\n // shim-side fd as half-closed-WR (subsequent sock_send returns\n // EPIPE), but does NOT call writer.close() on the underlying\n // socket. The peer will eventually see EOF when our socket is\n // fully closed (at fd_close or program exit). In the meantime,\n // sock_recv continues to drain the readable side correctly.\n // This trades request/response-protocol correctness (where the\n // peer expects EOF to know when the request is done) for\n // server-echo-protocol correctness (where the peer streams\n // back regardless). The probe is the latter case; documented\n // limit for the former: see docs/wasi-threads.md\n // and surrounding sock_* commentary.\n if (wantWr && !entry.halfClosedWr) {\n entry.halfClosedWr = true;\n }\n // Half-close read-only (SHUT_RD): cancel the reader to stop\n // delivery. This IS safe \u2014 cancelling the reader doesn't tear\n // down the underlying socket on the CF side.\n if (wantRd && !entry.eof) {\n if (entry.reader) {\n try { await entry.reader.cancel(); } catch {}\n }\n entry.eof = true;\n }\n } catch (e) {\n return __WASI_EIO;\n }\n return __WASI_ESUCCESS;\n },\n };\n\n // WASI socket and polling support B7: wrap the async socket imports in WebAssembly.Suspending\n // so the wasm caller can use sync-shape calls that yield to the JS\n // event loop. Requires V8 14.2+ (workerd Oct 2025+) \u2014 see\n // If Suspending isn't available (older runtime), socket imports remain\n // async fns that the wasm boundary will reject with a trap \u2014 caller\n // sees a clean failure via __wasiRunStartAsync's catch block.\n if (typeof WebAssembly !== 'undefined' && typeof WebAssembly.Suspending === 'function') {\n imports.sock_send = new WebAssembly.Suspending(imports.sock_send);\n imports.sock_recv = new WebAssembly.Suspending(imports.sock_recv);\n imports.sock_shutdown = new WebAssembly.Suspending(imports.sock_shutdown);\n // WASI socket and polling support B8: poll_oneoff also needs JSPI to support CLOCK\n // subscriptions (await setTimeout) and socket-fd readiness\n // (await reader.read()). Same Suspending shape as sock_*.\n imports.poll_oneoff = new WebAssembly.Suspending(imports.poll_oneoff);\n }\n\n return {\n wasiImport: imports,\n getStdout: () => stdoutBuf,\n getStderr: () => stderrBuf,\n };\n}\n\nfunction __wasiRunStart(instance, ctx) {\n try {\n const start = instance.exports._start;\n if (typeof start !== 'function') {\n return { exitCode: 1, error: '_start is not a function (got ' + typeof start + ')' };\n }\n start();\n return { exitCode: 0 };\n } catch (e) {\n if (e && e.constructor && e.constructor.name === '__WasiExit') {\n return { exitCode: e.code };\n }\n return { exitCode: 1, error: (e && e.message) ? e.message : String(e) };\n }\n}\n\n// WASI socket and polling support (P3/P4 forward-decl): async variant of runStart. Wraps _start\n// with WebAssembly.promising so suspending imports (sock_send, sock_recv,\n// poll_oneoff) can await Promises and have V8 suspend+resume the wasm\n// stack. Returns the SAME shape as __wasiRunStart but in a Promise.\n//\n// In P2 (this commit) no suspending imports exist yet \u2014 invoking this\n// function is functionally identical to __wasiRunStart but returns a\n// Promise. P3 (sockets) and P4 (poll_oneoff) wire the actual Suspending\n// imports + this async entrypoint together.\n//\n// Caller contract: legacy callers that need a sync return (ruby-runner)\n// keep using __wasiRunStart. New callers (wasm-runner WASI mode, post-\n// P3) switch to await __wasiRunStartAsync(...).\nasync function __wasiRunStartAsync(instance, ctx) {\n try {\n const start = instance.exports._start;\n if (typeof start !== 'function') {\n return { exitCode: 1, error: '_start is not a function (got ' + typeof start + ')' };\n }\n // WebAssembly.promising wraps the export so it returns a Promise<void>\n // that resolves when the (potentially-suspending) wasm computation\n // completes. Available since V8 14.2 (workerd Oct 2025) \u2014 see\n //\n // If promising isn't available (older runtimes), fall through to a\n // direct call \u2014 fully spec-compatible for non-suspending _start.\n if (typeof WebAssembly !== 'undefined' && typeof WebAssembly.promising === 'function') {\n const promisingStart = WebAssembly.promising(start);\n await promisingStart();\n } else {\n start();\n }\n return { exitCode: 0 };\n } catch (e) {\n if (e && e.constructor && e.constructor.name === '__WasiExit') {\n return { exitCode: e.code };\n }\n return { exitCode: 1, error: (e && e.message) ? e.message : String(e) };\n }\n}\n// \u2500\u2500 END: wasi-instance preamble \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n";
110
110
  /**
111
111
  * A bundle of file/dir state passed from supervisor → facet for a WASI
112
112
  * invocation. Files are base64-encoded so the JSON-serializable
113
113
  * loader-pool `context` field can carry them.
114
114
  *
115
- * Stream-B B1+B3: added optional `times` and `symlinks` fields. Backward-
115
+ * WASI socket and polling support B1+B3: added optional `times` and `symlinks` fields. Backward-
116
116
  * compatible — pre-B1 callers omit these and get default "now" timestamps
117
117
  * + empty symlink table.
118
118
  */
119
119
  export interface WasiFsSnapshot {
120
- /** Canonical VFS root (no leading slash). E.g. `home/user/wasi-w2`. */
120
+ /** Canonical VFS root (no leading slash). E.g. `home/user/wasi-files`. */
121
121
  root: string;
122
122
  /** Preopen list (order matters; preopens are assigned to fd 3, 4, …). */
123
123
  preopens: Array<{
@@ -129,7 +129,7 @@ export interface WasiFsSnapshot {
129
129
  /** Initial directory list (vfsPaths). */
130
130
  dirs: string[];
131
131
  /**
132
- * Stream-B B1: per-path nanosecond timestamps. Values are decimal strings
132
+ * WASI socket and polling support B1: per-path nanosecond timestamps. Values are decimal strings
133
133
  * (JSON.stringify-safe; BigInt would throw). Optional — omitted paths
134
134
  * default to wall-clock-now at init time.
135
135
  */
@@ -139,7 +139,7 @@ export interface WasiFsSnapshot {
139
139
  ctime: string;
140
140
  }>;
141
141
  /**
142
- * Stream-B B3: initial symlink table. `vfsPath → targetPath`. Target is
142
+ * WASI socket and polling support B3: initial symlink table. `vfsPath → targetPath`. Target is
143
143
  * stored verbatim per POSIX (resolution at lookup time).
144
144
  */
145
145
  symlinks?: Record<string, string>;
@@ -148,7 +148,7 @@ export interface WasiFsSnapshot {
148
148
  * Per-call return shape — what the facet's RPC produces back to the
149
149
  * supervisor. Mutations are diffs against the initial snapshot.
150
150
  *
151
- * Stream-B B1+B3: added timesChanged + symlinksCreated/Deleted. Backward-
151
+ * WASI socket and polling support B1+B3: added timesChanged + symlinksCreated/Deleted. Backward-
152
152
  * compatible — supervisors that don't read these see identical pre-B1
153
153
  * behavior.
154
154
  */
@@ -162,7 +162,7 @@ export interface WasiFsDiff {
162
162
  /** vfsPaths that were rmdir'd. */
163
163
  dirsDeleted: string[];
164
164
  /**
165
- * Stream-B B1: paths whose mtime/atime/ctime changed during the call.
165
+ * WASI socket and polling support B1: paths whose mtime/atime/ctime changed during the call.
166
166
  * Decimal-string nanoseconds.
167
167
  */
168
168
  timesChanged?: Record<string, {
@@ -170,21 +170,11 @@ export interface WasiFsDiff {
170
170
  atime: string;
171
171
  ctime: string;
172
172
  }>;
173
- /** Stream-B B3: new or modified symlinks. */
173
+ /** New or modified symlinks. */
174
174
  symlinksCreated?: Record<string, string>;
175
- /** Stream-B B3: removed symlinks. */
175
+ /** Removed symlinks. */
176
176
  symlinksDeleted?: string[];
177
177
  }
178
- /** Total count of functionally-implemented WASI fns (Wave-1 + Wave-2 + Stream-B P2 + P3 + P4). */
179
- export declare const WASI_WAVE2_FN_COUNT = 30;
180
- export declare const WASI_STREAM_B_P2_FN_COUNT = 36;
181
- export declare const WASI_STREAM_B_P3_FN_COUNT = 39;
182
- export declare const WASI_STREAM_B_P4_FN_COUNT = 40;
183
- /** Names of the Wave-1 + Wave-2 + Stream-B P2 functionally-implemented WASI fns. */
184
- export declare const WASI_WAVE2_FNS: readonly string[];
185
- /**
186
- * Backwards-compat alias kept for any caller still importing the Wave-1
187
- * symbol names. The Wave-2 module now subsumes Wave-1's count.
188
- */
189
- export declare const WASI_WAVE1_FNS: readonly string[];
178
+ /** Names of the WASI imports implemented by this shim. */
179
+ export declare const WASI_IMPLEMENTED_FNS: readonly string[];
190
180
  //# sourceMappingURL=wasi-instance.d.ts.map