@korajs/store 1.0.0-beta.12 → 1.0.0-beta.14

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 +856 -63
  15. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm-worker.js +529 -63
  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-IHKVMUAM.js +25 -0
  25. package/dist/chunk-5AQSGMJV.js +5471 -0
  26. package/dist/chunk-5AQSGMJV.js.map +1 -0
  27. package/dist/chunk-5JMGMPUE.js +591 -0
  28. package/dist/chunk-5JMGMPUE.js.map +1 -0
  29. package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
  30. package/dist/chunk-A6GFH5IG.js +36 -0
  31. package/dist/chunk-A6GFH5IG.js.map +1 -0
  32. package/dist/chunk-BILSA6XZ.js +608 -0
  33. package/dist/chunk-BILSA6XZ.js.map +1 -0
  34. package/dist/chunk-BQSIL6TT.js +1095 -0
  35. package/dist/chunk-BQSIL6TT.js.map +1 -0
  36. package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
  37. package/dist/chunk-J5AGXF3D.js.map +1 -0
  38. package/dist/chunk-KK3GBONL.js +112 -0
  39. package/dist/chunk-KK3GBONL.js.map +1 -0
  40. package/dist/chunk-KTVNANUH.js +876 -0
  41. package/dist/chunk-KTVNANUH.js.map +1 -0
  42. package/dist/chunk-QW4UU7NG.js +1003 -0
  43. package/dist/chunk-QW4UU7NG.js.map +1 -0
  44. package/dist/chunk-SRHDJF5B.js +186 -0
  45. package/dist/chunk-SRHDJF5B.js.map +1 -0
  46. package/dist/chunk-UC2LGFPV.js +810 -0
  47. package/dist/chunk-UC2LGFPV.js.map +1 -0
  48. package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
  49. package/dist/index.cjs +9533 -4246
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +1143 -110
  52. package/dist/index.d.ts +1143 -110
  53. package/dist/index.js +298 -3191
  54. package/dist/index.js.map +1 -1
  55. package/dist/internal.cjs +581 -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-MNDPR557.js +299 -0
  61. package/dist/restore-MNDPR557.js.map +1 -0
  62. package/dist/sql-builder-D0tC3sv9.d.cts +615 -0
  63. package/dist/sql-builder-D5UMzHEo.d.ts +615 -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-QCCNBYCU.js +12 -0
  67. package/dist/store-HNGX4KTT.js +16 -0
  68. package/dist/store-HNGX4KTT.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-IHKVMUAM.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-QCCNBYCU.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { KoraEventEmitter, SchemaDefinition } from '@korajs/core';
2
- import { S as StorageAdapter, T as Transaction, M as MigrationPlan, a as StorageOpenState } from '../types-BslDOTk0.cjs';
3
- import { W as WorkerBridge } from '../sqlite-wasm-channel-L6o3bWQK.cjs';
2
+ import { S as StorageAdapter, T as Transaction, M as MigrationPlan, a as StorageOpenState } from '../types-Dh-xyUjP.cjs';
3
+ import { W as WorkerBridge } from '../sqlite-wasm-channel-DVX93Uiq.cjs';
4
4
 
5
5
  /**
6
6
  * Options for creating an IndexedDbAdapter.
@@ -50,8 +50,18 @@ declare class IndexedDbAdapter implements StorageAdapter {
50
50
  private readonly emitter;
51
51
  private readonly scheduler;
52
52
  private storageOpenState;
53
+ private readonly options;
54
+ /** True in browsers (a real worker), where the backend choice is recorded. */
55
+ private readonly usesRealWorker;
53
56
  constructor(options?: IndexedDbAdapterOptions);
54
57
  open(schema: SchemaDefinition): Promise<void>;
58
+ /**
59
+ * Read the OPFS copy of this database through a short-lived SQLite WASM
60
+ * adapter (as leader, or as a follower of a tab that has it open). Refuses
61
+ * loudly when OPFS cannot be read, instead of starting an empty copy.
62
+ */
63
+ private readOpfsCopy;
64
+ private applyDump;
55
65
  close(): Promise<void>;
56
66
  execute(sql: string, params?: unknown[]): Promise<void>;
57
67
  query<T>(sql: string, params?: unknown[]): Promise<T[]>;
@@ -63,9 +73,25 @@ declare class IndexedDbAdapter implements StorageAdapter {
63
73
  * Useful before tab unload or in tests.
64
74
  */
65
75
  flushPersistence(): Promise<void>;
76
+ /**
77
+ * Durability barrier (RT-35): resolves once every write committed before the call is
78
+ * in a snapshot persisted to IndexedDB, and rejects when the snapshot cannot be
79
+ * written. Sync calls it before an operation leaves the device, so the server never
80
+ * holds an operation this device could lose on reload.
81
+ */
82
+ ensureDurable(): Promise<void>;
66
83
  private writeSnapshot;
67
84
  private handlePersistenceError;
68
- private restoreFromDumpFallback;
85
+ /**
86
+ * Restore the persisted snapshot into the worker database unless one was restored
87
+ * into it already (this adapter or another one sharing the worker). The binary
88
+ * snapshot (Node / test workers that support export) is imported whole; the JSON dump
89
+ * (browsers) is restored in one transaction.
90
+ */
91
+ private restoreIfFresh;
92
+ /** In `tx`: restore the JSON dump when no snapshot was restored into this database. */
93
+ private restoreDumpIfFresh;
94
+ /** A consistent snapshot: every table is read inside one transaction (STORE-7). */
69
95
  private exportDump;
70
96
  }
71
97
 
@@ -1,6 +1,6 @@
1
1
  import { KoraEventEmitter, SchemaDefinition } from '@korajs/core';
2
- import { S as StorageAdapter, T as Transaction, M as MigrationPlan, a as StorageOpenState } from '../types-BslDOTk0.js';
3
- import { W as WorkerBridge } from '../sqlite-wasm-channel-L6o3bWQK.js';
2
+ import { S as StorageAdapter, T as Transaction, M as MigrationPlan, a as StorageOpenState } from '../types-Dh-xyUjP.js';
3
+ import { W as WorkerBridge } from '../sqlite-wasm-channel-DVX93Uiq.js';
4
4
 
5
5
  /**
6
6
  * Options for creating an IndexedDbAdapter.
@@ -50,8 +50,18 @@ declare class IndexedDbAdapter implements StorageAdapter {
50
50
  private readonly emitter;
51
51
  private readonly scheduler;
52
52
  private storageOpenState;
53
+ private readonly options;
54
+ /** True in browsers (a real worker), where the backend choice is recorded. */
55
+ private readonly usesRealWorker;
53
56
  constructor(options?: IndexedDbAdapterOptions);
54
57
  open(schema: SchemaDefinition): Promise<void>;
58
+ /**
59
+ * Read the OPFS copy of this database through a short-lived SQLite WASM
60
+ * adapter (as leader, or as a follower of a tab that has it open). Refuses
61
+ * loudly when OPFS cannot be read, instead of starting an empty copy.
62
+ */
63
+ private readOpfsCopy;
64
+ private applyDump;
55
65
  close(): Promise<void>;
56
66
  execute(sql: string, params?: unknown[]): Promise<void>;
57
67
  query<T>(sql: string, params?: unknown[]): Promise<T[]>;
@@ -63,9 +73,25 @@ declare class IndexedDbAdapter implements StorageAdapter {
63
73
  * Useful before tab unload or in tests.
64
74
  */
65
75
  flushPersistence(): Promise<void>;
76
+ /**
77
+ * Durability barrier (RT-35): resolves once every write committed before the call is
78
+ * in a snapshot persisted to IndexedDB, and rejects when the snapshot cannot be
79
+ * written. Sync calls it before an operation leaves the device, so the server never
80
+ * holds an operation this device could lose on reload.
81
+ */
82
+ ensureDurable(): Promise<void>;
66
83
  private writeSnapshot;
67
84
  private handlePersistenceError;
68
- private restoreFromDumpFallback;
85
+ /**
86
+ * Restore the persisted snapshot into the worker database unless one was restored
87
+ * into it already (this adapter or another one sharing the worker). The binary
88
+ * snapshot (Node / test workers that support export) is imported whole; the JSON dump
89
+ * (browsers) is restored in one transaction.
90
+ */
91
+ private restoreIfFresh;
92
+ /** In `tx`: restore the JSON dump when no snapshot was restored into this database. */
93
+ private restoreDumpIfFresh;
94
+ /** A consistent snapshot: every table is read inside one transaction (STORE-7). */
69
95
  private exportDump;
70
96
  }
71
97
 
@@ -1,15 +1,30 @@
1
1
  import {
2
- SqliteWasmAdapter
3
- } from "../chunk-RJIEKEN4.js";
4
- import "../chunk-VDBJQ75K.js";
2
+ STORED_SCHEMA_VERSION_SQL,
3
+ storedSchemaVersion
4
+ } from "../chunk-A6GFH5IG.js";
5
+ import {
6
+ SqliteWasmAdapter,
7
+ deleteSnapshotFromIndexedDB,
8
+ exportDump,
9
+ isIndexedDbQuotaError,
10
+ isManifestAvailable,
11
+ loadDumpFromIndexedDB,
12
+ loadFromIndexedDB,
13
+ readManifestRecord,
14
+ recordDatabase,
15
+ restoreDumpStatements,
16
+ saveDumpToIndexedDB,
17
+ saveToIndexedDB
18
+ } from "../chunk-BQSIL6TT.js";
19
+ import "../chunk-KK3GBONL.js";
5
20
  import "../chunk-TVJEC5S2.js";
21
+ import "../chunk-5JMGMPUE.js";
6
22
  import {
7
23
  AdapterError,
8
- PersistenceError
9
- } from "../chunk-TUR7RGEP.js";
10
-
11
- // src/adapters/indexeddb-adapter.ts
12
- import { quoteIdent } from "@korajs/core";
24
+ PersistenceError,
25
+ SchemaVersionAheadError,
26
+ StorageBackendMismatchError
27
+ } from "../chunk-SRHDJF5B.js";
13
28
 
14
29
  // src/adapters/indexeddb-persistence-scheduler.ts
15
30
  var DEFAULT_DEBOUNCE_MS = 500;
@@ -20,6 +35,15 @@ var IndexedDbPersistenceScheduler = class {
20
35
  timer = null;
21
36
  inFlight = null;
22
37
  disposed = false;
38
+ /**
39
+ * Mutation generations (RT-35): `scheduled` counts schedule() calls (one per committed
40
+ * write), `persisted` is the generation the last SUCCESSFUL snapshot covered. A
41
+ * snapshot covers every generation scheduled before it started.
42
+ */
43
+ scheduledGeneration = 0;
44
+ persistedGeneration = 0;
45
+ /** Error of the last snapshot attempt, cleared by the next success. */
46
+ lastFlushError = null;
23
47
  onVisibilityChange;
24
48
  constructor(options) {
25
49
  this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
@@ -37,6 +61,7 @@ var IndexedDbPersistenceScheduler = class {
37
61
  /** Schedule a debounced snapshot write. */
38
62
  schedule() {
39
63
  if (this.disposed) return;
64
+ this.scheduledGeneration++;
40
65
  if (this.debounceMs <= 0) {
41
66
  void this.flushNow();
42
67
  return;
@@ -49,22 +74,75 @@ var IndexedDbPersistenceScheduler = class {
49
74
  void this.flushNow();
50
75
  }, this.debounceMs);
51
76
  }
52
- /** Cancel any pending debounce and persist immediately. */
77
+ /**
78
+ * Cancel any pending debounce and persist immediately. Resolves only when a snapshot
79
+ * taken after every write scheduled so far was written (STORE-7): a snapshot already in
80
+ * flight may predate the latest writes, so the call waits for it and then writes again
81
+ * while anything is still dirty. A failed snapshot ends the loop (the failure is
82
+ * reported through `onError`; {@link flushBarrier} is the variant that rejects).
83
+ */
53
84
  async flushNow() {
54
85
  if (this.disposed) return;
55
86
  if (this.timer !== null) {
56
87
  clearTimeout(this.timer);
57
88
  this.timer = null;
58
89
  }
59
- if (this.inFlight) {
60
- await this.inFlight;
61
- return;
90
+ let mustRun = this.inFlight === null;
91
+ for (; ; ) {
92
+ if (this.inFlight) {
93
+ await this.inFlight;
94
+ continue;
95
+ }
96
+ if (!mustRun && this.persistedGeneration >= this.scheduledGeneration) return;
97
+ mustRun = false;
98
+ this.inFlight = this.runFlush();
99
+ try {
100
+ await this.inFlight;
101
+ } finally {
102
+ this.inFlight = null;
103
+ }
104
+ if (this.lastFlushError !== null) return;
62
105
  }
63
- this.inFlight = this.runFlush();
64
- try {
65
- await this.inFlight;
66
- } finally {
67
- this.inFlight = null;
106
+ }
107
+ /** Whether a write was scheduled that no successful snapshot covers yet. */
108
+ isDirty() {
109
+ return this.persistedGeneration < this.scheduledGeneration;
110
+ }
111
+ /**
112
+ * Durability barrier (RT-35): resolve once every write scheduled before this call is
113
+ * in a snapshot persisted to IndexedDB. A snapshot already in flight may predate the
114
+ * latest write, so the barrier waits for it and then writes a fresh one when needed.
115
+ * Rejects when the snapshot cannot be written: the caller must treat the writes as
116
+ * not durable (for example, not upload them yet).
117
+ */
118
+ async flushBarrier() {
119
+ const target = this.scheduledGeneration;
120
+ if (this.persistedGeneration >= target) return;
121
+ if (this.disposed) {
122
+ throw new Error("IndexedDB persistence was disposed before the writes were persisted");
123
+ }
124
+ if (this.timer !== null) {
125
+ clearTimeout(this.timer);
126
+ this.timer = null;
127
+ }
128
+ for (let round = 0; round < 3 && this.persistedGeneration < target; round++) {
129
+ if (this.inFlight) {
130
+ try {
131
+ await this.inFlight;
132
+ } catch {
133
+ }
134
+ continue;
135
+ }
136
+ this.inFlight = this.runFlush();
137
+ try {
138
+ await this.inFlight;
139
+ } finally {
140
+ this.inFlight = null;
141
+ }
142
+ if (this.persistedGeneration < target) break;
143
+ }
144
+ if (this.persistedGeneration < target) {
145
+ throw this.lastFlushError instanceof Error ? this.lastFlushError : new Error("IndexedDB snapshot was not persisted");
68
146
  }
69
147
  }
70
148
  dispose() {
@@ -78,165 +156,18 @@ var IndexedDbPersistenceScheduler = class {
78
156
  }
79
157
  }
80
158
  async runFlush() {
159
+ const generation = this.scheduledGeneration;
81
160
  try {
82
161
  await this.flush();
162
+ this.persistedGeneration = Math.max(this.persistedGeneration, generation);
163
+ this.lastFlushError = null;
83
164
  } catch (error) {
165
+ this.lastFlushError = error;
84
166
  this.onError?.(error);
85
167
  }
86
168
  }
87
169
  };
88
170
 
89
- // src/adapters/sqlite-wasm-persistence.ts
90
- var IDB_DATABASE_NAME = "kora-persistence";
91
- var IDB_STORE_NAME = "databases";
92
- var IDB_VERSION = 1;
93
- var DUMP_SUFFIX = "::dump";
94
- function isIndexedDbQuotaError(error) {
95
- if (error instanceof DOMException && error.name === "QuotaExceededError") {
96
- return true;
97
- }
98
- if (error instanceof PersistenceError && error.context?.quotaExceeded === true) {
99
- return true;
100
- }
101
- if (error instanceof Error && /quota/i.test(error.message)) {
102
- return true;
103
- }
104
- return false;
105
- }
106
- function rejectWithIdbError(reject, message, context, event) {
107
- const domError = event.target?.error;
108
- if (domError && isIndexedDbQuotaError(domError)) {
109
- reject(
110
- new PersistenceError(message, {
111
- ...context,
112
- quotaExceeded: true,
113
- cause: domError.message
114
- })
115
- );
116
- return;
117
- }
118
- reject(new PersistenceError(message, context));
119
- }
120
- function openIdb() {
121
- return new Promise((resolve, reject) => {
122
- const request = indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION);
123
- request.onupgradeneeded = () => {
124
- const db = request.result;
125
- if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
126
- db.createObjectStore(IDB_STORE_NAME);
127
- }
128
- };
129
- request.onsuccess = () => resolve(request.result);
130
- request.onerror = () => reject(
131
- new PersistenceError(
132
- `Failed to open IndexedDB: ${request.error?.message ?? "unknown error"}`,
133
- { database: IDB_DATABASE_NAME }
134
- )
135
- );
136
- });
137
- }
138
- async function saveToIndexedDB(dbName, data) {
139
- const idb = await openIdb();
140
- try {
141
- await new Promise((resolve, reject) => {
142
- const tx = idb.transaction(IDB_STORE_NAME, "readwrite");
143
- const store = tx.objectStore(IDB_STORE_NAME);
144
- store.put(data, dbName);
145
- tx.oncomplete = () => resolve();
146
- tx.onerror = (event) => rejectWithIdbError(
147
- reject,
148
- `Failed to save database "${dbName}" to IndexedDB`,
149
- { dbName },
150
- event
151
- );
152
- });
153
- } finally {
154
- idb.close();
155
- }
156
- }
157
- async function saveDumpToIndexedDB(dbName, dump) {
158
- const idb = await openIdb();
159
- try {
160
- await new Promise((resolve, reject) => {
161
- const tx = idb.transaction(IDB_STORE_NAME, "readwrite");
162
- const store = tx.objectStore(IDB_STORE_NAME);
163
- store.put(dump, `${dbName}${DUMP_SUFFIX}`);
164
- tx.oncomplete = () => resolve();
165
- tx.onerror = (event) => rejectWithIdbError(
166
- reject,
167
- `Failed to save dump for database "${dbName}"`,
168
- { dbName },
169
- event
170
- );
171
- });
172
- } finally {
173
- idb.close();
174
- }
175
- }
176
- async function loadFromIndexedDB(dbName) {
177
- const idb = await openIdb();
178
- try {
179
- return await new Promise((resolve, reject) => {
180
- const tx = idb.transaction(IDB_STORE_NAME, "readonly");
181
- const store = tx.objectStore(IDB_STORE_NAME);
182
- const request = store.get(dbName);
183
- request.onsuccess = () => {
184
- const result = request.result;
185
- if (result instanceof Uint8Array) {
186
- resolve(result);
187
- } else if (result) {
188
- resolve(new Uint8Array(result));
189
- } else {
190
- resolve(null);
191
- }
192
- };
193
- request.onerror = () => reject(
194
- new PersistenceError(`Failed to load database "${dbName}" from IndexedDB`, { dbName })
195
- );
196
- });
197
- } finally {
198
- idb.close();
199
- }
200
- }
201
- async function loadDumpFromIndexedDB(dbName) {
202
- const idb = await openIdb();
203
- try {
204
- return await new Promise((resolve, reject) => {
205
- const tx = idb.transaction(IDB_STORE_NAME, "readonly");
206
- const store = tx.objectStore(IDB_STORE_NAME);
207
- const request = store.get(`${dbName}${DUMP_SUFFIX}`);
208
- request.onsuccess = () => {
209
- resolve(request.result ?? null);
210
- };
211
- request.onerror = () => reject(
212
- new PersistenceError(`Failed to load dump for database "${dbName}" from IndexedDB`, {
213
- dbName
214
- })
215
- );
216
- });
217
- } finally {
218
- idb.close();
219
- }
220
- }
221
- async function deleteSnapshotFromIndexedDB(dbName) {
222
- const idb = await openIdb();
223
- try {
224
- await new Promise((resolve, reject) => {
225
- const tx = idb.transaction(IDB_STORE_NAME, "readwrite");
226
- const store = tx.objectStore(IDB_STORE_NAME);
227
- store.delete(dbName);
228
- tx.oncomplete = () => resolve();
229
- tx.onerror = () => reject(
230
- new PersistenceError(`Failed to delete database snapshot "${dbName}" from IndexedDB`, {
231
- dbName
232
- })
233
- );
234
- });
235
- } finally {
236
- idb.close();
237
- }
238
- }
239
-
240
171
  // src/adapters/indexeddb-adapter.ts
241
172
  var IndexedDbAdapter = class {
242
173
  inner;
@@ -244,14 +175,29 @@ var IndexedDbAdapter = class {
244
175
  emitter;
245
176
  scheduler;
246
177
  storageOpenState = null;
178
+ options;
179
+ /** True in browsers (a real worker), where the backend choice is recorded. */
180
+ usesRealWorker;
247
181
  constructor(options = {}) {
182
+ this.options = options;
248
183
  this.dbName = options.dbName ?? "kora-db";
249
184
  this.emitter = options.emitter;
185
+ this.usesRealWorker = !options.bridge && options.workerUrl !== void 0;
250
186
  this.inner = new SqliteWasmAdapter({
251
187
  bridge: options.bridge,
252
188
  workerUrl: options.workerUrl,
253
189
  dbName: this.dbName,
254
- workerResponseTimeoutMs: options.workerResponseTimeoutMs
190
+ workerResponseTimeoutMs: options.workerResponseTimeoutMs,
191
+ // The inner SQLite database is in memory by design: this adapter makes it
192
+ // durable by persisting snapshots to IndexedDB, so the inner adapter must
193
+ // not refuse writes as non-durable, and must never touch OPFS (which would
194
+ // create a second, disjoint copy of the data).
195
+ allowNonDurable: true,
196
+ storage: "memory",
197
+ skipBackendRecord: true,
198
+ // A browser leader restores the snapshot into its own fresh worker database
199
+ // before serving other tabs (first open and promotion, STORE-6).
200
+ onLeaderWorkerOpened: (tx) => this.restoreDumpIfFresh(tx)
255
201
  });
256
202
  this.scheduler = new IndexedDbPersistenceScheduler({
257
203
  debounceMs: options.persistenceDebounceMs,
@@ -260,19 +206,91 @@ var IndexedDbAdapter = class {
260
206
  });
261
207
  }
262
208
  async open(schema) {
209
+ const tracksBackend = this.usesRealWorker && isManifestAvailable();
210
+ const recorded = tracksBackend ? await readManifestRecord(this.dbName).catch(() => null) : null;
211
+ const carried = recorded?.backend === "opfs" ? await this.readOpfsCopy(schema) : null;
263
212
  await this.inner.open(schema);
264
213
  this.storageOpenState = { persistent: true, mode: "indexeddb" };
265
- const persisted = await loadFromIndexedDB(this.dbName);
266
- if (!persisted) {
267
- await this.restoreFromDumpFallback();
214
+ if (carried && this.inner.isLeader()) {
215
+ await this.inner.transaction(async (tx) => {
216
+ await markRestored(tx);
217
+ });
218
+ await this.applyDump(carried);
219
+ await this.scheduler.flushNow();
220
+ await recordDatabase({ dbName: this.dbName, backend: "indexeddb" });
221
+ this.emitter?.emit({
222
+ type: "store:storage-migrated",
223
+ dbName: this.dbName,
224
+ from: "opfs",
225
+ to: "indexeddb",
226
+ message: `Database "${this.dbName}" moved from OPFS into IndexedDB because OPFS is not usable in this session.`
227
+ });
268
228
  return;
269
229
  }
230
+ if (this.inner.isLeader()) {
231
+ await this.restoreIfFresh();
232
+ }
233
+ const stored = storedSchemaVersion(
234
+ await this.inner.query(STORED_SCHEMA_VERSION_SQL)
235
+ );
236
+ if (stored > schema.version) {
237
+ this.scheduler.dispose();
238
+ await this.inner.close().catch(() => void 0);
239
+ throw new SchemaVersionAheadError(this.dbName, stored, schema.version);
240
+ }
241
+ if (tracksBackend) {
242
+ await recordDatabase({ dbName: this.dbName, backend: "indexeddb" }).catch(
243
+ (error) => console.warn(`[kora] Could not record the storage backend of "${this.dbName}":`, error)
244
+ );
245
+ }
246
+ }
247
+ /**
248
+ * Read the OPFS copy of this database through a short-lived SQLite WASM
249
+ * adapter (as leader, or as a follower of a tab that has it open). Refuses
250
+ * loudly when OPFS cannot be read, instead of starting an empty copy.
251
+ */
252
+ async readOpfsCopy(schema) {
253
+ const reader = new SqliteWasmAdapter({
254
+ dbName: this.dbName,
255
+ workerUrl: this.options.workerUrl,
256
+ workerResponseTimeoutMs: this.options.workerResponseTimeoutMs,
257
+ skipBackendRecord: true,
258
+ releaseOnFreeze: false
259
+ });
270
260
  try {
271
- await this.inner.importDatabase(persisted);
272
- } catch {
273
- await this.restoreFromDumpFallback();
261
+ await reader.open(schema);
262
+ if (reader.getStorageOpenState()?.persistent !== true) {
263
+ throw new StorageBackendMismatchError(
264
+ this.dbName,
265
+ "OPFS",
266
+ "IndexedDB",
267
+ `OPFS reported ${reader.getStorageOpenState()?.fallbackReason ?? "no durable storage"}`
268
+ );
269
+ }
270
+ let dump = { tables: [] };
271
+ await reader.transaction(async (tx) => {
272
+ dump = await exportDump((sql, params) => tx.query(sql, params));
273
+ });
274
+ return dump;
275
+ } catch (error) {
276
+ if (error instanceof StorageBackendMismatchError) throw error;
277
+ throw new StorageBackendMismatchError(
278
+ this.dbName,
279
+ "OPFS",
280
+ "IndexedDB",
281
+ error instanceof Error ? error.message : String(error)
282
+ );
283
+ } finally {
284
+ await reader.close().catch(() => void 0);
274
285
  }
275
286
  }
287
+ async applyDump(dump) {
288
+ await this.inner.transaction(async (tx) => {
289
+ for (const statement of restoreDumpStatements(dump)) {
290
+ await tx.execute(statement.sql, statement.params);
291
+ }
292
+ });
293
+ }
276
294
  async close() {
277
295
  await this.scheduler.flushNow();
278
296
  this.scheduler.dispose();
@@ -303,6 +321,15 @@ var IndexedDbAdapter = class {
303
321
  async flushPersistence() {
304
322
  await this.scheduler.flushNow();
305
323
  }
324
+ /**
325
+ * Durability barrier (RT-35): resolves once every write committed before the call is
326
+ * in a snapshot persisted to IndexedDB, and rejects when the snapshot cannot be
327
+ * written. Sync calls it before an operation leaves the device, so the server never
328
+ * holds an operation this device could lose on reload.
329
+ */
330
+ async ensureDurable() {
331
+ await this.scheduler.flushBarrier();
332
+ }
306
333
  async writeSnapshot() {
307
334
  const dump = await this.exportDump();
308
335
  await saveDumpToIndexedDB(this.dbName, dump);
@@ -334,59 +361,58 @@ var IndexedDbAdapter = class {
334
361
  code: quotaExceeded ? "QUOTA_EXCEEDED" : code
335
362
  });
336
363
  }
337
- async restoreFromDumpFallback() {
338
- const dump = await loadDumpFromIndexedDB(this.dbName);
339
- if (!dump) return;
340
- for (const table of dump.tables) {
341
- const name = ensureSafeIdentifier(table.name);
342
- await this.inner.execute(`DELETE FROM ${quoteIdent(name)}`);
343
- if (table.rows.length === 0) continue;
344
- for (const row of table.rows) {
345
- const columns = table.columns.filter(
346
- (column) => Object.prototype.hasOwnProperty.call(row, column)
347
- );
348
- if (columns.length === 0) continue;
349
- const placeholders = columns.map(() => "?").join(", ");
350
- const quotedColumns = columns.map((column) => quoteIdent(ensureSafeIdentifier(column))).join(", ");
351
- const values = columns.map((column) => row[column]);
352
- await this.inner.execute(
353
- `INSERT INTO ${quoteIdent(name)} (${quotedColumns}) VALUES (${placeholders})`,
354
- values
355
- );
364
+ /**
365
+ * Restore the persisted snapshot into the worker database unless one was restored
366
+ * into it already (this adapter or another one sharing the worker). The binary
367
+ * snapshot (Node / test workers that support export) is imported whole; the JSON dump
368
+ * (browsers) is restored in one transaction.
369
+ */
370
+ async restoreIfFresh() {
371
+ const persisted = await loadFromIndexedDB(this.dbName);
372
+ if (persisted) {
373
+ const fresh = { value: false };
374
+ await this.inner.transaction(async (tx) => {
375
+ fresh.value = !await isRestored(tx);
376
+ });
377
+ if (!fresh.value) return;
378
+ try {
379
+ await this.inner.importDatabase(persisted);
380
+ await this.inner.transaction(markRestored);
381
+ return;
382
+ } catch {
356
383
  }
357
384
  }
385
+ await this.inner.transaction((tx) => this.restoreDumpIfFresh(tx));
358
386
  }
359
- async exportDump() {
360
- const tableRows = await this.inner.query(
361
- "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
362
- );
363
- const tables = [];
364
- for (const tableRow of tableRows) {
365
- const tableName = ensureSafeIdentifier(tableRow.name);
366
- const columns = await this.inner.query(
367
- `PRAGMA table_info(${quoteIdent(tableName)})`
368
- );
369
- const columnNames = columns.map((column) => column.name);
370
- const rows = await this.inner.query(
371
- `SELECT * FROM ${quoteIdent(tableName)}`
372
- );
373
- tables.push({
374
- name: tableName,
375
- columns: columnNames,
376
- rows
377
- });
387
+ /** In `tx`: restore the JSON dump when no snapshot was restored into this database. */
388
+ async restoreDumpIfFresh(tx) {
389
+ if (await isRestored(tx)) return;
390
+ await markRestored(tx);
391
+ const dump = await loadDumpFromIndexedDB(this.dbName);
392
+ if (!dump) return;
393
+ for (const statement of restoreDumpStatements(dump)) {
394
+ await tx.execute(statement.sql, statement.params);
378
395
  }
379
- return { tables };
380
396
  }
381
- };
382
- function ensureSafeIdentifier(identifier) {
383
- if (!/^[a-zA-Z0-9_]+$/.test(identifier)) {
384
- throw new PersistenceError(`Unsafe SQL identifier: ${identifier}`, {
385
- code: "UNSAFE_IDENTIFIER",
386
- identifier
397
+ /** A consistent snapshot: every table is read inside one transaction (STORE-7). */
398
+ async exportDump() {
399
+ let dump = { tables: [] };
400
+ await this.inner.transaction(async (tx) => {
401
+ dump = await exportDump((sql, params) => tx.query(sql, params));
387
402
  });
403
+ return dump;
388
404
  }
389
- return identifier;
405
+ };
406
+ var RESTORED_MARKER = "_kora_idb_restored";
407
+ async function isRestored(tx) {
408
+ const rows = await tx.query(
409
+ "SELECT name FROM sqlite_temp_master WHERE type = 'table' AND name = ?",
410
+ [RESTORED_MARKER]
411
+ );
412
+ return rows.length > 0;
413
+ }
414
+ async function markRestored(tx) {
415
+ await tx.execute(`CREATE TEMP TABLE IF NOT EXISTS ${RESTORED_MARKER} (restored INTEGER)`);
390
416
  }
391
417
  function isUnsupportedWorkerExport(error) {
392
418
  return error instanceof AdapterError && error.context?.code === "EXPORT_NOT_SUPPORTED";