@tinycloud/cli 1.3.0-beta.30 → 1.3.0-beta.32
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/skills/tc-cli/SDK.md +7 -0
package/dist/legacy-entry.js
CHANGED
|
@@ -6856,6 +6856,74 @@ function storageError(error, action) {
|
|
|
6856
6856
|
function errnoOf(error) {
|
|
6857
6857
|
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
6858
6858
|
}
|
|
6859
|
+
async function writerBlobFiles(root) {
|
|
6860
|
+
const out = [];
|
|
6861
|
+
let shards;
|
|
6862
|
+
try {
|
|
6863
|
+
shards = await readdir2(root, { withFileTypes: true });
|
|
6864
|
+
} catch (error) {
|
|
6865
|
+
if (errnoOf(error) === "ENOENT" || errnoOf(error) === "ENOTDIR") return out;
|
|
6866
|
+
throw error;
|
|
6867
|
+
}
|
|
6868
|
+
for (const shard of shards) {
|
|
6869
|
+
const temp = shard.name === ".tmp";
|
|
6870
|
+
if (!shard.isDirectory() || !(temp || isShard(shard.name))) continue;
|
|
6871
|
+
const shardPath = join22(root, shard.name);
|
|
6872
|
+
let files;
|
|
6873
|
+
try {
|
|
6874
|
+
files = await readdir2(shardPath, { withFileTypes: true });
|
|
6875
|
+
} catch (error) {
|
|
6876
|
+
if (errnoOf(error) === "ENOENT" || errnoOf(error) === "ENOTDIR") continue;
|
|
6877
|
+
throw error;
|
|
6878
|
+
}
|
|
6879
|
+
for (const file of files) {
|
|
6880
|
+
if (!file.isFile()) continue;
|
|
6881
|
+
const ok = temp ? isTempName(file.name) : isHash(file.name) && file.name.startsWith(shard.name);
|
|
6882
|
+
if (!ok) continue;
|
|
6883
|
+
const path = join22(shardPath, file.name);
|
|
6884
|
+
const info = await lstat2(path).catch(() => void 0);
|
|
6885
|
+
if (!info?.isFile()) continue;
|
|
6886
|
+
out.push({ path, hash: temp ? null : file.name, size: info.size });
|
|
6887
|
+
}
|
|
6888
|
+
}
|
|
6889
|
+
return out;
|
|
6890
|
+
}
|
|
6891
|
+
async function unlinkWriterOrphans(root, keep) {
|
|
6892
|
+
let removed = 0;
|
|
6893
|
+
const touched = /* @__PURE__ */ new Set();
|
|
6894
|
+
for (const file of await writerBlobFiles(root)) {
|
|
6895
|
+
if (file.hash !== null && keep.has(file.hash)) continue;
|
|
6896
|
+
await rm3(file.path, { force: true });
|
|
6897
|
+
touched.add(dirname3(file.path));
|
|
6898
|
+
removed += 1;
|
|
6899
|
+
}
|
|
6900
|
+
for (const dir of touched) if (!dir.endsWith(".tmp")) await rmdir2(dir).catch(() => void 0);
|
|
6901
|
+
return removed;
|
|
6902
|
+
}
|
|
6903
|
+
async function unlinkBlobsIn(root, keep) {
|
|
6904
|
+
let removed = 0;
|
|
6905
|
+
for (const shard of await entriesOf(root)) {
|
|
6906
|
+
const shardPath = join22(root, shard);
|
|
6907
|
+
if (shard === ".tmp") {
|
|
6908
|
+
for (const name of await entriesOf(shardPath)) {
|
|
6909
|
+
await rm3(join22(shardPath, name), { force: true });
|
|
6910
|
+
removed += 1;
|
|
6911
|
+
}
|
|
6912
|
+
continue;
|
|
6913
|
+
}
|
|
6914
|
+
let kept = 0;
|
|
6915
|
+
for (const name of await entriesOf(shardPath)) {
|
|
6916
|
+
if (isHash(name) && keep.has(name)) {
|
|
6917
|
+
kept += 1;
|
|
6918
|
+
continue;
|
|
6919
|
+
}
|
|
6920
|
+
await rm3(join22(shardPath, name), { force: true });
|
|
6921
|
+
removed += 1;
|
|
6922
|
+
}
|
|
6923
|
+
if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
|
|
6924
|
+
}
|
|
6925
|
+
return removed;
|
|
6926
|
+
}
|
|
6859
6927
|
async function entriesOf(path) {
|
|
6860
6928
|
try {
|
|
6861
6929
|
return await readdir2(path);
|
|
@@ -6882,7 +6950,7 @@ async function acquireWritableMutation(shared) {
|
|
|
6882
6950
|
await previous;
|
|
6883
6951
|
return release;
|
|
6884
6952
|
}
|
|
6885
|
-
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;
|
|
6953
|
+
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;
|
|
6886
6954
|
var init_sqlite = __esm({
|
|
6887
6955
|
"../replica/dist/sqlite.js"() {
|
|
6888
6956
|
"use strict";
|
|
@@ -7059,6 +7127,8 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
7059
7127
|
sqliteOpenQueue = Promise.resolve();
|
|
7060
7128
|
DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
|
|
7061
7129
|
isHash = (name) => /^[0-9a-f]{64}$/.test(name);
|
|
7130
|
+
isShard = (name) => /^[0-9a-f]{2}$/.test(name);
|
|
7131
|
+
isTempName = (name) => /^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name);
|
|
7062
7132
|
FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.faults");
|
|
7063
7133
|
pinnedDirectories = new FinalizationRegistry((fd) => {
|
|
7064
7134
|
try {
|
|
@@ -7096,6 +7166,8 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
7096
7166
|
#guard;
|
|
7097
7167
|
/** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
|
|
7098
7168
|
#ino;
|
|
7169
|
+
#admit;
|
|
7170
|
+
#partitionId;
|
|
7099
7171
|
#dev;
|
|
7100
7172
|
#namedAncestors;
|
|
7101
7173
|
constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared, readonlySnapshot = false) {
|
|
@@ -7115,6 +7187,8 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
7115
7187
|
this.#dev = identity.dev;
|
|
7116
7188
|
this.#now = options.now ?? Date.now;
|
|
7117
7189
|
this.#faults = options[FAULTS] ?? {};
|
|
7190
|
+
this.#admit = options.admit;
|
|
7191
|
+
this.#partitionId = options.partitionId ?? "";
|
|
7118
7192
|
this.#guard = options.guard ?? ((section) => section());
|
|
7119
7193
|
this.#namedAncestors = namedAncestors;
|
|
7120
7194
|
}
|
|
@@ -7184,6 +7258,55 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
7184
7258
|
}
|
|
7185
7259
|
}
|
|
7186
7260
|
}
|
|
7261
|
+
/**
|
|
7262
|
+
* Blob content bytes on disk in the replica at `dir` (0 when absent): the writer's blob files under `blobs/`,
|
|
7263
|
+
* including writer temp files and blobs no entry references any more, so garbage a failed sync
|
|
7264
|
+
* left behind still counts. Counts exactly what `collectOrphans` may delete: foreign entries
|
|
7265
|
+
* (stray files, symlinks, unknown names) are neither counted nor touched. Only blob content is counted: not the database, its WAL, rows or the
|
|
7266
|
+
* pending store. Filesystem-only: it opens no
|
|
7267
|
+
* database, so it can sum a profile's replicas under the caller's own mutation hold.
|
|
7268
|
+
*/
|
|
7269
|
+
static async contentBytes(dir) {
|
|
7270
|
+
try {
|
|
7271
|
+
let bytes = 0;
|
|
7272
|
+
for (const file of await writerBlobFiles(join22(dir, "blobs"))) bytes += file.size;
|
|
7273
|
+
return bytes;
|
|
7274
|
+
} catch (error) {
|
|
7275
|
+
throw storageError(error, "Measuring replica content");
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
/**
|
|
7279
|
+
* Remove blobs and temp files in the replica at `dir` that no committed row references: what a
|
|
7280
|
+
* crash between writing blobs and committing (or between committing and collecting) leaves.
|
|
7281
|
+
* Returns the number of files removed; 0 when the replica is absent or unreadable.
|
|
7282
|
+
*
|
|
7283
|
+
* The caller MUST hold the profile mutation guard, which every writer of this replica holds
|
|
7284
|
+
* across its blob write and commit; that is what makes an unreferenced blob safe to delete
|
|
7285
|
+
* (it cannot belong to an in-flight write). Never call it without that hold. The database is
|
|
7286
|
+
* read only, so nothing here can take a lease or change committed state.
|
|
7287
|
+
*/
|
|
7288
|
+
static async collectOrphans(dir) {
|
|
7289
|
+
const dbPath = join22(dir, "replica.db");
|
|
7290
|
+
const identity = await lstat2(dbPath).catch(() => void 0);
|
|
7291
|
+
if (!identity?.isFile()) return 0;
|
|
7292
|
+
const opener = await loadSqlite();
|
|
7293
|
+
let keep;
|
|
7294
|
+
let db;
|
|
7295
|
+
try {
|
|
7296
|
+
db = opener(dbPath, { readonly: true });
|
|
7297
|
+
db.exec("PRAGMA busy_timeout = 0");
|
|
7298
|
+
keep = new Set(db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash));
|
|
7299
|
+
} catch {
|
|
7300
|
+
return 0;
|
|
7301
|
+
} finally {
|
|
7302
|
+
db?.close();
|
|
7303
|
+
}
|
|
7304
|
+
try {
|
|
7305
|
+
return await unlinkWriterOrphans(join22(dir, "blobs"), keep);
|
|
7306
|
+
} catch (error) {
|
|
7307
|
+
throw storageError(error, "Collecting orphaned content");
|
|
7308
|
+
}
|
|
7309
|
+
}
|
|
7187
7310
|
/**
|
|
7188
7311
|
* Open the replica store in `dir`. With `create: false` a missing replica is
|
|
7189
7312
|
* REPLICA_NOT_FOUND and nothing is written to disk.
|
|
@@ -7893,6 +8016,41 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
7893
8016
|
}
|
|
7894
8017
|
reused.set(change.hash, size);
|
|
7895
8018
|
}
|
|
8019
|
+
if (this.#admit) {
|
|
8020
|
+
await this.#collectLocked(t);
|
|
8021
|
+
const changed = new Set(p.changes.map((change) => change.key));
|
|
8022
|
+
const kept = /* @__PURE__ */ new Set();
|
|
8023
|
+
for (const change of p.changes) if (!change.deleted && change.content) kept.add(change.hash);
|
|
8024
|
+
let blobBytes = 0;
|
|
8025
|
+
for (const [hash, bytes] of p.blobs) {
|
|
8026
|
+
const stored = this.#db.get(
|
|
8027
|
+
"SELECT 1 AS hit FROM entry WHERE hash = ? AND content = 1 AND deleted = 0 LIMIT 1",
|
|
8028
|
+
hash
|
|
8029
|
+
);
|
|
8030
|
+
if (stored === void 0) blobBytes += bytes.byteLength;
|
|
8031
|
+
}
|
|
8032
|
+
const released = /* @__PURE__ */ new Map();
|
|
8033
|
+
for (const key of changed) {
|
|
8034
|
+
const old = this.#db.get(
|
|
8035
|
+
"SELECT hash, size FROM entry WHERE key = ? AND deleted = 0 AND content = 1 AND hash IS NOT NULL AND size IS NOT NULL",
|
|
8036
|
+
key
|
|
8037
|
+
);
|
|
8038
|
+
if (old === void 0 || kept.has(old.hash) || released.has(old.hash)) continue;
|
|
8039
|
+
const holders = this.#db.all(
|
|
8040
|
+
"SELECT key FROM entry WHERE hash = ? AND content = 1 AND deleted = 0",
|
|
8041
|
+
old.hash
|
|
8042
|
+
);
|
|
8043
|
+
if (holders.every((holder) => changed.has(holder.key))) released.set(old.hash, old.size);
|
|
8044
|
+
}
|
|
8045
|
+
let freed = 0;
|
|
8046
|
+
for (const size of released.values()) freed += size;
|
|
8047
|
+
await this.#admit({
|
|
8048
|
+
partitionId: this.#partitionId,
|
|
8049
|
+
blobBytes: blobBytes - freed,
|
|
8050
|
+
entries: p.changes.length,
|
|
8051
|
+
freeBytes: null
|
|
8052
|
+
});
|
|
8053
|
+
}
|
|
7896
8054
|
try {
|
|
7897
8055
|
await this.#writeBlobs(p.blobs);
|
|
7898
8056
|
} catch (error) {
|
|
@@ -7968,36 +8126,16 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
7968
8126
|
});
|
|
7969
8127
|
} catch (error) {
|
|
7970
8128
|
if (isReplicaError2(error, ReplicaErrorCode3.GRANT_REVOKED)) await this.#purgeContent().catch(() => void 0);
|
|
8129
|
+
else if (this.#admit) await this.#collectLocked(t).catch(() => void 0);
|
|
7971
8130
|
throw error;
|
|
7972
8131
|
}
|
|
7973
8132
|
this.#faults.afterCommit?.();
|
|
8133
|
+
if (this.#admit) await this.#collectLocked(t);
|
|
7974
8134
|
});
|
|
7975
8135
|
}
|
|
7976
8136
|
/** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
const root = join22(this.#pinnedDir, "blobs");
|
|
7980
|
-
for (const shard of await entriesOf(root)) {
|
|
7981
|
-
const shardPath = join22(root, shard);
|
|
7982
|
-
if (shard === ".tmp") {
|
|
7983
|
-
for (const name of await entriesOf(shardPath)) {
|
|
7984
|
-
await rm3(join22(shardPath, name), { force: true });
|
|
7985
|
-
removed += 1;
|
|
7986
|
-
}
|
|
7987
|
-
continue;
|
|
7988
|
-
}
|
|
7989
|
-
let kept = 0;
|
|
7990
|
-
for (const name of await entriesOf(shardPath)) {
|
|
7991
|
-
if (isHash(name) && keep.has(name)) {
|
|
7992
|
-
kept += 1;
|
|
7993
|
-
continue;
|
|
7994
|
-
}
|
|
7995
|
-
await rm3(join22(shardPath, name), { force: true });
|
|
7996
|
-
removed += 1;
|
|
7997
|
-
}
|
|
7998
|
-
if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
|
|
7999
|
-
}
|
|
8000
|
-
return removed;
|
|
8137
|
+
#unlinkBlobs(keep) {
|
|
8138
|
+
return unlinkBlobsIn(join22(this.#pinnedDir, "blobs"), keep);
|
|
8001
8139
|
}
|
|
8002
8140
|
/** Whether any content file (a blob or a temp file) is on disk. */
|
|
8003
8141
|
async #holdsContentFiles() {
|
|
@@ -8008,33 +8146,35 @@ CREATE TABLE IF NOT EXISTS grant_install (
|
|
|
8008
8146
|
return false;
|
|
8009
8147
|
}
|
|
8010
8148
|
async collectGarbage(t) {
|
|
8011
|
-
return this.#mutate(
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8149
|
+
return this.#mutate(() => this.#collectLocked(t));
|
|
8150
|
+
}
|
|
8151
|
+
/** `collectGarbage` for callers already inside the mutation hold (it cannot re-enter `#mutate`). */
|
|
8152
|
+
async #collectLocked(t) {
|
|
8153
|
+
const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
|
|
8154
|
+
try {
|
|
8155
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
8156
|
+
} catch (error) {
|
|
8157
|
+
release?.();
|
|
8158
|
+
throw storageError(error, "Collecting unused content");
|
|
8159
|
+
}
|
|
8160
|
+
try {
|
|
8161
|
+
this.#checkLease(t);
|
|
8162
|
+
await this.#faults.afterGarbageCollectionBegin?.();
|
|
8163
|
+
const keep = new Set(
|
|
8164
|
+
this.#db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash)
|
|
8165
|
+
);
|
|
8166
|
+
const removed = await this.#unlinkBlobs(keep);
|
|
8167
|
+
this.#db.exec("COMMIT");
|
|
8168
|
+
return removed;
|
|
8169
|
+
} catch (error) {
|
|
8019
8170
|
try {
|
|
8020
|
-
this.#
|
|
8021
|
-
|
|
8022
|
-
const keep = new Set(
|
|
8023
|
-
this.#db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash)
|
|
8024
|
-
);
|
|
8025
|
-
const removed = await this.#unlinkBlobs(keep);
|
|
8026
|
-
this.#db.exec("COMMIT");
|
|
8027
|
-
return removed;
|
|
8028
|
-
} catch (error) {
|
|
8029
|
-
try {
|
|
8030
|
-
this.#db.exec("ROLLBACK");
|
|
8031
|
-
} catch {
|
|
8032
|
-
}
|
|
8033
|
-
throw storageError(error, "Collecting unused content");
|
|
8034
|
-
} finally {
|
|
8035
|
-
release?.();
|
|
8171
|
+
this.#db.exec("ROLLBACK");
|
|
8172
|
+
} catch {
|
|
8036
8173
|
}
|
|
8037
|
-
|
|
8174
|
+
throw storageError(error, "Collecting unused content");
|
|
8175
|
+
} finally {
|
|
8176
|
+
release?.();
|
|
8177
|
+
}
|
|
8038
8178
|
}
|
|
8039
8179
|
async reset(t, reason, options = {}) {
|
|
8040
8180
|
await this.#mutate(async () => {
|