@tinycloud/replica 0.3.0-beta.6 → 0.3.0-beta.8
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/kv-storage-testing.js.map +1 -1
- package/dist/kv-storage.js +1 -1
- package/dist/kv-storage.worker.f6f98ad5dc547a29274c0b539fb78e25f8a2051ccfaafc9490cb307051a0e0ef.js +19004 -0
- package/dist/kv-storage.worker.js +1 -1
- package/dist/kv-storage.worker.js.map +1 -1
- package/dist/replica.worker.js +1 -1
- package/dist/replica.worker.js.map +1 -1
- package/dist/sqlite.cjs +198 -55
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +25 -1
- package/dist/sqlite.d.ts +25 -1
- package/dist/sqlite.js +198 -55
- package/dist/sqlite.js.map +1 -1
- package/package.json +1 -1
- package/dist/kv-storage.worker.f9311d47e8d4d08d5413987b5f8aa5e3cd180708f3e4debed3b0a3d938156fcd.js +0 -19004
package/dist/sqlite.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReplicaConsentAnchor } from '@tinycloud/sdk-services/kv/replication';
|
|
1
|
+
import { ReplicaConsentAnchor, ReplicaAdmitHook } from '@tinycloud/sdk-services/kv/replication';
|
|
2
2
|
import { R as ReplicaStore, j as ReplicaState, b as ReplicaStatus, h as ReplicaConfig, G as GrantRecord, i as ReplicaLastError, c as LeaseToken, l as VerifiedPage, g as LocalEntry, L as ListOpts } from './types-CQ61VnsP.cjs';
|
|
3
3
|
|
|
4
4
|
type SqlValue = string | number | null | Uint8Array;
|
|
@@ -88,6 +88,10 @@ type SqliteReplicaStoreOptions = {
|
|
|
88
88
|
/** Clock for leases and status (ms since epoch). */
|
|
89
89
|
now?: () => number;
|
|
90
90
|
guard?: MutationGuard;
|
|
91
|
+
/** Quota admission, run inside applyPage's guarded mutation before any write (TC-858 S5). */
|
|
92
|
+
admit?: ReplicaAdmitHook;
|
|
93
|
+
/** Partition the store belongs to; reported to `admit`. */
|
|
94
|
+
partitionId?: string;
|
|
91
95
|
};
|
|
92
96
|
/**
|
|
93
97
|
* The CLI replica store: SQLite (WAL, synchronous=FULL) for entries, cursor
|
|
@@ -112,6 +116,26 @@ declare class SqliteReplicaStore implements ReplicaStore {
|
|
|
112
116
|
grantInstall: SqliteGrantInstall | null;
|
|
113
117
|
activeGrantInstall: SqliteGrantInstall | null;
|
|
114
118
|
} | null>;
|
|
119
|
+
/**
|
|
120
|
+
* Blob content bytes on disk in the replica at `dir` (0 when absent): the writer's blob files under `blobs/`,
|
|
121
|
+
* including writer temp files and blobs no entry references any more, so garbage a failed sync
|
|
122
|
+
* left behind still counts. Counts exactly what `collectOrphans` may delete: foreign entries
|
|
123
|
+
* (stray files, symlinks, unknown names) are neither counted nor touched. Only blob content is counted: not the database, its WAL, rows or the
|
|
124
|
+
* pending store. Filesystem-only: it opens no
|
|
125
|
+
* database, so it can sum a profile's replicas under the caller's own mutation hold.
|
|
126
|
+
*/
|
|
127
|
+
static contentBytes(dir: string): Promise<number>;
|
|
128
|
+
/**
|
|
129
|
+
* Remove blobs and temp files in the replica at `dir` that no committed row references: what a
|
|
130
|
+
* crash between writing blobs and committing (or between committing and collecting) leaves.
|
|
131
|
+
* Returns the number of files removed; 0 when the replica is absent or unreadable.
|
|
132
|
+
*
|
|
133
|
+
* The caller MUST hold the profile mutation guard, which every writer of this replica holds
|
|
134
|
+
* across its blob write and commit; that is what makes an unreferenced blob safe to delete
|
|
135
|
+
* (it cannot belong to an in-flight write). Never call it without that hold. The database is
|
|
136
|
+
* read only, so nothing here can take a lease or change committed state.
|
|
137
|
+
*/
|
|
138
|
+
static collectOrphans(dir: string): Promise<number>;
|
|
115
139
|
/**
|
|
116
140
|
* Open the replica store in `dir`. With `create: false` a missing replica is
|
|
117
141
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
package/dist/sqlite.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReplicaConsentAnchor } from '@tinycloud/sdk-services/kv/replication';
|
|
1
|
+
import { ReplicaConsentAnchor, ReplicaAdmitHook } from '@tinycloud/sdk-services/kv/replication';
|
|
2
2
|
import { R as ReplicaStore, j as ReplicaState, b as ReplicaStatus, h as ReplicaConfig, G as GrantRecord, i as ReplicaLastError, c as LeaseToken, l as VerifiedPage, g as LocalEntry, L as ListOpts } from './types-CQ61VnsP.js';
|
|
3
3
|
|
|
4
4
|
type SqlValue = string | number | null | Uint8Array;
|
|
@@ -88,6 +88,10 @@ type SqliteReplicaStoreOptions = {
|
|
|
88
88
|
/** Clock for leases and status (ms since epoch). */
|
|
89
89
|
now?: () => number;
|
|
90
90
|
guard?: MutationGuard;
|
|
91
|
+
/** Quota admission, run inside applyPage's guarded mutation before any write (TC-858 S5). */
|
|
92
|
+
admit?: ReplicaAdmitHook;
|
|
93
|
+
/** Partition the store belongs to; reported to `admit`. */
|
|
94
|
+
partitionId?: string;
|
|
91
95
|
};
|
|
92
96
|
/**
|
|
93
97
|
* The CLI replica store: SQLite (WAL, synchronous=FULL) for entries, cursor
|
|
@@ -112,6 +116,26 @@ declare class SqliteReplicaStore implements ReplicaStore {
|
|
|
112
116
|
grantInstall: SqliteGrantInstall | null;
|
|
113
117
|
activeGrantInstall: SqliteGrantInstall | null;
|
|
114
118
|
} | null>;
|
|
119
|
+
/**
|
|
120
|
+
* Blob content bytes on disk in the replica at `dir` (0 when absent): the writer's blob files under `blobs/`,
|
|
121
|
+
* including writer temp files and blobs no entry references any more, so garbage a failed sync
|
|
122
|
+
* left behind still counts. Counts exactly what `collectOrphans` may delete: foreign entries
|
|
123
|
+
* (stray files, symlinks, unknown names) are neither counted nor touched. Only blob content is counted: not the database, its WAL, rows or the
|
|
124
|
+
* pending store. Filesystem-only: it opens no
|
|
125
|
+
* database, so it can sum a profile's replicas under the caller's own mutation hold.
|
|
126
|
+
*/
|
|
127
|
+
static contentBytes(dir: string): Promise<number>;
|
|
128
|
+
/**
|
|
129
|
+
* Remove blobs and temp files in the replica at `dir` that no committed row references: what a
|
|
130
|
+
* crash between writing blobs and committing (or between committing and collecting) leaves.
|
|
131
|
+
* Returns the number of files removed; 0 when the replica is absent or unreadable.
|
|
132
|
+
*
|
|
133
|
+
* The caller MUST hold the profile mutation guard, which every writer of this replica holds
|
|
134
|
+
* across its blob write and commit; that is what makes an unreferenced blob safe to delete
|
|
135
|
+
* (it cannot belong to an in-flight write). Never call it without that hold. The database is
|
|
136
|
+
* read only, so nothing here can take a lease or change committed state.
|
|
137
|
+
*/
|
|
138
|
+
static collectOrphans(dir: string): Promise<number>;
|
|
115
139
|
/**
|
|
116
140
|
* Open the replica store in `dir`. With `create: false` a missing replica is
|
|
117
141
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
package/dist/sqlite.js
CHANGED
|
@@ -695,6 +695,76 @@ var isHash = (name) => /^[0-9a-f]{64}$/.test(name);
|
|
|
695
695
|
function errnoOf(error) {
|
|
696
696
|
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
697
697
|
}
|
|
698
|
+
var isShard = (name) => /^[0-9a-f]{2}$/.test(name);
|
|
699
|
+
var isTempName = (name) => /^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name);
|
|
700
|
+
async function writerBlobFiles(root) {
|
|
701
|
+
const out = [];
|
|
702
|
+
let shards;
|
|
703
|
+
try {
|
|
704
|
+
shards = await readdir2(root, { withFileTypes: true });
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (errnoOf(error) === "ENOENT" || errnoOf(error) === "ENOTDIR") return out;
|
|
707
|
+
throw error;
|
|
708
|
+
}
|
|
709
|
+
for (const shard of shards) {
|
|
710
|
+
const temp = shard.name === ".tmp";
|
|
711
|
+
if (!shard.isDirectory() || !(temp || isShard(shard.name))) continue;
|
|
712
|
+
const shardPath = join2(root, shard.name);
|
|
713
|
+
let files;
|
|
714
|
+
try {
|
|
715
|
+
files = await readdir2(shardPath, { withFileTypes: true });
|
|
716
|
+
} catch (error) {
|
|
717
|
+
if (errnoOf(error) === "ENOENT" || errnoOf(error) === "ENOTDIR") continue;
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
for (const file of files) {
|
|
721
|
+
if (!file.isFile()) continue;
|
|
722
|
+
const ok = temp ? isTempName(file.name) : isHash(file.name) && file.name.startsWith(shard.name);
|
|
723
|
+
if (!ok) continue;
|
|
724
|
+
const path = join2(shardPath, file.name);
|
|
725
|
+
const info = await lstat2(path).catch(() => void 0);
|
|
726
|
+
if (!info?.isFile()) continue;
|
|
727
|
+
out.push({ path, hash: temp ? null : file.name, size: info.size });
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return out;
|
|
731
|
+
}
|
|
732
|
+
async function unlinkWriterOrphans(root, keep) {
|
|
733
|
+
let removed = 0;
|
|
734
|
+
const touched = /* @__PURE__ */ new Set();
|
|
735
|
+
for (const file of await writerBlobFiles(root)) {
|
|
736
|
+
if (file.hash !== null && keep.has(file.hash)) continue;
|
|
737
|
+
await rm(file.path, { force: true });
|
|
738
|
+
touched.add(dirname(file.path));
|
|
739
|
+
removed += 1;
|
|
740
|
+
}
|
|
741
|
+
for (const dir of touched) if (!dir.endsWith(".tmp")) await rmdir2(dir).catch(() => void 0);
|
|
742
|
+
return removed;
|
|
743
|
+
}
|
|
744
|
+
async function unlinkBlobsIn(root, keep) {
|
|
745
|
+
let removed = 0;
|
|
746
|
+
for (const shard of await entriesOf(root)) {
|
|
747
|
+
const shardPath = join2(root, shard);
|
|
748
|
+
if (shard === ".tmp") {
|
|
749
|
+
for (const name of await entriesOf(shardPath)) {
|
|
750
|
+
await rm(join2(shardPath, name), { force: true });
|
|
751
|
+
removed += 1;
|
|
752
|
+
}
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
let kept = 0;
|
|
756
|
+
for (const name of await entriesOf(shardPath)) {
|
|
757
|
+
if (isHash(name) && keep.has(name)) {
|
|
758
|
+
kept += 1;
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
await rm(join2(shardPath, name), { force: true });
|
|
762
|
+
removed += 1;
|
|
763
|
+
}
|
|
764
|
+
if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
|
|
765
|
+
}
|
|
766
|
+
return removed;
|
|
767
|
+
}
|
|
698
768
|
async function entriesOf(path) {
|
|
699
769
|
try {
|
|
700
770
|
return await readdir2(path);
|
|
@@ -758,6 +828,8 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
758
828
|
#guard;
|
|
759
829
|
/** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
|
|
760
830
|
#ino;
|
|
831
|
+
#admit;
|
|
832
|
+
#partitionId;
|
|
761
833
|
#dev;
|
|
762
834
|
#namedAncestors;
|
|
763
835
|
constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared, readonlySnapshot = false) {
|
|
@@ -777,6 +849,8 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
777
849
|
this.#dev = identity.dev;
|
|
778
850
|
this.#now = options.now ?? Date.now;
|
|
779
851
|
this.#faults = options[FAULTS] ?? {};
|
|
852
|
+
this.#admit = options.admit;
|
|
853
|
+
this.#partitionId = options.partitionId ?? "";
|
|
780
854
|
this.#guard = options.guard ?? ((section) => section());
|
|
781
855
|
this.#namedAncestors = namedAncestors;
|
|
782
856
|
}
|
|
@@ -846,6 +920,55 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
846
920
|
}
|
|
847
921
|
}
|
|
848
922
|
}
|
|
923
|
+
/**
|
|
924
|
+
* Blob content bytes on disk in the replica at `dir` (0 when absent): the writer's blob files under `blobs/`,
|
|
925
|
+
* including writer temp files and blobs no entry references any more, so garbage a failed sync
|
|
926
|
+
* left behind still counts. Counts exactly what `collectOrphans` may delete: foreign entries
|
|
927
|
+
* (stray files, symlinks, unknown names) are neither counted nor touched. Only blob content is counted: not the database, its WAL, rows or the
|
|
928
|
+
* pending store. Filesystem-only: it opens no
|
|
929
|
+
* database, so it can sum a profile's replicas under the caller's own mutation hold.
|
|
930
|
+
*/
|
|
931
|
+
static async contentBytes(dir) {
|
|
932
|
+
try {
|
|
933
|
+
let bytes = 0;
|
|
934
|
+
for (const file of await writerBlobFiles(join2(dir, "blobs"))) bytes += file.size;
|
|
935
|
+
return bytes;
|
|
936
|
+
} catch (error) {
|
|
937
|
+
throw storageError(error, "Measuring replica content");
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Remove blobs and temp files in the replica at `dir` that no committed row references: what a
|
|
942
|
+
* crash between writing blobs and committing (or between committing and collecting) leaves.
|
|
943
|
+
* Returns the number of files removed; 0 when the replica is absent or unreadable.
|
|
944
|
+
*
|
|
945
|
+
* The caller MUST hold the profile mutation guard, which every writer of this replica holds
|
|
946
|
+
* across its blob write and commit; that is what makes an unreferenced blob safe to delete
|
|
947
|
+
* (it cannot belong to an in-flight write). Never call it without that hold. The database is
|
|
948
|
+
* read only, so nothing here can take a lease or change committed state.
|
|
949
|
+
*/
|
|
950
|
+
static async collectOrphans(dir) {
|
|
951
|
+
const dbPath = join2(dir, "replica.db");
|
|
952
|
+
const identity = await lstat2(dbPath).catch(() => void 0);
|
|
953
|
+
if (!identity?.isFile()) return 0;
|
|
954
|
+
const opener = await loadSqlite();
|
|
955
|
+
let keep;
|
|
956
|
+
let db;
|
|
957
|
+
try {
|
|
958
|
+
db = opener(dbPath, { readonly: true });
|
|
959
|
+
db.exec("PRAGMA busy_timeout = 0");
|
|
960
|
+
keep = new Set(db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash));
|
|
961
|
+
} catch {
|
|
962
|
+
return 0;
|
|
963
|
+
} finally {
|
|
964
|
+
db?.close();
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
return await unlinkWriterOrphans(join2(dir, "blobs"), keep);
|
|
968
|
+
} catch (error) {
|
|
969
|
+
throw storageError(error, "Collecting orphaned content");
|
|
970
|
+
}
|
|
971
|
+
}
|
|
849
972
|
/**
|
|
850
973
|
* Open the replica store in `dir`. With `create: false` a missing replica is
|
|
851
974
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
@@ -1335,12 +1458,15 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1335
1458
|
}
|
|
1336
1459
|
#storeGrantPolicy(g, policy, existing) {
|
|
1337
1460
|
const installedAt = existing?.installedAt ?? policy.at;
|
|
1461
|
+
const keepExisting = existing !== null && existing.consentDeadline < policy.consentDeadline;
|
|
1462
|
+
const consentDeadline = keepExisting ? existing.consentDeadline : policy.consentDeadline;
|
|
1463
|
+
const consentAnchor = keepExisting ? existing.consentAnchor : policy.consentAnchor;
|
|
1338
1464
|
const effectiveExpiresAt = Math.min(
|
|
1339
1465
|
g.expiresAt === null ? Infinity : g.expiresAt * 1e3,
|
|
1340
1466
|
installedAt + policy.ttlMs,
|
|
1341
|
-
|
|
1467
|
+
consentDeadline
|
|
1342
1468
|
);
|
|
1343
|
-
if (!Number.isSafeInteger(installedAt) || installedAt < 0 || !Number.isSafeInteger(effectiveExpiresAt) || effectiveExpiresAt < 0 || !Number.isSafeInteger(
|
|
1469
|
+
if (!Number.isSafeInteger(installedAt) || installedAt < 0 || !Number.isSafeInteger(effectiveExpiresAt) || effectiveExpiresAt < 0 || !Number.isSafeInteger(consentDeadline) || consentDeadline < 0 || !Number.isSafeInteger(consentAnchor.at) || consentAnchor.at < 0 || !Number.isSafeInteger(policy.ttlMs) || policy.ttlMs <= 0) {
|
|
1344
1470
|
throw new ReplicaError(ReplicaErrorCode.GRANT_EXPIRED, "The replica grant has no finite effective expiry.");
|
|
1345
1471
|
}
|
|
1346
1472
|
this.#db.run(
|
|
@@ -1352,11 +1478,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1352
1478
|
g.cid,
|
|
1353
1479
|
installedAt,
|
|
1354
1480
|
effectiveExpiresAt,
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1481
|
+
consentDeadline,
|
|
1482
|
+
consentAnchor.at,
|
|
1483
|
+
consentAnchor.source
|
|
1358
1484
|
);
|
|
1359
|
-
return { installedAt, effectiveExpiresAt, consentDeadline
|
|
1485
|
+
return { installedAt, effectiveExpiresAt, consentDeadline, consentAnchor };
|
|
1360
1486
|
}
|
|
1361
1487
|
async installGatedGrant(g, policy) {
|
|
1362
1488
|
return this.#guardedWrite("Installing the gated grant", () => {
|
|
@@ -1552,6 +1678,41 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1552
1678
|
}
|
|
1553
1679
|
reused.set(change.hash, size);
|
|
1554
1680
|
}
|
|
1681
|
+
if (this.#admit) {
|
|
1682
|
+
await this.#collectLocked(t);
|
|
1683
|
+
const changed = new Set(p.changes.map((change) => change.key));
|
|
1684
|
+
const kept = /* @__PURE__ */ new Set();
|
|
1685
|
+
for (const change of p.changes) if (!change.deleted && change.content) kept.add(change.hash);
|
|
1686
|
+
let blobBytes = 0;
|
|
1687
|
+
for (const [hash, bytes] of p.blobs) {
|
|
1688
|
+
const stored = this.#db.get(
|
|
1689
|
+
"SELECT 1 AS hit FROM entry WHERE hash = ? AND content = 1 AND deleted = 0 LIMIT 1",
|
|
1690
|
+
hash
|
|
1691
|
+
);
|
|
1692
|
+
if (stored === void 0) blobBytes += bytes.byteLength;
|
|
1693
|
+
}
|
|
1694
|
+
const released = /* @__PURE__ */ new Map();
|
|
1695
|
+
for (const key of changed) {
|
|
1696
|
+
const old = this.#db.get(
|
|
1697
|
+
"SELECT hash, size FROM entry WHERE key = ? AND deleted = 0 AND content = 1 AND hash IS NOT NULL AND size IS NOT NULL",
|
|
1698
|
+
key
|
|
1699
|
+
);
|
|
1700
|
+
if (old === void 0 || kept.has(old.hash) || released.has(old.hash)) continue;
|
|
1701
|
+
const holders = this.#db.all(
|
|
1702
|
+
"SELECT key FROM entry WHERE hash = ? AND content = 1 AND deleted = 0",
|
|
1703
|
+
old.hash
|
|
1704
|
+
);
|
|
1705
|
+
if (holders.every((holder) => changed.has(holder.key))) released.set(old.hash, old.size);
|
|
1706
|
+
}
|
|
1707
|
+
let freed = 0;
|
|
1708
|
+
for (const size of released.values()) freed += size;
|
|
1709
|
+
await this.#admit({
|
|
1710
|
+
partitionId: this.#partitionId,
|
|
1711
|
+
blobBytes: blobBytes - freed,
|
|
1712
|
+
entries: p.changes.length,
|
|
1713
|
+
freeBytes: null
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1555
1716
|
try {
|
|
1556
1717
|
await this.#writeBlobs(p.blobs);
|
|
1557
1718
|
} catch (error) {
|
|
@@ -1627,36 +1788,16 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1627
1788
|
});
|
|
1628
1789
|
} catch (error) {
|
|
1629
1790
|
if (isReplicaError(error, ReplicaErrorCode.GRANT_REVOKED)) await this.#purgeContent().catch(() => void 0);
|
|
1791
|
+
else if (this.#admit) await this.#collectLocked(t).catch(() => void 0);
|
|
1630
1792
|
throw error;
|
|
1631
1793
|
}
|
|
1632
1794
|
this.#faults.afterCommit?.();
|
|
1795
|
+
if (this.#admit) await this.#collectLocked(t);
|
|
1633
1796
|
});
|
|
1634
1797
|
}
|
|
1635
1798
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
const root = join2(this.#pinnedDir, "blobs");
|
|
1639
|
-
for (const shard of await entriesOf(root)) {
|
|
1640
|
-
const shardPath = join2(root, shard);
|
|
1641
|
-
if (shard === ".tmp") {
|
|
1642
|
-
for (const name of await entriesOf(shardPath)) {
|
|
1643
|
-
await rm(join2(shardPath, name), { force: true });
|
|
1644
|
-
removed += 1;
|
|
1645
|
-
}
|
|
1646
|
-
continue;
|
|
1647
|
-
}
|
|
1648
|
-
let kept = 0;
|
|
1649
|
-
for (const name of await entriesOf(shardPath)) {
|
|
1650
|
-
if (isHash(name) && keep.has(name)) {
|
|
1651
|
-
kept += 1;
|
|
1652
|
-
continue;
|
|
1653
|
-
}
|
|
1654
|
-
await rm(join2(shardPath, name), { force: true });
|
|
1655
|
-
removed += 1;
|
|
1656
|
-
}
|
|
1657
|
-
if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
|
|
1658
|
-
}
|
|
1659
|
-
return removed;
|
|
1799
|
+
#unlinkBlobs(keep) {
|
|
1800
|
+
return unlinkBlobsIn(join2(this.#pinnedDir, "blobs"), keep);
|
|
1660
1801
|
}
|
|
1661
1802
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
1662
1803
|
async #holdsContentFiles() {
|
|
@@ -1667,33 +1808,35 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
|
|
|
1667
1808
|
return false;
|
|
1668
1809
|
}
|
|
1669
1810
|
async collectGarbage(t) {
|
|
1670
|
-
return this.#mutate(
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1811
|
+
return this.#mutate(() => this.#collectLocked(t));
|
|
1812
|
+
}
|
|
1813
|
+
/** `collectGarbage` for callers already inside the mutation hold (it cannot re-enter `#mutate`). */
|
|
1814
|
+
async #collectLocked(t) {
|
|
1815
|
+
const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
|
|
1816
|
+
try {
|
|
1817
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
release?.();
|
|
1820
|
+
throw storageError(error, "Collecting unused content");
|
|
1821
|
+
}
|
|
1822
|
+
try {
|
|
1823
|
+
this.#checkLease(t);
|
|
1824
|
+
await this.#faults.afterGarbageCollectionBegin?.();
|
|
1825
|
+
const keep = new Set(
|
|
1826
|
+
this.#db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash)
|
|
1827
|
+
);
|
|
1828
|
+
const removed = await this.#unlinkBlobs(keep);
|
|
1829
|
+
this.#db.exec("COMMIT");
|
|
1830
|
+
return removed;
|
|
1831
|
+
} catch (error) {
|
|
1678
1832
|
try {
|
|
1679
|
-
this.#
|
|
1680
|
-
|
|
1681
|
-
const keep = new Set(
|
|
1682
|
-
this.#db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash)
|
|
1683
|
-
);
|
|
1684
|
-
const removed = await this.#unlinkBlobs(keep);
|
|
1685
|
-
this.#db.exec("COMMIT");
|
|
1686
|
-
return removed;
|
|
1687
|
-
} catch (error) {
|
|
1688
|
-
try {
|
|
1689
|
-
this.#db.exec("ROLLBACK");
|
|
1690
|
-
} catch {
|
|
1691
|
-
}
|
|
1692
|
-
throw storageError(error, "Collecting unused content");
|
|
1693
|
-
} finally {
|
|
1694
|
-
release?.();
|
|
1833
|
+
this.#db.exec("ROLLBACK");
|
|
1834
|
+
} catch {
|
|
1695
1835
|
}
|
|
1696
|
-
|
|
1836
|
+
throw storageError(error, "Collecting unused content");
|
|
1837
|
+
} finally {
|
|
1838
|
+
release?.();
|
|
1839
|
+
}
|
|
1697
1840
|
}
|
|
1698
1841
|
async reset(t, reason, options = {}) {
|
|
1699
1842
|
await this.#mutate(async () => {
|