@tinycloud/replica 0.2.4-beta.3 → 0.3.0-beta.5
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 +29 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +7 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +20 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/dist/kv-storage-testing.d.ts +83 -0
- package/dist/kv-storage-testing.js +1145 -0
- package/dist/kv-storage-testing.js.map +1 -0
- package/dist/kv-storage.d.ts +155 -0
- package/dist/kv-storage.js +2587 -0
- package/dist/kv-storage.js.map +1 -0
- package/dist/kv-storage.worker.67e46cebbda277040312a770449123633491901d6cceacea95754a843e01e9c9.js +18996 -0
- package/dist/kv-storage.worker.d.ts +2 -0
- package/dist/kv-storage.worker.js +18996 -0
- package/dist/kv-storage.worker.js.map +1 -0
- package/dist/profile-gate.cjs +560 -0
- package/dist/profile-gate.cjs.map +1 -0
- package/dist/profile-gate.d.cts +89 -0
- package/dist/profile-gate.d.ts +89 -0
- package/dist/profile-gate.js +517 -0
- package/dist/profile-gate.js.map +1 -0
- package/dist/replica.worker.js +673 -295
- package/dist/replica.worker.js.map +1 -1
- package/dist/sqlite.cjs +734 -174
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +77 -8
- package/dist/sqlite.d.ts +77 -8
- package/dist/sqlite.js +710 -152
- package/dist/sqlite.js.map +1 -1
- package/dist/{types-f3-kYLQZ.d.cts → types-CQ61VnsP.d.cts} +1 -1
- package/dist/{types-f3-kYLQZ.d.ts → types-CQ61VnsP.d.ts} +1 -1
- package/package.json +20 -3
package/dist/sqlite.cjs
CHANGED
|
@@ -22,16 +22,19 @@ var sqlite_exports = {};
|
|
|
22
22
|
__export(sqlite_exports, {
|
|
23
23
|
DURABILITY: () => DURABILITY,
|
|
24
24
|
SqliteReplicaStore: () => SqliteReplicaStore,
|
|
25
|
+
inspectSqliteReplicaLayout: () => inspectSqliteReplicaLayout,
|
|
25
26
|
loadSqlite: () => loadSqlite,
|
|
26
|
-
nodeSqliteSupported: () => nodeSqliteSupported
|
|
27
|
+
nodeSqliteSupported: () => nodeSqliteSupported,
|
|
28
|
+
removeSqliteReplicaLayout: () => removeSqliteReplicaLayout
|
|
27
29
|
});
|
|
28
30
|
module.exports = __toCommonJS(sqlite_exports);
|
|
29
31
|
|
|
30
32
|
// src/sqlite/store.ts
|
|
33
|
+
var import_node_async_hooks = require("async_hooks");
|
|
31
34
|
var import_node_crypto = require("crypto");
|
|
32
|
-
var
|
|
33
|
-
var
|
|
34
|
-
var
|
|
35
|
+
var import_node_fs2 = require("fs");
|
|
36
|
+
var import_promises2 = require("fs/promises");
|
|
37
|
+
var import_node_path2 = require("path");
|
|
35
38
|
|
|
36
39
|
// src/errors.ts
|
|
37
40
|
var ReplicaErrorCode = {
|
|
@@ -89,7 +92,13 @@ var ReplicaErrorCode = {
|
|
|
89
92
|
/** A stored blob no longer hashes to its name. */
|
|
90
93
|
INTEGRITY_ERROR: "INTEGRITY_ERROR",
|
|
91
94
|
/** The replica handle was closed; in-flight calls settle with this code. */
|
|
92
|
-
CLOSED: "REPLICA_CLOSED"
|
|
95
|
+
CLOSED: "REPLICA_CLOSED",
|
|
96
|
+
/** The lifecycle gate refused the operation (shared contract `REPLICA_GATED`); `detail.gate` says why. */
|
|
97
|
+
GATED: "REPLICA_GATED",
|
|
98
|
+
/** Quota admission refused a page (`maxBytes` or the free-space floor); nothing committed. */
|
|
99
|
+
STORAGE_BUDGET: "STORAGE_BUDGET",
|
|
100
|
+
/** The consent deadline passed: the grant is not served until the user consents again. */
|
|
101
|
+
CONSENT_EXPIRED: "CONSENT_EXPIRED"
|
|
93
102
|
};
|
|
94
103
|
var REPLICA_ERROR = /* @__PURE__ */ Symbol.for("tinycloud.replica.error");
|
|
95
104
|
var ReplicaError = class extends Error {
|
|
@@ -226,32 +235,13 @@ async function loadSqlite() {
|
|
|
226
235
|
return (path, options) => wrap(options?.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path));
|
|
227
236
|
}
|
|
228
237
|
|
|
229
|
-
// src/sqlite/
|
|
230
|
-
var
|
|
231
|
-
var
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
release = resolve2;
|
|
237
|
-
});
|
|
238
|
-
await previous;
|
|
239
|
-
return release;
|
|
240
|
-
}
|
|
241
|
-
async function captureNamedPath(path) {
|
|
242
|
-
const names = [];
|
|
243
|
-
for (let current = (0, import_node_path.resolve)(path); ; current = (0, import_node_path.dirname)(current)) {
|
|
244
|
-
names.push(current);
|
|
245
|
-
if ((0, import_node_path.dirname)(current) === current) break;
|
|
246
|
-
}
|
|
247
|
-
const identities = [];
|
|
248
|
-
for (const name of names.reverse()) {
|
|
249
|
-
const stats = await (0, import_promises.lstat)(name, { bigint: true });
|
|
250
|
-
identities.push({ path: name, dev: stats.dev, ino: stats.ino });
|
|
251
|
-
}
|
|
252
|
-
return identities;
|
|
253
|
-
}
|
|
254
|
-
var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
|
|
238
|
+
// src/sqlite/layout.ts
|
|
239
|
+
var import_node_fs = require("fs");
|
|
240
|
+
var import_promises = require("fs/promises");
|
|
241
|
+
var import_node_path = require("path");
|
|
242
|
+
|
|
243
|
+
// src/sqlite/schema.ts
|
|
244
|
+
var SCHEMA_VERSION = 2;
|
|
255
245
|
var SCHEMA = `
|
|
256
246
|
CREATE TABLE IF NOT EXISTS replica (
|
|
257
247
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
@@ -311,7 +301,403 @@ CREATE TABLE IF NOT EXISTS entry (
|
|
|
311
301
|
)
|
|
312
302
|
) STRICT, WITHOUT ROWID;
|
|
313
303
|
CREATE INDEX IF NOT EXISTS entry_hash ON entry (hash) WHERE hash IS NOT NULL;
|
|
304
|
+
CREATE TABLE IF NOT EXISTS grant_install (
|
|
305
|
+
cid TEXT PRIMARY KEY,
|
|
306
|
+
installed_at INTEGER NOT NULL,
|
|
307
|
+
effective_expires_at INTEGER NOT NULL,
|
|
308
|
+
consent_deadline INTEGER NOT NULL,
|
|
309
|
+
consent_anchor_at INTEGER NOT NULL,
|
|
310
|
+
consent_anchor_source TEXT NOT NULL
|
|
311
|
+
) STRICT, WITHOUT ROWID;
|
|
314
312
|
`;
|
|
313
|
+
|
|
314
|
+
// src/sqlite/layout.ts
|
|
315
|
+
var POLICY_COLUMNS = [
|
|
316
|
+
"grant_installed_at REAL",
|
|
317
|
+
"grant_effective_expires_at REAL",
|
|
318
|
+
"grant_consent_deadline REAL",
|
|
319
|
+
"grant_consent_anchor_at REAL",
|
|
320
|
+
"grant_consent_anchor_source TEXT",
|
|
321
|
+
"pending_grant_installed_at REAL",
|
|
322
|
+
"pending_grant_effective_expires_at REAL",
|
|
323
|
+
"pending_grant_consent_deadline REAL",
|
|
324
|
+
"pending_grant_consent_anchor_at REAL",
|
|
325
|
+
"pending_grant_consent_anchor_source TEXT"
|
|
326
|
+
];
|
|
327
|
+
var normalizeSchema = (sql) => sql.replace(" IF NOT EXISTS", "").replace(/\s+/g, " ").trim();
|
|
328
|
+
var SOURCE_SCHEMA = SCHEMA.trim().split(";").map((sql) => sql.trim()).filter(Boolean).map((sql) => {
|
|
329
|
+
const match = /^CREATE (TABLE|INDEX) IF NOT EXISTS ([a-z_]+)/.exec(sql);
|
|
330
|
+
if (match === null) throw new Error("Invalid source replica schema.");
|
|
331
|
+
return { type: match[1].toLowerCase(), name: match[2], sql: normalizeSchema(sql) };
|
|
332
|
+
});
|
|
333
|
+
var V1_SCHEMA = SOURCE_SCHEMA.filter((entry) => entry.name !== "grant_install");
|
|
334
|
+
var POLICY_REPLICA = V1_SCHEMA.find((entry) => entry.name === "replica").sql.replace(/\) STRICT$/, `, ${POLICY_COLUMNS.join(", ")}) STRICT`);
|
|
335
|
+
var POLICY_SCHEMA = V1_SCHEMA.map((entry) => entry.name === "replica" ? { ...entry, sql: POLICY_REPLICA } : entry);
|
|
336
|
+
async function sourceMirrorSchema(path) {
|
|
337
|
+
let db;
|
|
338
|
+
try {
|
|
339
|
+
db = (await loadSqlite())(path, { readonly: true });
|
|
340
|
+
db.exec("BEGIN");
|
|
341
|
+
const version = db.get("PRAGMA user_version")?.user_version;
|
|
342
|
+
if (version !== 1 && version !== SCHEMA_VERSION) return false;
|
|
343
|
+
const rows = db.all(
|
|
344
|
+
"SELECT type, name, sql FROM sqlite_schema WHERE sql IS NOT NULL ORDER BY name"
|
|
345
|
+
);
|
|
346
|
+
const expected = version === 1 ? V1_SCHEMA : SOURCE_SCHEMA;
|
|
347
|
+
const matches = (schema) => rows.length === schema.length && rows.every((row) => {
|
|
348
|
+
const source = schema.find((entry) => entry.name === row.name);
|
|
349
|
+
return source?.type === row.type && source.sql === normalizeSchema(row.sql);
|
|
350
|
+
});
|
|
351
|
+
if (matches(expected)) return true;
|
|
352
|
+
if (version !== 2) return false;
|
|
353
|
+
return matches(POLICY_SCHEMA);
|
|
354
|
+
} catch {
|
|
355
|
+
return false;
|
|
356
|
+
} finally {
|
|
357
|
+
try {
|
|
358
|
+
db?.close();
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
var SQLITE_FILE = /^(?:replica\.db|replica\.db-(?:wal|shm|journal))$/;
|
|
364
|
+
var SHARD = /^[0-9a-f]{2}$/;
|
|
365
|
+
var HASH = /^[0-9a-f]{64}$/;
|
|
366
|
+
var TEMP = /^[0-9a-f]{64}\.[0-9a-f]{12}$/;
|
|
367
|
+
function sourceKind(parent, name, allowAdapterFence) {
|
|
368
|
+
if (parent === "") {
|
|
369
|
+
if (SQLITE_FILE.test(name) || allowAdapterFence && (name === "fence.json" || /^fence[.]json[.][0-9a-f]{12}[.]tmp$/.test(name))) return "file";
|
|
370
|
+
return name === "blobs" ? "directory" : "unknown";
|
|
371
|
+
}
|
|
372
|
+
if (parent === "blobs") return name === ".tmp" || SHARD.test(name) ? "directory" : "unknown";
|
|
373
|
+
if (parent === "blobs/.tmp") return TEMP.test(name) ? "file" : "unknown";
|
|
374
|
+
if (parent.startsWith("blobs/") && SHARD.test(parent.slice(6))) {
|
|
375
|
+
return HASH.test(name) && name.startsWith(parent.slice(6)) ? "file" : "unknown";
|
|
376
|
+
}
|
|
377
|
+
return "unknown";
|
|
378
|
+
}
|
|
379
|
+
function sameFile(a, b) {
|
|
380
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
381
|
+
}
|
|
382
|
+
function identityOf(path, kind, details) {
|
|
383
|
+
return { path, kind, dev: details.dev.toString(), ino: details.ino.toString() };
|
|
384
|
+
}
|
|
385
|
+
function fileKind(details) {
|
|
386
|
+
if (details.isFile()) return "file";
|
|
387
|
+
if (details.isDirectory()) return "directory";
|
|
388
|
+
if (details.isSymbolicLink()) return "symlink";
|
|
389
|
+
throw new Error("Unsupported replica entry type.");
|
|
390
|
+
}
|
|
391
|
+
async function pathComponents(path) {
|
|
392
|
+
const absolute = (0, import_node_path.resolve)(path);
|
|
393
|
+
const parts = absolute.slice((0, import_node_path.parse)(absolute).root.length).split(import_node_path.sep).filter(Boolean);
|
|
394
|
+
let current = (0, import_node_path.parse)(absolute).root;
|
|
395
|
+
const components = [];
|
|
396
|
+
for (const part of ["", ...parts]) {
|
|
397
|
+
if (part) current = (0, import_node_path.join)(current, part);
|
|
398
|
+
const details = await (0, import_promises.lstat)(current, { bigint: true });
|
|
399
|
+
if (details.isSymbolicLink()) throw new Error(`Symlink in replica path: ${current}`);
|
|
400
|
+
if (!details.isDirectory()) throw new Error(`Replica path component is not a directory: ${current}`);
|
|
401
|
+
components.push({ path: current, identity: details });
|
|
402
|
+
}
|
|
403
|
+
return components;
|
|
404
|
+
}
|
|
405
|
+
async function pinnedDirectory(path, parent, relative = "") {
|
|
406
|
+
const components = process.platform === "linux" ? void 0 : parent?.components ?? await pathComponents(path);
|
|
407
|
+
const before = await (0, import_promises.lstat)(path, { bigint: true });
|
|
408
|
+
if (before.isSymbolicLink()) throw new Error(`Symlink in replica path: ${path}`);
|
|
409
|
+
if (!before.isDirectory()) throw new Error("Replica directory is not a real directory.");
|
|
410
|
+
const handle = await (0, import_promises.open)(path, import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_DIRECTORY | import_node_fs.constants.O_NOFOLLOW);
|
|
411
|
+
try {
|
|
412
|
+
if (!sameFile(before, await handle.stat({ bigint: true }))) {
|
|
413
|
+
throw new Error("Replica directory changed during inspection.");
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
handle,
|
|
417
|
+
identity: before,
|
|
418
|
+
parent,
|
|
419
|
+
components,
|
|
420
|
+
children: [],
|
|
421
|
+
relative,
|
|
422
|
+
name: path,
|
|
423
|
+
path: process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : path
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
await handle.close();
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function checkDirectory(directory) {
|
|
431
|
+
const details = await (0, import_promises.lstat)(directory.name, { bigint: true });
|
|
432
|
+
if (!details.isDirectory() || !sameFile(directory.identity, details)) {
|
|
433
|
+
throw new Error(`Replica directory changed: ${directory.relative || directory.name}`);
|
|
434
|
+
}
|
|
435
|
+
if (directory.parent) await checkDirectory(directory.parent);
|
|
436
|
+
else if (directory.components) {
|
|
437
|
+
for (const component of directory.components) {
|
|
438
|
+
const current = await (0, import_promises.lstat)(component.path, { bigint: true });
|
|
439
|
+
if (current.isSymbolicLink() || !current.isDirectory() || !sameFile(component.identity, current)) {
|
|
440
|
+
throw new Error(`Replica path component changed: ${component.path}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async function inspect(dir, allowAdapterFence) {
|
|
446
|
+
const root = await pinnedDirectory(dir);
|
|
447
|
+
const directories = [root];
|
|
448
|
+
const unknownPaths = [];
|
|
449
|
+
const knownEntries = [];
|
|
450
|
+
const unknownEntries = [];
|
|
451
|
+
let hasDatabase = false;
|
|
452
|
+
try {
|
|
453
|
+
async function walk(parent) {
|
|
454
|
+
await checkDirectory(parent);
|
|
455
|
+
for (const name of (await (0, import_promises.readdir)(parent.path)).sort()) {
|
|
456
|
+
await checkDirectory(parent);
|
|
457
|
+
const relative = parent.relative ? `${parent.relative}/${name}` : name;
|
|
458
|
+
const path = (0, import_node_path.join)(parent.path, name);
|
|
459
|
+
const details = await (0, import_promises.lstat)(path, { bigint: true });
|
|
460
|
+
if (process.platform !== "linux" && details.isSymbolicLink()) {
|
|
461
|
+
throw new Error(`Symlink in replica path: ${relative}`);
|
|
462
|
+
}
|
|
463
|
+
const kind = fileKind(details);
|
|
464
|
+
const child = { path, relative, kind, identity: details, parent };
|
|
465
|
+
parent.children.push(child);
|
|
466
|
+
if (parent.relative === "" && name === "replica.db" && kind === "file") hasDatabase = true;
|
|
467
|
+
if (sourceKind(parent.relative, name, allowAdapterFence) !== kind) {
|
|
468
|
+
unknownPaths.push(relative);
|
|
469
|
+
unknownEntries.push(identityOf(relative, kind, details));
|
|
470
|
+
} else {
|
|
471
|
+
knownEntries.push(identityOf(relative, kind, details));
|
|
472
|
+
}
|
|
473
|
+
if (kind === "directory") {
|
|
474
|
+
const directory = await pinnedDirectory(path, parent, relative);
|
|
475
|
+
if (!sameFile(details, directory.identity)) {
|
|
476
|
+
await directory.handle.close();
|
|
477
|
+
throw new Error(`Replica directory changed during inspection: ${relative}`);
|
|
478
|
+
}
|
|
479
|
+
child.directory = directory;
|
|
480
|
+
directories.push(directory);
|
|
481
|
+
await walk(directory);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
await walk(root);
|
|
486
|
+
if (!hasDatabase) {
|
|
487
|
+
unknownPaths.push("replica.db");
|
|
488
|
+
} else if (!await sourceMirrorSchema((0, import_node_path.join)(root.path, "replica.db"))) {
|
|
489
|
+
const index = knownEntries.findIndex((entry) => entry.path === "replica.db");
|
|
490
|
+
if (index >= 0) unknownEntries.push(knownEntries.splice(index, 1)[0]);
|
|
491
|
+
unknownPaths.push("replica.db");
|
|
492
|
+
}
|
|
493
|
+
await checkDirectory(root);
|
|
494
|
+
const names = new Set(await (0, import_promises.readdir)(root.path));
|
|
495
|
+
for (let index = root.children.length - 1; index >= 0; index--) {
|
|
496
|
+
const child = root.children[index];
|
|
497
|
+
if (!names.has(child.relative)) {
|
|
498
|
+
if (child.directory) throw new Error(`Replica directory vanished: ${child.relative}`);
|
|
499
|
+
root.children.splice(index, 1);
|
|
500
|
+
for (const list of [knownEntries, unknownEntries]) {
|
|
501
|
+
const entryIndex = list.findIndex((entry) => entry.path === child.relative);
|
|
502
|
+
if (entryIndex >= 0) list.splice(entryIndex, 1);
|
|
503
|
+
}
|
|
504
|
+
const pathIndex = unknownPaths.indexOf(child.relative);
|
|
505
|
+
if (pathIndex >= 0) unknownPaths.splice(pathIndex, 1);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const current = await (0, import_promises.lstat)(child.path, { bigint: true });
|
|
509
|
+
if (!sameFile(child.identity, current) || fileKind(current) !== child.kind) {
|
|
510
|
+
throw new Error(`Replica child changed during schema inspection: ${child.relative}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
for (const name of names) {
|
|
514
|
+
if (root.children.some((child) => child.relative === name)) continue;
|
|
515
|
+
const path = (0, import_node_path.join)(root.path, name);
|
|
516
|
+
const details = await (0, import_promises.lstat)(path, { bigint: true });
|
|
517
|
+
if (!/^replica\.db-(?:wal|shm|journal)$/.test(name) || !details.isFile()) {
|
|
518
|
+
throw new Error(`Unexpected replica child during schema inspection: ${name}`);
|
|
519
|
+
}
|
|
520
|
+
root.children.push({ path, relative: name, kind: "file", identity: details, parent: root });
|
|
521
|
+
knownEntries.push(identityOf(name, "file", details));
|
|
522
|
+
}
|
|
523
|
+
return { root, directories, knownEntries, unknownPaths, unknownEntries };
|
|
524
|
+
} catch (error) {
|
|
525
|
+
await closeDirectories(directories);
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function closeDirectories(directories) {
|
|
530
|
+
for (const directory of directories.reverse()) await directory.handle.close();
|
|
531
|
+
}
|
|
532
|
+
async function inspectSqliteReplicaLayout(dir, options = {}) {
|
|
533
|
+
const inventory = await inspect(dir, options.allowAdapterFence === true);
|
|
534
|
+
try {
|
|
535
|
+
return {
|
|
536
|
+
knownEntries: inventory.knownEntries,
|
|
537
|
+
unknownPaths: inventory.unknownPaths,
|
|
538
|
+
unknownEntries: inventory.unknownEntries,
|
|
539
|
+
rootIdentity: { dev: inventory.root.identity.dev.toString(), ino: inventory.root.identity.ino.toString() }
|
|
540
|
+
};
|
|
541
|
+
} finally {
|
|
542
|
+
await closeDirectories(inventory.directories);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function approvedEntries(entries) {
|
|
546
|
+
const approved = /* @__PURE__ */ new Map();
|
|
547
|
+
for (const entry of entries) {
|
|
548
|
+
if (typeof entry !== "object" || entry === null || Object.keys(entry).sort().join(",") !== "dev,ino,kind,path" || typeof entry.path !== "string" || !entry.path || entry.path.split("/").some((part) => !part || part === "." || part === ".." || part.includes("\\")) || !["file", "directory", "symlink"].includes(entry.kind) || typeof entry.dev !== "string" || !/^(0|[1-9][0-9]*)$/.test(entry.dev) || typeof entry.ino !== "string" || !/^(0|[1-9][0-9]*)$/.test(entry.ino) || approved.has(entry.path)) {
|
|
549
|
+
throw new Error("Invalid or duplicate replica approval.");
|
|
550
|
+
}
|
|
551
|
+
approved.set(entry.path, entry);
|
|
552
|
+
}
|
|
553
|
+
return approved;
|
|
554
|
+
}
|
|
555
|
+
var LAYOUT_FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.layout-faults");
|
|
556
|
+
async function removeSqliteReplicaLayout(dir, options = {}) {
|
|
557
|
+
const inventory = await inspect(dir, options.allowAdapterFence === true);
|
|
558
|
+
try {
|
|
559
|
+
if (options.expectedRoot && (options.expectedRoot.dev !== inventory.root.identity.dev.toString() || options.expectedRoot.ino !== inventory.root.identity.ino.toString())) {
|
|
560
|
+
throw new Error("Replica root identity changed since approval.");
|
|
561
|
+
}
|
|
562
|
+
const approved = approvedEntries(options.approvedUnknown ?? []);
|
|
563
|
+
for (const entry of inventory.unknownEntries) {
|
|
564
|
+
const match = approved.get(entry.path);
|
|
565
|
+
if (!match || match.kind !== entry.kind || match.dev !== entry.dev || match.ino !== entry.ino) {
|
|
566
|
+
throw new Error(`Unknown replica child or approval identity mismatch: ${entry.path}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (options.expectedKnown) {
|
|
570
|
+
const expected = approvedEntries(options.expectedKnown);
|
|
571
|
+
for (const entry of inventory.knownEntries) {
|
|
572
|
+
const match = expected.get(entry.path);
|
|
573
|
+
if (!match || match.kind !== entry.kind || match.dev !== entry.dev || match.ino !== entry.ino) {
|
|
574
|
+
throw new Error(`Known replica child changed since preflight: ${entry.path}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (options.allowMissingDatabase && inventory.knownEntries.some((entry) => entry.path === "replica.db")) {
|
|
579
|
+
throw new Error("Replica database appeared after approval of its absence.");
|
|
580
|
+
}
|
|
581
|
+
if (inventory.unknownPaths.length !== inventory.unknownEntries.length && inventory.root.children.length > 0 && !options.allowMissingDatabase) {
|
|
582
|
+
throw new Error("Replica database is missing; cannot approve removal.");
|
|
583
|
+
}
|
|
584
|
+
async function removeDirectory(directory) {
|
|
585
|
+
for (const child of directory.children.sort((a, b) => a.relative === "replica.db" ? 1 : b.relative === "replica.db" ? -1 : 0)) {
|
|
586
|
+
if (child.directory) {
|
|
587
|
+
await removeDirectory(child.directory);
|
|
588
|
+
}
|
|
589
|
+
await options[LAYOUT_FAULTS]?.beforeUnlink?.(child.relative);
|
|
590
|
+
await checkDirectory(directory);
|
|
591
|
+
const details = await (0, import_promises.lstat)(child.path, { bigint: true });
|
|
592
|
+
if (process.platform !== "linux" && details.isSymbolicLink()) {
|
|
593
|
+
throw new Error(`Symlink in replica path: ${child.relative}`);
|
|
594
|
+
}
|
|
595
|
+
if (fileKind(details) !== child.kind || !sameFile(child.identity, details)) {
|
|
596
|
+
throw new Error(`Replica child identity changed: ${child.relative}`);
|
|
597
|
+
}
|
|
598
|
+
if (child.directory) await (0, import_promises.rmdir)(child.path);
|
|
599
|
+
else await (0, import_promises.unlink)(child.path);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
await removeDirectory(inventory.root);
|
|
603
|
+
await checkDirectory(inventory.root);
|
|
604
|
+
await (0, import_promises.rmdir)(inventory.root.name);
|
|
605
|
+
} finally {
|
|
606
|
+
await closeDirectories(inventory.directories);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/sqlite/store.ts
|
|
611
|
+
var sqliteOpenQueue = Promise.resolve();
|
|
612
|
+
async function acquireSqliteOpen() {
|
|
613
|
+
const previous = sqliteOpenQueue;
|
|
614
|
+
let release;
|
|
615
|
+
sqliteOpenQueue = new Promise((resolve3) => {
|
|
616
|
+
release = resolve3;
|
|
617
|
+
});
|
|
618
|
+
await previous;
|
|
619
|
+
return release;
|
|
620
|
+
}
|
|
621
|
+
async function captureNamedPath(path) {
|
|
622
|
+
const names = [];
|
|
623
|
+
for (let current = (0, import_node_path2.resolve)(path); ; current = (0, import_node_path2.dirname)(current)) {
|
|
624
|
+
names.push(current);
|
|
625
|
+
if ((0, import_node_path2.dirname)(current) === current) break;
|
|
626
|
+
}
|
|
627
|
+
const identities = [];
|
|
628
|
+
for (const name of names.reverse()) {
|
|
629
|
+
const stats = await (0, import_promises2.lstat)(name, { bigint: true });
|
|
630
|
+
identities.push({ path: name, dev: stats.dev, ino: stats.ino });
|
|
631
|
+
}
|
|
632
|
+
return identities;
|
|
633
|
+
}
|
|
634
|
+
async function openVerifiedDatabase(path, opener, options = {}) {
|
|
635
|
+
const leaf = await (0, import_promises2.open)(path, import_node_fs2.constants.O_RDONLY | import_node_fs2.constants.O_NOFOLLOW);
|
|
636
|
+
let db;
|
|
637
|
+
try {
|
|
638
|
+
const identity = await leaf.stat();
|
|
639
|
+
const named = await (0, import_promises2.lstat)(path);
|
|
640
|
+
if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino || options.expected !== void 0 && (options.expected.dev !== identity.dev || options.expected.ino !== identity.ino)) {
|
|
641
|
+
throw new Error("The replica database changed before SQLite opened it.");
|
|
642
|
+
}
|
|
643
|
+
const previousFds = process.platform === "linux" && options.discoverDescriptors !== false ? new Map((0, import_node_fs2.readdirSync)("/proc/self/fd").map((fd) => {
|
|
644
|
+
try {
|
|
645
|
+
return [fd, (0, import_node_fs2.readlinkSync)(`/proc/self/fd/${fd}`)];
|
|
646
|
+
} catch {
|
|
647
|
+
return [fd, ""];
|
|
648
|
+
}
|
|
649
|
+
})) : null;
|
|
650
|
+
db = options.readonly ? opener(path, { readonly: true }) : opener(path);
|
|
651
|
+
const afterOpen = (0, import_node_fs2.lstatSync)(path);
|
|
652
|
+
if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
|
|
653
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
654
|
+
}
|
|
655
|
+
if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
|
|
656
|
+
if (previousFds) {
|
|
657
|
+
const databases = (0, import_node_fs2.readdirSync)("/proc/self/fd").filter((fd) => {
|
|
658
|
+
try {
|
|
659
|
+
const target = (0, import_node_fs2.readlinkSync)(`/proc/self/fd/${fd}`);
|
|
660
|
+
return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
|
|
661
|
+
} catch {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
const sharedPager = options.readonly && databases.length === 0 && [...previousFds].some(([fd, target]) => {
|
|
666
|
+
if (Number(fd) === leaf.fd || !target.endsWith("/replica.db")) return false;
|
|
667
|
+
try {
|
|
668
|
+
const actual = (0, import_node_fs2.fstatSync)(Number(fd));
|
|
669
|
+
return actual.dev === identity.dev && actual.ino === identity.ino;
|
|
670
|
+
} catch {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
if (!databases.length && !sharedPager || databases.some((fd) => {
|
|
675
|
+
try {
|
|
676
|
+
const actual = (0, import_node_fs2.fstatSync)(Number(fd));
|
|
677
|
+
return actual.dev !== identity.dev || actual.ino !== identity.ino;
|
|
678
|
+
} catch {
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
})) {
|
|
682
|
+
throw new Error("SQLite opened a different replica database.");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const after = (0, import_node_fs2.lstatSync)(path);
|
|
686
|
+
if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
|
|
687
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
688
|
+
}
|
|
689
|
+
const checked = db;
|
|
690
|
+
db = void 0;
|
|
691
|
+
return { db: checked, identity };
|
|
692
|
+
} finally {
|
|
693
|
+
try {
|
|
694
|
+
db?.close();
|
|
695
|
+
} finally {
|
|
696
|
+
await leaf.close();
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
|
|
315
701
|
function grantOf(cid, bytes, audience, issuer, nbf, exp) {
|
|
316
702
|
if (cid === null || bytes === null || audience === null || issuer === null) return null;
|
|
317
703
|
return { cid, bytes: new Uint8Array(bytes), audience, issuer, notBefore: nbf, expiresAt: exp };
|
|
@@ -342,7 +728,7 @@ function errnoOf(error) {
|
|
|
342
728
|
}
|
|
343
729
|
async function entriesOf(path) {
|
|
344
730
|
try {
|
|
345
|
-
return await (0,
|
|
731
|
+
return await (0, import_promises2.readdir)(path);
|
|
346
732
|
} catch (error) {
|
|
347
733
|
if (errnoOf(error) === "ENOENT") return [];
|
|
348
734
|
throw error;
|
|
@@ -351,7 +737,7 @@ async function entriesOf(path) {
|
|
|
351
737
|
var FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.faults");
|
|
352
738
|
var pinnedDirectories = new FinalizationRegistry((fd) => {
|
|
353
739
|
try {
|
|
354
|
-
(0,
|
|
740
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
355
741
|
} catch {
|
|
356
742
|
}
|
|
357
743
|
});
|
|
@@ -362,7 +748,7 @@ function releaseWritable(shared) {
|
|
|
362
748
|
try {
|
|
363
749
|
shared.db.close();
|
|
364
750
|
} finally {
|
|
365
|
-
if (shared.directoryFd !== void 0) (0,
|
|
751
|
+
if (shared.directoryFd !== void 0) (0, import_node_fs2.closeSync)(shared.directoryFd);
|
|
366
752
|
}
|
|
367
753
|
}
|
|
368
754
|
var sharedStores = new FinalizationRegistry((shared) => {
|
|
@@ -374,12 +760,13 @@ var sharedStores = new FinalizationRegistry((shared) => {
|
|
|
374
760
|
async function acquireWritableMutation(shared) {
|
|
375
761
|
const previous = shared.mutationTail;
|
|
376
762
|
let release;
|
|
377
|
-
shared.mutationTail = new Promise((
|
|
378
|
-
release =
|
|
763
|
+
shared.mutationTail = new Promise((resolve3) => {
|
|
764
|
+
release = resolve3;
|
|
379
765
|
});
|
|
380
766
|
await previous;
|
|
381
767
|
return release;
|
|
382
768
|
}
|
|
769
|
+
var activeReadSection = new import_node_async_hooks.AsyncLocalStorage();
|
|
383
770
|
var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
384
771
|
dir;
|
|
385
772
|
#database;
|
|
@@ -393,9 +780,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
393
780
|
#activeMutations = 0;
|
|
394
781
|
#drained;
|
|
395
782
|
#shared;
|
|
783
|
+
#readonlySnapshot;
|
|
396
784
|
#pinnedDir;
|
|
397
785
|
#directoryFd;
|
|
398
|
-
#holder
|
|
786
|
+
#holder;
|
|
399
787
|
#now;
|
|
400
788
|
#faults;
|
|
401
789
|
#guard;
|
|
@@ -403,11 +791,12 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
403
791
|
#ino;
|
|
404
792
|
#dev;
|
|
405
793
|
#namedAncestors;
|
|
406
|
-
constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared) {
|
|
794
|
+
constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared, readonlySnapshot = false) {
|
|
407
795
|
this.dir = dir;
|
|
408
796
|
this.#pinnedDir = shared?.pinnedDir ?? (directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir);
|
|
409
797
|
this.#directoryFd = directoryFd;
|
|
410
798
|
this.#shared = shared;
|
|
799
|
+
this.#readonlySnapshot = readonlySnapshot;
|
|
411
800
|
if (shared) {
|
|
412
801
|
shared.references++;
|
|
413
802
|
sharedStores.register(this, shared, this);
|
|
@@ -432,10 +821,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
432
821
|
if (options.signal?.aborted) throw options.signal.reason ?? new ReplicaError(ReplicaErrorCode.CLOSED, "Replica status inspection was cancelled.");
|
|
433
822
|
};
|
|
434
823
|
checkAbort();
|
|
435
|
-
const dbPath = (0,
|
|
824
|
+
const dbPath = (0, import_node_path2.join)(dir, "replica.db");
|
|
436
825
|
let identity;
|
|
437
826
|
try {
|
|
438
|
-
identity = await (0,
|
|
827
|
+
identity = await (0, import_promises2.lstat)(dbPath);
|
|
439
828
|
if (!identity.isFile()) return null;
|
|
440
829
|
} catch (error) {
|
|
441
830
|
if (errnoOf(error) === "ENOENT") return null;
|
|
@@ -445,21 +834,30 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
445
834
|
const opener = await loadSqlite();
|
|
446
835
|
checkAbort();
|
|
447
836
|
let db;
|
|
837
|
+
let releaseOpen;
|
|
448
838
|
try {
|
|
449
|
-
|
|
839
|
+
releaseOpen = await acquireSqliteOpen();
|
|
840
|
+
const checked = await openVerifiedDatabase(dbPath, opener, { readonly: true, expected: identity });
|
|
841
|
+
db = checked.db;
|
|
842
|
+
releaseOpen();
|
|
843
|
+
releaseOpen = void 0;
|
|
450
844
|
db.exec("PRAGMA busy_timeout = 0");
|
|
451
845
|
checkAbort();
|
|
452
|
-
const store = new _SqliteReplicaStore(dir, db, identity, { create: false, ...options.now ? { now: options.now } : {} });
|
|
846
|
+
const store = new _SqliteReplicaStore(dir, db, checked.identity, { create: false, ...options.now ? { now: options.now } : {} });
|
|
453
847
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
454
848
|
const before = db.get("PRAGMA data_version").data_version;
|
|
455
849
|
const state = await store.open();
|
|
456
850
|
checkAbort();
|
|
457
851
|
if (state === null) return null;
|
|
458
852
|
const status = await store.status();
|
|
459
|
-
|
|
853
|
+
const latest = state.pendingGrant ?? state.grant;
|
|
854
|
+
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
855
|
+
const grantInstall = latest === null || version < 2 ? null : await store.getGrantInstall(latest.cid);
|
|
856
|
+
const activeGrantInstall = state.grant === null || version < 2 ? null : state.grant === latest ? grantInstall : await store.getGrantInstall(state.grant.cid);
|
|
857
|
+
await new Promise((resolve3) => setImmediate(resolve3));
|
|
460
858
|
checkAbort();
|
|
461
859
|
const after = db.get("PRAGMA data_version").data_version;
|
|
462
|
-
if (before === after) return { state, status };
|
|
860
|
+
if (before === after) return { state, status, grantInstall, activeGrantInstall };
|
|
463
861
|
}
|
|
464
862
|
throw new ReplicaError(ReplicaErrorCode.BUSY, "The replica changed during status inspection.");
|
|
465
863
|
} catch (error) {
|
|
@@ -468,7 +866,15 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
468
866
|
}
|
|
469
867
|
throw storageError(error, "Inspecting the replica");
|
|
470
868
|
} finally {
|
|
471
|
-
|
|
869
|
+
releaseOpen?.();
|
|
870
|
+
if (db) {
|
|
871
|
+
const releaseClose = await acquireSqliteOpen();
|
|
872
|
+
try {
|
|
873
|
+
db.close();
|
|
874
|
+
} finally {
|
|
875
|
+
releaseClose();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
472
878
|
}
|
|
473
879
|
}
|
|
474
880
|
/**
|
|
@@ -476,9 +882,9 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
476
882
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
477
883
|
*/
|
|
478
884
|
static async open(dir, options) {
|
|
479
|
-
const namedDbPath = (0,
|
|
885
|
+
const namedDbPath = (0, import_node_path2.join)(dir, "replica.db");
|
|
480
886
|
if (!options.create) {
|
|
481
|
-
const exists = await (0,
|
|
887
|
+
const exists = await (0, import_promises2.lstat)(namedDbPath).then(
|
|
482
888
|
(stats) => stats.isFile(),
|
|
483
889
|
() => false
|
|
484
890
|
);
|
|
@@ -488,11 +894,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
488
894
|
let store;
|
|
489
895
|
let directoryFd;
|
|
490
896
|
try {
|
|
491
|
-
if (options.create) await (0,
|
|
897
|
+
if (options.create) await (0, import_promises2.mkdir)(dir, { recursive: true, mode: 448 });
|
|
492
898
|
if (process.platform === "linux") {
|
|
493
|
-
directoryFd = (0,
|
|
899
|
+
directoryFd = (0, import_node_fs2.openSync)(dir, import_node_fs2.constants.O_RDONLY | import_node_fs2.constants.O_DIRECTORY | import_node_fs2.constants.O_NOFOLLOW);
|
|
494
900
|
}
|
|
495
|
-
const opened = directoryFd === void 0 ? (0,
|
|
901
|
+
const opened = directoryFd === void 0 ? (0, import_node_fs2.lstatSync)(dir, { bigint: true }) : (0, import_node_fs2.fstatSync)(directoryFd, { bigint: true });
|
|
496
902
|
if (!opened.isDirectory() || opened.isSymbolicLink() || options.expectedDirectory && (opened.dev !== BigInt(options.expectedDirectory.dev) || opened.ino !== BigInt(options.expectedDirectory.ino))) {
|
|
497
903
|
throw new Error("The replica directory changed before SQLite opened it.");
|
|
498
904
|
}
|
|
@@ -501,11 +907,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
501
907
|
throw new Error("The replica directory changed while checking its ancestry.");
|
|
502
908
|
}
|
|
503
909
|
const pinnedDir = directoryFd === void 0 ? dir : `/proc/self/fd/${directoryFd}`;
|
|
504
|
-
const dbPath = (0,
|
|
910
|
+
const dbPath = (0, import_node_path2.join)(pinnedDir, "replica.db");
|
|
505
911
|
if (options.create) {
|
|
506
|
-
await (0,
|
|
507
|
-
await Promise.all([(0,
|
|
508
|
-
const handle = await (0,
|
|
912
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.join)(pinnedDir, "blobs", ".tmp"), { recursive: true, mode: 448 });
|
|
913
|
+
await Promise.all([(0, import_promises2.chmod)(pinnedDir, 448), (0, import_promises2.chmod)((0, import_node_path2.join)(pinnedDir, "blobs"), 448), (0, import_promises2.chmod)((0, import_node_path2.join)(pinnedDir, "blobs", ".tmp"), 448)]);
|
|
914
|
+
const handle = await (0, import_promises2.open)(dbPath, import_node_fs2.constants.O_CREAT | import_node_fs2.constants.O_EXCL | import_node_fs2.constants.O_RDWR | import_node_fs2.constants.O_NOFOLLOW, 384).catch((error) => {
|
|
509
915
|
if (errnoOf(error) === "EEXIST") return null;
|
|
510
916
|
throw error;
|
|
511
917
|
});
|
|
@@ -517,11 +923,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
517
923
|
}
|
|
518
924
|
}
|
|
519
925
|
}
|
|
520
|
-
const leaf = await (0,
|
|
926
|
+
const leaf = await (0, import_promises2.open)(dbPath, import_node_fs2.constants.O_RDONLY | import_node_fs2.constants.O_NOFOLLOW);
|
|
521
927
|
let db;
|
|
522
928
|
try {
|
|
523
929
|
const identity = await leaf.stat();
|
|
524
|
-
const named = await (0,
|
|
930
|
+
const named = await (0, import_promises2.lstat)(dbPath);
|
|
525
931
|
if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
|
|
526
932
|
throw new Error("The replica database changed before SQLite opened it.");
|
|
527
933
|
}
|
|
@@ -531,40 +937,40 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
531
937
|
throw new Error("The replica database changed while another controller holds it open.");
|
|
532
938
|
}
|
|
533
939
|
if (active && !options.sqlite) {
|
|
534
|
-
const namedDirectory = (0,
|
|
940
|
+
const namedDirectory = (0, import_node_fs2.lstatSync)(dir, { bigint: true });
|
|
535
941
|
if (!namedDirectory.isDirectory() || namedDirectory.dev !== opened.dev || namedDirectory.ino !== opened.ino) {
|
|
536
942
|
throw new Error("The replica directory changed before its checked SQLite handle could be shared.");
|
|
537
943
|
}
|
|
538
|
-
const pinned = active.directoryFd === void 0 ? (0,
|
|
539
|
-
const activeLeaf = (0,
|
|
944
|
+
const pinned = active.directoryFd === void 0 ? (0, import_node_fs2.lstatSync)(active.pinnedDir, { bigint: true }) : (0, import_node_fs2.fstatSync)(active.directoryFd, { bigint: true });
|
|
945
|
+
const activeLeaf = (0, import_node_fs2.lstatSync)((0, import_node_path2.join)(active.pinnedDir, "replica.db"));
|
|
540
946
|
if (!pinned.isDirectory() || pinned.dev !== opened.dev || pinned.ino !== opened.ino || !activeLeaf.isFile() || activeLeaf.dev !== identity.dev || activeLeaf.ino !== identity.ino || active.directoryFd === void 0 && active.namedPath !== dir) {
|
|
541
947
|
throw new Error("The live replica changed before its checked SQLite handle could be shared.");
|
|
542
948
|
}
|
|
543
949
|
if (directoryFd !== void 0) {
|
|
544
|
-
(0,
|
|
950
|
+
(0, import_node_fs2.closeSync)(directoryFd);
|
|
545
951
|
directoryFd = void 0;
|
|
546
952
|
}
|
|
547
953
|
store = new _SqliteReplicaStore(dir, active.db, identity, options, active.directoryFd, namedAncestors, active);
|
|
548
954
|
}
|
|
549
955
|
if (!store) {
|
|
550
956
|
const opener = options.sqlite ?? await loadSqlite();
|
|
551
|
-
const previousFds = process.platform === "linux" && !options.sqlite ? new Map((0,
|
|
957
|
+
const previousFds = process.platform === "linux" && !options.sqlite ? new Map((0, import_node_fs2.readdirSync)("/proc/self/fd").map((fd) => {
|
|
552
958
|
try {
|
|
553
|
-
return [fd, (0,
|
|
959
|
+
return [fd, (0, import_node_fs2.readlinkSync)(`/proc/self/fd/${fd}`)];
|
|
554
960
|
} catch {
|
|
555
961
|
return [fd, ""];
|
|
556
962
|
}
|
|
557
963
|
})) : null;
|
|
558
964
|
db = opener(dbPath);
|
|
559
|
-
const afterOpen = (0,
|
|
965
|
+
const afterOpen = (0, import_node_fs2.lstatSync)(dbPath);
|
|
560
966
|
if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
|
|
561
967
|
throw new Error("The replica database changed while SQLite opened it.");
|
|
562
968
|
}
|
|
563
969
|
if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
|
|
564
970
|
if (previousFds) {
|
|
565
|
-
const databases = (0,
|
|
971
|
+
const databases = (0, import_node_fs2.readdirSync)("/proc/self/fd").filter((fd) => {
|
|
566
972
|
try {
|
|
567
|
-
const target = (0,
|
|
973
|
+
const target = (0, import_node_fs2.readlinkSync)(`/proc/self/fd/${fd}`);
|
|
568
974
|
return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
|
|
569
975
|
} catch {
|
|
570
976
|
return false;
|
|
@@ -572,7 +978,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
572
978
|
});
|
|
573
979
|
if (databases.length === 0 || databases.some((fd) => {
|
|
574
980
|
try {
|
|
575
|
-
const actual = (0,
|
|
981
|
+
const actual = (0, import_node_fs2.fstatSync)(Number(fd));
|
|
576
982
|
return actual.dev !== identity.dev || actual.ino !== identity.ino;
|
|
577
983
|
} catch {
|
|
578
984
|
return true;
|
|
@@ -581,7 +987,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
581
987
|
throw new Error("SQLite opened a different replica database.");
|
|
582
988
|
}
|
|
583
989
|
}
|
|
584
|
-
const after = (0,
|
|
990
|
+
const after = (0, import_node_fs2.lstatSync)(dbPath);
|
|
585
991
|
if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
|
|
586
992
|
throw new Error("The replica database changed while SQLite opened it.");
|
|
587
993
|
}
|
|
@@ -603,7 +1009,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
603
1009
|
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
604
1010
|
db.exec("COMMIT");
|
|
605
1011
|
}
|
|
606
|
-
const shared =
|
|
1012
|
+
const shared = {
|
|
607
1013
|
key,
|
|
608
1014
|
db,
|
|
609
1015
|
directoryFd,
|
|
@@ -615,7 +1021,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
615
1021
|
mutationTail: Promise.resolve()
|
|
616
1022
|
};
|
|
617
1023
|
store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors, shared);
|
|
618
|
-
if (
|
|
1024
|
+
if (!options.sqlite) writableStores.set(key, shared);
|
|
619
1025
|
db = void 0;
|
|
620
1026
|
}
|
|
621
1027
|
} finally {
|
|
@@ -627,7 +1033,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
627
1033
|
}
|
|
628
1034
|
} catch (error) {
|
|
629
1035
|
if (store) await store.close().catch(() => void 0);
|
|
630
|
-
else if (directoryFd !== void 0) (0,
|
|
1036
|
+
else if (directoryFd !== void 0) (0, import_node_fs2.closeSync)(directoryFd);
|
|
631
1037
|
throw storageError(error, `Opening the replica at ${dir}`);
|
|
632
1038
|
} finally {
|
|
633
1039
|
releaseOpen();
|
|
@@ -643,12 +1049,109 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
643
1049
|
throw error;
|
|
644
1050
|
}
|
|
645
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* A gated read borrows this controller's already-verified writable handle.
|
|
1054
|
+
* Borrow before the first await: the parent may close in the callback, but
|
|
1055
|
+
* its pin and native handle remain live until the snapshot releases them.
|
|
1056
|
+
* Serialize BEGIN through COMMIT/ROLLBACK with writes on the shared handle.
|
|
1057
|
+
*/
|
|
1058
|
+
async readTransaction(generation, checkGate, section) {
|
|
1059
|
+
this.#assertAccepting();
|
|
1060
|
+
const shared = this.#shared;
|
|
1061
|
+
if (!shared) throw new ReplicaError(ReplicaErrorCode.CLOSED, "The replica handle is closed.");
|
|
1062
|
+
const snapshot = new _SqliteReplicaStore(
|
|
1063
|
+
this.dir,
|
|
1064
|
+
shared.db,
|
|
1065
|
+
{ dev: this.#dev, ino: this.#ino },
|
|
1066
|
+
{ create: false, now: this.#now },
|
|
1067
|
+
shared.directoryFd,
|
|
1068
|
+
this.#namedAncestors,
|
|
1069
|
+
shared,
|
|
1070
|
+
true
|
|
1071
|
+
);
|
|
1072
|
+
let release;
|
|
1073
|
+
let active = false;
|
|
1074
|
+
let callbackFailed = false;
|
|
1075
|
+
try {
|
|
1076
|
+
release = await acquireWritableMutation(shared);
|
|
1077
|
+
await snapshot.#assertReadIdentity();
|
|
1078
|
+
snapshot.#db.exec("BEGIN");
|
|
1079
|
+
active = true;
|
|
1080
|
+
const tombstone = snapshot.#db.get("SELECT generation FROM replica WHERE id = 1");
|
|
1081
|
+
try {
|
|
1082
|
+
await checkGate();
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
callbackFailed = true;
|
|
1085
|
+
throw error;
|
|
1086
|
+
}
|
|
1087
|
+
if (tombstone === void 0 || tombstone.generation !== generation) {
|
|
1088
|
+
throw new ReplicaError(ReplicaErrorCode.RESET_REQUIRED, "The replica generation changed; reopen it to continue.");
|
|
1089
|
+
}
|
|
1090
|
+
let result;
|
|
1091
|
+
const context = { controller: this, active: true };
|
|
1092
|
+
try {
|
|
1093
|
+
try {
|
|
1094
|
+
result = await activeReadSection.run(context, () => section(snapshot));
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
callbackFailed = true;
|
|
1097
|
+
throw error;
|
|
1098
|
+
}
|
|
1099
|
+
} finally {
|
|
1100
|
+
context.active = false;
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
await checkGate();
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
callbackFailed = true;
|
|
1106
|
+
throw error;
|
|
1107
|
+
}
|
|
1108
|
+
await snapshot.#assertReadIdentity();
|
|
1109
|
+
snapshot.#db.exec("COMMIT");
|
|
1110
|
+
active = false;
|
|
1111
|
+
return result;
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
if (callbackFailed) throw error;
|
|
1114
|
+
if (isReplicaError(error) || typeof error === "object" && error !== null && "code" in error && error.code === "REPLICA_GATED") throw error;
|
|
1115
|
+
if (["ENOENT", "ELOOP", "ENOTDIR"].includes(String(errnoOf(error))) || /replica database changed|SQLite opened a different replica database/i.test(error instanceof Error ? error.message : String(error))) {
|
|
1116
|
+
throw new ReplicaError(ReplicaErrorCode.RESET_REQUIRED, "The replica was replaced; reopen it to continue.", void 0, { cause: error });
|
|
1117
|
+
}
|
|
1118
|
+
throw storageError(error, "Reading the replica snapshot");
|
|
1119
|
+
} finally {
|
|
1120
|
+
if (active) {
|
|
1121
|
+
try {
|
|
1122
|
+
snapshot.#db.exec("ROLLBACK");
|
|
1123
|
+
} catch {
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
try {
|
|
1127
|
+
await snapshot.close();
|
|
1128
|
+
} finally {
|
|
1129
|
+
release?.();
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
async #assertReadIdentity() {
|
|
1134
|
+
const shared = this.#shared;
|
|
1135
|
+
const pinned = shared.directoryFd === void 0 ? (0, import_node_fs2.lstatSync)(shared.pinnedDir) : (0, import_node_fs2.fstatSync)(shared.directoryFd);
|
|
1136
|
+
const namedDir = (0, import_node_fs2.lstatSync)(this.dir);
|
|
1137
|
+
const namedDb = (0, import_node_fs2.lstatSync)((0, import_node_path2.join)(this.dir, "replica.db"));
|
|
1138
|
+
const pinnedDb = (0, import_node_fs2.lstatSync)((0, import_node_path2.join)(shared.pinnedDir, "replica.db"));
|
|
1139
|
+
if (!pinned.isDirectory() || !namedDir.isDirectory() || pinned.dev !== namedDir.dev || pinned.ino !== namedDir.ino || !namedDb.isFile() || !pinnedDb.isFile() || namedDb.dev !== this.#dev || namedDb.ino !== this.#ino || pinnedDb.dev !== this.#dev || pinnedDb.ino !== this.#ino) {
|
|
1140
|
+
throw new ReplicaError(ReplicaErrorCode.RESET_REQUIRED, "The replica was replaced during its read; reopen it to continue.");
|
|
1141
|
+
}
|
|
1142
|
+
if (this.#namedAncestors.length) {
|
|
1143
|
+
const ancestors = await captureNamedPath(this.dir);
|
|
1144
|
+
if (ancestors.length !== this.#namedAncestors.length || ancestors.some((entry, index) => entry.dev !== this.#namedAncestors[index].dev || entry.ino !== this.#namedAncestors[index].ino)) {
|
|
1145
|
+
throw new ReplicaError(ReplicaErrorCode.RESET_REQUIRED, "The replica directory was replaced; reopen it to continue.");
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
646
1149
|
#row() {
|
|
647
1150
|
return this.#db.get("SELECT * FROM replica WHERE id = 1");
|
|
648
1151
|
}
|
|
649
1152
|
/** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
|
|
650
1153
|
async #assertPresent() {
|
|
651
|
-
const identity = await (0,
|
|
1154
|
+
const identity = await (0, import_promises2.lstat)((0, import_node_path2.join)(this.#pinnedDir, "replica.db")).catch(() => null);
|
|
652
1155
|
if (!identity?.isFile() || identity.dev !== this.#dev || identity.ino !== this.#ino) {
|
|
653
1156
|
throw new ReplicaError(
|
|
654
1157
|
ReplicaErrorCode.NOT_FOUND,
|
|
@@ -665,6 +1168,16 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
665
1168
|
/** A short mutation section: guarded (profile lock) and checked against the database this store opened. */
|
|
666
1169
|
#mutate(section) {
|
|
667
1170
|
this.#assertAccepting();
|
|
1171
|
+
if (this.#readonlySnapshot) {
|
|
1172
|
+
throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "A read-only replica snapshot cannot mutate the shared database.");
|
|
1173
|
+
}
|
|
1174
|
+
const read = activeReadSection.getStore();
|
|
1175
|
+
if (read?.active && read.controller === this) {
|
|
1176
|
+
throw new ReplicaError(
|
|
1177
|
+
ReplicaErrorCode.STORAGE_ERROR,
|
|
1178
|
+
"mutation attempted on the same controller while its readTransaction section is active; this would deadlock"
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
668
1181
|
this.#activeMutations++;
|
|
669
1182
|
return (async () => {
|
|
670
1183
|
try {
|
|
@@ -808,6 +1321,107 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
808
1321
|
);
|
|
809
1322
|
});
|
|
810
1323
|
}
|
|
1324
|
+
/** Per-CID first-install anchor and policy bound share the pending-grant transaction. */
|
|
1325
|
+
#grantInstall(cid) {
|
|
1326
|
+
const row = this.#db.get(
|
|
1327
|
+
"SELECT installed_at, effective_expires_at, consent_deadline, consent_anchor_at, consent_anchor_source FROM grant_install WHERE cid = ?",
|
|
1328
|
+
cid
|
|
1329
|
+
);
|
|
1330
|
+
if (row === void 0) return null;
|
|
1331
|
+
if (!(row.consent_anchor_source === "siwe" || row.consent_anchor_source === "delegation_iat" || row.consent_anchor_source === "delegation_nbf" || row.consent_anchor_source === "first_install") || !Number.isSafeInteger(row.installed_at) || row.installed_at < 0 || !Number.isSafeInteger(row.effective_expires_at) || row.effective_expires_at < 0 || !Number.isSafeInteger(row.consent_deadline) || row.consent_deadline < 0 || !Number.isSafeInteger(row.consent_anchor_at) || row.consent_anchor_at < 0) {
|
|
1332
|
+
throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The stored grant expiry or consent anchor is invalid.");
|
|
1333
|
+
}
|
|
1334
|
+
return {
|
|
1335
|
+
installedAt: row.installed_at,
|
|
1336
|
+
effectiveExpiresAt: row.effective_expires_at,
|
|
1337
|
+
consentDeadline: row.consent_deadline,
|
|
1338
|
+
consentAnchor: { at: row.consent_anchor_at, source: row.consent_anchor_source }
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
async getGrantInstall(cid) {
|
|
1342
|
+
return this.#grantInstall(cid);
|
|
1343
|
+
}
|
|
1344
|
+
/** Every installed CID, including superseded ones, for an origin-wide purge floor. */
|
|
1345
|
+
async grantInstallTimes() {
|
|
1346
|
+
const rows = this.#db.all(
|
|
1347
|
+
"SELECT cid, installed_at FROM grant_install"
|
|
1348
|
+
);
|
|
1349
|
+
return rows.map((row) => {
|
|
1350
|
+
if (typeof row.cid !== "string" || row.cid.length === 0 || !Number.isSafeInteger(row.installed_at) || row.installed_at < 0) {
|
|
1351
|
+
throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The stored grant install time is invalid.");
|
|
1352
|
+
}
|
|
1353
|
+
return { cid: row.cid, installedAt: row.installed_at };
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
/** Phase 2 installed grants cannot commit a page after their shorter local TTL. */
|
|
1357
|
+
#assertInstalledGrantTime() {
|
|
1358
|
+
const row = this.#row();
|
|
1359
|
+
const cid = row?.pending_grant_cid ?? row?.grant_cid;
|
|
1360
|
+
if (cid === void 0 || cid === null) return;
|
|
1361
|
+
const installed = this.#grantInstall(cid);
|
|
1362
|
+
const now = this.#now();
|
|
1363
|
+
if (installed !== null && (now >= installed.effectiveExpiresAt || now >= installed.consentDeadline)) {
|
|
1364
|
+
throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The installed grant expired before this page committed.");
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
#storeGrantPolicy(g, policy, existing) {
|
|
1368
|
+
const installedAt = existing?.installedAt ?? policy.at;
|
|
1369
|
+
const effectiveExpiresAt = Math.min(
|
|
1370
|
+
g.expiresAt === null ? Infinity : g.expiresAt * 1e3,
|
|
1371
|
+
installedAt + policy.ttlMs,
|
|
1372
|
+
policy.consentDeadline
|
|
1373
|
+
);
|
|
1374
|
+
if (!Number.isSafeInteger(installedAt) || installedAt < 0 || !Number.isSafeInteger(effectiveExpiresAt) || effectiveExpiresAt < 0 || !Number.isSafeInteger(policy.consentDeadline) || policy.consentDeadline < 0 || !Number.isSafeInteger(policy.consentAnchor.at) || policy.consentAnchor.at < 0 || !Number.isSafeInteger(policy.ttlMs) || policy.ttlMs <= 0) {
|
|
1375
|
+
throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The replica grant has no finite effective expiry.");
|
|
1376
|
+
}
|
|
1377
|
+
this.#db.run(
|
|
1378
|
+
`INSERT INTO grant_install (cid, installed_at, effective_expires_at, consent_deadline, consent_anchor_at, consent_anchor_source)
|
|
1379
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1380
|
+
ON CONFLICT (cid) DO UPDATE SET effective_expires_at = excluded.effective_expires_at,
|
|
1381
|
+
consent_deadline = excluded.consent_deadline, consent_anchor_at = excluded.consent_anchor_at,
|
|
1382
|
+
consent_anchor_source = excluded.consent_anchor_source`,
|
|
1383
|
+
g.cid,
|
|
1384
|
+
installedAt,
|
|
1385
|
+
effectiveExpiresAt,
|
|
1386
|
+
policy.consentDeadline,
|
|
1387
|
+
policy.consentAnchor.at,
|
|
1388
|
+
policy.consentAnchor.source
|
|
1389
|
+
);
|
|
1390
|
+
return { installedAt, effectiveExpiresAt, consentDeadline: policy.consentDeadline, consentAnchor: policy.consentAnchor };
|
|
1391
|
+
}
|
|
1392
|
+
async installGatedGrant(g, policy) {
|
|
1393
|
+
return this.#guardedWrite("Installing the gated grant", () => {
|
|
1394
|
+
const row = this.#row();
|
|
1395
|
+
if (row === void 0) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica has not been created.");
|
|
1396
|
+
const previous = this.#grantInstall(g.cid);
|
|
1397
|
+
if (previous === null && (row.grant_cid === g.cid || row.pending_grant_cid === g.cid)) {
|
|
1398
|
+
throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The installed grant has no durable first-install time.");
|
|
1399
|
+
}
|
|
1400
|
+
const next = this.#storeGrantPolicy(g, policy, previous);
|
|
1401
|
+
if (next.effectiveExpiresAt <= this.#now()) {
|
|
1402
|
+
throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The replica grant expired under the current policy.");
|
|
1403
|
+
}
|
|
1404
|
+
if (row.grant_cid !== g.cid && row.pending_grant_cid !== g.cid) {
|
|
1405
|
+
this.#db.run(
|
|
1406
|
+
`UPDATE replica SET pending_grant_cid = ?, pending_grant_bytes = ?, pending_grant_audience = ?,
|
|
1407
|
+
pending_grant_issuer = ?, pending_grant_nbf = ?, pending_grant_exp = ?, pending_grant_error = NULL WHERE id = 1`,
|
|
1408
|
+
g.cid,
|
|
1409
|
+
g.bytes,
|
|
1410
|
+
g.audience,
|
|
1411
|
+
g.issuer,
|
|
1412
|
+
g.notBefore,
|
|
1413
|
+
g.expiresAt
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
return next;
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
async recomputeGrantInstall(g, policy) {
|
|
1420
|
+
return this.#guardedWrite("Recomputing the gated grant", () => {
|
|
1421
|
+
const previous = this.#grantInstall(g.cid);
|
|
1422
|
+
return previous === null ? null : this.#storeGrantPolicy(g, policy, previous);
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
811
1425
|
async installGrant(g) {
|
|
812
1426
|
await this.#guardedWrite("Installing the grant", () => {
|
|
813
1427
|
const row = this.#row();
|
|
@@ -866,13 +1480,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
866
1480
|
const now = this.#now();
|
|
867
1481
|
if (row.lease_holder !== null && row.lease_expires_at !== null && row.lease_expires_at > now) return null;
|
|
868
1482
|
const token = row.lease_token + 1;
|
|
1483
|
+
const holder = this.#holder ??= `${process.pid}-${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
|
|
869
1484
|
this.#db.run(
|
|
870
1485
|
"UPDATE replica SET lease_token = ?, lease_holder = ?, lease_expires_at = ? WHERE id = 1",
|
|
871
1486
|
token,
|
|
872
|
-
|
|
1487
|
+
holder,
|
|
873
1488
|
now + ttlMs
|
|
874
1489
|
);
|
|
875
|
-
return { token, holder
|
|
1490
|
+
return { token, holder };
|
|
876
1491
|
});
|
|
877
1492
|
}
|
|
878
1493
|
async renewLease(t, ttlMs) {
|
|
@@ -904,7 +1519,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
904
1519
|
return held;
|
|
905
1520
|
}
|
|
906
1521
|
#blobPath(hash) {
|
|
907
|
-
return (0,
|
|
1522
|
+
return (0, import_node_path2.join)(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
|
|
908
1523
|
}
|
|
909
1524
|
/**
|
|
910
1525
|
* Make every blob durable before any row references it: write to a fresh
|
|
@@ -916,17 +1531,17 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
916
1531
|
const dirs = /* @__PURE__ */ new Set();
|
|
917
1532
|
for (const [hash, bytes] of blobs) {
|
|
918
1533
|
const final = this.#blobPath(hash);
|
|
919
|
-
const exists = await (0,
|
|
1534
|
+
const exists = await (0, import_promises2.stat)(final).then(
|
|
920
1535
|
() => true,
|
|
921
1536
|
() => false
|
|
922
1537
|
);
|
|
923
1538
|
if (exists) continue;
|
|
924
|
-
const shard = (0,
|
|
925
|
-
await (0,
|
|
1539
|
+
const shard = (0, import_node_path2.join)(this.#pinnedDir, "blobs", hash.slice(0, 2));
|
|
1540
|
+
await (0, import_promises2.mkdir)(shard, { mode: 448 }).catch((error) => {
|
|
926
1541
|
if (errnoOf(error) !== "EEXIST") throw error;
|
|
927
1542
|
});
|
|
928
|
-
const temp = (0,
|
|
929
|
-
const handle = await (0,
|
|
1543
|
+
const temp = (0, import_node_path2.join)(this.#pinnedDir, "blobs", ".tmp", `${hash}.${(0, import_node_crypto.randomBytes)(6).toString("hex")}`);
|
|
1544
|
+
const handle = await (0, import_promises2.open)(temp, import_node_fs2.constants.O_CREAT | import_node_fs2.constants.O_EXCL | import_node_fs2.constants.O_WRONLY, 384);
|
|
930
1545
|
try {
|
|
931
1546
|
await handle.writeFile(bytes);
|
|
932
1547
|
await handle.sync();
|
|
@@ -934,17 +1549,17 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
934
1549
|
await handle.close();
|
|
935
1550
|
}
|
|
936
1551
|
try {
|
|
937
|
-
await (0,
|
|
1552
|
+
await (0, import_promises2.link)(temp, final);
|
|
938
1553
|
} catch (error) {
|
|
939
1554
|
if (errnoOf(error) !== "EEXIST") throw error;
|
|
940
1555
|
} finally {
|
|
941
|
-
await (0,
|
|
1556
|
+
await (0, import_promises2.unlink)(temp);
|
|
942
1557
|
}
|
|
943
1558
|
dirs.add(shard);
|
|
944
|
-
dirs.add((0,
|
|
1559
|
+
dirs.add((0, import_node_path2.join)(this.#pinnedDir, "blobs"));
|
|
945
1560
|
}
|
|
946
1561
|
for (const dir of dirs) {
|
|
947
|
-
const handle = await (0,
|
|
1562
|
+
const handle = await (0, import_promises2.open)(dir, import_node_fs2.constants.O_RDONLY);
|
|
948
1563
|
try {
|
|
949
1564
|
await handle.sync();
|
|
950
1565
|
} finally {
|
|
@@ -955,6 +1570,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
955
1570
|
async applyPage(t, p) {
|
|
956
1571
|
await this.#mutate(async () => {
|
|
957
1572
|
this.#checkLease(t);
|
|
1573
|
+
this.#assertInstalledGrantTime();
|
|
958
1574
|
const reused = /* @__PURE__ */ new Map();
|
|
959
1575
|
for (const change of p.changes) {
|
|
960
1576
|
if (change.deleted || !change.content || p.blobs.has(change.hash) || reused.has(change.hash)) continue;
|
|
@@ -976,6 +1592,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
976
1592
|
try {
|
|
977
1593
|
await this.#write("Committing a sync page", () => {
|
|
978
1594
|
this.#checkLease(t);
|
|
1595
|
+
this.#assertInstalledGrantTime();
|
|
979
1596
|
const now = this.#now();
|
|
980
1597
|
if (p.window.notBefore !== null && now < Date.parse(p.window.notBefore)) {
|
|
981
1598
|
throw new ReplicaError(ReplicaErrorCode.GRANT_NOT_YET_VALID, `The grant is not valid before ${p.window.notBefore}; nothing was committed.`);
|
|
@@ -1049,12 +1666,12 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1049
1666
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
1050
1667
|
async #unlinkBlobs(keep) {
|
|
1051
1668
|
let removed = 0;
|
|
1052
|
-
const root = (0,
|
|
1669
|
+
const root = (0, import_node_path2.join)(this.#pinnedDir, "blobs");
|
|
1053
1670
|
for (const shard of await entriesOf(root)) {
|
|
1054
|
-
const shardPath = (0,
|
|
1671
|
+
const shardPath = (0, import_node_path2.join)(root, shard);
|
|
1055
1672
|
if (shard === ".tmp") {
|
|
1056
1673
|
for (const name of await entriesOf(shardPath)) {
|
|
1057
|
-
await (0,
|
|
1674
|
+
await (0, import_promises2.rm)((0, import_node_path2.join)(shardPath, name), { force: true });
|
|
1058
1675
|
removed += 1;
|
|
1059
1676
|
}
|
|
1060
1677
|
continue;
|
|
@@ -1065,18 +1682,18 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1065
1682
|
kept += 1;
|
|
1066
1683
|
continue;
|
|
1067
1684
|
}
|
|
1068
|
-
await (0,
|
|
1685
|
+
await (0, import_promises2.rm)((0, import_node_path2.join)(shardPath, name), { force: true });
|
|
1069
1686
|
removed += 1;
|
|
1070
1687
|
}
|
|
1071
|
-
if (kept === 0) await (0,
|
|
1688
|
+
if (kept === 0) await (0, import_promises2.rmdir)(shardPath).catch(() => void 0);
|
|
1072
1689
|
}
|
|
1073
1690
|
return removed;
|
|
1074
1691
|
}
|
|
1075
1692
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
1076
1693
|
async #holdsContentFiles() {
|
|
1077
|
-
const root = (0,
|
|
1694
|
+
const root = (0, import_node_path2.join)(this.#pinnedDir, "blobs");
|
|
1078
1695
|
for (const shard of await entriesOf(root)) {
|
|
1079
|
-
if ((await entriesOf((0,
|
|
1696
|
+
if ((await entriesOf((0, import_node_path2.join)(root, shard))).length > 0) return true;
|
|
1080
1697
|
}
|
|
1081
1698
|
return false;
|
|
1082
1699
|
}
|
|
@@ -1130,13 +1747,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1130
1747
|
}
|
|
1131
1748
|
}
|
|
1132
1749
|
/**
|
|
1133
|
-
* Remove
|
|
1134
|
-
*
|
|
1135
|
-
* store opened, checked under the write lock, so a paused process can
|
|
1136
|
-
* never remove a replica another process took over or recreated. Allowed
|
|
1137
|
-
* on a revoked replica. The store is unusable afterwards; close it.
|
|
1750
|
+
* Remove the source-created mirror layout under a live lease. Unknown files
|
|
1751
|
+
* are removed only when their exact preflight identities were approved.
|
|
1138
1752
|
*/
|
|
1139
|
-
async destroy(t) {
|
|
1753
|
+
async destroy(t, options = {}) {
|
|
1140
1754
|
await this.#mutate(async () => {
|
|
1141
1755
|
const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
|
|
1142
1756
|
try {
|
|
@@ -1147,92 +1761,36 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1147
1761
|
}
|
|
1148
1762
|
try {
|
|
1149
1763
|
this.#checkLease(t, { allowRevoked: true });
|
|
1150
|
-
const expected = this.#directoryFd === void 0 ? this.#namedAncestors.at(-1) : (0,
|
|
1764
|
+
const expected = this.#directoryFd === void 0 ? this.#namedAncestors.at(-1) : (0, import_node_fs2.fstatSync)(this.#directoryFd, { bigint: true });
|
|
1151
1765
|
if (!expected) {
|
|
1152
1766
|
throw new ReplicaError(ReplicaErrorCode.RUNTIME_UNSUPPORTED, "Replica removal requires an opened or identity-checked directory.");
|
|
1153
1767
|
}
|
|
1154
1768
|
const assertDirectory = async () => {
|
|
1155
|
-
const current = await (0,
|
|
1769
|
+
const current = await (0, import_promises2.lstat)(this.dir, { bigint: true });
|
|
1156
1770
|
if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
1157
1771
|
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica directory changed during removal.");
|
|
1158
1772
|
}
|
|
1159
1773
|
return current;
|
|
1160
1774
|
};
|
|
1161
1775
|
const pinned = await assertDirectory();
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
await assertDirectory();
|
|
1168
|
-
await (0, import_promises.rmdir)(this.dir);
|
|
1169
|
-
} else {
|
|
1170
|
-
const ancestors = this.#namedAncestors;
|
|
1171
|
-
const checkAncestors = async () => {
|
|
1172
|
-
for (const expected2 of ancestors) {
|
|
1173
|
-
const current = await (0, import_promises.lstat)(expected2.path, { bigint: true });
|
|
1174
|
-
if (current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
1175
|
-
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica ancestry changed during removal.");
|
|
1176
|
-
}
|
|
1776
|
+
const assertNamedPath = async () => {
|
|
1777
|
+
for (const ancestor of this.#namedAncestors) {
|
|
1778
|
+
const current = await (0, import_promises2.lstat)(ancestor.path, { bigint: true });
|
|
1779
|
+
if (current.dev !== ancestor.dev || current.ino !== ancestor.ino) {
|
|
1780
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica ancestry changed during removal.");
|
|
1177
1781
|
}
|
|
1178
|
-
};
|
|
1179
|
-
const directories = /* @__PURE__ */ new Map([[this.dir, pinned]]);
|
|
1180
|
-
const files = /* @__PURE__ */ new Map();
|
|
1181
|
-
const check = async (path, expected2, directory) => {
|
|
1182
|
-
if (path !== this.dir) {
|
|
1183
|
-
const parent = (0, import_node_path.dirname)(path);
|
|
1184
|
-
await check(parent, directories.get(parent), true);
|
|
1185
|
-
}
|
|
1186
|
-
await checkAncestors();
|
|
1187
|
-
const current = await (0, import_promises.lstat)(path, { bigint: true });
|
|
1188
|
-
if ((directory ? !current.isDirectory() : !current.isFile()) || current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
1189
|
-
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica layout changed during removal.");
|
|
1190
|
-
}
|
|
1191
|
-
};
|
|
1192
|
-
const record = async (path, directory) => {
|
|
1193
|
-
const parent = (0, import_node_path.dirname)(path);
|
|
1194
|
-
await check(parent, directories.get(parent), true);
|
|
1195
|
-
const current = await (0, import_promises.lstat)(path, { bigint: true });
|
|
1196
|
-
if (!(directory ? current.isDirectory() : current.isFile())) {
|
|
1197
|
-
throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
1198
|
-
}
|
|
1199
|
-
(directory ? directories : files).set(path, current);
|
|
1200
|
-
};
|
|
1201
|
-
const blobs = (0, import_node_path.join)(this.dir, "blobs");
|
|
1202
|
-
for (const name of await (0, import_promises.readdir)(this.dir)) {
|
|
1203
|
-
if (name === "blobs") await record(blobs, true);
|
|
1204
|
-
else if (["replica.db", "replica.db-wal", "replica.db-shm"].includes(name)) await record((0, import_node_path.join)(this.dir, name), false);
|
|
1205
|
-
else throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
1206
|
-
}
|
|
1207
|
-
if (directories.has(blobs)) {
|
|
1208
|
-
await check(blobs, directories.get(blobs), true);
|
|
1209
|
-
for (const shard of await (0, import_promises.readdir)(blobs)) {
|
|
1210
|
-
if (shard !== ".tmp" && !/^[0-9a-f]{2}$/.test(shard)) {
|
|
1211
|
-
throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica blob shard during removal.");
|
|
1212
|
-
}
|
|
1213
|
-
const shardPath = (0, import_node_path.join)(blobs, shard);
|
|
1214
|
-
await record(shardPath, true);
|
|
1215
|
-
await check(shardPath, directories.get(shardPath), true);
|
|
1216
|
-
for (const name of await (0, import_promises.readdir)(shardPath)) {
|
|
1217
|
-
if (shard === ".tmp" ? !/^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name) : !isHash(name) || !name.startsWith(shard)) {
|
|
1218
|
-
throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica blob file during removal.");
|
|
1219
|
-
}
|
|
1220
|
-
await record((0, import_node_path.join)(shardPath, name), false);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
for (const [path, identity] of files) {
|
|
1225
|
-
await check(path, identity, false);
|
|
1226
|
-
await (0, import_promises.unlink)(path);
|
|
1227
|
-
}
|
|
1228
|
-
for (const [path, identity] of [...directories].reverse()) {
|
|
1229
|
-
if (path === this.dir) continue;
|
|
1230
|
-
await check(path, identity, true);
|
|
1231
|
-
await (0, import_promises.rmdir)(path);
|
|
1232
1782
|
}
|
|
1233
1783
|
await assertDirectory();
|
|
1234
|
-
|
|
1235
|
-
|
|
1784
|
+
};
|
|
1785
|
+
await this.#assertPresent();
|
|
1786
|
+
try {
|
|
1787
|
+
await removeSqliteReplicaLayout(this.dir, {
|
|
1788
|
+
...options,
|
|
1789
|
+
expectedRoot: options.expectedRoot ?? { dev: pinned.dev.toString(), ino: pinned.ino.toString() }
|
|
1790
|
+
});
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
await assertNamedPath();
|
|
1793
|
+
throw error;
|
|
1236
1794
|
}
|
|
1237
1795
|
} catch (error) {
|
|
1238
1796
|
throw storageError(error, "Removing the replica");
|
|
@@ -1345,7 +1903,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1345
1903
|
async readContent(hash) {
|
|
1346
1904
|
if (!isHash(hash)) return void 0;
|
|
1347
1905
|
try {
|
|
1348
|
-
return new Uint8Array(await (0,
|
|
1906
|
+
return new Uint8Array(await (0, import_promises2.readFile)(this.#blobPath(hash)));
|
|
1349
1907
|
} catch (error) {
|
|
1350
1908
|
if (errnoOf(error) === "ENOENT") return void 0;
|
|
1351
1909
|
throw storageError(error, "Reading replica content");
|
|
@@ -1406,8 +1964,8 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1406
1964
|
this.#closing = true;
|
|
1407
1965
|
this.#closePromise = (async () => {
|
|
1408
1966
|
if (this.#activeMutations !== 0) {
|
|
1409
|
-
await new Promise((
|
|
1410
|
-
this.#drained =
|
|
1967
|
+
await new Promise((resolve3) => {
|
|
1968
|
+
this.#drained = resolve3;
|
|
1411
1969
|
});
|
|
1412
1970
|
}
|
|
1413
1971
|
this.#closed = true;
|
|
@@ -1421,7 +1979,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1421
1979
|
try {
|
|
1422
1980
|
this.#database.close();
|
|
1423
1981
|
} finally {
|
|
1424
|
-
if (fd !== void 0) (0,
|
|
1982
|
+
if (fd !== void 0) (0, import_node_fs2.closeSync)(fd);
|
|
1425
1983
|
}
|
|
1426
1984
|
}
|
|
1427
1985
|
})();
|
|
@@ -1432,7 +1990,9 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1432
1990
|
0 && (module.exports = {
|
|
1433
1991
|
DURABILITY,
|
|
1434
1992
|
SqliteReplicaStore,
|
|
1993
|
+
inspectSqliteReplicaLayout,
|
|
1435
1994
|
loadSqlite,
|
|
1436
|
-
nodeSqliteSupported
|
|
1995
|
+
nodeSqliteSupported,
|
|
1996
|
+
removeSqliteReplicaLayout
|
|
1437
1997
|
});
|
|
1438
1998
|
//# sourceMappingURL=sqlite.cjs.map
|