@delali/sirannon-db 0.2.3-next.37 → 0.2.3-next.39

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 (42) hide show
  1. package/dist/backup/index.d.ts +6 -84
  2. package/dist/backup/index.mjs +6 -584
  3. package/dist/backup-scheduler/index.d.ts +58 -8
  4. package/dist/backup-scheduler/index.mjs +3 -2
  5. package/dist/{change-tracker-DXHPFe9J.d.ts → change-tracker-uG9gWvsY.d.ts} +1 -1
  6. package/dist/chunk-2QCPVE67.mjs +143 -0
  7. package/dist/chunk-63VBFSCS.mjs +525 -0
  8. package/dist/chunk-7SC2TZAS.mjs +41 -0
  9. package/dist/{chunk-72I5UI5C.mjs → chunk-DVS4QSIZ.mjs} +51 -23
  10. package/dist/{chunk-BQTVNI5H.mjs → chunk-DXXCLTFF.mjs} +205 -45
  11. package/dist/{chunk-BQFQ65OL.mjs → chunk-LXCGPZPA.mjs} +9 -1
  12. package/dist/{chunk-ZKDBGX7H.mjs → chunk-OJQHM4MT.mjs} +365 -357
  13. package/dist/chunk-U445A3C4.mjs +88 -0
  14. package/dist/chunk-W77EEVOT.mjs +156 -0
  15. package/dist/client/index.d.ts +8 -8
  16. package/dist/client/topology.d.ts +4 -4
  17. package/dist/{client-base-CGBpjZUB.d.ts → client-base-C-B8u5Lm.d.ts} +2 -2
  18. package/dist/core/index.d.ts +9 -9
  19. package/dist/core/index.mjs +228 -48
  20. package/dist/{database-955qag_W.d.ts → database-BlhUdJAO.d.ts} +46 -39
  21. package/dist/{chain-queries-D5aZ-8v2.d.ts → database-backup-C090lMHc.d.ts} +52 -2
  22. package/dist/driver/better-sqlite3.d.ts +1 -1
  23. package/dist/driver/better-sqlite3.mjs +9 -6
  24. package/dist/driver/bun.d.ts +1 -1
  25. package/dist/driver/expo.d.ts +1 -1
  26. package/dist/driver/node.d.ts +1 -1
  27. package/dist/driver/node.mjs +9 -6
  28. package/dist/driver/wa-sqlite.d.ts +1 -1
  29. package/dist/file-migrations/index.d.ts +1 -1
  30. package/dist/{protocol-BfiujBsE.d.ts → protocol-QekTGoKn.d.ts} +2 -2
  31. package/dist/replication/index.d.ts +7 -7
  32. package/dist/restore-options-C9MW9gpm.d.ts +83 -0
  33. package/dist/server/index.d.ts +102 -9
  34. package/dist/server/index.mjs +355 -3
  35. package/dist/{server-options-D4VmfTHw.d.ts → server-options-DeK6ZG8u.d.ts} +8 -1
  36. package/dist/{sirannon-DaGUawHs.d.ts → sirannon-C9cTgFrw.d.ts} +48 -5
  37. package/dist/transport/grpc.d.ts +3 -3
  38. package/dist/transport/memory.d.ts +3 -3
  39. package/dist/{types-BEjS69rs.d.ts → types-C1yBGTdx.d.ts} +2 -2
  40. package/dist/{types-Xb0L3o3w.d.ts → types-DApb7dvw.d.ts} +266 -23
  41. package/package.json +8 -7
  42. package/dist/chunk-GBM4AU24.mjs +0 -42
@@ -1,5 +1,3 @@
1
- import { planBackupRestore, backupPiecesSafeToDelete } from '../chunk-YNTDOFTN.mjs';
2
- export { backupPiecesSafeToDelete, planBackupRestore } from '../chunk-YNTDOFTN.mjs';
3
1
  import { Transaction, LWWResolver, REMOTE_ORIGIN_NODE_ID, BatchReader, BatchApplier, recordLocalColumnVersions, HLC, loadPersistedHlc, isWellFormedHlc, HLC_CLOCK_META_KEY } from '../chunk-P2VJYRVY.mjs';
4
2
  export { Transaction } from '../chunk-P2VJYRVY.mjs';
5
3
  import { executeBatch, executeBatchSummary, execute, bindParams, executeGroup, query, queryForWire, queryOne } from '../chunk-HCCGEIZ2.mjs';
@@ -7,15 +5,19 @@ export { execute, executeBatch, query, queryOne } from '../chunk-HCCGEIZ2.mjs';
7
5
  import { tokenizeSql, unsupported, findClauses, readSource, readSortTerms, readWindow, readSelectItems, parameterName } from '../chunk-SBL6GN43.mjs';
8
6
  import { reconcileMigrationChecksums, migrationContentChecksum, ensureCdcEpoch, SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling, runBulkLoad, TransactionGrouper, PrimedSubscription } from '../chunk-ECEGYTVF.mjs';
9
7
  export { ChangeTracker, isBulkLoadDurability, runBulkLoad } from '../chunk-ECEGYTVF.mjs';
8
+ import { planBackupRestore, backupPiecesSafeToDelete } from '../chunk-YNTDOFTN.mjs';
9
+ export { backupPiecesSafeToDelete, planBackupRestore } from '../chunk-YNTDOFTN.mjs';
10
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-7FQRQH5Z.mjs';
11
11
  import { ReplicationError } from '../chunk-2QLXDHAP.mjs';
12
12
  import { MIGRATIONS_TABLE, LIVE_PROBE_TABLE_PREFIX, META_TABLE, CHANGES_TABLE, assertSqlAllowed, INTERNAL_TABLE_PREFIX, encodeTaggedValues, decodeTaggedValues } from '../chunk-7R4ER4FB.mjs';
13
13
  import '../chunk-GVCNMPOS.mjs';
14
- export { defineDriver } from '../chunk-BQFQ65OL.mjs';
15
- export { DEFAULT_CHAIN_NAME, readBackupChains } from '../chunk-YVF2ZJTE.mjs';
14
+ import { defaultStagingDir } from '../chunk-LXCGPZPA.mjs';
15
+ export { defineDriver } from '../chunk-LXCGPZPA.mjs';
16
16
  import { startCopyWithoutHoldingWriter } from '../chunk-W67A4H2E.mjs';
17
17
  import { DEFAULT_SYNCHRONOUS } from '../chunk-OUSWVNWT.mjs';
18
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';
19
21
  import { randomHex } from '../chunk-V5FUJEYY.mjs';
20
22
  import { LAZY_DOWN_SQL, parseMigrationFilename } from '../chunk-7C36BCSN.mjs';
21
23
  export { MIGRATION_FILENAME_PATTERN, parseMigrationFilename } from '../chunk-7C36BCSN.mjs';
@@ -202,6 +204,7 @@ var DatabaseBackupController = class {
202
204
  }
203
205
  cancellers = [];
204
206
  cycle = null;
207
+ cycleOptions = null;
205
208
  cycleStarted = Promise.resolve();
206
209
  require() {
207
210
  if (!this.engine) {
@@ -215,7 +218,7 @@ var DatabaseBackupController = class {
215
218
  requireCycle() {
216
219
  if (!this.cycle) {
217
220
  throw new SirannonError(
218
- `Database '${this.databaseId}' opened without the backups option, so it captures no change log and holds no chain`,
221
+ `Database '${this.databaseId}' opened without the backups option, so it captures no change log and extends no chain`,
219
222
  "BACKUP_UNSUPPORTED"
220
223
  );
221
224
  }
@@ -228,12 +231,13 @@ var DatabaseBackupController = class {
228
231
  this.engine?.streamsToDestination() ?? false
229
232
  );
230
233
  }
231
- backup(destPath) {
234
+ async backup(destPath) {
232
235
  const engine = this.require();
233
- return startCopyWithoutHoldingWriter(
236
+ const copy = await startCopyWithoutHoldingWriter(
234
237
  this.runExclusive,
235
238
  (onFirstStep) => engine.backup(this.acquireWriter(), destPath, onFirstStep)
236
239
  );
240
+ return { ...copy, databaseId: this.databaseId, sourcePath: this.sourcePath };
237
241
  }
238
242
  backupTo(options) {
239
243
  const engine = this.require();
@@ -249,7 +253,14 @@ var DatabaseBackupController = class {
249
253
  }
250
254
  schedule(options) {
251
255
  const engine = this.require();
252
- this.cancellers.push(engine.schedule(this.acquireWriter(), options, this.runExclusive));
256
+ this.cancellers.push(
257
+ engine.schedule(this.acquireWriter(), {
258
+ ...options,
259
+ databaseId: this.databaseId,
260
+ sourcePath: this.sourcePath,
261
+ runExclusive: this.runExclusive
262
+ })
263
+ );
253
264
  }
254
265
  /**
255
266
  * Builds the cycle and starts it in the background. Its first turn copies the
@@ -258,6 +269,7 @@ var DatabaseBackupController = class {
258
269
  */
259
270
  startCycle(options) {
260
271
  const engine = this.require();
272
+ this.cycleOptions = options;
261
273
  this.cycle = engine.createCycle({
262
274
  ...options,
263
275
  databaseId: this.databaseId,
@@ -281,6 +293,27 @@ var DatabaseBackupController = class {
281
293
  chains() {
282
294
  return this.requireCycle().chains();
283
295
  }
296
+ status() {
297
+ return this.requireCycle().status();
298
+ }
299
+ location() {
300
+ const options = this.cycleOptions;
301
+ if (!options) {
302
+ throw new SirannonError(
303
+ `Database '${this.databaseId}' opened without the backups option, so it captures no change log and extends no chain`,
304
+ "BACKUP_UNSUPPORTED"
305
+ );
306
+ }
307
+ return {
308
+ destination: options.destination,
309
+ chainName: options.chainName ?? DEFAULT_CHAIN_NAME,
310
+ stagingDir: options.stagingDir ?? defaultStagingDir(this.sourcePath),
311
+ ...options.destinationTimeoutMs === void 0 ? {} : { destinationTimeoutMs: options.destinationTimeoutMs }
312
+ };
313
+ }
314
+ verify(name) {
315
+ return this.requireCycle().verify(name);
316
+ }
284
317
  async restorePlan(moment) {
285
318
  return planBackupRestore(await this.chains(), moment);
286
319
  }
@@ -2138,10 +2171,11 @@ var DatabaseBackups = class extends DatabaseLifecycle {
2138
2171
  * runs in the gap between two steps rather than waiting for the whole copy.
2139
2172
  *
2140
2173
  * @param destPath - Path the copy is written to.
2174
+ * @returns The run identifier, the timings, the pages it moved, and the bytes the file holds.
2141
2175
  */
2142
2176
  async backup(destPath) {
2143
2177
  this.ensureOpen();
2144
- await this.runtime.backups.backup(destPath);
2178
+ return this.runtime.backups.backup(destPath);
2145
2179
  }
2146
2180
  /**
2147
2181
  * Copies this database to a destination you supply, in fixed-size pieces,
@@ -2175,7 +2209,7 @@ var DatabaseBackups = class extends DatabaseLifecycle {
2175
2209
  /**
2176
2210
  * Starts repeating backups on a cron schedule, keeping a bounded number of files.
2177
2211
  *
2178
- * @param options - Cron expression, destination directory, retention, time zone, and failure callback.
2212
+ * @param options - Cron expression, destination directory, retention, and time zone, along with the callbacks Sirannon calls after each finished copy and after each failure.
2179
2213
  */
2180
2214
  scheduleBackup(options) {
2181
2215
  this.ensureOpen();
@@ -2205,6 +2239,54 @@ var DatabaseBackups = class extends DatabaseLifecycle {
2205
2239
  this.ensureNotClosed();
2206
2240
  return this.runtime.backups.chains();
2207
2241
  }
2242
+ /**
2243
+ * Reads what this database's backup cycle is doing at this moment, and what
2244
+ * its recent turns produced.
2245
+ *
2246
+ * A caller that triggers a turn with {@link DatabaseBackups.captureBackupChanges}
2247
+ * without waiting on it would read this until that turn finishes. A full copy
2248
+ * of a large database reports its pages as SQLite moves them.
2249
+ *
2250
+ * @returns Whether a turn is under way, how far it has got, and the last run, skip, and failure.
2251
+ */
2252
+ backupStatus() {
2253
+ this.ensureNotClosed();
2254
+ return this.runtime.backups.status();
2255
+ }
2256
+ /**
2257
+ * Reads one of this database's backups back out of the destination and
2258
+ * compares it against the record the backup that wrote it left behind.
2259
+ *
2260
+ * A restore would fail on a damaged piece only once that restore had already
2261
+ * begun, so name the backup here beforehand. Sirannon then fetches every
2262
+ * piece in order and folds a SHA-256 over the bytes as they arrive, and it
2263
+ * compares that digest and the byte count against the record. Only one piece
2264
+ * is in memory at any moment, so a check over a large full copy needs no
2265
+ * local storage of its own.
2266
+ *
2267
+ * A missing piece, a byte count that differs from the recorded one, and a
2268
+ * digest that differs from the recorded one will each fail with
2269
+ * `BACKUP_DESTINATION_ERROR`.
2270
+ *
2271
+ * @param name - Name the backup is stored under, which every entry {@link DatabaseBackups.backupChain} returns states.
2272
+ * @returns The pieces read, the bytes they add up to, and the digest where the backup recorded one.
2273
+ */
2274
+ async verifyBackup(name) {
2275
+ this.ensureNotClosed();
2276
+ return this.runtime.backups.verify(name);
2277
+ }
2278
+ /**
2279
+ * Reads where this database's backups are stored.
2280
+ *
2281
+ * `restoreBackup` accepts what you get back, and it then rebuilds this
2282
+ * database from its own backups at whatever path you name.
2283
+ *
2284
+ * @returns The destination, the name its chains are listed under, and the directory Sirannon stages captures in.
2285
+ */
2286
+ backupLocation() {
2287
+ this.ensureNotClosed();
2288
+ return this.runtime.backups.location();
2289
+ }
2208
2290
  /**
2209
2291
  * Works out what a restore to a given moment has to read.
2210
2292
  *
@@ -3858,17 +3940,85 @@ function migrationsFromFiles(files, options) {
3858
3940
  return applyBaselineOption(migrations, options?.baseline);
3859
3941
  }
3860
3942
 
3943
+ // src/core/migrations/registry-set.ts
3944
+ var RegistryMigrationSet = class {
3945
+ constructor(source) {
3946
+ this.source = source;
3947
+ }
3948
+ loading = null;
3949
+ /**
3950
+ * Reads the set, loading it where a function supplies it.
3951
+ *
3952
+ * @returns The migrations, which is an empty list where the registry has none.
3953
+ * @throws When the function returns anything but a list.
3954
+ */
3955
+ load() {
3956
+ const source = this.source;
3957
+ if (source === void 0 || Array.isArray(source)) {
3958
+ return Promise.resolve(source ?? []);
3959
+ }
3960
+ if (this.loading) return this.loading;
3961
+ const loading = (async () => {
3962
+ const set = await source();
3963
+ if (!Array.isArray(set)) {
3964
+ throw new MigrationError(
3965
+ "The migrations source must return an array of migrations",
3966
+ 0,
3967
+ "MIGRATION_SOURCE_INVALID"
3968
+ );
3969
+ }
3970
+ return set;
3971
+ })();
3972
+ loading.catch(() => {
3973
+ if (this.loading === loading) this.loading = null;
3974
+ });
3975
+ this.loading = loading;
3976
+ return loading;
3977
+ }
3978
+ /**
3979
+ * Applies the set to one database. A database that refuses writes takes none.
3980
+ *
3981
+ * @param db - The database to migrate.
3982
+ */
3983
+ async applyTo(db) {
3984
+ if (this.source === void 0 || db.readOnly) return;
3985
+ const migrations = await this.load();
3986
+ if (migrations.length === 0) return;
3987
+ await db.migrate(migrations);
3988
+ }
3989
+ };
3990
+
3991
+ // src/core/sirannon-offline.ts
3992
+ async function takeDatabaseOffline(offline) {
3993
+ await offline.database.close();
3994
+ let outcome;
3995
+ try {
3996
+ outcome = { ok: true, value: await offline.action(offline.path) };
3997
+ } catch (err) {
3998
+ outcome = { ok: false, failure: err };
3999
+ }
4000
+ try {
4001
+ await offline.reopen();
4002
+ } catch (err) {
4003
+ outcome.reopenFailure = err;
4004
+ }
4005
+ return outcome;
4006
+ }
4007
+
3861
4008
  // src/core/sirannon.ts
3862
4009
  var Sirannon = class {
3863
4010
  dbs = /* @__PURE__ */ new Map();
4011
+ openedWith = /* @__PURE__ */ new Map();
4012
+ offline = /* @__PURE__ */ new Set();
4013
+ offlineWork = /* @__PURE__ */ new Set();
3864
4014
  opening = /* @__PURE__ */ new Set();
3865
4015
  resolving = /* @__PURE__ */ new Map();
3866
- migrationSet = null;
3867
4016
  _shutdown = false;
3868
4017
  _driver;
3869
4018
  hookRegistry;
3870
4019
  metricsCollector;
3871
4020
  lifecycleManager;
4021
+ migrations;
3872
4022
  /** The driver, hooks, metrics, lifecycle, migrations, and writer-worker default this registry was built with. */
3873
4023
  options;
3874
4024
  /**
@@ -3880,6 +4030,7 @@ var Sirannon = class {
3880
4030
  this.options = options;
3881
4031
  this._driver = options.driver;
3882
4032
  this.hookRegistry = new HookRegistry(options.hooks);
4033
+ this.migrations = new RegistryMigrationSet(options.migrations);
3883
4034
  this.metricsCollector = options.metrics ? new MetricsCollector(options.metrics) : null;
3884
4035
  this.lifecycleManager = options.lifecycle ? new LifecycleManager(options.lifecycle, {
3885
4036
  open: (id, path, opts) => this.open(id, path, opts),
@@ -3902,17 +4053,18 @@ var Sirannon = class {
3902
4053
  * @throws When the identifier is already registered.
3903
4054
  */
3904
4055
  async open(id, path, options) {
3905
- this.ensureRunning();
4056
+ this.ensureOpenAllowed(id);
3906
4057
  if (this.dbs.has(id) || this.opening.has(id)) {
3907
4058
  throw new DatabaseAlreadyExistsError(id);
3908
4059
  }
3909
4060
  this.opening.add(id);
4061
+ const resolvedOptions = this.withRegistryDefaults(options);
3910
4062
  let db;
3911
4063
  try {
3912
4064
  if (this.hookRegistry.has("beforeConnect")) {
3913
4065
  this.hookRegistry.invokeSync("beforeConnect", { databaseId: id, path });
3914
4066
  }
3915
- db = await Database.create(id, path, this._driver, this.withRegistryDefaults(options), {
4067
+ db = await Database.create(id, path, this._driver, resolvedOptions, {
3916
4068
  parentHooks: this.hookRegistry,
3917
4069
  metrics: this.metricsCollector ?? void 0
3918
4070
  });
@@ -3925,7 +4077,7 @@ var Sirannon = class {
3925
4077
  );
3926
4078
  }
3927
4079
  try {
3928
- await this.applyRegistryMigrations(db);
4080
+ await this.migrations.applyTo(db);
3929
4081
  } catch (err) {
3930
4082
  await db.close().catch(() => {
3931
4083
  });
@@ -3937,13 +4089,14 @@ var Sirannon = class {
3937
4089
  } finally {
3938
4090
  this.opening.delete(id);
3939
4091
  }
3940
- if (this._shutdown) {
4092
+ if (this._shutdown && !this.offline.has(id)) {
3941
4093
  await db.close().catch(() => {
3942
4094
  });
3943
4095
  throw new SirannonError("Sirannon has been shut down", "SHUTDOWN");
3944
4096
  }
3945
4097
  db.addCloseListener(() => {
3946
4098
  this.dbs.delete(id);
4099
+ this.openedWith.delete(id);
3947
4100
  this.lifecycleManager?.untrack(id);
3948
4101
  if (this.hookRegistry.has("databaseClose")) {
3949
4102
  try {
@@ -3959,6 +4112,7 @@ var Sirannon = class {
3959
4112
  });
3960
4113
  });
3961
4114
  this.dbs.set(id, db);
4115
+ this.openedWith.set(id, resolvedOptions === void 0 ? { path } : { path, options: resolvedOptions });
3962
4116
  this.lifecycleManager?.markActive(id);
3963
4117
  if (this.hookRegistry.has("databaseOpen")) {
3964
4118
  try {
@@ -3992,6 +4146,52 @@ var Sirannon = class {
3992
4146
  }
3993
4147
  await db.close();
3994
4148
  }
4149
+ /**
4150
+ * Closes one database, calls an action against the file behind it, and then
4151
+ * opens that database again under the same identifier with the settings it
4152
+ * had before.
4153
+ *
4154
+ * A restore rebuilds a database at the path it already occupies, so no
4155
+ * connection may be open on that file while Sirannon replaces its bytes. The
4156
+ * identifier answers nothing until that database is open again, though an
4157
+ * action that failed still leaves a database open at the end.
4158
+ *
4159
+ * @param id - Identifier of the database to take offline.
4160
+ * @param action - Runs with the database closed, and receives its file path.
4161
+ * @returns Whether the action returned, what it produced or threw, and what a failed reopen threw.
4162
+ * @throws When no database is open under the identifier, when it refuses writes, or when the close fails.
4163
+ *
4164
+ * @internal
4165
+ */
4166
+ async withDatabaseOffline(id, action) {
4167
+ this.ensureRunning();
4168
+ const db = this.dbs.get(id);
4169
+ const opened = this.openedWith.get(id);
4170
+ if (!db || !opened) throw new DatabaseNotFoundError(id);
4171
+ if (db.readOnly) throw new ReadOnlyError(id);
4172
+ this.dbs.delete(id);
4173
+ this.opening.add(id);
4174
+ this.offline.add(id);
4175
+ const work = takeDatabaseOffline({
4176
+ database: db,
4177
+ path: opened.path,
4178
+ action,
4179
+ reopen: async () => {
4180
+ this.opening.delete(id);
4181
+ try {
4182
+ return await this.open(id, opened.path, opened.options);
4183
+ } finally {
4184
+ this.offline.delete(id);
4185
+ }
4186
+ }
4187
+ }).finally(() => {
4188
+ this.opening.delete(id);
4189
+ this.offline.delete(id);
4190
+ this.offlineWork.delete(work);
4191
+ });
4192
+ this.offlineWork.add(work);
4193
+ return work;
4194
+ }
3995
4195
  /**
3996
4196
  * Returns an already-open database.
3997
4197
  *
@@ -4011,7 +4211,7 @@ var Sirannon = class {
4011
4211
  async resolve(id) {
4012
4212
  const db = this.get(id);
4013
4213
  if (db) return db;
4014
- if (this._shutdown) return void 0;
4214
+ if (this._shutdown || this.offline.has(id)) return void 0;
4015
4215
  const manager = this.lifecycleManager;
4016
4216
  if (!manager) return void 0;
4017
4217
  const pending = this.resolving.get(id);
@@ -4024,38 +4224,7 @@ var Sirannon = class {
4024
4224
  }
4025
4225
  /** @internal */
4026
4226
  registryMigrations() {
4027
- return this.loadMigrationSet();
4028
- }
4029
- async applyRegistryMigrations(db) {
4030
- if (this.options.migrations === void 0 || db.readOnly) return;
4031
- const migrations = await this.loadMigrationSet();
4032
- if (migrations.length === 0) return;
4033
- await db.migrate(migrations);
4034
- }
4035
- loadMigrationSet() {
4036
- const source = this.options.migrations;
4037
- if (source === void 0 || Array.isArray(source)) {
4038
- return Promise.resolve(source ?? []);
4039
- }
4040
- if (this.migrationSet) return this.migrationSet;
4041
- const loading = (async () => {
4042
- const set = await source();
4043
- if (!Array.isArray(set)) {
4044
- throw new MigrationError(
4045
- "The migrations source must return an array of migrations",
4046
- 0,
4047
- "MIGRATION_SOURCE_INVALID"
4048
- );
4049
- }
4050
- return set;
4051
- })();
4052
- loading.catch(() => {
4053
- if (this.migrationSet === loading) {
4054
- this.migrationSet = null;
4055
- }
4056
- });
4057
- this.migrationSet = loading;
4058
- return loading;
4227
+ return this.migrations.load();
4059
4228
  }
4060
4229
  /**
4061
4230
  * Reports whether a database is open under an identifier.
@@ -4076,10 +4245,17 @@ var Sirannon = class {
4076
4245
  }
4077
4246
  /**
4078
4247
  * Closes every open database and stops the lifecycle timers.
4248
+ *
4249
+ * A database whose file Sirannon is replacing delays the shutdown until that
4250
+ * work finishes, since a process that exits part-way through would leave the
4251
+ * path with no database on it at all.
4079
4252
  */
4080
4253
  async shutdown() {
4081
4254
  if (this._shutdown) return;
4082
4255
  this._shutdown = true;
4256
+ while (this.offlineWork.size > 0) {
4257
+ await Promise.allSettled([...this.offlineWork]);
4258
+ }
4083
4259
  this.lifecycleManager?.dispose();
4084
4260
  const errors = [];
4085
4261
  const snapshot = [...this.dbs.values()];
@@ -4140,6 +4316,10 @@ var Sirannon = class {
4140
4316
  throw new SirannonError("Sirannon has been shut down", "SHUTDOWN");
4141
4317
  }
4142
4318
  }
4319
+ ensureOpenAllowed(id) {
4320
+ if (this.offline.has(id)) return;
4321
+ this.ensureRunning();
4322
+ }
4143
4323
  };
4144
4324
 
4145
4325
  // src/core/operation-registry.ts
@@ -1,9 +1,9 @@
1
- import { B as BackupCapabilities, a as BackupRestorePlan, b as BackupSafeToDeleteOptions } from './chain-queries-D5aZ-8v2.js';
2
- import { a as SQLiteDriver, F as SynchronousLevel, aa as WorkerHostOptions, i as SQLiteConnection, X as DriverCapabilities, ab as BackupEngine, v as BackupToDestinationOptions, k as BackupRunReport, E as BackupScheduleOptions, n as BackupCycleOptions, g as BackupChain, h as BackupChainRecord, T as Transaction, ac as WriterContext, M as Migration, G as MigrationResult, R as RollbackResult, a5 as QueryHookContext, Q as ConnectionHookContext, K as BeforeSubscribeHook, Z as HookConfig, $ as MetricsConfig, a6 as QueryMetrics, U as ConnectionMetrics, O as CDCMetrics, D as DatabaseOptions, B as BeforeQueryHook, A as AfterQueryHook } from './types-Xb0L3o3w.js';
3
- import { C as ChangeTracker } from './change-tracker-DXHPFe9J.js';
1
+ import { D as DatabaseBackupController, B as BackupCapabilities, a as BackupChainLocation, b as BackupRestorePlan, c as BackupSafeToDeleteOptions } from './database-backup-C090lMHc.js';
2
+ import { S as SQLiteDriver, U as SynchronousLevel, ak as WorkerHostOptions, I as SQLiteConnection, T as Transaction, al as WriterContext, M as Migration, W as MigrationResult, X as RollbackResult, af as QueryHookContext, a2 as ConnectionHookContext, $ as BeforeSubscribeHook, a7 as HookConfig, a9 as MetricsConfig, ag as QueryMetrics, a3 as ConnectionMetrics, a0 as CDCMetrics, D as DatabaseOptions, p as BackupFileReport, y as BackupToDestinationOptions, f as BackupRunReport, O as BackupScheduleOptions, g as BackupChain, o as BackupCycleStatus, z as BackupVerifyResult, k as BackupChainRecord, b as BeforeQueryHook, A as AfterQueryHook } from './types-DApb7dvw.js';
3
+ import { C as ChangeTracker } from './change-tracker-uG9gWvsY.js';
4
4
  import { C as ChangeEvent, S as Subscription, c as SubscriptionBuilder, P as Params, Q as QueryOptions, E as ExecuteResult } from './query-types-Cv8N7pXj.js';
5
5
  import { H as HLCTimestamp, c as ReplicationChange, C as ConflictResolver, A as ApplyResult, R as ReplicationBatch } from './types-CjhxcjhA.js';
6
- import { h as AppliedMigrationRow, b as BulkLoadOptions, a as BulkLoadResult } from './server-options-D4VmfTHw.js';
6
+ import { h as AppliedMigrationRow, b as BulkLoadOptions, a as BulkLoadResult } from './server-options-DeK6ZG8u.js';
7
7
  import { L as LiveQueryOptions, a as LiveQuery } from './types-BCejqzNA.js';
8
8
 
9
9
  /**
@@ -43,39 +43,6 @@ declare class ConnectionPool {
43
43
  close(): Promise<void>;
44
44
  }
45
45
 
46
- type RunExclusive$2 = (op: () => Promise<void>) => Promise<void>;
47
- declare class DatabaseBackupController {
48
- private readonly runExclusive;
49
- private readonly acquireWriter;
50
- private readonly driverCapabilities;
51
- private readonly databaseId;
52
- private readonly sourcePath;
53
- private readonly engine?;
54
- private readonly cancellers;
55
- private cycle;
56
- private cycleStarted;
57
- constructor(runExclusive: RunExclusive$2, acquireWriter: () => SQLiteConnection, driverCapabilities: DriverCapabilities, databaseId: string, sourcePath: string, engine?: BackupEngine | undefined);
58
- private require;
59
- private requireCycle;
60
- capabilities(): BackupCapabilities;
61
- backup(destPath: string): Promise<void>;
62
- backupTo(options: BackupToDestinationOptions): Promise<BackupRunReport>;
63
- schedule(options: BackupScheduleOptions): void;
64
- /**
65
- * Builds the cycle and starts it in the background. Its first turn copies the
66
- * whole database, and an open cannot wait on that. A failure reaches the
67
- * caller through the cycle's own error callback.
68
- */
69
- startCycle(options: BackupCycleOptions): void;
70
- captureChanges(): Promise<BackupRunReport | undefined>;
71
- chains(): Promise<BackupChain[]>;
72
- restorePlan(moment: number): Promise<BackupRestorePlan>;
73
- piecesSafeToDelete(options?: BackupSafeToDeleteOptions): Promise<BackupChainRecord[]>;
74
- /** Stops the cycle, capturing the log one final time so nothing written since the previous turn is lost. */
75
- stopCycle(): Promise<void>;
76
- cancelAll(): void;
77
- }
78
-
79
46
  declare class SubscriptionManager {
80
47
  private nextId;
81
48
  private readonly subscriptions;
@@ -645,8 +612,9 @@ declare class DatabaseBackups extends DatabaseLifecycle {
645
612
  * runs in the gap between two steps rather than waiting for the whole copy.
646
613
  *
647
614
  * @param destPath - Path the copy is written to.
615
+ * @returns The run identifier, the timings, the pages it moved, and the bytes the file holds.
648
616
  */
649
- backup(destPath: string): Promise<void>;
617
+ backup(destPath: string): Promise<BackupFileReport>;
650
618
  /**
651
619
  * Copies this database to a destination you supply, in fixed-size pieces,
652
620
  * while it stays open for reads and writes.
@@ -674,7 +642,7 @@ declare class DatabaseBackups extends DatabaseLifecycle {
674
642
  /**
675
643
  * Starts repeating backups on a cron schedule, keeping a bounded number of files.
676
644
  *
677
- * @param options - Cron expression, destination directory, retention, time zone, and failure callback.
645
+ * @param options - Cron expression, destination directory, retention, and time zone, along with the callbacks Sirannon calls after each finished copy and after each failure.
678
646
  */
679
647
  scheduleBackup(options: BackupScheduleOptions): void;
680
648
  /**
@@ -695,6 +663,45 @@ declare class DatabaseBackups extends DatabaseLifecycle {
695
663
  * @returns One entry per chain, newest first, each with its full copy and its change pieces in order.
696
664
  */
697
665
  backupChain(): Promise<BackupChain[]>;
666
+ /**
667
+ * Reads what this database's backup cycle is doing at this moment, and what
668
+ * its recent turns produced.
669
+ *
670
+ * A caller that triggers a turn with {@link DatabaseBackups.captureBackupChanges}
671
+ * without waiting on it would read this until that turn finishes. A full copy
672
+ * of a large database reports its pages as SQLite moves them.
673
+ *
674
+ * @returns Whether a turn is under way, how far it has got, and the last run, skip, and failure.
675
+ */
676
+ backupStatus(): BackupCycleStatus;
677
+ /**
678
+ * Reads one of this database's backups back out of the destination and
679
+ * compares it against the record the backup that wrote it left behind.
680
+ *
681
+ * A restore would fail on a damaged piece only once that restore had already
682
+ * begun, so name the backup here beforehand. Sirannon then fetches every
683
+ * piece in order and folds a SHA-256 over the bytes as they arrive, and it
684
+ * compares that digest and the byte count against the record. Only one piece
685
+ * is in memory at any moment, so a check over a large full copy needs no
686
+ * local storage of its own.
687
+ *
688
+ * A missing piece, a byte count that differs from the recorded one, and a
689
+ * digest that differs from the recorded one will each fail with
690
+ * `BACKUP_DESTINATION_ERROR`.
691
+ *
692
+ * @param name - Name the backup is stored under, which every entry {@link DatabaseBackups.backupChain} returns states.
693
+ * @returns The pieces read, the bytes they add up to, and the digest where the backup recorded one.
694
+ */
695
+ verifyBackup(name: string): Promise<BackupVerifyResult>;
696
+ /**
697
+ * Reads where this database's backups are stored.
698
+ *
699
+ * `restoreBackup` accepts what you get back, and it then rebuilds this
700
+ * database from its own backups at whatever path you name.
701
+ *
702
+ * @returns The destination, the name its chains are listed under, and the directory Sirannon stages captures in.
703
+ */
704
+ backupLocation(): BackupChainLocation;
698
705
  /**
699
706
  * Works out what a restore to a given moment has to read.
700
707
  *
@@ -1,4 +1,4 @@
1
- import { e as BackupChainBase, f as BackupChainChange, g as BackupChain, h as BackupChainRecord } from './types-Xb0L3o3w.js';
1
+ import { h as BackupChainBase, i as BackupChainChange, g as BackupChain, k as BackupChainRecord, B as BackupDestination, I as SQLiteConnection, P as DriverCapabilities, Q as BackupEngine, p as BackupFileReport, y as BackupToDestinationOptions, f as BackupRunReport, O as BackupScheduleOptions, n as BackupCycleOptions, o as BackupCycleStatus, z as BackupVerifyResult } from './types-DApb7dvw.js';
2
2
 
3
3
  /**
4
4
  * What one runtime supports of the backup operations, so a caller learns
@@ -88,4 +88,54 @@ declare function planBackupRestore(chains: readonly BackupChain[], moment: numbe
88
88
  */
89
89
  declare function backupPiecesSafeToDelete(chains: readonly BackupChain[], options?: BackupSafeToDeleteOptions): BackupChainRecord[];
90
90
 
91
- export { type BackupCapabilities as B, type BackupRestorePlan as a, type BackupSafeToDeleteOptions as b, backupPiecesSafeToDelete as c, planBackupRestore as p };
91
+ type RunExclusive = (op: () => Promise<void>) => Promise<void>;
92
+ /** Where one database's backups are stored, and what a restore of that database reads.
93
+ * @public
94
+ */
95
+ interface BackupChainLocation {
96
+ /** The destination its pieces and its chain records go to. */
97
+ destination: BackupDestination;
98
+ /** Name that destination lists its chains under. */
99
+ chainName: string;
100
+ /** Directory the cycle stages each capture in before that capture goes out. */
101
+ stagingDir: string;
102
+ /** Deadline the operator set on every call to that destination, where they set one. */
103
+ destinationTimeoutMs?: number;
104
+ }
105
+ declare class DatabaseBackupController {
106
+ private readonly runExclusive;
107
+ private readonly acquireWriter;
108
+ private readonly driverCapabilities;
109
+ private readonly databaseId;
110
+ private readonly sourcePath;
111
+ private readonly engine?;
112
+ private readonly cancellers;
113
+ private cycle;
114
+ private cycleOptions;
115
+ private cycleStarted;
116
+ constructor(runExclusive: RunExclusive, acquireWriter: () => SQLiteConnection, driverCapabilities: DriverCapabilities, databaseId: string, sourcePath: string, engine?: BackupEngine | undefined);
117
+ private require;
118
+ private requireCycle;
119
+ capabilities(): BackupCapabilities;
120
+ backup(destPath: string): Promise<BackupFileReport>;
121
+ backupTo(options: BackupToDestinationOptions): Promise<BackupRunReport>;
122
+ schedule(options: BackupScheduleOptions): void;
123
+ /**
124
+ * Builds the cycle and starts it in the background. Its first turn copies the
125
+ * whole database, and an open cannot wait on that. A failure reaches the
126
+ * caller through the cycle's own error callback.
127
+ */
128
+ startCycle(options: BackupCycleOptions): void;
129
+ captureChanges(): Promise<BackupRunReport | undefined>;
130
+ chains(): Promise<BackupChain[]>;
131
+ status(): BackupCycleStatus;
132
+ location(): BackupChainLocation;
133
+ verify(name: string): Promise<BackupVerifyResult>;
134
+ restorePlan(moment: number): Promise<BackupRestorePlan>;
135
+ piecesSafeToDelete(options?: BackupSafeToDeleteOptions): Promise<BackupChainRecord[]>;
136
+ /** Stops the cycle, capturing the log one final time so nothing written since the previous turn is lost. */
137
+ stopCycle(): Promise<void>;
138
+ cancelAll(): void;
139
+ }
140
+
141
+ export { type BackupCapabilities as B, DatabaseBackupController as D, type BackupChainLocation as a, type BackupRestorePlan as b, type BackupSafeToDeleteOptions as c, backupPiecesSafeToDelete as d, planBackupRestore as p };
@@ -1,4 +1,4 @@
1
- import { a as SQLiteDriver } from '../types-Xb0L3o3w.js';
1
+ import { S as SQLiteDriver } from '../types-DApb7dvw.js';
2
2
  import '../query-types-Cv8N7pXj.js';
3
3
 
4
4
  /**
@@ -1,12 +1,15 @@
1
- import { nodeResolveExtensionPath, nodeWriterContext, nodeBackupEngine, nodeStreamingSupport, WriterWorker, createStatementCache, loadThroughRuntime, narrowSafeBigInt, narrowRowIntegers, narrowRowsIntegers } from '../chunk-ZKDBGX7H.mjs';
2
- import '../chunk-72I5UI5C.mjs';
3
- import '../chunk-BQTVNI5H.mjs';
4
- import '../chunk-GBM4AU24.mjs';
5
- import { defineDriver } from '../chunk-BQFQ65OL.mjs';
6
- import '../chunk-YVF2ZJTE.mjs';
1
+ import { nodeResolveExtensionPath, nodeWriterContext, nodeBackupEngine, nodeStreamingSupport, WriterWorker, createStatementCache, loadThroughRuntime, narrowSafeBigInt, narrowRowIntegers, narrowRowsIntegers } from '../chunk-OJQHM4MT.mjs';
2
+ import '../chunk-7SC2TZAS.mjs';
3
+ import '../chunk-DXXCLTFF.mjs';
4
+ import { defineDriver } from '../chunk-LXCGPZPA.mjs';
7
5
  import '../chunk-W67A4H2E.mjs';
8
6
  import '../chunk-YLAVDZCS.mjs';
7
+ import '../chunk-W77EEVOT.mjs';
8
+ import '../chunk-U445A3C4.mjs';
9
+ import '../chunk-2QCPVE67.mjs';
10
+ import '../chunk-DVS4QSIZ.mjs';
9
11
  import { synchronousPragmaValue } from '../chunk-OUSWVNWT.mjs';
12
+ import '../chunk-YVF2ZJTE.mjs';
10
13
  import '../chunk-V5FUJEYY.mjs';
11
14
  import { BackupError } from '../chunk-PBRXXISQ.mjs';
12
15