@tinycloud/cli 1.3.0-beta.5 → 1.3.0-beta.6

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.
@@ -19234,9 +19234,44 @@ var pinnedDirectories = new FinalizationRegistry((fd) => {
19234
19234
  } catch {
19235
19235
  }
19236
19236
  });
19237
+ var writableStores = /* @__PURE__ */ new Map();
19238
+ function releaseWritable(shared) {
19239
+ if (shared.references === 0 || --shared.references !== 0) return;
19240
+ if (writableStores.get(shared.key) === shared) writableStores.delete(shared.key);
19241
+ try {
19242
+ shared.db.close();
19243
+ } finally {
19244
+ if (shared.directoryFd !== void 0) closeSync(shared.directoryFd);
19245
+ }
19246
+ }
19247
+ var sharedStores = new FinalizationRegistry((shared) => {
19248
+ try {
19249
+ releaseWritable(shared);
19250
+ } catch {
19251
+ }
19252
+ });
19253
+ async function acquireWritableMutation(shared) {
19254
+ const previous = shared.mutationTail;
19255
+ let release;
19256
+ shared.mutationTail = new Promise((resolve22) => {
19257
+ release = resolve22;
19258
+ });
19259
+ await previous;
19260
+ return release;
19261
+ }
19237
19262
  var SqliteReplicaStore = class _SqliteReplicaStore {
19238
19263
  dir;
19239
- #db;
19264
+ #database;
19265
+ get #db() {
19266
+ if (this.#closed) throw new ReplicaError2(ReplicaErrorCode2.CLOSED, "The replica store is closed.");
19267
+ return this.#database;
19268
+ }
19269
+ #closed = false;
19270
+ #closing = false;
19271
+ #closePromise;
19272
+ #activeMutations = 0;
19273
+ #drained;
19274
+ #shared;
19240
19275
  #pinnedDir;
19241
19276
  #directoryFd;
19242
19277
  #holder = `${process.pid}-${randomBytes2(6).toString("hex")}`;
@@ -19247,12 +19282,18 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19247
19282
  #ino;
19248
19283
  #dev;
19249
19284
  #namedAncestors;
19250
- constructor(dir, db, identity, options, directoryFd, namedAncestors = []) {
19285
+ constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared) {
19251
19286
  this.dir = dir;
19252
- this.#pinnedDir = directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir;
19287
+ this.#pinnedDir = shared?.pinnedDir ?? (directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir);
19253
19288
  this.#directoryFd = directoryFd;
19254
- if (directoryFd !== void 0) pinnedDirectories.register(this, directoryFd, this);
19255
- this.#db = db;
19289
+ this.#shared = shared;
19290
+ if (shared) {
19291
+ shared.references++;
19292
+ sharedStores.register(this, shared, this);
19293
+ } else if (directoryFd !== void 0) {
19294
+ pinnedDirectories.register(this, directoryFd, this);
19295
+ }
19296
+ this.#database = db;
19256
19297
  this.#ino = identity.ino;
19257
19298
  this.#dev = identity.dev;
19258
19299
  this.#now = options.now ?? Date.now;
@@ -19322,7 +19363,6 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19322
19363
  );
19323
19364
  if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No regular replica database at ${dir}.`);
19324
19365
  }
19325
- const opener = options.sqlite ?? await loadSqlite();
19326
19366
  const releaseOpen = await acquireSqliteOpen();
19327
19367
  let store;
19328
19368
  let directoryFd;
@@ -19364,63 +19404,99 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19364
19404
  if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
19365
19405
  throw new Error("The replica database changed before SQLite opened it.");
19366
19406
  }
19367
- const previousFds = process.platform === "linux" && !options.sqlite ? new Map(readdirSync2("/proc/self/fd").map((fd) => {
19368
- try {
19369
- return [fd, readlinkSync(`/proc/self/fd/${fd}`)];
19370
- } catch {
19371
- return [fd, ""];
19407
+ const key = `${opened.dev}:${opened.ino}`;
19408
+ const active = writableStores.get(key);
19409
+ if (active && (active.dev !== identity.dev || active.ino !== identity.ino)) {
19410
+ throw new Error("The replica database changed while another controller holds it open.");
19411
+ }
19412
+ if (active && !options.sqlite) {
19413
+ const namedDirectory = lstatSync(dir, { bigint: true });
19414
+ if (!namedDirectory.isDirectory() || namedDirectory.dev !== opened.dev || namedDirectory.ino !== opened.ino) {
19415
+ throw new Error("The replica directory changed before its checked SQLite handle could be shared.");
19416
+ }
19417
+ const pinned = active.directoryFd === void 0 ? lstatSync(active.pinnedDir, { bigint: true }) : fstatSync(active.directoryFd, { bigint: true });
19418
+ const activeLeaf = lstatSync(join7(active.pinnedDir, "replica.db"));
19419
+ if (!pinned.isDirectory() || pinned.dev !== opened.dev || pinned.ino !== opened.ino || !activeLeaf.isFile() || activeLeaf.dev !== identity.dev || activeLeaf.ino !== identity.ino || active.directoryFd === void 0 && active.namedPath !== dir) {
19420
+ throw new Error("The live replica changed before its checked SQLite handle could be shared.");
19372
19421
  }
19373
- })) : null;
19374
- db = opener(dbPath);
19375
- const afterOpen = lstatSync(dbPath);
19376
- if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
19377
- throw new Error("The replica database changed while SQLite opened it.");
19422
+ if (directoryFd !== void 0) {
19423
+ closeSync(directoryFd);
19424
+ directoryFd = void 0;
19425
+ }
19426
+ store = new _SqliteReplicaStore(dir, active.db, identity, options, active.directoryFd, namedAncestors, active);
19378
19427
  }
19379
- if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
19380
- if (previousFds) {
19381
- const databases = readdirSync2("/proc/self/fd").filter((fd) => {
19428
+ if (!store) {
19429
+ const opener = options.sqlite ?? await loadSqlite();
19430
+ const previousFds = process.platform === "linux" && !options.sqlite ? new Map(readdirSync2("/proc/self/fd").map((fd) => {
19382
19431
  try {
19383
- const target = readlinkSync(`/proc/self/fd/${fd}`);
19384
- return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
19432
+ return [fd, readlinkSync(`/proc/self/fd/${fd}`)];
19385
19433
  } catch {
19386
- return false;
19434
+ return [fd, ""];
19387
19435
  }
19388
- });
19389
- if (databases.length === 0 || databases.some((fd) => {
19390
- try {
19391
- const actual = fstatSync(Number(fd));
19392
- return actual.dev !== identity.dev || actual.ino !== identity.ino;
19393
- } catch {
19394
- return true;
19436
+ })) : null;
19437
+ db = opener(dbPath);
19438
+ const afterOpen = lstatSync(dbPath);
19439
+ if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
19440
+ throw new Error("The replica database changed while SQLite opened it.");
19441
+ }
19442
+ if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
19443
+ if (previousFds) {
19444
+ const databases = readdirSync2("/proc/self/fd").filter((fd) => {
19445
+ try {
19446
+ const target = readlinkSync(`/proc/self/fd/${fd}`);
19447
+ return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
19448
+ } catch {
19449
+ return false;
19450
+ }
19451
+ });
19452
+ if (databases.length === 0 || databases.some((fd) => {
19453
+ try {
19454
+ const actual = fstatSync(Number(fd));
19455
+ return actual.dev !== identity.dev || actual.ino !== identity.ino;
19456
+ } catch {
19457
+ return true;
19458
+ }
19459
+ })) {
19460
+ throw new Error("SQLite opened a different replica database.");
19395
19461
  }
19396
- })) {
19397
- throw new Error("SQLite opened a different replica database.");
19398
- }
19399
- }
19400
- const after = lstatSync(dbPath);
19401
- if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
19402
- throw new Error("The replica database changed while SQLite opened it.");
19403
- }
19404
- db.exec("PRAGMA busy_timeout = 5000");
19405
- db.exec("PRAGMA journal_mode = WAL");
19406
- db.exec("PRAGMA synchronous = FULL");
19407
- db.exec("PRAGMA secure_delete = ON");
19408
- db.exec("PRAGMA foreign_keys = ON");
19409
- const version = db.get("PRAGMA user_version")?.user_version ?? 0;
19410
- if (version > SCHEMA_VERSION) {
19411
- throw new ReplicaError2(
19412
- ReplicaErrorCode2.STORAGE_ERROR,
19413
- `The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
19414
- );
19415
- }
19416
- if (version < SCHEMA_VERSION) {
19417
- db.exec("BEGIN IMMEDIATE");
19418
- db.exec(SCHEMA);
19419
- db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
19420
- db.exec("COMMIT");
19462
+ }
19463
+ const after = lstatSync(dbPath);
19464
+ if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
19465
+ throw new Error("The replica database changed while SQLite opened it.");
19466
+ }
19467
+ db.exec("PRAGMA busy_timeout = 5000");
19468
+ db.exec("PRAGMA journal_mode = WAL");
19469
+ db.exec("PRAGMA synchronous = FULL");
19470
+ db.exec("PRAGMA secure_delete = ON");
19471
+ db.exec("PRAGMA foreign_keys = ON");
19472
+ const version = db.get("PRAGMA user_version")?.user_version ?? 0;
19473
+ if (version > SCHEMA_VERSION) {
19474
+ throw new ReplicaError2(
19475
+ ReplicaErrorCode2.STORAGE_ERROR,
19476
+ `The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
19477
+ );
19478
+ }
19479
+ if (version < SCHEMA_VERSION) {
19480
+ db.exec("BEGIN IMMEDIATE");
19481
+ db.exec(SCHEMA);
19482
+ db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
19483
+ db.exec("COMMIT");
19484
+ }
19485
+ const shared = options.sqlite ? void 0 : {
19486
+ key,
19487
+ db,
19488
+ directoryFd,
19489
+ pinnedDir,
19490
+ namedPath: dir,
19491
+ dev: identity.dev,
19492
+ ino: identity.ino,
19493
+ references: 0,
19494
+ mutationTail: Promise.resolve()
19495
+ };
19496
+ store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors, shared);
19497
+ if (shared) writableStores.set(key, shared);
19498
+ db = void 0;
19421
19499
  }
19422
- store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors);
19423
- db = void 0;
19424
19500
  } finally {
19425
19501
  try {
19426
19502
  db?.close();
@@ -19459,30 +19535,53 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19459
19535
  );
19460
19536
  }
19461
19537
  }
19538
+ /** New calls stop at close; accepted mutation sections may still finish their transactions. */
19539
+ #assertAccepting() {
19540
+ if (this.#closing || this.#closed) {
19541
+ throw new ReplicaError2(ReplicaErrorCode2.CLOSED, "The replica store is closed.");
19542
+ }
19543
+ }
19462
19544
  /** A short mutation section: guarded (profile lock) and checked against the database this store opened. */
19463
19545
  #mutate(section) {
19464
- return this.#guard(async () => {
19465
- await this.#assertPresent();
19466
- return section();
19467
- });
19546
+ this.#assertAccepting();
19547
+ this.#activeMutations++;
19548
+ return (async () => {
19549
+ try {
19550
+ return await this.#guard(async () => {
19551
+ await this.#assertPresent();
19552
+ return section();
19553
+ });
19554
+ } finally {
19555
+ if (--this.#activeMutations === 0) {
19556
+ const drained = this.#drained;
19557
+ this.#drained = void 0;
19558
+ drained?.();
19559
+ }
19560
+ }
19561
+ })();
19468
19562
  }
19469
19563
  /** Run `body` in a write transaction (BEGIN IMMEDIATE). Callers outside a mutation section use `#guardedWrite`. */
19470
- #write(action, body) {
19564
+ async #write(action, body) {
19565
+ const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
19471
19566
  try {
19472
- this.#db.exec("BEGIN IMMEDIATE");
19473
- } catch (error) {
19474
- throw storageError(error, action);
19475
- }
19476
- try {
19477
- const result = body();
19478
- this.#db.exec("COMMIT");
19479
- return result;
19480
- } catch (error) {
19481
19567
  try {
19482
- this.#db.exec("ROLLBACK");
19483
- } catch {
19568
+ this.#db.exec("BEGIN IMMEDIATE");
19569
+ } catch (error) {
19570
+ throw storageError(error, action);
19571
+ }
19572
+ try {
19573
+ const result = body();
19574
+ this.#db.exec("COMMIT");
19575
+ return result;
19576
+ } catch (error) {
19577
+ try {
19578
+ this.#db.exec("ROLLBACK");
19579
+ } catch {
19580
+ }
19581
+ throw storageError(error, action);
19484
19582
  }
19485
- throw storageError(error, action);
19583
+ } finally {
19584
+ release?.();
19486
19585
  }
19487
19586
  }
19488
19587
  /** One write transaction as its own mutation section. */
@@ -19509,6 +19608,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19509
19608
  }
19510
19609
  }
19511
19610
  async open() {
19611
+ this.#assertAccepting();
19512
19612
  let row;
19513
19613
  try {
19514
19614
  row = this.#row();
@@ -19670,6 +19770,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19670
19770
  });
19671
19771
  }
19672
19772
  async hasContent(hashes) {
19773
+ this.#assertAccepting();
19673
19774
  const held = /* @__PURE__ */ new Set();
19674
19775
  for (let start = 0; start < hashes.length; start += 500) {
19675
19776
  const chunk = hashes.slice(start, start + 500);
@@ -19752,7 +19853,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19752
19853
  }
19753
19854
  await this.#faults.afterBlobs?.();
19754
19855
  try {
19755
- this.#write("Committing a sync page", () => {
19856
+ await this.#write("Committing a sync page", () => {
19756
19857
  this.#checkLease(t);
19757
19858
  const now = this.#now();
19758
19859
  if (p.window.notBefore !== null && now < Date.parse(p.window.notBefore)) {
@@ -19860,13 +19961,16 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19860
19961
  }
19861
19962
  async collectGarbage(t) {
19862
19963
  return this.#mutate(async () => {
19964
+ const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
19863
19965
  try {
19864
19966
  this.#db.exec("BEGIN IMMEDIATE");
19865
19967
  } catch (error) {
19968
+ release?.();
19866
19969
  throw storageError(error, "Collecting unused content");
19867
19970
  }
19868
19971
  try {
19869
19972
  this.#checkLease(t);
19973
+ await this.#faults.afterGarbageCollectionBegin?.();
19870
19974
  const keep = new Set(
19871
19975
  this.#db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash)
19872
19976
  );
@@ -19879,12 +19983,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19879
19983
  } catch {
19880
19984
  }
19881
19985
  throw storageError(error, "Collecting unused content");
19986
+ } finally {
19987
+ release?.();
19882
19988
  }
19883
19989
  });
19884
19990
  }
19885
19991
  async reset(t, reason, options = {}) {
19886
19992
  await this.#mutate(async () => {
19887
- this.#write("Resetting the replica", () => {
19993
+ await this.#write("Resetting the replica", () => {
19888
19994
  this.#checkLease(t);
19889
19995
  this.#db.run("DELETE FROM entry");
19890
19996
  this.#db.run(
@@ -19911,9 +20017,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19911
20017
  */
19912
20018
  async destroy(t) {
19913
20019
  await this.#mutate(async () => {
20020
+ const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
19914
20021
  try {
19915
20022
  this.#db.exec("BEGIN IMMEDIATE");
19916
20023
  } catch (error) {
20024
+ release?.();
19917
20025
  throw storageError(error, "Removing the replica");
19918
20026
  }
19919
20027
  try {
@@ -20012,6 +20120,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
20012
20120
  this.#db.exec("ROLLBACK");
20013
20121
  } catch {
20014
20122
  }
20123
+ release?.();
20015
20124
  }
20016
20125
  });
20017
20126
  }
@@ -20055,7 +20164,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
20055
20164
  * when both are done.
20056
20165
  */
20057
20166
  async #purgeContent() {
20058
- this.#write("Starting the revocation purge", () => {
20167
+ await this.#write("Starting the revocation purge", () => {
20059
20168
  this.#db.run("UPDATE replica SET purge_pending = 1 WHERE id = 1 AND revoked_detail IS NOT NULL");
20060
20169
  });
20061
20170
  try {
@@ -20066,7 +20175,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
20066
20175
  if (!this.#checkpoint()) {
20067
20176
  throw new ReplicaError2(ReplicaErrorCode2.BUSY, "A reader holds the replica's write-ahead log; the purge finishes on the next open.");
20068
20177
  }
20069
- this.#write("Finishing the revocation purge", () => {
20178
+ await this.#write("Finishing the revocation purge", () => {
20070
20179
  this.#db.run("UPDATE replica SET purge_pending = 0 WHERE id = 1 AND revoked_detail IS NOT NULL");
20071
20180
  });
20072
20181
  }
@@ -20076,6 +20185,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
20076
20185
  });
20077
20186
  }
20078
20187
  async get(key) {
20188
+ this.#assertAccepting();
20079
20189
  try {
20080
20190
  const row = this.#db.get("SELECT key, deleted, etag, hash, metadata, content FROM entry WHERE key = ?", key);
20081
20191
  return row === void 0 ? void 0 : entryOf(row);
@@ -20084,6 +20194,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
20084
20194
  }
20085
20195
  }
20086
20196
  async list(o) {
20197
+ this.#assertAccepting();
20087
20198
  const where = ["deleted = 0"];
20088
20199
  const params = [];
20089
20200
  if (o.prefix !== void 0 && o.prefix !== "") {
@@ -20169,15 +20280,31 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
20169
20280
  lastReset: state2.lastReset
20170
20281
  };
20171
20282
  }
20172
- async close() {
20173
- const fd = this.#directoryFd;
20174
- this.#directoryFd = void 0;
20175
- if (fd !== void 0) pinnedDirectories.unregister(this);
20176
- try {
20177
- this.#db.close();
20178
- } finally {
20179
- if (fd !== void 0) closeSync(fd);
20180
- }
20283
+ close() {
20284
+ if (this.#closePromise) return this.#closePromise;
20285
+ this.#closing = true;
20286
+ this.#closePromise = (async () => {
20287
+ if (this.#activeMutations !== 0) {
20288
+ await new Promise((resolve22) => {
20289
+ this.#drained = resolve22;
20290
+ });
20291
+ }
20292
+ this.#closed = true;
20293
+ const fd = this.#directoryFd;
20294
+ this.#directoryFd = void 0;
20295
+ if (this.#shared) {
20296
+ sharedStores.unregister(this);
20297
+ releaseWritable(this.#shared);
20298
+ } else {
20299
+ if (fd !== void 0) pinnedDirectories.unregister(this);
20300
+ try {
20301
+ this.#database.close();
20302
+ } finally {
20303
+ if (fd !== void 0) closeSync(fd);
20304
+ }
20305
+ }
20306
+ })();
20307
+ return this.#closePromise;
20181
20308
  }
20182
20309
  };
20183
20310