@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.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/sqlite/store.ts
|
|
2
2
|
import { randomBytes } from "crypto";
|
|
3
|
-
import { constants as fsConstants } from "fs";
|
|
4
|
-
import { chmod, link, mkdir, open, readFile, readdir, rm, rmdir, stat, unlink } from "fs/promises";
|
|
5
|
-
import { join } from "path";
|
|
3
|
+
import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, readdirSync, readlinkSync } from "fs";
|
|
4
|
+
import { chmod, link, lstat, mkdir, open, readFile, readdir, rm, rmdir, stat, unlink } from "fs/promises";
|
|
5
|
+
import { dirname, join, resolve } from "path";
|
|
6
6
|
|
|
7
7
|
// src/errors.ts
|
|
8
8
|
var ReplicaErrorCode = {
|
|
@@ -199,6 +199,29 @@ async function loadSqlite() {
|
|
|
199
199
|
|
|
200
200
|
// src/sqlite/store.ts
|
|
201
201
|
var SCHEMA_VERSION = 1;
|
|
202
|
+
var sqliteOpenQueue = Promise.resolve();
|
|
203
|
+
async function acquireSqliteOpen() {
|
|
204
|
+
const previous = sqliteOpenQueue;
|
|
205
|
+
let release;
|
|
206
|
+
sqliteOpenQueue = new Promise((resolve2) => {
|
|
207
|
+
release = resolve2;
|
|
208
|
+
});
|
|
209
|
+
await previous;
|
|
210
|
+
return release;
|
|
211
|
+
}
|
|
212
|
+
async function captureNamedPath(path) {
|
|
213
|
+
const names = [];
|
|
214
|
+
for (let current = resolve(path); ; current = dirname(current)) {
|
|
215
|
+
names.push(current);
|
|
216
|
+
if (dirname(current) === current) break;
|
|
217
|
+
}
|
|
218
|
+
const identities = [];
|
|
219
|
+
for (const name of names.reverse()) {
|
|
220
|
+
const stats = await lstat(name, { bigint: true });
|
|
221
|
+
identities.push({ path: name, dev: stats.dev, ino: stats.ino });
|
|
222
|
+
}
|
|
223
|
+
return identities;
|
|
224
|
+
}
|
|
202
225
|
var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
|
|
203
226
|
var SCHEMA = `
|
|
204
227
|
CREATE TABLE IF NOT EXISTS replica (
|
|
@@ -297,22 +320,37 @@ async function entriesOf(path) {
|
|
|
297
320
|
}
|
|
298
321
|
}
|
|
299
322
|
var FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.faults");
|
|
323
|
+
var pinnedDirectories = new FinalizationRegistry((fd) => {
|
|
324
|
+
try {
|
|
325
|
+
closeSync(fd);
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
});
|
|
300
329
|
var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
301
330
|
dir;
|
|
302
331
|
#db;
|
|
332
|
+
#pinnedDir;
|
|
333
|
+
#directoryFd;
|
|
303
334
|
#holder = `${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
304
335
|
#now;
|
|
305
336
|
#faults;
|
|
306
337
|
#guard;
|
|
307
338
|
/** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
|
|
308
339
|
#ino;
|
|
309
|
-
|
|
340
|
+
#dev;
|
|
341
|
+
#namedAncestors;
|
|
342
|
+
constructor(dir, db, identity, options, directoryFd, namedAncestors = []) {
|
|
310
343
|
this.dir = dir;
|
|
344
|
+
this.#pinnedDir = directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir;
|
|
345
|
+
this.#directoryFd = directoryFd;
|
|
346
|
+
if (directoryFd !== void 0) pinnedDirectories.register(this, directoryFd, this);
|
|
311
347
|
this.#db = db;
|
|
312
|
-
this.#ino = ino;
|
|
348
|
+
this.#ino = identity.ino;
|
|
349
|
+
this.#dev = identity.dev;
|
|
313
350
|
this.#now = options.now ?? Date.now;
|
|
314
351
|
this.#faults = options[FAULTS] ?? {};
|
|
315
352
|
this.#guard = options.guard ?? ((section) => section());
|
|
353
|
+
this.#namedAncestors = namedAncestors;
|
|
316
354
|
}
|
|
317
355
|
/**
|
|
318
356
|
* Read-only status snapshot; does not create a replica, migrate, recover
|
|
@@ -325,9 +363,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
325
363
|
};
|
|
326
364
|
checkAbort();
|
|
327
365
|
const dbPath = join(dir, "replica.db");
|
|
328
|
-
let
|
|
366
|
+
let identity;
|
|
329
367
|
try {
|
|
330
|
-
|
|
368
|
+
identity = await lstat(dbPath);
|
|
369
|
+
if (!identity.isFile()) return null;
|
|
331
370
|
} catch (error) {
|
|
332
371
|
if (errnoOf(error) === "ENOENT") return null;
|
|
333
372
|
throw storageError(error, "Inspecting the replica");
|
|
@@ -340,14 +379,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
340
379
|
db = opener(dbPath, { readonly: true });
|
|
341
380
|
db.exec("PRAGMA busy_timeout = 0");
|
|
342
381
|
checkAbort();
|
|
343
|
-
const store = new _SqliteReplicaStore(dir, db,
|
|
382
|
+
const store = new _SqliteReplicaStore(dir, db, identity, { create: false, ...options.now ? { now: options.now } : {} });
|
|
344
383
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
345
384
|
const before = db.get("PRAGMA data_version").data_version;
|
|
346
385
|
const state = await store.open();
|
|
347
386
|
checkAbort();
|
|
348
387
|
if (state === null) return null;
|
|
349
388
|
const status = await store.status();
|
|
350
|
-
await new Promise((
|
|
389
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
351
390
|
checkAbort();
|
|
352
391
|
const after = db.get("PRAGMA data_version").data_version;
|
|
353
392
|
if (before === after) return { state, status };
|
|
@@ -367,65 +406,145 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
367
406
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
368
407
|
*/
|
|
369
408
|
static async open(dir, options) {
|
|
370
|
-
const
|
|
409
|
+
const namedDbPath = join(dir, "replica.db");
|
|
371
410
|
if (!options.create) {
|
|
372
|
-
const exists = await
|
|
373
|
-
() =>
|
|
411
|
+
const exists = await lstat(namedDbPath).then(
|
|
412
|
+
(stats) => stats.isFile(),
|
|
374
413
|
() => false
|
|
375
414
|
);
|
|
376
|
-
if (!exists) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `No replica at ${dir}.`);
|
|
415
|
+
if (!exists) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `No regular replica database at ${dir}.`);
|
|
377
416
|
}
|
|
378
417
|
const opener = options.sqlite ?? await loadSqlite();
|
|
418
|
+
const releaseOpen = await acquireSqliteOpen();
|
|
379
419
|
let store;
|
|
420
|
+
let directoryFd;
|
|
380
421
|
try {
|
|
381
|
-
if (options.create) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const handle = await open(dbPath, fsConstants.O_CREAT | fsConstants.O_RDWR, 384);
|
|
385
|
-
await handle.close();
|
|
386
|
-
await chmod(dbPath, 384);
|
|
422
|
+
if (options.create) await mkdir(dir, { recursive: true, mode: 448 });
|
|
423
|
+
if (process.platform === "linux") {
|
|
424
|
+
directoryFd = openSync(dir, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
|
|
387
425
|
}
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
396
|
-
if (version > SCHEMA_VERSION) {
|
|
397
|
-
db.close();
|
|
398
|
-
throw new ReplicaError(
|
|
399
|
-
ReplicaErrorCode.STORAGE_ERROR,
|
|
400
|
-
`The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
|
|
401
|
-
);
|
|
426
|
+
const opened = directoryFd === void 0 ? lstatSync(dir, { bigint: true }) : fstatSync(directoryFd, { bigint: true });
|
|
427
|
+
if (!opened.isDirectory() || opened.isSymbolicLink() || options.expectedDirectory && (opened.dev !== BigInt(options.expectedDirectory.dev) || opened.ino !== BigInt(options.expectedDirectory.ino))) {
|
|
428
|
+
throw new Error("The replica directory changed before SQLite opened it.");
|
|
429
|
+
}
|
|
430
|
+
const namedAncestors = process.platform === "linux" ? [] : await captureNamedPath(dir);
|
|
431
|
+
if (namedAncestors.length && (namedAncestors[namedAncestors.length - 1].dev !== opened.dev || namedAncestors[namedAncestors.length - 1].ino !== opened.ino)) {
|
|
432
|
+
throw new Error("The replica directory changed while checking its ancestry.");
|
|
402
433
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
434
|
+
const pinnedDir = directoryFd === void 0 ? dir : `/proc/self/fd/${directoryFd}`;
|
|
435
|
+
const dbPath = join(pinnedDir, "replica.db");
|
|
436
|
+
if (options.create) {
|
|
437
|
+
await mkdir(join(pinnedDir, "blobs", ".tmp"), { recursive: true, mode: 448 });
|
|
438
|
+
await Promise.all([chmod(pinnedDir, 448), chmod(join(pinnedDir, "blobs"), 448), chmod(join(pinnedDir, "blobs", ".tmp"), 448)]);
|
|
439
|
+
const handle = await open(dbPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_RDWR | fsConstants.O_NOFOLLOW, 384).catch((error) => {
|
|
440
|
+
if (errnoOf(error) === "EEXIST") return null;
|
|
441
|
+
throw error;
|
|
442
|
+
});
|
|
443
|
+
if (handle) {
|
|
444
|
+
try {
|
|
445
|
+
await handle.chmod(384);
|
|
446
|
+
} finally {
|
|
447
|
+
await handle.close();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const leaf = await open(dbPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
452
|
+
let db;
|
|
453
|
+
try {
|
|
454
|
+
const identity = await leaf.stat();
|
|
455
|
+
const named = await lstat(dbPath);
|
|
456
|
+
if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
|
|
457
|
+
throw new Error("The replica database changed before SQLite opened it.");
|
|
458
|
+
}
|
|
459
|
+
const previousFds = process.platform === "linux" && !options.sqlite ? new Map(readdirSync("/proc/self/fd").map((fd) => {
|
|
460
|
+
try {
|
|
461
|
+
return [fd, readlinkSync(`/proc/self/fd/${fd}`)];
|
|
462
|
+
} catch {
|
|
463
|
+
return [fd, ""];
|
|
464
|
+
}
|
|
465
|
+
})) : null;
|
|
466
|
+
db = opener(dbPath);
|
|
467
|
+
const afterOpen = lstatSync(dbPath);
|
|
468
|
+
if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
|
|
469
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
470
|
+
}
|
|
471
|
+
if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
|
|
472
|
+
if (previousFds) {
|
|
473
|
+
const databases = readdirSync("/proc/self/fd").filter((fd) => {
|
|
474
|
+
try {
|
|
475
|
+
const target = readlinkSync(`/proc/self/fd/${fd}`);
|
|
476
|
+
return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
|
|
477
|
+
} catch {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
if (databases.length === 0 || databases.some((fd) => {
|
|
482
|
+
try {
|
|
483
|
+
const actual = fstatSync(Number(fd));
|
|
484
|
+
return actual.dev !== identity.dev || actual.ino !== identity.ino;
|
|
485
|
+
} catch {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
})) {
|
|
489
|
+
throw new Error("SQLite opened a different replica database.");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const after = lstatSync(dbPath);
|
|
493
|
+
if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
|
|
494
|
+
throw new Error("The replica database changed while SQLite opened it.");
|
|
495
|
+
}
|
|
496
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
497
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
498
|
+
db.exec("PRAGMA synchronous = FULL");
|
|
499
|
+
db.exec("PRAGMA secure_delete = ON");
|
|
500
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
501
|
+
const version = db.get("PRAGMA user_version")?.user_version ?? 0;
|
|
502
|
+
if (version > SCHEMA_VERSION) {
|
|
503
|
+
throw new ReplicaError(
|
|
504
|
+
ReplicaErrorCode.STORAGE_ERROR,
|
|
505
|
+
`The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (version < SCHEMA_VERSION) {
|
|
509
|
+
db.exec("BEGIN IMMEDIATE");
|
|
510
|
+
db.exec(SCHEMA);
|
|
511
|
+
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
512
|
+
db.exec("COMMIT");
|
|
513
|
+
}
|
|
514
|
+
store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors);
|
|
515
|
+
db = void 0;
|
|
516
|
+
} finally {
|
|
517
|
+
try {
|
|
518
|
+
db?.close();
|
|
519
|
+
} finally {
|
|
520
|
+
await leaf.close();
|
|
521
|
+
}
|
|
408
522
|
}
|
|
409
|
-
store = new _SqliteReplicaStore(dir, db, ino, options);
|
|
410
523
|
} catch (error) {
|
|
524
|
+
if (store) await store.close().catch(() => void 0);
|
|
525
|
+
else if (directoryFd !== void 0) closeSync(directoryFd);
|
|
411
526
|
throw storageError(error, `Opening the replica at ${dir}`);
|
|
527
|
+
} finally {
|
|
528
|
+
releaseOpen();
|
|
412
529
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
await store.#
|
|
530
|
+
try {
|
|
531
|
+
const row = store.#row();
|
|
532
|
+
if (row?.revoked_detail != null && (row.purge_pending === 1 || await store.#holdsContentFiles().catch(() => true))) {
|
|
533
|
+
await store.#finishRevocationPurge().catch(() => void 0);
|
|
534
|
+
}
|
|
535
|
+
return store;
|
|
536
|
+
} catch (error) {
|
|
537
|
+
await store.close().catch(() => void 0);
|
|
538
|
+
throw error;
|
|
416
539
|
}
|
|
417
|
-
return store;
|
|
418
540
|
}
|
|
419
541
|
#row() {
|
|
420
542
|
return this.#db.get("SELECT * FROM replica WHERE id = 1");
|
|
421
543
|
}
|
|
422
544
|
/** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
|
|
423
545
|
async #assertPresent() {
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
() => void 0
|
|
427
|
-
);
|
|
428
|
-
if (ino !== this.#ino) {
|
|
546
|
+
const identity = await lstat(join(this.#pinnedDir, "replica.db")).catch(() => null);
|
|
547
|
+
if (!identity?.isFile() || identity.dev !== this.#dev || identity.ino !== this.#ino) {
|
|
429
548
|
throw new ReplicaError(
|
|
430
549
|
ReplicaErrorCode.NOT_FOUND,
|
|
431
550
|
`The replica at ${this.dir} was deleted while this command ran (its profile was deleted or the replica purged); nothing more was written.`
|
|
@@ -655,7 +774,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
655
774
|
return held;
|
|
656
775
|
}
|
|
657
776
|
#blobPath(hash) {
|
|
658
|
-
return join(this
|
|
777
|
+
return join(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
|
|
659
778
|
}
|
|
660
779
|
/**
|
|
661
780
|
* Make every blob durable before any row references it: write to a fresh
|
|
@@ -672,11 +791,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
672
791
|
() => false
|
|
673
792
|
);
|
|
674
793
|
if (exists) continue;
|
|
675
|
-
const shard = join(this
|
|
794
|
+
const shard = join(this.#pinnedDir, "blobs", hash.slice(0, 2));
|
|
676
795
|
await mkdir(shard, { mode: 448 }).catch((error) => {
|
|
677
796
|
if (errnoOf(error) !== "EEXIST") throw error;
|
|
678
797
|
});
|
|
679
|
-
const temp = join(this
|
|
798
|
+
const temp = join(this.#pinnedDir, "blobs", ".tmp", `${hash}.${randomBytes(6).toString("hex")}`);
|
|
680
799
|
const handle = await open(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 384);
|
|
681
800
|
try {
|
|
682
801
|
await handle.writeFile(bytes);
|
|
@@ -692,7 +811,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
692
811
|
await unlink(temp);
|
|
693
812
|
}
|
|
694
813
|
dirs.add(shard);
|
|
695
|
-
dirs.add(join(this
|
|
814
|
+
dirs.add(join(this.#pinnedDir, "blobs"));
|
|
696
815
|
}
|
|
697
816
|
for (const dir of dirs) {
|
|
698
817
|
const handle = await open(dir, fsConstants.O_RDONLY);
|
|
@@ -800,7 +919,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
800
919
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
801
920
|
async #unlinkBlobs(keep) {
|
|
802
921
|
let removed = 0;
|
|
803
|
-
const root = join(this
|
|
922
|
+
const root = join(this.#pinnedDir, "blobs");
|
|
804
923
|
for (const shard of await entriesOf(root)) {
|
|
805
924
|
const shardPath = join(root, shard);
|
|
806
925
|
if (shard === ".tmp") {
|
|
@@ -825,7 +944,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
825
944
|
}
|
|
826
945
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
827
946
|
async #holdsContentFiles() {
|
|
828
|
-
const root = join(this
|
|
947
|
+
const root = join(this.#pinnedDir, "blobs");
|
|
829
948
|
for (const shard of await entriesOf(root)) {
|
|
830
949
|
if ((await entriesOf(join(root, shard))).length > 0) return true;
|
|
831
950
|
}
|
|
@@ -891,8 +1010,93 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
891
1010
|
}
|
|
892
1011
|
try {
|
|
893
1012
|
this.#checkLease(t, { allowRevoked: true });
|
|
1013
|
+
const expected = this.#directoryFd === void 0 ? this.#namedAncestors.at(-1) : fstatSync(this.#directoryFd, { bigint: true });
|
|
1014
|
+
if (!expected) {
|
|
1015
|
+
throw new ReplicaError(ReplicaErrorCode.RUNTIME_UNSUPPORTED, "Replica removal requires an opened or identity-checked directory.");
|
|
1016
|
+
}
|
|
1017
|
+
const assertDirectory = async () => {
|
|
1018
|
+
const current = await lstat(this.dir, { bigint: true });
|
|
1019
|
+
if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
1020
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica directory changed during removal.");
|
|
1021
|
+
}
|
|
1022
|
+
return current;
|
|
1023
|
+
};
|
|
1024
|
+
const pinned = await assertDirectory();
|
|
894
1025
|
await this.#assertPresent();
|
|
895
|
-
|
|
1026
|
+
if (process.platform === "linux") {
|
|
1027
|
+
for (const entry of await readdir(this.#pinnedDir)) {
|
|
1028
|
+
await rm(join(this.#pinnedDir, entry), { recursive: true, force: true });
|
|
1029
|
+
}
|
|
1030
|
+
await assertDirectory();
|
|
1031
|
+
await rmdir(this.dir);
|
|
1032
|
+
} else {
|
|
1033
|
+
const ancestors = this.#namedAncestors;
|
|
1034
|
+
const checkAncestors = async () => {
|
|
1035
|
+
for (const expected2 of ancestors) {
|
|
1036
|
+
const current = await lstat(expected2.path, { bigint: true });
|
|
1037
|
+
if (current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
1038
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica ancestry changed during removal.");
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
const directories = /* @__PURE__ */ new Map([[this.dir, pinned]]);
|
|
1043
|
+
const files = /* @__PURE__ */ new Map();
|
|
1044
|
+
const check = async (path, expected2, directory) => {
|
|
1045
|
+
if (path !== this.dir) {
|
|
1046
|
+
const parent = dirname(path);
|
|
1047
|
+
await check(parent, directories.get(parent), true);
|
|
1048
|
+
}
|
|
1049
|
+
await checkAncestors();
|
|
1050
|
+
const current = await lstat(path, { bigint: true });
|
|
1051
|
+
if ((directory ? !current.isDirectory() : !current.isFile()) || current.dev !== expected2.dev || current.ino !== expected2.ino) {
|
|
1052
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica layout changed during removal.");
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
const record = async (path, directory) => {
|
|
1056
|
+
const parent = dirname(path);
|
|
1057
|
+
await check(parent, directories.get(parent), true);
|
|
1058
|
+
const current = await lstat(path, { bigint: true });
|
|
1059
|
+
if (!(directory ? current.isDirectory() : current.isFile())) {
|
|
1060
|
+
throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
1061
|
+
}
|
|
1062
|
+
(directory ? directories : files).set(path, current);
|
|
1063
|
+
};
|
|
1064
|
+
const blobs = join(this.dir, "blobs");
|
|
1065
|
+
for (const name of await readdir(this.dir)) {
|
|
1066
|
+
if (name === "blobs") await record(blobs, true);
|
|
1067
|
+
else if (["replica.db", "replica.db-wal", "replica.db-shm"].includes(name)) await record(join(this.dir, name), false);
|
|
1068
|
+
else throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica layout during removal.");
|
|
1069
|
+
}
|
|
1070
|
+
if (directories.has(blobs)) {
|
|
1071
|
+
await check(blobs, directories.get(blobs), true);
|
|
1072
|
+
for (const shard of await readdir(blobs)) {
|
|
1073
|
+
if (shard !== ".tmp" && !/^[0-9a-f]{2}$/.test(shard)) {
|
|
1074
|
+
throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica blob shard during removal.");
|
|
1075
|
+
}
|
|
1076
|
+
const shardPath = join(blobs, shard);
|
|
1077
|
+
await record(shardPath, true);
|
|
1078
|
+
await check(shardPath, directories.get(shardPath), true);
|
|
1079
|
+
for (const name of await readdir(shardPath)) {
|
|
1080
|
+
if (shard === ".tmp" ? !/^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name) : !isHash(name) || !name.startsWith(shard)) {
|
|
1081
|
+
throw new ReplicaError(ReplicaErrorCode.STORAGE_ERROR, "Unexpected replica blob file during removal.");
|
|
1082
|
+
}
|
|
1083
|
+
await record(join(shardPath, name), false);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
for (const [path, identity] of files) {
|
|
1088
|
+
await check(path, identity, false);
|
|
1089
|
+
await unlink(path);
|
|
1090
|
+
}
|
|
1091
|
+
for (const [path, identity] of [...directories].reverse()) {
|
|
1092
|
+
if (path === this.dir) continue;
|
|
1093
|
+
await check(path, identity, true);
|
|
1094
|
+
await rmdir(path);
|
|
1095
|
+
}
|
|
1096
|
+
await assertDirectory();
|
|
1097
|
+
await checkAncestors();
|
|
1098
|
+
await rmdir(this.dir);
|
|
1099
|
+
}
|
|
896
1100
|
} catch (error) {
|
|
897
1101
|
throw storageError(error, "Removing the replica");
|
|
898
1102
|
} finally {
|
|
@@ -1058,7 +1262,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1058
1262
|
};
|
|
1059
1263
|
}
|
|
1060
1264
|
async close() {
|
|
1061
|
-
this.#
|
|
1265
|
+
const fd = this.#directoryFd;
|
|
1266
|
+
this.#directoryFd = void 0;
|
|
1267
|
+
if (fd !== void 0) pinnedDirectories.unregister(this);
|
|
1268
|
+
try {
|
|
1269
|
+
this.#db.close();
|
|
1270
|
+
} finally {
|
|
1271
|
+
if (fd !== void 0) closeSync(fd);
|
|
1272
|
+
}
|
|
1062
1273
|
}
|
|
1063
1274
|
};
|
|
1064
1275
|
export {
|