@delali/sirannon-db 0.3.5 → 0.3.6-next.78

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 (72) hide show
  1. package/dist/backup/index.d.ts +5 -5
  2. package/dist/backup/index.mjs +5 -3
  3. package/dist/backup-scheduler/index.d.ts +2 -2
  4. package/dist/backup-scheduler/index.mjs +2 -2
  5. package/dist/chunk-22TYGH7J.mjs +55 -0
  6. package/dist/{chunk-SB3MIFLG.mjs → chunk-2ZQOZFX7.mjs} +29 -6
  7. package/dist/{chunk-UIDQGPJY.mjs → chunk-4O7WYZHN.mjs} +38 -3
  8. package/dist/{chunk-ZFFUVYYE.mjs → chunk-5DKWHT6K.mjs} +109 -1
  9. package/dist/chunk-5O4J6LQQ.mjs +35 -0
  10. package/dist/{chunk-CZL4D3BM.mjs → chunk-6CABU54O.mjs} +2 -2
  11. package/dist/{chunk-JAQ2I4OT.mjs → chunk-A5ULJVK7.mjs} +11 -11
  12. package/dist/{chunk-RLBKZZ24.mjs → chunk-AC7TKWNH.mjs} +1 -0
  13. package/dist/chunk-BKO4SCEC.mjs +99 -0
  14. package/dist/{chunk-KOTBP4AW.mjs → chunk-C7E4H6UN.mjs} +24 -4
  15. package/dist/{chunk-G4AG3ESO.mjs → chunk-FBA7P4ZA.mjs} +114 -149
  16. package/dist/{chunk-DKML5425.mjs → chunk-GBVB6WSC.mjs} +2 -2
  17. package/dist/chunk-I4JY5QPA.mjs +51 -0
  18. package/dist/{chunk-L6EX6K27.mjs → chunk-MO4APBYG.mjs} +54 -13
  19. package/dist/{chunk-RPWBYASV.mjs → chunk-MTOQ6QIS.mjs} +1 -1
  20. package/dist/chunk-MX77PGEN.mjs +60 -0
  21. package/dist/{chunk-BTUD6XQO.mjs → chunk-N3MHZV3B.mjs} +11 -29
  22. package/dist/{chunk-UHUDPQFI.mjs → chunk-OJXV4L3N.mjs} +1 -3
  23. package/dist/chunk-QF7WF7RJ.mjs +34 -0
  24. package/dist/chunk-RLBV4YTB.mjs +22 -0
  25. package/dist/{chunk-ISDT4RBD.mjs → chunk-ULUZUBON.mjs} +1 -1
  26. package/dist/{chunk-AP4367HK.mjs → chunk-VGRAAQX3.mjs} +4 -4
  27. package/dist/chunk-W74HFQFD.mjs +153 -0
  28. package/dist/chunk-Y4SP7CRF.mjs +9 -0
  29. package/dist/{chunk-JFVEGN7J.mjs → chunk-ZAUHAJ4G.mjs} +2 -1
  30. package/dist/{chunk-DDIHBUEN.mjs → chunk-ZWW53O5P.mjs} +19 -7
  31. package/dist/client/index.d.ts +8 -8
  32. package/dist/client/topology.d.ts +4 -4
  33. package/dist/{client-base-Doaqwrx3.d.ts → client-base-DJZbSY55.d.ts} +2 -2
  34. package/dist/{client-streams-MKPE4MXW.mjs → client-streams-F7IG3PTU.mjs} +13 -16
  35. package/dist/core/index.d.ts +48 -12
  36. package/dist/core/index.mjs +1812 -107
  37. package/dist/core/writer-worker.mjs +28 -4
  38. package/dist/{database-fz-8CJBP.d.ts → database-BZ96ZzAx.d.ts} +411 -184
  39. package/dist/{database-backup-BwTNiBm2.d.ts → database-backup-BhM8yJ1A.d.ts} +32 -2
  40. package/dist/driver/better-sqlite3.d.ts +11 -1
  41. package/dist/driver/better-sqlite3.mjs +212 -23
  42. package/dist/driver/bun.d.ts +1 -1
  43. package/dist/driver/bun.mjs +15 -22
  44. package/dist/driver/expo.d.ts +1 -1
  45. package/dist/driver/expo.mjs +185 -16
  46. package/dist/driver/node.d.ts +1 -1
  47. package/dist/driver/node.mjs +15 -62
  48. package/dist/driver/wa-sqlite.d.ts +1 -1
  49. package/dist/driver/wa-sqlite.mjs +14 -153
  50. package/dist/{change-tracker-Dpo99EI-.d.ts → errors-DLMAGQFb.d.ts} +200 -2
  51. package/dist/file-migrations/index.d.ts +1 -1
  52. package/dist/peer-refusal-RR37UOLV.mjs +1 -0
  53. package/dist/{protocol-DZSYSv0S.d.ts → protocol-DE10taas.d.ts} +1 -1
  54. package/dist/replication/coordinator/etcd.mjs +1 -1
  55. package/dist/replication/index.d.ts +7 -7
  56. package/dist/replication/index.mjs +66 -9
  57. package/dist/{restore-options-CcMukOLO.d.ts → restore-options-B6HV_5XT.d.ts} +6 -1
  58. package/dist/server/index.d.ts +10 -10
  59. package/dist/server/index.mjs +506 -350
  60. package/dist/{server-options-B9CsAZp5.d.ts → server-options-Bhdlvehe.d.ts} +14 -1
  61. package/dist/{server-streams-SBYCHWWM.mjs → server-streams-Q2AZAW3N.mjs} +6 -30
  62. package/dist/{sirannon-D0CjDULa.d.ts → sirannon-Ba1o2HUQ.d.ts} +5 -5
  63. package/dist/transport/grpc.d.ts +15 -4
  64. package/dist/transport/grpc.mjs +34 -37
  65. package/dist/transport/memory.d.ts +4 -3
  66. package/dist/transport/memory.mjs +8 -26
  67. package/dist/{types-DTmi7pXp.d.ts → types-BWyv_N18.d.ts} +189 -1
  68. package/dist/{types-BlZ9GG1a.d.ts → types-CDnlkVRK.d.ts} +8 -2
  69. package/package.json +17 -9
  70. package/dist/chunk-DBN57KVR.mjs +0 -171
  71. package/dist/chunk-W67A4H2E.mjs +0 -23
  72. package/dist/chunk-YW7IKQ7T.mjs +0 -62
@@ -1,9 +1,9 @@
1
- import { C as ChangeTracker, P as PruneBoundarySource } from './change-tracker-Dpo99EI-.js';
2
- import { D as DatabaseBackupController, B as BackupCapabilities, a as BackupChainLocation, b as BackupRestorePlan, c as BackupSafeToDeleteOptions } from './database-backup-BwTNiBm2.js';
3
- import { S as SQLiteDriver, V as SynchronousLevel, aq as WorkerHostOptions, J as SQLiteConnection, ar as ChangeDispatchObserver, as as SubscriptionManager, at as WriterTransactionRunner, T as Transaction, au as StatementMeter, av as WriterContext, M as Migration, X as MigrationResult, Y as RollbackResult, al as QueryHookContext, a5 as ConnectionHookContext, a2 as BeforeSubscribeHook, a1 as BeforeSnapshotHook, a0 as BeforePushHook, ab as HookConfig, b as MetricsCollector, aw as QueryOutcomeMeasure, ax as QueryFailureMeasure, ay as QueryOutcome, az as RowCounts, D as DatabaseOptions, q as BackupFileReport, z as BackupToDestinationOptions, g as BackupRunReport, P as BackupScheduleOptions, h as BackupChain, p as BackupCycleStatus, C as BackupVerifyResult, l as BackupChainRecord, c as BeforeQueryHook, A as AfterQueryHook } from './types-DTmi7pXp.js';
1
+ import { C as ChangeTracker, S as SirannonError, P as PruneBoundarySource } from './errors-DLMAGQFb.js';
2
+ import { J as EncryptionKey, S as SQLiteDriver, X as SynchronousLevel, aw as WorkerHostOptions, L as SQLiteConnection, ax as ChangeDispatchObserver, ay as SubscriptionManager, az as WriterTransactionRunner, T as Transaction, aA as StatementMeter, aB as WriterContext, M as Migration, Z as MigrationResult, _ as RollbackResult, ar as QueryHookContext, a7 as ConnectionHookContext, a4 as BeforeSubscribeHook, a3 as BeforeSnapshotHook, a2 as BeforePushHook, ah as HookConfig, b as MetricsCollector, aC as QueryOutcomeMeasure, aD as QueryFailureMeasure, aE as QueryOutcome, aF as RowCounts, aq as OpenOptions, ag as EncryptionStatus, ae as EncryptionChange, D as DatabaseOptions, q as BackupFileReport, z as BackupToDestinationOptions, g as BackupRunReport, R as BackupScheduleOptions, h as BackupChain, p as BackupCycleStatus, C as BackupVerifyResult, l as BackupChainRecord, c as BeforeQueryHook, A as AfterQueryHook } from './types-BWyv_N18.js';
3
+ import { D as DatabaseBackupController, B as BackupCapabilities, a as BackupChainLocation, b as BackupRestorePlan, c as BackupSafeToDeleteOptions } from './database-backup-BhM8yJ1A.js';
4
4
  import { H as HLCTimestamp, c as ReplicationChange, C as ConflictResolver, A as ApplyResult, R as ReplicationBatch } from './types-BG31TtrQ.js';
5
5
  import { c as SubscriptionBuilder, P as Params, Q as QueryOptions, E as ExecuteResult, C as ChangeEvent } from './query-types-5B02ixH0.js';
6
- import { A as AppliedMigrationRow, c as BulkLoadOptions, a as BulkLoadResult } from './server-options-B9CsAZp5.js';
6
+ import { A as AppliedMigrationRow, c as BulkLoadOptions, a as BulkLoadResult } from './server-options-Bhdlvehe.js';
7
7
  import { L as LiveQueryOptions, a as LiveQuery } from './types-CEJG0eFS.js';
8
8
 
9
9
  interface DeviceRetentionPolicy {
@@ -19,201 +19,69 @@ interface ChangeRetentionOptions extends DeviceRetentionOptions {
19
19
  }
20
20
 
21
21
  /**
22
- * The base class for every sirannon-db error, which has a machine-readable
23
- * {@link SirannonError.code}. Extend it to define an error of your own.
22
+ * A function that returns a database key, such as {@link EncryptionOptions.key}.
24
23
  *
25
- * @public
26
- */
27
- declare class SirannonError extends Error {
28
- /**
29
- * The machine-readable code that the server maps to an HTTP status.
30
- */
31
- readonly code: string;
32
- constructor(message: string,
33
- /**
34
- * The machine-readable code that the server maps to an HTTP status.
35
- */
36
- code: string);
37
- }
38
- /**
39
- * Thrown when the registry has no open database under an identifier, because
40
- * the database was never opened or is already closed.
41
- *
42
- * @public
43
- */
44
- declare class DatabaseNotFoundError extends SirannonError {
45
- constructor(id: string);
46
- }
47
- /**
48
- * Thrown when a caller opens a database under an identifier that the registry
49
- * already uses, since each identifier names one database.
50
- *
51
- * @public
52
- */
53
- declare class DatabaseAlreadyExistsError extends SirannonError {
54
- constructor(id: string);
55
- }
56
- /**
57
- * Thrown when a caller writes to a database that is open in read-only mode.
58
- *
59
- * @public
60
- */
61
- declare class ReadOnlyError extends SirannonError {
62
- constructor(id: string);
63
- }
64
- /**
65
- * Thrown when SQLite fails to execute a statement, with that statement in
66
- * {@link QueryError.sql} for your logs.
67
- *
68
- * @public
69
- */
70
- declare class QueryError extends SirannonError {
71
- /**
72
- * The statement that failed.
73
- */
74
- readonly sql: string;
75
- constructor(message: string,
76
- /**
77
- * The statement that failed.
78
- */
79
- sql: string);
80
- }
81
- /**
82
- * Sirannon raises this error when SQLite refuses to commit a transaction whose
83
- * statements all succeeded, such as on a deferred foreign key; `cause` holds
84
- * SQLite's own error. The server responds to its `TRANSACTION_ERROR` code with status 400.
85
- *
86
- * @public
87
- */
88
- declare class TransactionError extends SirannonError {
89
- constructor(message: string, cause?: unknown);
90
- }
91
- /**
92
- * Thrown when a migration fails, with the version of that migration in
93
- * {@link MigrationError.version}.
94
- *
95
- * @public
96
- */
97
- declare class MigrationError extends SirannonError {
98
- /**
99
- * The version of the migration that failed.
100
- */
101
- readonly version: number;
102
- constructor(message: string,
103
- /**
104
- * The version of the migration that failed.
105
- */
106
- version: number, code?: string);
107
- }
108
- /**
109
- * An error that a before-hook can throw to reject an operation, which the
110
- * server responds to with status 403. The message includes the optional
111
- * `reason`, so that a caller can tell one hook policy from another.
112
- *
113
- * @public
114
- */
115
- declare class HookDeniedError extends SirannonError {
116
- constructor(hookName: string, reason?: string);
117
- }
118
- /**
119
- * Rejects one request with a status of your own; throw it from an authenticate hook or a registered operation.
120
- *
121
- * @public
122
- */
123
- declare class RequestDeniedError extends SirannonError {
124
- /**
125
- * The HTTP status that the server responds to the rejected request with.
126
- */
127
- readonly status: number;
128
- constructor(status: number, code: string, message: string);
129
- }
130
- /**
131
- * Thrown when change capture cannot proceed, for example when a caller watches
132
- * a table that does not exist, when Sirannon cannot read the change-log epoch
133
- * or the node identity, or when a live query uses a statement that Sirannon
134
- * cannot keep current.
135
- *
136
- * @public
137
- */
138
- declare class CDCError extends SirannonError {
139
- constructor(message: string);
140
- }
141
- /**
142
- * Thrown when a statement through the query API names a `_sirannon` table,
143
- * modifies a `sqlite_` table, contains ATTACH or DETACH, or sets
144
- * `PRAGMA writable_schema`. Sirannon rejects these statements to keep its
145
- * change log, its replication ledger, and SQLite's schema catalogue out of the
146
- * caller's reach.
147
- *
148
- * @public
149
- */
150
- declare class ForbiddenSqlError extends SirannonError {
151
- constructor(message: string);
152
- }
153
- /**
154
- * Thrown when a copy to a file or a scheduled backup fails, or when its path,
155
- * cron expression, or time zone is invalid.
156
- *
157
- * @public
24
+ * @internal
158
25
  */
159
- declare class BackupError extends SirannonError {
160
- constructor(message: string);
161
- }
26
+ type EncryptionKeySource = () => EncryptionKey | Promise<EncryptionKey>;
162
27
  /**
163
- * Thrown when a caller asks a closed or empty connection pool for a connection, asks a
164
- * read-only pool for the writer, or closes a pool and one of its connections
165
- * fails to close.
28
+ * A key that Sirannon copied out of a key function's result, which the caller wipes once the connections that need it
29
+ * are open.
166
30
  *
167
- * @public
31
+ * @internal
168
32
  */
169
- declare class ConnectionPoolError extends SirannonError {
170
- constructor(message: string);
33
+ interface ResolvedEncryptionKey {
34
+ /** A copy of the 32 key bytes that no other code holds. */
35
+ bytes: Uint8Array;
36
+ /** The identifier that came with the key. */
37
+ id?: string;
171
38
  }
39
+
172
40
  /**
173
- * Thrown when the lifecycle resolver would open a database past `maxOpen`, and
174
- * evicting the least recently used database frees no slot. Close a database
175
- * before you open another one.
41
+ * The settings that an open passes to the driver for an encrypted database.
176
42
  *
177
- * @public
43
+ * @internal
178
44
  */
179
- declare class MaxDatabasesError extends SirannonError {
180
- constructor(max: number);
45
+ interface KeyedOpenOptions {
46
+ encryption?: {
47
+ key: Uint8Array;
48
+ };
181
49
  }
182
50
  /**
183
- * Thrown when more writes are pending than the writer-worker limit allows, so
184
- * that the database sheds load. The server responds to it with status 503 and
185
- * a `Retry-After` header.
51
+ * Holds the key function of one open database, and calls it each time the database opens connections, so that no key
52
+ * bytes stay in memory between opens.
186
53
  *
187
- * @public
54
+ * @internal
188
55
  */
189
- declare class WriteOverloadError extends SirannonError {
190
- /**
191
- * The number of pending writes that the database accepts before it rejects more.
192
- */
193
- readonly limit: number;
56
+ declare class EncryptionKeyring {
57
+ private source;
58
+ private lastKeyId;
59
+ constructor(source: EncryptionKeySource | null);
60
+ /** `true` when the database opens under a key. */
61
+ get encrypted(): boolean;
62
+ /** The identifier that came with the key that the last open used, or `undefined` when it came with none. */
63
+ get keyId(): string | undefined;
64
+ /** The key function that the database currently opens under, or `null` for a plaintext database. */
65
+ get keySource(): EncryptionKeySource | null;
194
66
  /**
195
- * The number of milliseconds that the caller should wait before it retries.
67
+ * Makes the database open every later connection under a new key function, or as plaintext.
68
+ *
69
+ * @param source - The key function of the key that the file now holds, or `null` for a plaintext file.
196
70
  */
197
- readonly retryAfterMs: number;
198
- constructor(
71
+ replace(source: EncryptionKeySource | null): void;
199
72
  /**
200
- * The number of pending writes that the database accepts before it rejects more.
73
+ * Calls the key function once, passes the key to the opens, and wipes Sirannon's copy of the key once they settle.
74
+ *
75
+ * @param open - Opens the connections with the settings that carry the key.
76
+ * @returns What `open` returns.
201
77
  */
202
- limit: number,
78
+ openWith<T>(open: (options: KeyedOpenOptions, key: ResolvedEncryptionKey | undefined) => Promise<T>): Promise<T>;
203
79
  /**
204
- * The number of milliseconds that the caller should wait before it retries.
80
+ * Calls the key function and returns a copy of the key bytes, which the caller hands to a writer worker and wipes.
81
+ *
82
+ * @returns The key bytes.
205
83
  */
206
- retryAfterMs: number);
207
- }
208
- /**
209
- * Thrown when Sirannon cannot load a compiled SQLite extension. The message
210
- * names the extension path, and the optional `cause` gives the reason that
211
- * SQLite or Sirannon reported.
212
- *
213
- * @public
214
- */
215
- declare class ExtensionError extends SirannonError {
216
- constructor(path: string, cause?: string);
84
+ freshKeyBytes(): Promise<Uint8Array>;
217
85
  }
218
86
 
219
87
  /**
@@ -231,6 +99,8 @@ interface ConnectionPoolOptions {
231
99
  walAutoCheckpoint?: number;
232
100
  useWriterWorker?: boolean;
233
101
  workerHostOptions?: WorkerHostOptions;
102
+ keyring?: EncryptionKeyring;
103
+ prepareReopened?: (connections: readonly SQLiteConnection[]) => Promise<void>;
234
104
  }
235
105
  /**
236
106
  * Opens one writer connection and a set of reader connections for a single database file, and returns the readers in rotation.
@@ -238,18 +108,62 @@ interface ConnectionPoolOptions {
238
108
  * @internal
239
109
  */
240
110
  declare class ConnectionPool {
241
- private readonly writer;
242
- private readonly readers;
111
+ private readonly options;
112
+ private writer;
113
+ private readers;
243
114
  private readerIndex;
244
115
  private closed;
116
+ private isSuspended;
117
+ private readsInFlight;
118
+ private readsSettled;
119
+ private reopenFailure;
245
120
  private constructor();
246
121
  static create(options: ConnectionPoolOptions): Promise<ConnectionPool>;
122
+ private ensureAvailable;
247
123
  acquireReader(): SQLiteConnection;
248
124
  acquireWriter(): SQLiteConnection;
125
+ /**
126
+ * Returns the next reader connection and keeps it open until the caller passes the lease back through
127
+ * {@link ConnectionPool.releaseReader}, so that {@link ConnectionPool.closeReaders} closes no reader under a read.
128
+ *
129
+ * @returns The reader connection, which the caller releases exactly once when its read settles.
130
+ */
131
+ leaseReader(): SQLiteConnection;
132
+ /** Ends a lease that {@link ConnectionPool.leaseReader} returned. */
133
+ releaseReader(): void;
134
+ /**
135
+ * Returns what a read on the next reader connection returns, and keeps that connection open until the read settles,
136
+ * so that {@link ConnectionPool.closeReaders} closes no reader under a read.
137
+ *
138
+ * @param read - The read to run on the reader.
139
+ * @returns The value of the read.
140
+ */
141
+ withReader<T>(read: (conn: SQLiteConnection) => Promise<T>): Promise<T>;
249
142
  /** Returns the writer and every reader, so that the caller can apply an operation to every connection in the pool. */
250
143
  connections(): readonly SQLiteConnection[];
251
144
  get readerCount(): number;
252
145
  get isReadOnly(): boolean;
146
+ /**
147
+ * Waits for every read on a reader to settle, then closes every reader and leaves the writer open, so that a
148
+ * checkpoint on the writer can reach the end of the log. Every acquire fails with `ENCRYPTION_CHANGE_IN_PROGRESS`
149
+ * until {@link ConnectionPool.reopen} returns.
150
+ *
151
+ * @param drainTimeoutMs - How long to wait for the reads to settle.
152
+ * @throws An `EncryptionError` with `ENCRYPTION_CHANGE_ABORTED` when a read runs past the timeout, which leaves the
153
+ * readers open and the pool available.
154
+ */
155
+ closeReaders(drainTimeoutMs: number): Promise<void>;
156
+ private readsDrained;
157
+ /** Closes the writer after {@link ConnectionPool.closeReaders}. */
158
+ closeWriter(): Promise<void>;
159
+ /** `true` from {@link ConnectionPool.closeReaders} until {@link ConnectionPool.reopen} returns. */
160
+ get suspended(): boolean;
161
+ /**
162
+ * Opens the writer and the readers again, under the key that the pool's keyring holds now, and loads into them
163
+ * every extension that the database loaded into the connections they replace. When the connections fail to open,
164
+ * every acquire fails with that error until a later reopen succeeds.
165
+ */
166
+ reopen(): Promise<void>;
253
167
  close(): Promise<void>;
254
168
  }
255
169
 
@@ -360,6 +274,7 @@ declare class DatabaseCdcController {
360
274
  private stopPolling;
361
275
  private stamper;
362
276
  private epochRequest;
277
+ private readonly liveConnectionListeners;
363
278
  constructor(runExclusive: RunExclusive$1, acquireWriter: () => SQLiteConnection, pollInterval: number, retention: number, openSnapshotConnection: (() => Promise<SQLiteConnection>) | null, deviceRetention: DeviceRetentionPolicy, onDispatched?: ChangeDispatchObserver | undefined);
364
279
  ensureEpoch(): Promise<string>;
365
280
  readAtPositionWith<T>(read: (conn: SQLiteConnection) => Promise<T>): Promise<{
@@ -372,6 +287,27 @@ declare class DatabaseCdcController {
372
287
  run: RunExclusive$1;
373
288
  }>;
374
289
  closeLiveConnection(): Promise<void>;
290
+ /**
291
+ * Registers a function that the controller calls when a key change closes the live connection to replace the file,
292
+ * so that each live query plans again on the connection that opens afterwards.
293
+ *
294
+ * @param listener - The function to call.
295
+ * @returns A function that removes the registration.
296
+ */
297
+ onLiveConnectionReplaced(listener: () => void): () => void;
298
+ /**
299
+ * Closes the live connection before a key change replaces the file, after telling every live query to plan again
300
+ * on the connection that the next call opens once the replacement ends. Where live queries read through the
301
+ * pool's writer, {@link DatabaseCdcController.resumeOnReopenedPool} tells them instead, once that writer exists.
302
+ */
303
+ releaseLiveConnection(): Promise<void>;
304
+ /**
305
+ * Starts polling again where polling was on, and has every live query that reads through the pool's writer plan
306
+ * again, after the pool opened its connections to the file that a key change left in place. The key change calls
307
+ * this while it holds the writer, so no live read reaches the closed writer.
308
+ */
309
+ resumeOnReopenedPool(): void;
310
+ private replanLiveQueries;
375
311
  get subscriptions(): SubscriptionManager | null;
376
312
  get changeTracker(): ChangeTracker | null;
377
313
  watch(table: string): Promise<void>;
@@ -638,6 +574,7 @@ declare class DatabaseSyncController {
638
574
  private readonly acquireWriter;
639
575
  private readonly cdc;
640
576
  private pkResolver;
577
+ private cachedOn;
641
578
  private tablesReady;
642
579
  private metaReady;
643
580
  private outboxReader;
@@ -668,6 +605,7 @@ declare class DatabaseSyncController {
668
605
  private setMetaSeq;
669
606
  private ensureMeta;
670
607
  private ensureSyncTables;
608
+ private resolverOn;
671
609
  private ensureOutboxReader;
672
610
  private ensureApplier;
673
611
  private catchUpLocalColumnVersions;
@@ -754,11 +692,212 @@ declare class DatabaseWriteController {
754
692
  private runInTransaction;
755
693
  }
756
694
 
695
+ /**
696
+ * Counts the replication engines that write to one database through a separate writer connection, so that the
697
+ * database can refuse an operation that would replace its file under those connections.
698
+ *
699
+ * @internal
700
+ */
701
+ declare class RegisteredReplicationEngines {
702
+ private readonly databaseId;
703
+ private count;
704
+ private readonly replacements;
705
+ constructor(databaseId: string);
706
+ /**
707
+ * Records one running engine, and returns the call that removes the record, which does nothing after its first
708
+ * call.
709
+ *
710
+ * @returns The call that removes the record.
711
+ * @throws The error of a file replacement under way, which would leave the engine on the replaced file.
712
+ */
713
+ register(): () => void;
714
+ /**
715
+ * Throws when an engine runs, because the engine's writer connection would stay on the replaced file.
716
+ *
717
+ * @param operation - The operation that would replace the file, as the error message states it.
718
+ * @throws A `ReplicationError` with `REPLICATION_ACTIVE` while an engine runs.
719
+ */
720
+ assertNone(operation: string): void;
721
+ /**
722
+ * Refuses the operation while an engine runs, and otherwise refuses every engine that starts before the returned
723
+ * call ends the replacement.
724
+ *
725
+ * @param operation - The operation that replaces the file, as the error message states it.
726
+ * @param refuseEngine - Builds the error that an engine which starts during the replacement receives.
727
+ * @returns The call that ends the replacement.
728
+ * @throws A `ReplicationError` with `REPLICATION_ACTIVE` while an engine runs.
729
+ */
730
+ beginReplacement(operation: string, refuseEngine: () => SirannonError): () => void;
731
+ }
732
+
733
+ /**
734
+ * Records the resolved path of every compiled extension that a database has
735
+ * loaded, and tracks every connection that the database opens beyond its pool,
736
+ * so that each of those connections can call the extensions' functions. SQLite
737
+ * scopes a loaded extension to the connection that loaded it, and a database
738
+ * opens a new connection for each consistent snapshot read and one more for its
739
+ * live queries.
740
+ *
741
+ * This class performs loads and opens one at a time, so a connection either
742
+ * opens after a load and loads that extension as it opens, or opens before the
743
+ * load and is one of the connections that the load covers.
744
+ *
745
+ * @internal
746
+ */
747
+ declare class LoadedExtensions {
748
+ private readonly driver;
749
+ private readonly resolvedPaths;
750
+ private readonly openedConnections;
751
+ private queue;
752
+ constructor(driver: SQLiteDriver);
753
+ private runInTurn;
754
+ private loadRecorded;
755
+ /** Loads an extension into the pool's connections and every tracked connection, then records its resolved path. */
756
+ load(poolConnections: readonly SQLiteConnection[], extensionPath: string): Promise<void>;
757
+ /** Loads every recorded extension into connections that replaced the ones that the database loaded them into. */
758
+ reload(connections: readonly SQLiteConnection[]): Promise<void>;
759
+ /**
760
+ * Opens a connection, loads every recorded extension into it, and returns a
761
+ * wrapper that forwards each call to it. Closing the wrapper stops the
762
+ * tracking and closes the connection underneath.
763
+ *
764
+ * @param openConnection - The function that opens the connection that this database needs.
765
+ * @returns The wrapper to read and write through.
766
+ */
767
+ open(openConnection: () => Promise<SQLiteConnection>): Promise<SQLiteConnection>;
768
+ }
769
+
770
+ /**
771
+ * A part of the database that keeps a connection to the file open between calls, and closes and reopens it when a
772
+ * key change replaces the file.
773
+ *
774
+ * @internal
775
+ */
776
+ interface FileConnectionHolder {
777
+ /** Closes the held connection and stops the work that uses it. */
778
+ release(): Promise<void>;
779
+ /** Opens a new connection to the file that the key change left in place, and resumes the work that uses it. */
780
+ reopen(): Promise<void>;
781
+ }
782
+ /**
783
+ * Opens every connection to a database file beyond the pool, under the database's current key and with its loaded
784
+ * extensions, and tracks each one, so that a key change can close all of them before it replaces the file and open
785
+ * them again afterwards.
786
+ *
787
+ * @internal
788
+ */
789
+ declare class DatabaseFileConnections {
790
+ private readonly driver;
791
+ private readonly path;
792
+ private readonly keyring;
793
+ private readonly extensions;
794
+ private active;
795
+ private readonly holders;
796
+ private replacement;
797
+ private drained;
798
+ constructor(driver: SQLiteDriver, path: string, keyring: EncryptionKeyring, extensions: LoadedExtensions);
799
+ /**
800
+ * Opens a connection to the database file, after any file replacement under way has ended.
801
+ *
802
+ * @param options - The open settings, without a key, which this method adds.
803
+ * @returns The connection, which the caller closes.
804
+ */
805
+ open(options: OpenOptions): Promise<SQLiteConnection>;
806
+ private settle;
807
+ /**
808
+ * Registers a part of the database that keeps a connection open between calls.
809
+ *
810
+ * @param holder - The release and reopen calls of that part.
811
+ * @returns A function that removes the registration.
812
+ */
813
+ hold(holder: FileConnectionHolder): () => void;
814
+ /**
815
+ * Makes later opens wait, has every holder close its connection, and waits until every other connection to the
816
+ * file has closed.
817
+ *
818
+ * @param timeoutMs - How long to wait for the connections that a call holds for the duration of one read.
819
+ * @throws An `EncryptionError` with `ENCRYPTION_CHANGE_ABORTED` when a connection stays open past the deadline.
820
+ */
821
+ closeForReplacement(timeoutMs: number): Promise<void>;
822
+ /** Lets opens proceed again, and has every holder open its connection to the file that is now in place. */
823
+ reopenAfterReplacement(): Promise<void>;
824
+ }
825
+
826
+ /**
827
+ * The parts of an open database that a key change works through.
828
+ *
829
+ * @internal
830
+ */
831
+ interface KeyChangeDeps {
832
+ path: string;
833
+ driver: SQLiteDriver;
834
+ keyring: EncryptionKeyring;
835
+ pool: ConnectionPool;
836
+ writerLock: WriterLock;
837
+ fileConnections: DatabaseFileConnections;
838
+ cdc: DatabaseCdcController;
839
+ backups: DatabaseBackupController;
840
+ }
841
+
842
+ /**
843
+ * What the encryption controller needs beyond the parts that a key change works through.
844
+ *
845
+ * @internal
846
+ */
847
+ interface EncryptionControllerDeps extends KeyChangeDeps {
848
+ databaseId: string;
849
+ readOnly: boolean;
850
+ replicationEngines: RegisteredReplicationEngines;
851
+ onKeyChanged?: (key: EncryptionKeySource | null) => void;
852
+ }
853
+ /**
854
+ * Reports how a database is encrypted, and runs one key change at a time.
855
+ *
856
+ * @internal
857
+ */
858
+ declare class DatabaseEncryptionController {
859
+ private readonly deps;
860
+ private change;
861
+ private running;
862
+ private closing;
863
+ private described;
864
+ constructor(deps: EncryptionControllerDeps);
865
+ /** `true` while the database file opens under a key. */
866
+ get encrypted(): boolean;
867
+ /**
868
+ * Returns whether the file is encrypted, the scheme that it is encrypted under, the engine's version, the stored
869
+ * key identifier and change time, and the current or last key change.
870
+ *
871
+ * @returns The status.
872
+ */
873
+ status(): Promise<EncryptionStatus>;
874
+ /**
875
+ * Encrypts the database, changes its key, or removes encryption, and resolves once the change ends.
876
+ *
877
+ * @param change - The target key function, or `null` for a plaintext file.
878
+ * @returns The status after the change.
879
+ * @throws `READ_ONLY` on a read-only database, `ENCRYPTION_CHANGE_IN_PROGRESS` while another change runs,
880
+ * `REPLICATION_ACTIVE` while a replication engine writes to the database, and the error that stopped the change.
881
+ */
882
+ changeKey(change: EncryptionChange): Promise<EncryptionStatus>;
883
+ private runChange;
884
+ /** Stops a running key change at its next step, and waits for it to leave the database as it was. */
885
+ cancel(): Promise<void>;
886
+ private targetOf;
887
+ private finished;
888
+ private failed;
889
+ private describeFile;
890
+ }
891
+
757
892
  interface DatabaseInternals {
758
893
  parentHooks?: HookRegistry;
759
894
  metrics?: MetricsCollector;
895
+ onEncryptionChanged?: (key: EncryptionKeySource | null) => void;
760
896
  }
761
897
  interface DatabaseRuntime {
898
+ fileConnections: DatabaseFileConnections;
899
+ encryption: DatabaseEncryptionController;
900
+ replicationEngines: RegisteredReplicationEngines;
762
901
  pool: ConnectionPool;
763
902
  writeGate: WriteGate;
764
903
  writerLock: WriterLock;
@@ -1003,6 +1142,94 @@ declare class DatabaseBackups extends DatabaseLifecycle {
1003
1142
  backupPiecesSafeToDelete(options?: BackupSafeToDeleteOptions): Promise<BackupChainRecord[]>;
1004
1143
  }
1005
1144
 
1145
+ /**
1146
+ * Gives an open database its encryption methods: the status of its key, and the key change that encrypts it, gives it
1147
+ * a new key, or removes encryption while it stays open.
1148
+ *
1149
+ * Open a database through {@link Sirannon.open}, which returns a {@link Database} that extends this class.
1150
+ *
1151
+ * @public
1152
+ */
1153
+ declare class DatabaseEncryption extends DatabaseBackups {
1154
+ /**
1155
+ * Returns whether the database file holds ciphertext, the version of the encryption engine, the identifier that
1156
+ * came with the current key, the time of the last finished key change, and the progress of the current or last
1157
+ * key change. The status never contains a key.
1158
+ *
1159
+ * @returns The encryption status.
1160
+ */
1161
+ encryptionStatus(): Promise<EncryptionStatus>;
1162
+ /**
1163
+ * Encrypts a plaintext database, gives an encrypted one a new key, or, with `key` set to `null`, stores it as
1164
+ * plaintext, while the database stays open for reads and writes.
1165
+ *
1166
+ * Sirannon copies a snapshot to a new file under the target key, applies the writes committed since the snapshot in
1167
+ * rounds, holds writes only for the last round, and then replaces the file. Until the replacement, the database
1168
+ * keeps its current key. After it, every connection opens under the new key, so open the database under that key
1169
+ * from then on. When the database captures backups, the next capture starts a new chain with a full copy under the
1170
+ * new key.
1171
+ *
1172
+ * On the Expo driver, the change needs expo-file-system 19.0.0 or later. Once an app has called this method, keep
1173
+ * expo-file-system 19.0.0 or later installed in every later build, because the open that recovers from an
1174
+ * interrupted change reads the files beside the database through it. Without it, the open cannot see that a change
1175
+ * stopped part way.
1176
+ *
1177
+ * @param change - The function that returns the target key, or `null` to remove encryption.
1178
+ * @returns The encryption status once the change ends.
1179
+ * @throws An `EncryptionError` with `ENCRYPTION_UNSUPPORTED` on an Expo app without expo-file-system 19.0.0 or
1180
+ * later, before the change writes any file, with `ENCRYPTION_CHANGE_IN_PROGRESS` while another change runs, or with
1181
+ * `ENCRYPTION_CHANGE_ABORTED` when a schema change or a write that the change cannot apply exactly stops it, which
1182
+ * leaves the database under its current key, `READ_ONLY` on a read-only database, and a `ReplicationError` with
1183
+ * `REPLICATION_ACTIVE` while a replication engine writes to the database, which the caller stops before the change
1184
+ * and starts again afterwards.
1185
+ */
1186
+ changeEncryption(change: EncryptionChange): Promise<EncryptionStatus>;
1187
+ /**
1188
+ * Opens a connection to this database's file beyond the pool, under the database's current key, which waits while
1189
+ * a key change replaces the file. The caller closes the connection.
1190
+ *
1191
+ * @param options - The open settings, without a key.
1192
+ * @returns The connection.
1193
+ * @internal
1194
+ */
1195
+ openFileConnection(options: OpenOptions): Promise<SQLiteConnection>;
1196
+ /**
1197
+ * Registers a part of the server that keeps a connection from {@link DatabaseEncryption.openFileConnection} open
1198
+ * between calls, so that a key change can close it before it replaces the file and reopen it afterwards.
1199
+ *
1200
+ * @param holder - The release and reopen calls.
1201
+ * @returns A function that removes the registration.
1202
+ * @internal
1203
+ */
1204
+ holdFileConnection(holder: FileConnectionHolder): () => void;
1205
+ /**
1206
+ * Returns whether the database file opens under a key, which a replication engine announces to its peers. A key
1207
+ * change can alter it only while no engine runs, so the value holds for an engine's whole run.
1208
+ *
1209
+ * @returns `true` for an encrypted database.
1210
+ * @internal
1211
+ */
1212
+ get encryptedFile(): boolean;
1213
+ /**
1214
+ * Records a replication engine that writes to this database through a separate writer connection, so that a key
1215
+ * change or a restore refuses to replace the file under that connection while the engine runs.
1216
+ *
1217
+ * @returns A function that removes the record, which the engine calls when it stops.
1218
+ * @throws `ENCRYPTION_CHANGE_IN_PROGRESS` while a key change runs, and `DATABASE_CLOSED` on a closed database.
1219
+ * @internal
1220
+ */
1221
+ registerReplicationEngine(): () => void;
1222
+ /**
1223
+ * Throws while a replication engine writes to this database, because replacing the file would leave the engine's
1224
+ * writer connection on the replaced file.
1225
+ *
1226
+ * @param operation - The operation that would replace the file, as the error message states it.
1227
+ * @throws A `ReplicationError` with `REPLICATION_ACTIVE` while an engine runs.
1228
+ * @internal
1229
+ */
1230
+ assertNoReplicationEngine(operation: string): void;
1231
+ }
1232
+
1006
1233
  /**
1007
1234
  * An open SQLite database, with methods for reads, writes, transactions, migrations, change subscriptions, and live queries.
1008
1235
  *
@@ -1010,7 +1237,7 @@ declare class DatabaseBackups extends DatabaseLifecycle {
1010
1237
  *
1011
1238
  * @public
1012
1239
  */
1013
- declare class Database extends DatabaseBackups {
1240
+ declare class Database extends DatabaseEncryption {
1014
1241
  /** @internal */
1015
1242
  static create(id: string, path: string, driver: SQLiteDriver, options?: DatabaseOptions, internals?: DatabaseInternals): Promise<Database>;
1016
1243
  /** @internal */
@@ -1231,4 +1458,4 @@ declare class Database extends DatabaseBackups {
1231
1458
  onAfterQuery(hook: AfterQueryHook): HookDispose;
1232
1459
  }
1233
1460
 
1234
- export { BackupError as B, CDCError as C, Database as D, ExtensionError as E, ForbiddenSqlError as F, HookRegistry as H, MaxDatabasesError as M, type PushHookContext as P, QueryError as Q, ReadOnlyError as R, SirannonError as S, TransactionError as T, WriteOverloadError as W, type HookDispose as a, ConnectionPool as b, ConnectionPoolError as c, type ConnectionPoolOptions as d, DatabaseAlreadyExistsError as e, DatabaseBackups as f, DatabaseLifecycle as g, DatabaseNotFoundError as h, HookDeniedError as i, type HookEvent as j, type HookEventContextMap as k, type HookHandler as l, MigrationError as m, RequestDeniedError as n, type SnapshotHookContext as o, type SubscribeHookContext as p, type DeviceSyncPort as q, HLC as r };
1461
+ export { ConnectionPool as C, Database as D, HookRegistry as H, type PushHookContext as P, type SnapshotHookContext as S, type HookDispose as a, type DeviceSyncPort as b, type ConnectionPoolOptions as c, DatabaseBackups as d, DatabaseEncryption as e, DatabaseLifecycle as f, type HookEvent as g, type HookEventContextMap as h, type HookHandler as i, type SubscribeHookContext as j, HLC as k };