@delali/sirannon-db 0.3.4-next.62 → 0.3.4

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.
Files changed (108) hide show
  1. package/README.md +79 -56
  2. package/dist/backup/index.d.ts +44 -45
  3. package/dist/backup/index.mjs +7 -7
  4. package/dist/backup-scheduler/index.d.ts +42 -41
  5. package/dist/backup-scheduler/index.mjs +4 -4
  6. package/dist/baseline-Co3iIsN_.d.ts +19 -0
  7. package/dist/change-tracker-DqCdeEyx.d.ts +128 -0
  8. package/dist/{chunk-VOSJBZ6Q.mjs → chunk-2ES3DT4A.mjs} +71 -33
  9. package/dist/{chunk-6CMCO7S4.mjs → chunk-2O3PP4CF.mjs} +1 -1
  10. package/dist/{chunk-E7Q7HE46.mjs → chunk-3BZLZ47R.mjs} +26 -26
  11. package/dist/chunk-4KASJKKK.mjs +2501 -0
  12. package/dist/{chunk-MYNI2WFS.mjs → chunk-52XW3Z76.mjs} +40 -8
  13. package/dist/chunk-55YRLSM2.mjs +57 -0
  14. package/dist/{chunk-KDAHYFW7.mjs → chunk-6AOJCNRU.mjs} +5 -5
  15. package/dist/{chunk-KXTYR2OQ.mjs → chunk-A3PHCO6W.mjs} +39 -111
  16. package/dist/chunk-ADNWEHGZ.mjs +299 -0
  17. package/dist/{chunk-2QLXDHAP.mjs → chunk-BLMRNSUG.mjs} +11 -2
  18. package/dist/chunk-BVMZJOVM.mjs +5 -0
  19. package/dist/{chunk-IWGIYDMZ.mjs → chunk-BZWDPF4G.mjs} +1 -1
  20. package/dist/{chunk-YVF2ZJTE.mjs → chunk-CZL4D3BM.mjs} +1 -1
  21. package/dist/{chunk-7SC2TZAS.mjs → chunk-DX4WGJL7.mjs} +2 -2
  22. package/dist/{chunk-45MKELYA.mjs → chunk-ESTZZPT5.mjs} +132 -13
  23. package/dist/{chunk-QP5L5DFJ.mjs → chunk-FJPR2CJX.mjs} +3 -3
  24. package/dist/chunk-HR7K3GZW.mjs +36 -0
  25. package/dist/{chunk-LXCGPZPA.mjs → chunk-IEKIYO4V.mjs} +1 -1
  26. package/dist/{chunk-TUD5CJ76.mjs → chunk-KB4OLJAD.mjs} +26 -19
  27. package/dist/{chunk-PBRXXISQ.mjs → chunk-KINDFBQP.mjs} +3 -2
  28. package/dist/{chunk-E6IMX7RS.mjs → chunk-KJLNYOOL.mjs} +2 -2
  29. package/dist/{chunk-BTTFW4Z4.mjs → chunk-KU32OT4H.mjs} +2 -10
  30. package/dist/{chunk-JQWGHSEH.mjs → chunk-NVSQ2T5Q.mjs} +71 -70
  31. package/dist/{chunk-W4EUTJNT.mjs → chunk-NZGSDALX.mjs} +94 -96
  32. package/dist/{chunk-U445A3C4.mjs → chunk-QGVRRKB2.mjs} +1 -1
  33. package/dist/{chunk-GYCEJNU4.mjs → chunk-R7GBXNTO.mjs} +1 -1
  34. package/dist/{chunk-7C36BCSN.mjs → chunk-VDN6FHU4.mjs} +1 -1
  35. package/dist/{chunk-6IYS4WLH.mjs → chunk-VO3ZWA2G.mjs} +142 -115
  36. package/dist/chunk-WOMADPM4.mjs +12 -0
  37. package/dist/{chunk-OUSWVNWT.mjs → chunk-YCMB6GR4.mjs} +1 -1
  38. package/dist/{chunk-YNTDOFTN.mjs → chunk-YW7IKQ7T.mjs} +1 -1
  39. package/dist/{chunk-M2BEAH5I.mjs → chunk-ZN4HI5FK.mjs} +126 -72
  40. package/dist/{chunk-SBL6GN43.mjs → chunk-ZYWI45EN.mjs} +1 -1
  41. package/dist/client/index.d.ts +135 -137
  42. package/dist/client/index.mjs +177 -82
  43. package/dist/client/topology.d.ts +31 -31
  44. package/dist/client/topology.mjs +42 -23
  45. package/dist/client-base-BxTocU1x.d.ts +398 -0
  46. package/dist/client-streams-YKANXMWO.mjs +112 -0
  47. package/dist/codegen/cli.mjs +4 -4
  48. package/dist/codegen/index.d.ts +36 -29
  49. package/dist/codegen/index.mjs +4 -4
  50. package/dist/core/index.d.ts +130 -115
  51. package/dist/core/index.mjs +387 -297
  52. package/dist/core/writer-worker.mjs +3 -3
  53. package/dist/database-CGDsXW1K.d.ts +959 -0
  54. package/dist/database-backup-DcLhxoa1.d.ts +143 -0
  55. package/dist/driver/better-sqlite3.d.ts +11 -10
  56. package/dist/driver/better-sqlite3.mjs +11 -11
  57. package/dist/driver/bun.d.ts +9 -8
  58. package/dist/driver/expo.d.ts +6 -7
  59. package/dist/driver/node.d.ts +11 -10
  60. package/dist/driver/node.mjs +13 -11
  61. package/dist/driver/wa-sqlite.d.ts +6 -6
  62. package/dist/errors-CJUkQ-KL.d.ts +199 -0
  63. package/dist/file-migrations/index.d.ts +20 -18
  64. package/dist/file-migrations/index.mjs +3 -3
  65. package/dist/forward-rpc-VRWFZXHN.mjs +44 -0
  66. package/dist/operation-registry-CGhNT-Sn.d.ts +96 -0
  67. package/dist/primary-wins-CHNOYuN7.d.ts +81 -0
  68. package/dist/protocol-DFpNNsIT.d.ts +153 -0
  69. package/dist/query-types-5B02ixH0.d.ts +129 -0
  70. package/dist/react/index.d.ts +22 -22
  71. package/dist/replication/coordinator/etcd.d.ts +37 -31
  72. package/dist/replication/coordinator/etcd.mjs +296 -131
  73. package/dist/replication/index.d.ts +201 -161
  74. package/dist/replication/index.mjs +630 -529
  75. package/dist/restore-options-CH8mE-Tb.d.ts +84 -0
  76. package/dist/server/index.d.ts +201 -191
  77. package/dist/server/index.mjs +736 -619
  78. package/dist/server-options-aYtCkefX.d.ts +317 -0
  79. package/dist/server-streams-24XL4T43.mjs +254 -0
  80. package/dist/sirannon-J3EgPVD7.d.ts +179 -0
  81. package/dist/transport/grpc.d.ts +37 -24
  82. package/dist/transport/grpc.mjs +83 -3199
  83. package/dist/transport/memory.d.ts +25 -26
  84. package/dist/transport/memory.mjs +18 -9
  85. package/dist/types-BrAiDGOb.d.ts +505 -0
  86. package/dist/types-CEJG0eFS.d.ts +88 -0
  87. package/dist/types-Dc8oWlxj.d.ts +1580 -0
  88. package/dist/types-DqODKeOq.d.ts +123 -0
  89. package/dist/types-guEKSzMr.d.ts +367 -0
  90. package/package.json +7 -7
  91. package/dist/baseline-D93hcIEE.d.ts +0 -17
  92. package/dist/change-tracker-CjRFuZdB.d.ts +0 -144
  93. package/dist/client-base-Cmd9EVme.d.ts +0 -397
  94. package/dist/database-BLQvqjsw.d.ts +0 -917
  95. package/dist/database-backup-B7oszQ2n.d.ts +0 -141
  96. package/dist/errors-Dei4GdBb.d.ts +0 -196
  97. package/dist/operation-registry-oiPuSYI6.d.ts +0 -94
  98. package/dist/primary-wins-B0np8JS3.d.ts +0 -85
  99. package/dist/protocol-HI-nY8DD.d.ts +0 -152
  100. package/dist/query-types-p1liOP-b.d.ts +0 -127
  101. package/dist/restore-options-CksYsH1l.d.ts +0 -83
  102. package/dist/server-options-Bj5iJnjx.d.ts +0 -292
  103. package/dist/sirannon-BPxC5tC2.d.ts +0 -178
  104. package/dist/types-B0EhgDbr.d.ts +0 -340
  105. package/dist/types-CbjfRwrK.d.ts +0 -499
  106. package/dist/types-CjhxcjhA.d.ts +0 -123
  107. package/dist/types-DPSmjU54.d.ts +0 -1518
  108. package/dist/types-DTO4CYhW.d.ts +0 -88
@@ -1,29 +1,29 @@
1
- import { Transaction, LWWResolver, REMOTE_ORIGIN_NODE_ID, BatchReader, BatchApplier, recordLocalColumnVersions, HLC, loadPersistedHlc, isWellFormedHlc, HLC_CLOCK_META_KEY } from '../chunk-KXTYR2OQ.mjs';
2
- export { Transaction } from '../chunk-KXTYR2OQ.mjs';
3
- import { executeBatch, executeBatchSummary, execute, bindParams, executeGroup, query, queryForWire, queryOne } from '../chunk-MYNI2WFS.mjs';
4
- export { execute, executeBatch, query, queryOne } from '../chunk-MYNI2WFS.mjs';
5
- import { tokenizeSql, unsupported, findClauses, readSource, readSortTerms, readWindow, readSelectItems, parameterName } from '../chunk-SBL6GN43.mjs';
6
- import { reconcileMigrationChecksums, migrationContentChecksum, ensureCdcEpoch, SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling, runBulkLoad, TransactionGrouper, PrimedSubscription } from '../chunk-M2BEAH5I.mjs';
7
- export { ChangeTracker, isBulkLoadDurability, runBulkLoad } from '../chunk-M2BEAH5I.mjs';
8
- import { planBackupRestore, backupPiecesSafeToDelete } from '../chunk-YNTDOFTN.mjs';
9
- export { backupPiecesSafeToDelete, planBackupRestore } from '../chunk-YNTDOFTN.mjs';
10
- import { ensureMigrationsTable, prepareUpdateMigrationChecksum, prepareInsertMigration, selectAppliedMigrationsNewestFirst, prepareDeleteMigration, selectAppliedMigrations, tableInfoRows, selectTableSql, selectTableExists, selectMetaValue, upsertMetaValue, SEQ_STRING_RE, replaceMigrationHistory, highestMigrationVersion, selectCountOutboundChanges, ensureMetaTable, ensureChangesTable, ensureBatchApplyTables, PkResolver, selectMaxChangeSeq, selectMaxAppliedSourceSeq, insertMetaValueIfAbsent, selectMaxChangeHlc, updateUnstampedChangeStampsSql, UPSERT_META_VALUE_SQL, setForeignKeysEnabled, IDENTIFIER_RE, validateDdlSafety, ensureStagedChangesTable, upsertStagedChangeSql, selectFirstStagedTransactionEnd, deleteStagedChangesUpToSeq, selectMaxStagedSeq, deleteAllStagedChanges, deleteMetaValue, assertSafeIdentifier, selectStagedChangesInRange } from '../chunk-45MKELYA.mjs';
11
- import { ReplicationError } from '../chunk-2QLXDHAP.mjs';
12
- import { MIGRATIONS_TABLE, LIVE_PROBE_TABLE_PREFIX, META_TABLE, CHANGES_TABLE, assertSqlAllowed, INTERNAL_TABLE_PREFIX, eventTimestampToChangedAt, encodeTaggedValues, isReservedIdentifier, decodeTaggedValues, changedAtToEventTimestamp } from '../chunk-6CMCO7S4.mjs';
1
+ import { Transaction, LWWResolver, REMOTE_ORIGIN_NODE_ID, BatchReader, BatchApplier, recordLocalColumnVersions, loadPersistedHlc, isWellFormedHlc, HLC_CLOCK_META_KEY } from '../chunk-A3PHCO6W.mjs';
2
+ export { Transaction } from '../chunk-A3PHCO6W.mjs';
3
+ import { runWriterTransaction, executeBatch, executeBatchSummary, execute, bindParams, executeGroup, query, queryForWire, queryOne } from '../chunk-52XW3Z76.mjs';
4
+ export { execute, executeBatch, query, queryOne } from '../chunk-52XW3Z76.mjs';
5
+ import { tokenizeSql, unsupported, findClauses, readSource, readSortTerms, readWindow, readSelectItems, parameterName } from '../chunk-ZYWI45EN.mjs';
6
+ import { reconcileMigrationChecksums, migrationContentChecksum, resolveDeviceRetentionPolicy, ensureCdcEpoch, SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling, runBulkLoad, TransactionGrouper, PrimedSubscription } from '../chunk-ZN4HI5FK.mjs';
7
+ export { ChangeTracker, isBulkLoadDurability, runBulkLoad } from '../chunk-ZN4HI5FK.mjs';
8
+ import { planBackupRestore, backupPiecesSafeToDelete } from '../chunk-YW7IKQ7T.mjs';
9
+ export { backupPiecesSafeToDelete, planBackupRestore } from '../chunk-YW7IKQ7T.mjs';
10
+ import { ensureMigrationsTable, prepareUpdateMigrationChecksum, prepareInsertMigration, selectAppliedMigrationsNewestFirst, prepareDeleteMigration, selectAppliedMigrations, tableInfoRows, selectTableSql, selectTableExists, selectMetaValue, upsertMetaValue, SEQ_STRING_RE, replaceMigrationHistory, highestMigrationVersion, selectCountOutboundChanges, ensureMetaTable, ensureChangesTable, ensureBatchApplyTables, PkResolver, selectMaxChangeSeq, selectMaxAppliedSourceSeq, insertMetaValueIfAbsent, HLC, selectMaxChangeHlc, updateUnstampedChangeStampsSql, UPSERT_META_VALUE_SQL, setForeignKeysEnabled, IDENTIFIER_RE, validateDdlSafety, ensureStagedChangesTable, upsertStagedChangeSql, selectFirstStagedTransactionEnd, deleteStagedChangesUpToSeq, selectMaxStagedSeq, deleteAllStagedChanges, deleteMetaValue, assertSafeIdentifier, selectStagedChangesInRange } from '../chunk-ESTZZPT5.mjs';
11
+ import { ReplicationError } from '../chunk-BLMRNSUG.mjs';
12
+ import { MIGRATIONS_TABLE, LIVE_PROBE_TABLE_PREFIX, META_TABLE, CHANGES_TABLE, assertSqlAllowed, INTERNAL_TABLE_PREFIX, eventTimestampToChangedAt, encodeTaggedValues, isReservedIdentifier, decodeTaggedValues, changedAtToEventTimestamp } from '../chunk-2O3PP4CF.mjs';
13
13
  import '../chunk-GVCNMPOS.mjs';
14
- import { defaultStagingDir } from '../chunk-LXCGPZPA.mjs';
15
- export { defineDriver } from '../chunk-LXCGPZPA.mjs';
14
+ import { defaultStagingDir } from '../chunk-IEKIYO4V.mjs';
15
+ export { defineDriver } from '../chunk-IEKIYO4V.mjs';
16
16
  import { startCopyWithoutHoldingWriter } from '../chunk-W67A4H2E.mjs';
17
- import { DEFAULT_SYNCHRONOUS } from '../chunk-OUSWVNWT.mjs';
18
- export { DEFAULT_SYNCHRONOUS, isSynchronousLevel, synchronousPragmaValue } from '../chunk-OUSWVNWT.mjs';
19
- import { DEFAULT_CHAIN_NAME } from '../chunk-YVF2ZJTE.mjs';
20
- export { DEFAULT_CHAIN_NAME, readBackupChains } from '../chunk-YVF2ZJTE.mjs';
17
+ import { DEFAULT_SYNCHRONOUS } from '../chunk-YCMB6GR4.mjs';
18
+ export { DEFAULT_SYNCHRONOUS, isSynchronousLevel, synchronousPragmaValue } from '../chunk-YCMB6GR4.mjs';
19
+ import { DEFAULT_CHAIN_NAME } from '../chunk-CZL4D3BM.mjs';
20
+ export { DEFAULT_CHAIN_NAME, readBackupChains } from '../chunk-CZL4D3BM.mjs';
21
21
  import { invokeCallerCallback, randomHex } from '../chunk-QBBLSQHQ.mjs';
22
- import { LAZY_DOWN_SQL, parseMigrationFilename } from '../chunk-7C36BCSN.mjs';
23
- export { MIGRATION_FILENAME_PATTERN, parseMigrationFilename } from '../chunk-7C36BCSN.mjs';
24
- import { resolveEffectiveBaseline, planPendingMigrations, SQLITE_USER_VERSION_MAX, applyBaselineOption } from '../chunk-IWGIYDMZ.mjs';
25
- import { SirannonError, ConnectionPoolError, MigrationError, ReadOnlyError, MaxDatabasesError, DatabaseAlreadyExistsError, DatabaseNotFoundError, CDCError, WriteOverloadError, ExtensionError } from '../chunk-PBRXXISQ.mjs';
26
- export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, ForbiddenSqlError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, RequestDeniedError, SirannonError, TransactionError, WriteOverloadError } from '../chunk-PBRXXISQ.mjs';
22
+ import { LAZY_DOWN_SQL, parseMigrationFilename } from '../chunk-VDN6FHU4.mjs';
23
+ export { MIGRATION_FILENAME_PATTERN, parseMigrationFilename } from '../chunk-VDN6FHU4.mjs';
24
+ import { resolveEffectiveBaseline, planPendingMigrations, SQLITE_USER_VERSION_MAX, applyBaselineOption } from '../chunk-BZWDPF4G.mjs';
25
+ import { SirannonError, ConnectionPoolError, MigrationError, ReadOnlyError, MaxDatabasesError, DatabaseAlreadyExistsError, DatabaseNotFoundError, CDCError, WriteOverloadError, ExtensionError } from '../chunk-KINDFBQP.mjs';
26
+ export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, ForbiddenSqlError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, RequestDeniedError, SirannonError, TransactionError, WriteOverloadError } from '../chunk-KINDFBQP.mjs';
27
27
 
28
28
  // src/core/connection-pool.ts
29
29
  async function closeAllSilently(connections) {
@@ -101,7 +101,7 @@ var ConnectionPool = class _ConnectionPool {
101
101
  }
102
102
  return this.writer;
103
103
  }
104
- /** Returns the writer and every reader, for an operation that must apply to the whole pool. */
104
+ /** Returns the writer and every reader, so that the caller can apply an operation to every connection in the pool. */
105
105
  connections() {
106
106
  if (this.closed) {
107
107
  throw new ConnectionPoolError("Connection pool is closed");
@@ -263,9 +263,10 @@ var DatabaseBackupController = class {
263
263
  );
264
264
  }
265
265
  /**
266
- * Builds the cycle and starts it in the background. Its first turn copies the
267
- * whole database, and an open cannot wait on that. A failure reaches the
268
- * caller through the cycle's own error callback.
266
+ * Builds the backup cycle and starts it without awaiting the first turn, which
267
+ * copies the whole database, so that the open can return first. When the start
268
+ * fails, this method passes the error to the `onError` callback in the cycle
269
+ * options.
269
270
  */
270
271
  startCycle(options) {
271
272
  const engine = this.require();
@@ -318,7 +319,7 @@ var DatabaseBackupController = class {
318
319
  async piecesSafeToDelete(options) {
319
320
  return backupPiecesSafeToDelete(await this.chains(), options);
320
321
  }
321
- /** Stops the cycle, capturing the log one final time so nothing written since the previous turn is lost. */
322
+ /** Stops the backup cycle after one last capture, which sends the writes made since the previous turn to the destination. */
322
323
  async stopCycle() {
323
324
  if (!this.cycle) return;
324
325
  const cycle = this.cycle;
@@ -445,12 +446,14 @@ var SyncStamper = class _SyncStamper {
445
446
 
446
447
  // src/core/database-cdc.ts
447
448
  var DatabaseCdcController = class {
448
- constructor(runExclusive, acquireWriter, pollInterval, retention, openSnapshotConnection) {
449
+ constructor(runExclusive, acquireWriter, pollInterval, retention, openSnapshotConnection, deviceRetention, onDispatched) {
449
450
  this.runExclusive = runExclusive;
450
451
  this.acquireWriter = acquireWriter;
451
452
  this.pollInterval = pollInterval;
452
453
  this.retention = retention;
453
454
  this.openSnapshotConnection = openSnapshotConnection;
455
+ this.deviceRetention = deviceRetention;
456
+ this.onDispatched = onDispatched;
454
457
  }
455
458
  tracker = null;
456
459
  subscriptionManager = null;
@@ -545,7 +548,7 @@ var DatabaseCdcController = class {
545
548
  return Transaction.run(writer, fn);
546
549
  }
547
550
  const state = { sawDdl: false, droppedTables: [] };
548
- const result = await writer.transaction(async (txConn) => {
551
+ const result = await runWriterTransaction(writer, async (txConn) => {
549
552
  const value = await fn(new CdcAwareTransaction(txConn, tracker, state));
550
553
  await this.applyStamps(txConn);
551
554
  return value;
@@ -563,7 +566,7 @@ var DatabaseCdcController = class {
563
566
  }
564
567
  ensure() {
565
568
  const tracker = this.tracker ?? new ChangeTracker({ retention: this.retention });
566
- const subscriptions = this.subscriptionManager ?? new SubscriptionManager();
569
+ const subscriptions = this.subscriptionManager ?? new SubscriptionManager(this.onDispatched);
567
570
  this.tracker = tracker;
568
571
  this.subscriptionManager = subscriptions;
569
572
  return { tracker, subscriptions };
@@ -577,7 +580,8 @@ var DatabaseCdcController = class {
577
580
  this.subscriptionManager,
578
581
  this.pollInterval,
579
582
  void 0,
580
- this.runExclusive
583
+ this.runExclusive,
584
+ this.deviceRetention
581
585
  );
582
586
  }
583
587
  };
@@ -872,6 +876,7 @@ var DatabaseMigrationController = class {
872
876
  };
873
877
 
874
878
  // src/core/hooks/query-hooks.ts
879
+ var STATEMENT_SUCCEEDED = { failed: false };
875
880
  function fireBeforeQueryHooks(parentHooks, localHooks, databaseId, sql, params, options) {
876
881
  const hasParent = parentHooks?.has("beforeQuery");
877
882
  const hasLocal = localHooks.has("beforeQuery");
@@ -886,19 +891,13 @@ function fireBeforeQueryHooks(parentHooks, localHooks, databaseId, sql, params,
886
891
  parentHooks?.invokeSync("beforeQuery", ctx);
887
892
  localHooks.invokeSync("beforeQuery", ctx);
888
893
  }
889
- function fireAfterQueryHooks(parentHooks, localHooks, databaseId, sql, params, durationMs) {
894
+ function fireAfterQueryHooks(parentHooks, localHooks, databaseId, sql, params, durationMs, outcome) {
890
895
  const hasParent = parentHooks?.has("afterQuery");
891
896
  const hasLocal = localHooks.has("afterQuery");
892
897
  if (!hasParent && !hasLocal) return;
893
- const ctx = { databaseId, sql, params, durationMs };
894
- try {
895
- parentHooks?.invokeSync("afterQuery", ctx);
896
- } catch {
897
- }
898
- try {
899
- localHooks.invokeSync("afterQuery", ctx);
900
- } catch {
901
- }
898
+ const ctx = outcome.failed ? { databaseId, sql, params, durationMs, error: outcome.error } : { databaseId, sql, params, durationMs };
899
+ parentHooks?.invokeSyncIgnoringFailures("afterQuery", ctx);
900
+ localHooks.invokeSyncIgnoringFailures("afterQuery", ctx);
902
901
  }
903
902
 
904
903
  // src/core/database-observability.ts
@@ -916,41 +915,50 @@ var DatabaseObserver = class {
916
915
  this.fireBefore(sql, params, options);
917
916
  const start = performance.now();
918
917
  try {
919
- return await op();
920
- } finally {
921
- this.fireAfter(sql, params, performance.now() - start);
918
+ const result = await op();
919
+ this.fireAfter(sql, params, performance.now() - start, STATEMENT_SUCCEEDED);
920
+ return result;
921
+ } catch (error) {
922
+ this.fireAfter(sql, params, performance.now() - start, { failed: true, error });
923
+ throw error;
922
924
  }
923
925
  }
924
- track(sql, op) {
926
+ track(sql, op, measure) {
925
927
  if (!this.metrics) return op();
926
- return this.metrics.trackQuery(op, { databaseId: this.databaseId, sql });
928
+ return this.metrics.trackQuery(op, { databaseId: this.databaseId, sql }, measure);
927
929
  }
928
930
  async withTransactionHooks(statements, op) {
929
931
  for (const statement of statements) this.fireBefore(statement.sql, statement.params);
930
932
  const start = performance.now();
931
933
  try {
932
- return await this.trackEach(statements, op);
933
- } finally {
934
- const durationMs = performance.now() - start;
935
- for (const statement of statements) this.fireAfter(statement.sql, statement.params, durationMs);
934
+ const results = await this.trackEach(statements, op);
935
+ this.fireAfterEach(statements, performance.now() - start, STATEMENT_SUCCEEDED);
936
+ return results;
937
+ } catch (error) {
938
+ this.fireAfterEach(statements, performance.now() - start, { failed: true, error });
939
+ throw error;
936
940
  }
937
941
  }
942
+ fireAfterEach(statements, durationMs, outcome) {
943
+ for (const statement of statements) this.fireAfter(statement.sql, statement.params, durationMs, outcome);
944
+ }
938
945
  trackEach(statements, op) {
939
946
  const metrics = this.metrics;
940
947
  if (!metrics) return op();
941
948
  let run = op;
942
- for (const statement of statements) {
949
+ statements.forEach((statement, index) => {
943
950
  const inner = run;
944
- const sql = statement.sql;
945
- run = () => metrics.trackQuery(inner, { databaseId: this.databaseId, sql });
946
- }
951
+ run = () => metrics.trackQuery(inner, { databaseId: this.databaseId, sql: statement.sql }, (results) => ({
952
+ changes: results[index]?.changes
953
+ }));
954
+ });
947
955
  return run();
948
956
  }
949
957
  fireBefore(sql, params, options) {
950
958
  fireBeforeQueryHooks(this.parentHooks, this.localHooks, this.databaseId, sql, params, options);
951
959
  }
952
- fireAfter(sql, params, durationMs) {
953
- fireAfterQueryHooks(this.parentHooks, this.localHooks, this.databaseId, sql, params, durationMs);
960
+ fireAfter(sql, params, durationMs, outcome) {
961
+ fireAfterQueryHooks(this.parentHooks, this.localHooks, this.databaseId, sql, params, durationMs, outcome);
954
962
  }
955
963
  };
956
964
 
@@ -1301,7 +1309,7 @@ var DatabaseSyncController = class {
1301
1309
  }
1302
1310
  applyLocalPruneBoundary() {
1303
1311
  if (this.localPruneBoundary !== null) {
1304
- this.cdc.changeTracker?.setPruneBoundary(this.localPruneBoundary);
1312
+ this.cdc.changeTracker?.setPruneBoundary("device-sync", this.localPruneBoundary);
1305
1313
  }
1306
1314
  }
1307
1315
  readOutboxBatch(afterSeq, limit) {
@@ -1579,6 +1587,14 @@ function toError(error) {
1579
1587
  }
1580
1588
 
1581
1589
  // src/core/database-writes.ts
1590
+ function changesOf(result) {
1591
+ return { changes: result.changes };
1592
+ }
1593
+ function totalChanges(results) {
1594
+ let changes = 0;
1595
+ for (const result of results) changes += result.changes;
1596
+ return { changes };
1597
+ }
1582
1598
  var DatabaseWriteController = class {
1583
1599
  constructor(deps) {
1584
1600
  this.deps = deps;
@@ -1592,7 +1608,8 @@ var DatabaseWriteController = class {
1592
1608
  () => writeGate.run(
1593
1609
  () => observer.track(
1594
1610
  sql,
1595
- () => writerLock.isHeld() ? groupCommitter.runUngrouped(sql, params) : groupCommitter.submit(sql, params)
1611
+ () => writerLock.isHeld() ? groupCommitter.runUngrouped(sql, params) : groupCommitter.submit(sql, params),
1612
+ changesOf
1596
1613
  )
1597
1614
  )
1598
1615
  );
@@ -1605,7 +1622,12 @@ var DatabaseWriteController = class {
1605
1622
  options,
1606
1623
  () => writeGate.run(
1607
1624
  () => writerLock.run(
1608
- () => this.runInTransaction(pool.acquireWriter(), sql, (txConn) => executeBatch(txConn, sql, paramsBatch))
1625
+ () => this.runInTransaction(
1626
+ pool.acquireWriter(),
1627
+ sql,
1628
+ (txConn) => executeBatch(txConn, sql, paramsBatch),
1629
+ totalChanges
1630
+ )
1609
1631
  )
1610
1632
  )
1611
1633
  );
@@ -1626,7 +1648,7 @@ var DatabaseWriteController = class {
1626
1648
  durability: options?.durability,
1627
1649
  checkpoint: options?.checkpoint ?? true,
1628
1650
  capturesChangeLog,
1629
- loadRows: () => this.runInTransaction(writer, sql, (txConn) => executeBatchSummary(txConn, sql, paramsBatch))
1651
+ loadRows: () => this.runInTransaction(writer, sql, (txConn) => executeBatchSummary(txConn, sql, paramsBatch), changesOf)
1630
1652
  });
1631
1653
  })
1632
1654
  )
@@ -1655,15 +1677,16 @@ var DatabaseWriteController = class {
1655
1677
  return results;
1656
1678
  });
1657
1679
  }
1658
- async runInTransaction(writer, sql, run) {
1680
+ async runInTransaction(writer, sql, run, measure) {
1659
1681
  const { observer, cdc } = this.deps;
1660
1682
  const result = await observer.track(
1661
1683
  sql,
1662
- () => writer.transaction(async (txConn) => {
1684
+ () => runWriterTransaction(writer, async (txConn) => {
1663
1685
  const value = await run(txConn);
1664
1686
  await cdc.applyStamps(txConn);
1665
1687
  return value;
1666
- })
1688
+ }),
1689
+ measure
1667
1690
  );
1668
1691
  await applyDdlSideEffectsIfRelevant(cdc.changeTracker, writer, sql);
1669
1692
  return result;
@@ -1678,8 +1701,16 @@ var HOOK_CONFIG_MAP = {
1678
1701
  onDatabaseOpen: "databaseOpen",
1679
1702
  onDatabaseClose: "databaseClose",
1680
1703
  onBeforeSubscribe: "beforeSubscribe",
1681
- onBeforeSnapshot: "beforeSnapshot"
1704
+ onBeforeSnapshot: "beforeSnapshot",
1705
+ onBeforePush: "beforePush"
1682
1706
  };
1707
+ function callSynchronously(event, hook, ctx) {
1708
+ const result = hook(ctx);
1709
+ if (result == null || typeof result.then !== "function") return;
1710
+ result.then(void 0, () => {
1711
+ });
1712
+ throw new Error(`Hook for '${event}' returned a Promise. Use invoke() for async hooks.`);
1713
+ }
1683
1714
  var HookRegistry = class {
1684
1715
  hooks = /* @__PURE__ */ new Map();
1685
1716
  constructor(config) {
@@ -1713,9 +1744,17 @@ var HookRegistry = class {
1713
1744
  if (!list || list.length === 0) return;
1714
1745
  const snapshot = list.slice();
1715
1746
  for (const hook of snapshot) {
1716
- const result = hook(ctx);
1717
- if (result != null && typeof result.then === "function") {
1718
- throw new Error(`Hook for '${event}' returned a Promise. Use invoke() for async hooks.`);
1747
+ callSynchronously(event, hook, ctx);
1748
+ }
1749
+ }
1750
+ invokeSyncIgnoringFailures(event, ctx) {
1751
+ const list = this.hooks.get(event);
1752
+ if (!list || list.length === 0) return;
1753
+ const snapshot = list.slice();
1754
+ for (const hook of snapshot) {
1755
+ try {
1756
+ callSynchronously(event, hook, ctx);
1757
+ } catch {
1719
1758
  }
1720
1759
  }
1721
1760
  }
@@ -1844,7 +1883,7 @@ var LoadedExtensions = class {
1844
1883
  await connection.loadExtension(resolvedPath);
1845
1884
  }
1846
1885
  }
1847
- /** Loads an extension onto the pool's connections and every connection opened beyond it, then records it. */
1886
+ /** Loads an extension into the pool's connections and every tracked connection, then records its resolved path. */
1848
1887
  load(poolConnections, extensionPath) {
1849
1888
  return this.runInTurn(async () => {
1850
1889
  const resolved = await loadExtension(this.driver, [...poolConnections, ...this.openedConnections], extensionPath);
@@ -1852,12 +1891,12 @@ var LoadedExtensions = class {
1852
1891
  });
1853
1892
  }
1854
1893
  /**
1855
- * Opens a connection, loads every recorded extension onto it, and returns a
1856
- * connection that forwards to it. Closing the returned connection stops the
1857
- * tracking and closes the one underneath.
1894
+ * Opens a connection, loads every recorded extension into it, and returns a
1895
+ * wrapper that forwards each call to it. Closing the wrapper stops the
1896
+ * tracking and closes the connection underneath.
1858
1897
  *
1859
- * @param openConnection - Opens the connection this database needs.
1860
- * @returns The connection to read and write through.
1898
+ * @param openConnection - The function that opens the connection that this database needs.
1899
+ * @returns The wrapper to read and write through.
1861
1900
  */
1862
1901
  open(openConnection) {
1863
1902
  return this.runInTurn(async () => {
@@ -1994,7 +2033,9 @@ async function createDatabaseRuntime(id, path, driver, options, internals) {
1994
2033
  () => pool.acquireWriter(),
1995
2034
  options?.cdcPollInterval ?? 50,
1996
2035
  options?.cdcRetention ?? 36e5,
1997
- canOpenSnapshotConnection ? openSnapshotConnection : null
2036
+ canOpenSnapshotConnection ? openSnapshotConnection : null,
2037
+ resolveDeviceRetentionPolicy(options),
2038
+ internals?.metrics?.observeDispatch(id)
1998
2039
  );
1999
2040
  const sync = new DatabaseSyncController(
2000
2041
  (op) => writeGate.run(() => writerLock.run(op)),
@@ -2068,20 +2109,26 @@ async function closeDatabaseRuntime(runtime, closeListeners) {
2068
2109
 
2069
2110
  // src/core/database-lifecycle.ts
2070
2111
  var DatabaseLifecycle = class {
2071
- /** Identifier this database was opened under. */
2112
+ /** The identifier that this database was opened under. */
2072
2113
  id;
2073
- /** File path of the SQLite database. */
2114
+ /** The file path of the SQLite database. */
2074
2115
  path;
2075
- /** Whether this database refuses writes. */
2116
+ /** Whether this database rejects writes. */
2076
2117
  readOnly;
2077
2118
  /**
2078
- * Pool, locks, and controllers this database runs its work through.
2119
+ * The retention settings that this database was opened with, which form an empty object when the caller set none.
2120
+ *
2121
+ * @internal
2122
+ */
2123
+ changeRetention;
2124
+ /**
2125
+ * The pool, locks, and controllers that this database sends its work through.
2079
2126
  *
2080
2127
  * @internal
2081
2128
  */
2082
2129
  runtime;
2083
2130
  /**
2084
- * Functions that run while this database closes.
2131
+ * The functions that Sirannon calls while this database closes.
2085
2132
  *
2086
2133
  * @internal
2087
2134
  */
@@ -2092,11 +2139,16 @@ var DatabaseLifecycle = class {
2092
2139
  this.path = path;
2093
2140
  this.runtime = runtime;
2094
2141
  this.readOnly = options?.readOnly ?? false;
2142
+ this.changeRetention = {
2143
+ ...options?.cdcRetention === void 0 ? {} : { cdcRetention: options.cdcRetention },
2144
+ ...options?.deviceCursorRetention === void 0 ? {} : { deviceCursorRetention: options.deviceCursorRetention },
2145
+ ...options?.maxChangesHeldForDevice === void 0 ? {} : { maxChangesHeldForDevice: options.maxChangesHeldForDevice }
2146
+ };
2095
2147
  }
2096
2148
  /**
2097
- * Registers a function that runs while this database closes.
2149
+ * Registers a function that Sirannon calls while this database closes.
2098
2150
  *
2099
- * @param fn - Called during {@link DatabaseLifecycle.close}.
2151
+ * @param fn - The function to call during {@link DatabaseLifecycle.close}.
2100
2152
  *
2101
2153
  * @internal
2102
2154
  */
@@ -2105,7 +2157,7 @@ var DatabaseLifecycle = class {
2105
2157
  this.closeListeners.push(fn);
2106
2158
  }
2107
2159
  /**
2108
- * Closes every connection this database holds and ends its subscriptions.
2160
+ * Closes every connection that this database opened, and stops delivering changes to its subscriptions.
2109
2161
  */
2110
2162
  async close() {
2111
2163
  if (this.closing) return;
@@ -2113,19 +2165,19 @@ var DatabaseLifecycle = class {
2113
2165
  await closeDatabaseRuntime(this.runtime, this.closeListeners);
2114
2166
  }
2115
2167
  /**
2116
- * Whether this database has been closed.
2168
+ * True once {@link DatabaseLifecycle.close} has started on this database.
2117
2169
  */
2118
2170
  get closed() {
2119
2171
  return this.closing;
2120
2172
  }
2121
2173
  /**
2122
- * Number of read connections the pool holds.
2174
+ * The number of read connections in the pool.
2123
2175
  */
2124
2176
  get readerCount() {
2125
2177
  return this.runtime.pool.readerCount;
2126
2178
  }
2127
2179
  /**
2128
- * Refuses the call when this database is closed, is loading a snapshot, or refuses writes.
2180
+ * Throws when this database is closed, is loading a snapshot, or rejects writes.
2129
2181
  *
2130
2182
  * @internal
2131
2183
  */
@@ -2134,7 +2186,7 @@ var DatabaseLifecycle = class {
2134
2186
  if (this.readOnly) throw new ReadOnlyError(this.id);
2135
2187
  }
2136
2188
  /**
2137
- * Refuses the call when this database is closed or is loading a snapshot.
2189
+ * Throws when this database is closed or is loading a snapshot.
2138
2190
  *
2139
2191
  * @internal
2140
2192
  */
@@ -2148,7 +2200,7 @@ var DatabaseLifecycle = class {
2148
2200
  }
2149
2201
  }
2150
2202
  /**
2151
- * Refuses the call when this database is closed.
2203
+ * Throws when this database is closed.
2152
2204
  *
2153
2205
  * @internal
2154
2206
  */
@@ -2164,71 +2216,74 @@ var DatabaseBackups = class extends DatabaseLifecycle {
2164
2216
  /**
2165
2217
  * Copies this database to a file while it stays open for reads and writes.
2166
2218
  *
2167
- * SQLite moves the pages in steps on the connection that writes, so a write
2168
- * runs in the gap between two steps rather than waiting for the whole copy.
2219
+ * Sirannon holds the writer only until SQLite copies the first step of pages,
2220
+ * so later writes commit between the steps of the copy and wait for none of it.
2169
2221
  *
2170
- * @param destPath - Path the copy is written to.
2171
- * @returns The run identifier, the timings, the pages it moved, and the bytes the file holds.
2222
+ * @param destPath - The path of the file that Sirannon writes the copy to.
2223
+ * @returns The run identifier, the timings, the number of pages copied, and the size of the file in bytes.
2172
2224
  */
2173
2225
  async backup(destPath) {
2174
2226
  this.ensureOpen();
2175
2227
  return this.runtime.backups.backup(destPath);
2176
2228
  }
2177
2229
  /**
2178
- * Copies this database to a destination you supply, in fixed-size pieces,
2179
- * while it stays open for reads and writes.
2230
+ * Copies this database to a destination that you supply, in fixed-size
2231
+ * pieces, while it stays open for reads and writes.
2180
2232
  *
2181
- * Sirannon carries no storage client, so the destination is where you
2182
- * connect object storage or anything else that moves bytes.
2233
+ * Sirannon includes no storage client, so you connect object storage, or any
2234
+ * other store, through the destination.
2183
2235
  *
2184
- * The pieces reach the destination as SQLite writes them where this runtime
2185
- * carries the compiled streaming extension, so the run needs no local disk.
2186
- * Every other runtime writes one local file first and needs local disk equal
2187
- * to the backup. {@link DatabaseBackups.backupCapabilities} states which of
2188
- * the two this one does.
2236
+ * Where this runtime has the compiled streaming extension, Sirannon sends each
2237
+ * piece to the destination as SQLite writes it, so the copy needs no local
2238
+ * disk. On every other runtime, Sirannon writes one local file first, which
2239
+ * needs local disk equal to the size of the backup.
2240
+ * {@link DatabaseBackups.backupCapabilities} reports which of the two routes
2241
+ * applies to this runtime.
2189
2242
  *
2190
- * @param options - Destination, naming, piece size, and progress reporting.
2191
- * @returns The run identifier, the timings, what the run wrote, and how often the copy restarted.
2243
+ * @param options - The destination, the naming, the piece size, and the progress callback.
2244
+ * @returns The run identifier, the timings, what the copy wrote, and how many times the copy restarted.
2192
2245
  */
2193
2246
  async backupTo(options) {
2194
2247
  this.ensureOpen();
2195
2248
  return this.runtime.backups.backupTo(options);
2196
2249
  }
2197
2250
  /**
2198
- * Reports which backup operations this database's runtime supports, so you
2199
- * learn before a run rather than when one fails.
2251
+ * Reports which backup operations this database's runtime supports, so that
2252
+ * you can check them before you start a backup.
2200
2253
  *
2201
- * @returns What this runtime copies, whether it needs local disk, and whether it schedules.
2254
+ * @returns Whether this runtime can copy the database, whether a copy needs local disk, and whether it can schedule copies.
2202
2255
  */
2203
2256
  backupCapabilities() {
2204
2257
  return this.runtime.backups.capabilities();
2205
2258
  }
2206
2259
  /**
2207
- * Starts repeating backups on a cron schedule, keeping a bounded number of files.
2260
+ * Starts copying this database to a directory on a cron schedule, and deletes
2261
+ * the oldest copies beyond {@link BackupScheduleOptions.maxFiles}.
2208
2262
  *
2209
- * @param options - Cron expression, destination directory, retention, and time zone, along with the callbacks Sirannon calls after each finished copy and after each failure.
2263
+ * @param options - The cron expression, destination directory, retention, and time zone, with the callbacks that Sirannon calls after each finished copy and after each failure.
2210
2264
  */
2211
2265
  scheduleBackup(options) {
2212
2266
  this.ensureOpen();
2213
2267
  this.runtime.backups.schedule(options);
2214
2268
  }
2215
2269
  /**
2216
- * Runs one turn of the checkpoint cycle now instead of waiting for its
2217
- * interval. A turn sends any capture still waiting, reads the log frames
2270
+ * Takes one turn of the backup cycle now, without waiting for the interval. A
2271
+ * turn sends any capture that is still waiting, captures the log frames
2218
2272
  * written since the previous turn, and then checkpoints the log.
2219
2273
  *
2220
- * A database needs the `backups` option to have a cycle at all. On a node its
2221
- * replication group backs up from somewhere else, the turn writes nothing and
2222
- * reports a skip through `onSkip`.
2274
+ * Only a database opened with the `backups` option has a cycle, and on any
2275
+ * other database this throws `BACKUP_UNSUPPORTED`. On a node whose replication
2276
+ * group takes its backups from another node, Sirannon writes nothing on the
2277
+ * turn and reports a skip through `onSkip`.
2223
2278
  *
2224
- * @returns What the turn wrote, or undefined where it wrote nothing.
2279
+ * @returns What Sirannon wrote on the turn, or undefined when it wrote nothing.
2225
2280
  */
2226
2281
  async captureBackupChanges() {
2227
2282
  this.ensureOpen();
2228
2283
  return this.runtime.backups.captureChanges();
2229
2284
  }
2230
2285
  /**
2231
- * Lists what the backup destination holds.
2286
+ * Lists the backup chains at the destination.
2232
2287
  *
2233
2288
  * @returns One entry per chain, newest first, each with its full copy and its change pieces in order.
2234
2289
  */
@@ -2237,12 +2292,13 @@ var DatabaseBackups = class extends DatabaseLifecycle {
2237
2292
  return this.runtime.backups.chains();
2238
2293
  }
2239
2294
  /**
2240
- * Reads what this database's backup cycle is doing at this moment, and what
2241
- * its recent turns produced.
2295
+ * Returns what this database's backup cycle is doing now, and the outcome of
2296
+ * its recent turns.
2242
2297
  *
2243
- * A caller that triggers a turn with {@link DatabaseBackups.captureBackupChanges}
2244
- * without waiting on it would read this until that turn finishes. A full copy
2245
- * of a large database reports its pages as SQLite moves them.
2298
+ * You can poll this while a turn that you started with
2299
+ * {@link DatabaseBackups.captureBackupChanges} is still under way, and during
2300
+ * a full copy of a large database, the status includes the pages that SQLite
2301
+ * has copied so far.
2246
2302
  *
2247
2303
  * @returns Whether a turn is under way, how far it has got, and the last run, skip, and failure.
2248
2304
  */
@@ -2251,64 +2307,64 @@ var DatabaseBackups = class extends DatabaseLifecycle {
2251
2307
  return this.runtime.backups.status();
2252
2308
  }
2253
2309
  /**
2254
- * Reads one of this database's backups back out of the destination and
2255
- * compares it against the record the backup that wrote it left behind.
2310
+ * Fetches one of this database's backups from the destination and checks it
2311
+ * against the record that Sirannon wrote with that backup.
2256
2312
  *
2257
- * A restore would fail on a damaged piece only once that restore had already
2258
- * begun, so name the backup here beforehand. Sirannon then fetches every
2259
- * piece in order and folds a SHA-256 over the bytes as they arrive, and it
2260
- * compares that digest and the byte count against the record. Only one piece
2261
- * is in memory at any moment, so a check over a large full copy needs no
2262
- * local storage of its own.
2313
+ * A restore fails on a damaged piece only after the restore has started, so
2314
+ * call this beforehand to check the backup. Sirannon fetches every piece in
2315
+ * order, computes a SHA-256 over the bytes as they arrive, and compares that
2316
+ * digest and the byte count with the record. Sirannon keeps one piece in
2317
+ * memory at a time, so checking a large full copy needs no local disk.
2263
2318
  *
2264
- * A missing piece, a byte count that differs from the recorded one, and a
2265
- * digest that differs from the recorded one will each fail with
2266
- * `BACKUP_DESTINATION_ERROR`.
2319
+ * This method throws `BACKUP_DESTINATION_ERROR` when a piece is missing, or
2320
+ * when the byte count or the digest differs from the record.
2267
2321
  *
2268
- * @param name - Name the backup is stored under, which every entry {@link DatabaseBackups.backupChain} returns states.
2269
- * @returns The pieces read, the bytes they add up to, and the digest where the backup recorded one.
2322
+ * @param name - The name that the backup is stored under, as each entry from {@link DatabaseBackups.backupChain} gives it.
2323
+ * @returns The number of pieces read, their total size in bytes, and the digest when the backup recorded one.
2270
2324
  */
2271
2325
  async verifyBackup(name) {
2272
2326
  this.ensureNotClosed();
2273
2327
  return this.runtime.backups.verify(name);
2274
2328
  }
2275
2329
  /**
2276
- * Reads where this database's backups are stored.
2330
+ * Returns where this database's backups are stored.
2277
2331
  *
2278
- * `restoreBackup` accepts what you get back, and it then rebuilds this
2279
- * database from its own backups at whatever path you name.
2332
+ * Pass the destination, chain name, and deadline from the result to
2333
+ * `restoreBackup` to rebuild this database from its backups at a path that
2334
+ * you name.
2280
2335
  *
2281
- * @returns The destination, the name its chains are listed under, and the directory Sirannon stages captures in.
2336
+ * @returns The destination, the name that the chains are listed under, and the directory where Sirannon stages captures.
2282
2337
  */
2283
2338
  backupLocation() {
2284
2339
  this.ensureNotClosed();
2285
2340
  return this.runtime.backups.location();
2286
2341
  }
2287
2342
  /**
2288
- * Works out what a restore to a given moment has to read.
2343
+ * Returns the full copy and the change pieces that a restore to a given
2344
+ * moment would apply.
2289
2345
  *
2290
- * The result also tells you the moment it would actually reach, which is when
2291
- * the last piece was captured. One piece covers every write in the interval
2292
- * it was taken over, so a restore arrives at a piece boundary, not at the
2293
- * exact millisecond you named.
2346
+ * The plan also includes the moment that the restored database would reflect,
2347
+ * which is the capture time of its last piece. Each piece contains every write
2348
+ * from one capture interval, so a restore stops at a piece boundary, which
2349
+ * can fall before the millisecond that you name.
2294
2350
  *
2295
- * @param moment - Epoch milliseconds you want back.
2296
- * @returns The full copy, the change pieces to apply in order, and the moment the result reflects.
2351
+ * @param moment - The moment to restore to, in epoch milliseconds.
2352
+ * @returns The full copy, the change pieces to apply in order, and the moment that the result reflects.
2297
2353
  */
2298
2354
  async backupRestorePlan(moment) {
2299
2355
  this.ensureNotClosed();
2300
2356
  return this.runtime.backups.restorePlan(moment);
2301
2357
  }
2302
2358
  /**
2303
- * Tells you which backups no restore still needs, so you can delete them
2304
- * knowing exactly what you give up. Name the earliest moment you still want
2305
- * to reach and the answer also covers the older chains a newer full copy
2306
- * already spans.
2359
+ * Returns the backup records that no restore needs any longer, so that you
2360
+ * can delete them from the destination yourself.
2307
2361
  *
2308
- * Sirannon lists them and deletes nothing. The destination is yours.
2362
+ * When you pass the earliest moment that a restore must still reach, the list
2363
+ * also includes every chain whose full copy finished before the full copy
2364
+ * that a restore to that moment would start from.
2309
2365
  *
2310
- * @param options - How far back you still want to be able to restore.
2311
- * @returns The records you may delete, oldest first.
2366
+ * @param options - The earliest moment that a restore must still reach.
2367
+ * @returns The records that you can delete, oldest chain first.
2312
2368
  */
2313
2369
  async backupPiecesSafeToDelete(options) {
2314
2370
  this.ensureNotClosed();
@@ -2317,12 +2373,18 @@ var DatabaseBackups = class extends DatabaseLifecycle {
2317
2373
  };
2318
2374
 
2319
2375
  // src/core/database-reads.ts
2376
+ function countRows(rows) {
2377
+ return { rowsReturned: rows.length };
2378
+ }
2379
+ function countRow(row) {
2380
+ return { rowsReturned: row === void 0 ? 0 : 1 };
2381
+ }
2320
2382
  function readRows(deps, sql, params, options) {
2321
2383
  return deps.observer.withQueryHooks(
2322
2384
  sql,
2323
2385
  params,
2324
2386
  options,
2325
- () => onReadConnection(deps, sql, (conn) => query(conn, sql, params))
2387
+ () => onReadConnection(deps, sql, (conn) => query(conn, sql, params), countRows)
2326
2388
  );
2327
2389
  }
2328
2390
  function readWireRows(deps, sql, params, options) {
@@ -2330,7 +2392,7 @@ function readWireRows(deps, sql, params, options) {
2330
2392
  sql,
2331
2393
  params,
2332
2394
  options,
2333
- () => onReadConnection(deps, sql, (conn) => queryForWire(conn, sql, params))
2395
+ () => onReadConnection(deps, sql, (conn) => queryForWire(conn, sql, params), countRows)
2334
2396
  );
2335
2397
  }
2336
2398
  function readOneRow(deps, sql, params, options) {
@@ -2338,14 +2400,14 @@ function readOneRow(deps, sql, params, options) {
2338
2400
  sql,
2339
2401
  params,
2340
2402
  options,
2341
- () => onReadConnection(deps, sql, (conn) => queryOne(conn, sql, params))
2403
+ () => onReadConnection(deps, sql, (conn) => queryOne(conn, sql, params), countRow)
2342
2404
  );
2343
2405
  }
2344
- function onReadConnection(deps, sql, op) {
2406
+ function onReadConnection(deps, sql, op, measure) {
2345
2407
  if (deps.pool.readerCount === 0) {
2346
- return deps.writerLock.run(() => deps.observer.track(sql, () => op(deps.pool.acquireWriter())));
2408
+ return deps.writerLock.run(() => deps.observer.track(sql, () => op(deps.pool.acquireWriter()), measure));
2347
2409
  }
2348
- return deps.observer.track(sql, () => op(deps.pool.acquireReader()));
2410
+ return deps.observer.track(sql, () => op(deps.pool.acquireReader()), measure);
2349
2411
  }
2350
2412
 
2351
2413
  // src/core/live/column-collations.ts
@@ -3430,22 +3492,23 @@ var Database = class _Database extends DatabaseBackups {
3430
3492
  return this.runtime.sync.applyChanges(batch, resolver);
3431
3493
  }
3432
3494
  /**
3433
- * Returns the device-sync port for this database, which reads and advances
3434
- * the cursors a `SyncController` keeps against a server.
3495
+ * Returns the device-sync port for this database, which a `SyncController`
3496
+ * calls to apply pulled changes, read unpushed local changes, and store its
3497
+ * push and pull cursors.
3435
3498
  *
3436
- * @returns The port, which `downloadDatabaseSnapshot` also accepts.
3499
+ * @returns The port, which you can also pass to `downloadDatabaseSnapshot`.
3437
3500
  */
3438
3501
  deviceSync() {
3439
3502
  this.ensureNotClosed();
3440
3503
  return this.runtime.sync.devicePort();
3441
3504
  }
3442
3505
  /**
3443
- * Runs a read and returns every row.
3506
+ * Executes a read and returns every row.
3444
3507
  *
3445
- * @param sql - The statement to run.
3446
- * @param params - Values bound to the statement, named or positional.
3447
- * @param options - Read concern for this statement.
3448
- * @returns The rows the statement produced.
3508
+ * @param sql - The statement to execute.
3509
+ * @param params - The values to bind to the statement, named or positional.
3510
+ * @param options - The read concern for this statement.
3511
+ * @returns The rows that the statement returns.
3449
3512
  */
3450
3513
  async query(sql, params, options) {
3451
3514
  this.ensureOpen();
@@ -3457,35 +3520,35 @@ var Database = class _Database extends DatabaseBackups {
3457
3520
  return readWireRows(this.runtime.reads, sql, params, options);
3458
3521
  }
3459
3522
  /**
3460
- * Runs a read and returns its first row.
3523
+ * Executes a read and returns its first row.
3461
3524
  *
3462
- * @param sql - The statement to run.
3463
- * @param params - Values bound to the statement, named or positional.
3464
- * @param options - Read concern for this statement.
3465
- * @returns The first row, or undefined when the statement produced none.
3525
+ * @param sql - The statement to execute.
3526
+ * @param params - The values to bind to the statement, named or positional.
3527
+ * @param options - The read concern for this statement.
3528
+ * @returns The first row, or undefined when the statement returns none.
3466
3529
  */
3467
3530
  async queryOne(sql, params, options) {
3468
3531
  this.ensureOpen();
3469
3532
  return readOneRow(this.runtime.reads, sql, params, options);
3470
3533
  }
3471
3534
  /**
3472
- * Runs one write.
3535
+ * Executes one write statement.
3473
3536
  *
3474
- * @param sql - The statement to run.
3475
- * @param params - Values bound to the statement, named or positional.
3476
- * @param options - Write concern for this statement.
3477
- * @returns How many rows changed, and the last inserted row id.
3537
+ * @param sql - The statement to execute.
3538
+ * @param params - The values to bind to the statement, named or positional.
3539
+ * @param options - The write concern for this statement.
3540
+ * @returns The number of rows that the statement changed, and the row id of the last inserted row.
3478
3541
  */
3479
3542
  async execute(sql, params, options) {
3480
3543
  this.ensureWritable();
3481
3544
  return this.runtime.writes.execute(sql, params, options);
3482
3545
  }
3483
3546
  /**
3484
- * Runs one statement over many parameter sets inside a single transaction.
3547
+ * Executes one statement once for each parameter set, inside a single transaction.
3485
3548
  *
3486
- * @param sql - The statement to run for each parameter set.
3487
- * @param paramsBatch - One parameter set per run.
3488
- * @param options - Write concern for the transaction.
3549
+ * @param sql - The statement to execute for each parameter set.
3550
+ * @param paramsBatch - One parameter set for each execution.
3551
+ * @param options - The write concern for the transaction.
3489
3552
  * @returns One result per parameter set, in order.
3490
3553
  */
3491
3554
  async executeBatch(sql, paramsBatch, options) {
@@ -3493,34 +3556,38 @@ var Database = class _Database extends DatabaseBackups {
3493
3556
  return this.runtime.writes.executeBatch(sql, paramsBatch, options);
3494
3557
  }
3495
3558
  /**
3496
- * Imports many rows with relaxed writer durability, then restores the configured level.
3559
+ * Imports many rows at a relaxed synchronous level, then restores the level that you configured.
3560
+ *
3561
+ * Use this for a load that you can re-run from scratch, and use
3562
+ * {@link Database.executeBatch} where the rows need full durability. Sirannon
3563
+ * holds the writer lock for the whole load and commits every row in one
3564
+ * transaction. Whether the load succeeds or fails, Sirannon restores the
3565
+ * configured level before this method resolves. The result contains totals
3566
+ * for the whole load, so that a large load keeps no per-row results in memory.
3497
3567
  *
3498
- * Use this for a load you can re-run from scratch, because
3499
- * {@link Database.executeBatch} keeps full durability. The load holds the
3500
- * writer lock throughout and restores the configured level before this
3501
- * resolves, whether it succeeds or fails. One transaction covers the whole
3502
- * batch, and the rows are summed rather than returned one by one to bound
3503
- * memory on a large load. Like {@link Database.execute}, this writes only to
3504
- * the local database; under replication the server routes loads through the
3505
- * engine instead.
3568
+ * Like {@link Database.execute}, this writes to the local database only. A
3569
+ * server whose execution target has no `bulkLoad` method, such as the
3570
+ * replication engine, rejects bulk loads with `BULK_LOAD_UNSUPPORTED`.
3506
3571
  *
3507
- * @param sql - The statement to run for each parameter set.
3572
+ * @param sql - The statement to execute for each parameter set.
3508
3573
  * @param paramsBatch - One parameter set per row.
3509
- * @param options - Durability during the load, and whether it ends with a checkpoint.
3510
- * @returns How many rows the load applied and how many rows changed.
3574
+ * @param options - The synchronous level during the load, and whether the load ends with a checkpoint.
3575
+ * @returns The number of parameter sets that the load applied, and the number of rows that changed.
3511
3576
  */
3512
3577
  async bulkLoad(sql, paramsBatch, options) {
3513
3578
  this.ensureWritable();
3514
3579
  return this.runtime.writes.bulkLoad(sql, paramsBatch, options);
3515
3580
  }
3516
3581
  /**
3517
- * Runs a fixed list of statements in one transaction so that several callers can share one commit.
3582
+ * Executes a fixed list of statements in one transaction, which Sirannon can commit together with other callers' writes.
3518
3583
  *
3519
- * This takes the statements up front rather than a callback, because a group
3520
- * cannot wait on an arbitrary caller-supplied callback without delaying every
3521
- * transaction beside it.
3584
+ * When every statement is an INSERT, UPDATE, DELETE, or REPLACE, Sirannon
3585
+ * groups the transaction with other writes into one commit, and otherwise it
3586
+ * executes the transaction on its own. This method takes the statements up
3587
+ * front, because a group commit that waited on a caller's callback would delay
3588
+ * every other transaction in the group.
3522
3589
  *
3523
- * @param statements - The statements to run, in order, each with its own parameters.
3590
+ * @param statements - The statements to execute, in order, each with its own parameters.
3524
3591
  * @returns One result per statement, in order.
3525
3592
  */
3526
3593
  async executeTransaction(statements) {
@@ -3529,19 +3596,19 @@ var Database = class _Database extends DatabaseBackups {
3529
3596
  return this.runtime.writes.executeTransaction(statements);
3530
3597
  }
3531
3598
  /**
3532
- * Runs a function inside one transaction, committing when it returns and rolling back when it throws.
3599
+ * Calls a function inside one transaction, and commits when the function returns or rolls back when it throws.
3533
3600
  *
3534
- * @param fn - Receives the transaction and runs statements on it.
3535
- * @returns Whatever the function returned.
3601
+ * @param fn - The function to call with the transaction, which executes its statements through it.
3602
+ * @returns The value that the function returns.
3536
3603
  */
3537
3604
  async transaction(fn) {
3538
3605
  this.ensureWritable();
3539
3606
  return this.runtime.writes.transaction(fn);
3540
3607
  }
3541
3608
  /**
3542
- * Starts recording changes to a table so that subscribers and replication see them.
3609
+ * Starts recording changes to a table, so that subscribers and replication receive them.
3543
3610
  *
3544
- * @param table - Name of the table to watch.
3611
+ * @param table - The name of the table to watch.
3545
3612
  */
3546
3613
  async watch(table) {
3547
3614
  this.ensureWritable();
@@ -3550,17 +3617,17 @@ var Database = class _Database extends DatabaseBackups {
3550
3617
  /**
3551
3618
  * Stops recording changes to a table.
3552
3619
  *
3553
- * @param table - Name of the table to stop watching.
3620
+ * @param table - The name of the table to stop watching.
3554
3621
  */
3555
3622
  async unwatch(table) {
3556
3623
  this.ensureOpen();
3557
3624
  await this.runtime.cdc.unwatch(table);
3558
3625
  }
3559
3626
  /**
3560
- * Runs a CDC maintenance write (change-log pruning) on the shared writer
3561
- * under the writer lock. Serialising it with application writes keeps it
3562
- * from becoming a second writer that contends for SQLite's single write
3563
- * lock and stalls the event loop on `busy_timeout`.
3627
+ * Executes a change-log maintenance write, such as pruning, on the shared
3628
+ * writer connection under the writer lock, so that it queues behind
3629
+ * application writes and never contends with them for SQLite's write lock,
3630
+ * which would block the event loop for the `busy_timeout`.
3564
3631
  *
3565
3632
  * @internal
3566
3633
  */
@@ -3574,22 +3641,22 @@ var Database = class _Database extends DatabaseBackups {
3574
3641
  await this.runtime.cdc.ensureStamping();
3575
3642
  }
3576
3643
  /**
3577
- * Begins a change subscription on a watched table.
3644
+ * Starts building a change subscription on a watched table.
3578
3645
  *
3579
- * @param table - Name of the watched table.
3580
- * @returns A builder you narrow with a filter and then subscribe to.
3646
+ * @param table - The name of the watched table.
3647
+ * @returns A builder that you narrow with a filter and then subscribe to.
3581
3648
  */
3582
3649
  on(table) {
3583
3650
  this.ensureOpen();
3584
3651
  return this.runtime.cdc.on(table);
3585
3652
  }
3586
3653
  /**
3587
- * Opens a live query that keeps a registered read's rows current as the tables behind it change.
3654
+ * Opens a live query that keeps the rows of a single-table SELECT current as that table changes, and watches the table first.
3588
3655
  *
3589
- * @param operation - Name of the registered read, or a reference built by {@link operationRef}.
3590
- * @param args - Arguments the operation takes.
3591
- * @param options - Re-read jitter and the transaction size above which the query re-reads.
3592
- * @returns The live query, already subscribed.
3656
+ * @param sql - The SELECT statement to keep current.
3657
+ * @param params - The values to bind to the statement, named or positional.
3658
+ * @param options - The re-read jitter, the transaction size above which the query re-reads, and the reporter for listener failures.
3659
+ * @returns The live query, holding its first rows and subscribed to changes.
3593
3660
  */
3594
3661
  async live(sql, params, options) {
3595
3662
  this.ensureWritable();
@@ -3597,17 +3664,17 @@ var Database = class _Database extends DatabaseBackups {
3597
3664
  return openLiveQuery({ cdc, watch: (table) => this.watch(table) }, sql, params, options);
3598
3665
  }
3599
3666
  /**
3600
- * Applies every migration this database has not yet applied, in ascending version order.
3667
+ * Applies every migration that this database has yet to apply, in ascending version order.
3601
3668
  *
3602
3669
  * @param migrations - The full set of migrations for this database.
3603
- * @returns Which migrations this call applied, and how many it skipped.
3670
+ * @returns The migrations that this call applied, and the number that the database had already applied.
3604
3671
  */
3605
3672
  async migrate(migrations) {
3606
3673
  this.ensureOpen();
3607
3674
  return this.runtime.migrations.migrate(migrations);
3608
3675
  }
3609
3676
  /**
3610
- * Lists the migrations this database has applied.
3677
+ * Lists the migrations that this database has applied.
3611
3678
  *
3612
3679
  * @returns One entry per applied migration, with its version, name, and checksum.
3613
3680
  */
@@ -3616,38 +3683,38 @@ var Database = class _Database extends DatabaseBackups {
3616
3683
  return this.runtime.migrations.applied();
3617
3684
  }
3618
3685
  /**
3619
- * Undoes applied migrations, newest first.
3686
+ * Reverts applied migrations, newest first.
3620
3687
  *
3621
- * @param migrations - The full set of migrations so that the runner finds each down statement.
3622
- * @param version - Lowest version to keep. Without it, only the newest migration is undone.
3623
- * @returns Which migrations this call undid.
3688
+ * @param migrations - The full set of migrations, from which the runner takes each down statement.
3689
+ * @param version - The version to roll back to, which stays applied with every version below it; when you omit it, the runner reverts only the newest migration.
3690
+ * @returns The migrations that this call reverted.
3624
3691
  */
3625
3692
  async rollback(migrations, version) {
3626
3693
  this.ensureOpen();
3627
3694
  return this.runtime.migrations.rollback(migrations, version);
3628
3695
  }
3629
3696
  /**
3630
- * Loads a SQLite extension into this database.
3697
+ * Loads a compiled SQLite extension into every connection of this database.
3631
3698
  *
3632
- * @param extensionPath - Path to the extension, which the driver resolves to an absolute path.
3699
+ * @param extensionPath - The path to the extension, which the driver resolves to an absolute path.
3633
3700
  */
3634
3701
  async loadExtension(extensionPath) {
3635
3702
  this.ensureOpen();
3636
3703
  await this.runtime.loadExtension(extensionPath);
3637
3704
  }
3638
3705
  /**
3639
- * Registers a hook that runs before each statement on this database. Throw from it to refuse the statement.
3706
+ * Registers a hook that Sirannon calls before each statement on this database; throw from the hook to reject the statement.
3640
3707
  *
3641
- * @param hook - Receives the statement, its parameters, and the concerns it carries.
3708
+ * @param hook - The hook, which Sirannon calls with the statement, its parameters, and its read or write concern.
3642
3709
  * @returns A function that removes the hook.
3643
3710
  */
3644
3711
  onBeforeQuery(hook) {
3645
3712
  return this.runtime.hookRegistry.register("beforeQuery", hook);
3646
3713
  }
3647
3714
  /**
3648
- * Registers a hook that runs after each statement on this database.
3715
+ * Registers a hook that Sirannon calls after each statement on this database.
3649
3716
  *
3650
- * @param hook - Receives the statement and how long it took.
3717
+ * @param hook - The hook, which Sirannon calls with the statement and its duration.
3651
3718
  * @returns A function that removes the hook.
3652
3719
  */
3653
3720
  onAfterQuery(hook) {
@@ -3788,9 +3855,10 @@ var LifecycleManager = class {
3788
3855
  };
3789
3856
 
3790
3857
  // src/core/lifecycle/tenant.ts
3791
- var SAFE_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
3858
+ var SAFE_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
3792
3859
  var MAX_ID_LENGTH = 255;
3793
3860
  var MAX_FILENAME_LENGTH = 255;
3861
+ var SAFE_ID_RULE = `1 to ${MAX_ID_LENGTH} lowercase letters, digits, underscores, or hyphens, starting with a letter or a digit`;
3794
3862
  function sanitizeTenantId(id) {
3795
3863
  if (!id || id.length > MAX_ID_LENGTH) return void 0;
3796
3864
  if (!SAFE_ID_PATTERN.test(id)) return void 0;
@@ -3799,7 +3867,7 @@ function sanitizeTenantId(id) {
3799
3867
  function tenantPath(basePath, tenantId, extension = ".db") {
3800
3868
  const sanitized = sanitizeTenantId(tenantId);
3801
3869
  if (!sanitized) {
3802
- throw new Error(`Invalid tenant ID: '${tenantId}'`);
3870
+ throw new Error(`Invalid tenant ID: '${tenantId}'. A tenant ID must be ${SAFE_ID_RULE}.`);
3803
3871
  }
3804
3872
  const filename = `${sanitized}${extension}`;
3805
3873
  if (filename.length > MAX_FILENAME_LENGTH) {
@@ -3828,14 +3896,17 @@ var MetricsCollector = class {
3828
3896
  constructor(config) {
3829
3897
  this.config = config ?? {};
3830
3898
  }
3831
- async trackQuery(fn, context) {
3899
+ async trackQuery(fn, context, measure) {
3832
3900
  if (!this.config.onQueryComplete) {
3833
3901
  return fn();
3834
3902
  }
3835
3903
  const start = performance.now();
3836
3904
  let failed = false;
3905
+ let outcome = {};
3837
3906
  try {
3838
- return await fn();
3907
+ const result = await fn();
3908
+ if (measure) outcome = measure(result);
3909
+ return result;
3839
3910
  } catch (err) {
3840
3911
  failed = true;
3841
3912
  throw err;
@@ -3844,6 +3915,7 @@ var MetricsCollector = class {
3844
3915
  try {
3845
3916
  this.config.onQueryComplete({
3846
3917
  ...context,
3918
+ ...outcome,
3847
3919
  durationMs,
3848
3920
  error: failed
3849
3921
  });
@@ -3867,6 +3939,12 @@ var MetricsCollector = class {
3867
3939
  } catch {
3868
3940
  }
3869
3941
  }
3942
+ observeDispatch(databaseId) {
3943
+ if (!this.config.onCDCEvent) return void 0;
3944
+ return (event, subscriberCount) => {
3945
+ this.trackCDCEvent({ databaseId, table: event.table, operation: event.type, subscriberCount });
3946
+ };
3947
+ }
3870
3948
  get active() {
3871
3949
  return !!(this.config.onQueryComplete || this.config.onConnectionOpen || this.config.onConnectionClose || this.config.onCDCEvent);
3872
3950
  }
@@ -3997,10 +4075,10 @@ var RegistryMigrationSet = class {
3997
4075
  }
3998
4076
  loading = null;
3999
4077
  /**
4000
- * Reads the set, loading it where a function supplies it.
4078
+ * Returns the migrations, and calls the source function on the first request when the migrations come from a function.
4001
4079
  *
4002
- * @returns The migrations, which is an empty list where the registry has none.
4003
- * @throws When the function returns anything but a list.
4080
+ * @returns The migrations, or an empty list when the registry has none.
4081
+ * @throws A `MigrationError` with code `MIGRATION_SOURCE_INVALID` when the function returns something other than an array.
4004
4082
  */
4005
4083
  load() {
4006
4084
  const source = this.source;
@@ -4026,7 +4104,7 @@ var RegistryMigrationSet = class {
4026
4104
  return loading;
4027
4105
  }
4028
4106
  /**
4029
- * Applies the set to one database. A database that refuses writes takes none.
4107
+ * Applies the migrations to one database, and skips a read-only database.
4030
4108
  *
4031
4109
  * @param db - The database to migrate.
4032
4110
  */
@@ -4038,6 +4116,25 @@ var RegistryMigrationSet = class {
4038
4116
  }
4039
4117
  };
4040
4118
 
4119
+ // src/core/registry-defaults.ts
4120
+ function withRegistryDefaults(registry, options) {
4121
+ const defaults = {};
4122
+ if (registry.writerWorker !== void 0 && options?.writerWorker === void 0) {
4123
+ defaults.writerWorker = registry.writerWorker;
4124
+ }
4125
+ if (registry.cdcRetention !== void 0 && options?.cdcRetention === void 0) {
4126
+ defaults.cdcRetention = registry.cdcRetention;
4127
+ }
4128
+ if (registry.deviceCursorRetention !== void 0 && options?.deviceCursorRetention === void 0) {
4129
+ defaults.deviceCursorRetention = registry.deviceCursorRetention;
4130
+ }
4131
+ if (registry.maxChangesHeldForDevice !== void 0 && options?.maxChangesHeldForDevice === void 0) {
4132
+ defaults.maxChangesHeldForDevice = registry.maxChangesHeldForDevice;
4133
+ }
4134
+ if (Object.keys(defaults).length === 0) return options;
4135
+ return { ...options, ...defaults };
4136
+ }
4137
+
4041
4138
  // src/core/sirannon-offline.ts
4042
4139
  async function takeDatabaseOffline(offline) {
4043
4140
  await offline.database.close();
@@ -4085,12 +4182,12 @@ var Sirannon = class {
4085
4182
  metricsCollector;
4086
4183
  lifecycleManager;
4087
4184
  migrations;
4088
- /** The driver, hooks, metrics, lifecycle, migrations, and writer-worker default this registry was built with. */
4185
+ /** The driver, hooks, metrics, lifecycle, migrations, and writer-worker and retention defaults that this registry was constructed with. */
4089
4186
  options;
4090
4187
  /**
4091
- * Builds a registry.
4188
+ * Creates a registry.
4092
4189
  *
4093
- * @param options - Driver, hooks, metrics, lifecycle, migrations, and the writer-worker default.
4190
+ * @param options - The driver, hooks, metrics, lifecycle, migrations, and the defaults for every database that the registry opens.
4094
4191
  */
4095
4192
  constructor(options) {
4096
4193
  this.options = options;
@@ -4113,14 +4210,18 @@ var Sirannon = class {
4113
4210
  get hookRegistry() {
4114
4211
  return this._hookRegistry;
4115
4212
  }
4213
+ /** @internal */
4214
+ get metrics() {
4215
+ return this.metricsCollector;
4216
+ }
4116
4217
  /**
4117
4218
  * Opens a database and registers it under an identifier.
4118
4219
  *
4119
- * @param id - Identifier callers reach this database by.
4120
- * @param path - File path of the SQLite database.
4121
- * @param options - Pool size, journal mode, durability, and change-capture settings.
4220
+ * @param id - The identifier that callers use to reach this database.
4221
+ * @param path - The file path of the SQLite database.
4222
+ * @param options - The pool size, journal mode, synchronous level, and change-capture settings.
4122
4223
  * @returns The open database.
4123
- * @throws When the identifier is already registered.
4224
+ * @throws A `DatabaseAlreadyExistsError` when the identifier is already in use, or `DATABASE_OPEN_FAILED` when the open or a registry migration fails.
4124
4225
  */
4125
4226
  async open(id, path, options) {
4126
4227
  this.ensureOpenAllowed(id);
@@ -4128,7 +4229,7 @@ var Sirannon = class {
4128
4229
  throw new DatabaseAlreadyExistsError(id);
4129
4230
  }
4130
4231
  this.opening.add(id);
4131
- const resolvedOptions = this.withRegistryDefaults(options);
4232
+ const resolvedOptions = withRegistryDefaults(this.options, options);
4132
4233
  let db;
4133
4234
  try {
4134
4235
  if (this._hookRegistry.has("beforeConnect")) {
@@ -4169,10 +4270,7 @@ var Sirannon = class {
4169
4270
  this.openedWith.delete(id);
4170
4271
  this.lifecycleManager?.untrack(id);
4171
4272
  if (this._hookRegistry.has("databaseClose")) {
4172
- try {
4173
- this._hookRegistry.invokeSync("databaseClose", { databaseId: id, path });
4174
- } catch {
4175
- }
4273
+ this._hookRegistry.invokeSyncIgnoringFailures("databaseClose", { databaseId: id, path });
4176
4274
  }
4177
4275
  this.metricsCollector?.trackConnection({
4178
4276
  databaseId: id,
@@ -4185,10 +4283,7 @@ var Sirannon = class {
4185
4283
  this.openedWith.set(id, resolvedOptions === void 0 ? { path } : { path, options: resolvedOptions });
4186
4284
  this.lifecycleManager?.markActive(id);
4187
4285
  if (this._hookRegistry.has("databaseOpen")) {
4188
- try {
4189
- this._hookRegistry.invokeSync("databaseOpen", { databaseId: id, path });
4190
- } catch {
4191
- }
4286
+ this._hookRegistry.invokeSyncIgnoringFailures("databaseOpen", { databaseId: id, path });
4192
4287
  }
4193
4288
  this.metricsCollector?.trackConnection({
4194
4289
  databaseId: id,
@@ -4198,15 +4293,10 @@ var Sirannon = class {
4198
4293
  });
4199
4294
  return db;
4200
4295
  }
4201
- withRegistryDefaults(options) {
4202
- const fallback = this.options.writerWorker;
4203
- if (fallback === void 0 || options?.writerWorker !== void 0) return options;
4204
- return { ...options, writerWorker: fallback };
4205
- }
4206
4296
  /**
4207
4297
  * Closes one database and removes it from the registry.
4208
4298
  *
4209
- * @param id - Identifier of the database to close.
4299
+ * @param id - The identifier of the database to close.
4210
4300
  */
4211
4301
  async close(id) {
4212
4302
  this.ensureRunning();
@@ -4217,19 +4307,18 @@ var Sirannon = class {
4217
4307
  await db.close();
4218
4308
  }
4219
4309
  /**
4220
- * Closes one database, calls an action against the file behind it, and then
4221
- * opens that database again under the same identifier with the settings it
4222
- * had before.
4310
+ * Closes one database, calls an action on its file, and then opens that
4311
+ * database again under the same identifier with its earlier settings.
4223
4312
  *
4224
- * A restore rebuilds a database at the path it already occupies, so no
4225
- * connection may be open on that file while Sirannon replaces its bytes. The
4226
- * identifier answers nothing until that database is open again, though an
4227
- * action that failed still leaves a database open at the end.
4313
+ * A restore rebuilds a database at its current path, so no connection may be
4314
+ * open on that file while Sirannon replaces its bytes. {@link Sirannon.get}
4315
+ * returns undefined for the identifier until the database is open again, and
4316
+ * Sirannon reopens the database even after a failed action.
4228
4317
  *
4229
- * @param id - Identifier of the database to take offline.
4230
- * @param action - Runs with the database closed, and receives its file path.
4231
- * @returns Whether the action returned, what it produced or threw, and what a failed reopen threw.
4232
- * @throws When no database is open under the identifier, when it refuses writes, or when the close fails.
4318
+ * @param id - The identifier of the database to take offline.
4319
+ * @param action - The function that Sirannon calls with the database file path once the database is closed.
4320
+ * @returns Whether the action returned, the value that it returned or the error that it threw, and any error that the reopen threw.
4321
+ * @throws A `DatabaseNotFoundError` when no database is open under the identifier, a `ReadOnlyError` when the database is read-only, or the close error when the close fails.
4233
4322
  *
4234
4323
  * @internal
4235
4324
  */
@@ -4265,7 +4354,7 @@ var Sirannon = class {
4265
4354
  /**
4266
4355
  * Returns an already-open database.
4267
4356
  *
4268
- * @param id - Identifier of the database.
4357
+ * @param id - The identifier of the database.
4269
4358
  * @returns The database, or undefined when none is open under that identifier.
4270
4359
  */
4271
4360
  get(id) {
@@ -4278,15 +4367,15 @@ var Sirannon = class {
4278
4367
  return void 0;
4279
4368
  }
4280
4369
  /**
4281
- * Returns an open database, opening it through the lifecycle resolver where it is not open yet.
4370
+ * Returns an open database, and opens it through the lifecycle resolver when it is not open yet.
4282
4371
  *
4283
- * This is the call an in-process application makes for a database the registry opens on first
4284
- * use through the `lifecycle.autoOpen` resolver, such as one file per tenant. Concurrent calls
4285
- * for the same unopened identifier share one open, so each receives the same database or the
4372
+ * An in-process application calls this for a database that the registry opens on first use
4373
+ * through the `lifecycle.autoOpen` resolver, such as one file per tenant. Concurrent calls for
4374
+ * the same unopened identifier share one open, so each call receives the same database or the
4286
4375
  * same error.
4287
4376
  *
4288
- * @param id - Identifier of the database.
4289
- * @returns The database, or undefined when none is open and the resolver names no path for it.
4377
+ * @param id - The identifier of the database.
4378
+ * @returns The database, or undefined when none is open and the resolver returns no path for it.
4290
4379
  */
4291
4380
  async resolve(id) {
4292
4381
  const db = this.get(id);
@@ -4303,29 +4392,30 @@ var Sirannon = class {
4303
4392
  return inFlight;
4304
4393
  }
4305
4394
  /**
4306
- * Returns the registry migration set every database applies as it opens.
4395
+ * Returns the registry's migration set, which Sirannon applies to each writable database as it opens.
4307
4396
  *
4308
- * Where the set came from a function, the registry calls that function once and
4309
- * caches what it returned, so this reports the same list on every call.
4397
+ * When the set comes from a function, the registry calls that function once and caches the
4398
+ * result, so this returns the same list on every call; after a failed load, the next call
4399
+ * tries again.
4310
4400
  *
4311
- * @returns The migrations declared in `SirannonOptions.migrations`, or an empty list where none are.
4401
+ * @returns The migrations declared in `SirannonOptions.migrations`, or an empty list when there are none.
4312
4402
  */
4313
4403
  registryMigrations() {
4314
4404
  return this.migrations.load();
4315
4405
  }
4316
4406
  /**
4317
- * Reports whether a database is open under an identifier.
4407
+ * Returns whether a database is open under an identifier.
4318
4408
  *
4319
- * @param id - Identifier to check.
4320
- * @returns True when the registry holds an open database under it.
4409
+ * @param id - The identifier to check.
4410
+ * @returns True when the registry has an open database under that identifier.
4321
4411
  */
4322
4412
  has(id) {
4323
4413
  return this.dbs.has(id);
4324
4414
  }
4325
4415
  /**
4326
- * Returns every database this registry currently holds open.
4416
+ * Returns every database that this registry has open.
4327
4417
  *
4328
- * @returns The open databases, keyed by identifier.
4418
+ * @returns A copy of the open databases, keyed by identifier.
4329
4419
  */
4330
4420
  databases() {
4331
4421
  return new Map(this.dbs);
@@ -4333,9 +4423,9 @@ var Sirannon = class {
4333
4423
  /**
4334
4424
  * Closes every open database and stops the lifecycle timers.
4335
4425
  *
4336
- * A database whose file Sirannon is replacing delays the shutdown until that
4337
- * work finishes, since a process that exits part-way through would leave the
4338
- * path with no database on it at all.
4426
+ * When Sirannon is replacing a database's file, the shutdown waits for that
4427
+ * work to finish, because a process that exits part-way through the
4428
+ * replacement can leave no database at the path.
4339
4429
  */
4340
4430
  async shutdown() {
4341
4431
  if (this._shutdown) return;
@@ -4347,45 +4437,45 @@ var Sirannon = class {
4347
4437
  await closeEveryDatabase(this.dbs);
4348
4438
  }
4349
4439
  /**
4350
- * Registers a hook that runs before each statement on every database in this registry. Throw from it to refuse the statement.
4440
+ * Registers a hook that Sirannon calls before each statement on every database in this registry; throw from the hook to reject the statement.
4351
4441
  *
4352
- * @param hook - Receives the statement, its parameters, and the concerns it carries.
4442
+ * @param hook - The hook, which Sirannon calls with the statement, its parameters, and its read or write concern.
4353
4443
  * @returns A function that removes the hook.
4354
4444
  */
4355
4445
  onBeforeQuery(hook) {
4356
4446
  return this._hookRegistry.register("beforeQuery", hook);
4357
4447
  }
4358
4448
  /**
4359
- * Registers a hook that runs after each statement on every database in this registry.
4449
+ * Registers a hook that Sirannon calls after each statement on every database in this registry.
4360
4450
  *
4361
- * @param hook - Receives the statement and how long it took.
4451
+ * @param hook - The hook, which Sirannon calls with the statement and its duration.
4362
4452
  * @returns A function that removes the hook.
4363
4453
  */
4364
4454
  onAfterQuery(hook) {
4365
4455
  return this._hookRegistry.register("afterQuery", hook);
4366
4456
  }
4367
4457
  /**
4368
- * Registers a hook that runs before a database connection opens.
4458
+ * Registers a hook that Sirannon calls before it opens a database.
4369
4459
  *
4370
- * @param hook - Receives the database identifier and its file path.
4460
+ * @param hook - The hook, which Sirannon calls with the database identifier and file path.
4371
4461
  * @returns A function that removes the hook.
4372
4462
  */
4373
4463
  onBeforeConnect(hook) {
4374
4464
  return this._hookRegistry.register("beforeConnect", hook);
4375
4465
  }
4376
4466
  /**
4377
- * Registers a hook that runs once a database is open.
4467
+ * Registers a hook that Sirannon calls once a database is open.
4378
4468
  *
4379
- * @param hook - Receives the database identifier and its file path.
4469
+ * @param hook - The hook, which Sirannon calls with the database identifier and file path.
4380
4470
  * @returns A function that removes the hook.
4381
4471
  */
4382
4472
  onDatabaseOpen(hook) {
4383
4473
  return this._hookRegistry.register("databaseOpen", hook);
4384
4474
  }
4385
4475
  /**
4386
- * Registers a hook that runs once a database is closed.
4476
+ * Registers a hook that Sirannon calls once a database is closed.
4387
4477
  *
4388
- * @param hook - Receives the database identifier and its file path.
4478
+ * @param hook - The hook, which Sirannon calls with the database identifier and file path.
4389
4479
  * @returns A function that removes the hook.
4390
4480
  */
4391
4481
  onDatabaseClose(hook) {