@korajs/store 1.0.0-beta.11 → 1.0.0-beta.13

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 (92) hide show
  1. package/README.md +29 -37
  2. package/dist/adapters/better-sqlite3.cjs +90 -4
  3. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  4. package/dist/adapters/better-sqlite3.d.cts +21 -12
  5. package/dist/adapters/better-sqlite3.d.ts +21 -12
  6. package/dist/adapters/better-sqlite3.js +69 -5
  7. package/dist/adapters/better-sqlite3.js.map +1 -1
  8. package/dist/adapters/indexeddb.cjs +1316 -610
  9. package/dist/adapters/indexeddb.cjs.map +1 -1
  10. package/dist/adapters/indexeddb.d.cts +29 -3
  11. package/dist/adapters/indexeddb.d.ts +29 -3
  12. package/dist/adapters/indexeddb.js +247 -221
  13. package/dist/adapters/indexeddb.js.map +1 -1
  14. package/dist/adapters/sqlite-wasm-worker.cjs +803 -62
  15. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm-worker.js +480 -62
  17. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  18. package/dist/adapters/sqlite-wasm.cjs +1032 -344
  19. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  20. package/dist/adapters/sqlite-wasm.d.cts +239 -18
  21. package/dist/adapters/sqlite-wasm.d.ts +239 -18
  22. package/dist/adapters/sqlite-wasm.js +111 -5
  23. package/dist/adapters/sqlite-wasm.js.map +1 -1
  24. package/dist/backup-4NUPDEMB.js +25 -0
  25. package/dist/chunk-32EDKWMI.js +876 -0
  26. package/dist/chunk-32EDKWMI.js.map +1 -0
  27. package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
  28. package/dist/chunk-A6GFH5IG.js +36 -0
  29. package/dist/chunk-A6GFH5IG.js.map +1 -0
  30. package/dist/chunk-DUKTWVZW.js +175 -0
  31. package/dist/chunk-DUKTWVZW.js.map +1 -0
  32. package/dist/chunk-HZTDBLDB.js +607 -0
  33. package/dist/chunk-HZTDBLDB.js.map +1 -0
  34. package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
  35. package/dist/chunk-J5AGXF3D.js.map +1 -0
  36. package/dist/chunk-MCQKEHOD.js +112 -0
  37. package/dist/chunk-MCQKEHOD.js.map +1 -0
  38. package/dist/chunk-P4V375FY.js +810 -0
  39. package/dist/chunk-P4V375FY.js.map +1 -0
  40. package/dist/chunk-QDUSJMFV.js +1095 -0
  41. package/dist/chunk-QDUSJMFV.js.map +1 -0
  42. package/dist/chunk-SJP2HTYZ.js +1003 -0
  43. package/dist/chunk-SJP2HTYZ.js.map +1 -0
  44. package/dist/chunk-W5F2LWG6.js +591 -0
  45. package/dist/chunk-W5F2LWG6.js.map +1 -0
  46. package/dist/chunk-Y5LRH7KI.js +5341 -0
  47. package/dist/chunk-Y5LRH7KI.js.map +1 -0
  48. package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
  49. package/dist/index.cjs +9354 -4157
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +1064 -108
  52. package/dist/index.d.ts +1064 -108
  53. package/dist/index.js +249 -3092
  54. package/dist/index.js.map +1 -1
  55. package/dist/internal.cjs +580 -264
  56. package/dist/internal.cjs.map +1 -1
  57. package/dist/internal.d.cts +15 -85
  58. package/dist/internal.d.ts +15 -85
  59. package/dist/internal.js +10 -8
  60. package/dist/restore-FR7XIKT2.js +299 -0
  61. package/dist/restore-FR7XIKT2.js.map +1 -0
  62. package/dist/sql-builder-C4u55jKW.d.cts +608 -0
  63. package/dist/sql-builder-CGckdBDL.d.ts +608 -0
  64. package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
  65. package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
  66. package/dist/sqlite-wasm-channel-MLHLKEED.js +12 -0
  67. package/dist/store-IMWKZLWB.js +16 -0
  68. package/dist/store-IMWKZLWB.js.map +1 -0
  69. package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
  70. package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
  71. package/package.json +3 -2
  72. package/dist/backup-5S7K2Z35.js +0 -14
  73. package/dist/chunk-GSJ7EEUC.js.map +0 -1
  74. package/dist/chunk-HS6DBMST.js +0 -383
  75. package/dist/chunk-HS6DBMST.js.map +0 -1
  76. package/dist/chunk-RJIEKEN4.js +0 -751
  77. package/dist/chunk-RJIEKEN4.js.map +0 -1
  78. package/dist/chunk-TUR7RGEP.js +0 -84
  79. package/dist/chunk-TUR7RGEP.js.map +0 -1
  80. package/dist/chunk-VDBJQ75K.js +0 -78
  81. package/dist/chunk-VDBJQ75K.js.map +0 -1
  82. package/dist/chunk-YT7MRFCP.js +0 -687
  83. package/dist/chunk-YT7MRFCP.js.map +0 -1
  84. package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
  85. package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
  86. package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
  87. package/dist/types-BOO9EPt1.d.ts +0 -170
  88. package/dist/types-C_GDavG-.d.cts +0 -170
  89. /package/dist/{backup-5S7K2Z35.js.map → backup-4NUPDEMB.js.map} +0 -0
  90. /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
  91. /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
  92. /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-MLHLKEED.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { KoraEventEmitter, SchemaDefinition } from '@korajs/core';
2
- import { S as StorageAdapter, a as StorageOpenState, T as Transaction, M as MigrationPlan } from '../types-BslDOTk0.cjs';
3
- import { W as WorkerBridge } from '../sqlite-wasm-channel-L6o3bWQK.cjs';
4
- export { a as WebWorkerBridge, b as WorkerRequest, c as WorkerResponse } from '../sqlite-wasm-channel-L6o3bWQK.cjs';
2
+ import { S as StorageAdapter, T as Transaction, a as StorageOpenState, M as MigrationPlan } from '../types-Dh-xyUjP.cjs';
3
+ import { W as WorkerBridge } from '../sqlite-wasm-channel-DVX93Uiq.cjs';
4
+ export { a as WebWorkerBridge, b as WorkerRequest, c as WorkerResponse, d as WorkerSendOptions, e as WorkerStatusEvent } from '../sqlite-wasm-channel-DVX93Uiq.cjs';
5
5
 
6
6
  /**
7
7
  * Async mutex for serializing access to a resource across `await` boundaries.
@@ -26,6 +26,19 @@ declare class Mutex {
26
26
  private createRelease;
27
27
  }
28
28
 
29
+ /** Per-request options for {@link SqliteWasmAdapter.execute} and `query`. */
30
+ interface StorageRequestOptions {
31
+ /**
32
+ * Stop waiting for the response. The leader may still apply a write that
33
+ * already reached it; retry with the same `requestId` to stay idempotent.
34
+ */
35
+ signal?: AbortSignal;
36
+ /**
37
+ * Stable id for this request. A follower tab's retry with the same id is
38
+ * answered from the leader's response cache instead of being applied twice.
39
+ */
40
+ requestId?: string;
41
+ }
29
42
  /**
30
43
  * Options for creating a SqliteWasmAdapter.
31
44
  */
@@ -36,7 +49,7 @@ interface SqliteWasmAdapterOptions {
36
49
  */
37
50
  bridge?: WorkerBridge;
38
51
  /**
39
- * Database name for persistence. Used as the OPFS file name or IDB key.
52
+ * Database name for persistence. Each database gets its own OPFS pool.
40
53
  */
41
54
  dbName?: string;
42
55
  /**
@@ -58,18 +71,66 @@ interface SqliteWasmAdapterOptions {
58
71
  */
59
72
  emitNonPersistentDiagnostic?: boolean;
60
73
  /**
61
- * When set, storage diagnostics are emitted here: `store:opfs-unavailable` when
62
- * persistence silently degraded to a non-persistent in-memory database, and
63
- * `store:db-name-collision` when another runtime on this origin was already
64
- * using this database name.
74
+ * Accept writes when durable OPFS storage could not be obtained and the
75
+ * database runs in memory. Defaults to false: a non-durable open or promotion
76
+ * emits the blocking `store:durability-lost` event and every write is refused
77
+ * with {@link StorageDurabilityError}, so data is never accepted and then lost
78
+ * on reload. Set true only for apps that can tolerate losing local writes.
79
+ */
80
+ allowNonDurable?: boolean;
81
+ /**
82
+ * For `createApp()`: skip the durability check at open only, because the caller
83
+ * inspects {@link SqliteWasmAdapter.getStorageOpenState} right after open and
84
+ * switches to a durable backend itself. A later leader promotion that loses
85
+ * durable storage is still enforced.
86
+ */
87
+ deferOpenDurabilityCheck?: boolean;
88
+ /**
89
+ * Release storage ownership when the page is frozen or hidden into the
90
+ * back/forward cache (`freeze` / `pagehide`), so a visible tab can take over
91
+ * instead of waiting on a tab that cannot run; ownership is re-acquired on
92
+ * `resume` / `pageshow`. Defaults to true.
93
+ */
94
+ releaseOnFreeze?: boolean;
95
+ /**
96
+ * When set, storage diagnostics are emitted here: `store:opfs-unavailable`,
97
+ * `store:durability-lost`, `store:storage-blocked` (another holder has the
98
+ * storage; the open waits), `store:storage-migrated` and
99
+ * `store:db-name-collision`.
65
100
  */
66
101
  emitter?: KoraEventEmitter;
102
+ /**
103
+ * @internal Used by the IndexedDB adapter: open an in-memory database that
104
+ * never touches OPFS (the IndexedDB adapter persists snapshots itself).
105
+ */
106
+ storage?: 'opfs' | 'memory';
107
+ /**
108
+ * @internal Do not read or write the storage manifest (the IndexedDB adapter's
109
+ * inner database and its one-shot OPFS reader manage the manifest themselves).
110
+ */
111
+ skipBackendRecord?: boolean;
112
+ /**
113
+ * @internal Used by the IndexedDB adapter (STORE-6): runs in one transaction on this
114
+ * tab's OWN worker right after it opened the database as storage leader (first open
115
+ * or promotion), before the worker serves any other tab. The IndexedDB adapter
116
+ * restores its persisted snapshot here, so a snapshot is only ever restored into a
117
+ * freshly created worker database and never over a leader's live one. Throwing fails
118
+ * the open (or the promotion, which then refuses writes).
119
+ */
120
+ onLeaderWorkerOpened?: (tx: Transaction) => Promise<void>;
67
121
  }
68
122
  /**
69
123
  * SQLite WASM adapter that communicates with a SQLite instance through a WorkerBridge.
70
124
  *
71
- * In browsers, the bridge is backed by a Web Worker running SQLite WASM with OPFS persistence.
72
- * In Node.js tests, the bridge is backed by MockWorkerBridge wrapping better-sqlite3.
125
+ * In browsers, one tab per database (the leader) runs a dedicated worker that
126
+ * owns the database's OPFS pool; other tabs reach that worker over a
127
+ * BroadcastChannel. In Node.js tests, the bridge is backed by MockWorkerBridge
128
+ * wrapping better-sqlite3.
129
+ *
130
+ * Storage is durable or loud, never silently in memory (W8a): another holder of
131
+ * the storage makes the open wait (reported with `store:storage-blocked`); only a
132
+ * runtime without OPFS SAH support gets a non-durable database, which refuses
133
+ * writes unless the app opted in with `allowNonDurable`.
73
134
  *
74
135
  * @example
75
136
  * ```typescript
@@ -83,6 +144,8 @@ interface SqliteWasmAdapterOptions {
83
144
  */
84
145
  declare class SqliteWasmAdapter implements StorageAdapter {
85
146
  private bridge;
147
+ /** The dedicated worker this tab runs while it is the database's leader. */
148
+ private ownWorker;
86
149
  private opened;
87
150
  private readonly mutex;
88
151
  private readonly injectedBridge;
@@ -97,10 +160,45 @@ declare class SqliteWasmAdapter implements StorageAdapter {
97
160
  /** Retained so a follower promoted to leader can re-open its own worker. */
98
161
  private schema;
99
162
  private promoting;
163
+ private closing;
164
+ private suspended;
165
+ /** Requests wait here while leadership changes hands (promotion, resume). */
166
+ private gate;
167
+ private removeLifecycleListeners;
168
+ private readonly allowNonDurable;
169
+ private readonly deferOpenDurabilityCheck;
170
+ private readonly releaseOnFreeze;
171
+ private readonly storage;
172
+ private readonly skipBackendRecord;
173
+ /** Set when this adapter lost (or never had) durable storage; writes are refused. */
174
+ private durabilityLoss;
175
+ private readonly onLeaderWorkerOpened;
100
176
  constructor(options?: SqliteWasmAdapterOptions);
101
177
  open(schema: SchemaDefinition): Promise<void>;
102
- private openDurableWorkerBridge;
178
+ /**
179
+ * Join the database as leader (own worker) or follower (RPC to the leader's
180
+ * worker), then open it. The open waits, without a timeout, while another
181
+ * holder still has the storage; the worker reports that as a blocking state.
182
+ */
183
+ private attachAndOpen;
184
+ /** Open the database in this tab's own worker and start serving followers. */
185
+ private openLeaderWorker;
186
+ /**
187
+ * Whether this runtime owns the database's worker: the storage leader in a browser,
188
+ * or any adapter with an injected bridge (tests, Node). A follower tab relays every
189
+ * request to the leader's worker (STORE-6).
190
+ */
191
+ isLeader(): boolean;
192
+ private checkedOpen;
103
193
  private openCurrentBridge;
194
+ private onWorkerEvent;
195
+ /**
196
+ * Leader-only bookkeeping after a durable open: report a legacy-pool
197
+ * migration, move an IndexedDB-held copy into OPFS explicitly (never two
198
+ * disjoint copies), and record OPFS as this database's backend.
199
+ */
200
+ private afterLeaderOpen;
201
+ private applyDumpThroughWorker;
104
202
  /**
105
203
  * Surface a `store:opfs-unavailable` diagnostic when the worker reported that
106
204
  * persistence silently degraded to an in-memory database, so a data-loss
@@ -109,18 +207,58 @@ declare class SqliteWasmAdapter implements StorageAdapter {
109
207
  */
110
208
  private reportStorageMode;
111
209
  getStorageOpenState(): StorageOpenState | null;
210
+ /**
211
+ * Durable or loud, never silent (NEW-STORE-6). When the open or a promotion
212
+ * ended on non-persistent storage and the app did not opt into that, emit the
213
+ * blocking `store:durability-lost` event and refuse every later write. A
214
+ * bridge that reports no mode (the Node mock) is not judged.
215
+ */
216
+ private enforceDurability;
217
+ private guardDurableWrite;
112
218
  private warnSharedWorkerDeprecated;
219
+ /**
220
+ * Close the database. A leader closes the database file and releases the OPFS
221
+ * pool inside its worker, terminates the worker, and only then releases the
222
+ * tab leader lock, so the next leader never races a still-running worker for
223
+ * the file handles (NEW-STORE-10). A follower just leaves.
224
+ */
113
225
  close(): Promise<void>;
226
+ /**
227
+ * Release everything this adapter holds, in ownership order: lifecycle
228
+ * listeners, then the worker (which holds the OPFS pool and its Web Lock),
229
+ * then the tab leader lock or queued promotion request.
230
+ */
231
+ private teardown;
114
232
  /**
115
233
  * Promotes a follower to leader after the previous leader released the storage
116
- * lock (its tab closed or crashed). The lock is only granted once the old leader
117
- * is gone, so opening our own worker here is safe from the OPFS single-writer
118
- * rule. In-flight follower RPCs are rejected and left to the caller / reactive
119
- * layer to retry against the new leader.
234
+ * lock (its tab closed, crashed, or suspended on freeze). The new worker waits
235
+ * on the pool's Web Lock and handles, so it never races the previous worker.
236
+ * Requests issued during the hand-off wait for it; requests that were in
237
+ * flight to the old leader fail with a retriable `BridgeTerminatedError`.
120
238
  */
121
239
  private promoteToLeader;
122
- execute(sql: string, params?: unknown[]): Promise<void>;
123
- query<T>(sql: string, params?: unknown[]): Promise<T[]>;
240
+ /**
241
+ * Page Lifecycle: a frozen or bfcached page cannot run, so it must not keep
242
+ * other tabs waiting on its storage. Leaders terminate their worker (freeing
243
+ * the OPFS pool) and then release the leader lock, so a visible tab is
244
+ * promoted; followers withdraw their queued lock request so a frozen tab is
245
+ * never granted leadership it cannot use. Never uses Web Locks `steal`.
246
+ */
247
+ private installLifecycleListeners;
248
+ /** Synchronous by necessity: a `freeze` handler gets no further turns. */
249
+ private suspend;
250
+ private resume;
251
+ /**
252
+ * Non-transactional write. Takes the same mutex as {@link transaction}, so it
253
+ * never lands inside this tab's open transaction and is never rolled back
254
+ * with it (STORE-8). Inside a transaction callback, use the `tx` handle.
255
+ */
256
+ execute(sql: string, params?: unknown[], options?: StorageRequestOptions): Promise<void>;
257
+ /**
258
+ * Non-transactional read. Waits for this tab's open transaction to finish, so
259
+ * it never observes uncommitted (possibly rolled-back) rows (STORE-8).
260
+ */
261
+ query<T>(sql: string, params?: unknown[], options?: StorageRequestOptions): Promise<T[]>;
124
262
  transaction(fn: (tx: Transaction) => Promise<void>): Promise<void>;
125
263
  migrate(from: number, to: number, migration: MigrationPlan): Promise<void>;
126
264
  /**
@@ -137,4 +275,87 @@ declare class SqliteWasmAdapter implements StorageAdapter {
137
275
  private sendChecked;
138
276
  }
139
277
 
140
- export { Mutex, SqliteWasmAdapter, type SqliteWasmAdapterOptions, WorkerBridge };
278
+ /**
279
+ * Kora-owned manifest of the local databases this origin created (W8a).
280
+ *
281
+ * One record per database name, kept in its own IndexedDB database so it is
282
+ * readable from any tab without opening the OPFS pools. It records which backend
283
+ * holds the database's data, which is how Kora keeps OPFS and IndexedDB from
284
+ * ever holding two disjoint copies: a backend switch is a recorded, explicit
285
+ * migration, never a silent fresh start (NEW-STORE-5, LMS-5c).
286
+ *
287
+ * The manifest is advisory for listing and authoritative for the backend
288
+ * choice. Nothing is ever evicted automatically; deletion is the explicit
289
+ * `deleteDatabase()` API.
290
+ */
291
+ /** Where a database's data lives. */
292
+ type LocalDatabaseBackend = 'opfs' | 'indexeddb';
293
+ /** One manifest entry, as returned by `listDatabases()`. */
294
+ interface LocalDatabaseRecord {
295
+ /** The store name (`store.name`, plus the per-user suffix if namespaced). */
296
+ dbName: string;
297
+ /** The backend that holds the authoritative copy. */
298
+ backend: LocalDatabaseBackend;
299
+ /** OPFS pool holding the file (opfs backend only). */
300
+ poolName?: string;
301
+ /** Wall-clock milliseconds; informational only, never used for ordering. */
302
+ createdAt: number;
303
+ /** Wall-clock milliseconds of the last durable open; informational only. */
304
+ lastOpenedAt: number;
305
+ }
306
+
307
+ /**
308
+ * Explicit management of the local databases on this origin (W8a, NEW-STORE-7).
309
+ *
310
+ * Kora never evicts a database on its own: a full pool is prevented by
311
+ * per-database pools and capacity reservation, and removing data is always an
312
+ * explicit call that refuses while the database is open or still holds
313
+ * operations the server has not acknowledged.
314
+ */
315
+
316
+ /** Read-only access to a database handed to the unsynced-data check. */
317
+ interface LocalDatabaseReader {
318
+ dbName: string;
319
+ query<T>(sql: string, params?: unknown[]): Promise<T[]>;
320
+ }
321
+ /** Options for {@link deleteLocalDatabase}. */
322
+ interface DeleteLocalDatabaseOptions {
323
+ /** URL of the Kora SQLite worker (the same one the app's store uses). */
324
+ workerUrl: string | URL;
325
+ /**
326
+ * Decides whether the database still holds operations the server never
327
+ * acknowledged. Injected by the app layer, which knows its sync setup.
328
+ * When it returns true the delete is refused with {@link UnsyncedDataError}.
329
+ */
330
+ hasUnsyncedOperations?: (db: LocalDatabaseReader) => Promise<boolean>;
331
+ /** Delete even when unsynced operations exist (they are lost). */
332
+ force?: boolean;
333
+ /** Worker response timeout. Defaults to 30000ms. */
334
+ workerResponseTimeoutMs?: number;
335
+ }
336
+ /**
337
+ * Databases Kora has recorded on this origin (created or opened since W8a).
338
+ * A database still in the pre-W8a shared pool appears after its first open.
339
+ */
340
+ declare function listLocalDatabases(): Promise<LocalDatabaseRecord[]>;
341
+ /**
342
+ * Permanently delete a local database and its OPFS pool or IndexedDB copy.
343
+ *
344
+ * Refuses with {@link StorageInUseError} while any tab has the database open,
345
+ * and with {@link UnsyncedDataError} while `hasUnsyncedOperations` reports
346
+ * unsynced operations (unless `force`). Never touches any other database.
347
+ *
348
+ * @returns true when something was deleted, false when the database did not exist
349
+ */
350
+ declare function deleteLocalDatabase(dbName: string, options: DeleteLocalDatabaseOptions): Promise<boolean>;
351
+
352
+ /**
353
+ * The OPFS SAH pool that owns `dbName`. Database names that are already safe
354
+ * map to `kora-opfs-<dbName>`; anything sanitized or truncated gets a hash suffix
355
+ * so two distinct names can never share a pool.
356
+ *
357
+ * @example opfsPoolNameFor('school__user_alice') === 'kora-opfs-school__user_alice'
358
+ */
359
+ declare function opfsPoolNameFor(dbName: string): string;
360
+
361
+ export { type DeleteLocalDatabaseOptions, type LocalDatabaseBackend, type LocalDatabaseReader, type LocalDatabaseRecord, Mutex, SqliteWasmAdapter, type SqliteWasmAdapterOptions, type StorageRequestOptions, WorkerBridge, deleteLocalDatabase, listLocalDatabases, opfsPoolNameFor };
@@ -1,7 +1,7 @@
1
1
  import { KoraEventEmitter, SchemaDefinition } from '@korajs/core';
2
- import { S as StorageAdapter, a as StorageOpenState, T as Transaction, M as MigrationPlan } from '../types-BslDOTk0.js';
3
- import { W as WorkerBridge } from '../sqlite-wasm-channel-L6o3bWQK.js';
4
- export { a as WebWorkerBridge, b as WorkerRequest, c as WorkerResponse } from '../sqlite-wasm-channel-L6o3bWQK.js';
2
+ import { S as StorageAdapter, T as Transaction, a as StorageOpenState, M as MigrationPlan } from '../types-Dh-xyUjP.js';
3
+ import { W as WorkerBridge } from '../sqlite-wasm-channel-DVX93Uiq.js';
4
+ export { a as WebWorkerBridge, b as WorkerRequest, c as WorkerResponse, d as WorkerSendOptions, e as WorkerStatusEvent } from '../sqlite-wasm-channel-DVX93Uiq.js';
5
5
 
6
6
  /**
7
7
  * Async mutex for serializing access to a resource across `await` boundaries.
@@ -26,6 +26,19 @@ declare class Mutex {
26
26
  private createRelease;
27
27
  }
28
28
 
29
+ /** Per-request options for {@link SqliteWasmAdapter.execute} and `query`. */
30
+ interface StorageRequestOptions {
31
+ /**
32
+ * Stop waiting for the response. The leader may still apply a write that
33
+ * already reached it; retry with the same `requestId` to stay idempotent.
34
+ */
35
+ signal?: AbortSignal;
36
+ /**
37
+ * Stable id for this request. A follower tab's retry with the same id is
38
+ * answered from the leader's response cache instead of being applied twice.
39
+ */
40
+ requestId?: string;
41
+ }
29
42
  /**
30
43
  * Options for creating a SqliteWasmAdapter.
31
44
  */
@@ -36,7 +49,7 @@ interface SqliteWasmAdapterOptions {
36
49
  */
37
50
  bridge?: WorkerBridge;
38
51
  /**
39
- * Database name for persistence. Used as the OPFS file name or IDB key.
52
+ * Database name for persistence. Each database gets its own OPFS pool.
40
53
  */
41
54
  dbName?: string;
42
55
  /**
@@ -58,18 +71,66 @@ interface SqliteWasmAdapterOptions {
58
71
  */
59
72
  emitNonPersistentDiagnostic?: boolean;
60
73
  /**
61
- * When set, storage diagnostics are emitted here: `store:opfs-unavailable` when
62
- * persistence silently degraded to a non-persistent in-memory database, and
63
- * `store:db-name-collision` when another runtime on this origin was already
64
- * using this database name.
74
+ * Accept writes when durable OPFS storage could not be obtained and the
75
+ * database runs in memory. Defaults to false: a non-durable open or promotion
76
+ * emits the blocking `store:durability-lost` event and every write is refused
77
+ * with {@link StorageDurabilityError}, so data is never accepted and then lost
78
+ * on reload. Set true only for apps that can tolerate losing local writes.
79
+ */
80
+ allowNonDurable?: boolean;
81
+ /**
82
+ * For `createApp()`: skip the durability check at open only, because the caller
83
+ * inspects {@link SqliteWasmAdapter.getStorageOpenState} right after open and
84
+ * switches to a durable backend itself. A later leader promotion that loses
85
+ * durable storage is still enforced.
86
+ */
87
+ deferOpenDurabilityCheck?: boolean;
88
+ /**
89
+ * Release storage ownership when the page is frozen or hidden into the
90
+ * back/forward cache (`freeze` / `pagehide`), so a visible tab can take over
91
+ * instead of waiting on a tab that cannot run; ownership is re-acquired on
92
+ * `resume` / `pageshow`. Defaults to true.
93
+ */
94
+ releaseOnFreeze?: boolean;
95
+ /**
96
+ * When set, storage diagnostics are emitted here: `store:opfs-unavailable`,
97
+ * `store:durability-lost`, `store:storage-blocked` (another holder has the
98
+ * storage; the open waits), `store:storage-migrated` and
99
+ * `store:db-name-collision`.
65
100
  */
66
101
  emitter?: KoraEventEmitter;
102
+ /**
103
+ * @internal Used by the IndexedDB adapter: open an in-memory database that
104
+ * never touches OPFS (the IndexedDB adapter persists snapshots itself).
105
+ */
106
+ storage?: 'opfs' | 'memory';
107
+ /**
108
+ * @internal Do not read or write the storage manifest (the IndexedDB adapter's
109
+ * inner database and its one-shot OPFS reader manage the manifest themselves).
110
+ */
111
+ skipBackendRecord?: boolean;
112
+ /**
113
+ * @internal Used by the IndexedDB adapter (STORE-6): runs in one transaction on this
114
+ * tab's OWN worker right after it opened the database as storage leader (first open
115
+ * or promotion), before the worker serves any other tab. The IndexedDB adapter
116
+ * restores its persisted snapshot here, so a snapshot is only ever restored into a
117
+ * freshly created worker database and never over a leader's live one. Throwing fails
118
+ * the open (or the promotion, which then refuses writes).
119
+ */
120
+ onLeaderWorkerOpened?: (tx: Transaction) => Promise<void>;
67
121
  }
68
122
  /**
69
123
  * SQLite WASM adapter that communicates with a SQLite instance through a WorkerBridge.
70
124
  *
71
- * In browsers, the bridge is backed by a Web Worker running SQLite WASM with OPFS persistence.
72
- * In Node.js tests, the bridge is backed by MockWorkerBridge wrapping better-sqlite3.
125
+ * In browsers, one tab per database (the leader) runs a dedicated worker that
126
+ * owns the database's OPFS pool; other tabs reach that worker over a
127
+ * BroadcastChannel. In Node.js tests, the bridge is backed by MockWorkerBridge
128
+ * wrapping better-sqlite3.
129
+ *
130
+ * Storage is durable or loud, never silently in memory (W8a): another holder of
131
+ * the storage makes the open wait (reported with `store:storage-blocked`); only a
132
+ * runtime without OPFS SAH support gets a non-durable database, which refuses
133
+ * writes unless the app opted in with `allowNonDurable`.
73
134
  *
74
135
  * @example
75
136
  * ```typescript
@@ -83,6 +144,8 @@ interface SqliteWasmAdapterOptions {
83
144
  */
84
145
  declare class SqliteWasmAdapter implements StorageAdapter {
85
146
  private bridge;
147
+ /** The dedicated worker this tab runs while it is the database's leader. */
148
+ private ownWorker;
86
149
  private opened;
87
150
  private readonly mutex;
88
151
  private readonly injectedBridge;
@@ -97,10 +160,45 @@ declare class SqliteWasmAdapter implements StorageAdapter {
97
160
  /** Retained so a follower promoted to leader can re-open its own worker. */
98
161
  private schema;
99
162
  private promoting;
163
+ private closing;
164
+ private suspended;
165
+ /** Requests wait here while leadership changes hands (promotion, resume). */
166
+ private gate;
167
+ private removeLifecycleListeners;
168
+ private readonly allowNonDurable;
169
+ private readonly deferOpenDurabilityCheck;
170
+ private readonly releaseOnFreeze;
171
+ private readonly storage;
172
+ private readonly skipBackendRecord;
173
+ /** Set when this adapter lost (or never had) durable storage; writes are refused. */
174
+ private durabilityLoss;
175
+ private readonly onLeaderWorkerOpened;
100
176
  constructor(options?: SqliteWasmAdapterOptions);
101
177
  open(schema: SchemaDefinition): Promise<void>;
102
- private openDurableWorkerBridge;
178
+ /**
179
+ * Join the database as leader (own worker) or follower (RPC to the leader's
180
+ * worker), then open it. The open waits, without a timeout, while another
181
+ * holder still has the storage; the worker reports that as a blocking state.
182
+ */
183
+ private attachAndOpen;
184
+ /** Open the database in this tab's own worker and start serving followers. */
185
+ private openLeaderWorker;
186
+ /**
187
+ * Whether this runtime owns the database's worker: the storage leader in a browser,
188
+ * or any adapter with an injected bridge (tests, Node). A follower tab relays every
189
+ * request to the leader's worker (STORE-6).
190
+ */
191
+ isLeader(): boolean;
192
+ private checkedOpen;
103
193
  private openCurrentBridge;
194
+ private onWorkerEvent;
195
+ /**
196
+ * Leader-only bookkeeping after a durable open: report a legacy-pool
197
+ * migration, move an IndexedDB-held copy into OPFS explicitly (never two
198
+ * disjoint copies), and record OPFS as this database's backend.
199
+ */
200
+ private afterLeaderOpen;
201
+ private applyDumpThroughWorker;
104
202
  /**
105
203
  * Surface a `store:opfs-unavailable` diagnostic when the worker reported that
106
204
  * persistence silently degraded to an in-memory database, so a data-loss
@@ -109,18 +207,58 @@ declare class SqliteWasmAdapter implements StorageAdapter {
109
207
  */
110
208
  private reportStorageMode;
111
209
  getStorageOpenState(): StorageOpenState | null;
210
+ /**
211
+ * Durable or loud, never silent (NEW-STORE-6). When the open or a promotion
212
+ * ended on non-persistent storage and the app did not opt into that, emit the
213
+ * blocking `store:durability-lost` event and refuse every later write. A
214
+ * bridge that reports no mode (the Node mock) is not judged.
215
+ */
216
+ private enforceDurability;
217
+ private guardDurableWrite;
112
218
  private warnSharedWorkerDeprecated;
219
+ /**
220
+ * Close the database. A leader closes the database file and releases the OPFS
221
+ * pool inside its worker, terminates the worker, and only then releases the
222
+ * tab leader lock, so the next leader never races a still-running worker for
223
+ * the file handles (NEW-STORE-10). A follower just leaves.
224
+ */
113
225
  close(): Promise<void>;
226
+ /**
227
+ * Release everything this adapter holds, in ownership order: lifecycle
228
+ * listeners, then the worker (which holds the OPFS pool and its Web Lock),
229
+ * then the tab leader lock or queued promotion request.
230
+ */
231
+ private teardown;
114
232
  /**
115
233
  * Promotes a follower to leader after the previous leader released the storage
116
- * lock (its tab closed or crashed). The lock is only granted once the old leader
117
- * is gone, so opening our own worker here is safe from the OPFS single-writer
118
- * rule. In-flight follower RPCs are rejected and left to the caller / reactive
119
- * layer to retry against the new leader.
234
+ * lock (its tab closed, crashed, or suspended on freeze). The new worker waits
235
+ * on the pool's Web Lock and handles, so it never races the previous worker.
236
+ * Requests issued during the hand-off wait for it; requests that were in
237
+ * flight to the old leader fail with a retriable `BridgeTerminatedError`.
120
238
  */
121
239
  private promoteToLeader;
122
- execute(sql: string, params?: unknown[]): Promise<void>;
123
- query<T>(sql: string, params?: unknown[]): Promise<T[]>;
240
+ /**
241
+ * Page Lifecycle: a frozen or bfcached page cannot run, so it must not keep
242
+ * other tabs waiting on its storage. Leaders terminate their worker (freeing
243
+ * the OPFS pool) and then release the leader lock, so a visible tab is
244
+ * promoted; followers withdraw their queued lock request so a frozen tab is
245
+ * never granted leadership it cannot use. Never uses Web Locks `steal`.
246
+ */
247
+ private installLifecycleListeners;
248
+ /** Synchronous by necessity: a `freeze` handler gets no further turns. */
249
+ private suspend;
250
+ private resume;
251
+ /**
252
+ * Non-transactional write. Takes the same mutex as {@link transaction}, so it
253
+ * never lands inside this tab's open transaction and is never rolled back
254
+ * with it (STORE-8). Inside a transaction callback, use the `tx` handle.
255
+ */
256
+ execute(sql: string, params?: unknown[], options?: StorageRequestOptions): Promise<void>;
257
+ /**
258
+ * Non-transactional read. Waits for this tab's open transaction to finish, so
259
+ * it never observes uncommitted (possibly rolled-back) rows (STORE-8).
260
+ */
261
+ query<T>(sql: string, params?: unknown[], options?: StorageRequestOptions): Promise<T[]>;
124
262
  transaction(fn: (tx: Transaction) => Promise<void>): Promise<void>;
125
263
  migrate(from: number, to: number, migration: MigrationPlan): Promise<void>;
126
264
  /**
@@ -137,4 +275,87 @@ declare class SqliteWasmAdapter implements StorageAdapter {
137
275
  private sendChecked;
138
276
  }
139
277
 
140
- export { Mutex, SqliteWasmAdapter, type SqliteWasmAdapterOptions, WorkerBridge };
278
+ /**
279
+ * Kora-owned manifest of the local databases this origin created (W8a).
280
+ *
281
+ * One record per database name, kept in its own IndexedDB database so it is
282
+ * readable from any tab without opening the OPFS pools. It records which backend
283
+ * holds the database's data, which is how Kora keeps OPFS and IndexedDB from
284
+ * ever holding two disjoint copies: a backend switch is a recorded, explicit
285
+ * migration, never a silent fresh start (NEW-STORE-5, LMS-5c).
286
+ *
287
+ * The manifest is advisory for listing and authoritative for the backend
288
+ * choice. Nothing is ever evicted automatically; deletion is the explicit
289
+ * `deleteDatabase()` API.
290
+ */
291
+ /** Where a database's data lives. */
292
+ type LocalDatabaseBackend = 'opfs' | 'indexeddb';
293
+ /** One manifest entry, as returned by `listDatabases()`. */
294
+ interface LocalDatabaseRecord {
295
+ /** The store name (`store.name`, plus the per-user suffix if namespaced). */
296
+ dbName: string;
297
+ /** The backend that holds the authoritative copy. */
298
+ backend: LocalDatabaseBackend;
299
+ /** OPFS pool holding the file (opfs backend only). */
300
+ poolName?: string;
301
+ /** Wall-clock milliseconds; informational only, never used for ordering. */
302
+ createdAt: number;
303
+ /** Wall-clock milliseconds of the last durable open; informational only. */
304
+ lastOpenedAt: number;
305
+ }
306
+
307
+ /**
308
+ * Explicit management of the local databases on this origin (W8a, NEW-STORE-7).
309
+ *
310
+ * Kora never evicts a database on its own: a full pool is prevented by
311
+ * per-database pools and capacity reservation, and removing data is always an
312
+ * explicit call that refuses while the database is open or still holds
313
+ * operations the server has not acknowledged.
314
+ */
315
+
316
+ /** Read-only access to a database handed to the unsynced-data check. */
317
+ interface LocalDatabaseReader {
318
+ dbName: string;
319
+ query<T>(sql: string, params?: unknown[]): Promise<T[]>;
320
+ }
321
+ /** Options for {@link deleteLocalDatabase}. */
322
+ interface DeleteLocalDatabaseOptions {
323
+ /** URL of the Kora SQLite worker (the same one the app's store uses). */
324
+ workerUrl: string | URL;
325
+ /**
326
+ * Decides whether the database still holds operations the server never
327
+ * acknowledged. Injected by the app layer, which knows its sync setup.
328
+ * When it returns true the delete is refused with {@link UnsyncedDataError}.
329
+ */
330
+ hasUnsyncedOperations?: (db: LocalDatabaseReader) => Promise<boolean>;
331
+ /** Delete even when unsynced operations exist (they are lost). */
332
+ force?: boolean;
333
+ /** Worker response timeout. Defaults to 30000ms. */
334
+ workerResponseTimeoutMs?: number;
335
+ }
336
+ /**
337
+ * Databases Kora has recorded on this origin (created or opened since W8a).
338
+ * A database still in the pre-W8a shared pool appears after its first open.
339
+ */
340
+ declare function listLocalDatabases(): Promise<LocalDatabaseRecord[]>;
341
+ /**
342
+ * Permanently delete a local database and its OPFS pool or IndexedDB copy.
343
+ *
344
+ * Refuses with {@link StorageInUseError} while any tab has the database open,
345
+ * and with {@link UnsyncedDataError} while `hasUnsyncedOperations` reports
346
+ * unsynced operations (unless `force`). Never touches any other database.
347
+ *
348
+ * @returns true when something was deleted, false when the database did not exist
349
+ */
350
+ declare function deleteLocalDatabase(dbName: string, options: DeleteLocalDatabaseOptions): Promise<boolean>;
351
+
352
+ /**
353
+ * The OPFS SAH pool that owns `dbName`. Database names that are already safe
354
+ * map to `kora-opfs-<dbName>`; anything sanitized or truncated gets a hash suffix
355
+ * so two distinct names can never share a pool.
356
+ *
357
+ * @example opfsPoolNameFor('school__user_alice') === 'kora-opfs-school__user_alice'
358
+ */
359
+ declare function opfsPoolNameFor(dbName: string): string;
360
+
361
+ export { type DeleteLocalDatabaseOptions, type LocalDatabaseBackend, type LocalDatabaseReader, type LocalDatabaseRecord, Mutex, SqliteWasmAdapter, type SqliteWasmAdapterOptions, type StorageRequestOptions, WorkerBridge, deleteLocalDatabase, listLocalDatabases, opfsPoolNameFor };