@tinycloud/replica 0.2.4-beta.2 → 0.3.0-beta.4
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.618d3a20b6f3f54ed78b68f1dc244dddc3daba46e91e9ae2b577685cc9339315.js +18891 -0
- package/dist/kv-storage.worker.d.ts +2 -0
- package/dist/kv-storage.worker.js +18891 -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 +444 -171
- package/dist/replica.worker.js.map +1 -1
- package/dist/sqlite.cjs +926 -239
- 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 +914 -229
- 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,16 +737,53 @@ 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
|
});
|
|
744
|
+
var writableStores = /* @__PURE__ */ new Map();
|
|
745
|
+
function releaseWritable(shared) {
|
|
746
|
+
if (shared.references === 0 || --shared.references !== 0) return;
|
|
747
|
+
if (writableStores.get(shared.key) === shared) writableStores.delete(shared.key);
|
|
748
|
+
try {
|
|
749
|
+
shared.db.close();
|
|
750
|
+
} finally {
|
|
751
|
+
if (shared.directoryFd !== void 0) (0, import_node_fs2.closeSync)(shared.directoryFd);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
var sharedStores = new FinalizationRegistry((shared) => {
|
|
755
|
+
try {
|
|
756
|
+
releaseWritable(shared);
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
async function acquireWritableMutation(shared) {
|
|
761
|
+
const previous = shared.mutationTail;
|
|
762
|
+
let release;
|
|
763
|
+
shared.mutationTail = new Promise((resolve3) => {
|
|
764
|
+
release = resolve3;
|
|
765
|
+
});
|
|
766
|
+
await previous;
|
|
767
|
+
return release;
|
|
768
|
+
}
|
|
769
|
+
var activeReadSection = new import_node_async_hooks.AsyncLocalStorage();
|
|
358
770
|
var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
359
771
|
dir;
|
|
360
|
-
#
|
|
772
|
+
#database;
|
|
773
|
+
get #db() {
|
|
774
|
+
if (this.#closed) throw new ReplicaError(ReplicaErrorCode.CLOSED, "The replica store is closed.");
|
|
775
|
+
return this.#database;
|
|
776
|
+
}
|
|
777
|
+
#closed = false;
|
|
778
|
+
#closing = false;
|
|
779
|
+
#closePromise;
|
|
780
|
+
#activeMutations = 0;
|
|
781
|
+
#drained;
|
|
782
|
+
#shared;
|
|
783
|
+
#readonlySnapshot;
|
|
361
784
|
#pinnedDir;
|
|
362
785
|
#directoryFd;
|
|
363
|
-
#holder
|
|
786
|
+
#holder;
|
|
364
787
|
#now;
|
|
365
788
|
#faults;
|
|
366
789
|
#guard;
|
|
@@ -368,12 +791,19 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
368
791
|
#ino;
|
|
369
792
|
#dev;
|
|
370
793
|
#namedAncestors;
|
|
371
|
-
constructor(dir, db, identity, options, directoryFd, namedAncestors = []) {
|
|
794
|
+
constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared, readonlySnapshot = false) {
|
|
372
795
|
this.dir = dir;
|
|
373
|
-
this.#pinnedDir = directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir;
|
|
796
|
+
this.#pinnedDir = shared?.pinnedDir ?? (directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir);
|
|
374
797
|
this.#directoryFd = directoryFd;
|
|
375
|
-
|
|
376
|
-
this.#
|
|
798
|
+
this.#shared = shared;
|
|
799
|
+
this.#readonlySnapshot = readonlySnapshot;
|
|
800
|
+
if (shared) {
|
|
801
|
+
shared.references++;
|
|
802
|
+
sharedStores.register(this, shared, this);
|
|
803
|
+
} else if (directoryFd !== void 0) {
|
|
804
|
+
pinnedDirectories.register(this, directoryFd, this);
|
|
805
|
+
}
|
|
806
|
+
this.#database = db;
|
|
377
807
|
this.#ino = identity.ino;
|
|
378
808
|
this.#dev = identity.dev;
|
|
379
809
|
this.#now = options.now ?? Date.now;
|
|
@@ -391,10 +821,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
391
821
|
if (options.signal?.aborted) throw options.signal.reason ?? new ReplicaError(ReplicaErrorCode.CLOSED, "Replica status inspection was cancelled.");
|
|
392
822
|
};
|
|
393
823
|
checkAbort();
|
|
394
|
-
const dbPath = (0,
|
|
824
|
+
const dbPath = (0, import_node_path2.join)(dir, "replica.db");
|
|
395
825
|
let identity;
|
|
396
826
|
try {
|
|
397
|
-
identity = await (0,
|
|
827
|
+
identity = await (0, import_promises2.lstat)(dbPath);
|
|
398
828
|
if (!identity.isFile()) return null;
|
|
399
829
|
} catch (error) {
|
|
400
830
|
if (errnoOf(error) === "ENOENT") return null;
|
|
@@ -404,21 +834,30 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
404
834
|
const opener = await loadSqlite();
|
|
405
835
|
checkAbort();
|
|
406
836
|
let db;
|
|
837
|
+
let releaseOpen;
|
|
407
838
|
try {
|
|
408
|
-
|
|
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;
|
|
409
844
|
db.exec("PRAGMA busy_timeout = 0");
|
|
410
845
|
checkAbort();
|
|
411
|
-
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 } : {} });
|
|
412
847
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
413
848
|
const before = db.get("PRAGMA data_version").data_version;
|
|
414
849
|
const state = await store.open();
|
|
415
850
|
checkAbort();
|
|
416
851
|
if (state === null) return null;
|
|
417
852
|
const status = await store.status();
|
|
418
|
-
|
|
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));
|
|
419
858
|
checkAbort();
|
|
420
859
|
const after = db.get("PRAGMA data_version").data_version;
|
|
421
|
-
if (before === after) return { state, status };
|
|
860
|
+
if (before === after) return { state, status, grantInstall, activeGrantInstall };
|
|
422
861
|
}
|
|
423
862
|
throw new ReplicaError(ReplicaErrorCode.BUSY, "The replica changed during status inspection.");
|
|
424
863
|
} catch (error) {
|
|
@@ -427,7 +866,15 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
427
866
|
}
|
|
428
867
|
throw storageError(error, "Inspecting the replica");
|
|
429
868
|
} finally {
|
|
430
|
-
|
|
869
|
+
releaseOpen?.();
|
|
870
|
+
if (db) {
|
|
871
|
+
const releaseClose = await acquireSqliteOpen();
|
|
872
|
+
try {
|
|
873
|
+
db.close();
|
|
874
|
+
} finally {
|
|
875
|
+
releaseClose();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
431
878
|
}
|
|
432
879
|
}
|
|
433
880
|
/**
|
|
@@ -435,24 +882,23 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
435
882
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
436
883
|
*/
|
|
437
884
|
static async open(dir, options) {
|
|
438
|
-
const namedDbPath = (0,
|
|
885
|
+
const namedDbPath = (0, import_node_path2.join)(dir, "replica.db");
|
|
439
886
|
if (!options.create) {
|
|
440
|
-
const exists = await (0,
|
|
887
|
+
const exists = await (0, import_promises2.lstat)(namedDbPath).then(
|
|
441
888
|
(stats) => stats.isFile(),
|
|
442
889
|
() => false
|
|
443
890
|
);
|
|
444
891
|
if (!exists) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `No regular replica database at ${dir}.`);
|
|
445
892
|
}
|
|
446
|
-
const opener = options.sqlite ?? await loadSqlite();
|
|
447
893
|
const releaseOpen = await acquireSqliteOpen();
|
|
448
894
|
let store;
|
|
449
895
|
let directoryFd;
|
|
450
896
|
try {
|
|
451
|
-
if (options.create) await (0,
|
|
897
|
+
if (options.create) await (0, import_promises2.mkdir)(dir, { recursive: true, mode: 448 });
|
|
452
898
|
if (process.platform === "linux") {
|
|
453
|
-
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);
|
|
454
900
|
}
|
|
455
|
-
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 });
|
|
456
902
|
if (!opened.isDirectory() || opened.isSymbolicLink() || options.expectedDirectory && (opened.dev !== BigInt(options.expectedDirectory.dev) || opened.ino !== BigInt(options.expectedDirectory.ino))) {
|
|
457
903
|
throw new Error("The replica directory changed before SQLite opened it.");
|
|
458
904
|
}
|
|
@@ -461,11 +907,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
461
907
|
throw new Error("The replica directory changed while checking its ancestry.");
|
|
462
908
|
}
|
|
463
909
|
const pinnedDir = directoryFd === void 0 ? dir : `/proc/self/fd/${directoryFd}`;
|
|
464
|
-
const dbPath = (0,
|
|
910
|
+
const dbPath = (0, import_node_path2.join)(pinnedDir, "replica.db");
|
|
465
911
|
if (options.create) {
|
|
466
|
-
await (0,
|
|
467
|
-
await Promise.all([(0,
|
|
468
|
-
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) => {
|
|
469
915
|
if (errnoOf(error) === "EEXIST") return null;
|
|
470
916
|
throw error;
|
|
471
917
|
});
|
|
@@ -477,71 +923,107 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
477
923
|
}
|
|
478
924
|
}
|
|
479
925
|
}
|
|
480
|
-
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);
|
|
481
927
|
let db;
|
|
482
928
|
try {
|
|
483
929
|
const identity = await leaf.stat();
|
|
484
|
-
const named = await (0,
|
|
930
|
+
const named = await (0, import_promises2.lstat)(dbPath);
|
|
485
931
|
if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
|
|
486
932
|
throw new Error("The replica database changed before SQLite opened it.");
|
|
487
933
|
}
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
934
|
+
const key = `${opened.dev}:${opened.ino}`;
|
|
935
|
+
const active = writableStores.get(key);
|
|
936
|
+
if (active && (active.dev !== identity.dev || active.ino !== identity.ino)) {
|
|
937
|
+
throw new Error("The replica database changed while another controller holds it open.");
|
|
938
|
+
}
|
|
939
|
+
if (active && !options.sqlite) {
|
|
940
|
+
const namedDirectory = (0, import_node_fs2.lstatSync)(dir, { bigint: true });
|
|
941
|
+
if (!namedDirectory.isDirectory() || namedDirectory.dev !== opened.dev || namedDirectory.ino !== opened.ino) {
|
|
942
|
+
throw new Error("The replica directory changed before its checked SQLite handle could be shared.");
|
|
943
|
+
}
|
|
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"));
|
|
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) {
|
|
947
|
+
throw new Error("The live replica changed before its checked SQLite handle could be shared.");
|
|
493
948
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
949
|
+
if (directoryFd !== void 0) {
|
|
950
|
+
(0, import_node_fs2.closeSync)(directoryFd);
|
|
951
|
+
directoryFd = void 0;
|
|
952
|
+
}
|
|
953
|
+
store = new _SqliteReplicaStore(dir, active.db, identity, options, active.directoryFd, namedAncestors, active);
|
|
499
954
|
}
|
|
500
|
-
if (
|
|
501
|
-
|
|
502
|
-
const
|
|
955
|
+
if (!store) {
|
|
956
|
+
const opener = options.sqlite ?? await loadSqlite();
|
|
957
|
+
const previousFds = process.platform === "linux" && !options.sqlite ? new Map((0, import_node_fs2.readdirSync)("/proc/self/fd").map((fd) => {
|
|
503
958
|
try {
|
|
504
|
-
|
|
505
|
-
return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
|
|
959
|
+
return [fd, (0, import_node_fs2.readlinkSync)(`/proc/self/fd/${fd}`)];
|
|
506
960
|
} catch {
|
|
507
|
-
return
|
|
961
|
+
return [fd, ""];
|
|
508
962
|
}
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
963
|
+
})) : null;
|
|
964
|
+
db = opener(dbPath);
|
|
965
|
+
const afterOpen = (0, import_node_fs2.lstatSync)(dbPath);
|
|
966
|
+
if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
|
|
967
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
968
|
+
}
|
|
969
|
+
if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
|
|
970
|
+
if (previousFds) {
|
|
971
|
+
const databases = (0, import_node_fs2.readdirSync)("/proc/self/fd").filter((fd) => {
|
|
972
|
+
try {
|
|
973
|
+
const target = (0, import_node_fs2.readlinkSync)(`/proc/self/fd/${fd}`);
|
|
974
|
+
return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
|
|
975
|
+
} catch {
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
if (databases.length === 0 || databases.some((fd) => {
|
|
980
|
+
try {
|
|
981
|
+
const actual = (0, import_node_fs2.fstatSync)(Number(fd));
|
|
982
|
+
return actual.dev !== identity.dev || actual.ino !== identity.ino;
|
|
983
|
+
} catch {
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
})) {
|
|
987
|
+
throw new Error("SQLite opened a different replica database.");
|
|
516
988
|
}
|
|
517
|
-
})) {
|
|
518
|
-
throw new Error("SQLite opened a different replica database.");
|
|
519
989
|
}
|
|
990
|
+
const after = (0, import_node_fs2.lstatSync)(dbPath);
|
|
991
|
+
if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
|
|
992
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
993
|
+
}
|
|
994
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
995
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
996
|
+
db.exec("PRAGMA synchronous = FULL");
|
|
997
|
+
db.exec("PRAGMA secure_delete = ON");
|
|
998
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
999
|
+
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
1000
|
+
if (version > SCHEMA_VERSION) {
|
|
1001
|
+
throw new ReplicaError(
|
|
1002
|
+
ReplicaErrorCode.STORAGE_ERROR,
|
|
1003
|
+
`The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
if (version < SCHEMA_VERSION) {
|
|
1007
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1008
|
+
db.exec(SCHEMA);
|
|
1009
|
+
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
1010
|
+
db.exec("COMMIT");
|
|
1011
|
+
}
|
|
1012
|
+
const shared = {
|
|
1013
|
+
key,
|
|
1014
|
+
db,
|
|
1015
|
+
directoryFd,
|
|
1016
|
+
pinnedDir,
|
|
1017
|
+
namedPath: dir,
|
|
1018
|
+
dev: identity.dev,
|
|
1019
|
+
ino: identity.ino,
|
|
1020
|
+
references: 0,
|
|
1021
|
+
mutationTail: Promise.resolve()
|
|
1022
|
+
};
|
|
1023
|
+
store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors, shared);
|
|
1024
|
+
if (!options.sqlite) writableStores.set(key, shared);
|
|
1025
|
+
db = void 0;
|
|
520
1026
|
}
|
|
521
|
-
const after = (0, import_node_fs.lstatSync)(dbPath);
|
|
522
|
-
if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
|
|
523
|
-
throw new Error("The replica database changed while SQLite opened it.");
|
|
524
|
-
}
|
|
525
|
-
db.exec("PRAGMA busy_timeout = 5000");
|
|
526
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
527
|
-
db.exec("PRAGMA synchronous = FULL");
|
|
528
|
-
db.exec("PRAGMA secure_delete = ON");
|
|
529
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
530
|
-
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
531
|
-
if (version > SCHEMA_VERSION) {
|
|
532
|
-
throw new ReplicaError(
|
|
533
|
-
ReplicaErrorCode.STORAGE_ERROR,
|
|
534
|
-
`The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
if (version < SCHEMA_VERSION) {
|
|
538
|
-
db.exec("BEGIN IMMEDIATE");
|
|
539
|
-
db.exec(SCHEMA);
|
|
540
|
-
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
541
|
-
db.exec("COMMIT");
|
|
542
|
-
}
|
|
543
|
-
store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors);
|
|
544
|
-
db = void 0;
|
|
545
1027
|
} finally {
|
|
546
1028
|
try {
|
|
547
1029
|
db?.close();
|
|
@@ -551,7 +1033,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
551
1033
|
}
|
|
552
1034
|
} catch (error) {
|
|
553
1035
|
if (store) await store.close().catch(() => void 0);
|
|
554
|
-
else if (directoryFd !== void 0) (0,
|
|
1036
|
+
else if (directoryFd !== void 0) (0, import_node_fs2.closeSync)(directoryFd);
|
|
555
1037
|
throw storageError(error, `Opening the replica at ${dir}`);
|
|
556
1038
|
} finally {
|
|
557
1039
|
releaseOpen();
|
|
@@ -567,12 +1049,109 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
567
1049
|
throw error;
|
|
568
1050
|
}
|
|
569
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
|
+
}
|
|
570
1149
|
#row() {
|
|
571
1150
|
return this.#db.get("SELECT * FROM replica WHERE id = 1");
|
|
572
1151
|
}
|
|
573
1152
|
/** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
|
|
574
1153
|
async #assertPresent() {
|
|
575
|
-
const identity = await (0,
|
|
1154
|
+
const identity = await (0, import_promises2.lstat)((0, import_node_path2.join)(this.#pinnedDir, "replica.db")).catch(() => null);
|
|
576
1155
|
if (!identity?.isFile() || identity.dev !== this.#dev || identity.ino !== this.#ino) {
|
|
577
1156
|
throw new ReplicaError(
|
|
578
1157
|
ReplicaErrorCode.NOT_FOUND,
|
|
@@ -580,30 +1159,63 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
580
1159
|
);
|
|
581
1160
|
}
|
|
582
1161
|
}
|
|
1162
|
+
/** New calls stop at close; accepted mutation sections may still finish their transactions. */
|
|
1163
|
+
#assertAccepting() {
|
|
1164
|
+
if (this.#closing || this.#closed) {
|
|
1165
|
+
throw new ReplicaError(ReplicaErrorCode.CLOSED, "The replica store is closed.");
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
583
1168
|
/** A short mutation section: guarded (profile lock) and checked against the database this store opened. */
|
|
584
1169
|
#mutate(section) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}
|
|
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
|
+
}
|
|
1181
|
+
this.#activeMutations++;
|
|
1182
|
+
return (async () => {
|
|
1183
|
+
try {
|
|
1184
|
+
return await this.#guard(async () => {
|
|
1185
|
+
await this.#assertPresent();
|
|
1186
|
+
return section();
|
|
1187
|
+
});
|
|
1188
|
+
} finally {
|
|
1189
|
+
if (--this.#activeMutations === 0) {
|
|
1190
|
+
const drained = this.#drained;
|
|
1191
|
+
this.#drained = void 0;
|
|
1192
|
+
drained?.();
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
})();
|
|
589
1196
|
}
|
|
590
1197
|
/** Run `body` in a write transaction (BEGIN IMMEDIATE). Callers outside a mutation section use `#guardedWrite`. */
|
|
591
|
-
#write(action, body) {
|
|
592
|
-
|
|
593
|
-
this.#db.exec("BEGIN IMMEDIATE");
|
|
594
|
-
} catch (error) {
|
|
595
|
-
throw storageError(error, action);
|
|
596
|
-
}
|
|
1198
|
+
async #write(action, body) {
|
|
1199
|
+
const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
|
|
597
1200
|
try {
|
|
598
|
-
const result = body();
|
|
599
|
-
this.#db.exec("COMMIT");
|
|
600
|
-
return result;
|
|
601
|
-
} catch (error) {
|
|
602
1201
|
try {
|
|
603
|
-
this.#db.exec("
|
|
604
|
-
} catch {
|
|
1202
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
throw storageError(error, action);
|
|
605
1205
|
}
|
|
606
|
-
|
|
1206
|
+
try {
|
|
1207
|
+
const result = body();
|
|
1208
|
+
this.#db.exec("COMMIT");
|
|
1209
|
+
return result;
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
try {
|
|
1212
|
+
this.#db.exec("ROLLBACK");
|
|
1213
|
+
} catch {
|
|
1214
|
+
}
|
|
1215
|
+
throw storageError(error, action);
|
|
1216
|
+
}
|
|
1217
|
+
} finally {
|
|
1218
|
+
release?.();
|
|
607
1219
|
}
|
|
608
1220
|
}
|
|
609
1221
|
/** One write transaction as its own mutation section. */
|
|
@@ -630,6 +1242,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
630
1242
|
}
|
|
631
1243
|
}
|
|
632
1244
|
async open() {
|
|
1245
|
+
this.#assertAccepting();
|
|
633
1246
|
let row;
|
|
634
1247
|
try {
|
|
635
1248
|
row = this.#row();
|
|
@@ -708,6 +1321,107 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
708
1321
|
);
|
|
709
1322
|
});
|
|
710
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
|
+
}
|
|
711
1425
|
async installGrant(g) {
|
|
712
1426
|
await this.#guardedWrite("Installing the grant", () => {
|
|
713
1427
|
const row = this.#row();
|
|
@@ -766,13 +1480,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
766
1480
|
const now = this.#now();
|
|
767
1481
|
if (row.lease_holder !== null && row.lease_expires_at !== null && row.lease_expires_at > now) return null;
|
|
768
1482
|
const token = row.lease_token + 1;
|
|
1483
|
+
const holder = this.#holder ??= `${process.pid}-${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
|
|
769
1484
|
this.#db.run(
|
|
770
1485
|
"UPDATE replica SET lease_token = ?, lease_holder = ?, lease_expires_at = ? WHERE id = 1",
|
|
771
1486
|
token,
|
|
772
|
-
|
|
1487
|
+
holder,
|
|
773
1488
|
now + ttlMs
|
|
774
1489
|
);
|
|
775
|
-
return { token, holder
|
|
1490
|
+
return { token, holder };
|
|
776
1491
|
});
|
|
777
1492
|
}
|
|
778
1493
|
async renewLease(t, ttlMs) {
|
|
@@ -791,6 +1506,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
791
1506
|
});
|
|
792
1507
|
}
|
|
793
1508
|
async hasContent(hashes) {
|
|
1509
|
+
this.#assertAccepting();
|
|
794
1510
|
const held = /* @__PURE__ */ new Set();
|
|
795
1511
|
for (let start = 0; start < hashes.length; start += 500) {
|
|
796
1512
|
const chunk = hashes.slice(start, start + 500);
|
|
@@ -803,7 +1519,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
803
1519
|
return held;
|
|
804
1520
|
}
|
|
805
1521
|
#blobPath(hash) {
|
|
806
|
-
return (0,
|
|
1522
|
+
return (0, import_node_path2.join)(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
|
|
807
1523
|
}
|
|
808
1524
|
/**
|
|
809
1525
|
* Make every blob durable before any row references it: write to a fresh
|
|
@@ -815,17 +1531,17 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
815
1531
|
const dirs = /* @__PURE__ */ new Set();
|
|
816
1532
|
for (const [hash, bytes] of blobs) {
|
|
817
1533
|
const final = this.#blobPath(hash);
|
|
818
|
-
const exists = await (0,
|
|
1534
|
+
const exists = await (0, import_promises2.stat)(final).then(
|
|
819
1535
|
() => true,
|
|
820
1536
|
() => false
|
|
821
1537
|
);
|
|
822
1538
|
if (exists) continue;
|
|
823
|
-
const shard = (0,
|
|
824
|
-
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) => {
|
|
825
1541
|
if (errnoOf(error) !== "EEXIST") throw error;
|
|
826
1542
|
});
|
|
827
|
-
const temp = (0,
|
|
828
|
-
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);
|
|
829
1545
|
try {
|
|
830
1546
|
await handle.writeFile(bytes);
|
|
831
1547
|
await handle.sync();
|
|
@@ -833,17 +1549,17 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
833
1549
|
await handle.close();
|
|
834
1550
|
}
|
|
835
1551
|
try {
|
|
836
|
-
await (0,
|
|
1552
|
+
await (0, import_promises2.link)(temp, final);
|
|
837
1553
|
} catch (error) {
|
|
838
1554
|
if (errnoOf(error) !== "EEXIST") throw error;
|
|
839
1555
|
} finally {
|
|
840
|
-
await (0,
|
|
1556
|
+
await (0, import_promises2.unlink)(temp);
|
|
841
1557
|
}
|
|
842
1558
|
dirs.add(shard);
|
|
843
|
-
dirs.add((0,
|
|
1559
|
+
dirs.add((0, import_node_path2.join)(this.#pinnedDir, "blobs"));
|
|
844
1560
|
}
|
|
845
1561
|
for (const dir of dirs) {
|
|
846
|
-
const handle = await (0,
|
|
1562
|
+
const handle = await (0, import_promises2.open)(dir, import_node_fs2.constants.O_RDONLY);
|
|
847
1563
|
try {
|
|
848
1564
|
await handle.sync();
|
|
849
1565
|
} finally {
|
|
@@ -854,6 +1570,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
854
1570
|
async applyPage(t, p) {
|
|
855
1571
|
await this.#mutate(async () => {
|
|
856
1572
|
this.#checkLease(t);
|
|
1573
|
+
this.#assertInstalledGrantTime();
|
|
857
1574
|
const reused = /* @__PURE__ */ new Map();
|
|
858
1575
|
for (const change of p.changes) {
|
|
859
1576
|
if (change.deleted || !change.content || p.blobs.has(change.hash) || reused.has(change.hash)) continue;
|
|
@@ -873,8 +1590,9 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
873
1590
|
}
|
|
874
1591
|
await this.#faults.afterBlobs?.();
|
|
875
1592
|
try {
|
|
876
|
-
this.#write("Committing a sync page", () => {
|
|
1593
|
+
await this.#write("Committing a sync page", () => {
|
|
877
1594
|
this.#checkLease(t);
|
|
1595
|
+
this.#assertInstalledGrantTime();
|
|
878
1596
|
const now = this.#now();
|
|
879
1597
|
if (p.window.notBefore !== null && now < Date.parse(p.window.notBefore)) {
|
|
880
1598
|
throw new ReplicaError(ReplicaErrorCode.GRANT_NOT_YET_VALID, `The grant is not valid before ${p.window.notBefore}; nothing was committed.`);
|
|
@@ -948,12 +1666,12 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
948
1666
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
949
1667
|
async #unlinkBlobs(keep) {
|
|
950
1668
|
let removed = 0;
|
|
951
|
-
const root = (0,
|
|
1669
|
+
const root = (0, import_node_path2.join)(this.#pinnedDir, "blobs");
|
|
952
1670
|
for (const shard of await entriesOf(root)) {
|
|
953
|
-
const shardPath = (0,
|
|
1671
|
+
const shardPath = (0, import_node_path2.join)(root, shard);
|
|
954
1672
|
if (shard === ".tmp") {
|
|
955
1673
|
for (const name of await entriesOf(shardPath)) {
|
|
956
|
-
await (0,
|
|
1674
|
+
await (0, import_promises2.rm)((0, import_node_path2.join)(shardPath, name), { force: true });
|
|
957
1675
|
removed += 1;
|
|
958
1676
|
}
|
|
959
1677
|
continue;
|
|
@@ -964,30 +1682,33 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
964
1682
|
kept += 1;
|
|
965
1683
|
continue;
|
|
966
1684
|
}
|
|
967
|
-
await (0,
|
|
1685
|
+
await (0, import_promises2.rm)((0, import_node_path2.join)(shardPath, name), { force: true });
|
|
968
1686
|
removed += 1;
|
|
969
1687
|
}
|
|
970
|
-
if (kept === 0) await (0,
|
|
1688
|
+
if (kept === 0) await (0, import_promises2.rmdir)(shardPath).catch(() => void 0);
|
|
971
1689
|
}
|
|
972
1690
|
return removed;
|
|
973
1691
|
}
|
|
974
1692
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
975
1693
|
async #holdsContentFiles() {
|
|
976
|
-
const root = (0,
|
|
1694
|
+
const root = (0, import_node_path2.join)(this.#pinnedDir, "blobs");
|
|
977
1695
|
for (const shard of await entriesOf(root)) {
|
|
978
|
-
if ((await entriesOf((0,
|
|
1696
|
+
if ((await entriesOf((0, import_node_path2.join)(root, shard))).length > 0) return true;
|
|
979
1697
|
}
|
|
980
1698
|
return false;
|
|
981
1699
|
}
|
|
982
1700
|
async collectGarbage(t) {
|
|
983
1701
|
return this.#mutate(async () => {
|
|
1702
|
+
const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
|
|
984
1703
|
try {
|
|
985
1704
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
986
1705
|
} catch (error) {
|
|
1706
|
+
release?.();
|
|
987
1707
|
throw storageError(error, "Collecting unused content");
|
|
988
1708
|
}
|
|
989
1709
|
try {
|
|
990
1710
|
this.#checkLease(t);
|
|
1711
|
+
await this.#faults.afterGarbageCollectionBegin?.();
|
|
991
1712
|
const keep = new Set(
|
|
992
1713
|
this.#db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash)
|
|
993
1714
|
);
|
|
@@ -1000,12 +1721,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1000
1721
|
} catch {
|
|
1001
1722
|
}
|
|
1002
1723
|
throw storageError(error, "Collecting unused content");
|
|
1724
|
+
} finally {
|
|
1725
|
+
release?.();
|
|
1003
1726
|
}
|
|
1004
1727
|
});
|
|
1005
1728
|
}
|
|
1006
1729
|
async reset(t, reason, options = {}) {
|
|
1007
1730
|
await this.#mutate(async () => {
|
|
1008
|
-
this.#write("Resetting the replica", () => {
|
|
1731
|
+
await this.#write("Resetting the replica", () => {
|
|
1009
1732
|
this.#checkLease(t);
|
|
1010
1733
|
this.#db.run("DELETE FROM entry");
|
|
1011
1734
|
this.#db.run(
|
|
@@ -1024,107 +1747,50 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1024
1747
|
}
|
|
1025
1748
|
}
|
|
1026
1749
|
/**
|
|
1027
|
-
* Remove
|
|
1028
|
-
*
|
|
1029
|
-
* store opened, checked under the write lock, so a paused process can
|
|
1030
|
-
* never remove a replica another process took over or recreated. Allowed
|
|
1031
|
-
* 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.
|
|
1032
1752
|
*/
|
|
1033
|
-
async destroy(t) {
|
|
1753
|
+
async destroy(t, options = {}) {
|
|
1034
1754
|
await this.#mutate(async () => {
|
|
1755
|
+
const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
|
|
1035
1756
|
try {
|
|
1036
1757
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
1037
1758
|
} catch (error) {
|
|
1759
|
+
release?.();
|
|
1038
1760
|
throw storageError(error, "Removing the replica");
|
|
1039
1761
|
}
|
|
1040
1762
|
try {
|
|
1041
1763
|
this.#checkLease(t, { allowRevoked: true });
|
|
1042
|
-
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 });
|
|
1043
1765
|
if (!expected) {
|
|
1044
1766
|
throw new ReplicaError(ReplicaErrorCode.RUNTIME_UNSUPPORTED, "Replica removal requires an opened or identity-checked directory.");
|
|
1045
1767
|
}
|
|
1046
1768
|
const assertDirectory = async () => {
|
|
1047
|
-
const current = await (0,
|
|
1769
|
+
const current = await (0, import_promises2.lstat)(this.dir, { bigint: true });
|
|
1048
1770
|
if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
1049
1771
|
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica directory changed during removal.");
|
|
1050
1772
|
}
|
|
1051
1773
|
return current;
|
|
1052
1774
|
};
|
|
1053
1775
|
const pinned = await assertDirectory();
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
await assertDirectory();
|
|
1060
|
-
await (0, import_promises.rmdir)(this.dir);
|
|
1061
|
-
} else {
|
|
1062
|
-
const ancestors = this.#namedAncestors;
|
|
1063
|
-
const checkAncestors = async () => {
|
|
1064
|
-
for (const expected2 of ancestors) {
|
|
1065
|
-
const current = await (0, import_promises.lstat)(expected2.path, { bigint: true });
|
|
1066
|
-
if (current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
1067
|
-
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica ancestry changed during removal.");
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
1071
|
-
const directories = /* @__PURE__ */ new Map([[this.dir, pinned]]);
|
|
1072
|
-
const files = /* @__PURE__ */ new Map();
|
|
1073
|
-
const check = async (path, expected2, directory) => {
|
|
1074
|
-
if (path !== this.dir) {
|
|
1075
|
-
const parent = (0, import_node_path.dirname)(path);
|
|
1076
|
-
await check(parent, directories.get(parent), true);
|
|
1077
|
-
}
|
|
1078
|
-
await checkAncestors();
|
|
1079
|
-
const current = await (0, import_promises.lstat)(path, { bigint: true });
|
|
1080
|
-
if ((directory ? !current.isDirectory() : !current.isFile()) || current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
1081
|
-
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica layout changed during removal.");
|
|
1082
|
-
}
|
|
1083
|
-
};
|
|
1084
|
-
const record = async (path, directory) => {
|
|
1085
|
-
const parent = (0, import_node_path.dirname)(path);
|
|
1086
|
-
await check(parent, directories.get(parent), true);
|
|
1087
|
-
const current = await (0, import_promises.lstat)(path, { bigint: true });
|
|
1088
|
-
if (!(directory ? current.isDirectory() : current.isFile())) {
|
|
1089
|
-
throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
1090
|
-
}
|
|
1091
|
-
(directory ? directories : files).set(path, current);
|
|
1092
|
-
};
|
|
1093
|
-
const blobs = (0, import_node_path.join)(this.dir, "blobs");
|
|
1094
|
-
for (const name of await (0, import_promises.readdir)(this.dir)) {
|
|
1095
|
-
if (name === "blobs") await record(blobs, true);
|
|
1096
|
-
else if (["replica.db", "replica.db-wal", "replica.db-shm"].includes(name)) await record((0, import_node_path.join)(this.dir, name), false);
|
|
1097
|
-
else throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
1098
|
-
}
|
|
1099
|
-
if (directories.has(blobs)) {
|
|
1100
|
-
await check(blobs, directories.get(blobs), true);
|
|
1101
|
-
for (const shard of await (0, import_promises.readdir)(blobs)) {
|
|
1102
|
-
if (shard !== ".tmp" && !/^[0-9a-f]{2}$/.test(shard)) {
|
|
1103
|
-
throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica blob shard during removal.");
|
|
1104
|
-
}
|
|
1105
|
-
const shardPath = (0, import_node_path.join)(blobs, shard);
|
|
1106
|
-
await record(shardPath, true);
|
|
1107
|
-
await check(shardPath, directories.get(shardPath), true);
|
|
1108
|
-
for (const name of await (0, import_promises.readdir)(shardPath)) {
|
|
1109
|
-
if (shard === ".tmp" ? !/^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name) : !isHash(name) || !name.startsWith(shard)) {
|
|
1110
|
-
throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica blob file during removal.");
|
|
1111
|
-
}
|
|
1112
|
-
await record((0, import_node_path.join)(shardPath, name), false);
|
|
1113
|
-
}
|
|
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.");
|
|
1114
1781
|
}
|
|
1115
1782
|
}
|
|
1116
|
-
for (const [path, identity] of files) {
|
|
1117
|
-
await check(path, identity, false);
|
|
1118
|
-
await (0, import_promises.unlink)(path);
|
|
1119
|
-
}
|
|
1120
|
-
for (const [path, identity] of [...directories].reverse()) {
|
|
1121
|
-
if (path === this.dir) continue;
|
|
1122
|
-
await check(path, identity, true);
|
|
1123
|
-
await (0, import_promises.rmdir)(path);
|
|
1124
|
-
}
|
|
1125
1783
|
await assertDirectory();
|
|
1126
|
-
|
|
1127
|
-
|
|
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;
|
|
1128
1794
|
}
|
|
1129
1795
|
} catch (error) {
|
|
1130
1796
|
throw storageError(error, "Removing the replica");
|
|
@@ -1133,6 +1799,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1133
1799
|
this.#db.exec("ROLLBACK");
|
|
1134
1800
|
} catch {
|
|
1135
1801
|
}
|
|
1802
|
+
release?.();
|
|
1136
1803
|
}
|
|
1137
1804
|
});
|
|
1138
1805
|
}
|
|
@@ -1176,7 +1843,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1176
1843
|
* when both are done.
|
|
1177
1844
|
*/
|
|
1178
1845
|
async #purgeContent() {
|
|
1179
|
-
this.#write("Starting the revocation purge", () => {
|
|
1846
|
+
await this.#write("Starting the revocation purge", () => {
|
|
1180
1847
|
this.#db.run("UPDATE replica SET purge_pending = 1 WHERE id = 1 AND revoked_detail IS NOT NULL");
|
|
1181
1848
|
});
|
|
1182
1849
|
try {
|
|
@@ -1187,7 +1854,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1187
1854
|
if (!this.#checkpoint()) {
|
|
1188
1855
|
throw new ReplicaError(ReplicaErrorCode.BUSY, "A reader holds the replica's write-ahead log; the purge finishes on the next open.");
|
|
1189
1856
|
}
|
|
1190
|
-
this.#write("Finishing the revocation purge", () => {
|
|
1857
|
+
await this.#write("Finishing the revocation purge", () => {
|
|
1191
1858
|
this.#db.run("UPDATE replica SET purge_pending = 0 WHERE id = 1 AND revoked_detail IS NOT NULL");
|
|
1192
1859
|
});
|
|
1193
1860
|
}
|
|
@@ -1197,6 +1864,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1197
1864
|
});
|
|
1198
1865
|
}
|
|
1199
1866
|
async get(key) {
|
|
1867
|
+
this.#assertAccepting();
|
|
1200
1868
|
try {
|
|
1201
1869
|
const row = this.#db.get("SELECT key, deleted, etag, hash, metadata, content FROM entry WHERE key = ?", key);
|
|
1202
1870
|
return row === void 0 ? void 0 : entryOf(row);
|
|
@@ -1205,6 +1873,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1205
1873
|
}
|
|
1206
1874
|
}
|
|
1207
1875
|
async list(o) {
|
|
1876
|
+
this.#assertAccepting();
|
|
1208
1877
|
const where = ["deleted = 0"];
|
|
1209
1878
|
const params = [];
|
|
1210
1879
|
if (o.prefix !== void 0 && o.prefix !== "") {
|
|
@@ -1234,7 +1903,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1234
1903
|
async readContent(hash) {
|
|
1235
1904
|
if (!isHash(hash)) return void 0;
|
|
1236
1905
|
try {
|
|
1237
|
-
return new Uint8Array(await (0,
|
|
1906
|
+
return new Uint8Array(await (0, import_promises2.readFile)(this.#blobPath(hash)));
|
|
1238
1907
|
} catch (error) {
|
|
1239
1908
|
if (errnoOf(error) === "ENOENT") return void 0;
|
|
1240
1909
|
throw storageError(error, "Reading replica content");
|
|
@@ -1290,22 +1959,40 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1290
1959
|
lastReset: state.lastReset
|
|
1291
1960
|
};
|
|
1292
1961
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
this.#
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1962
|
+
close() {
|
|
1963
|
+
if (this.#closePromise) return this.#closePromise;
|
|
1964
|
+
this.#closing = true;
|
|
1965
|
+
this.#closePromise = (async () => {
|
|
1966
|
+
if (this.#activeMutations !== 0) {
|
|
1967
|
+
await new Promise((resolve3) => {
|
|
1968
|
+
this.#drained = resolve3;
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
this.#closed = true;
|
|
1972
|
+
const fd = this.#directoryFd;
|
|
1973
|
+
this.#directoryFd = void 0;
|
|
1974
|
+
if (this.#shared) {
|
|
1975
|
+
sharedStores.unregister(this);
|
|
1976
|
+
releaseWritable(this.#shared);
|
|
1977
|
+
} else {
|
|
1978
|
+
if (fd !== void 0) pinnedDirectories.unregister(this);
|
|
1979
|
+
try {
|
|
1980
|
+
this.#database.close();
|
|
1981
|
+
} finally {
|
|
1982
|
+
if (fd !== void 0) (0, import_node_fs2.closeSync)(fd);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
})();
|
|
1986
|
+
return this.#closePromise;
|
|
1302
1987
|
}
|
|
1303
1988
|
};
|
|
1304
1989
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1305
1990
|
0 && (module.exports = {
|
|
1306
1991
|
DURABILITY,
|
|
1307
1992
|
SqliteReplicaStore,
|
|
1993
|
+
inspectSqliteReplicaLayout,
|
|
1308
1994
|
loadSqlite,
|
|
1309
|
-
nodeSqliteSupported
|
|
1995
|
+
nodeSqliteSupported,
|
|
1996
|
+
removeSqliteReplicaLayout
|
|
1310
1997
|
});
|
|
1311
1998
|
//# sourceMappingURL=sqlite.cjs.map
|