@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.
- package/LICENSE +8 -0
- package/README.md +9 -0
- package/dist/browser-wali-command.d.ts +7 -0
- package/dist/browser-wali-command.d.ts.map +1 -1
- package/dist/browser-wali-command.js +70 -18
- package/dist/browser-wali-command.js.map +1 -1
- package/dist/bulk-memory.d.ts +4 -2
- package/dist/bulk-memory.d.ts.map +1 -1
- package/dist/bulk-memory.js +32 -9
- package/dist/bulk-memory.js.map +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +6 -2
- package/dist/capabilities.js.map +1 -1
- package/dist/child-processes.d.ts +2 -0
- package/dist/child-processes.d.ts.map +1 -1
- package/dist/child-processes.js +9 -1
- package/dist/child-processes.js.map +1 -1
- package/dist/clocks.d.ts +3 -0
- package/dist/clocks.d.ts.map +1 -0
- package/dist/clocks.js +44 -0
- package/dist/clocks.js.map +1 -0
- package/dist/coherent-file-locks.d.ts.map +1 -1
- package/dist/coherent-file-locks.js +2 -1
- package/dist/coherent-file-locks.js.map +1 -1
- package/dist/credentials.d.ts +4 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +46 -0
- package/dist/credentials.js.map +1 -0
- package/dist/descriptors.d.ts +14 -0
- package/dist/descriptors.d.ts.map +1 -1
- package/dist/descriptors.js +45 -5
- package/dist/descriptors.js.map +1 -1
- package/dist/environment.d.ts +4 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +23 -0
- package/dist/environment.js.map +1 -0
- package/dist/epoll-ready.d.ts +27 -0
- package/dist/epoll-ready.d.ts.map +1 -0
- package/dist/epoll-ready.js +88 -0
- package/dist/epoll-ready.js.map +1 -0
- package/dist/events.d.ts +1 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +55 -24
- package/dist/events.js.map +1 -1
- package/dist/filesystem-host-session.d.ts +65 -0
- package/dist/filesystem-host-session.d.ts.map +1 -0
- package/dist/filesystem-host-session.js +951 -0
- package/dist/filesystem-host-session.js.map +1 -0
- package/dist/filesystem-host.d.ts +13 -81
- package/dist/filesystem-host.d.ts.map +1 -1
- package/dist/filesystem-host.js +14 -882
- package/dist/filesystem-host.js.map +1 -1
- package/dist/filesystem-protocol.d.ts +3 -1
- package/dist/filesystem-protocol.d.ts.map +1 -1
- package/dist/filesystem-remote.d.ts +8 -3
- package/dist/filesystem-remote.d.ts.map +1 -1
- package/dist/filesystem-remote.js +43 -20
- package/dist/filesystem-remote.js.map +1 -1
- package/dist/filesystem-service-client.d.ts +4 -0
- package/dist/filesystem-service-client.d.ts.map +1 -0
- package/dist/filesystem-service-client.js +16 -0
- package/dist/filesystem-service-client.js.map +1 -0
- package/dist/filesystem.d.ts +26 -4
- package/dist/filesystem.d.ts.map +1 -1
- package/dist/filesystem.js +633 -175
- package/dist/filesystem.js.map +1 -1
- package/dist/fork-memory.d.ts +23 -0
- package/dist/fork-memory.d.ts.map +1 -0
- package/dist/fork-memory.js +93 -0
- package/dist/fork-memory.js.map +1 -0
- package/dist/fork-worker.js +32 -11
- package/dist/fork-worker.js.map +1 -1
- package/dist/jumps.d.ts +7 -0
- package/dist/jumps.d.ts.map +1 -1
- package/dist/jumps.js +19 -1
- package/dist/jumps.js.map +1 -1
- package/dist/kernel-filesystem-worker.d.ts +2 -0
- package/dist/kernel-filesystem-worker.d.ts.map +1 -0
- package/dist/kernel-filesystem-worker.js +28 -0
- package/dist/kernel-filesystem-worker.js.map +1 -0
- package/dist/kernel-filesystem.d.ts +12 -0
- package/dist/kernel-filesystem.d.ts.map +1 -0
- package/dist/kernel-filesystem.js +45 -0
- package/dist/kernel-filesystem.js.map +1 -0
- package/dist/kernel-network-worker.d.ts +2 -0
- package/dist/kernel-network-worker.d.ts.map +1 -0
- package/dist/kernel-network-worker.js +14 -0
- package/dist/kernel-network-worker.js.map +1 -0
- package/dist/kernel-network.d.ts +14 -0
- package/dist/kernel-network.d.ts.map +1 -0
- package/dist/kernel-network.js +50 -0
- package/dist/kernel-network.js.map +1 -0
- package/dist/kernel-process-client.d.ts +6 -1
- package/dist/kernel-process-client.d.ts.map +1 -1
- package/dist/kernel-process-client.js +10 -1
- package/dist/kernel-process-client.js.map +1 -1
- package/dist/module-layout.d.ts +8 -0
- package/dist/module-layout.d.ts.map +1 -1
- package/dist/module-layout.js +263 -2
- package/dist/module-layout.js.map +1 -1
- package/dist/network-host.d.ts +6 -0
- package/dist/network-host.d.ts.map +1 -0
- package/dist/network-host.js +1582 -0
- package/dist/network-host.js.map +1 -0
- package/dist/network-protocol.d.ts +9 -2
- package/dist/network-protocol.d.ts.map +1 -1
- package/dist/network-protocol.js +3 -0
- package/dist/network-protocol.js.map +1 -1
- package/dist/network-service-client.d.ts +4 -0
- package/dist/network-service-client.d.ts.map +1 -0
- package/dist/network-service-client.js +16 -0
- package/dist/network-service-client.js.map +1 -0
- package/dist/network-worker.js +5 -1150
- package/dist/network-worker.js.map +1 -1
- package/dist/network.d.ts +13 -1
- package/dist/network.d.ts.map +1 -1
- package/dist/network.js +175 -48
- package/dist/network.js.map +1 -1
- package/dist/paths.d.ts +11 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +97 -0
- package/dist/paths.js.map +1 -0
- package/dist/process-bridge.d.ts +12 -4
- package/dist/process-bridge.d.ts.map +1 -1
- package/dist/process-bridge.js +53 -17
- package/dist/process-bridge.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +8 -0
- package/dist/program.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +150 -92
- package/dist/runtime.js.map +1 -1
- package/dist/shared-buffer-pool.d.ts +18 -5
- package/dist/shared-buffer-pool.d.ts.map +1 -1
- package/dist/shared-buffer-pool.js +86 -14
- package/dist/shared-buffer-pool.js.map +1 -1
- package/dist/shared-file-layer.d.ts +3 -1
- package/dist/shared-file-layer.d.ts.map +1 -1
- package/dist/shared-file-layer.js +150 -35
- package/dist/shared-file-layer.js.map +1 -1
- package/dist/shared-pipes.d.ts +3 -1
- package/dist/shared-pipes.d.ts.map +1 -1
- package/dist/shared-pipes.js +105 -33
- package/dist/shared-pipes.js.map +1 -1
- package/dist/signal-interruption.d.ts +9 -0
- package/dist/signal-interruption.d.ts.map +1 -0
- package/dist/signal-interruption.js +6 -0
- package/dist/signal-interruption.js.map +1 -0
- package/dist/signals.d.ts +17 -6
- package/dist/signals.d.ts.map +1 -1
- package/dist/signals.js +101 -22
- package/dist/signals.js.map +1 -1
- package/dist/snapshot-filesystem.d.ts +12 -1
- package/dist/snapshot-filesystem.d.ts.map +1 -1
- package/dist/snapshot-filesystem.js +241 -73
- package/dist/snapshot-filesystem.js.map +1 -1
- package/dist/socket-flags.d.ts +3 -0
- package/dist/socket-flags.d.ts.map +1 -0
- package/dist/socket-flags.js +9 -0
- package/dist/socket-flags.js.map +1 -0
- package/dist/special-files.d.ts +6 -0
- package/dist/special-files.d.ts.map +1 -0
- package/dist/special-files.js +56 -0
- package/dist/special-files.js.map +1 -0
- package/dist/stdin.d.ts.map +1 -1
- package/dist/stdin.js +13 -5
- package/dist/stdin.js.map +1 -1
- package/dist/symlink.d.ts +2 -1
- package/dist/symlink.d.ts.map +1 -1
- package/dist/symlink.js +25 -93
- package/dist/symlink.js.map +1 -1
- package/dist/system.d.ts +2 -4
- package/dist/system.d.ts.map +1 -1
- package/dist/system.js +46 -29
- package/dist/system.js.map +1 -1
- package/dist/thread-coordinator-worker.js +58 -24
- package/dist/thread-coordinator-worker.js.map +1 -1
- package/dist/thread-lifecycle.d.ts +9 -0
- package/dist/thread-lifecycle.d.ts.map +1 -0
- package/dist/thread-lifecycle.js +30 -0
- package/dist/thread-lifecycle.js.map +1 -0
- package/dist/thread-worker.js +125 -44
- package/dist/thread-worker.js.map +1 -1
- package/dist/threads.d.ts +6 -0
- package/dist/threads.d.ts.map +1 -1
- package/dist/threads.js +46 -6
- package/dist/threads.js.map +1 -1
- package/dist/tty-job-control.d.ts +13 -0
- package/dist/tty-job-control.d.ts.map +1 -0
- package/dist/tty-job-control.js +56 -0
- package/dist/tty-job-control.js.map +1 -0
- package/dist/tty-settings.d.ts +3 -0
- package/dist/tty-settings.d.ts.map +1 -0
- package/dist/tty-settings.js +25 -0
- package/dist/tty-settings.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wait-state.d.ts +8 -0
- package/dist/wait-state.d.ts.map +1 -1
- package/dist/wait-state.js +15 -0
- package/dist/wait-state.js.map +1 -1
- package/dist/worker-program-protocol.d.ts +22 -2
- package/dist/worker-program-protocol.d.ts.map +1 -1
- package/dist/worker-program-worker.js +31 -9
- package/dist/worker-program-worker.js.map +1 -1
- package/dist/worker-program.d.ts +12 -2
- package/dist/worker-program.d.ts.map +1 -1
- package/dist/worker-program.js +214 -36
- package/dist/worker-program.js.map +1 -1
- package/dist/write-vector.d.ts +3 -0
- package/dist/write-vector.d.ts.map +1 -0
- package/dist/write-vector.js +35 -0
- package/dist/write-vector.js.map +1 -0
- 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
|