@volter/browser-wali 0.1.166 → 0.1.168

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 (215) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +9 -0
  3. package/dist/browser-wali-command.d.ts +7 -0
  4. package/dist/browser-wali-command.d.ts.map +1 -1
  5. package/dist/browser-wali-command.js +70 -18
  6. package/dist/browser-wali-command.js.map +1 -1
  7. package/dist/bulk-memory.d.ts +4 -2
  8. package/dist/bulk-memory.d.ts.map +1 -1
  9. package/dist/bulk-memory.js +32 -9
  10. package/dist/bulk-memory.js.map +1 -1
  11. package/dist/capabilities.d.ts +1 -1
  12. package/dist/capabilities.d.ts.map +1 -1
  13. package/dist/capabilities.js +6 -2
  14. package/dist/capabilities.js.map +1 -1
  15. package/dist/child-processes.d.ts +2 -0
  16. package/dist/child-processes.d.ts.map +1 -1
  17. package/dist/child-processes.js +9 -1
  18. package/dist/child-processes.js.map +1 -1
  19. package/dist/clocks.d.ts +3 -0
  20. package/dist/clocks.d.ts.map +1 -0
  21. package/dist/clocks.js +44 -0
  22. package/dist/clocks.js.map +1 -0
  23. package/dist/coherent-file-locks.d.ts.map +1 -1
  24. package/dist/coherent-file-locks.js +2 -1
  25. package/dist/coherent-file-locks.js.map +1 -1
  26. package/dist/credentials.d.ts +4 -0
  27. package/dist/credentials.d.ts.map +1 -0
  28. package/dist/credentials.js +46 -0
  29. package/dist/credentials.js.map +1 -0
  30. package/dist/descriptors.d.ts +14 -0
  31. package/dist/descriptors.d.ts.map +1 -1
  32. package/dist/descriptors.js +45 -5
  33. package/dist/descriptors.js.map +1 -1
  34. package/dist/environment.d.ts +4 -0
  35. package/dist/environment.d.ts.map +1 -0
  36. package/dist/environment.js +23 -0
  37. package/dist/environment.js.map +1 -0
  38. package/dist/epoll-ready.d.ts +27 -0
  39. package/dist/epoll-ready.d.ts.map +1 -0
  40. package/dist/epoll-ready.js +88 -0
  41. package/dist/epoll-ready.js.map +1 -0
  42. package/dist/events.d.ts +1 -0
  43. package/dist/events.d.ts.map +1 -1
  44. package/dist/events.js +55 -24
  45. package/dist/events.js.map +1 -1
  46. package/dist/filesystem-host-session.d.ts +65 -0
  47. package/dist/filesystem-host-session.d.ts.map +1 -0
  48. package/dist/filesystem-host-session.js +951 -0
  49. package/dist/filesystem-host-session.js.map +1 -0
  50. package/dist/filesystem-host.d.ts +13 -81
  51. package/dist/filesystem-host.d.ts.map +1 -1
  52. package/dist/filesystem-host.js +14 -882
  53. package/dist/filesystem-host.js.map +1 -1
  54. package/dist/filesystem-protocol.d.ts +3 -1
  55. package/dist/filesystem-protocol.d.ts.map +1 -1
  56. package/dist/filesystem-remote.d.ts +8 -3
  57. package/dist/filesystem-remote.d.ts.map +1 -1
  58. package/dist/filesystem-remote.js +43 -20
  59. package/dist/filesystem-remote.js.map +1 -1
  60. package/dist/filesystem-service-client.d.ts +4 -0
  61. package/dist/filesystem-service-client.d.ts.map +1 -0
  62. package/dist/filesystem-service-client.js +16 -0
  63. package/dist/filesystem-service-client.js.map +1 -0
  64. package/dist/filesystem.d.ts +26 -4
  65. package/dist/filesystem.d.ts.map +1 -1
  66. package/dist/filesystem.js +633 -175
  67. package/dist/filesystem.js.map +1 -1
  68. package/dist/fork-memory.d.ts +23 -0
  69. package/dist/fork-memory.d.ts.map +1 -0
  70. package/dist/fork-memory.js +93 -0
  71. package/dist/fork-memory.js.map +1 -0
  72. package/dist/fork-worker.js +32 -11
  73. package/dist/fork-worker.js.map +1 -1
  74. package/dist/jumps.d.ts +7 -0
  75. package/dist/jumps.d.ts.map +1 -1
  76. package/dist/jumps.js +19 -1
  77. package/dist/jumps.js.map +1 -1
  78. package/dist/kernel-filesystem-worker.d.ts +2 -0
  79. package/dist/kernel-filesystem-worker.d.ts.map +1 -0
  80. package/dist/kernel-filesystem-worker.js +28 -0
  81. package/dist/kernel-filesystem-worker.js.map +1 -0
  82. package/dist/kernel-filesystem.d.ts +12 -0
  83. package/dist/kernel-filesystem.d.ts.map +1 -0
  84. package/dist/kernel-filesystem.js +45 -0
  85. package/dist/kernel-filesystem.js.map +1 -0
  86. package/dist/kernel-network-worker.d.ts +2 -0
  87. package/dist/kernel-network-worker.d.ts.map +1 -0
  88. package/dist/kernel-network-worker.js +14 -0
  89. package/dist/kernel-network-worker.js.map +1 -0
  90. package/dist/kernel-network.d.ts +14 -0
  91. package/dist/kernel-network.d.ts.map +1 -0
  92. package/dist/kernel-network.js +50 -0
  93. package/dist/kernel-network.js.map +1 -0
  94. package/dist/kernel-process-client.d.ts +6 -1
  95. package/dist/kernel-process-client.d.ts.map +1 -1
  96. package/dist/kernel-process-client.js +10 -1
  97. package/dist/kernel-process-client.js.map +1 -1
  98. package/dist/module-layout.d.ts +8 -0
  99. package/dist/module-layout.d.ts.map +1 -1
  100. package/dist/module-layout.js +263 -2
  101. package/dist/module-layout.js.map +1 -1
  102. package/dist/network-host.d.ts +6 -0
  103. package/dist/network-host.d.ts.map +1 -0
  104. package/dist/network-host.js +1582 -0
  105. package/dist/network-host.js.map +1 -0
  106. package/dist/network-protocol.d.ts +9 -2
  107. package/dist/network-protocol.d.ts.map +1 -1
  108. package/dist/network-protocol.js +3 -0
  109. package/dist/network-protocol.js.map +1 -1
  110. package/dist/network-service-client.d.ts +4 -0
  111. package/dist/network-service-client.d.ts.map +1 -0
  112. package/dist/network-service-client.js +16 -0
  113. package/dist/network-service-client.js.map +1 -0
  114. package/dist/network-worker.js +5 -1150
  115. package/dist/network-worker.js.map +1 -1
  116. package/dist/network.d.ts +13 -1
  117. package/dist/network.d.ts.map +1 -1
  118. package/dist/network.js +175 -48
  119. package/dist/network.js.map +1 -1
  120. package/dist/paths.d.ts +11 -0
  121. package/dist/paths.d.ts.map +1 -0
  122. package/dist/paths.js +97 -0
  123. package/dist/paths.js.map +1 -0
  124. package/dist/process-bridge.d.ts +12 -4
  125. package/dist/process-bridge.d.ts.map +1 -1
  126. package/dist/process-bridge.js +53 -17
  127. package/dist/process-bridge.js.map +1 -1
  128. package/dist/program.d.ts.map +1 -1
  129. package/dist/program.js +8 -0
  130. package/dist/program.js.map +1 -1
  131. package/dist/runtime.d.ts.map +1 -1
  132. package/dist/runtime.js +150 -92
  133. package/dist/runtime.js.map +1 -1
  134. package/dist/shared-buffer-pool.d.ts +18 -5
  135. package/dist/shared-buffer-pool.d.ts.map +1 -1
  136. package/dist/shared-buffer-pool.js +86 -14
  137. package/dist/shared-buffer-pool.js.map +1 -1
  138. package/dist/shared-file-layer.d.ts +3 -1
  139. package/dist/shared-file-layer.d.ts.map +1 -1
  140. package/dist/shared-file-layer.js +150 -35
  141. package/dist/shared-file-layer.js.map +1 -1
  142. package/dist/shared-pipes.d.ts +3 -1
  143. package/dist/shared-pipes.d.ts.map +1 -1
  144. package/dist/shared-pipes.js +105 -33
  145. package/dist/shared-pipes.js.map +1 -1
  146. package/dist/signal-interruption.d.ts +9 -0
  147. package/dist/signal-interruption.d.ts.map +1 -0
  148. package/dist/signal-interruption.js +6 -0
  149. package/dist/signal-interruption.js.map +1 -0
  150. package/dist/signals.d.ts +17 -6
  151. package/dist/signals.d.ts.map +1 -1
  152. package/dist/signals.js +101 -22
  153. package/dist/signals.js.map +1 -1
  154. package/dist/snapshot-filesystem.d.ts +12 -1
  155. package/dist/snapshot-filesystem.d.ts.map +1 -1
  156. package/dist/snapshot-filesystem.js +241 -73
  157. package/dist/snapshot-filesystem.js.map +1 -1
  158. package/dist/socket-flags.d.ts +3 -0
  159. package/dist/socket-flags.d.ts.map +1 -0
  160. package/dist/socket-flags.js +9 -0
  161. package/dist/socket-flags.js.map +1 -0
  162. package/dist/special-files.d.ts +6 -0
  163. package/dist/special-files.d.ts.map +1 -0
  164. package/dist/special-files.js +56 -0
  165. package/dist/special-files.js.map +1 -0
  166. package/dist/stdin.d.ts.map +1 -1
  167. package/dist/stdin.js +13 -5
  168. package/dist/stdin.js.map +1 -1
  169. package/dist/symlink.d.ts +2 -1
  170. package/dist/symlink.d.ts.map +1 -1
  171. package/dist/symlink.js +25 -93
  172. package/dist/symlink.js.map +1 -1
  173. package/dist/system.d.ts +2 -4
  174. package/dist/system.d.ts.map +1 -1
  175. package/dist/system.js +46 -29
  176. package/dist/system.js.map +1 -1
  177. package/dist/thread-coordinator-worker.js +58 -24
  178. package/dist/thread-coordinator-worker.js.map +1 -1
  179. package/dist/thread-lifecycle.d.ts +9 -0
  180. package/dist/thread-lifecycle.d.ts.map +1 -0
  181. package/dist/thread-lifecycle.js +30 -0
  182. package/dist/thread-lifecycle.js.map +1 -0
  183. package/dist/thread-worker.js +125 -44
  184. package/dist/thread-worker.js.map +1 -1
  185. package/dist/threads.d.ts +6 -0
  186. package/dist/threads.d.ts.map +1 -1
  187. package/dist/threads.js +46 -6
  188. package/dist/threads.js.map +1 -1
  189. package/dist/tty-job-control.d.ts +13 -0
  190. package/dist/tty-job-control.d.ts.map +1 -0
  191. package/dist/tty-job-control.js +56 -0
  192. package/dist/tty-job-control.js.map +1 -0
  193. package/dist/tty-settings.d.ts +3 -0
  194. package/dist/tty-settings.d.ts.map +1 -0
  195. package/dist/tty-settings.js +25 -0
  196. package/dist/tty-settings.js.map +1 -0
  197. package/dist/types.d.ts +26 -0
  198. package/dist/types.d.ts.map +1 -1
  199. package/dist/wait-state.d.ts +8 -0
  200. package/dist/wait-state.d.ts.map +1 -1
  201. package/dist/wait-state.js +15 -0
  202. package/dist/wait-state.js.map +1 -1
  203. package/dist/worker-program-protocol.d.ts +22 -2
  204. package/dist/worker-program-protocol.d.ts.map +1 -1
  205. package/dist/worker-program-worker.js +31 -9
  206. package/dist/worker-program-worker.js.map +1 -1
  207. package/dist/worker-program.d.ts +12 -2
  208. package/dist/worker-program.d.ts.map +1 -1
  209. package/dist/worker-program.js +214 -36
  210. package/dist/worker-program.js.map +1 -1
  211. package/dist/write-vector.d.ts +3 -0
  212. package/dist/write-vector.d.ts.map +1 -0
  213. package/dist/write-vector.js +35 -0
  214. package/dist/write-vector.js.map +1 -0
  215. package/package.json +1 -1
@@ -0,0 +1,951 @@
1
+ import { releaseWaliSharedBuffers, markWaliSharedBufferWritten } from "./shared-buffer-pool.js";
2
+ import { lowerTreeOf } from "./shared-file-layer.js";
3
+ import { openPackReader } from "@volter/browser-runtime/browser-pack-store.js";
4
+ import { SnapshotWaliFileSystem } from "./snapshot-filesystem.js";
5
+ import { WaliSharedPipeHost } from "./shared-pipes.js";
6
+ import { WaliDescriptionRegistry, WaliProcessDescriptorTable } from "./descriptors.js";
7
+ import { WaliFileDescriptorTable } from "./filesystem.js";
8
+ import { applyProcessFilesystemPatch, createProcessFilesystemPatch } from "./process-bridge.js";
9
+ import { BrowserFileLockClient } from "@volter/browser-runtime/browser-file-locks.js";
10
+ import { BrowserFileCoherenceClient } from "@volter/browser-runtime/browser-file-coherence.js";
11
+ import { WaliCoherentFileLockClient } from "./coherent-file-locks.js";
12
+ import { encodeBase64, decodeBase64 } from "@volter/browser-runtime/browser-base64.js";
13
+ import { currentEntry } from "@volter/browser-runtime/browser-process-filesystem.js";
14
+ import { FILESYSTEM_SNAPSHOT_CHUNK_BYTES, FilesystemOperation, } from "./filesystem-protocol.js";
15
+ import { openLayerStores } from "./layer-stores.js";
16
+ /** Process-tree filesystem state, multiplexed in the kernel worker or held locally. */
17
+ export function createWaliFilesystemHost() {
18
+ let retired = false;
19
+ const cleanup = new Set();
20
+ let filesystem = new SnapshotWaliFileSystem();
21
+ let store;
22
+ let descriptors;
23
+ let sharedPipes;
24
+ let rootMemory;
25
+ const processes = new Map();
26
+ let nextProcess = 1;
27
+ let finalizeUpdates;
28
+ let prepareSpawn;
29
+ let importPeerPatch;
30
+ const encoder = new TextEncoder();
31
+ const decoder = new TextDecoder();
32
+ /**
33
+ * ON THE PROGRAM'S MAIN THREAD (ADR-0046). The host answers the main thread's
34
+ * own requests as calls and every other channel's when the main thread enters
35
+ * the host (`pumpHost`); its event loop does not run while the program does,
36
+ * so what it did on messages and timers it does there too: the page's imports
37
+ * arrive in a shared mailbox, and the program's changes go to the page when
38
+ * it blocks (before its output, too: `prepareSpawn`). Not on a timer: a file
39
+ * being written is published whole, and publishing every few milliseconds
40
+ * copied a `.blend` Blender was still writing again and again (2026-09-27:
41
+ * a 2 MB save went from 11 ms to 27-38).
42
+ */
43
+ let inThread = false;
44
+ /** Rung by every request on a channel, and by the page when it fills the mailbox: [0] counts the rings. */
45
+ let doorbell;
46
+ let doorbellSeen = 0;
47
+ /** Every channel attached, for the main thread to answer. */
48
+ const channels = new Set();
49
+ /** Requests a blocking lock left unanswered, asked again at every pump. */
50
+ const deferred = new Set();
51
+ /** [0] 1 when an import waits, [1] its length; its JSON after 8 bytes. Growable. */
52
+ let mailbox;
53
+ let publishUpdates;
54
+ let unpublishedSince = 0;
55
+ let pumping = false;
56
+ /** Set when the program starts: the port's imports were taken until then, the mailbox's after (ADR-0046). */
57
+ let portClosed = false;
58
+ let portTaken = 0;
59
+ let closePort;
60
+ /** The program's private trees, whose changes are not published (`privateRoots`). */
61
+ let privateRoots = [];
62
+ /** Set once the program's last changes have gone to the page: an import after that is the page's own. */
63
+ let hostFinalized = false;
64
+ const RETRY = Symbol("retry");
65
+ /** The program is about to run: the port is read no longer, and the page is told which import it took last. */
66
+ function closeHostPort() { closePort?.(); }
67
+ /** The host's run is over: nothing of it is held for the next (review of c96148c). */
68
+ function resetHost() {
69
+ retired = true;
70
+ for (const close of cleanup)
71
+ close();
72
+ cleanup.clear();
73
+ for (const id of [...processes.keys()])
74
+ releaseProcess(id);
75
+ descriptors?.descriptors.exit();
76
+ for (const entry of channels) {
77
+ entry.state.closed = true;
78
+ entry.port.onmessage = null;
79
+ entry.port.close();
80
+ }
81
+ store?.close();
82
+ store = undefined;
83
+ filesystem.setMutationHandler(() => { });
84
+ portClosed = false;
85
+ portTaken = 0;
86
+ closePort = undefined;
87
+ inThread = false;
88
+ doorbell = undefined;
89
+ channels.clear();
90
+ deferred.clear();
91
+ mailbox = undefined;
92
+ publishUpdates = undefined;
93
+ publishError = undefined;
94
+ unpublishedSince = 0;
95
+ hostFinalized = false;
96
+ filesystem = new SnapshotWaliFileSystem();
97
+ descriptors = undefined;
98
+ processes.clear();
99
+ rootMemory = undefined;
100
+ sharedPipes = undefined;
101
+ prepareSpawn = undefined;
102
+ finalizeUpdates = undefined;
103
+ importPeerPatch = () => { throw new Error("The WALI filesystem owner has ended."); };
104
+ }
105
+ /** The host runs on the calling realm's main thread from here on (ADR-0046); call before `initializeHost`. */
106
+ function hostInThread(bell) {
107
+ inThread = true;
108
+ doorbell = new Int32Array(bell);
109
+ doorbellSeen = Atomics.load(doorbell, 0);
110
+ }
111
+ /** The host's tree and descriptors for a run, in whichever realm runs it. */
112
+ async function initializeHost(data) {
113
+ const event = { data };
114
+ retired = false;
115
+ if (data.fileLocks)
116
+ cleanup.add(() => data.fileLocks.channel.port.close());
117
+ if (data.fileCoherence)
118
+ cleanup.add(() => data.fileCoherence.port.close());
119
+ channels.clear();
120
+ deferred.clear();
121
+ publishUpdates = undefined;
122
+ publishError = undefined;
123
+ mailbox = undefined;
124
+ unpublishedSince = 0;
125
+ hostFinalized = false;
126
+ portClosed = false;
127
+ portTaken = 0;
128
+ closePort = undefined;
129
+ privateRoots = (data.privateRoots ?? []).map(normalizeRoot);
130
+ {
131
+ finalizeUpdates = undefined;
132
+ prepareSpawn = undefined;
133
+ // Layers whose bytes are a store the host installed (ADR-0048), opened before the tree is made.
134
+ const layerStores = await openLayerStores(data.layers);
135
+ if (data.store) {
136
+ // The tree is read from the project's store, one file as the program opens it.
137
+ store = await openPackReader({ navigator: globalThis.navigator }, data.store.projectId);
138
+ if (!store)
139
+ throw new Error("The project's store is not there to read.");
140
+ filesystem = SnapshotWaliFileSystem.fromStore(lowerTreeOf(store, data.layers, layerStores), data.snapshot);
141
+ }
142
+ else if (data.layers) {
143
+ filesystem = SnapshotWaliFileSystem.fromStore(lowerTreeOf(undefined, data.layers, layerStores), data.snapshot);
144
+ }
145
+ else {
146
+ filesystem = SnapshotWaliFileSystem.fromSnapshot(data.snapshot);
147
+ }
148
+ importPeerPatch = (patch, base) => applyProcessFilesystemPatch(filesystem, patch, base);
149
+ const lockClient = event.data.fileLocks
150
+ ? new BrowserFileLockClient(event.data.fileLocks.channel) : undefined;
151
+ sharedPipes = data.sharedPipes;
152
+ rootMemory = data.memory;
153
+ descriptors = new WaliFileDescriptorTable({
154
+ initializeStdio: true,
155
+ ...(sharedPipes ? { descriptors: new WaliProcessDescriptorTable(new WaliDescriptionRegistry(sharedPipes.descriptions), sharedPipes.table), descriptorState: sharedPipes } : {}),
156
+ filesystem,
157
+ ...(data.filesystemPolicy ? { filesystemPolicy: data.filesystemPolicy } : {}),
158
+ cwd: event.data.cwd,
159
+ stdin: new Uint8Array(),
160
+ executablePath: event.data.executablePath,
161
+ memory: () => event.data.memory,
162
+ emit: () => -9n,
163
+ descriptorMaximum: 19_999,
164
+ pid: event.data.pid,
165
+ // A declined offer is said once, where a worker's console goes (the page's devtools, a Node host's stderr).
166
+ ...(privateRoots.length ? { privateRoots, privateTreeAdoptable: () => processes.size === 0, onDiagnostic: (text) => console.warn(text) } : {}),
167
+ ...(lockClient && event.data.fileLocks ? {
168
+ fileLocks: event.data.fileCoherence
169
+ ? new WaliCoherentFileLockClient(lockClient, new BrowserFileCoherenceClient(event.data.fileCoherence), filesystem)
170
+ : lockClient,
171
+ lockOwner: event.data.fileLocks.owner,
172
+ } : {}),
173
+ });
174
+ if (event.data.updatePort) {
175
+ let updateTimer;
176
+ const updatePort = event.data.updatePort;
177
+ cleanup.add(() => {
178
+ if (updateTimer !== undefined)
179
+ clearTimeout(updateTimer);
180
+ updatePort.onmessage = null;
181
+ updatePort.close();
182
+ });
183
+ updatePort.start();
184
+ const locallyChangedPaths = new Set();
185
+ /**
186
+ * THE PATHS THAT CHANGED, never a diff of two whole trees.
187
+ *
188
+ * This flush used to snapshot the entire filesystem twice and diff it,
189
+ * to publish the two small files a directory-channel round trip writes.
190
+ * The cost was proportional to everything the guest had EVER written,
191
+ * so it grew with the session: measured 2026-09-19 against Blender's
192
+ * real staged tree, 1.3 ms on a fresh session and ~1,500 ms forty-one
193
+ * calls in, three times per host round trip. That WAS the WALI channel
194
+ * floor. `takeChangedPaths()` states the same fact in the size of the
195
+ * change instead of the size of the history.
196
+ */
197
+ const flushUpdates = () => {
198
+ if (updateTimer)
199
+ clearTimeout(updateTimer);
200
+ updateTimer = undefined;
201
+ const paths = filesystem.takeChangedPaths();
202
+ // A program's private trees are never the page's (privateRoots).
203
+ if (privateRoots.length > 0)
204
+ for (const path of paths)
205
+ if (privateRoots.some((root) => underRoot(path, root)))
206
+ paths.delete(path);
207
+ if (paths.size === 0)
208
+ return;
209
+ for (const path of paths)
210
+ locallyChangedPaths.add(path);
211
+ const files = [];
212
+ const removed = [];
213
+ for (const path of paths) {
214
+ // Raw bytes, transferred: base64 of every written file was most of
215
+ // what publishing cost, on the program's own thread (ADR-0046).
216
+ const entry = filesystem.writtenEntry(path, true);
217
+ if (entry)
218
+ files.push(entry);
219
+ else
220
+ removed.push(path);
221
+ }
222
+ if (files.length > 0 || removed.length > 0) {
223
+ updatePort.postMessage({ type: "filesystem", patch: { files, removed } }, files.flatMap((entry) => entry.bytes ? [entry.bytes.buffer] : []));
224
+ unacknowledged = true;
225
+ }
226
+ };
227
+ // Patches posted since the page last acknowledged a checkpoint. With
228
+ // none, the page has committed every write and a checkpoint proves
229
+ // nothing, so the call returns at once: a process's output asks this
230
+ // before each write (`WaliSharedFileDescriptorHost`), not only a spawn.
231
+ let unacknowledged = false;
232
+ prepareSpawn = () => {
233
+ flushUpdates();
234
+ if (!unacknowledged)
235
+ return;
236
+ // Spawn and output use other MessagePorts. Acknowledgement on the
237
+ // ordered update channel proves the page committed these writes first.
238
+ const control = new Int32Array(new SharedArrayBuffer(4));
239
+ updatePort.postMessage({ type: "checkpoint", control: control.buffer });
240
+ Atomics.wait(control, 0, 0, 30_000);
241
+ if (Atomics.load(control, 0) !== 1)
242
+ throw new Error("WALI filesystem spawn checkpoint failed.");
243
+ unacknowledged = false;
244
+ };
245
+ let importing = false;
246
+ let finalized = false;
247
+ finalizeUpdates = () => {
248
+ if (finalized)
249
+ return;
250
+ flushUpdates();
251
+ unpublishedSince = 0;
252
+ finalized = true;
253
+ hostFinalized = true;
254
+ // All guest mutations precede this marker on the same channel.
255
+ // Imports arriving after the cutoff remain the host's own state.
256
+ updatePort.postMessage({ type: "finalized" });
257
+ };
258
+ importPeerPatch = (patch, base) => {
259
+ if (finalized)
260
+ throw new Error("The WALI filesystem is finalized.");
261
+ // Child waits and page imports use the same atomic peer path. Replaying
262
+ // them as per-file writes changes timestamps and echoes peer changes.
263
+ flushUpdates();
264
+ importing = true;
265
+ try {
266
+ // Opening the shared store can observe a newer file than the page's
267
+ // snapshot. That cached state is not a competing guest write. For
268
+ // untouched paths the host update is authoritative; paths changed
269
+ // by the guest retain the supplied base and its conflict checks.
270
+ let expected = base;
271
+ if (base) {
272
+ const entries = new Map(base.files.map(entry => [entry.path, entry]));
273
+ for (const path of [...patch.files.map(entry => entry.path), ...patch.removed]) {
274
+ if (locallyChangedPaths.has(path))
275
+ continue;
276
+ const entry = currentEntry(filesystem, path);
277
+ if (entry)
278
+ entries.set(path, entry);
279
+ else
280
+ entries.delete(path);
281
+ }
282
+ expected = { files: [...entries.values()] };
283
+ }
284
+ applyProcessFilesystemPatch(filesystem, patch, expected);
285
+ // THE PEER'S OWN CHANGES ARE NOT NEWS TO THE PEER. Applying the
286
+ // patch marked every path in it; draining without publishing is
287
+ // what stops this channel echoing the page's writes back at it.
288
+ filesystem.takeChangedPaths();
289
+ updatePort.postMessage({ type: "imported", patch });
290
+ }
291
+ finally {
292
+ importing = false;
293
+ }
294
+ };
295
+ if (inThread) {
296
+ publishUpdates = flushUpdates;
297
+ closePort = () => { portClosed = true; updatePort.postMessage({ type: "port-closed", taken: portTaken }); };
298
+ // Growable: the page grows it to fit an import (the lib types predate growable SharedArrayBuffers).
299
+ const buffer = new SharedArrayBuffer(1 << 20, { maxByteLength: 1 << 30 });
300
+ mailbox = { control: new Int32Array(buffer, 0, 2), buffer };
301
+ updatePort.postMessage({ type: "mailbox", buffer, doorbell: doorbell.buffer });
302
+ publishError = (error) => updatePort.postMessage({ type: "import-error", message: error instanceof Error ? error.message : String(error) });
303
+ }
304
+ updatePort.onmessage = (event) => {
305
+ if (event.data.type !== "import" || finalized || portClosed)
306
+ return;
307
+ portTaken = event.data.sequence ?? portTaken;
308
+ try {
309
+ importPeerPatch(event.data.patch, event.data.base);
310
+ }
311
+ catch (error) {
312
+ updatePort.postMessage({ type: "import-error", message: error instanceof Error ? error.message : String(error) });
313
+ }
314
+ };
315
+ // ONE TASK, NOT A WINDOW. `takeChangedPaths` made a flush cost the size
316
+ // of the change rather than the size of the session's history, so this
317
+ // wait stopped being a batching window and became pure answer latency:
318
+ // it was 100 ms, and `print(1)` through the editor's door measured
319
+ // 230 ms against the standalone skew's 51.
320
+ //
321
+ // IT WAS NOT SAFE TO DROP UNTIL THE CHANNEL HAD ONE WRITER, and that is
322
+ // why this is a comment and not a bare zero. At one-task cadence the
323
+ // directory channel used to die on `assertPatchBase` -- "write conflict
324
+ // at /work/.vgai-session/out/<n>.done" -- because the page's poll loop
325
+ // unlinked four files in the GUEST's own directory after every answer
326
+ // and `pending.filesystemBase` does not carry those deletions until the
327
+ // guest acknowledges an import that may be queued behind another. The
328
+ // 100 ms only made that window almost always empty (call 1 of 10
329
+ // survived at 0 ms). The fix was the protocol's, not this line's: the
330
+ // session owns `out/` and `ask/`, the page owns `in/` and `reply/`, and
331
+ // no path is named by patches from both sides, so the window has
332
+ // nothing left to lose. `setTimeout(_, 0)` still coalesces everything a
333
+ // single guest syscall batch mutates.
334
+ filesystem.setMutationHandler(() => {
335
+ if (importing || finalized)
336
+ return;
337
+ if (inThread) {
338
+ if (unpublishedSince === 0)
339
+ unpublishedSince = performance.now();
340
+ return;
341
+ }
342
+ updateTimer ??= setTimeout(flushUpdates, 0);
343
+ });
344
+ }
345
+ }
346
+ }
347
+ function attachChannel(channel) {
348
+ const control = new Int32Array(channel.control);
349
+ const payload = new Uint8Array(channel.payload);
350
+ const state = { process: channel.process ?? 0,
351
+ ...(channel.bufferLease ? { bufferLease: channel.bufferLease } : {}) };
352
+ const entry = { control, payload, state, port: channel.port };
353
+ const retireChannel = () => {
354
+ channel.releasePort?.close();
355
+ releaseWaliSharedBuffers(channel.bufferLease);
356
+ };
357
+ cleanup.add(retireChannel);
358
+ channels.add(entry);
359
+ if (channel.releasePort) {
360
+ const port = channel.releasePort;
361
+ port.onmessage = () => {
362
+ releaseProcess(state.process);
363
+ cleanup.delete(retireChannel);
364
+ port.postMessage(true);
365
+ port.close();
366
+ releaseWaliSharedBuffers(channel.bufferLease);
367
+ };
368
+ port.start();
369
+ }
370
+ if (channel.descriptorLimits) {
371
+ const table = state.process === 0 ? rootDescriptors()?.descriptors : processes.get(state.process).descriptors.descriptors;
372
+ table?.attachDescriptorLimits(channel.descriptorLimits);
373
+ }
374
+ if (state.process !== 0 && channel.sharedPipes) {
375
+ const table = processes.get(state.process).descriptors.descriptors;
376
+ table.attachSharedEntries(channel.sharedPipes.table, channel.bufferLease);
377
+ }
378
+ channel.port.onmessage = (event) => {
379
+ if (!channels.has(entry))
380
+ return;
381
+ if (typeof event.data !== "number" && "kind" in event.data) {
382
+ attachChannel({ ...event.data, process: event.data.process ?? state.process });
383
+ return;
384
+ }
385
+ if (typeof event.data !== "number") {
386
+ const process = processes.get(state.process);
387
+ if (!process)
388
+ throw new Error("Unknown inherited descriptor table");
389
+ process.memory = event.data.memory;
390
+ return;
391
+ }
392
+ void dispatch(control, payload, state, event.data);
393
+ };
394
+ channel.port.start();
395
+ return entry;
396
+ }
397
+ /** Close ownership, including descriptor caches that capture the process. */
398
+ function releaseProcess(id) {
399
+ const process = processes.get(id);
400
+ try {
401
+ process?.descriptors.descriptors.exit();
402
+ }
403
+ finally {
404
+ // A copied table's memory getter closes over this object. Deleting only
405
+ // the map entry left the child memory alive through ChannelState.handlers.
406
+ if (process)
407
+ delete process.memory;
408
+ processes.delete(id);
409
+ for (const entry of channels) {
410
+ if (entry.state.process !== id)
411
+ continue;
412
+ entry.state.closed = true;
413
+ delete entry.state.handlers;
414
+ delete entry.state.direct;
415
+ deferred.delete(entry);
416
+ channels.delete(entry);
417
+ entry.port.onmessage = null;
418
+ entry.port.close();
419
+ }
420
+ }
421
+ }
422
+ async function dispatch(control, payload, state, requestId) {
423
+ if (retired || state.closed || Atomics.load(control, 0) !== requestId)
424
+ return;
425
+ let result = -38;
426
+ let output = new Uint8Array();
427
+ // CloseProcess retires this channel synchronously, but its caller still
428
+ // needs this final answer before the producer/reap release handshake.
429
+ let replyingToClose = false;
430
+ try {
431
+ const operation = Atomics.load(control, 1);
432
+ replyingToClose = operation === FilesystemOperation.CloseProcess;
433
+ if (operation === FilesystemOperation.SyscallBinary) {
434
+ const pending = binarySyscall(payload, state);
435
+ if (pending === RETRY)
436
+ return;
437
+ // Converted before the answer is claimed: a throw after the claim would leave the caller waiting.
438
+ const value = BigInt(pending instanceof Promise ? await pending : pending);
439
+ // As below: a stale completion never writes over a later request.
440
+ if (retired || (state.closed && !replyingToClose) || Atomics.compareExchange(control, 0, requestId, -requestId) !== requestId)
441
+ return;
442
+ markWaliSharedBufferWritten(state.bufferLease, 1, 8);
443
+ (state.view ??= new DataView(payload.buffer, payload.byteOffset, payload.byteLength)).setBigInt64(0, value, true);
444
+ Atomics.store(control, 2, 0);
445
+ Atomics.store(control, 4, 8);
446
+ Atomics.store(control, 0, requestId + 1);
447
+ Atomics.notify(control, 0);
448
+ return;
449
+ }
450
+ const mapped = mappingOperation(operation, payload, state);
451
+ if (mapped) {
452
+ if (mapped.output.byteLength > payload.byteLength)
453
+ throw Object.assign(new Error("EMSGSIZE"), { code: "EMSGSIZE" });
454
+ if (retired || (state.closed && !replyingToClose) || Atomics.compareExchange(control, 0, requestId, -requestId) !== requestId)
455
+ return;
456
+ markWaliSharedBufferWritten(state.bufferLease, 1, mapped.output.byteLength);
457
+ payload.set(mapped.output);
458
+ Atomics.store(control, 2, mapped.result);
459
+ Atomics.store(control, 4, mapped.output.byteLength);
460
+ Atomics.store(control, 0, requestId + 1);
461
+ Atomics.notify(control, 0);
462
+ return;
463
+ }
464
+ const inputLength = Atomics.load(control, 3);
465
+ const input = inputLength > 0
466
+ ? JSON.parse(decoder.decode(payload.slice(0, inputLength)))
467
+ : {};
468
+ const pending = operate(operation, input, state);
469
+ if (pending === RETRY)
470
+ return;
471
+ const response = pending instanceof Promise ? await pending : pending;
472
+ output = response === undefined ? new Uint8Array() : encoder.encode(JSON.stringify(response));
473
+ result = 0;
474
+ }
475
+ catch (error) {
476
+ result = negativeErrno(error);
477
+ }
478
+ if (output.byteLength > payload.byteLength) {
479
+ result = -90;
480
+ output = new Uint8Array();
481
+ }
482
+ // A timed-out caller may already have cancelled this generation and begun a
483
+ // later request. Never let a stale completion overwrite its response.
484
+ if (retired || (state.closed && !replyingToClose) || Atomics.compareExchange(control, 0, requestId, -requestId) !== requestId)
485
+ return;
486
+ if (output.byteLength > 0) {
487
+ markWaliSharedBufferWritten(state.bufferLease, 1, output.byteLength);
488
+ payload.set(output);
489
+ }
490
+ Atomics.store(control, 2, result);
491
+ Atomics.store(control, 4, output.byteLength);
492
+ Atomics.store(control, 0, requestId + 1);
493
+ Atomics.notify(control, 0);
494
+ }
495
+ function rootDescriptors() { return descriptors; }
496
+ function operate(operation, input, state) {
497
+ const descriptors = state.process === 0 ? rootDescriptors() : processes.get(state.process)?.descriptors;
498
+ if (operation === FilesystemOperation.GetCwd)
499
+ return descriptors?.currentWorkingDirectory();
500
+ if (operation === FilesystemOperation.CopyForExec || operation === FilesystemOperation.CopyForFork) {
501
+ if (!descriptors)
502
+ throw new Error("Missing parent descriptor table");
503
+ const process = {
504
+ descriptors: descriptors[operation === FilesystemOperation.CopyForFork ? "copyForFork" : "copyForExec"](() => {
505
+ if (!process.memory)
506
+ throw new Error("Child memory is not attached");
507
+ return process.memory;
508
+ }, Array.isArray(input.stdio) ? input.stdio.map(requiredNumber) : undefined),
509
+ };
510
+ const id = nextProcess++;
511
+ processes.set(id, process);
512
+ // The child's cwd and umask are the parent's at the copy; they ride this
513
+ // answer rather than round trips of their own on every fork.
514
+ return { process: id, cwd: descriptors.currentWorkingDirectory(), umask: descriptors.creationMask() };
515
+ }
516
+ if (operation === FilesystemOperation.CloseProcess) {
517
+ if (state.process !== 0)
518
+ releaseProcess(state.process);
519
+ else
520
+ descriptors?.descriptors.exit();
521
+ return;
522
+ }
523
+ const path = () => requiredString(input.path);
524
+ if (operation === FilesystemOperation.PrepareSpawn) {
525
+ prepareSpawn?.();
526
+ return;
527
+ }
528
+ if (operation === FilesystemOperation.ImportPeerPatch) {
529
+ importPeerPatch(input.patch);
530
+ return;
531
+ }
532
+ if (operation === FilesystemOperation.Exists)
533
+ return filesystem.existsSync(path());
534
+ if (operation === FilesystemOperation.Stat || operation === FilesystemOperation.Lstat) {
535
+ const stat = operation === FilesystemOperation.Lstat ? filesystem.lstatSync(path()) : filesystem.statSync(path());
536
+ return {
537
+ file: stat.isFile(), directory: stat.isDirectory(), symbolicLink: stat.isSymbolicLink?.(),
538
+ size: stat.size, mode: stat.mode,
539
+ mtimeMs: stat.mtimeMs, atimeMs: stat.atimeMs, ctimeMs: stat.ctimeMs, nlink: stat.nlink,
540
+ uid: stat.uid, gid: stat.gid, dev: stat.dev, ino: stat.ino, rdev: stat.rdev,
541
+ blksize: stat.blksize, blocks: stat.blocks,
542
+ };
543
+ }
544
+ if (operation === FilesystemOperation.ReadFile)
545
+ return { content: encodeBase64(filesystem.readFileSync(path())) };
546
+ if (operation === FilesystemOperation.WriteFile) {
547
+ filesystem.writeFileSync(path(), decodeBase64(requiredString(input.content)));
548
+ return;
549
+ }
550
+ if (operation === FilesystemOperation.ReadRange) {
551
+ const offset = requiredNumber(input.offset);
552
+ const length = requiredNumber(input.length);
553
+ const bytes = filesystem.readRangeSync
554
+ ? filesystem.readRangeSync(path(), offset, length)
555
+ : filesystem.readFileSync(path()).subarray(offset, offset + length);
556
+ return { content: encodeBase64(bytes) };
557
+ }
558
+ if (operation === FilesystemOperation.WriteRange) {
559
+ const offset = requiredNumber(input.offset);
560
+ const bytes = decodeBase64(requiredString(input.content));
561
+ if (filesystem.writeRangeSync)
562
+ filesystem.writeRangeSync(path(), offset, bytes);
563
+ else {
564
+ const current = filesystem.existsSync(path()) ? filesystem.readFileSync(path()) : new Uint8Array();
565
+ const next = new Uint8Array(Math.max(current.byteLength, offset + bytes.byteLength));
566
+ next.set(current);
567
+ next.set(bytes, offset);
568
+ filesystem.writeFileSync(path(), next);
569
+ }
570
+ return;
571
+ }
572
+ if (operation === FilesystemOperation.Mkdir) {
573
+ filesystem.mkdirSync(path(), { recursive: input.recursive === true });
574
+ return;
575
+ }
576
+ if (operation === FilesystemOperation.Readdir)
577
+ return filesystem.readdirSync(path());
578
+ if (operation === FilesystemOperation.Unlink) {
579
+ filesystem.unlinkSync(path());
580
+ return;
581
+ }
582
+ if (operation === FilesystemOperation.Rmdir) {
583
+ filesystem.rmdirSync(path());
584
+ return;
585
+ }
586
+ if (operation === FilesystemOperation.Rename) {
587
+ filesystem.renameSync(requiredString(input.from), requiredString(input.to));
588
+ return;
589
+ }
590
+ if (operation === FilesystemOperation.Snapshot)
591
+ return filesystem.toSnapshot();
592
+ if (operation === FilesystemOperation.SnapshotSize || operation === FilesystemOperation.FinalSnapshotSize) {
593
+ if (operation === FilesystemOperation.FinalSnapshotSize)
594
+ finalizeUpdates?.();
595
+ state.snapshot = encoder.encode(JSON.stringify(filesystem.toSnapshot()));
596
+ return { length: state.snapshot.byteLength };
597
+ }
598
+ if (operation === FilesystemOperation.SnapshotChunk) {
599
+ const offset = requiredNumber(input.offset);
600
+ const length = requiredNumber(input.length);
601
+ if (!state.snapshot || offset < 0 || length < 0 || length > FILESYSTEM_SNAPSHOT_CHUNK_BYTES
602
+ || offset + length > state.snapshot.byteLength) {
603
+ throw Object.assign(new Error("EINVAL"), { code: "EINVAL" });
604
+ }
605
+ const content = encodeBase64(state.snapshot.subarray(offset, offset + length));
606
+ if (offset + length === state.snapshot.byteLength)
607
+ delete state.snapshot;
608
+ return { content };
609
+ }
610
+ if (operation === FilesystemOperation.ReplaceSnapshot) {
611
+ filesystem.replaceSnapshot(input.snapshot);
612
+ return;
613
+ }
614
+ if (operation === FilesystemOperation.ReplaceSnapshotStart || operation === FilesystemOperation.ImportPeerPatchStart) {
615
+ const length = requiredNumber(input.length);
616
+ if (length <= 0 || length > 256 * 1024 * 1024) {
617
+ throw Object.assign(new Error("EINVAL"), { code: "EINVAL" });
618
+ }
619
+ state.replacement = { bytes: new Uint8Array(length), offset: 0, peerPatch: operation === FilesystemOperation.ImportPeerPatchStart };
620
+ return;
621
+ }
622
+ if (operation === FilesystemOperation.ReplaceSnapshotChunk || operation === FilesystemOperation.ImportPeerPatchChunk) {
623
+ const offset = requiredNumber(input.offset);
624
+ const chunk = decodeBase64(requiredString(input.content));
625
+ const replacement = state.replacement;
626
+ if (!replacement || replacement.peerPatch !== (operation === FilesystemOperation.ImportPeerPatchChunk)
627
+ || offset !== replacement.offset || chunk.byteLength > FILESYSTEM_SNAPSHOT_CHUNK_BYTES
628
+ || offset + chunk.byteLength > replacement.bytes.byteLength) {
629
+ throw Object.assign(new Error("EINVAL"), { code: "EINVAL" });
630
+ }
631
+ replacement.bytes.set(chunk, offset);
632
+ replacement.offset += chunk.byteLength;
633
+ if (replacement.offset === replacement.bytes.byteLength) {
634
+ delete state.replacement;
635
+ const value = JSON.parse(decoder.decode(replacement.bytes));
636
+ if (replacement.peerPatch)
637
+ importPeerPatch(value);
638
+ else
639
+ filesystem.replaceSnapshot(value);
640
+ }
641
+ return;
642
+ }
643
+ if (operation === FilesystemOperation.Syscall) {
644
+ if (!descriptors)
645
+ throw Object.assign(new Error("EIO"), { code: "EIO" });
646
+ const name = requiredString(input.name);
647
+ const handler = syscallHandlers(state, descriptors)[name];
648
+ if (!handler)
649
+ throw Object.assign(new Error("ENOSYS"), { code: "ENOSYS" });
650
+ const args = Array.isArray(input.args) ? input.args.map((value) => BigInt(requiredString(value))) : [];
651
+ const result = invokeSyscall(name, handler, args);
652
+ if (result === RETRY)
653
+ return RETRY;
654
+ return result instanceof Promise ? result.then((value) => ({ result: String(value) })) : { result: String(result) };
655
+ }
656
+ if (operation === FilesystemOperation.ReadForMapping) {
657
+ if (!descriptors)
658
+ throw Object.assign(new Error("EIO"), { code: "EIO" });
659
+ const value = descriptors.readForMapping(requiredNumber(input.fd), requiredNumber(input.offset), requiredNumber(input.length));
660
+ return value ? { content: encodeBase64(value) } : {};
661
+ }
662
+ if (operation === FilesystemOperation.MappingPath) {
663
+ if (!descriptors)
664
+ throw Object.assign(new Error("EIO"), { code: "EIO" });
665
+ const value = descriptors.mappingPath(requiredNumber(input.fd));
666
+ return value ? { path: value } : {};
667
+ }
668
+ if (operation === FilesystemOperation.WriteForMapping) {
669
+ if (!descriptors)
670
+ throw Object.assign(new Error("EIO"), { code: "EIO" });
671
+ return { written: descriptors.writeForMapping(requiredString(input.path), requiredNumber(input.offset), decodeBase64(requiredString(input.content))) };
672
+ }
673
+ throw Object.assign(new Error("ENOSYS"), { code: "ENOSYS" });
674
+ }
675
+ /** The channel's syscall handlers, made once for its process's descriptor table (see `ChannelState.handlers`). */
676
+ function syscallHandlers(state, descriptors) {
677
+ const table = descriptors.descriptors;
678
+ const held = state.handlers;
679
+ if (held && held.table === descriptors && held.entries === table.sharedEntries.buffer)
680
+ return held.imports;
681
+ const pipeHost = sharedPipes && new WaliSharedPipeHost(() => {
682
+ const memory = state.process === 0 ? rootMemory : processes.get(state.process)?.memory;
683
+ if (!memory)
684
+ throw new Error("The WALI filesystem process has no memory.");
685
+ return memory;
686
+ }, { ...sharedPipes, table: table.sharedEntries.buffer }, table);
687
+ const imports = pipeHost ? pipeHost.imports(descriptors.imports()) : descriptors.imports();
688
+ state.handlers = { table: descriptors, entries: table.sharedEntries.buffer, imports };
689
+ return imports;
690
+ }
691
+ /** A `SyscallBinary` request's result (see its layout in `filesystem-protocol.ts`). */
692
+ function binarySyscall(payload, state) {
693
+ const descriptors = state.process === 0 ? rootDescriptors() : processes.get(state.process)?.descriptors;
694
+ if (!descriptors)
695
+ throw Object.assign(new Error("EIO"), { code: "EIO" });
696
+ const view = state.view ??= new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
697
+ const count = payload[0];
698
+ const name = decoder.decode(payload.slice(8 + count * 8, 8 + count * 8 + payload[1]));
699
+ const args = [];
700
+ for (let index = 0; index < count; index += 1)
701
+ args.push(view.getBigInt64(8 + index * 8, true));
702
+ const handler = syscallHandlers(state, descriptors)[name];
703
+ if (!handler)
704
+ throw Object.assign(new Error("ENOSYS"), { code: "ENOSYS" });
705
+ return invokeSyscall(name, handler, args);
706
+ }
707
+ /** Synchronous, except a blocking advisory lock, which is retried without blocking this worker. */
708
+ function invokeSyscall(name, handler, args) {
709
+ let nonblockingArgs;
710
+ if (name === "SYS_flock") {
711
+ const operation = Number(args[1] ?? 0);
712
+ if ((operation & 4) === 0 && (operation & 8) === 0) {
713
+ nonblockingArgs = [...args];
714
+ nonblockingArgs[1] = BigInt(operation | 4);
715
+ }
716
+ }
717
+ else if (name === "SYS_fcntl") {
718
+ const command = Number(args[1] ?? 0);
719
+ if (command === 7 || command === 38) {
720
+ nonblockingArgs = [...args];
721
+ nonblockingArgs[1] = BigInt(command === 7 ? 6 : 37);
722
+ }
723
+ }
724
+ if (!nonblockingArgs)
725
+ return handler(...args) ?? 0;
726
+ if (inThread) {
727
+ // On the main thread a lock that would block is not waited for here: its
728
+ // holder may be the main thread itself. The request stays unanswered and
729
+ // is asked again at the next pump.
730
+ const result = handler(...nonblockingArgs) ?? 0;
731
+ return BigInt(result) === -11n ? RETRY : result;
732
+ }
733
+ return blockingLock(handler, nonblockingArgs);
734
+ }
735
+ async function blockingLock(handler, nonblockingArgs) {
736
+ for (;;) {
737
+ if (retired)
738
+ return -9;
739
+ const result = handler(...nonblockingArgs) ?? 0;
740
+ if (BigInt(result) !== -11n)
741
+ return result;
742
+ // A blocking advisory lock must not block this worker: it owns the shared
743
+ // descriptor table for every pthread, including the holder that must
744
+ // write and unlock the file. Yield between nonblocking attempts so those
745
+ // syscalls can make progress on their own per-thread RPC channels.
746
+ await new Promise((resolve) => setTimeout(resolve, 5));
747
+ }
748
+ }
749
+ function requiredString(value) {
750
+ if (typeof value !== "string")
751
+ throw Object.assign(new Error("EINVAL"), { code: "EINVAL" });
752
+ return value;
753
+ }
754
+ function requiredNumber(value) {
755
+ if (typeof value !== "number" || !Number.isSafeInteger(value))
756
+ throw Object.assign(new Error("EINVAL"), { code: "EINVAL" });
757
+ return value;
758
+ }
759
+ function negativeErrno(error) {
760
+ const message = error instanceof Error ? error.message : String(error);
761
+ const name = typeof error === "object" && error !== null && "code" in error ? String(error.code)
762
+ : /\b(E[A-Z]+)\b/u.exec(message)?.[1] ?? "EIO";
763
+ return -({ ENOENT: 2, EBADF: 9, EACCES: 13, EEXIST: 17, ENOTDIR: 20, EISDIR: 21,
764
+ EINVAL: 22, EMFILE: 24, ENOSPC: 28, ESPIPE: 29, ERANGE: 34, ENOSYS: 38,
765
+ ENOTEMPTY: 39, EMSGSIZE: 90 }[name] ?? 5);
766
+ }
767
+ /**
768
+ * One channel's waiting request, answered now on this thread (ADR-0046). A
769
+ * request whose blocking lock is held is left waiting and asked again later.
770
+ * Answers exactly as `dispatch` does.
771
+ */
772
+ function serveNow(entry) {
773
+ const { control, payload, state } = entry;
774
+ const requestId = Atomics.load(control, 0);
775
+ if (requestId <= 0 || (requestId & 1) === 0)
776
+ return false;
777
+ let result = -38;
778
+ let output = new Uint8Array();
779
+ let binary;
780
+ let mapped;
781
+ try {
782
+ const operation = Atomics.load(control, 1);
783
+ if (operation === FilesystemOperation.SyscallBinary) {
784
+ const value = binarySyscall(payload, state);
785
+ if (value === RETRY) {
786
+ deferred.add(entry);
787
+ return false;
788
+ }
789
+ if (value instanceof Promise)
790
+ throw new Error("EIO: an asynchronous syscall on the main thread");
791
+ binary = BigInt(value);
792
+ }
793
+ else if ((mapped = mappingOperation(operation, payload, state)) !== undefined) {
794
+ output = mapped.output;
795
+ }
796
+ else {
797
+ const inputLength = Atomics.load(control, 3);
798
+ const input = inputLength > 0 ? JSON.parse(decoder.decode(payload.slice(0, inputLength))) : {};
799
+ const response = operate(operation, input, state);
800
+ if (response === RETRY) {
801
+ deferred.add(entry);
802
+ return false;
803
+ }
804
+ if (response instanceof Promise)
805
+ throw new Error("EIO: an asynchronous operation on the main thread");
806
+ output = response === undefined ? new Uint8Array() : encoder.encode(JSON.stringify(response));
807
+ }
808
+ result = mapped?.result ?? 0;
809
+ }
810
+ catch (error) {
811
+ result = negativeErrno(error);
812
+ binary = undefined;
813
+ }
814
+ deferred.delete(entry);
815
+ if (output.byteLength > payload.byteLength) {
816
+ result = -90;
817
+ output = new Uint8Array();
818
+ }
819
+ if (Atomics.compareExchange(control, 0, requestId, -requestId) !== requestId)
820
+ return true;
821
+ if (binary !== undefined) {
822
+ markWaliSharedBufferWritten(state.bufferLease, 1, 8);
823
+ (state.view ??= new DataView(payload.buffer, payload.byteOffset, payload.byteLength)).setBigInt64(0, binary, true);
824
+ output = new Uint8Array(8);
825
+ }
826
+ else if (output.byteLength > 0) {
827
+ markWaliSharedBufferWritten(state.bufferLease, 1, output.byteLength);
828
+ payload.set(output);
829
+ }
830
+ Atomics.store(control, 2, result);
831
+ Atomics.store(control, 4, binary !== undefined ? 8 : output.byteLength);
832
+ Atomics.store(control, 0, requestId + 1);
833
+ Atomics.notify(control, 0);
834
+ return true;
835
+ }
836
+ /** Whether a pump has anything to do: a ring or a deferred lock. */
837
+ function hostWaiting() {
838
+ if (!inThread)
839
+ return false;
840
+ return Atomics.load(doorbell, 0) !== doorbellSeen || deferred.size > 0;
841
+ }
842
+ /**
843
+ * What the main thread owes while it is in the host (ADR-0046): the page's
844
+ * import, every other channel's waiting request, and, when it is about to
845
+ * block or has not published for a while, its changes to the page.
846
+ */
847
+ function pumpHost(blocking = false) {
848
+ if (!inThread || pumping)
849
+ return;
850
+ pumping = true;
851
+ try {
852
+ doorbellSeen = Atomics.load(doorbell, 0);
853
+ if (mailbox && !hostFinalized && Atomics.load(mailbox.control, 0) === 1) {
854
+ // Never thrown out of a pump: every wait of this thread pumps, and one
855
+ // that swallowed the error would spin (review of 31309ca).
856
+ try {
857
+ const length = Atomics.load(mailbox.control, 1);
858
+ const message = JSON.parse(decoder.decode(new Uint8Array(mailbox.buffer, 8, length).slice()));
859
+ Atomics.store(mailbox.control, 0, 0);
860
+ if (message.type === "import")
861
+ importPeerPatch(message.patch, message.base);
862
+ }
863
+ catch (error) {
864
+ Atomics.store(mailbox.control, 0, 0);
865
+ publishError?.(error);
866
+ }
867
+ }
868
+ for (const entry of channels)
869
+ serveNow(entry);
870
+ if (publishUpdates && unpublishedSince !== 0 && blocking) {
871
+ unpublishedSince = 0;
872
+ publishUpdates();
873
+ }
874
+ }
875
+ finally {
876
+ pumping = false;
877
+ }
878
+ }
879
+ let publishError;
880
+ /** A raw mapping operation (`ReadForMappingBinary`, `WriteForMappingBinary`): its result and the bytes it answers. */
881
+ function mappingOperation(operation, payload, state) {
882
+ if (operation !== FilesystemOperation.ReadForMappingBinary && operation !== FilesystemOperation.WriteForMappingBinary)
883
+ return undefined;
884
+ const descriptors = state.process === 0 ? rootDescriptors() : processes.get(state.process)?.descriptors;
885
+ if (!descriptors)
886
+ throw Object.assign(new Error("EIO"), { code: "EIO" });
887
+ const view = state.view ??= new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
888
+ if (operation === FilesystemOperation.ReadForMappingBinary) {
889
+ const length = Math.min(view.getInt32(16, true), payload.byteLength);
890
+ const value = descriptors.readForMapping(view.getInt32(0, true), view.getFloat64(8, true), length);
891
+ return value ? { result: 0, output: value } : { result: 1, output: new Uint8Array() };
892
+ }
893
+ const pathLength = view.getUint32(8, true);
894
+ const valueLength = view.getUint32(12, true);
895
+ const path = decoder.decode(payload.slice(16, 16 + pathLength));
896
+ const written = descriptors.writeForMapping(path, view.getFloat64(0, true), payload.slice(16 + pathLength, 16 + pathLength + valueLength));
897
+ return { result: 0, output: new Uint8Array([written ? 1 : 0]) };
898
+ }
899
+ /** A root as a path, as runtime.ts makes it: absolute, no empty component, no trailing slash; "/" itself kept. */
900
+ function normalizeRoot(root) { return `/${root.split("/").filter(Boolean).join("/")}`; }
901
+ /** Whether `path` is `root` or lies below it. */
902
+ function underRoot(path, root) {
903
+ return root === "/" || path === root || path.startsWith(`${root}/`);
904
+ }
905
+ /**
906
+ * A syscall on the main thread's own channel, called (ADR-0046): the same
907
+ * handlers `SyscallBinary` reaches, with no payload encoded and decoded
908
+ * between the caller and them. Undefined while a held lock defers it.
909
+ */
910
+ function syscallNow(entry, name, args) {
911
+ try {
912
+ const state = entry.state;
913
+ const table = state.process === 0 ? rootDescriptors() : processes.get(state.process)?.descriptors;
914
+ if (!table)
915
+ throw Object.assign(new Error("EIO"), { code: "EIO" });
916
+ const handler = syscallHandlers(state, table)[name];
917
+ if (!handler)
918
+ throw Object.assign(new Error("ENOSYS"), { code: "ENOSYS" });
919
+ const result = invokeSyscall(name, handler, args);
920
+ if (result === RETRY)
921
+ return undefined;
922
+ if (result instanceof Promise)
923
+ throw new Error("EIO: an asynchronous syscall on the main thread");
924
+ return BigInt(result);
925
+ }
926
+ catch (error) {
927
+ // As a channel answers a throw: its errno, which the caller throws as it would the answer's.
928
+ return { failed: negativeErrno(error) };
929
+ }
930
+ }
931
+ /** The main thread's own channel's handlers, and the errno a throw from one answers (ADR-0046). */
932
+ function handlersNow(entry) {
933
+ const state = entry.state;
934
+ const table = state.process === 0 ? rootDescriptors() : processes.get(state.process)?.descriptors;
935
+ if (!table)
936
+ throw Object.assign(new Error("EIO"), { code: "EIO" });
937
+ // THE FILE TABLE'S OWN HANDLERS, made once per table: the caller is the
938
+ // main thread's router, which the runtime's pipe layer reaches only with a
939
+ // descriptor it found is not a pipe, so the host's pipe layer around these
940
+ // (`syscallHandlers`, for the other threads' requests) would ask again; and
941
+ // a new result object per call was the rest (2026-09-28).
942
+ const held = state.direct;
943
+ if (held && held.table === table)
944
+ return held.handlers;
945
+ const handlers = { imports: table.imports(), errno: negativeErrno, owns: (fd) => table.ownsFile(fd) };
946
+ state.direct = { table, handlers };
947
+ return handlers;
948
+ }
949
+ return { closeHostPort, resetHost, hostInThread, initializeHost, attachChannel, serveNow, hostWaiting, pumpHost, underRoot, syscallNow, handlersNow };
950
+ }
951
+ //# sourceMappingURL=filesystem-host-session.js.map