@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/README.md +98 -0
- package/dist/bin/wsd-linux-x64 +0 -0
- package/dist/git.d.ts +119 -0
- package/dist/git.js +271 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.js +2902 -0
- package/dist/index.js.map +1 -0
- package/dist/shared-Dj4r_9xb.js +737 -0
- package/dist/shared-Dj4r_9xb.js.map +1 -0
- package/dist/shared-RIdME5uo.d.ts +302 -0
- package/package.json +72 -0
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
|