@tinycloud/cli 1.3.0-beta.29 → 1.3.0-beta.31
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/index.js +190 -50
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +190 -50
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -4548,6 +4548,74 @@ function storageError(error, action) {
|
|
|
4548
4548
|
function errnoOf(error) {
|
|
4549
4549
|
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
4550
4550
|
}
|
|
4551
|
+
async function writerBlobFiles(root) {
|
|
4552
|
+
const out = [];
|
|
4553
|
+
let shards;
|
|
4554
|
+
try {
|
|
4555
|
+
shards = await readdir2(root, { withFileTypes: true });
|
|
4556
|
+
} catch (error) {
|
|
4557
|
+
if (errnoOf(error) === "ENOENT" || errnoOf(error) === "ENOTDIR") return out;
|
|
4558
|
+
throw error;
|
|
4559
|
+
}
|
|
4560
|
+
for (const shard of shards) {
|
|
4561
|
+
const temp = shard.name === ".tmp";
|
|
4562
|
+
if (!shard.isDirectory() || !(temp || isShard(shard.name))) continue;
|
|
4563
|
+
const shardPath = join22(root, shard.name);
|
|
4564
|
+
let files;
|
|
4565
|
+
try {
|
|
4566
|
+
files = await readdir2(shardPath, { withFileTypes: true });
|
|
4567
|
+
} catch (error) {
|
|
4568
|
+
if (errnoOf(error) === "ENOENT" || errnoOf(error) === "ENOTDIR") continue;
|
|
4569
|
+
throw error;
|
|
4570
|
+
}
|
|
4571
|
+
for (const file of files) {
|
|
4572
|
+
if (!file.isFile()) continue;
|
|
4573
|
+
const ok = temp ? isTempName(file.name) : isHash(file.name) && file.name.startsWith(shard.name);
|
|
4574
|
+
if (!ok) continue;
|
|
4575
|
+
const path = join22(shardPath, file.name);
|
|
4576
|
+
const info = await lstat2(path).catch(() => void 0);
|
|
4577
|
+
if (!info?.isFile()) continue;
|
|
4578
|
+
out.push({ path, hash: temp ? null : file.name, size: info.size });
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
return out;
|
|
4582
|
+
}
|
|
4583
|
+
async function unlinkWriterOrphans(root, keep) {
|
|
4584
|
+
let removed = 0;
|
|
4585
|
+
const touched = /* @__PURE__ */ new Set();
|
|
4586
|
+
for (const file of await writerBlobFiles(root)) {
|
|
4587
|
+
if (file.hash !== null && keep.has(file.hash)) continue;
|
|
4588
|
+
await rm3(file.path, { force: true });
|
|
4589
|
+
touched.add(dirname3(file.path));
|
|
4590
|
+
removed += 1;
|
|
4591
|
+
}
|
|
4592
|
+
for (const dir of touched) if (!dir.endsWith(".tmp")) await rmdir2(dir).catch(() => void 0);
|
|
4593
|
+
return removed;
|
|
4594
|
+
}
|
|
4595
|
+
async function unlinkBlobsIn(root, keep) {
|
|
4596
|
+
let removed = 0;
|
|
4597
|
+
for (const shard of await entriesOf(root)) {
|
|
4598
|
+
const shardPath = join22(root, shard);
|
|
4599
|
+
if (shard === ".tmp") {
|
|
4600
|
+
for (const name of await entriesOf(shardPath)) {
|
|
4601
|
+
await rm3(join22(shardPath, name), { force: true });
|
|
4602
|
+
removed += 1;
|
|
4603
|
+
}
|
|
4604
|
+
continue;
|
|
4605
|
+
}
|
|
4606
|
+
let kept = 0;
|
|
4607
|
+
for (const name of await entriesOf(shardPath)) {
|
|
4608
|
+
if (isHash(name) && keep.has(name)) {
|
|
4609
|
+
kept += 1;
|
|
4610
|
+
continue;
|
|
4611
|
+
}
|
|
4612
|
+
await rm3(join22(shardPath, name), { force: true });
|
|
4613
|
+
removed += 1;
|
|
4614
|
+
}
|
|
4615
|
+
if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
|
|
4616
|
+
}
|
|
4617
|
+
return removed;
|
|
4618
|
+
}
|
|
4551
4619
|
async function entriesOf(path) {
|
|
4552
4620
|
try {
|
|
4553
4621
|
return await readdir2(path);
|
|
@@ -4574,7 +4642,7 @@ async function acquireWritableMutation(shared) {
|
|
|
4574
4642
|
await previous;
|
|
4575
4643
|
return release;
|
|
4576
4644
|
}
|
|
4577
|
-
var ReplicaErrorCode3, REPLICA_ERROR3, ReplicaError3, SCHEMA_VERSION, SCHEMA, POLICY_COLUMNS, normalizeSchema, SOURCE_SCHEMA, V1_SCHEMA, POLICY_REPLICA, POLICY_SCHEMA, SQLITE_FILE, SHARD, HASH, TEMP, LAYOUT_FAULTS, sqliteOpenQueue, DURABILITY, isHash, FAULTS, pinnedDirectories, writableStores, sharedStores, activeReadSection, SqliteReplicaStore;
|
|
4645
|
+
var ReplicaErrorCode3, REPLICA_ERROR3, ReplicaError3, SCHEMA_VERSION, SCHEMA, POLICY_COLUMNS, normalizeSchema, SOURCE_SCHEMA, V1_SCHEMA, POLICY_REPLICA, POLICY_SCHEMA, SQLITE_FILE, SHARD, HASH, TEMP, LAYOUT_FAULTS, sqliteOpenQueue, DURABILITY, isHash, isShard, isTempName, FAULTS, pinnedDirectories, writableStores, sharedStores, activeReadSection, SqliteReplicaStore;
|
|
4578
4646
|
var init_sqlite = __esm({
|
|
4579
4647
|
"../replica/dist/sqlite.js"() {
|
|
4580
4648
|
"use strict";
|
|
@@ -4751,6 +4819,8 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
4751
4819
|
sqliteOpenQueue = Promise.resolve();
|
|
4752
4820
|
DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
|
|
4753
4821
|
isHash = (name) => /^[0-9a-f]{64}$/.test(name);
|
|
4822
|
+
isShard = (name) => /^[0-9a-f]{2}$/.test(name);
|
|
4823
|
+
isTempName = (name) => /^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name);
|
|
4754
4824
|
FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.faults");
|
|
4755
4825
|
pinnedDirectories = new FinalizationRegistry((fd) => {
|
|
4756
4826
|
try {
|
|
@@ -4788,6 +4858,8 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
4788
4858
|
#guard;
|
|
4789
4859
|
/** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
|
|
4790
4860
|
#ino;
|
|
4861
|
+
#admit;
|
|
4862
|
+
#partitionId;
|
|
4791
4863
|
#dev;
|
|
4792
4864
|
#namedAncestors;
|
|
4793
4865
|
constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared, readonlySnapshot = false) {
|
|
@@ -4807,6 +4879,8 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
4807
4879
|
this.#dev = identity.dev;
|
|
4808
4880
|
this.#now = options.now ?? Date.now;
|
|
4809
4881
|
this.#faults = options[FAULTS] ?? {};
|
|
4882
|
+
this.#admit = options.admit;
|
|
4883
|
+
this.#partitionId = options.partitionId ?? "";
|
|
4810
4884
|
this.#guard = options.guard ?? ((section) => section());
|
|
4811
4885
|
this.#namedAncestors = namedAncestors;
|
|
4812
4886
|
}
|
|
@@ -4876,6 +4950,55 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
4876
4950
|
}
|
|
4877
4951
|
}
|
|
4878
4952
|
}
|
|
4953
|
+
/**
|
|
4954
|
+
* Blob content bytes on disk in the replica at `dir` (0 when absent): the writer's blob files under `blobs/`,
|
|
4955
|
+
* including writer temp files and blobs no entry references any more, so garbage a failed sync
|
|
4956
|
+
* left behind still counts. Counts exactly what `collectOrphans` may delete: foreign entries
|
|
4957
|
+
* (stray files, symlinks, unknown names) are neither counted nor touched. Only blob content is counted: not the database, its WAL, rows or the
|
|
4958
|
+
* pending store. Filesystem-only: it opens no
|
|
4959
|
+
* database, so it can sum a profile's replicas under the caller's own mutation hold.
|
|
4960
|
+
*/
|
|
4961
|
+
static async contentBytes(dir) {
|
|
4962
|
+
try {
|
|
4963
|
+
let bytes = 0;
|
|
4964
|
+
for (const file of await writerBlobFiles(join22(dir, "blobs"))) bytes += file.size;
|
|
4965
|
+
return bytes;
|
|
4966
|
+
} catch (error) {
|
|
4967
|
+
throw storageError(error, "Measuring replica content");
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
/**
|
|
4971
|
+
* Remove blobs and temp files in the replica at `dir` that no committed row references: what a
|
|
4972
|
+
* crash between writing blobs and committing (or between committing and collecting) leaves.
|
|
4973
|
+
* Returns the number of files removed; 0 when the replica is absent or unreadable.
|
|
4974
|
+
*
|
|
4975
|
+
* The caller MUST hold the profile mutation guard, which every writer of this replica holds
|
|
4976
|
+
* across its blob write and commit; that is what makes an unreferenced blob safe to delete
|
|
4977
|
+
* (it cannot belong to an in-flight write). Never call it without that hold. The database is
|
|
4978
|
+
* read only, so nothing here can take a lease or change committed state.
|
|
4979
|
+
*/
|
|
4980
|
+
static async collectOrphans(dir) {
|
|
4981
|
+
const dbPath = join22(dir, "replica.db");
|
|
4982
|
+
const identity = await lstat2(dbPath).catch(() => void 0);
|
|
4983
|
+
if (!identity?.isFile()) return 0;
|
|
4984
|
+
const opener = await loadSqlite();
|
|
4985
|
+
let keep;
|
|
4986
|
+
let db;
|
|
4987
|
+
try {
|
|
4988
|
+
db = opener(dbPath, { readonly: true });
|
|
4989
|
+
db.exec("PRAGMA busy_timeout = 0");
|
|
4990
|
+
keep = new Set(db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash));
|
|
4991
|
+
} catch {
|
|
4992
|
+
return 0;
|
|
4993
|
+
} finally {
|
|
4994
|
+
db?.close();
|
|
4995
|
+
}
|
|
4996
|
+
try {
|
|
4997
|
+
return await unlinkWriterOrphans(join22(dir, "blobs"), keep);
|
|
4998
|
+
} catch (error) {
|
|
4999
|
+
throw storageError(error, "Collecting orphaned content");
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
4879
5002
|
/**
|
|
4880
5003
|
* Open the replica store in `dir`. With `create: false` a missing replica is
|
|
4881
5004
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
@@ -5585,6 +5708,41 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
5585
5708
|
}
|
|
5586
5709
|
reused.set(change.hash, size);
|
|
5587
5710
|
}
|
|
5711
|
+
if (this.#admit) {
|
|
5712
|
+
await this.#collectLocked(t);
|
|
5713
|
+
const changed = new Set(p.changes.map((change) => change.key));
|
|
5714
|
+
const kept = /* @__PURE__ */ new Set();
|
|
5715
|
+
for (const change of p.changes) if (!change.deleted && change.content) kept.add(change.hash);
|
|
5716
|
+
let blobBytes = 0;
|
|
5717
|
+
for (const [hash, bytes] of p.blobs) {
|
|
5718
|
+
const stored = this.#db.get(
|
|
5719
|
+
"SELECT 1 AS hit FROM entry WHERE hash = ? AND content = 1 AND deleted = 0 LIMIT 1",
|
|
5720
|
+
hash
|
|
5721
|
+
);
|
|
5722
|
+
if (stored === void 0) blobBytes += bytes.byteLength;
|
|
5723
|
+
}
|
|
5724
|
+
const released = /* @__PURE__ */ new Map();
|
|
5725
|
+
for (const key of changed) {
|
|
5726
|
+
const old = this.#db.get(
|
|
5727
|
+
"SELECT hash, size FROM entry WHERE key = ? AND deleted = 0 AND content = 1 AND hash IS NOT NULL AND size IS NOT NULL",
|
|
5728
|
+
key
|
|
5729
|
+
);
|
|
5730
|
+
if (old === void 0 || kept.has(old.hash) || released.has(old.hash)) continue;
|
|
5731
|
+
const holders = this.#db.all(
|
|
5732
|
+
"SELECT key FROM entry WHERE hash = ? AND content = 1 AND deleted = 0",
|
|
5733
|
+
old.hash
|
|
5734
|
+
);
|
|
5735
|
+
if (holders.every((holder) => changed.has(holder.key))) released.set(old.hash, old.size);
|
|
5736
|
+
}
|
|
5737
|
+
let freed = 0;
|
|
5738
|
+
for (const size of released.values()) freed += size;
|
|
5739
|
+
await this.#admit({
|
|
5740
|
+
partitionId: this.#partitionId,
|
|
5741
|
+
blobBytes: blobBytes - freed,
|
|
5742
|
+
entries: p.changes.length,
|
|
5743
|
+
freeBytes: null
|
|
5744
|
+
});
|
|
5745
|
+
}
|
|
5588
5746
|
try {
|
|
5589
5747
|
await this.#writeBlobs(p.blobs);
|
|
5590
5748
|
} catch (error) {
|
|
@@ -5660,36 +5818,16 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
5660
5818
|
});
|
|
5661
5819
|
} catch (error) {
|
|
5662
5820
|
if (isReplicaError(error, ReplicaErrorCode3.GRANT_REVOKED)) await this.#purgeContent().catch(() => void 0);
|
|
5821
|
+
else if (this.#admit) await this.#collectLocked(t).catch(() => void 0);
|
|
5663
5822
|
throw error;
|
|
5664
5823
|
}
|
|
5665
5824
|
this.#faults.afterCommit?.();
|
|
5825
|
+
if (this.#admit) await this.#collectLocked(t);
|
|
5666
5826
|
});
|
|
5667
5827
|
}
|
|
5668
5828
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
const root = join22(this.#pinnedDir, "blobs");
|
|
5672
|
-
for (const shard of await entriesOf(root)) {
|
|
5673
|
-
const shardPath = join22(root, shard);
|
|
5674
|
-
if (shard === ".tmp") {
|
|
5675
|
-
for (const name of await entriesOf(shardPath)) {
|
|
5676
|
-
await rm3(join22(shardPath, name), { force: true });
|
|
5677
|
-
removed += 1;
|
|
5678
|
-
}
|
|
5679
|
-
continue;
|
|
5680
|
-
}
|
|
5681
|
-
let kept = 0;
|
|
5682
|
-
for (const name of await entriesOf(shardPath)) {
|
|
5683
|
-
if (isHash(name) && keep.has(name)) {
|
|
5684
|
-
kept += 1;
|
|
5685
|
-
continue;
|
|
5686
|
-
}
|
|
5687
|
-
await rm3(join22(shardPath, name), { force: true });
|
|
5688
|
-
removed += 1;
|
|
5689
|
-
}
|
|
5690
|
-
if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
|
|
5691
|
-
}
|
|
5692
|
-
return removed;
|
|
5829
|
+
#unlinkBlobs(keep) {
|
|
5830
|
+
return unlinkBlobsIn(join22(this.#pinnedDir, "blobs"), keep);
|
|
5693
5831
|
}
|
|
5694
5832
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
5695
5833
|
async #holdsContentFiles() {
|
|
@@ -5700,33 +5838,35 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
5700
5838
|
return false;
|
|
5701
5839
|
}
|
|
5702
5840
|
async collectGarbage(t) {
|
|
5703
|
-
return this.#mutate(
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5841
|
+
return this.#mutate(() => this.#collectLocked(t));
|
|
5842
|
+
}
|
|
5843
|
+
/** `collectGarbage` for callers already inside the mutation hold (it cannot re-enter `#mutate`). */
|
|
5844
|
+
async #collectLocked(t) {
|
|
5845
|
+
const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
|
|
5846
|
+
try {
|
|
5847
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
5848
|
+
} catch (error) {
|
|
5849
|
+
release?.();
|
|
5850
|
+
throw storageError(error, "Collecting unused content");
|
|
5851
|
+
}
|
|
5852
|
+
try {
|
|
5853
|
+
this.#checkLease(t);
|
|
5854
|
+
await this.#faults.afterGarbageCollectionBegin?.();
|
|
5855
|
+
const keep = new Set(
|
|
5856
|
+
this.#db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash)
|
|
5857
|
+
);
|
|
5858
|
+
const removed = await this.#unlinkBlobs(keep);
|
|
5859
|
+
this.#db.exec("COMMIT");
|
|
5860
|
+
return removed;
|
|
5861
|
+
} catch (error) {
|
|
5711
5862
|
try {
|
|
5712
|
-
this.#
|
|
5713
|
-
|
|
5714
|
-
const keep = new Set(
|
|
5715
|
-
this.#db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash)
|
|
5716
|
-
);
|
|
5717
|
-
const removed = await this.#unlinkBlobs(keep);
|
|
5718
|
-
this.#db.exec("COMMIT");
|
|
5719
|
-
return removed;
|
|
5720
|
-
} catch (error) {
|
|
5721
|
-
try {
|
|
5722
|
-
this.#db.exec("ROLLBACK");
|
|
5723
|
-
} catch {
|
|
5724
|
-
}
|
|
5725
|
-
throw storageError(error, "Collecting unused content");
|
|
5726
|
-
} finally {
|
|
5727
|
-
release?.();
|
|
5863
|
+
this.#db.exec("ROLLBACK");
|
|
5864
|
+
} catch {
|
|
5728
5865
|
}
|
|
5729
|
-
|
|
5866
|
+
throw storageError(error, "Collecting unused content");
|
|
5867
|
+
} finally {
|
|
5868
|
+
release?.();
|
|
5869
|
+
}
|
|
5730
5870
|
}
|
|
5731
5871
|
async reset(t, reason, options = {}) {
|
|
5732
5872
|
await this.#mutate(async () => {
|