@tinycloud/replica 0.2.4-beta.1 → 0.2.4-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.ts +336 -0
- package/dist/replica.worker.js +1 -1
- package/dist/replica.worker.js.map +1 -1
- package/dist/sqlite.cjs +264 -53
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +5 -0
- package/dist/sqlite.d.ts +5 -0
- package/dist/sqlite.js +267 -56
- package/dist/sqlite.js.map +1 -1
- package/package.json +1 -1
package/dist/sqlite.cjs
CHANGED
|
@@ -228,6 +228,29 @@ async function loadSqlite() {
|
|
|
228
228
|
|
|
229
229
|
// src/sqlite/store.ts
|
|
230
230
|
var SCHEMA_VERSION = 1;
|
|
231
|
+
var sqliteOpenQueue = Promise.resolve();
|
|
232
|
+
async function acquireSqliteOpen() {
|
|
233
|
+
const previous = sqliteOpenQueue;
|
|
234
|
+
let release;
|
|
235
|
+
sqliteOpenQueue = new Promise((resolve2) => {
|
|
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
|
+
}
|
|
231
254
|
var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
|
|
232
255
|
var SCHEMA = `
|
|
233
256
|
CREATE TABLE IF NOT EXISTS replica (
|
|
@@ -326,22 +349,37 @@ async function entriesOf(path) {
|
|
|
326
349
|
}
|
|
327
350
|
}
|
|
328
351
|
var FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.faults");
|
|
352
|
+
var pinnedDirectories = new FinalizationRegistry((fd) => {
|
|
353
|
+
try {
|
|
354
|
+
(0, import_node_fs.closeSync)(fd);
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
});
|
|
329
358
|
var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
330
359
|
dir;
|
|
331
360
|
#db;
|
|
361
|
+
#pinnedDir;
|
|
362
|
+
#directoryFd;
|
|
332
363
|
#holder = `${process.pid}-${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
|
|
333
364
|
#now;
|
|
334
365
|
#faults;
|
|
335
366
|
#guard;
|
|
336
367
|
/** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
|
|
337
368
|
#ino;
|
|
338
|
-
|
|
369
|
+
#dev;
|
|
370
|
+
#namedAncestors;
|
|
371
|
+
constructor(dir, db, identity, options, directoryFd, namedAncestors = []) {
|
|
339
372
|
this.dir = dir;
|
|
373
|
+
this.#pinnedDir = directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir;
|
|
374
|
+
this.#directoryFd = directoryFd;
|
|
375
|
+
if (directoryFd !== void 0) pinnedDirectories.register(this, directoryFd, this);
|
|
340
376
|
this.#db = db;
|
|
341
|
-
this.#ino = ino;
|
|
377
|
+
this.#ino = identity.ino;
|
|
378
|
+
this.#dev = identity.dev;
|
|
342
379
|
this.#now = options.now ?? Date.now;
|
|
343
380
|
this.#faults = options[FAULTS] ?? {};
|
|
344
381
|
this.#guard = options.guard ?? ((section) => section());
|
|
382
|
+
this.#namedAncestors = namedAncestors;
|
|
345
383
|
}
|
|
346
384
|
/**
|
|
347
385
|
* Read-only status snapshot; does not create a replica, migrate, recover
|
|
@@ -354,9 +392,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
354
392
|
};
|
|
355
393
|
checkAbort();
|
|
356
394
|
const dbPath = (0, import_node_path.join)(dir, "replica.db");
|
|
357
|
-
let
|
|
395
|
+
let identity;
|
|
358
396
|
try {
|
|
359
|
-
|
|
397
|
+
identity = await (0, import_promises.lstat)(dbPath);
|
|
398
|
+
if (!identity.isFile()) return null;
|
|
360
399
|
} catch (error) {
|
|
361
400
|
if (errnoOf(error) === "ENOENT") return null;
|
|
362
401
|
throw storageError(error, "Inspecting the replica");
|
|
@@ -369,14 +408,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
369
408
|
db = opener(dbPath, { readonly: true });
|
|
370
409
|
db.exec("PRAGMA busy_timeout = 0");
|
|
371
410
|
checkAbort();
|
|
372
|
-
const store = new _SqliteReplicaStore(dir, db,
|
|
411
|
+
const store = new _SqliteReplicaStore(dir, db, identity, { create: false, ...options.now ? { now: options.now } : {} });
|
|
373
412
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
374
413
|
const before = db.get("PRAGMA data_version").data_version;
|
|
375
414
|
const state = await store.open();
|
|
376
415
|
checkAbort();
|
|
377
416
|
if (state === null) return null;
|
|
378
417
|
const status = await store.status();
|
|
379
|
-
await new Promise((
|
|
418
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
380
419
|
checkAbort();
|
|
381
420
|
const after = db.get("PRAGMA data_version").data_version;
|
|
382
421
|
if (before === after) return { state, status };
|
|
@@ -396,65 +435,145 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
396
435
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
397
436
|
*/
|
|
398
437
|
static async open(dir, options) {
|
|
399
|
-
const
|
|
438
|
+
const namedDbPath = (0, import_node_path.join)(dir, "replica.db");
|
|
400
439
|
if (!options.create) {
|
|
401
|
-
const exists = await (0, import_promises.
|
|
402
|
-
() =>
|
|
440
|
+
const exists = await (0, import_promises.lstat)(namedDbPath).then(
|
|
441
|
+
(stats) => stats.isFile(),
|
|
403
442
|
() => false
|
|
404
443
|
);
|
|
405
|
-
if (!exists) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `No replica at ${dir}.`);
|
|
444
|
+
if (!exists) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `No regular replica database at ${dir}.`);
|
|
406
445
|
}
|
|
407
446
|
const opener = options.sqlite ?? await loadSqlite();
|
|
447
|
+
const releaseOpen = await acquireSqliteOpen();
|
|
408
448
|
let store;
|
|
449
|
+
let directoryFd;
|
|
409
450
|
try {
|
|
410
|
-
if (options.create) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const handle = await (0, import_promises.open)(dbPath, import_node_fs.constants.O_CREAT | import_node_fs.constants.O_RDWR, 384);
|
|
414
|
-
await handle.close();
|
|
415
|
-
await (0, import_promises.chmod)(dbPath, 384);
|
|
451
|
+
if (options.create) await (0, import_promises.mkdir)(dir, { recursive: true, mode: 448 });
|
|
452
|
+
if (process.platform === "linux") {
|
|
453
|
+
directoryFd = (0, import_node_fs.openSync)(dir, import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_DIRECTORY | import_node_fs.constants.O_NOFOLLOW);
|
|
416
454
|
}
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
425
|
-
if (version > SCHEMA_VERSION) {
|
|
426
|
-
db.close();
|
|
427
|
-
throw new ReplicaError(
|
|
428
|
-
ReplicaErrorCode.STORAGE_ERROR,
|
|
429
|
-
`The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
|
|
430
|
-
);
|
|
455
|
+
const opened = directoryFd === void 0 ? (0, import_node_fs.lstatSync)(dir, { bigint: true }) : (0, import_node_fs.fstatSync)(directoryFd, { bigint: true });
|
|
456
|
+
if (!opened.isDirectory() || opened.isSymbolicLink() || options.expectedDirectory && (opened.dev !== BigInt(options.expectedDirectory.dev) || opened.ino !== BigInt(options.expectedDirectory.ino))) {
|
|
457
|
+
throw new Error("The replica directory changed before SQLite opened it.");
|
|
458
|
+
}
|
|
459
|
+
const namedAncestors = process.platform === "linux" ? [] : await captureNamedPath(dir);
|
|
460
|
+
if (namedAncestors.length && (namedAncestors[namedAncestors.length - 1].dev !== opened.dev || namedAncestors[namedAncestors.length - 1].ino !== opened.ino)) {
|
|
461
|
+
throw new Error("The replica directory changed while checking its ancestry.");
|
|
431
462
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
463
|
+
const pinnedDir = directoryFd === void 0 ? dir : `/proc/self/fd/${directoryFd}`;
|
|
464
|
+
const dbPath = (0, import_node_path.join)(pinnedDir, "replica.db");
|
|
465
|
+
if (options.create) {
|
|
466
|
+
await (0, import_promises.mkdir)((0, import_node_path.join)(pinnedDir, "blobs", ".tmp"), { recursive: true, mode: 448 });
|
|
467
|
+
await Promise.all([(0, import_promises.chmod)(pinnedDir, 448), (0, import_promises.chmod)((0, import_node_path.join)(pinnedDir, "blobs"), 448), (0, import_promises.chmod)((0, import_node_path.join)(pinnedDir, "blobs", ".tmp"), 448)]);
|
|
468
|
+
const handle = await (0, import_promises.open)(dbPath, import_node_fs.constants.O_CREAT | import_node_fs.constants.O_EXCL | import_node_fs.constants.O_RDWR | import_node_fs.constants.O_NOFOLLOW, 384).catch((error) => {
|
|
469
|
+
if (errnoOf(error) === "EEXIST") return null;
|
|
470
|
+
throw error;
|
|
471
|
+
});
|
|
472
|
+
if (handle) {
|
|
473
|
+
try {
|
|
474
|
+
await handle.chmod(384);
|
|
475
|
+
} finally {
|
|
476
|
+
await handle.close();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const leaf = await (0, import_promises.open)(dbPath, import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_NOFOLLOW);
|
|
481
|
+
let db;
|
|
482
|
+
try {
|
|
483
|
+
const identity = await leaf.stat();
|
|
484
|
+
const named = await (0, import_promises.lstat)(dbPath);
|
|
485
|
+
if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
|
|
486
|
+
throw new Error("The replica database changed before SQLite opened it.");
|
|
487
|
+
}
|
|
488
|
+
const previousFds = process.platform === "linux" && !options.sqlite ? new Map((0, import_node_fs.readdirSync)("/proc/self/fd").map((fd) => {
|
|
489
|
+
try {
|
|
490
|
+
return [fd, (0, import_node_fs.readlinkSync)(`/proc/self/fd/${fd}`)];
|
|
491
|
+
} catch {
|
|
492
|
+
return [fd, ""];
|
|
493
|
+
}
|
|
494
|
+
})) : null;
|
|
495
|
+
db = opener(dbPath);
|
|
496
|
+
const afterOpen = (0, import_node_fs.lstatSync)(dbPath);
|
|
497
|
+
if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
|
|
498
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
499
|
+
}
|
|
500
|
+
if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
|
|
501
|
+
if (previousFds) {
|
|
502
|
+
const databases = (0, import_node_fs.readdirSync)("/proc/self/fd").filter((fd) => {
|
|
503
|
+
try {
|
|
504
|
+
const target = (0, import_node_fs.readlinkSync)(`/proc/self/fd/${fd}`);
|
|
505
|
+
return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
if (databases.length === 0 || databases.some((fd) => {
|
|
511
|
+
try {
|
|
512
|
+
const actual = (0, import_node_fs.fstatSync)(Number(fd));
|
|
513
|
+
return actual.dev !== identity.dev || actual.ino !== identity.ino;
|
|
514
|
+
} catch {
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
})) {
|
|
518
|
+
throw new Error("SQLite opened a different replica database.");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
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
|
+
} finally {
|
|
546
|
+
try {
|
|
547
|
+
db?.close();
|
|
548
|
+
} finally {
|
|
549
|
+
await leaf.close();
|
|
550
|
+
}
|
|
437
551
|
}
|
|
438
|
-
store = new _SqliteReplicaStore(dir, db, ino, options);
|
|
439
552
|
} catch (error) {
|
|
553
|
+
if (store) await store.close().catch(() => void 0);
|
|
554
|
+
else if (directoryFd !== void 0) (0, import_node_fs.closeSync)(directoryFd);
|
|
440
555
|
throw storageError(error, `Opening the replica at ${dir}`);
|
|
556
|
+
} finally {
|
|
557
|
+
releaseOpen();
|
|
441
558
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
await store.#
|
|
559
|
+
try {
|
|
560
|
+
const row = store.#row();
|
|
561
|
+
if (row?.revoked_detail != null && (row.purge_pending === 1 || await store.#holdsContentFiles().catch(() => true))) {
|
|
562
|
+
await store.#finishRevocationPurge().catch(() => void 0);
|
|
563
|
+
}
|
|
564
|
+
return store;
|
|
565
|
+
} catch (error) {
|
|
566
|
+
await store.close().catch(() => void 0);
|
|
567
|
+
throw error;
|
|
445
568
|
}
|
|
446
|
-
return store;
|
|
447
569
|
}
|
|
448
570
|
#row() {
|
|
449
571
|
return this.#db.get("SELECT * FROM replica WHERE id = 1");
|
|
450
572
|
}
|
|
451
573
|
/** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
|
|
452
574
|
async #assertPresent() {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
() => void 0
|
|
456
|
-
);
|
|
457
|
-
if (ino !== this.#ino) {
|
|
575
|
+
const identity = await (0, import_promises.lstat)((0, import_node_path.join)(this.#pinnedDir, "replica.db")).catch(() => null);
|
|
576
|
+
if (!identity?.isFile() || identity.dev !== this.#dev || identity.ino !== this.#ino) {
|
|
458
577
|
throw new ReplicaError(
|
|
459
578
|
ReplicaErrorCode.NOT_FOUND,
|
|
460
579
|
`The replica at ${this.dir} was deleted while this command ran (its profile was deleted or the replica purged); nothing more was written.`
|
|
@@ -684,7 +803,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
684
803
|
return held;
|
|
685
804
|
}
|
|
686
805
|
#blobPath(hash) {
|
|
687
|
-
return (0, import_node_path.join)(this
|
|
806
|
+
return (0, import_node_path.join)(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
|
|
688
807
|
}
|
|
689
808
|
/**
|
|
690
809
|
* Make every blob durable before any row references it: write to a fresh
|
|
@@ -701,11 +820,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
701
820
|
() => false
|
|
702
821
|
);
|
|
703
822
|
if (exists) continue;
|
|
704
|
-
const shard = (0, import_node_path.join)(this
|
|
823
|
+
const shard = (0, import_node_path.join)(this.#pinnedDir, "blobs", hash.slice(0, 2));
|
|
705
824
|
await (0, import_promises.mkdir)(shard, { mode: 448 }).catch((error) => {
|
|
706
825
|
if (errnoOf(error) !== "EEXIST") throw error;
|
|
707
826
|
});
|
|
708
|
-
const temp = (0, import_node_path.join)(this
|
|
827
|
+
const temp = (0, import_node_path.join)(this.#pinnedDir, "blobs", ".tmp", `${hash}.${(0, import_node_crypto.randomBytes)(6).toString("hex")}`);
|
|
709
828
|
const handle = await (0, import_promises.open)(temp, import_node_fs.constants.O_CREAT | import_node_fs.constants.O_EXCL | import_node_fs.constants.O_WRONLY, 384);
|
|
710
829
|
try {
|
|
711
830
|
await handle.writeFile(bytes);
|
|
@@ -721,7 +840,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
721
840
|
await (0, import_promises.unlink)(temp);
|
|
722
841
|
}
|
|
723
842
|
dirs.add(shard);
|
|
724
|
-
dirs.add((0, import_node_path.join)(this
|
|
843
|
+
dirs.add((0, import_node_path.join)(this.#pinnedDir, "blobs"));
|
|
725
844
|
}
|
|
726
845
|
for (const dir of dirs) {
|
|
727
846
|
const handle = await (0, import_promises.open)(dir, import_node_fs.constants.O_RDONLY);
|
|
@@ -829,7 +948,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
829
948
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
830
949
|
async #unlinkBlobs(keep) {
|
|
831
950
|
let removed = 0;
|
|
832
|
-
const root = (0, import_node_path.join)(this
|
|
951
|
+
const root = (0, import_node_path.join)(this.#pinnedDir, "blobs");
|
|
833
952
|
for (const shard of await entriesOf(root)) {
|
|
834
953
|
const shardPath = (0, import_node_path.join)(root, shard);
|
|
835
954
|
if (shard === ".tmp") {
|
|
@@ -854,7 +973,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
854
973
|
}
|
|
855
974
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
856
975
|
async #holdsContentFiles() {
|
|
857
|
-
const root = (0, import_node_path.join)(this
|
|
976
|
+
const root = (0, import_node_path.join)(this.#pinnedDir, "blobs");
|
|
858
977
|
for (const shard of await entriesOf(root)) {
|
|
859
978
|
if ((await entriesOf((0, import_node_path.join)(root, shard))).length > 0) return true;
|
|
860
979
|
}
|
|
@@ -920,8 +1039,93 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
920
1039
|
}
|
|
921
1040
|
try {
|
|
922
1041
|
this.#checkLease(t, { allowRevoked: true });
|
|
1042
|
+
const expected = this.#directoryFd === void 0 ? this.#namedAncestors.at(-1) : (0, import_node_fs.fstatSync)(this.#directoryFd, { bigint: true });
|
|
1043
|
+
if (!expected) {
|
|
1044
|
+
throw new ReplicaError(ReplicaErrorCode.RUNTIME_UNSUPPORTED, "Replica removal requires an opened or identity-checked directory.");
|
|
1045
|
+
}
|
|
1046
|
+
const assertDirectory = async () => {
|
|
1047
|
+
const current = await (0, import_promises.lstat)(this.dir, { bigint: true });
|
|
1048
|
+
if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
1049
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica directory changed during removal.");
|
|
1050
|
+
}
|
|
1051
|
+
return current;
|
|
1052
|
+
};
|
|
1053
|
+
const pinned = await assertDirectory();
|
|
923
1054
|
await this.#assertPresent();
|
|
924
|
-
|
|
1055
|
+
if (process.platform === "linux") {
|
|
1056
|
+
for (const entry of await (0, import_promises.readdir)(this.#pinnedDir)) {
|
|
1057
|
+
await (0, import_promises.rm)((0, import_node_path.join)(this.#pinnedDir, entry), { recursive: true, force: true });
|
|
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
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
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
|
+
await assertDirectory();
|
|
1126
|
+
await checkAncestors();
|
|
1127
|
+
await (0, import_promises.rmdir)(this.dir);
|
|
1128
|
+
}
|
|
925
1129
|
} catch (error) {
|
|
926
1130
|
throw storageError(error, "Removing the replica");
|
|
927
1131
|
} finally {
|
|
@@ -1087,7 +1291,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1087
1291
|
};
|
|
1088
1292
|
}
|
|
1089
1293
|
async close() {
|
|
1090
|
-
this.#
|
|
1294
|
+
const fd = this.#directoryFd;
|
|
1295
|
+
this.#directoryFd = void 0;
|
|
1296
|
+
if (fd !== void 0) pinnedDirectories.unregister(this);
|
|
1297
|
+
try {
|
|
1298
|
+
this.#db.close();
|
|
1299
|
+
} finally {
|
|
1300
|
+
if (fd !== void 0) (0, import_node_fs.closeSync)(fd);
|
|
1301
|
+
}
|
|
1091
1302
|
}
|
|
1092
1303
|
};
|
|
1093
1304
|
// Annotate the CommonJS export names for ESM import in node:
|