@cloudflare/workspace 0.0.0-alpha.0

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/dist/index.js ADDED
@@ -0,0 +1,2902 @@
1
+ import { createHash } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+ import { RpcTarget, newWebSocketRpcSession } from "capnweb";
4
+ import { RpcTarget as RpcTarget$1, WorkerEntrypoint } from "cloudflare:workers";
5
+ //#region ../dofs/src/errors.ts
6
+ function createWorkspaceError(code, message, path) {
7
+ const error = new Error(path === void 0 ? message : `${message}: ${path}`);
8
+ error.name = "WorkspaceFsError";
9
+ error.code = code;
10
+ error.path = path;
11
+ return error;
12
+ }
13
+ function invalidPath(path, reason) {
14
+ return createWorkspaceError("EINVAL", `Invalid path (${reason})`, path);
15
+ }
16
+ //#endregion
17
+ //#region ../dofs/src/path.ts
18
+ function canonicalizePath(path) {
19
+ if (path.length === 0) throw invalidPath(path, "empty");
20
+ if (!path.startsWith("/")) throw invalidPath(path, "must be absolute");
21
+ if (path.includes("\0")) throw invalidPath(path, "contains NUL byte");
22
+ const parts = [];
23
+ for (const part of path.split("/")) {
24
+ if (part === "" || part === ".") continue;
25
+ if (part === "..") {
26
+ if (parts.length === 0) throw invalidPath(path, "escapes root");
27
+ parts.pop();
28
+ continue;
29
+ }
30
+ parts.push(part);
31
+ }
32
+ const canonical = parts.length === 0 ? "/" : `/${parts.join("/")}`;
33
+ const name = parts.length === 0 ? "" : parts[parts.length - 1];
34
+ const parentParts = parts.slice(0, -1);
35
+ return {
36
+ path: canonical,
37
+ parts,
38
+ name,
39
+ parentPath: parts.length === 0 ? void 0 : parentParts.length === 0 ? "/" : `/${parentParts.join("/")}`
40
+ };
41
+ }
42
+ //#endregion
43
+ //#region ../dofs/src/schema/core.ts
44
+ const CORE_STATEMENTS = [
45
+ `CREATE TABLE IF NOT EXISTS vfs_meta (
46
+ k TEXT PRIMARY KEY,
47
+ v INTEGER NOT NULL
48
+ )`,
49
+ `CREATE TABLE IF NOT EXISTS vfs_nodes (
50
+ inode INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ type TEXT NOT NULL CHECK(type IN ('file','dir','symlink')),
52
+ mode INTEGER NOT NULL DEFAULT 493,
53
+ mtime INTEGER NOT NULL,
54
+ rev INTEGER NOT NULL DEFAULT 0,
55
+ mount_root TEXT,
56
+ stub_size INTEGER,
57
+ manifest_hash BLOB,
58
+ link_target TEXT
59
+ )`,
60
+ `CREATE TABLE IF NOT EXISTS vfs_dirents (
61
+ parent_inode INTEGER NOT NULL,
62
+ name TEXT NOT NULL,
63
+ child_inode INTEGER NOT NULL,
64
+ PRIMARY KEY (parent_inode, name)
65
+ )`,
66
+ `CREATE INDEX IF NOT EXISTS vfs_dirents_by_child ON vfs_dirents(child_inode)`,
67
+ `CREATE INDEX IF NOT EXISTS vfs_nodes_by_rev ON vfs_nodes(rev)`,
68
+ `CREATE INDEX IF NOT EXISTS vfs_nodes_by_manifest_hash
69
+ ON vfs_nodes(manifest_hash) WHERE manifest_hash IS NOT NULL`,
70
+ `CREATE TABLE IF NOT EXISTS vfs_blobs (
71
+ hash BLOB PRIMARY KEY,
72
+ size INTEGER NOT NULL,
73
+ last_seen INTEGER NOT NULL
74
+ )`,
75
+ `CREATE TABLE IF NOT EXISTS vfs_blob_bytes (
76
+ hash BLOB PRIMARY KEY REFERENCES vfs_blobs(hash) ON DELETE CASCADE,
77
+ bytes BLOB NOT NULL
78
+ )`,
79
+ `CREATE TABLE IF NOT EXISTS vfs_chunks (
80
+ inode INTEGER NOT NULL,
81
+ idx INTEGER NOT NULL,
82
+ hash BLOB NOT NULL,
83
+ size INTEGER NOT NULL,
84
+ PRIMARY KEY (inode, idx)
85
+ )`,
86
+ `CREATE INDEX IF NOT EXISTS vfs_chunks_by_hash ON vfs_chunks(hash)`
87
+ ];
88
+ //#endregion
89
+ //#region ../dofs/src/schema/migrations.ts
90
+ function v1_to_v2_add_mounts_mode(db) {
91
+ db.run(`ALTER TABLE _vfs_mounts
92
+ ADD COLUMN mode TEXT NOT NULL DEFAULT 'read-only'
93
+ CHECK(mode IN ('read-only', 'read-write'))`);
94
+ }
95
+ const MIGRATIONS = [{
96
+ from: 1,
97
+ to: 2,
98
+ migrator: v1_to_v2_add_mounts_mode
99
+ }];
100
+ function runMigrations(db, current, target) {
101
+ let version = current;
102
+ while (version < target) {
103
+ const next = MIGRATIONS.find((m) => m.from === version);
104
+ if (next === void 0) throw new Error(`dofs schema: no migration registered for v${version} -> v${target}`);
105
+ next.migrator(db);
106
+ version = next.to;
107
+ }
108
+ return version;
109
+ }
110
+ //#endregion
111
+ //#region ../dofs/src/schema/sync.ts
112
+ const SYNC_STATEMENTS = [
113
+ `CREATE TABLE IF NOT EXISTS vfs_manifests (
114
+ hash BLOB PRIMARY KEY,
115
+ size INTEGER NOT NULL,
116
+ encoded BLOB NOT NULL,
117
+ last_seen INTEGER NOT NULL DEFAULT 0
118
+ )`,
119
+ `CREATE TABLE IF NOT EXISTS vfs_changes (
120
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
121
+ rev INTEGER NOT NULL,
122
+ path TEXT NOT NULL,
123
+ op TEXT NOT NULL CHECK(op IN ('delete'))
124
+ )`,
125
+ `CREATE INDEX IF NOT EXISTS vfs_changes_by_rev ON vfs_changes(rev)`,
126
+ `CREATE INDEX IF NOT EXISTS vfs_changes_by_path ON vfs_changes(path, id DESC)`,
127
+ `CREATE TABLE IF NOT EXISTS _vfs_watermark (
128
+ k TEXT PRIMARY KEY,
129
+ v INTEGER NOT NULL
130
+ )`,
131
+ `CREATE TABLE IF NOT EXISTS _vfs_mounts (
132
+ root TEXT PRIMARY KEY,
133
+ kind TEXT NOT NULL,
134
+ indexed INTEGER NOT NULL DEFAULT 0,
135
+ mode TEXT NOT NULL DEFAULT 'read-only'
136
+ CHECK(mode IN ('read-only', 'read-write'))
137
+ )`
138
+ ];
139
+ //#endregion
140
+ //#region ../dofs/src/schema/index.ts
141
+ function initializeSchema(db, now) {
142
+ db.transactionSync(() => {
143
+ for (const statement of CORE_STATEMENTS) db.run(statement);
144
+ for (const statement of SYNC_STATEMENTS) db.run(statement);
145
+ const onDiskVersion = db.one("SELECT v FROM vfs_meta WHERE k = ?", "schema_version")?.v ?? 0;
146
+ if (onDiskVersion > 2) throw createWorkspaceError("EIO", `Unsupported workspace filesystem schema version ${onDiskVersion}`);
147
+ if (onDiskVersion > 0 && onDiskVersion < 2) runMigrations(db, onDiskVersion, 2);
148
+ db.run("INSERT OR IGNORE INTO vfs_meta (k, v) VALUES (?, ?)", "schema_version", 2);
149
+ db.run("UPDATE vfs_meta SET v = ? WHERE k = ?", 2, "schema_version");
150
+ db.run("INSERT OR IGNORE INTO vfs_meta (k, v) VALUES (?, ?)", "rev", 1);
151
+ db.run("INSERT OR IGNORE INTO _vfs_watermark (k, v) VALUES (?, ?)", "pushRev", 0);
152
+ db.run("INSERT OR IGNORE INTO _vfs_watermark (k, v) VALUES (?, ?)", "fetchRev", 0);
153
+ db.run(`INSERT OR IGNORE INTO vfs_nodes
154
+ (inode, type, mode, mtime, rev)
155
+ VALUES (?, 'dir', ?, ?, 0)`, 1, 493, now());
156
+ });
157
+ }
158
+ //#endregion
159
+ //#region ../dofs/src/fs/resolve.ts
160
+ const MAX_SYMLINK_FOLLOWS = 40;
161
+ function resolveInode(db, path, options = {}) {
162
+ const followFinal = options.followSymlinks !== false;
163
+ return resolveParts(db, canonicalizePath(path).parts, followFinal, 0);
164
+ }
165
+ function resolveParts(db, parts, followFinal, follows) {
166
+ const root = readNode(db, 1);
167
+ if (root === null) return null;
168
+ let current = root;
169
+ for (let i = 0; i < parts.length; i++) {
170
+ const isFinal = i === parts.length - 1;
171
+ if (current.type !== "dir") return null;
172
+ const child = db.one("SELECT child_inode FROM vfs_dirents WHERE parent_inode = ? AND name = ?", current.inode, parts[i]);
173
+ if (child === void 0) return null;
174
+ const next = readNode(db, child.child_inode);
175
+ if (next === null) return null;
176
+ if (next.type === "symlink" && (!isFinal || followFinal)) {
177
+ follows += 1;
178
+ if (follows > MAX_SYMLINK_FOLLOWS) throw createWorkspaceError("ELOOP", "too many symlinks resolving path");
179
+ const resolved = resolveParts(db, canonicalizePath(next.link_target ?? "").parts, true, follows);
180
+ if (resolved === null) return null;
181
+ current = {
182
+ inode: resolved.inode,
183
+ type: resolved.type,
184
+ mode: resolved.mode,
185
+ mtime: resolved.mtime,
186
+ link_target: resolved.linkTarget ?? null
187
+ };
188
+ continue;
189
+ }
190
+ current = next;
191
+ }
192
+ return {
193
+ inode: current.inode,
194
+ type: current.type,
195
+ mode: current.mode,
196
+ mtime: current.mtime,
197
+ linkTarget: current.link_target ?? void 0
198
+ };
199
+ }
200
+ function readNode(db, inode) {
201
+ return db.one("SELECT inode, type, mode, mtime, link_target FROM vfs_nodes WHERE inode = ?", inode) ?? null;
202
+ }
203
+ //#endregion
204
+ //#region ../dofs/src/fs/find.ts
205
+ function find(db, directory, pattern) {
206
+ const { path: canonical } = canonicalizePath(directory);
207
+ const node = resolveInode(db, canonical);
208
+ if (node === null) throw createWorkspaceError("ENOENT", `no such path: ${canonical}`, canonical);
209
+ if (node.type !== "dir") throw createWorkspaceError("ENOTDIR", `not a directory: ${canonical}`, canonical);
210
+ const out = [];
211
+ const regex = pattern !== void 0 ? compileGlob(pattern) : void 0;
212
+ walk(db, node.inode, canonical, out);
213
+ if (regex === void 0) return out;
214
+ const prefix = canonical === "/" ? "/" : `${canonical}/`;
215
+ return out.filter((entry) => {
216
+ if (!entry.path.startsWith(prefix)) return false;
217
+ const rel = entry.path.slice(prefix.length);
218
+ return regex.test(rel);
219
+ });
220
+ }
221
+ function walk(db, parentInode, parentPath, out) {
222
+ const children = db.all(`SELECT d.name AS name, d.child_inode AS child_inode, n.type AS type
223
+ FROM vfs_dirents d
224
+ JOIN vfs_nodes n ON n.inode = d.child_inode
225
+ WHERE d.parent_inode = ?
226
+ ORDER BY d.name`, parentInode);
227
+ for (const child of children) {
228
+ const childPath = parentPath === "/" ? `/${child.name}` : `${parentPath}/${child.name}`;
229
+ out.push({
230
+ path: childPath,
231
+ type: child.type
232
+ });
233
+ if (child.type === "dir") walk(db, child.child_inode, childPath, out);
234
+ }
235
+ }
236
+ function compileGlob(pattern) {
237
+ let re = "";
238
+ let i = 0;
239
+ while (i < pattern.length) {
240
+ const ch = pattern[i];
241
+ if (ch === "*") {
242
+ if (pattern[i + 1] === "*") if (pattern[i + 2] === "/") {
243
+ re += "(?:.*/)?";
244
+ i += 3;
245
+ } else {
246
+ re += ".*";
247
+ i += 2;
248
+ }
249
+ else {
250
+ re += "[^/]*";
251
+ i += 1;
252
+ }
253
+ continue;
254
+ }
255
+ if (REGEX_METACHARS.has(ch)) re += `\\${ch}`;
256
+ else re += ch;
257
+ i += 1;
258
+ }
259
+ return new RegExp(`^${re}$`);
260
+ }
261
+ const REGEX_METACHARS = new Set([
262
+ ".",
263
+ "+",
264
+ "?",
265
+ "^",
266
+ "$",
267
+ "(",
268
+ ")",
269
+ "[",
270
+ "]",
271
+ "{",
272
+ "}",
273
+ "|",
274
+ "\\"
275
+ ]);
276
+ //#endregion
277
+ //#region ../dofs/src/fs/readFile.ts
278
+ async function readFile(db, path, optionsOrEncoding, now = Date.now) {
279
+ const wantString = optionsOrEncoding === "utf8" || typeof optionsOrEncoding === "object" && optionsOrEncoding?.encoding === "utf8";
280
+ const node = resolveInode(db, path);
281
+ if (node === null) throw createWorkspaceError("ENOENT", `no such file: ${path}`, path);
282
+ if (node.type !== "file") throw createWorkspaceError("EISDIR", `path is a directory: ${path}`, path);
283
+ const chunks = db.all("SELECT hash, size FROM vfs_chunks WHERE inode = ? ORDER BY idx", node.inode);
284
+ if (wantString) {
285
+ const totalSize = chunks.reduce((acc, c) => acc + c.size, 0);
286
+ const out = new Uint8Array(totalSize);
287
+ let offset = 0;
288
+ const touched = now();
289
+ for (const chunk of chunks) {
290
+ const row = db.one("SELECT bytes FROM vfs_blob_bytes WHERE hash = ?", chunk.hash);
291
+ if (row === void 0) throw createWorkspaceError("EIO", `missing blob bytes for ${path}`, path);
292
+ out.set(row.bytes, offset);
293
+ offset += row.bytes.byteLength;
294
+ }
295
+ if (chunks.length > 0) touchBlobs(db, chunks, touched);
296
+ return new TextDecoder().decode(out);
297
+ }
298
+ let i = 0;
299
+ return new ReadableStream({ pull(controller) {
300
+ if (i >= chunks.length) {
301
+ controller.close();
302
+ return;
303
+ }
304
+ const chunk = chunks[i++];
305
+ const row = db.one("SELECT bytes FROM vfs_blob_bytes WHERE hash = ?", chunk.hash);
306
+ if (row === void 0) {
307
+ controller.error(createWorkspaceError("EIO", `missing blob bytes for ${path}`, path));
308
+ return;
309
+ }
310
+ db.run("UPDATE vfs_blobs SET last_seen = ? WHERE hash = ?", now(), chunk.hash);
311
+ controller.enqueue(row.bytes);
312
+ } });
313
+ }
314
+ function touchBlobs(db, chunks, at) {
315
+ const seen = /* @__PURE__ */ new Set();
316
+ for (const chunk of chunks) {
317
+ const key = bufferKey(chunk.hash);
318
+ if (seen.has(key)) continue;
319
+ seen.add(key);
320
+ db.run("UPDATE vfs_blobs SET last_seen = ? WHERE hash = ?", at, chunk.hash);
321
+ }
322
+ }
323
+ function bufferKey(bytes) {
324
+ let key = "";
325
+ for (const byte of bytes) key += byte.toString(16).padStart(2, "0");
326
+ return key;
327
+ }
328
+ //#endregion
329
+ //#region ../dofs/src/fs/grep.ts
330
+ async function grep(db, pattern, path, options = {}) {
331
+ const { path: canonical } = canonicalizePath(path);
332
+ const node = resolveInode(db, canonical);
333
+ if (node === null) throw createWorkspaceError("ENOENT", `no such path: ${canonical}`, canonical);
334
+ const filePaths = node.type === "file" ? [canonical] : find(db, canonical).filter((entry) => entry.type === "file").map((entry) => entry.path);
335
+ const matches = [];
336
+ for (const filePath of filePaths) await scanFile(db, filePath, pattern, options, matches);
337
+ return matches;
338
+ }
339
+ async function scanFile(db, path, pattern, options, out) {
340
+ const reader = (await readFile(db, path)).getReader();
341
+ const decoder = new TextDecoder("utf-8", { fatal: false });
342
+ const needle = options.ignoreCase ? pattern.toUpperCase() : pattern;
343
+ let tail = "";
344
+ let lineNo = 1;
345
+ while (true) {
346
+ const { value, done } = await reader.read();
347
+ if (done) break;
348
+ if (value === void 0) continue;
349
+ const text = tail + decoder.decode(value, { stream: true });
350
+ const newlineIdx = text.lastIndexOf("\n");
351
+ const ready = newlineIdx === -1 ? "" : text.slice(0, newlineIdx);
352
+ tail = newlineIdx === -1 ? text : text.slice(newlineIdx + 1);
353
+ if (ready.length > 0) lineNo = scanLines(ready, lineNo, needle, options.ignoreCase === true, path, out);
354
+ }
355
+ tail += decoder.decode();
356
+ if (tail.length > 0) scanLines(tail, lineNo, needle, options.ignoreCase === true, path, out);
357
+ }
358
+ function scanLines(block, startLine, needle, ignoreCase, path, out) {
359
+ let line = startLine;
360
+ let cursor = 0;
361
+ while (cursor <= block.length) {
362
+ const next = block.indexOf("\n", cursor);
363
+ const end = next === -1 ? block.length : next;
364
+ const text = block.slice(cursor, end);
365
+ if ((ignoreCase ? text.toUpperCase() : text).includes(needle)) out.push({
366
+ path,
367
+ line,
368
+ text
369
+ });
370
+ line += 1;
371
+ if (next === -1) break;
372
+ cursor = next + 1;
373
+ }
374
+ return line;
375
+ }
376
+ //#endregion
377
+ //#region ../dofs/src/fs/ls.ts
378
+ const LS_QUERY = `
379
+ WITH RECURSIVE walk(inode, path, type) AS (
380
+ SELECT inode, '', type FROM vfs_nodes WHERE inode = ?
381
+ UNION ALL
382
+ SELECT n.inode, w.path || '/' || d.name, n.type
383
+ FROM walk w
384
+ JOIN vfs_dirents d ON d.parent_inode = w.inode
385
+ JOIN vfs_nodes n ON n.inode = d.child_inode
386
+ )
387
+ SELECT path FROM walk
388
+ WHERE type = 'file'
389
+ AND (? = '/' OR path = ? OR path LIKE ? || '/%')
390
+ ORDER BY path
391
+ `;
392
+ function ls(db, prefix) {
393
+ const { path: canonical } = canonicalizePath(prefix);
394
+ return db.all(LS_QUERY, 1, canonical, canonical, canonical).map((row) => row.path);
395
+ }
396
+ //#endregion
397
+ //#region ../dofs/src/rev.ts
398
+ function incrementRev(db) {
399
+ db.run("UPDATE vfs_meta SET v = v + 1 WHERE k = 'rev'");
400
+ const next = db.scalar("SELECT v FROM vfs_meta WHERE k = ?", "rev");
401
+ if (next === void 0) throw new Error("vfs_meta.rev row missing; was initializeSchema run?");
402
+ return next;
403
+ }
404
+ //#endregion
405
+ //#region ../dofs/src/fs/mount-guard.ts
406
+ const cache = /* @__PURE__ */ new WeakMap();
407
+ function invalidateReadOnlyMountCache(db) {
408
+ cache.delete(db);
409
+ }
410
+ function loadReadOnlyRoots(db) {
411
+ const roots = db.all("SELECT root FROM _vfs_mounts WHERE mode = 'read-only'").map((r) => r.root);
412
+ cache.set(db, roots);
413
+ return roots;
414
+ }
415
+ function getReadOnlyMountRoots(db) {
416
+ const cached = cache.get(db);
417
+ if (cached !== void 0) return cached;
418
+ return loadReadOnlyRoots(db);
419
+ }
420
+ function overlapsRoot(path, root) {
421
+ return path === root || path.startsWith(`${root}/`) || root.startsWith(`${path}/`);
422
+ }
423
+ function assertNotReadOnly(db, path) {
424
+ const roots = getReadOnlyMountRoots(db);
425
+ if (roots.length === 0) return;
426
+ for (const root of roots) if (overlapsRoot(path, root)) throw createWorkspaceError("EROFS", `read-only mount at ${root}: cannot modify`, path);
427
+ }
428
+ function readOnlyRootFor(db, path) {
429
+ const roots = getReadOnlyMountRoots(db);
430
+ for (const root of roots) if (overlapsRoot(path, root)) return root;
431
+ }
432
+ //#endregion
433
+ //#region ../dofs/src/fs/mkdir.ts
434
+ function lookupChild(db, parentInode, name) {
435
+ const row = db.one("SELECT child_inode FROM vfs_dirents WHERE parent_inode = ? AND name = ?", parentInode, name);
436
+ if (row === void 0) return;
437
+ const node = db.one("SELECT inode, type FROM vfs_nodes WHERE inode = ?", row.child_inode);
438
+ if (node === void 0) return;
439
+ return node;
440
+ }
441
+ function createDir(db, parentInode, name, mode, mtime, rev) {
442
+ db.run("INSERT INTO vfs_nodes (type, mode, mtime, rev) VALUES ('dir', ?, ?, ?)", mode, mtime, rev);
443
+ const inode = db.scalar("SELECT last_insert_rowid()");
444
+ if (inode === void 0) throw createWorkspaceError("EIO", "failed to allocate inode");
445
+ db.run("INSERT INTO vfs_dirents (parent_inode, name, child_inode) VALUES (?, ?, ?)", parentInode, name, inode);
446
+ return inode;
447
+ }
448
+ function mkdir(db, path, options, now) {
449
+ const { parts, path: canonical } = canonicalizePath(path);
450
+ const recursive = options.recursive === true;
451
+ const mode = (options.mode ?? 493) & 4095;
452
+ if (parts.length === 0) throw createWorkspaceError("EEXIST", `path exists: ${canonical}`, canonical);
453
+ assertNotReadOnly(db, canonical);
454
+ db.transactionSync(() => {
455
+ const rev = incrementRev(db);
456
+ const mtime = now();
457
+ let parentInode = 1;
458
+ for (let i = 0; i < parts.length - 1; i++) {
459
+ const name = parts[i];
460
+ const existing = lookupChild(db, parentInode, name);
461
+ if (existing === void 0) {
462
+ if (!recursive) throw createWorkspaceError("ENOENT", `parent directory missing: ${canonical}`, canonical);
463
+ parentInode = createDir(db, parentInode, name, 493, mtime, rev);
464
+ continue;
465
+ }
466
+ if (existing.type !== "dir") throw createWorkspaceError("ENOTDIR", `parent path segment is not a directory: ${canonical}`, canonical);
467
+ parentInode = existing.inode;
468
+ }
469
+ const leafName = parts[parts.length - 1];
470
+ const existing = lookupChild(db, parentInode, leafName);
471
+ if (existing !== void 0) {
472
+ if (recursive && existing.type === "dir") return;
473
+ throw createWorkspaceError("EEXIST", `path exists: ${canonical}`, canonical);
474
+ }
475
+ createDir(db, parentInode, leafName, mode, mtime, rev);
476
+ });
477
+ }
478
+ //#endregion
479
+ //#region ../dofs/src/fs/readdir.ts
480
+ function readdir(db, path) {
481
+ const { path: canonical } = canonicalizePath(path);
482
+ const node = resolveInode(db, canonical);
483
+ if (node === null) throw createWorkspaceError("ENOENT", `no such path: ${canonical}`, canonical);
484
+ if (node.type !== "dir") throw createWorkspaceError("ENOTDIR", `not a directory: ${canonical}`, canonical);
485
+ return db.all(`SELECT d.name AS name, n.type AS type
486
+ FROM vfs_dirents d
487
+ JOIN vfs_nodes n ON n.inode = d.child_inode
488
+ WHERE d.parent_inode = ?
489
+ ORDER BY d.name`, node.inode).map((row) => ({
490
+ name: row.name,
491
+ parentPath: canonical,
492
+ isFile: row.type === "file",
493
+ isDirectory: row.type === "dir"
494
+ }));
495
+ }
496
+ //#endregion
497
+ //#region ../dofs/src/sync/changes.ts
498
+ function recordDelete(db, rev, path) {
499
+ db.run("INSERT INTO vfs_changes (rev, path, op) VALUES (?, ?, 'delete')", rev, path);
500
+ }
501
+ function materialiseChange(db, path) {
502
+ const canonical = canonicalizePath(path).path;
503
+ const live = resolveInode(db, canonical, { followSymlinks: false });
504
+ if (live !== null) {
505
+ const rev = db.one("SELECT rev FROM vfs_nodes WHERE inode = ?", live.inode)?.rev ?? 0;
506
+ if (live.type === "dir") return {
507
+ kind: "dir",
508
+ rev,
509
+ path: canonical,
510
+ mode: live.mode,
511
+ mtime: live.mtime
512
+ };
513
+ if (live.type === "symlink") return {
514
+ kind: "symlink",
515
+ rev,
516
+ path: canonical,
517
+ target: live.linkTarget ?? "",
518
+ mode: live.mode,
519
+ mtime: live.mtime
520
+ };
521
+ const chunks = db.all("SELECT hash, size FROM vfs_chunks WHERE inode = ? ORDER BY idx", live.inode);
522
+ let size = 0;
523
+ for (const c of chunks) size += c.size;
524
+ return {
525
+ kind: "file",
526
+ rev,
527
+ path: canonical,
528
+ mode: live.mode,
529
+ mtime: live.mtime,
530
+ size,
531
+ chunks
532
+ };
533
+ }
534
+ const tomb = db.one("SELECT rev, op FROM vfs_changes WHERE path = ? ORDER BY id DESC LIMIT 1", canonical);
535
+ if (tomb?.op === "delete") return {
536
+ kind: "delete",
537
+ rev: tomb.rev,
538
+ path: canonical
539
+ };
540
+ return null;
541
+ }
542
+ //#endregion
543
+ //#region ../dofs/src/fs/rm.ts
544
+ function* walkPostOrder(db, rootInode, rootPath) {
545
+ const stack = [{
546
+ inode: rootInode,
547
+ path: rootPath,
548
+ type: "dir",
549
+ expanded: false
550
+ }];
551
+ while (stack.length > 0) {
552
+ const top = stack[stack.length - 1];
553
+ if (top.type === "file" || top.expanded) {
554
+ stack.pop();
555
+ yield {
556
+ path: top.path,
557
+ inode: top.inode,
558
+ type: top.type
559
+ };
560
+ continue;
561
+ }
562
+ top.expanded = true;
563
+ const children = db.all(`SELECT d.name AS name, d.child_inode AS child_inode, n.type AS type
564
+ FROM vfs_dirents d
565
+ JOIN vfs_nodes n ON n.inode = d.child_inode
566
+ WHERE d.parent_inode = ?
567
+ ORDER BY d.name`, top.inode);
568
+ for (const child of children) {
569
+ const childPath = top.path === "/" ? `/${child.name}` : `${top.path}/${child.name}`;
570
+ stack.push({
571
+ inode: child.child_inode,
572
+ path: childPath,
573
+ type: child.type,
574
+ expanded: false
575
+ });
576
+ }
577
+ }
578
+ }
579
+ function rm(db, path, options) {
580
+ const { parts, path: canonical } = canonicalizePath(path);
581
+ if (parts.length === 0) throw createWorkspaceError("EPERM", `cannot remove the root directory`, canonical);
582
+ assertNotReadOnly(db, canonical);
583
+ const force = options.force === true;
584
+ const recursive = options.recursive === true;
585
+ db.transactionSync(() => {
586
+ const node = resolveInode(db, canonical);
587
+ if (node === null) {
588
+ if (force) return;
589
+ throw createWorkspaceError("ENOENT", `no such path: ${canonical}`, canonical);
590
+ }
591
+ if (node.type === "dir" && !recursive) {
592
+ if ((db.scalar("SELECT COUNT(*) FROM vfs_dirents WHERE parent_inode = ?", node.inode) ?? 0) > 0) throw createWorkspaceError("ENOTEMPTY", `directory not empty: ${canonical}`, canonical);
593
+ }
594
+ const rev = incrementRev(db);
595
+ if (node.type === "file" || !recursive) {
596
+ removeInode(db, node.inode, node.type);
597
+ recordDelete(db, rev, canonical);
598
+ return;
599
+ }
600
+ for (const entry of walkPostOrder(db, node.inode, canonical)) {
601
+ removeInode(db, entry.inode, entry.type);
602
+ recordDelete(db, rev, entry.path);
603
+ }
604
+ });
605
+ }
606
+ function removeInode(db, inode, type) {
607
+ db.run("DELETE FROM vfs_dirents WHERE child_inode = ?", inode);
608
+ if (type === "file") db.run("DELETE FROM vfs_chunks WHERE inode = ?", inode);
609
+ db.run("DELETE FROM vfs_nodes WHERE inode = ?", inode);
610
+ }
611
+ //#endregion
612
+ //#region ../dofs/src/fs/stat.ts
613
+ function stat(db, path) {
614
+ const { name } = canonicalizePath(path);
615
+ const node = resolveInode(db, path);
616
+ if (node === null) throw createWorkspaceError("ENOENT", `no such path: ${path}`, path);
617
+ const isDirectory = node.type === "dir";
618
+ const isFile = node.type === "file";
619
+ const size = isFile ? db.scalar("SELECT COALESCE(SUM(size), 0) FROM vfs_chunks WHERE inode = ?", node.inode) ?? 0 : 0;
620
+ return {
621
+ name,
622
+ mode: node.mode,
623
+ mtime: node.mtime,
624
+ size,
625
+ isFile,
626
+ isDirectory
627
+ };
628
+ }
629
+ //#endregion
630
+ //#region ../dofs/src/sync/blobs.ts
631
+ function stageBlob(db, hash, bytes, now) {
632
+ db.run("INSERT INTO vfs_blobs (hash, size, last_seen) VALUES (?, ?, ?) ON CONFLICT(hash) DO UPDATE SET last_seen = excluded.last_seen", hash, bytes.byteLength, now);
633
+ db.run("INSERT INTO vfs_blob_bytes (hash, bytes) VALUES (?, ?) ON CONFLICT(hash) DO NOTHING", hash, bytes);
634
+ }
635
+ function toHex(bytes) {
636
+ let out = "";
637
+ for (let i = 0; i < bytes.byteLength; i++) out += bytes[i].toString(16).padStart(2, "0");
638
+ return out;
639
+ }
640
+ function sha256$1(bytes) {
641
+ return new Uint8Array(createHash("sha256").update(bytes).digest());
642
+ }
643
+ function computeManifestHash(chunks) {
644
+ const encoded = {
645
+ version: 1,
646
+ chunks: chunks.map((c) => ({
647
+ hash: toHex(c.hash),
648
+ size: c.size
649
+ }))
650
+ };
651
+ return sha256$1(new TextEncoder().encode(JSON.stringify(encoded)));
652
+ }
653
+ function buildManifest(db, chunks, now) {
654
+ const hash = computeManifestHash(chunks);
655
+ const size = chunks.reduce((acc, c) => acc + c.size, 0);
656
+ const encoded = {
657
+ version: 1,
658
+ chunks: chunks.map((c) => ({
659
+ hash: toHex(c.hash),
660
+ size: c.size
661
+ }))
662
+ };
663
+ const bytes = new TextEncoder().encode(JSON.stringify(encoded));
664
+ db.run("INSERT INTO vfs_manifests (hash, size, encoded, last_seen) VALUES (?, ?, ?, ?) ON CONFLICT(hash) DO UPDATE SET last_seen = excluded.last_seen", hash, size, bytes, now);
665
+ return hash;
666
+ }
667
+ //#endregion
668
+ //#region ../dofs/src/fs/writeFile.ts
669
+ const CHUNK_SIZE = 512 * 1024;
670
+ function resolveParent(db, parts, canonical) {
671
+ let parentInode = 1;
672
+ for (let i = 0; i < parts.length - 1; i++) {
673
+ const name = parts[i];
674
+ const child = db.one("SELECT child_inode FROM vfs_dirents WHERE parent_inode = ? AND name = ?", parentInode, name);
675
+ if (child === void 0) throw createWorkspaceError("ENOENT", `parent directory missing: ${canonical}`, canonical);
676
+ const next = db.one("SELECT inode, type FROM vfs_nodes WHERE inode = ?", child.child_inode);
677
+ if (next === void 0) throw createWorkspaceError("ENOENT", `dangling dirent: ${canonical}`, canonical);
678
+ if (next.type !== "dir") throw createWorkspaceError("ENOTDIR", `parent path segment is not a directory: ${canonical}`, canonical);
679
+ parentInode = next.inode;
680
+ }
681
+ return parentInode;
682
+ }
683
+ async function materialize(content) {
684
+ if (typeof content === "string") return new TextEncoder().encode(content);
685
+ return content;
686
+ }
687
+ function sha256(bytes) {
688
+ const hash = createHash("sha256");
689
+ hash.update(bytes);
690
+ return new Uint8Array(hash.digest());
691
+ }
692
+ function chunksOf(bytes) {
693
+ const chunks = [];
694
+ for (let offset = 0; offset < bytes.byteLength; offset += CHUNK_SIZE) {
695
+ const end = Math.min(offset + CHUNK_SIZE, bytes.byteLength);
696
+ const slice = bytes.subarray(offset, end);
697
+ const hash = sha256(slice);
698
+ chunks.push({
699
+ hash,
700
+ bytes: slice,
701
+ size: slice.byteLength
702
+ });
703
+ }
704
+ return chunks;
705
+ }
706
+ async function writeFile(db, path, content, options, now) {
707
+ if (content instanceof ReadableStream) {
708
+ await writeFileStreaming(db, path, content, options, now);
709
+ return;
710
+ }
711
+ writeFileSync(db, path, await materialize(content), options, now);
712
+ }
713
+ async function writeFileStreaming(db, path, source, options, now) {
714
+ const { parts, path: canonical } = canonicalizePath(path);
715
+ if (parts.length === 0) throw createWorkspaceError("EISDIR", "cannot write to the root directory", canonical);
716
+ assertNotReadOnly(db, canonical);
717
+ const mode = (options.mode ?? 420) & 4095;
718
+ const mtime = now();
719
+ const chunkRefs = [];
720
+ let carry;
721
+ const flush = (chunk) => {
722
+ const hash = sha256(chunk);
723
+ stageBlob(db, hash, chunk, mtime);
724
+ chunkRefs.push({
725
+ hash,
726
+ size: chunk.byteLength
727
+ });
728
+ };
729
+ const reader = source.getReader();
730
+ try {
731
+ while (true) {
732
+ const { value, done } = await reader.read();
733
+ if (done) break;
734
+ if (value === void 0 || value.byteLength === 0) continue;
735
+ let input = value;
736
+ if (carry !== void 0) {
737
+ const merged = new Uint8Array(carry.byteLength + input.byteLength);
738
+ merged.set(carry, 0);
739
+ merged.set(input, carry.byteLength);
740
+ input = merged;
741
+ carry = void 0;
742
+ }
743
+ let offset = 0;
744
+ while (input.byteLength - offset >= CHUNK_SIZE) {
745
+ flush(input.slice(offset, offset + CHUNK_SIZE));
746
+ offset += CHUNK_SIZE;
747
+ }
748
+ if (offset < input.byteLength) carry = input.slice(offset);
749
+ }
750
+ } finally {
751
+ reader.releaseLock();
752
+ }
753
+ if (carry !== void 0 && carry.byteLength > 0) flush(carry);
754
+ db.transactionSync(() => {
755
+ const parentInode = resolveParent(db, parts, canonical);
756
+ const leafName = parts[parts.length - 1];
757
+ const existing = db.one("SELECT child_inode FROM vfs_dirents WHERE parent_inode = ? AND name = ?", parentInode, leafName);
758
+ let inode;
759
+ if (existing !== void 0) {
760
+ if (db.one("SELECT type FROM vfs_nodes WHERE inode = ?", existing.child_inode)?.type === "dir") throw createWorkspaceError("EISDIR", `path is a directory: ${canonical}`, canonical);
761
+ inode = existing.child_inode;
762
+ db.run("DELETE FROM vfs_chunks WHERE inode = ?", inode);
763
+ } else {
764
+ db.run("INSERT INTO vfs_nodes (type, mode, mtime, rev) VALUES ('file', ?, ?, 0)", mode, mtime);
765
+ const allocated = db.scalar("SELECT last_insert_rowid()");
766
+ if (allocated === void 0) throw createWorkspaceError("EIO", "failed to allocate inode");
767
+ inode = allocated;
768
+ db.run("INSERT INTO vfs_dirents (parent_inode, name, child_inode) VALUES (?, ?, ?)", parentInode, leafName, inode);
769
+ }
770
+ for (let idx = 0; idx < chunkRefs.length; idx++) {
771
+ const ref = chunkRefs[idx];
772
+ db.run("INSERT INTO vfs_chunks (inode, idx, hash, size) VALUES (?, ?, ?, ?)", inode, idx, ref.hash, ref.size);
773
+ }
774
+ const manifestHash = buildManifest(db, chunkRefs, mtime);
775
+ const rev = incrementRev(db);
776
+ db.run("UPDATE vfs_nodes SET mode = ?, mtime = ?, rev = ?, manifest_hash = ? WHERE inode = ?", mode, mtime, rev, manifestHash, inode);
777
+ });
778
+ }
779
+ function writeFileSync(db, path, bytes, options, now) {
780
+ const { parts, path: canonical } = canonicalizePath(path);
781
+ if (parts.length === 0) throw createWorkspaceError("EISDIR", "cannot write to the root directory", canonical);
782
+ assertNotReadOnly(db, canonical);
783
+ const mode = (options.mode ?? 420) & 4095;
784
+ const chunks = chunksOf(bytes);
785
+ const mtime = now();
786
+ db.transactionSync(() => {
787
+ const parentInode = resolveParent(db, parts, canonical);
788
+ const leafName = parts[parts.length - 1];
789
+ const existing = db.one("SELECT child_inode FROM vfs_dirents WHERE parent_inode = ? AND name = ?", parentInode, leafName);
790
+ let inode;
791
+ if (existing !== void 0) {
792
+ if (db.one("SELECT type FROM vfs_nodes WHERE inode = ?", existing.child_inode)?.type === "dir") throw createWorkspaceError("EISDIR", `path is a directory: ${canonical}`, canonical);
793
+ inode = existing.child_inode;
794
+ db.run("DELETE FROM vfs_chunks WHERE inode = ?", inode);
795
+ } else {
796
+ db.run("INSERT INTO vfs_nodes (type, mode, mtime, rev) VALUES ('file', ?, ?, 0)", mode, mtime);
797
+ const allocated = db.scalar("SELECT last_insert_rowid()");
798
+ if (allocated === void 0) throw createWorkspaceError("EIO", "failed to allocate inode");
799
+ inode = allocated;
800
+ db.run("INSERT INTO vfs_dirents (parent_inode, name, child_inode) VALUES (?, ?, ?)", parentInode, leafName, inode);
801
+ }
802
+ for (let idx = 0; idx < chunks.length; idx++) {
803
+ const chunk = chunks[idx];
804
+ db.run("INSERT INTO vfs_blobs (hash, size, last_seen) VALUES (?, ?, ?) ON CONFLICT(hash) DO UPDATE SET last_seen = excluded.last_seen", chunk.hash, chunk.size, mtime);
805
+ db.run("INSERT INTO vfs_blob_bytes (hash, bytes) VALUES (?, ?) ON CONFLICT(hash) DO NOTHING", chunk.hash, chunk.bytes);
806
+ db.run("INSERT INTO vfs_chunks (inode, idx, hash, size) VALUES (?, ?, ?, ?)", inode, idx, chunk.hash, chunk.size);
807
+ }
808
+ const manifestHash = buildManifest(db, chunks, mtime);
809
+ const rev = incrementRev(db);
810
+ db.run("UPDATE vfs_nodes SET mode = ?, mtime = ?, rev = ?, manifest_hash = ? WHERE inode = ?", mode, mtime, rev, manifestHash, inode);
811
+ });
812
+ }
813
+ //#endregion
814
+ //#region ../dofs/src/fs/filesystem.ts
815
+ var WorkspaceFilesystem = class {
816
+ db;
817
+ now;
818
+ constructor(db, options = {}) {
819
+ this.db = db;
820
+ this.now = options.now ?? Date.now;
821
+ }
822
+ readFile(path, optionsOrEncoding) {
823
+ return readFile(this.db, path, optionsOrEncoding, this.now);
824
+ }
825
+ async stat(path) {
826
+ return stat(this.db, path);
827
+ }
828
+ async readdir(path) {
829
+ return readdir(this.db, path);
830
+ }
831
+ async find(directory, pattern) {
832
+ return find(this.db, directory, pattern);
833
+ }
834
+ async ls(prefix) {
835
+ return ls(this.db, prefix);
836
+ }
837
+ grep(pattern, path, options = {}) {
838
+ return grep(this.db, pattern, path, options);
839
+ }
840
+ writeFile(path, content, options = {}) {
841
+ return writeFile(this.db, path, content, options, this.now);
842
+ }
843
+ async mkdir(path, options = {}) {
844
+ mkdir(this.db, path, options, this.now);
845
+ }
846
+ async rm(path, options = {}) {
847
+ rm(this.db, path, options);
848
+ }
849
+ };
850
+ //#endregion
851
+ //#region ../dofs/src/fs/readlink.ts
852
+ function readlink(db, path) {
853
+ const node = resolveInode(db, path, { followSymlinks: false });
854
+ if (node === null) throw createWorkspaceError("ENOENT", `no such path: ${path}`, path);
855
+ if (node.type !== "symlink" || node.linkTarget === void 0) throw createWorkspaceError("EINVAL", `not a symlink: ${path}`, path);
856
+ return node.linkTarget;
857
+ }
858
+ //#endregion
859
+ //#region ../dofs/src/fs/symlink.ts
860
+ function symlink(db, target, path, now) {
861
+ const { parts, path: canonical } = canonicalizePath(path);
862
+ if (parts.length === 0) throw createWorkspaceError("EEXIST", "cannot symlink onto root", canonical);
863
+ db.transactionSync(() => {
864
+ let parentInode = 1;
865
+ for (let i = 0; i < parts.length - 1; i++) {
866
+ const child = db.one("SELECT child_inode FROM vfs_dirents WHERE parent_inode = ? AND name = ?", parentInode, parts[i]);
867
+ if (child === void 0) throw createWorkspaceError("ENOENT", `parent directory missing: ${canonical}`, canonical);
868
+ const next = db.one("SELECT inode, type FROM vfs_nodes WHERE inode = ?", child.child_inode);
869
+ if (next === void 0 || next.type !== "dir") throw createWorkspaceError("ENOTDIR", `parent path segment is not a directory: ${canonical}`, canonical);
870
+ parentInode = next.inode;
871
+ }
872
+ const leafName = parts[parts.length - 1];
873
+ if (db.one("SELECT child_inode FROM vfs_dirents WHERE parent_inode = ? AND name = ?", parentInode, leafName) !== void 0) throw createWorkspaceError("EEXIST", `path exists: ${canonical}`, canonical);
874
+ const rev = incrementRev(db);
875
+ const mtime = now();
876
+ db.run("INSERT INTO vfs_nodes (type, mode, mtime, rev, link_target) VALUES ('symlink', ?, ?, ?, ?)", 511, mtime, rev, target);
877
+ const inode = db.scalar("SELECT last_insert_rowid()");
878
+ if (inode === void 0) throw createWorkspaceError("EIO", "failed to allocate inode");
879
+ db.run("INSERT INTO vfs_dirents (parent_inode, name, child_inode) VALUES (?, ?, ?)", parentInode, leafName, inode);
880
+ });
881
+ }
882
+ //#endregion
883
+ //#region ../dofs/src/sync/ignore.ts
884
+ function isIgnored(path, patterns) {
885
+ if (patterns.length === 0) return false;
886
+ const segments = path.split("/").filter((s) => s.length > 0);
887
+ for (const segment of segments) for (const p of patterns) if (segment === p) return true;
888
+ return false;
889
+ }
890
+ //#endregion
891
+ //#region ../dofs/src/sync/coalesce.ts
892
+ function pathOf(db, inode) {
893
+ if (inode === 1) return "/";
894
+ const segments = [];
895
+ let current = inode;
896
+ for (let i = 0; i < 1e6; i++) {
897
+ const row = db.one("SELECT parent_inode, name FROM vfs_dirents WHERE child_inode = ?", current);
898
+ if (row === void 0) return null;
899
+ segments.push(row.name);
900
+ if (row.parent_inode === 1) {
901
+ segments.reverse();
902
+ return `/${segments.join("/")}`;
903
+ }
904
+ current = row.parent_inode;
905
+ }
906
+ return null;
907
+ }
908
+ async function* coalesceChanges(db, sinceRev, options = {}) {
909
+ const ignore = options.ignore ?? [];
910
+ const candidates = /* @__PURE__ */ new Map();
911
+ const touched = db.all("SELECT inode, rev FROM vfs_nodes WHERE rev > ? ORDER BY rev", sinceRev);
912
+ for (const { inode, rev } of touched) {
913
+ const path = pathOf(db, inode);
914
+ if (path === null) continue;
915
+ if (isIgnored(path, ignore)) continue;
916
+ const prior = candidates.get(path);
917
+ if (prior === void 0 || rev > prior.rev) candidates.set(path, {
918
+ path,
919
+ rev
920
+ });
921
+ }
922
+ const tombs = db.all("SELECT path, MAX(rev) AS rev FROM vfs_changes WHERE rev > ? AND op = 'delete' GROUP BY path", sinceRev);
923
+ for (const { path, rev } of tombs) {
924
+ if (isIgnored(path, ignore)) continue;
925
+ const prior = candidates.get(path);
926
+ if (prior === void 0 || rev > prior.rev) candidates.set(path, {
927
+ path,
928
+ rev
929
+ });
930
+ }
931
+ const ordered = Array.from(candidates.values()).sort((a, b) => {
932
+ if (a.rev !== b.rev) return a.rev - b.rev;
933
+ return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
934
+ });
935
+ for (const { path } of ordered) {
936
+ const entry = materialiseChange(db, path);
937
+ if (entry !== null) yield entry;
938
+ }
939
+ }
940
+ //#endregion
941
+ //#region ../dofs/src/sync/watermarks.ts
942
+ function readWatermark(db, key) {
943
+ return db.scalar("SELECT v FROM _vfs_watermark WHERE k = ?", key) ?? 0;
944
+ }
945
+ function writeWatermark(db, key, value) {
946
+ db.run("INSERT INTO _vfs_watermark (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v", key, value);
947
+ }
948
+ function currentRev(db) {
949
+ return db.scalar("SELECT v FROM vfs_meta WHERE k = 'rev'") ?? 0;
950
+ }
951
+ //#endregion
952
+ //#region ../dofs/src/fs/watch.ts
953
+ function createWatcher(db, path, options, defaultInterval) {
954
+ const { path: canonical } = canonicalizePath(path);
955
+ const prefix = canonical === "/" ? "/" : `${canonical}/`;
956
+ const recursive = options.recursive === true;
957
+ const interval = options.interval ?? defaultInterval;
958
+ const emitter = new EventEmitter();
959
+ let cursor = currentRev(db);
960
+ let closed = false;
961
+ const tick = async () => {
962
+ if (closed) return;
963
+ try {
964
+ const seen = /* @__PURE__ */ new Set();
965
+ for await (const entry of coalesceChanges(db, cursor)) {
966
+ if (!isInScope(entry.path, canonical, prefix, recursive)) continue;
967
+ if (seen.has(entry.path)) continue;
968
+ seen.add(entry.path);
969
+ const filename = relativeName(entry.path, canonical);
970
+ const eventType = entry.kind === "delete" ? "rename" : "change";
971
+ emitter.emit("change", eventType, filename);
972
+ }
973
+ cursor = currentRev(db);
974
+ } catch (error) {
975
+ emitter.emit("error", error);
976
+ }
977
+ };
978
+ const handle = setInterval(() => void tick(), interval);
979
+ handle.unref?.();
980
+ emitter.close = () => {
981
+ if (closed) return;
982
+ closed = true;
983
+ clearInterval(handle);
984
+ emitter.emit("close");
985
+ };
986
+ if (options.signal !== void 0) if (options.signal.aborted) emitter.close();
987
+ else options.signal.addEventListener("abort", () => emitter.close(), { once: true });
988
+ return emitter;
989
+ }
990
+ function isInScope(entryPath, watchedPath, prefix, recursive) {
991
+ if (entryPath === watchedPath) return true;
992
+ if (!entryPath.startsWith(prefix)) return false;
993
+ if (recursive) return true;
994
+ return !entryPath.slice(prefix.length).includes("/");
995
+ }
996
+ function relativeName(entryPath, watchedPath) {
997
+ if (entryPath === watchedPath) return "";
998
+ const prefix = watchedPath === "/" ? "/" : `${watchedPath}/`;
999
+ return entryPath.startsWith(prefix) ? entryPath.slice(prefix.length) : entryPath;
1000
+ }
1001
+ function createWatchAsyncIterable(watcher) {
1002
+ const pending = [];
1003
+ const waiters = [];
1004
+ let done = false;
1005
+ watcher.on("change", (eventType, filename) => {
1006
+ const event = {
1007
+ eventType,
1008
+ filename
1009
+ };
1010
+ const next = waiters.shift();
1011
+ if (next) next({
1012
+ value: event,
1013
+ done: false
1014
+ });
1015
+ else pending.push(event);
1016
+ });
1017
+ watcher.on("close", () => {
1018
+ done = true;
1019
+ while (waiters.length > 0) {
1020
+ const next = waiters.shift();
1021
+ if (next) next({
1022
+ value: void 0,
1023
+ done: true
1024
+ });
1025
+ }
1026
+ });
1027
+ return {
1028
+ [Symbol.asyncIterator]() {
1029
+ return this;
1030
+ },
1031
+ next() {
1032
+ const buffered = pending.shift();
1033
+ if (buffered) return Promise.resolve({
1034
+ value: buffered,
1035
+ done: false
1036
+ });
1037
+ if (done) return Promise.resolve({
1038
+ value: void 0,
1039
+ done: true
1040
+ });
1041
+ return new Promise((resolve) => waiters.push(resolve));
1042
+ },
1043
+ async return() {
1044
+ watcher.close();
1045
+ return {
1046
+ value: void 0,
1047
+ done: true
1048
+ };
1049
+ }
1050
+ };
1051
+ }
1052
+ //#endregion
1053
+ //#region ../dofs/src/provider.ts
1054
+ var SQLiteWorkspaceProvider = class {
1055
+ db;
1056
+ now;
1057
+ readonly = false;
1058
+ supportsSymlinks = true;
1059
+ supportsWatch = true;
1060
+ #fds = /* @__PURE__ */ new Map();
1061
+ #nextFd = 3;
1062
+ watchIntervalMs;
1063
+ constructor(db, options = {}) {
1064
+ this.db = db;
1065
+ this.now = options.now ?? Date.now;
1066
+ this.watchIntervalMs = options.watchIntervalMs ?? 100;
1067
+ }
1068
+ open(path, flags, mode) {
1069
+ return Promise.resolve(this.openSync(path, flags, mode));
1070
+ }
1071
+ openSync(path, flags = "r", _mode) {
1072
+ const { read, write, truncate, append, create, exclusive } = parseFlags(flags);
1073
+ const existing = resolveInode(this.db, path);
1074
+ if (existing === null) {
1075
+ if (!create) throw createWorkspaceError("ENOENT", `no such file: ${path}`, path);
1076
+ writeFileSync(this.db, path, new Uint8Array(), {}, this.now);
1077
+ } else {
1078
+ if (existing.type !== "file") throw createWorkspaceError("EISDIR", `path is a directory: ${path}`, path);
1079
+ if (exclusive) throw createWorkspaceError("EEXIST", `path exists: ${path}`, path);
1080
+ if (truncate) writeFileSync(this.db, path, new Uint8Array(), {}, this.now);
1081
+ }
1082
+ const stat$1 = stat(this.db, path);
1083
+ const fd = this.#nextFd++;
1084
+ this.#fds.set(fd, {
1085
+ path,
1086
+ position: append ? stat$1.size : 0,
1087
+ readable: read,
1088
+ writable: write,
1089
+ append
1090
+ });
1091
+ return fd;
1092
+ }
1093
+ stat(path, options) {
1094
+ return Promise.resolve(this.statSync(path, options));
1095
+ }
1096
+ statSync(path, _options) {
1097
+ const s = stat(this.db, path);
1098
+ const ino = resolveInode(this.db, path)?.inode ?? 0;
1099
+ return wrapStats({
1100
+ mode: s.mode,
1101
+ size: s.size,
1102
+ mtimeMs: s.mtime,
1103
+ ino,
1104
+ isFile: s.isFile,
1105
+ isDirectory: s.isDirectory,
1106
+ isSymbolicLink: false
1107
+ });
1108
+ }
1109
+ lstat(path, options) {
1110
+ return Promise.resolve(this.lstatSync(path, options));
1111
+ }
1112
+ lstatSync(path, _options) {
1113
+ const node = resolveInode(this.db, path, { followSymlinks: false });
1114
+ if (node === null) throw createWorkspaceError("ENOENT", `no such path: ${path}`, path);
1115
+ const isSymlink = node.type === "symlink";
1116
+ const size = isSymlink ? (node.linkTarget ?? "").length : node.type === "file" ? this.db.scalar("SELECT COALESCE(SUM(size), 0) FROM vfs_chunks WHERE inode = ?", node.inode) ?? 0 : 0;
1117
+ return wrapStats({
1118
+ mode: node.mode,
1119
+ size,
1120
+ mtimeMs: node.mtime,
1121
+ ino: node.inode,
1122
+ isFile: node.type === "file",
1123
+ isDirectory: node.type === "dir",
1124
+ isSymbolicLink: isSymlink
1125
+ });
1126
+ }
1127
+ readdir(path, options) {
1128
+ return Promise.resolve(this.readdirSync(path, options));
1129
+ }
1130
+ readdirSync(path, options) {
1131
+ const entries = readdir(this.db, path);
1132
+ if (options?.withFileTypes === true) return entries.map((entry) => wrapDirent(entry));
1133
+ return entries.map((entry) => entry.name);
1134
+ }
1135
+ mkdir(path, options) {
1136
+ return Promise.resolve(this.mkdirSync(path, options));
1137
+ }
1138
+ mkdirSync(path, options) {
1139
+ mkdir(this.db, path, options ?? {}, this.now);
1140
+ }
1141
+ rmdir(path) {
1142
+ this.rmdirSync(path);
1143
+ return Promise.resolve();
1144
+ }
1145
+ rmdirSync(path) {
1146
+ rm(this.db, path, {});
1147
+ }
1148
+ unlink(path) {
1149
+ this.unlinkSync(path);
1150
+ return Promise.resolve();
1151
+ }
1152
+ unlinkSync(path) {
1153
+ rm(this.db, path, {});
1154
+ }
1155
+ rename(oldPath, newPath) {
1156
+ this.renameSync(oldPath, newPath);
1157
+ return Promise.resolve();
1158
+ }
1159
+ renameSync(oldPath, newPath) {
1160
+ const node = resolveInode(this.db, oldPath);
1161
+ if (node === null) throw createWorkspaceError("ENOENT", `no such path: ${oldPath}`, oldPath);
1162
+ const { parts, path: newCanonical } = canonicalizePath(newPath);
1163
+ if (parts.length === 0) throw createWorkspaceError("EINVAL", "cannot rename onto root", newCanonical);
1164
+ const newName = parts[parts.length - 1];
1165
+ const newParentPath = parts.length === 1 ? "/" : `/${parts.slice(0, -1).join("/")}`;
1166
+ const newParent = resolveInode(this.db, newParentPath);
1167
+ if (newParent === null || newParent.type !== "dir") throw createWorkspaceError("ENOENT", `parent directory missing: ${newCanonical}`, newCanonical);
1168
+ this.db.transactionSync(() => {
1169
+ const existing = this.db.one(`SELECT d.child_inode AS child_inode, n.type AS type
1170
+ FROM vfs_dirents d JOIN vfs_nodes n ON n.inode = d.child_inode
1171
+ WHERE d.parent_inode = ? AND d.name = ?`, newParent.inode, newName);
1172
+ if (existing !== void 0 && existing.child_inode !== node.inode) {
1173
+ if (existing.type === "dir") {
1174
+ if ((this.db.scalar("SELECT COUNT(*) FROM vfs_dirents WHERE parent_inode = ?", existing.child_inode) ?? 0) > 0) throw createWorkspaceError("ENOTEMPTY", `not empty: ${newCanonical}`, newCanonical);
1175
+ }
1176
+ this.db.run("DELETE FROM vfs_dirents WHERE child_inode = ?", existing.child_inode);
1177
+ this.db.run("DELETE FROM vfs_chunks WHERE inode = ?", existing.child_inode);
1178
+ this.db.run("DELETE FROM vfs_nodes WHERE inode = ?", existing.child_inode);
1179
+ }
1180
+ this.db.run("DELETE FROM vfs_dirents WHERE child_inode = ?", node.inode);
1181
+ this.db.run("INSERT INTO vfs_dirents (parent_inode, name, child_inode) VALUES (?, ?, ?)", newParent.inode, newName, node.inode);
1182
+ });
1183
+ }
1184
+ readFile(path, options) {
1185
+ return Promise.resolve(this.readFileSync(path, options));
1186
+ }
1187
+ readFileSync(path, options) {
1188
+ const node = resolveInode(this.db, path);
1189
+ if (node === null) throw createWorkspaceError("ENOENT", `no such file: ${path}`, path);
1190
+ if (node.type !== "file") throw createWorkspaceError("EISDIR", `path is a directory: ${path}`, path);
1191
+ const chunks = this.db.all("SELECT hash, size FROM vfs_chunks WHERE inode = ? ORDER BY idx", node.inode);
1192
+ let total = 0;
1193
+ for (const c of chunks) total += c.size;
1194
+ const out = Buffer.alloc(total);
1195
+ let offset = 0;
1196
+ for (const chunk of chunks) {
1197
+ const row = this.db.one("SELECT bytes FROM vfs_blob_bytes WHERE hash = ?", chunk.hash);
1198
+ if (row === void 0) throw createWorkspaceError("EIO", `missing blob bytes for ${path}`, path);
1199
+ out.set(row.bytes, offset);
1200
+ offset += row.bytes.byteLength;
1201
+ }
1202
+ const encoding = typeof options === "string" ? options : options?.encoding;
1203
+ return encoding ? out.toString(encoding) : out;
1204
+ }
1205
+ writeFile(path, data, options) {
1206
+ this.writeFileSync(path, data, options);
1207
+ return Promise.resolve();
1208
+ }
1209
+ writeFileSync(path, data, options) {
1210
+ const mode = typeof options === "string" ? void 0 : options?.mode;
1211
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1212
+ writeFileSync(this.db, path, bytes, { mode }, this.now);
1213
+ }
1214
+ appendFile(_path, _data, _options) {
1215
+ return Promise.reject(notImplemented("appendFile"));
1216
+ }
1217
+ appendFileSync(_path, _data, _options) {
1218
+ throw notImplemented("appendFileSync");
1219
+ }
1220
+ exists(path) {
1221
+ return Promise.resolve(this.existsSync(path));
1222
+ }
1223
+ existsSync(path) {
1224
+ try {
1225
+ return resolveInode(this.db, path) !== null;
1226
+ } catch {
1227
+ return false;
1228
+ }
1229
+ }
1230
+ copyFile(_src, _dest, _mode) {
1231
+ return Promise.reject(notImplemented("copyFile"));
1232
+ }
1233
+ copyFileSync(_src, _dest, _mode) {
1234
+ throw notImplemented("copyFileSync");
1235
+ }
1236
+ internalModuleStat(_path) {
1237
+ throw notImplemented("internalModuleStat");
1238
+ }
1239
+ realpath(path, _options) {
1240
+ return Promise.resolve(this.realpathSync(path));
1241
+ }
1242
+ realpathSync(path, _options) {
1243
+ const { path: canonical } = canonicalizePath(path);
1244
+ if (resolveInode(this.db, canonical) === null) throw createWorkspaceError("ENOENT", `no such path: ${canonical}`, canonical);
1245
+ return canonical;
1246
+ }
1247
+ access(path, _mode) {
1248
+ this.accessSync(path);
1249
+ return Promise.resolve();
1250
+ }
1251
+ accessSync(path, _mode) {
1252
+ if (resolveInode(this.db, path) === null) throw createWorkspaceError("ENOENT", `no such path: ${path}`, path);
1253
+ }
1254
+ closeSync(fd) {
1255
+ if (!this.#fds.delete(fd)) throw createWorkspaceError("EBADF", `unknown fd ${fd}`);
1256
+ }
1257
+ readSync(fd, buffer, offset, length, position) {
1258
+ const state = this.#fdOrThrow(fd);
1259
+ if (!state.readable) throw createWorkspaceError("EBADF", `fd ${fd} is not readable`);
1260
+ const startAt = position ?? state.position;
1261
+ const bytes = readFileBytesSync(this.db, state.path);
1262
+ if (startAt >= bytes.byteLength) return 0;
1263
+ const end = Math.min(startAt + length, bytes.byteLength);
1264
+ const n = end - startAt;
1265
+ (buffer instanceof Buffer ? buffer : Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength)).set(bytes.subarray(startAt, end), offset);
1266
+ if (position === null || position === void 0) state.position += n;
1267
+ return n;
1268
+ }
1269
+ writeSync(fd, buffer, offset = 0, length = buffer.byteLength - offset, position = null) {
1270
+ const state = this.#fdOrThrow(fd);
1271
+ if (!state.writable) throw createWorkspaceError("EBADF", `fd ${fd} is not writable`);
1272
+ const existing = readFileBytesSync(this.db, state.path);
1273
+ const startAt = state.append ? existing.byteLength : position ?? state.position;
1274
+ const next = spliceBytes(existing, startAt, buffer, offset, length);
1275
+ writeFileSync(this.db, state.path, next, {}, this.now);
1276
+ if (position === null || position === void 0) state.position = startAt + length;
1277
+ return length;
1278
+ }
1279
+ fstatSync(fd, _options) {
1280
+ const state = this.#fdOrThrow(fd);
1281
+ return this.statSync(state.path);
1282
+ }
1283
+ truncateSync(path, len) {
1284
+ const node = resolveInode(this.db, path);
1285
+ if (node === null) throw createWorkspaceError("ENOENT", `no such path: ${path}`, path);
1286
+ if (node.type !== "file") throw createWorkspaceError("EISDIR", `path is a directory: ${path}`, path);
1287
+ const existing = readFileBytesSync(this.db, path);
1288
+ if (existing.byteLength === len) return;
1289
+ let next;
1290
+ if (len < existing.byteLength) next = existing.subarray(0, len);
1291
+ else {
1292
+ next = new Uint8Array(len);
1293
+ next.set(existing, 0);
1294
+ }
1295
+ writeFileSync(this.db, path, next, {}, this.now);
1296
+ }
1297
+ ftruncateSync(fd, len) {
1298
+ const state = this.#fdOrThrow(fd);
1299
+ this.truncateSync(state.path, len);
1300
+ }
1301
+ #fdOrThrow(fd) {
1302
+ const state = this.#fds.get(fd);
1303
+ if (state === void 0) throw createWorkspaceError("EBADF", `unknown fd ${fd}`);
1304
+ return state;
1305
+ }
1306
+ readlink(path, _options) {
1307
+ return Promise.resolve(this.readlinkSync(path));
1308
+ }
1309
+ readlinkSync(path, _options) {
1310
+ return readlink(this.db, path);
1311
+ }
1312
+ symlink(target, path, _type) {
1313
+ this.symlinkSync(target, path);
1314
+ return Promise.resolve();
1315
+ }
1316
+ symlinkSync(target, path, _type) {
1317
+ symlink(this.db, target, path, this.now);
1318
+ }
1319
+ watch(path, options = {}) {
1320
+ return createWatcher(this.db, path, options, this.watchIntervalMs);
1321
+ }
1322
+ watchAsync(path, options = {}) {
1323
+ return createWatchAsyncIterable(this.watch(path, options));
1324
+ }
1325
+ watchFile(_path, _options, _listener) {
1326
+ throw notImplemented("watchFile");
1327
+ }
1328
+ unwatchFile(_path, _listener) {
1329
+ throw notImplemented("unwatchFile");
1330
+ }
1331
+ };
1332
+ function notImplemented(method) {
1333
+ return createWorkspaceError("ENOSYS", `SQLiteWorkspaceProvider.${method} is not implemented yet`);
1334
+ }
1335
+ const S_IFREG = 32768;
1336
+ const S_IFDIR = 16384;
1337
+ const S_IFLNK = 40960;
1338
+ function fileTypeBits(input) {
1339
+ if (input.isDirectory) return S_IFDIR;
1340
+ if (input.isSymbolicLink) return S_IFLNK;
1341
+ if (input.isFile) return S_IFREG;
1342
+ return 0;
1343
+ }
1344
+ function wrapStats(input) {
1345
+ const mtime = new Date(input.mtimeMs);
1346
+ return {
1347
+ dev: 0,
1348
+ mode: input.mode & 4095 | fileTypeBits(input),
1349
+ nlink: 1,
1350
+ uid: 0,
1351
+ gid: 0,
1352
+ rdev: 0,
1353
+ blksize: 4096,
1354
+ ino: input.ino,
1355
+ size: input.size,
1356
+ blocks: Math.ceil(input.size / 512),
1357
+ atimeMs: input.mtimeMs,
1358
+ mtimeMs: input.mtimeMs,
1359
+ ctimeMs: input.mtimeMs,
1360
+ birthtimeMs: input.mtimeMs,
1361
+ atime: mtime,
1362
+ mtime,
1363
+ ctime: mtime,
1364
+ birthtime: mtime,
1365
+ isFile: () => input.isFile,
1366
+ isDirectory: () => input.isDirectory,
1367
+ isSymbolicLink: () => input.isSymbolicLink,
1368
+ isBlockDevice: () => false,
1369
+ isCharacterDevice: () => false,
1370
+ isFIFO: () => false,
1371
+ isSocket: () => false
1372
+ };
1373
+ }
1374
+ function wrapDirent(input) {
1375
+ const fullPath = input.parentPath === "/" ? `/${input.name}` : `${input.parentPath}/${input.name}`;
1376
+ return {
1377
+ name: input.name,
1378
+ parentPath: input.parentPath,
1379
+ path: fullPath,
1380
+ isFile: () => input.isFile,
1381
+ isDirectory: () => input.isDirectory,
1382
+ isSymbolicLink: () => false,
1383
+ isBlockDevice: () => false,
1384
+ isCharacterDevice: () => false,
1385
+ isFIFO: () => false,
1386
+ isSocket: () => false
1387
+ };
1388
+ }
1389
+ function parseFlags(flags) {
1390
+ switch (flags) {
1391
+ case "r": return {
1392
+ read: true,
1393
+ write: false,
1394
+ create: false,
1395
+ truncate: false,
1396
+ append: false,
1397
+ exclusive: false
1398
+ };
1399
+ case "r+": return {
1400
+ read: true,
1401
+ write: true,
1402
+ create: false,
1403
+ truncate: false,
1404
+ append: false,
1405
+ exclusive: false
1406
+ };
1407
+ case "w": return {
1408
+ read: false,
1409
+ write: true,
1410
+ create: true,
1411
+ truncate: true,
1412
+ append: false,
1413
+ exclusive: false
1414
+ };
1415
+ case "w+": return {
1416
+ read: true,
1417
+ write: true,
1418
+ create: true,
1419
+ truncate: true,
1420
+ append: false,
1421
+ exclusive: false
1422
+ };
1423
+ case "wx": return {
1424
+ read: false,
1425
+ write: true,
1426
+ create: true,
1427
+ truncate: false,
1428
+ append: false,
1429
+ exclusive: true
1430
+ };
1431
+ case "wx+": return {
1432
+ read: true,
1433
+ write: true,
1434
+ create: true,
1435
+ truncate: false,
1436
+ append: false,
1437
+ exclusive: true
1438
+ };
1439
+ case "a": return {
1440
+ read: false,
1441
+ write: true,
1442
+ create: true,
1443
+ truncate: false,
1444
+ append: true,
1445
+ exclusive: false
1446
+ };
1447
+ case "a+": return {
1448
+ read: true,
1449
+ write: true,
1450
+ create: true,
1451
+ truncate: false,
1452
+ append: true,
1453
+ exclusive: false
1454
+ };
1455
+ case "ax": return {
1456
+ read: false,
1457
+ write: true,
1458
+ create: true,
1459
+ truncate: false,
1460
+ append: true,
1461
+ exclusive: true
1462
+ };
1463
+ case "ax+": return {
1464
+ read: true,
1465
+ write: true,
1466
+ create: true,
1467
+ truncate: false,
1468
+ append: true,
1469
+ exclusive: true
1470
+ };
1471
+ default: throw createWorkspaceError("EINVAL", `unsupported fs flag: ${flags}`);
1472
+ }
1473
+ }
1474
+ function readFileBytesSync(db, path) {
1475
+ const node = resolveInode(db, path);
1476
+ if (node === null) throw createWorkspaceError("ENOENT", `no such file: ${path}`, path);
1477
+ if (node.type !== "file") throw createWorkspaceError("EISDIR", `path is a directory: ${path}`, path);
1478
+ const chunks = db.all("SELECT hash, size FROM vfs_chunks WHERE inode = ? ORDER BY idx", node.inode);
1479
+ let total = 0;
1480
+ for (const c of chunks) total += c.size;
1481
+ const out = new Uint8Array(total);
1482
+ let pos = 0;
1483
+ for (const chunk of chunks) {
1484
+ const row = db.one("SELECT bytes FROM vfs_blob_bytes WHERE hash = ?", chunk.hash);
1485
+ if (row === void 0) throw createWorkspaceError("EIO", `missing blob bytes for ${path}`, path);
1486
+ out.set(row.bytes, pos);
1487
+ pos += row.bytes.byteLength;
1488
+ }
1489
+ return out;
1490
+ }
1491
+ function spliceBytes(dst, at, src, srcOffset, length) {
1492
+ const newLength = Math.max(dst.byteLength, at + length);
1493
+ const out = new Uint8Array(newLength);
1494
+ out.set(dst, 0);
1495
+ const srcView = src.subarray(srcOffset, srcOffset + length);
1496
+ out.set(srcView, at);
1497
+ return out;
1498
+ }
1499
+ //#endregion
1500
+ //#region ../dofs/src/storage.ts
1501
+ var Database = class {
1502
+ sql;
1503
+ transactionSync;
1504
+ #txDepth = 0;
1505
+ constructor(storage) {
1506
+ this.sql = storage.sql;
1507
+ this.transactionSync = (closure) => {
1508
+ if (this.#txDepth > 0) {
1509
+ const sp = `_t${this.#txDepth}`;
1510
+ this.sql.exec(`SAVEPOINT ${sp}`);
1511
+ this.#txDepth++;
1512
+ try {
1513
+ const result = closure();
1514
+ this.sql.exec(`RELEASE ${sp}`);
1515
+ return result;
1516
+ } catch (error) {
1517
+ this.sql.exec(`ROLLBACK TO ${sp}`);
1518
+ this.sql.exec(`RELEASE ${sp}`);
1519
+ throw error;
1520
+ } finally {
1521
+ this.#txDepth--;
1522
+ }
1523
+ }
1524
+ this.#txDepth++;
1525
+ try {
1526
+ if (storage.transactionSync !== void 0) return storage.transactionSync(closure);
1527
+ if (storage.transaction !== void 0) {
1528
+ const result = storage.transaction(closure);
1529
+ if (result !== void 0 && result !== null && typeof result === "object" && "then" in result) throw new Error("Durable Object storage adapter requires synchronous transactions");
1530
+ return result;
1531
+ }
1532
+ return closure();
1533
+ } finally {
1534
+ this.#txDepth--;
1535
+ }
1536
+ };
1537
+ }
1538
+ run(query, ...bindings) {
1539
+ this.sql.exec(query, ...bindings);
1540
+ }
1541
+ all(query, ...bindings) {
1542
+ return this.sql.exec(query, ...bindings).toArray().map((row) => normalizeRow(row));
1543
+ }
1544
+ one(query, ...bindings) {
1545
+ return this.all(query, ...bindings)[0];
1546
+ }
1547
+ scalar(query, ...bindings) {
1548
+ const row = this.one(query, ...bindings);
1549
+ if (row === void 0) return;
1550
+ const [value] = Object.values(row);
1551
+ return value;
1552
+ }
1553
+ };
1554
+ function normalizeRow(row) {
1555
+ const out = {};
1556
+ for (const key of Object.keys(row)) {
1557
+ const value = row[key];
1558
+ out[key] = value instanceof ArrayBuffer ? new Uint8Array(value) : value;
1559
+ }
1560
+ return out;
1561
+ }
1562
+ //#endregion
1563
+ //#region ../dofs/src/sync/apply.ts
1564
+ const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
1565
+ const DEFAULT_MAX_PATHS = 1024;
1566
+ function hex$1(bytes) {
1567
+ let s = "";
1568
+ for (let i = 0; i < bytes.byteLength; i++) s += bytes[i].toString(16).padStart(2, "0");
1569
+ return s;
1570
+ }
1571
+ async function applyChanges(db, entries, objects, options = {}) {
1572
+ const revBeforeApply = currentRev(db);
1573
+ const maxBytes = options.maxBytesPerBatch ?? DEFAULT_MAX_BYTES;
1574
+ const maxPaths = options.maxPathsPerBatch ?? DEFAULT_MAX_PATHS;
1575
+ let bytesInBatch = 0;
1576
+ let pathsInBatch = 0;
1577
+ let applied = 0;
1578
+ const skipped = [];
1579
+ const flush = () => {
1580
+ bytesInBatch = 0;
1581
+ pathsInBatch = 0;
1582
+ };
1583
+ for await (const entry of entries) {
1584
+ if (options.source === "upstream" && entry.kind !== "delete") {
1585
+ if (alreadyApplied(db, entry)) continue;
1586
+ }
1587
+ const blockingRoot = readOnlyRootFor(db, entry.path);
1588
+ if (blockingRoot !== void 0) {
1589
+ skipped.push({
1590
+ path: entry.path,
1591
+ mountRoot: blockingRoot,
1592
+ op: entry.kind === "delete" ? "delete" : "write",
1593
+ reason: "read-only"
1594
+ });
1595
+ continue;
1596
+ }
1597
+ if (entry.kind === "delete") {
1598
+ try {
1599
+ rm(db, entry.path, {
1600
+ recursive: true,
1601
+ force: true
1602
+ });
1603
+ } catch {}
1604
+ applied++;
1605
+ pathsInBatch++;
1606
+ if (pathsInBatch >= maxPaths) flush();
1607
+ continue;
1608
+ }
1609
+ if (entry.kind === "dir") {
1610
+ mkdir(db, entry.path, {
1611
+ mode: entry.mode,
1612
+ recursive: true
1613
+ }, () => entry.mtime);
1614
+ applied++;
1615
+ pathsInBatch++;
1616
+ if (pathsInBatch >= maxPaths) flush();
1617
+ continue;
1618
+ }
1619
+ if (entry.kind === "symlink") {
1620
+ symlink(db, entry.target, entry.path, () => entry.mtime);
1621
+ applied++;
1622
+ pathsInBatch++;
1623
+ if (pathsInBatch >= maxPaths) flush();
1624
+ continue;
1625
+ }
1626
+ const parts = [];
1627
+ let total = 0;
1628
+ for (const c of entry.chunks) {
1629
+ const k = hex$1(c.hash);
1630
+ let bytes = objects.get(k);
1631
+ if (bytes === void 0) bytes = db.one("SELECT bytes FROM vfs_blob_bytes WHERE hash = ?", c.hash)?.bytes;
1632
+ if (bytes === void 0) throw new Error(`applyChanges: missing object ${k} for ${entry.path}`);
1633
+ parts.push(bytes);
1634
+ total += bytes.byteLength;
1635
+ }
1636
+ const buf = new Uint8Array(total);
1637
+ let off = 0;
1638
+ for (const p of parts) {
1639
+ buf.set(p, off);
1640
+ off += p.byteLength;
1641
+ }
1642
+ await writeFile(db, entry.path, buf, { mode: entry.mode }, () => entry.mtime);
1643
+ applied++;
1644
+ bytesInBatch += total;
1645
+ pathsInBatch++;
1646
+ if (bytesInBatch >= maxBytes || pathsInBatch >= maxPaths) flush();
1647
+ }
1648
+ if (options.advanceFetchRev !== void 0) {
1649
+ const current = readWatermark(db, "fetchRev");
1650
+ if (options.advanceFetchRev > current) writeWatermark(db, "fetchRev", options.advanceFetchRev);
1651
+ }
1652
+ if (options.source === "upstream") {
1653
+ const revAfter = currentRev(db);
1654
+ const existing = readWatermark(db, "pushRev");
1655
+ if (existing >= revBeforeApply && revAfter > existing) writeWatermark(db, "pushRev", revAfter);
1656
+ }
1657
+ return {
1658
+ applied,
1659
+ skipped
1660
+ };
1661
+ }
1662
+ function alreadyApplied(db, entry) {
1663
+ const live = resolveInode(db, entry.path, { followSymlinks: false });
1664
+ if (live === null) return false;
1665
+ if (entry.kind === "file") {
1666
+ if (live.type !== "file") return false;
1667
+ const row = db.one("SELECT manifest_hash FROM vfs_nodes WHERE inode = ?", live.inode);
1668
+ if (!row?.manifest_hash) return false;
1669
+ const wanted = computeManifestHash(entry.chunks);
1670
+ return uint8Equal(row.manifest_hash, wanted);
1671
+ }
1672
+ if (entry.kind === "dir") return live.type === "dir" && (live.mode & 4095) === (entry.mode & 4095);
1673
+ return live.type === "symlink" && live.linkTarget === entry.target && (live.mode & 4095) === (entry.mode & 4095);
1674
+ }
1675
+ function uint8Equal(a, b) {
1676
+ if (a.byteLength !== b.byteLength) return false;
1677
+ for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
1678
+ return true;
1679
+ }
1680
+ //#endregion
1681
+ //#region ../dofs/src/sync/invariant.ts
1682
+ function assertAppliedPushRev(appliedPushRev, pushRev) {
1683
+ if (appliedPushRev < pushRev) throw new Error(`cross-side invariant violated: appliedPushRev (${appliedPushRev}) < pushRev (${pushRev})`);
1684
+ }
1685
+ //#endregion
1686
+ //#region src/heartbeat.ts
1687
+ function startHeartbeat(options) {
1688
+ const { intervalMs, ping, onFailure } = options;
1689
+ let stopped = false;
1690
+ const timer = setInterval(() => {
1691
+ if (stopped) return;
1692
+ ping().catch((error) => {
1693
+ if (stopped) return;
1694
+ stopped = true;
1695
+ clearInterval(timer);
1696
+ onFailure(error instanceof Error ? error : new Error(String(error)));
1697
+ });
1698
+ }, intervalMs);
1699
+ return () => {
1700
+ if (stopped) return;
1701
+ stopped = true;
1702
+ clearInterval(timer);
1703
+ };
1704
+ }
1705
+ //#endregion
1706
+ //#region src/backends/cloudflare-container.ts
1707
+ const DEFAULT_EGRESS_HOST = "workspace.internal";
1708
+ const DEFAULT_CONTAINER_PORT = 8080;
1709
+ const DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
1710
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 2e4;
1711
+ var CloudflareContainerBackend = class {
1712
+ id = "cloudflare-container";
1713
+ #options;
1714
+ #pendingUpgrade;
1715
+ #resolveUpgrade;
1716
+ #rejectUpgrade;
1717
+ #handle;
1718
+ constructor(options) {
1719
+ this.#options = {
1720
+ container: options.container,
1721
+ workspace: options.workspace,
1722
+ containerEnv: options.containerEnv,
1723
+ egressHost: options.egressHost ?? DEFAULT_EGRESS_HOST,
1724
+ containerPort: options.containerPort ?? DEFAULT_CONTAINER_PORT,
1725
+ connectTimeoutMs: options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
1726
+ heartbeatIntervalMs: options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS
1727
+ };
1728
+ }
1729
+ async connect() {
1730
+ if (this.#handle) return this.#handle;
1731
+ const deadline = Date.now() + this.#options.connectTimeoutMs;
1732
+ const host = await (await this.#options.container()).getWorkspaceContainer();
1733
+ await host.start({
1734
+ PORT: String(this.#options.containerPort),
1735
+ MOUNT_POINT: "/workspace",
1736
+ ...this.#options.containerEnv
1737
+ });
1738
+ await host.interceptOutboundHttp(this.#options.egressHost, this.#options.workspace);
1739
+ this.#armUpgrade();
1740
+ await this.#waitForPort(host, deadline);
1741
+ await this.#postConnect(host, deadline);
1742
+ const ws = await this.#waitForUpgrade(deadline);
1743
+ const stub = newWebSocketRpcSession(ws);
1744
+ let stopHeartbeat;
1745
+ const closed = new Promise((resolve) => {
1746
+ let fired = false;
1747
+ const onClose = () => {
1748
+ if (fired) return;
1749
+ fired = true;
1750
+ stopHeartbeat?.();
1751
+ resolve();
1752
+ this.#handle = void 0;
1753
+ };
1754
+ ws.addEventListener("close", onClose, { once: true });
1755
+ ws.addEventListener("error", onClose, { once: true });
1756
+ stub.onRpcBroken(onClose);
1757
+ });
1758
+ if (this.#options.heartbeatIntervalMs > 0) stopHeartbeat = startHeartbeat({
1759
+ intervalMs: this.#options.heartbeatIntervalMs,
1760
+ ping: () => stub.sync.watermarks(),
1761
+ onFailure: () => {
1762
+ try {
1763
+ ws.close();
1764
+ } catch {}
1765
+ }
1766
+ });
1767
+ const handle = {
1768
+ rpc: stub,
1769
+ closed,
1770
+ close: async () => {
1771
+ stopHeartbeat?.();
1772
+ try {
1773
+ stub[Symbol.dispose]?.();
1774
+ } catch {}
1775
+ try {
1776
+ ws.close();
1777
+ } catch {}
1778
+ this.#handle = void 0;
1779
+ }
1780
+ };
1781
+ this.#handle = handle;
1782
+ return handle;
1783
+ }
1784
+ async handleFetch(req) {
1785
+ if (new URL(req.url).pathname !== "/ws") return new Response("not found", { status: 404 });
1786
+ if (req.headers.get("upgrade") !== "websocket") return new Response("expected websocket upgrade", { status: 426 });
1787
+ const pair = new WebSocketPair();
1788
+ const [client, server] = [pair[0], pair[1]];
1789
+ server.accept();
1790
+ if (this.#resolveUpgrade) this.#resolveUpgrade(server);
1791
+ else {
1792
+ server.close(1011, "no pending connect");
1793
+ return new Response("no pending connect", { status: 409 });
1794
+ }
1795
+ return new Response(null, {
1796
+ status: 101,
1797
+ webSocket: client
1798
+ });
1799
+ }
1800
+ #armUpgrade() {
1801
+ this.#pendingUpgrade = new Promise((resolve, reject) => {
1802
+ this.#resolveUpgrade = resolve;
1803
+ this.#rejectUpgrade = reject;
1804
+ });
1805
+ this.#pendingUpgrade.catch(() => {});
1806
+ }
1807
+ #clearUpgrade() {
1808
+ this.#pendingUpgrade = void 0;
1809
+ this.#resolveUpgrade = void 0;
1810
+ this.#rejectUpgrade = void 0;
1811
+ }
1812
+ async #waitForPort(host, deadline) {
1813
+ let lastError;
1814
+ while (Date.now() < deadline) try {
1815
+ (await host.port(this.#options.containerPort).fetch("http://container/health", { method: "HEAD" })).body?.cancel();
1816
+ return;
1817
+ } catch (error) {
1818
+ lastError = error;
1819
+ await sleep$1(250);
1820
+ }
1821
+ this.#rejectUpgrade?.(/* @__PURE__ */ new Error("port did not open"));
1822
+ this.#clearUpgrade();
1823
+ throw new Error(`CloudflareContainerBackend: container port ${this.#options.containerPort} did not open: ${describeError(lastError)}`);
1824
+ }
1825
+ async #postConnect(host, deadline) {
1826
+ const remaining = Math.max(0, deadline - Date.now());
1827
+ let res;
1828
+ try {
1829
+ res = await host.port(this.#options.containerPort).fetch("http://container/connect", {
1830
+ method: "POST",
1831
+ headers: { "content-type": "application/json" },
1832
+ body: JSON.stringify({
1833
+ url: `http://${this.#options.egressHost}`,
1834
+ healthTimeoutMs: remaining
1835
+ })
1836
+ });
1837
+ } catch (error) {
1838
+ this.#rejectUpgrade?.(error);
1839
+ this.#clearUpgrade();
1840
+ throw new Error(`CloudflareContainerBackend: POST /connect failed: ${describeError(error)}`);
1841
+ }
1842
+ if (!res.ok) {
1843
+ const body = await res.text().catch(() => "");
1844
+ this.#rejectUpgrade?.(/* @__PURE__ */ new Error(`/connect ${res.status}`));
1845
+ this.#clearUpgrade();
1846
+ throw new Error(`CloudflareContainerBackend: POST /connect returned ${res.status}: ${body}`);
1847
+ }
1848
+ }
1849
+ async #waitForUpgrade(deadline) {
1850
+ const upgrade = this.#pendingUpgrade;
1851
+ if (!upgrade) throw new Error("CloudflareContainerBackend: upgrade promise missing");
1852
+ const remaining = Math.max(0, deadline - Date.now());
1853
+ let timer;
1854
+ try {
1855
+ return await Promise.race([upgrade, new Promise((_, reject) => {
1856
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`CloudflareContainerBackend: /ws upgrade did not arrive within ${this.#options.connectTimeoutMs}ms`)), remaining);
1857
+ })]);
1858
+ } finally {
1859
+ if (timer) clearTimeout(timer);
1860
+ this.#clearUpgrade();
1861
+ }
1862
+ }
1863
+ };
1864
+ function sleep$1(ms) {
1865
+ return new Promise((resolve) => setTimeout(resolve, ms));
1866
+ }
1867
+ function describeError(error) {
1868
+ if (error instanceof Error) return error.message;
1869
+ return String(error);
1870
+ }
1871
+ //#endregion
1872
+ //#region src/backends/container-host.ts
1873
+ var WorkspaceContainerAPI = class extends RpcTarget$1 {
1874
+ #container;
1875
+ #ctx;
1876
+ constructor(ctx) {
1877
+ super();
1878
+ if (!ctx.container) throw new Error("WorkspaceContainerAPI: DO is not container-enabled (check wrangler.jsonc)");
1879
+ this.#container = ctx.container;
1880
+ this.#ctx = ctx;
1881
+ }
1882
+ async start(env) {
1883
+ if (this.#container.running) return;
1884
+ this.#container.start({
1885
+ enableInternet: true,
1886
+ env
1887
+ });
1888
+ }
1889
+ async interceptOutboundHttp(host, ref) {
1890
+ const exports = this.#ctx.exports;
1891
+ await this.#container.interceptOutboundHttp(host, exports.WorkspaceProxy({ props: ref }));
1892
+ }
1893
+ port(port) {
1894
+ return this.#container.getTcpPort(port);
1895
+ }
1896
+ };
1897
+ function withWorkspaceContainer(Base) {
1898
+ class WithWorkspaceContainer extends Base {
1899
+ getWorkspaceContainer() {
1900
+ return new WorkspaceContainerAPI(this.ctx);
1901
+ }
1902
+ }
1903
+ return WithWorkspaceContainer;
1904
+ }
1905
+ //#endregion
1906
+ //#region ../rpc/dist/client.js
1907
+ function createWorkspaceClient(options) {
1908
+ const ws = new (options.WebSocketImpl ?? WebSocket)(options.url);
1909
+ const stub = newWebSocketRpcSession(ws);
1910
+ return new Proxy(stub, { get(target, prop, receiver) {
1911
+ if (prop === "close") return async () => {
1912
+ try {
1913
+ target[Symbol.dispose]?.();
1914
+ } catch {}
1915
+ await new Promise((resolve) => {
1916
+ const w = ws;
1917
+ if (w.readyState >= 2) {
1918
+ resolve();
1919
+ return;
1920
+ }
1921
+ ws.addEventListener("close", () => resolve(), { once: true });
1922
+ w.close();
1923
+ setTimeout(resolve, 200);
1924
+ });
1925
+ };
1926
+ return Reflect.get(target, prop, receiver);
1927
+ } });
1928
+ }
1929
+ //#endregion
1930
+ //#region src/backends/test.ts
1931
+ var TestBackend = class {
1932
+ id = "test";
1933
+ #url;
1934
+ constructor(options) {
1935
+ this.#url = options.url;
1936
+ }
1937
+ async connect() {
1938
+ const wsUrl = toWebSocketUrl(this.#url);
1939
+ await probeHealth(this.#url);
1940
+ const client = createWorkspaceClient({ url: `${wsUrl}/ws` });
1941
+ return {
1942
+ rpc: client,
1943
+ close: async () => {
1944
+ await client.close();
1945
+ }
1946
+ };
1947
+ }
1948
+ };
1949
+ function toWebSocketUrl(input) {
1950
+ if (input.startsWith("ws://") || input.startsWith("wss://")) return stripTrailingSlash(input);
1951
+ if (input.startsWith("http://")) return stripTrailingSlash(`ws://${input.slice(7)}`);
1952
+ if (input.startsWith("https://")) return stripTrailingSlash(`wss://${input.slice(8)}`);
1953
+ throw new Error(`TestBackend: unsupported URL scheme in ${input}`);
1954
+ }
1955
+ function stripTrailingSlash(url) {
1956
+ return url.endsWith("/") ? url.slice(0, -1) : url;
1957
+ }
1958
+ async function probeHealth(url) {
1959
+ const healthUrl = `${stripTrailingSlash(toHttpUrl(url))}/health`;
1960
+ let response;
1961
+ try {
1962
+ response = await fetch(healthUrl);
1963
+ } catch (cause) {
1964
+ throw new Error(`TestBackend: ${healthUrl} is not reachable. Is the wsd container running? (${cause instanceof Error ? cause.message : String(cause)})`);
1965
+ }
1966
+ if (!response.ok) throw new Error(`TestBackend: ${healthUrl} returned ${response.status} ${response.statusText}`);
1967
+ }
1968
+ function toHttpUrl(input) {
1969
+ if (input.startsWith("ws://")) return `http://${input.slice(5)}`;
1970
+ if (input.startsWith("wss://")) return `https://${input.slice(6)}`;
1971
+ return input;
1972
+ }
1973
+ //#endregion
1974
+ //#region src/mounts/providers/r2.ts
1975
+ function normalisePrefix(prefix) {
1976
+ if (!prefix) return "";
1977
+ return prefix.endsWith("/") ? prefix : `${prefix}/`;
1978
+ }
1979
+ async function* paginate(bucket, prefix, limit) {
1980
+ let cursor;
1981
+ while (true) {
1982
+ const page = await bucket.list({
1983
+ prefix: prefix.length > 0 ? prefix : void 0,
1984
+ cursor,
1985
+ limit
1986
+ });
1987
+ for (const obj of page.objects) yield obj;
1988
+ if (!page.truncated || !page.cursor) return;
1989
+ cursor = page.cursor;
1990
+ }
1991
+ }
1992
+ async function runBounded(tasks, concurrency) {
1993
+ if (concurrency < 1) concurrency = 1;
1994
+ let next = 0;
1995
+ const workers = [];
1996
+ let firstError;
1997
+ for (let i = 0; i < Math.min(concurrency, tasks.length); i++) workers.push((async () => {
1998
+ while (true) {
1999
+ if (firstError !== void 0) return;
2000
+ const idx = next++;
2001
+ if (idx >= tasks.length) return;
2002
+ try {
2003
+ await tasks[idx]();
2004
+ } catch (error) {
2005
+ if (firstError === void 0) firstError = error;
2006
+ return;
2007
+ }
2008
+ }
2009
+ })());
2010
+ await Promise.all(workers);
2011
+ if (firstError !== void 0) throw firstError;
2012
+ }
2013
+ function R2Bucket(bucket, options = {}) {
2014
+ const prefix = normalisePrefix(options.prefix);
2015
+ const mode = options.mode ?? "read-only";
2016
+ const listLimit = options.listLimit ?? 1e3;
2017
+ const concurrency = options.concurrency ?? 8;
2018
+ return {
2019
+ kind: "r2",
2020
+ mode,
2021
+ strategy: "eager",
2022
+ maxBytes: options.maxBytes,
2023
+ maxEntries: options.maxEntries,
2024
+ async materialize(api) {
2025
+ const keys = [];
2026
+ for await (const obj of paginate(bucket, prefix, listLimit)) keys.push(obj);
2027
+ await runBounded(keys.map((entry) => async () => {
2028
+ const got = await bucket.get(entry.key);
2029
+ if (got === null) throw new Error(`R2Bucket: object disappeared mid-materialize: ${entry.key}`);
2030
+ const relKey = prefix.length > 0 ? entry.key.slice(prefix.length) : entry.key;
2031
+ if (relKey.length === 0) return;
2032
+ await api.writeFile(`${api.root}/${relKey}`, got.body);
2033
+ }), concurrency);
2034
+ }
2035
+ };
2036
+ }
2037
+ //#endregion
2038
+ //#region src/proxy.ts
2039
+ var WorkspaceProxy = class extends WorkerEntrypoint {
2040
+ async fetch(request) {
2041
+ const url = new URL(request.url);
2042
+ if (url.pathname === "/health") return new Response("ok\n", { headers: { "content-type": "text/plain; charset=utf-8" } });
2043
+ if (url.pathname === "/ws") {
2044
+ const { binding, id } = this.ctx.props;
2045
+ const ns = this.env[binding];
2046
+ if (!ns) return new Response(`WorkspaceProxy: env.${binding} is not a DurableObjectNamespace`, { status: 500 });
2047
+ return ns.get(ns.idFromString(id)).fetch(request);
2048
+ }
2049
+ return new Response("not found", { status: 404 });
2050
+ }
2051
+ };
2052
+ //#endregion
2053
+ //#region src/shell.ts
2054
+ var WorkspaceShell = class {
2055
+ #shell;
2056
+ #sync;
2057
+ constructor(shell, sync) {
2058
+ this.#shell = shell;
2059
+ this.#sync = sync;
2060
+ }
2061
+ async exec(command, options = {}) {
2062
+ let pushed = 0;
2063
+ try {
2064
+ pushed = await this.#sync.push();
2065
+ } catch {}
2066
+ const envelope = await this.#shell.exec({
2067
+ command,
2068
+ id: options.id,
2069
+ cwd: options.cwd,
2070
+ timeoutMs: options.timeoutMs
2071
+ });
2072
+ const events = disposeOnDone$1(envelope.events, () => maybeDispose$1(envelope));
2073
+ return wrapHandle(this.#shell, this.#sync, envelope.id, events, options.encoding, pushed);
2074
+ }
2075
+ async get(id, options = {}) {
2076
+ const after = resumeToAfter(options.resume);
2077
+ const envelope = await this.#shell.getExec({
2078
+ id,
2079
+ after
2080
+ });
2081
+ const events = disposeOnDone$1(envelope.events, () => maybeDispose$1(envelope));
2082
+ return wrapHandle(this.#shell, this.#sync, id, events, options.encoding, 0);
2083
+ }
2084
+ };
2085
+ function resumeToAfter(resume) {
2086
+ if (resume === void 0 || resume === "full") return void 0;
2087
+ if (resume === "tail") return "tail";
2088
+ return resume;
2089
+ }
2090
+ function wrapHandle(shell, sync, id, wireEvents, encoding, pushed) {
2091
+ const [forUser, forWatcher] = wireEvents.tee();
2092
+ const exited = watchForExit(forWatcher);
2093
+ const stream = pipeEvents(forUser, encoding);
2094
+ const handle = stream;
2095
+ Object.defineProperties(handle, {
2096
+ id: {
2097
+ value: id,
2098
+ enumerable: false,
2099
+ writable: false
2100
+ },
2101
+ result: {
2102
+ value: () => drainToResult(stream, encoding, sync, pushed),
2103
+ enumerable: false,
2104
+ writable: false
2105
+ },
2106
+ kill: {
2107
+ value: async (signal) => {
2108
+ await shell.killExec({
2109
+ id,
2110
+ signal
2111
+ });
2112
+ await exited;
2113
+ },
2114
+ enumerable: false,
2115
+ writable: false
2116
+ }
2117
+ });
2118
+ return handle;
2119
+ }
2120
+ function watchForExit(events) {
2121
+ return (async () => {
2122
+ const reader = events.getReader();
2123
+ try {
2124
+ while (true) {
2125
+ const { value, done } = await reader.read();
2126
+ if (done) return;
2127
+ if (value.name === "exit") return;
2128
+ }
2129
+ } catch {} finally {
2130
+ reader.releaseLock();
2131
+ }
2132
+ })();
2133
+ }
2134
+ function pipeEvents(source, encoding) {
2135
+ if (encoding !== "utf8") return source;
2136
+ const stdoutDec = new TextDecoder("utf-8", { fatal: false });
2137
+ const stderrDec = new TextDecoder("utf-8", { fatal: false });
2138
+ return source.pipeThrough(new TransformStream({
2139
+ transform(event, controller) {
2140
+ if (event.name === "stdout") controller.enqueue({
2141
+ id: event.id,
2142
+ seq: event.seq,
2143
+ name: "stdout",
2144
+ value: stdoutDec.decode(event.value, { stream: true })
2145
+ });
2146
+ else if (event.name === "stderr") controller.enqueue({
2147
+ id: event.id,
2148
+ seq: event.seq,
2149
+ name: "stderr",
2150
+ value: stderrDec.decode(event.value, { stream: true })
2151
+ });
2152
+ else controller.enqueue(event);
2153
+ },
2154
+ flush(_controller) {
2155
+ stdoutDec.decode();
2156
+ stderrDec.decode();
2157
+ }
2158
+ }));
2159
+ }
2160
+ async function drainToResult(stream, encoding, sync, pushed) {
2161
+ const reader = stream.getReader();
2162
+ const stdoutParts = [];
2163
+ const stderrParts = [];
2164
+ let exitCode = -1;
2165
+ try {
2166
+ while (true) {
2167
+ const { value, done } = await reader.read();
2168
+ if (done) break;
2169
+ if (value.name === "stdout") stdoutParts.push(value.value);
2170
+ else if (value.name === "stderr") stderrParts.push(value.value);
2171
+ else exitCode = value.value;
2172
+ }
2173
+ } finally {
2174
+ reader.releaseLock();
2175
+ }
2176
+ let pulled = 0;
2177
+ let skipped = [];
2178
+ try {
2179
+ const result = await sync.pull();
2180
+ pulled = result.applied;
2181
+ skipped = result.skipped;
2182
+ } catch {}
2183
+ return {
2184
+ exitCode,
2185
+ stdout: joinParts(stdoutParts, encoding),
2186
+ stderr: joinParts(stderrParts, encoding),
2187
+ pushed,
2188
+ pulled,
2189
+ skipped
2190
+ };
2191
+ }
2192
+ function joinParts(parts, encoding) {
2193
+ if (parts.length === 0) return encoding === "utf8" ? "" : new Uint8Array(0);
2194
+ if (typeof parts[0] === "string") return parts.join("");
2195
+ const arrays = parts;
2196
+ const total = arrays.reduce((acc, a) => acc + a.byteLength, 0);
2197
+ const out = new Uint8Array(total);
2198
+ let offset = 0;
2199
+ for (const a of arrays) {
2200
+ out.set(a, offset);
2201
+ offset += a.byteLength;
2202
+ }
2203
+ return out;
2204
+ }
2205
+ function disposeOnDone$1(stream, onDone) {
2206
+ let fired = false;
2207
+ const fire = () => {
2208
+ if (fired) return;
2209
+ fired = true;
2210
+ try {
2211
+ onDone();
2212
+ } catch {}
2213
+ };
2214
+ return stream.pipeThrough(new TransformStream({
2215
+ transform(chunk, controller) {
2216
+ controller.enqueue(chunk);
2217
+ },
2218
+ flush() {
2219
+ fire();
2220
+ },
2221
+ cancel() {
2222
+ fire();
2223
+ }
2224
+ }));
2225
+ }
2226
+ function maybeDispose$1(value) {
2227
+ const d = value?.[Symbol.dispose];
2228
+ if (typeof d === "function") d.call(value);
2229
+ }
2230
+ //#endregion
2231
+ //#region ../rpc/dist/debug.js
2232
+ let trackingEnabled = readFlag();
2233
+ function readFlag() {
2234
+ try {
2235
+ if ((globalThis.process?.env)?.CAPNWEB_TRACK_STUBS === "1") return true;
2236
+ } catch {}
2237
+ const override = globalThis.CAPNWEB_TRACK_STUBS;
2238
+ return override === "1" || override === true;
2239
+ }
2240
+ const counters = /* @__PURE__ */ new Map();
2241
+ function trackStub(target) {
2242
+ if (!trackingEnabled) return;
2243
+ const name = target.constructor?.name ?? "anonymous";
2244
+ counters.set(name, (counters.get(name) ?? 0) + 1);
2245
+ }
2246
+ function untrackStub(target) {
2247
+ if (!trackingEnabled) return;
2248
+ const name = target.constructor?.name ?? "anonymous";
2249
+ const current = counters.get(name) ?? 0;
2250
+ if (current <= 1) counters.delete(name);
2251
+ else counters.set(name, current - 1);
2252
+ }
2253
+ //#endregion
2254
+ //#region src/stub.ts
2255
+ var WorkspaceFilesystemStub = class extends RpcTarget {
2256
+ #ws;
2257
+ constructor(ws) {
2258
+ super();
2259
+ this.#ws = ws;
2260
+ trackStub(this);
2261
+ }
2262
+ [Symbol.dispose]() {
2263
+ untrackStub(this);
2264
+ }
2265
+ readFile(path, optionsOrEncoding) {
2266
+ return this.#ws.fs.readFile(path, optionsOrEncoding);
2267
+ }
2268
+ stat(path) {
2269
+ return this.#ws.fs.stat(path);
2270
+ }
2271
+ readdir(path) {
2272
+ return this.#ws.fs.readdir(path);
2273
+ }
2274
+ find(directory, pattern) {
2275
+ return this.#ws.fs.find(directory, pattern);
2276
+ }
2277
+ ls(prefix) {
2278
+ return this.#ws.fs.ls(prefix);
2279
+ }
2280
+ grep(pattern, path, options = {}) {
2281
+ return this.#ws.fs.grep(pattern, path, options);
2282
+ }
2283
+ writeFile(path, content, options = {}) {
2284
+ return this.#ws.fs.writeFile(path, content, options);
2285
+ }
2286
+ mkdir(path, options = {}) {
2287
+ return this.#ws.fs.mkdir(path, options);
2288
+ }
2289
+ rm(path, options = {}) {
2290
+ return this.#ws.fs.rm(path, options);
2291
+ }
2292
+ };
2293
+ var WorkspaceExecHandleStub = class extends RpcTarget {
2294
+ #pending;
2295
+ constructor(pending) {
2296
+ super();
2297
+ this.#pending = pending;
2298
+ trackStub(this);
2299
+ }
2300
+ [Symbol.dispose]() {
2301
+ untrackStub(this);
2302
+ }
2303
+ async result() {
2304
+ const result = await this.#pending;
2305
+ return {
2306
+ exitCode: result.exitCode,
2307
+ stdout: result.stdout,
2308
+ stderr: result.stderr
2309
+ };
2310
+ }
2311
+ };
2312
+ var WorkspaceShellStub = class extends RpcTarget {
2313
+ #ws;
2314
+ constructor(ws) {
2315
+ super();
2316
+ this.#ws = ws;
2317
+ trackStub(this);
2318
+ }
2319
+ [Symbol.dispose]() {
2320
+ untrackStub(this);
2321
+ }
2322
+ async exec(command, options = {}) {
2323
+ return new WorkspaceExecHandleStub(options.encoding === "utf8" ? this.#ws.shell.exec(command, {
2324
+ cwd: options.cwd,
2325
+ encoding: "utf8"
2326
+ }).then((handle) => handle.result()) : this.#ws.shell.exec(command, { cwd: options.cwd }).then((handle) => handle.result()));
2327
+ }
2328
+ };
2329
+ var WorkspaceStub = class extends RpcTarget {
2330
+ #fs;
2331
+ #shell;
2332
+ constructor(ws) {
2333
+ super();
2334
+ this.#fs = new WorkspaceFilesystemStub(ws);
2335
+ this.#shell = new WorkspaceShellStub(ws);
2336
+ trackStub(this);
2337
+ }
2338
+ [Symbol.dispose]() {
2339
+ this.#fs[Symbol.dispose]();
2340
+ this.#shell[Symbol.dispose]();
2341
+ untrackStub(this);
2342
+ }
2343
+ get fs() {
2344
+ return this.#fs;
2345
+ }
2346
+ get shell() {
2347
+ return this.#shell;
2348
+ }
2349
+ };
2350
+ //#endregion
2351
+ //#region ../rpc/src/sync-driver.ts
2352
+ function hex(bytes) {
2353
+ let s = "";
2354
+ for (let i = 0; i < bytes.byteLength; i++) s += bytes[i].toString(16).padStart(2, "0");
2355
+ return s;
2356
+ }
2357
+ function disposeOnDone(stream, onDone) {
2358
+ let fired = false;
2359
+ const fire = () => {
2360
+ if (fired) return;
2361
+ fired = true;
2362
+ try {
2363
+ onDone();
2364
+ } catch {}
2365
+ };
2366
+ return stream.pipeThrough(new TransformStream({
2367
+ transform(chunk, controller) {
2368
+ controller.enqueue(chunk);
2369
+ },
2370
+ flush() {
2371
+ fire();
2372
+ },
2373
+ cancel() {
2374
+ fire();
2375
+ }
2376
+ }));
2377
+ }
2378
+ function maybeDispose(value) {
2379
+ const d = value?.[Symbol.dispose];
2380
+ if (typeof d === "function") d.call(value);
2381
+ }
2382
+ const PULL_BATCH_SIZE = 256;
2383
+ async function pullOnce(db, remote) {
2384
+ const sinceRev = readWatermark(db, "fetchRev");
2385
+ const localPushRev = readWatermark(db, "pushRev");
2386
+ const fetchResult = await remote.fetchChanges({ sinceRev });
2387
+ const { currentRev: remoteRev, appliedPushRev } = fetchResult;
2388
+ assertAppliedPushRev(appliedPushRev, localPushRev);
2389
+ const stream = disposeOnDone(fetchResult.stream, () => maybeDispose(fetchResult));
2390
+ if (remoteRev <= sinceRev) {
2391
+ await stream.cancel().catch(() => {});
2392
+ return {
2393
+ applied: 0,
2394
+ skipped: []
2395
+ };
2396
+ }
2397
+ const reader = stream.getReader();
2398
+ let totalApplied = 0;
2399
+ const totalSkipped = [];
2400
+ let streamDone = false;
2401
+ try {
2402
+ while (!streamDone) {
2403
+ const batch = [];
2404
+ const wantedHashes = [];
2405
+ const seenHash = /* @__PURE__ */ new Set();
2406
+ let batchMaxRev = 0;
2407
+ while (batch.length < PULL_BATCH_SIZE) {
2408
+ const { value, done } = await reader.read();
2409
+ if (done) {
2410
+ streamDone = true;
2411
+ break;
2412
+ }
2413
+ batch.push(value);
2414
+ if (value.rev > batchMaxRev) batchMaxRev = value.rev;
2415
+ if (value.kind === "file") for (const c of value.chunks) {
2416
+ const k = hex(c.hash);
2417
+ if (!seenHash.has(k)) {
2418
+ seenHash.add(k);
2419
+ wantedHashes.push(c.hash);
2420
+ }
2421
+ }
2422
+ }
2423
+ if (batch.length === 0) break;
2424
+ if (wantedHashes.length > 0) {
2425
+ const haveSubset = await remote.hasObjects(wantedHashes);
2426
+ const remoteHasLocally = /* @__PURE__ */ new Set();
2427
+ for (const h of haveSubset) remoteHasLocally.add(hex(h));
2428
+ const missing = wantedHashes.filter((h) => {
2429
+ const k = hex(h);
2430
+ if (!remoteHasLocally.has(k)) return false;
2431
+ return db.one("SELECT hash FROM vfs_blobs WHERE hash = ?", h) === void 0;
2432
+ });
2433
+ if (missing.length > 0) {
2434
+ const bytesReader = (await remote.fetchObjects(missing)).getReader();
2435
+ try {
2436
+ while (true) {
2437
+ const { value, done } = await bytesReader.read();
2438
+ if (done) break;
2439
+ stageBlob(db, value.hash, value.bytes, Date.now());
2440
+ }
2441
+ } finally {
2442
+ bytesReader.releaseLock();
2443
+ }
2444
+ }
2445
+ }
2446
+ const batchResult = await applyChanges(db, batch, /* @__PURE__ */ new Map(), {
2447
+ source: "upstream",
2448
+ advanceFetchRev: batchMaxRev
2449
+ });
2450
+ totalApplied += batchResult.applied;
2451
+ if (batchResult.skipped.length > 0) for (const s of batchResult.skipped) totalSkipped.push(s);
2452
+ }
2453
+ } finally {
2454
+ reader.releaseLock();
2455
+ }
2456
+ if (remoteRev > readWatermark(db, "fetchRev")) writeWatermark(db, "fetchRev", remoteRev);
2457
+ return {
2458
+ applied: totalApplied,
2459
+ skipped: totalSkipped
2460
+ };
2461
+ }
2462
+ async function pushOnce(db, remote) {
2463
+ const sincePush = readWatermark(db, "pushRev");
2464
+ const localRev = currentRev(db);
2465
+ if (localRev <= sincePush) return 0;
2466
+ const entries = [];
2467
+ const wantedHashes = [];
2468
+ const seenHash = /* @__PURE__ */ new Set();
2469
+ for await (const e of coalesceChanges(db, sincePush)) {
2470
+ entries.push(e);
2471
+ if (e.kind === "file") for (const c of e.chunks) {
2472
+ const k = hex(c.hash);
2473
+ if (!seenHash.has(k)) {
2474
+ seenHash.add(k);
2475
+ wantedHashes.push(c.hash);
2476
+ }
2477
+ }
2478
+ }
2479
+ if (entries.length === 0) return 0;
2480
+ const remoteHas = /* @__PURE__ */ new Set();
2481
+ if (wantedHashes.length > 0) {
2482
+ const have = await remote.hasObjects(wantedHashes);
2483
+ for (const h of have) remoteHas.add(hex(h));
2484
+ }
2485
+ const missing = wantedHashes.filter((h) => !remoteHas.has(hex(h)));
2486
+ if (missing.length > 0) {
2487
+ const local = (function* () {
2488
+ for (const h of missing) {
2489
+ const row = db.one("SELECT bytes FROM vfs_blob_bytes WHERE hash = ?", h);
2490
+ if (row === void 0) throw new Error(`pushOnce: missing local blob ${hex(h)}`);
2491
+ yield {
2492
+ hash: h,
2493
+ bytes: row.bytes
2494
+ };
2495
+ }
2496
+ })();
2497
+ const bytesStream = new ReadableStream({ pull(controller) {
2498
+ const next = local.next();
2499
+ if (next.done) controller.close();
2500
+ else controller.enqueue(next.value);
2501
+ } });
2502
+ await remote.pushObjects(bytesStream);
2503
+ }
2504
+ const entryStream = new ReadableStream({ start(controller) {
2505
+ for (const e of entries) controller.enqueue(e);
2506
+ controller.close();
2507
+ } });
2508
+ assertAppliedPushRev((await remote.push({
2509
+ senderRev: localRev,
2510
+ changes: entryStream
2511
+ })).appliedPushRev, localRev);
2512
+ writeWatermark(db, "pushRev", localRev);
2513
+ return entries.length;
2514
+ }
2515
+ async function reconcileWatermarks(db, remote) {
2516
+ const remoteWatermarks = await remote.watermarks();
2517
+ const localFetchRev = readWatermark(db, "fetchRev");
2518
+ const localPushRev = readWatermark(db, "pushRev");
2519
+ let fetchRevReset = false;
2520
+ let pushRevReset = false;
2521
+ if (remoteWatermarks.currentRev < localFetchRev) {
2522
+ writeWatermark(db, "fetchRev", 0);
2523
+ fetchRevReset = true;
2524
+ }
2525
+ if (remoteWatermarks.pushRev < localPushRev) {
2526
+ writeWatermark(db, "pushRev", 0);
2527
+ pushRevReset = true;
2528
+ }
2529
+ return {
2530
+ fetchRevReset,
2531
+ pushRevReset
2532
+ };
2533
+ }
2534
+ //#endregion
2535
+ //#region src/mounts/index.ts
2536
+ async function runIndex(opts) {
2537
+ const { db, fs, mounts } = opts;
2538
+ const status = /* @__PURE__ */ new Map();
2539
+ for (const root of mounts.keys()) {
2540
+ const row = db.one("SELECT indexed FROM _vfs_mounts WHERE root = ?", root);
2541
+ status.set(root, row?.indexed === 1);
2542
+ }
2543
+ const pending = [...mounts.entries()].filter(([root]) => status.get(root) !== true);
2544
+ if (pending.length === 0) return;
2545
+ const failures = (await Promise.allSettled(pending.map(async ([root, mount]) => {
2546
+ db.run("INSERT INTO _vfs_mounts (root, kind, mode, indexed) VALUES (?, ?, 'read-write', 0)\n ON CONFLICT(root) DO UPDATE SET kind = excluded.kind, mode = 'read-write', indexed = 0", root, mount.kind);
2547
+ invalidateReadOnlyMountCache(db);
2548
+ const api = createWriteAPI({
2549
+ fs,
2550
+ root,
2551
+ mount
2552
+ });
2553
+ try {
2554
+ await fs.mkdir(root, { recursive: true });
2555
+ await mount.materialize(api);
2556
+ stampMountProvenance(db, root);
2557
+ } catch (error) {
2558
+ try {
2559
+ await fs.rm(root, {
2560
+ recursive: true,
2561
+ force: true
2562
+ });
2563
+ } catch {}
2564
+ throw error;
2565
+ }
2566
+ db.run("UPDATE _vfs_mounts SET mode = ?, indexed = 1 WHERE root = ?", mount.mode, root);
2567
+ invalidateReadOnlyMountCache(db);
2568
+ }))).filter((r) => r.status === "rejected");
2569
+ if (failures.length > 0) throw failures[0].reason;
2570
+ }
2571
+ function findInodeAt(db, absPath) {
2572
+ if (absPath === "/") return 1;
2573
+ const parts = absPath.split("/").filter((p) => p.length > 0);
2574
+ let current = 1;
2575
+ for (const name of parts) {
2576
+ const row = db.one("SELECT d.child_inode AS child_inode, n.type AS type\n FROM vfs_dirents d JOIN vfs_nodes n ON n.inode = d.child_inode\n WHERE d.parent_inode = ? AND d.name = ?", current, name);
2577
+ if (row === void 0) return void 0;
2578
+ current = row.child_inode;
2579
+ }
2580
+ return current;
2581
+ }
2582
+ function stampMountProvenance(db, root) {
2583
+ const rootInode = findInodeAt(db, root);
2584
+ if (rootInode === void 0) return;
2585
+ const inodes = [rootInode];
2586
+ const queue = [rootInode];
2587
+ while (queue.length > 0) {
2588
+ const parent = queue.shift();
2589
+ const children = db.all("SELECT child_inode FROM vfs_dirents WHERE parent_inode = ?", parent);
2590
+ for (const c of children) {
2591
+ inodes.push(c.child_inode);
2592
+ queue.push(c.child_inode);
2593
+ }
2594
+ }
2595
+ for (const inode of inodes) db.run("UPDATE vfs_nodes SET mount_root = ? WHERE inode = ?", root, inode);
2596
+ }
2597
+ function createWriteAPI(opts) {
2598
+ const { fs, root, mount } = opts;
2599
+ const maxBytes = mount.maxBytes;
2600
+ const maxEntries = mount.maxEntries;
2601
+ let bytesWritten = 0;
2602
+ let entriesWritten = 0;
2603
+ function checkPath(absPath) {
2604
+ if (!absPath.startsWith(`${root}/`) && absPath !== root) throw new Error(`mount ${root}: writeFile/mkdir target ${absPath} is outside the mount root`);
2605
+ }
2606
+ return {
2607
+ root,
2608
+ async writeFile(absPath, source, mode) {
2609
+ checkPath(absPath);
2610
+ if (maxEntries !== void 0 && entriesWritten + 1 > maxEntries) throw new Error(`mount ${root}: maxEntries=${maxEntries} exceeded`);
2611
+ entriesWritten += 1;
2612
+ const lastSlash = absPath.lastIndexOf("/");
2613
+ if (lastSlash > 0) await fs.mkdir(absPath.slice(0, lastSlash), { recursive: true });
2614
+ let toWrite = source;
2615
+ if (maxBytes !== void 0) {
2616
+ const [counted, forwarded] = source.tee();
2617
+ toWrite = forwarded;
2618
+ const cancelForwarded = (reason) => {
2619
+ forwarded.cancel(reason).catch(() => {});
2620
+ };
2621
+ const counter = (async () => {
2622
+ const reader = counted.getReader();
2623
+ try {
2624
+ while (true) {
2625
+ const { value, done } = await reader.read();
2626
+ if (done) break;
2627
+ if (value === void 0) continue;
2628
+ bytesWritten += value.byteLength;
2629
+ if (bytesWritten > maxBytes) {
2630
+ const err = /* @__PURE__ */ new Error(`mount ${root}: maxBytes=${maxBytes} exceeded (saw ${bytesWritten})`);
2631
+ cancelForwarded(err);
2632
+ throw err;
2633
+ }
2634
+ }
2635
+ } finally {
2636
+ reader.releaseLock();
2637
+ }
2638
+ })();
2639
+ const writePromise = fs.writeFile(absPath, toWrite, { mode });
2640
+ const [counterResult, writeResult] = await Promise.allSettled([counter, writePromise]);
2641
+ if (counterResult.status === "rejected") throw counterResult.reason;
2642
+ if (writeResult.status === "rejected") throw writeResult.reason;
2643
+ return;
2644
+ }
2645
+ await fs.writeFile(absPath, toWrite, { mode });
2646
+ },
2647
+ async mkdir(absPath, mode) {
2648
+ checkPath(absPath);
2649
+ if (maxEntries !== void 0 && entriesWritten + 1 > maxEntries) throw new Error(`mount ${root}: maxEntries=${maxEntries} exceeded`);
2650
+ entriesWritten += 1;
2651
+ await fs.mkdir(absPath, {
2652
+ recursive: true,
2653
+ mode
2654
+ });
2655
+ }
2656
+ };
2657
+ }
2658
+ var MountIndex = class {
2659
+ #db;
2660
+ #fs;
2661
+ #mounts;
2662
+ #inFlight;
2663
+ #done = false;
2664
+ constructor(opts) {
2665
+ this.#db = opts.db;
2666
+ this.#fs = opts.fs;
2667
+ this.#mounts = opts.mounts;
2668
+ }
2669
+ ensureIndexed() {
2670
+ if (this.#done) return Promise.resolve();
2671
+ if (this.#inFlight) return this.#inFlight;
2672
+ if (this.#mounts.size === 0) {
2673
+ this.#done = true;
2674
+ return Promise.resolve();
2675
+ }
2676
+ this.#inFlight = runIndex({
2677
+ db: this.#db,
2678
+ fs: this.#fs,
2679
+ mounts: this.#mounts
2680
+ }).then(() => {
2681
+ this.#done = true;
2682
+ }).finally(() => {
2683
+ this.#inFlight = void 0;
2684
+ });
2685
+ return this.#inFlight;
2686
+ }
2687
+ };
2688
+ //#endregion
2689
+ //#region src/mounts/registry.ts
2690
+ function validateRoot(root) {
2691
+ if (root.length === 0 || !root.startsWith("/")) throw new Error(`mount root must be absolute (starts with '/'): ${JSON.stringify(root)}`);
2692
+ if (root.length > 1 && root.endsWith("/")) throw new Error(`mount root must not have a trailing slash: ${JSON.stringify(root)}`);
2693
+ }
2694
+ function rejectNesting(roots) {
2695
+ for (let i = 0; i < roots.length; i++) for (let j = 0; j < roots.length; j++) {
2696
+ if (i === j) continue;
2697
+ const a = roots[i];
2698
+ const b = roots[j];
2699
+ if (b.startsWith(`${a}/`)) throw new Error(`mount roots must not nest: ${a} contains ${b}`);
2700
+ }
2701
+ }
2702
+ function buildMountRegistry(mounts, options) {
2703
+ const out = /* @__PURE__ */ new Map();
2704
+ if (mounts === void 0) return out;
2705
+ const roots = Object.keys(mounts);
2706
+ for (const root of roots) validateRoot(root);
2707
+ rejectNesting(roots);
2708
+ const sessionId = options.sessionId ?? "";
2709
+ let vfsCached;
2710
+ const vfs = () => {
2711
+ if (vfsCached === void 0) vfsCached = options.vfs();
2712
+ return vfsCached;
2713
+ };
2714
+ for (const root of roots) {
2715
+ const value = mounts[root];
2716
+ if (typeof value === "function") {
2717
+ const ctx = {
2718
+ sessionId,
2719
+ root,
2720
+ vfs: vfs()
2721
+ };
2722
+ out.set(root, value(ctx));
2723
+ } else out.set(root, value);
2724
+ }
2725
+ return out;
2726
+ }
2727
+ //#endregion
2728
+ //#region src/workspace.ts
2729
+ var Workspace = class {
2730
+ #db;
2731
+ #fs;
2732
+ /**
2733
+ * Lazily-constructed dofs provider. Built on first `provider()`
2734
+ * call; cached so repeated callers share the same instance.
2735
+ */
2736
+ #provider;
2737
+ #backends;
2738
+ #reconnect;
2739
+ #now;
2740
+ #mounts;
2741
+ #mountIndex;
2742
+ #handle;
2743
+ #shell;
2744
+ #readyPromise;
2745
+ #mutationTail = Promise.resolve();
2746
+ constructor(options) {
2747
+ if (options.backends.length === 0) throw new Error("Workspace requires at least one backend");
2748
+ this.#now = options.now ?? Date.now;
2749
+ this.#db = new Database(options.storage);
2750
+ initializeSchema(this.#db, this.#now);
2751
+ this.#fs = new WorkspaceFilesystem(this.#db, { now: this.#now });
2752
+ this.#backends = options.backends.slice();
2753
+ this.#reconnect = options.reconnect ?? {
2754
+ attempts: 1,
2755
+ initialDelayMs: 0,
2756
+ maxDelayMs: 0
2757
+ };
2758
+ this.#mounts = buildMountRegistry(options.mounts, {
2759
+ sessionId: options.sessionId,
2760
+ vfs: () => this.provider()
2761
+ });
2762
+ this.#mountIndex = new MountIndex({
2763
+ db: this.#db,
2764
+ fs: this.#fs,
2765
+ mounts: this.#mounts
2766
+ });
2767
+ }
2768
+ ensureMountsIndexed() {
2769
+ return this.#mountIndex.ensureIndexed();
2770
+ }
2771
+ mounts() {
2772
+ return new Map(this.#mounts);
2773
+ }
2774
+ get db() {
2775
+ return this.#db;
2776
+ }
2777
+ get fs() {
2778
+ return this.#fs;
2779
+ }
2780
+ /**
2781
+ * Underlying dofs `SQLiteWorkspaceProvider` over the local store.
2782
+ *
2783
+ * This is the `@platformatic/vfs`-shaped provider — a node:fs
2784
+ * surface with full symlink support. Callers that want a
2785
+ * `VirtualFileSystem` (e.g. to hand to isomorphic-git) wrap it
2786
+ * themselves to keep `@platformatic/vfs` out of this package's
2787
+ * dependency tree:
2788
+ *
2789
+ * ```ts
2790
+ * import { create, VirtualProvider } from "@platformatic/vfs";
2791
+ * import type { SQLiteWorkspaceProvider } from "@cloudflare/dofs";
2792
+ *
2793
+ * class Glue extends VirtualProvider {
2794
+ * constructor(private inner: SQLiteWorkspaceProvider) { super(); }
2795
+ * override get readonly() { return this.inner.readonly; }
2796
+ * override get supportsSymlinks() { return this.inner.supportsSymlinks; }
2797
+ * override get supportsWatch() { return this.inner.supportsWatch; }
2798
+ * }
2799
+ * // Forward every node:fs method to `inner` via a
2800
+ * // `for (const name of [...]) Object.defineProperty(...)` loop.
2801
+ * const vfs = create(new Glue(workspace.provider()));
2802
+ * ```
2803
+ *
2804
+ * Available immediately; doesn't need `ready()` because the
2805
+ * provider only reads/writes the local store, not the wire.
2806
+ */
2807
+ provider() {
2808
+ if (!this.#provider) this.#provider = new SQLiteWorkspaceProvider(this.#db, { now: this.#now });
2809
+ return this.#provider;
2810
+ }
2811
+ get shell() {
2812
+ if (!this.#shell) throw new Error("Workspace not connected — await ready() first");
2813
+ return this.#shell;
2814
+ }
2815
+ ready() {
2816
+ if (this.#readyPromise) return this.#readyPromise;
2817
+ this.#readyPromise = (async () => {
2818
+ await this.#connect();
2819
+ await this.#mountIndex.ensureIndexed();
2820
+ })();
2821
+ return this.#readyPromise;
2822
+ }
2823
+ stub() {
2824
+ this.shell;
2825
+ return new WorkspaceStub(this);
2826
+ }
2827
+ push() {
2828
+ return this.#serialize(async () => {
2829
+ await this.ready();
2830
+ if (!this.#handle) throw new Error("Workspace not connected");
2831
+ return pushOnce(this.#db, this.#handle.rpc.sync);
2832
+ });
2833
+ }
2834
+ pull() {
2835
+ return this.#serialize(async () => {
2836
+ await this.ready();
2837
+ if (!this.#handle) throw new Error("Workspace not connected");
2838
+ return pullOnce(this.#db, this.#handle.rpc.sync);
2839
+ });
2840
+ }
2841
+ #serialize(fn) {
2842
+ const run = this.#mutationTail.then(fn, fn);
2843
+ this.#mutationTail = run.then(() => void 0, () => void 0);
2844
+ return run;
2845
+ }
2846
+ async close() {
2847
+ if (this.#handle) try {
2848
+ await this.#handle.close();
2849
+ } finally {
2850
+ this.#handle = void 0;
2851
+ this.#shell = void 0;
2852
+ this.#readyPromise = void 0;
2853
+ }
2854
+ }
2855
+ async #connect() {
2856
+ const { attempts, initialDelayMs, maxDelayMs } = this.#reconnect;
2857
+ let delay = initialDelayMs;
2858
+ let lastError;
2859
+ for (let attempt = 1; attempt <= attempts; attempt++) try {
2860
+ await this.#connectOnce();
2861
+ return;
2862
+ } catch (error) {
2863
+ lastError = error instanceof Error ? error : new Error(String(error));
2864
+ if (attempt === attempts) break;
2865
+ await sleep(delay);
2866
+ delay = Math.min(delay * 2 || 1, maxDelayMs);
2867
+ }
2868
+ throw lastError;
2869
+ }
2870
+ async #connectOnce() {
2871
+ const errors = [];
2872
+ for (const backend of this.#backends) try {
2873
+ const handle = await backend.connect();
2874
+ await reconcileWatermarks(this.#db, handle.rpc.sync);
2875
+ this.#handle = handle;
2876
+ this.#shell = new WorkspaceShell(handle.rpc.shell, this);
2877
+ if (handle.closed) handle.closed.catch(() => {}).then(() => {
2878
+ if (this.#handle === handle) {
2879
+ this.#handle = void 0;
2880
+ this.#shell = void 0;
2881
+ this.#readyPromise = void 0;
2882
+ }
2883
+ });
2884
+ return;
2885
+ } catch (error) {
2886
+ errors.push({
2887
+ id: backend.id,
2888
+ error
2889
+ });
2890
+ }
2891
+ const summary = errors.map(({ id, error }) => ` - ${id}: ${error instanceof Error ? error.message : String(error)}`).join("\n");
2892
+ throw new Error(`Workspace: no backend reachable\n${summary}`);
2893
+ }
2894
+ };
2895
+ function sleep(ms) {
2896
+ if (ms <= 0) return Promise.resolve();
2897
+ return new Promise((resolve) => setTimeout(resolve, ms));
2898
+ }
2899
+ //#endregion
2900
+ export { CloudflareContainerBackend, R2Bucket, SQLiteWorkspaceProvider, TestBackend, Workspace, WorkspaceContainerAPI, WorkspaceExecHandleStub, WorkspaceFilesystemStub, WorkspaceProxy, WorkspaceShell, WorkspaceShellStub, WorkspaceStub, withWorkspaceContainer };
2901
+
2902
+ //# sourceMappingURL=index.js.map