@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
package/README.md CHANGED
@@ -1,22 +1,24 @@
1
1
  # @korajs/store
2
2
 
3
- Local storage engine for Kora.js. Supports SQLite WASM with OPFS persistence, IndexedDB fallback, and native SQLite for server-side use. Provides CRUD operations, reactive queries, and subscriptions.
3
+ The local data layer of Kora.js: SQLite WASM on OPFS (in a worker), an IndexedDB fallback, native
4
+ SQLite for Node.js, the operation log, the per-field fold that materializes records, reactive
5
+ queries, transactions, sequences and blob storage.
4
6
 
5
- > Most developers don't install this directly. Use [`korajs`](https://www.npmjs.com/package/korajs) instead.
7
+ > Most apps do not install this directly: `createApp()` from
8
+ > [`korajs`](https://www.npmjs.com/package/korajs) creates and opens the store, and you use it
9
+ > through `app.<collection>`.
6
10
 
7
11
  ## Install
8
12
 
9
13
  ```bash
10
- pnpm add @korajs/store
14
+ pnpm add @korajs/store@beta
11
15
  ```
12
16
 
13
- ## Usage
14
-
15
- ### Create a Store
17
+ ## Usage through createApp
16
18
 
19
+ <!-- docs-check: standalone -->
17
20
  ```typescript
18
- import { createStore } from '@korajs/store'
19
- import { defineSchema, t } from '@korajs/core'
21
+ import { createApp, defineSchema, t } from 'korajs'
20
22
 
21
23
  const schema = defineSchema({
22
24
  version: 1,
@@ -31,45 +33,35 @@ const schema = defineSchema({
31
33
  },
32
34
  })
33
35
 
34
- const store = await createStore({ schema, adapter: 'sqlite-wasm' })
35
- ```
36
+ const app = createApp({ schema })
37
+ await app.ready
36
38
 
37
- ### Insert and Query
39
+ await app.todos.insert({ title: 'Ship Kora v1' })
40
+ const active = await app.todos.where({ completed: false }).orderBy('createdAt', 'desc').limit(10).exec()
38
41
 
39
- ```typescript
40
- await store.insert('todos', { title: 'Ship Kora v1' })
41
-
42
- const active = await store.query('todos', {
43
- where: { completed: false },
44
- orderBy: { createdAt: 'desc' },
45
- limit: 10,
42
+ const unsubscribe = app.todos.where({ completed: false }).subscribe((todos) => {
43
+ // Called with the current results, then whenever they change
44
+ console.log(todos.length)
46
45
  })
47
46
  ```
48
47
 
49
- ### Reactive Subscriptions
48
+ ## Storage adapters
50
49
 
51
- ```typescript
52
- const unsubscribe = store.subscribe(
53
- { collection: 'todos', where: { completed: false }, orderBy: { createdAt: 'asc' } },
54
- (todos) => {
55
- // Called immediately with current data, then on every change
56
- console.log(todos)
57
- }
58
- )
59
- ```
50
+ | Adapter | Entry point | Environment |
51
+ |---------|-------------|-------------|
52
+ | `sqlite-wasm` | `@korajs/store/sqlite-wasm` | Browsers with OPFS (default) |
53
+ | `indexeddb` | `@korajs/store/indexeddb` | Browsers without usable OPFS |
54
+ | `better-sqlite3` | `@korajs/store/better-sqlite3` | Node.js, Electron |
60
55
 
61
- ## Storage Adapters
56
+ `createApp` picks one automatically. Kora never runs on non-durable storage silently: when nothing
57
+ durable can open it emits `store:durability-lost` and refuses writes with `StorageDurabilityError`.
58
+ `FilesystemBlobStore` lives in `@korajs/store/blob-fs` so browser bundles never include `node:fs`.
62
59
 
63
- | Adapter | Environment | Persistence |
64
- |---------|-------------|-------------|
65
- | `sqlite-wasm` | Browser (default) | OPFS |
66
- | `indexeddb` | Browser (fallback) | IndexedDB |
67
- | `sqlite-native` | Node.js / Electron | Filesystem |
60
+ ## Documentation
68
61
 
69
- The adapter is selected automatically based on environment. SQLite WASM runs in a Web Worker to avoid blocking the main thread.
62
+ [Storage Configuration](https://korajs.dev/guide/storage-configuration) and the
63
+ [Store API reference](https://korajs.dev/api/store).
70
64
 
71
65
  ## License
72
66
 
73
67
  MIT
74
-
75
- See the [full documentation](https://github.com/ehoneahobed/kora) for guides, API reference, and examples.
@@ -45,6 +45,22 @@ var AdapterError = class extends import_core.KoraError {
45
45
  this.name = "AdapterError";
46
46
  }
47
47
  };
48
+ var SchemaVersionAheadError = class extends import_core.KoraError {
49
+ constructor(dbName, storedVersion, codeVersion) {
50
+ super(
51
+ `Database "${dbName}" is at schema version ${storedVersion}, newer than this build's schema version ${codeVersion}: a newer build of the app already migrated it. It was not opened and nothing in it changed. Run the newer build (reload while online, or accept the update); never lower schema.version in a deployed app.`,
52
+ "SCHEMA_VERSION_AHEAD",
53
+ { dbName, storedVersion, codeVersion }
54
+ );
55
+ this.dbName = dbName;
56
+ this.storedVersion = storedVersion;
57
+ this.codeVersion = codeVersion;
58
+ this.name = "SchemaVersionAheadError";
59
+ }
60
+ dbName;
61
+ storedVersion;
62
+ codeVersion;
63
+ };
48
64
  var StoreNotOpenError = class extends import_core.KoraError {
49
65
  constructor() {
50
66
  super("Store is not open. Call store.open() before performing operations.", "STORE_NOT_OPEN");
@@ -52,6 +68,18 @@ var StoreNotOpenError = class extends import_core.KoraError {
52
68
  }
53
69
  };
54
70
 
71
+ // src/migrations/schema-ceiling.ts
72
+ var DIRECTIVE = /^--kora:schema-ceiling (\d+)$/;
73
+ function parseSchemaCeiling(sql) {
74
+ const match = DIRECTIVE.exec(sql.trim());
75
+ return match ? Number(match[1]) : null;
76
+ }
77
+ function storedSchemaVersion(rows) {
78
+ const value = Number(rows[0]?.value ?? 0);
79
+ return Number.isFinite(value) ? value : 0;
80
+ }
81
+ var STORED_SCHEMA_VERSION_SQL = "SELECT value FROM _kora_meta WHERE key = 'schema_version'";
82
+
55
83
  // src/adapters/mutex.ts
56
84
  var Mutex = class {
57
85
  locked = false;
@@ -89,6 +117,7 @@ var Mutex = class {
89
117
  };
90
118
 
91
119
  // src/adapters/better-sqlite3-adapter.ts
120
+ var STATEMENT_CACHE_SIZE = 256;
92
121
  var BetterSqlite3Adapter = class {
93
122
  /**
94
123
  * @param path - Database file path, or ':memory:' for in-memory database
@@ -98,6 +127,8 @@ var BetterSqlite3Adapter = class {
98
127
  }
99
128
  path;
100
129
  db = null;
130
+ statements = /* @__PURE__ */ new Map();
131
+ statementsDb = null;
101
132
  /**
102
133
  * Serializes transactions. better-sqlite3 is synchronous, but our
103
134
  * `transaction()` is async and yields the event loop at each `await` inside
@@ -113,6 +144,18 @@ var BetterSqlite3Adapter = class {
113
144
  this.db.pragma("foreign_keys = ON");
114
145
  const statements = (0, import_core2.generateFullDDL)(schema);
115
146
  for (const sql of statements) {
147
+ const ceiling = parseSchemaCeiling(sql);
148
+ if (ceiling !== null) {
149
+ const stored = storedSchemaVersion(
150
+ this.db.prepare(STORED_SCHEMA_VERSION_SQL).all()
151
+ );
152
+ if (stored > ceiling) {
153
+ this.db.close();
154
+ this.db = null;
155
+ throw new SchemaVersionAheadError(this.path, stored, ceiling);
156
+ }
157
+ continue;
158
+ }
116
159
  if (sql.startsWith("--kora:safe-alter")) {
117
160
  try {
118
161
  this.db.exec(sql.replace("--kora:safe-alter\n", ""));
@@ -129,30 +172,48 @@ var BetterSqlite3Adapter = class {
129
172
  }
130
173
  async close() {
131
174
  if (this.db) {
175
+ this.statements.clear();
132
176
  this.db.close();
133
177
  this.db = null;
134
178
  }
135
179
  }
180
+ /**
181
+ * Non-transactional write. Takes the same mutex as {@link transaction} so it
182
+ * never lands inside another caller's open transaction, where a rollback of
183
+ * that transaction would silently discard it (STORE-8). Code running inside a
184
+ * transaction callback must use the `tx` handle, never this method.
185
+ */
136
186
  async execute(sql, params) {
137
187
  const db = this.getDb();
188
+ const release = await this.txMutex.acquire();
138
189
  try {
139
- db.prepare(sql).run(...params ?? []);
190
+ this.statement(db, sql).run(...params ?? []);
140
191
  } catch (error) {
141
192
  throw new AdapterError(`Execute failed: ${error.message}`, {
142
193
  sql,
143
194
  params
144
195
  });
196
+ } finally {
197
+ release();
145
198
  }
146
199
  }
200
+ /**
201
+ * Non-transactional read. Waits for any open transaction to finish so a reader
202
+ * never observes another caller's uncommitted (possibly rolled-back) rows
203
+ * (STORE-8). Inside a transaction callback, read through the `tx` handle.
204
+ */
147
205
  async query(sql, params) {
148
206
  const db = this.getDb();
207
+ const release = await this.txMutex.acquire();
149
208
  try {
150
- return db.prepare(sql).all(...params ?? []);
209
+ return this.statement(db, sql).all(...params ?? []);
151
210
  } catch (error) {
152
211
  throw new AdapterError(`Query failed: ${error.message}`, {
153
212
  sql,
154
213
  params
155
214
  });
215
+ } finally {
216
+ release();
156
217
  }
157
218
  }
158
219
  async transaction(fn) {
@@ -164,7 +225,7 @@ var BetterSqlite3Adapter = class {
164
225
  const tx = {
165
226
  execute: async (sql, params) => {
166
227
  try {
167
- db.prepare(sql).run(...params ?? []);
228
+ this.statement(db, sql).run(...params ?? []);
168
229
  } catch (error) {
169
230
  throw new AdapterError(`Transaction execute failed: ${error.message}`, {
170
231
  sql,
@@ -174,7 +235,7 @@ var BetterSqlite3Adapter = class {
174
235
  },
175
236
  query: async (sql, params) => {
176
237
  try {
177
- return db.prepare(sql).all(...params ?? []);
238
+ return this.statement(db, sql).all(...params ?? []);
178
239
  } catch (error) {
179
240
  throw new AdapterError(`Transaction query failed: ${error.message}`, {
180
241
  sql,
@@ -217,6 +278,31 @@ var BetterSqlite3Adapter = class {
217
278
  release();
218
279
  }
219
280
  }
281
+ /**
282
+ * A prepared statement for `sql`, cached by its text (bounded, least recently
283
+ * used evicted). Preparing dominated the write path: every local or remote write
284
+ * runs the same handful of statements. SQLite re-prepares a cached statement
285
+ * itself when the schema changes.
286
+ */
287
+ statement(db, sql) {
288
+ if (this.statementsDb !== db) {
289
+ this.statements.clear();
290
+ this.statementsDb = db;
291
+ }
292
+ const cached = this.statements.get(sql);
293
+ if (cached) {
294
+ this.statements.delete(sql);
295
+ this.statements.set(sql, cached);
296
+ return cached;
297
+ }
298
+ const prepared = db.prepare(sql);
299
+ this.statements.set(sql, prepared);
300
+ if (this.statements.size > STATEMENT_CACHE_SIZE) {
301
+ const oldest = this.statements.keys().next().value;
302
+ if (oldest !== void 0) this.statements.delete(oldest);
303
+ }
304
+ return prepared;
305
+ }
220
306
  getDb() {
221
307
  if (!this.db) {
222
308
  throw new StoreNotOpenError();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/better-sqlite3.ts","../../src/adapters/better-sqlite3-adapter.ts","../../src/errors.ts","../../src/adapters/mutex.ts"],"sourcesContent":["// Entry point for @korajs/store/better-sqlite3\nexport { BetterSqlite3Adapter } from './better-sqlite3-adapter'\n","import { generateFullDDL } from '@korajs/core'\nimport type { SchemaDefinition } from '@korajs/core'\nimport type Database from 'better-sqlite3'\nimport { AdapterError, StoreNotOpenError } from '../errors'\nimport type { MigrationPlan, StorageAdapter, Transaction } from '../types'\nimport { Mutex } from './mutex'\n\n/**\n * Storage adapter backed by better-sqlite3 for Node.js environments.\n * Used for testing and server-side usage.\n *\n * @example\n * ```typescript\n * import { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\n *\n * const adapter = new BetterSqlite3Adapter(':memory:')\n * ```\n */\nexport class BetterSqlite3Adapter implements StorageAdapter {\n\tprivate db: Database.Database | null = null\n\n\t/**\n\t * Serializes transactions. better-sqlite3 is synchronous, but our\n\t * `transaction()` is async and yields the event loop at each `await` inside\n\t * the callback. Without this mutex, a transaction started while another is\n\t * mid-flight (e.g. a relayed remote operation applied during a local write)\n\t * issues a nested `BEGIN`, which SQLite rejects, and the operation is lost.\n\t */\n\tprivate readonly txMutex = new Mutex()\n\n\t/**\n\t * @param path - Database file path, or ':memory:' for in-memory database\n\t */\n\tconstructor(private readonly path: string = ':memory:') {}\n\n\tasync open(schema: SchemaDefinition): Promise<void> {\n\t\t// Dynamic import so better-sqlite3 is only loaded when this adapter is used\n\t\tconst BetterSqlite3 = (await import('better-sqlite3')).default\n\t\tthis.db = new BetterSqlite3(this.path)\n\n\t\t// WAL mode for better concurrent read/write performance\n\t\tthis.db.pragma('journal_mode = WAL')\n\t\t// Enable foreign keys\n\t\tthis.db.pragma('foreign_keys = ON')\n\n\t\tconst statements = generateFullDDL(schema)\n\t\tfor (const sql of statements) {\n\t\t\tif (sql.startsWith('--kora:safe-alter')) {\n\t\t\t\t// Safe ALTER TABLE — ignore \"duplicate column name\" errors for existing columns\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(sql.replace('--kora:safe-alter\\n', ''))\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = (e as Error).message || ''\n\t\t\t\t\t// Tolerate duplicate columns (already exists) and NOT NULL without defaults\n\t\t\t\t\t// (column may be added or renamed by a migration step instead)\n\t\t\t\t\tif (\n\t\t\t\t\t\t!msg.includes('duplicate column name') &&\n\t\t\t\t\t\t!msg.includes('Cannot add a NOT NULL column with default value NULL')\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.db.exec(sql)\n\t\t\t}\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.db) {\n\t\t\tthis.db.close()\n\t\t\tthis.db = null\n\t\t}\n\t}\n\n\tasync execute(sql: string, params?: unknown[]): Promise<void> {\n\t\tconst db = this.getDb()\n\t\ttry {\n\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Execute failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t}\n\t}\n\n\tasync query<T>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\tconst db = this.getDb()\n\t\ttry {\n\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Query failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t}\n\t}\n\n\tasync transaction(fn: (tx: Transaction) => Promise<void>): Promise<void> {\n\t\tconst db = this.getDb()\n\n\t\t// Serialize with any other in-flight transaction so BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\t// better-sqlite3's transaction() is synchronous, but our interface is async.\n\t\t\t// We use BEGIN/COMMIT/ROLLBACK manually for the async callback.\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tconst tx: Transaction = {\n\t\t\t\t\texecute: async (sql: string, params?: unknown[]): Promise<void> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction execute failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tquery: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction query failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tawait fn(tx)\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow error\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tasync migrate(from: number, to: number, migration: MigrationPlan): Promise<void> {\n\t\tconst db = this.getDb()\n\t\t// Serialize against transactions so migration's BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tfor (const sql of migration.statements) {\n\t\t\t\t\tdb.exec(sql)\n\t\t\t\t}\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow new AdapterError(\n\t\t\t\t\t`Migration from v${from} to v${to} failed: ${(error as Error).message}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tfrom,\n\t\t\t\t\t\tto,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tprivate getDb(): Database.Database {\n\t\tif (!this.db) {\n\t\t\tthrow new StoreNotOpenError()\n\t\t}\n\t\treturn this.db\n\t}\n}\n","import { KoraError } from '@korajs/core'\n\n/**\n * Thrown when a query is invalid (bad field names, invalid operators, etc.).\n */\nexport class QueryError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'QUERY_ERROR', context)\n\t\tthis.name = 'QueryError'\n\t}\n}\n\n/**\n * Thrown when a record is not found by ID (findById, update, delete on missing record).\n */\nexport class RecordNotFoundError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(`Record \"${recordId}\" not found in collection \"${collection}\"`, 'RECORD_NOT_FOUND', {\n\t\t\tcollection,\n\t\t\trecordId,\n\t\t})\n\t\tthis.name = 'RecordNotFoundError'\n\t}\n}\n\n/**\n * Thrown inside `applyRemoteOperation` when the row's version state changed\n * between the caller's snapshot read and the guarded write (a concurrent local\n * mutation landed in the window). The transaction is rolled back with nothing\n * written; the caller re-reads fresh state, recomputes its merge, and retries.\n * This is the optimistic-concurrency guard that keeps merge-engine results\n * (richtext, add-wins arrays, constraint resolutions) from clobbering newer\n * local edits they never saw.\n */\nexport class OptimisticLockError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(\n\t\t\t`Row version changed while merging record \"${recordId}\" in \"${collection}\"; retry with fresh state`,\n\t\t\t'OPTIMISTIC_LOCK',\n\t\t\t{ collection, recordId },\n\t\t)\n\t\tthis.name = 'OptimisticLockError'\n\t}\n}\n\n/**\n * Thrown when a storage adapter operation fails.\n */\nexport class AdapterError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'ADAPTER_ERROR', context)\n\t\tthis.name = 'AdapterError'\n\t}\n}\n\n/**\n * Thrown when an operation is attempted on a store that has not been opened.\n */\nexport class StoreNotOpenError extends KoraError {\n\tconstructor() {\n\t\tsuper('Store is not open. Call store.open() before performing operations.', 'STORE_NOT_OPEN')\n\t\tthis.name = 'StoreNotOpenError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker fails to initialize (WASM load failure, OPFS unavailable, etc.).\n */\nexport class WorkerInitError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Worker initialization failed: ${message}`, 'WORKER_INIT_ERROR', context)\n\t\tthis.name = 'WorkerInitError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker does not respond within the configured timeout.\n */\nexport class WorkerTimeoutError extends KoraError {\n\tconstructor(operation: string, timeoutMs: number) {\n\t\tsuper(\n\t\t\t`Worker did not respond within ${timeoutMs}ms for operation \"${operation}\"`,\n\t\t\t'WORKER_TIMEOUT',\n\t\t\t{ operation, timeoutMs },\n\t\t)\n\t\tthis.name = 'WorkerTimeoutError'\n\t}\n}\n\n/**\n * Thrown when a follower tab cannot reach a live leader tab over the multi-tab\n * broadcast channel (the leader tab was closed, crashed, or is unresponsive).\n * Distinct from {@link WorkerTimeoutError}: this fails fast on a confirmed-absent\n * leader instead of waiting out the full RPC timeout.\n */\nexport class NoLeaderError extends KoraError {\n\tconstructor(operation: string) {\n\t\tsuper(\n\t\t\t`No live leader tab is answering multi-tab storage RPC for operation \"${operation}\"`,\n\t\t\t'NO_LEADER',\n\t\t\t{ operation },\n\t\t)\n\t\tthis.name = 'NoLeaderError'\n\t}\n}\n\n/**\n * Thrown when IndexedDB persistence operations fail (serialize/deserialize).\n */\nexport class PersistenceError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Persistence error: ${message}`, 'PERSISTENCE_ERROR', context)\n\t\tthis.name = 'PersistenceError'\n\t}\n}\n","/**\n * Async mutex for serializing access to a resource across `await` boundaries.\n *\n * This exists because an adapter's `transaction()` is async (its callback may\n * await), but the underlying database transaction (BEGIN…COMMIT) is not\n * reentrant: starting a second transaction while one is open is an error. When\n * a client applies a relayed remote operation while a local write is in flight,\n * the two `transaction()` calls interleave at their `await` points, and without\n * serialization the second `BEGIN` collides with the first, silently dropping\n * the operation. Serializing every transaction through this mutex guarantees at\n * most one is active at a time.\n */\nexport class Mutex {\n\tprivate locked = false\n\tprivate readonly waiters: Array<() => void> = []\n\n\t/**\n\t * Acquire the mutex. Resolves with a release function once the lock is held.\n\t * If the mutex is already held, the caller waits (FIFO) until it is released.\n\t */\n\tasync acquire(): Promise<() => void> {\n\t\tif (!this.locked) {\n\t\t\tthis.locked = true\n\t\t\treturn this.createRelease()\n\t\t}\n\n\t\treturn new Promise<() => void>((resolve) => {\n\t\t\tthis.waiters.push(() => {\n\t\t\t\tresolve(this.createRelease())\n\t\t\t})\n\t\t})\n\t}\n\n\tprivate createRelease(): () => void {\n\t\tlet released = false\n\t\treturn () => {\n\t\t\tif (released) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\treleased = true\n\t\t\tconst next = this.waiters.shift()\n\t\t\tif (next) {\n\t\t\t\tnext()\n\t\t\t} else {\n\t\t\t\tthis.locked = false\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,eAAgC;;;ACAhC,kBAA0B;AAgDnB,IAAM,eAAN,cAA2B,sBAAU;AAAA,EAC3C,YAAY,SAAiB,SAAmC;AAC/D,UAAM,SAAS,iBAAiB,OAAO;AACvC,SAAK,OAAO;AAAA,EACb;AACD;AAKO,IAAM,oBAAN,cAAgC,sBAAU;AAAA,EAChD,cAAc;AACb,UAAM,sEAAsE,gBAAgB;AAC5F,SAAK,OAAO;AAAA,EACb;AACD;;;ACnDO,IAAM,QAAN,MAAY;AAAA,EACV,SAAS;AAAA,EACA,UAA6B,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,EAM/C,MAAM,UAA+B;AACpC,QAAI,CAAC,KAAK,QAAQ;AACjB,WAAK,SAAS;AACd,aAAO,KAAK,cAAc;AAAA,IAC3B;AAEA,WAAO,IAAI,QAAoB,CAAC,YAAY;AAC3C,WAAK,QAAQ,KAAK,MAAM;AACvB,gBAAQ,KAAK,cAAc,CAAC;AAAA,MAC7B,CAAC;AAAA,IACF,CAAC;AAAA,EACF;AAAA,EAEQ,gBAA4B;AACnC,QAAI,WAAW;AACf,WAAO,MAAM;AACZ,UAAI,UAAU;AACb;AAAA,MACD;AACA,iBAAW;AACX,YAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,UAAI,MAAM;AACT,aAAK;AAAA,MACN,OAAO;AACN,aAAK,SAAS;AAAA,MACf;AAAA,IACD;AAAA,EACD;AACD;;;AF9BO,IAAM,uBAAN,MAAqD;AAAA;AAAA;AAAA;AAAA,EAe3D,YAA6B,OAAe,YAAY;AAA3B;AAAA,EAA4B;AAAA,EAA5B;AAAA,EAdrB,KAA+B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAStB,UAAU,IAAI,MAAM;AAAA,EAOrC,MAAM,KAAK,QAAyC;AAEnD,UAAM,iBAAiB,MAAM,OAAO,gBAAgB,GAAG;AACvD,SAAK,KAAK,IAAI,cAAc,KAAK,IAAI;AAGrC,SAAK,GAAG,OAAO,oBAAoB;AAEnC,SAAK,GAAG,OAAO,mBAAmB;AAElC,UAAM,iBAAa,8BAAgB,MAAM;AACzC,eAAW,OAAO,YAAY;AAC7B,UAAI,IAAI,WAAW,mBAAmB,GAAG;AAExC,YAAI;AACH,eAAK,GAAG,KAAK,IAAI,QAAQ,uBAAuB,EAAE,CAAC;AAAA,QACpD,SAAS,GAAG;AACX,gBAAM,MAAO,EAAY,WAAW;AAGpC,cACC,CAAC,IAAI,SAAS,uBAAuB,KACrC,CAAC,IAAI,SAAS,sDAAsD,GACnE;AACD,kBAAM;AAAA,UACP;AAAA,QACD;AAAA,MACD,OAAO;AACN,aAAK,GAAG,KAAK,GAAG;AAAA,MACjB;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAM,QAAuB;AAC5B,QAAI,KAAK,IAAI;AACZ,WAAK,GAAG,MAAM;AACd,WAAK,KAAK;AAAA,IACX;AAAA,EACD;AAAA,EAEA,MAAM,QAAQ,KAAa,QAAmC;AAC7D,UAAM,KAAK,KAAK,MAAM;AACtB,QAAI;AACH,SAAG,QAAQ,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,IACtC,SAAS,OAAO;AACf,YAAM,IAAI,aAAa,mBAAoB,MAAgB,OAAO,IAAI;AAAA,QACrE;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF;AAAA,EACD;AAAA,EAEA,MAAM,MAAS,KAAa,QAAkC;AAC7D,UAAM,KAAK,KAAK,MAAM;AACtB,QAAI;AACH,aAAO,GAAG,QAAQ,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,IAC7C,SAAS,OAAO;AACf,YAAM,IAAI,aAAa,iBAAkB,MAAgB,OAAO,IAAI;AAAA,QACnE;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF;AAAA,EACD;AAAA,EAEA,MAAM,YAAY,IAAuD;AACxE,UAAM,KAAK,KAAK,MAAM;AAGtB,UAAM,UAAU,MAAM,KAAK,QAAQ,QAAQ;AAC3C,QAAI;AAGH,SAAG,KAAK,OAAO;AACf,UAAI;AACH,cAAM,KAAkB;AAAA,UACvB,SAAS,OAAO,KAAa,WAAsC;AAClE,gBAAI;AACH,iBAAG,QAAQ,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,YACtC,SAAS,OAAO;AACf,oBAAM,IAAI,aAAa,+BAAgC,MAAgB,OAAO,IAAI;AAAA,gBACjF;AAAA,gBACA;AAAA,cACD,CAAC;AAAA,YACF;AAAA,UACD;AAAA,UACA,OAAO,OAAU,KAAa,WAAqC;AAClE,gBAAI;AACH,qBAAO,GAAG,QAAQ,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,YAC7C,SAAS,OAAO;AACf,oBAAM,IAAI,aAAa,6BAA8B,MAAgB,OAAO,IAAI;AAAA,gBAC/E;AAAA,gBACA;AAAA,cACD,CAAC;AAAA,YACF;AAAA,UACD;AAAA,QACD;AACA,cAAM,GAAG,EAAE;AACX,WAAG,KAAK,QAAQ;AAAA,MACjB,SAAS,OAAO;AACf,WAAG,KAAK,UAAU;AAClB,cAAM;AAAA,MACP;AAAA,IACD,UAAE;AACD,cAAQ;AAAA,IACT;AAAA,EACD;AAAA,EAEA,MAAM,QAAQ,MAAc,IAAY,WAAyC;AAChF,UAAM,KAAK,KAAK,MAAM;AAEtB,UAAM,UAAU,MAAM,KAAK,QAAQ,QAAQ;AAC3C,QAAI;AACH,SAAG,KAAK,OAAO;AACf,UAAI;AACH,mBAAW,OAAO,UAAU,YAAY;AACvC,aAAG,KAAK,GAAG;AAAA,QACZ;AACA,WAAG,KAAK,QAAQ;AAAA,MACjB,SAAS,OAAO;AACf,WAAG,KAAK,UAAU;AAClB,cAAM,IAAI;AAAA,UACT,mBAAmB,IAAI,QAAQ,EAAE,YAAa,MAAgB,OAAO;AAAA,UACrE;AAAA,YACC;AAAA,YACA;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAAA,IACD,UAAE;AACD,cAAQ;AAAA,IACT;AAAA,EACD;AAAA,EAEQ,QAA2B;AAClC,QAAI,CAAC,KAAK,IAAI;AACb,YAAM,IAAI,kBAAkB;AAAA,IAC7B;AACA,WAAO,KAAK;AAAA,EACb;AACD;","names":["import_core"]}
1
+ {"version":3,"sources":["../../src/adapters/better-sqlite3.ts","../../src/adapters/better-sqlite3-adapter.ts","../../src/errors.ts","../../src/migrations/schema-ceiling.ts","../../src/adapters/mutex.ts"],"sourcesContent":["// Entry point for @korajs/store/better-sqlite3\nexport { BetterSqlite3Adapter } from './better-sqlite3-adapter'\n","import { generateFullDDL } from '@korajs/core'\nimport type { SchemaDefinition } from '@korajs/core'\nimport type Database from 'better-sqlite3'\nimport { AdapterError, SchemaVersionAheadError, StoreNotOpenError } from '../errors'\nimport {\n\tSTORED_SCHEMA_VERSION_SQL,\n\tparseSchemaCeiling,\n\tstoredSchemaVersion,\n} from '../migrations/schema-ceiling'\nimport type { MigrationPlan, StorageAdapter, Transaction } from '../types'\nimport { Mutex } from './mutex'\n\n/**\n * Storage adapter backed by better-sqlite3 for Node.js environments.\n * Used for testing and server-side usage.\n *\n * @example\n * ```typescript\n * import { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\n *\n * const adapter = new BetterSqlite3Adapter(':memory:')\n * ```\n */\n/** Prepared statements kept per adapter (distinct SQL texts, LRU). */\nconst STATEMENT_CACHE_SIZE = 256\n\nexport class BetterSqlite3Adapter implements StorageAdapter {\n\tprivate db: Database.Database | null = null\n\tprivate readonly statements = new Map<string, Database.Statement>()\n\tprivate statementsDb: Database.Database | null = null\n\n\t/**\n\t * Serializes transactions. better-sqlite3 is synchronous, but our\n\t * `transaction()` is async and yields the event loop at each `await` inside\n\t * the callback. Without this mutex, a transaction started while another is\n\t * mid-flight (e.g. a relayed remote operation applied during a local write)\n\t * issues a nested `BEGIN`, which SQLite rejects, and the operation is lost.\n\t */\n\tprivate readonly txMutex = new Mutex()\n\n\t/**\n\t * @param path - Database file path, or ':memory:' for in-memory database\n\t */\n\tconstructor(private readonly path: string = ':memory:') {}\n\n\tasync open(schema: SchemaDefinition): Promise<void> {\n\t\t// Dynamic import so better-sqlite3 is only loaded when this adapter is used\n\t\tconst BetterSqlite3 = (await import('better-sqlite3')).default\n\t\tthis.db = new BetterSqlite3(this.path)\n\n\t\t// WAL mode for better concurrent read/write performance\n\t\tthis.db.pragma('journal_mode = WAL')\n\t\t// Enable foreign keys\n\t\tthis.db.pragma('foreign_keys = ON')\n\n\t\tconst statements = generateFullDDL(schema)\n\t\tfor (const sql of statements) {\n\t\t\tconst ceiling = parseSchemaCeiling(sql)\n\t\t\tif (ceiling !== null) {\n\t\t\t\t// A database a newer build migrated gets none of this build's DDL (RT-109).\n\t\t\t\tconst stored = storedSchemaVersion(\n\t\t\t\t\tthis.db.prepare(STORED_SCHEMA_VERSION_SQL).all() as Array<{ value: unknown }>,\n\t\t\t\t)\n\t\t\t\tif (stored > ceiling) {\n\t\t\t\t\tthis.db.close()\n\t\t\t\t\tthis.db = null\n\t\t\t\t\tthrow new SchemaVersionAheadError(this.path, stored, ceiling)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (sql.startsWith('--kora:safe-alter')) {\n\t\t\t\t// Safe ALTER TABLE — ignore \"duplicate column name\" errors for existing columns\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(sql.replace('--kora:safe-alter\\n', ''))\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = (e as Error).message || ''\n\t\t\t\t\t// Tolerate duplicate columns (already exists) and NOT NULL without defaults\n\t\t\t\t\t// (column may be added or renamed by a migration step instead)\n\t\t\t\t\tif (\n\t\t\t\t\t\t!msg.includes('duplicate column name') &&\n\t\t\t\t\t\t!msg.includes('Cannot add a NOT NULL column with default value NULL')\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.db.exec(sql)\n\t\t\t}\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.db) {\n\t\t\tthis.statements.clear()\n\t\t\tthis.db.close()\n\t\t\tthis.db = null\n\t\t}\n\t}\n\n\t/**\n\t * Non-transactional write. Takes the same mutex as {@link transaction} so it\n\t * never lands inside another caller's open transaction, where a rollback of\n\t * that transaction would silently discard it (STORE-8). Code running inside a\n\t * transaction callback must use the `tx` handle, never this method.\n\t */\n\tasync execute(sql: string, params?: unknown[]): Promise<void> {\n\t\tconst db = this.getDb()\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\tthis.statement(db, sql).run(...(params ?? []))\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Execute failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\t/**\n\t * Non-transactional read. Waits for any open transaction to finish so a reader\n\t * never observes another caller's uncommitted (possibly rolled-back) rows\n\t * (STORE-8). Inside a transaction callback, read through the `tx` handle.\n\t */\n\tasync query<T>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\tconst db = this.getDb()\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\treturn this.statement(db, sql).all(...(params ?? [])) as T[]\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Query failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tasync transaction(fn: (tx: Transaction) => Promise<void>): Promise<void> {\n\t\tconst db = this.getDb()\n\n\t\t// Serialize with any other in-flight transaction so BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\t// better-sqlite3's transaction() is synchronous, but our interface is async.\n\t\t\t// We use BEGIN/COMMIT/ROLLBACK manually for the async callback.\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tconst tx: Transaction = {\n\t\t\t\t\texecute: async (sql: string, params?: unknown[]): Promise<void> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tthis.statement(db, sql).run(...(params ?? []))\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction execute failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tquery: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn this.statement(db, sql).all(...(params ?? [])) as T[]\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction query failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tawait fn(tx)\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow error\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tasync migrate(from: number, to: number, migration: MigrationPlan): Promise<void> {\n\t\tconst db = this.getDb()\n\t\t// Serialize against transactions so migration's BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tfor (const sql of migration.statements) {\n\t\t\t\t\tdb.exec(sql)\n\t\t\t\t}\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow new AdapterError(\n\t\t\t\t\t`Migration from v${from} to v${to} failed: ${(error as Error).message}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tfrom,\n\t\t\t\t\t\tto,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\t/**\n\t * A prepared statement for `sql`, cached by its text (bounded, least recently\n\t * used evicted). Preparing dominated the write path: every local or remote write\n\t * runs the same handful of statements. SQLite re-prepares a cached statement\n\t * itself when the schema changes.\n\t */\n\tprivate statement(db: Database.Database, sql: string): Database.Statement {\n\t\t// A re-open replaces the connection: statements belong to the one they were\n\t\t// prepared on.\n\t\tif (this.statementsDb !== db) {\n\t\t\tthis.statements.clear()\n\t\t\tthis.statementsDb = db\n\t\t}\n\t\tconst cached = this.statements.get(sql)\n\t\tif (cached) {\n\t\t\tthis.statements.delete(sql)\n\t\t\tthis.statements.set(sql, cached)\n\t\t\treturn cached\n\t\t}\n\t\tconst prepared = db.prepare(sql)\n\t\tthis.statements.set(sql, prepared)\n\t\tif (this.statements.size > STATEMENT_CACHE_SIZE) {\n\t\t\tconst oldest = this.statements.keys().next().value\n\t\t\tif (oldest !== undefined) this.statements.delete(oldest)\n\t\t}\n\t\treturn prepared\n\t}\n\n\tprivate getDb(): Database.Database {\n\t\tif (!this.db) {\n\t\t\tthrow new StoreNotOpenError()\n\t\t}\n\t\treturn this.db\n\t}\n}\n","import { KoraError } from '@korajs/core'\n\n/**\n * Thrown when a query is invalid (bad field names, invalid operators, etc.).\n */\nexport class QueryError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'QUERY_ERROR', context)\n\t\tthis.name = 'QueryError'\n\t}\n}\n\n/**\n * Thrown when a record is not found by ID (findById, update, delete on missing record).\n */\nexport class RecordNotFoundError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(`Record \"${recordId}\" not found in collection \"${collection}\"`, 'RECORD_NOT_FOUND', {\n\t\t\tcollection,\n\t\t\trecordId,\n\t\t})\n\t\tthis.name = 'RecordNotFoundError'\n\t}\n}\n\n/**\n * Thrown inside `applyRemoteOperation` when the row's version state changed\n * between the caller's snapshot read and the guarded write (a concurrent local\n * mutation landed in the window). The transaction is rolled back with nothing\n * written; the caller re-reads fresh state, recomputes its merge, and retries.\n * This is the optimistic-concurrency guard that keeps merge-engine results\n * (richtext, add-wins arrays, constraint resolutions) from clobbering newer\n * local edits they never saw.\n */\nexport class OptimisticLockError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(\n\t\t\t`Row version changed while merging record \"${recordId}\" in \"${collection}\"; retry with fresh state`,\n\t\t\t'OPTIMISTIC_LOCK',\n\t\t\t{ collection, recordId },\n\t\t)\n\t\tthis.name = 'OptimisticLockError'\n\t}\n}\n\n/**\n * Thrown when a storage adapter operation fails.\n */\nexport class AdapterError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'ADAPTER_ERROR', context)\n\t\tthis.name = 'AdapterError'\n\t}\n}\n\n/**\n * Thrown when a write reaches a storage adapter that could not obtain durable\n * storage (it opened, or was promoted to leader, on a non-persistent in-memory\n * database). Kora refuses such writes instead of accepting data that would be\n * lost on reload. Reads still work.\n *\n * Fix: close other tabs or apps using the same origin's OPFS storage and reopen,\n * or opt in explicitly with `allowNonDurable: true` if in-memory storage is\n * acceptable for this app.\n */\nexport class StorageDurabilityError extends KoraError {\n\tconstructor(dbName: string, phase: 'open' | 'promotion', reason: string) {\n\t\tsuper(\n\t\t\t`Database \"${dbName}\" has no durable storage (${reason} during ${phase}); writes are refused so they are not silently lost on reload. Close other tabs or apps using this origin's storage and reload, or set allowNonDurable: true to accept in-memory storage.`,\n\t\t\t'STORAGE_DURABILITY_LOST',\n\t\t\t{ dbName, phase, reason },\n\t\t)\n\t\tthis.name = 'StorageDurabilityError'\n\t}\n}\n\n/**\n * Thrown when the store opens a database a NEWER build of the app already migrated: its\n * stored schema version is above this code's `schema.version` (RT-109). Running older\n * code against it would write operations of the old schema into the new one, or fail\n * part-way, so the store refuses before changing anything and emits `store:schema-ahead`.\n *\n * Fix: run the newer build (reload while online, or accept the app's update prompt).\n * Never lower `schema.version` in a deployed app.\n */\nexport class SchemaVersionAheadError extends KoraError {\n\tconstructor(\n\t\tpublic readonly dbName: string,\n\t\tpublic readonly storedVersion: number,\n\t\tpublic readonly codeVersion: number,\n\t) {\n\t\tsuper(\n\t\t\t`Database \"${dbName}\" is at schema version ${storedVersion}, newer than this build's schema version ${codeVersion}: a newer build of the app already migrated it. It was not opened and nothing in it changed. Run the newer build (reload while online, or accept the update); never lower schema.version in a deployed app.`,\n\t\t\t'SCHEMA_VERSION_AHEAD',\n\t\t\t{ dbName, storedVersion, codeVersion },\n\t\t)\n\t\tthis.name = 'SchemaVersionAheadError'\n\t}\n}\n\n/**\n * Thrown when an operation is attempted on a store that has not been opened.\n */\nexport class StoreNotOpenError extends KoraError {\n\tconstructor() {\n\t\tsuper('Store is not open. Call store.open() before performing operations.', 'STORE_NOT_OPEN')\n\t\tthis.name = 'StoreNotOpenError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker fails to initialize (WASM load failure, OPFS unavailable, etc.).\n */\nexport class WorkerInitError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Worker initialization failed: ${message}`, 'WORKER_INIT_ERROR', context)\n\t\tthis.name = 'WorkerInitError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker does not respond within the configured timeout.\n */\nexport class WorkerTimeoutError extends KoraError {\n\tconstructor(operation: string, timeoutMs: number) {\n\t\tsuper(\n\t\t\t`Worker did not respond within ${timeoutMs}ms for operation \"${operation}\"`,\n\t\t\t'WORKER_TIMEOUT',\n\t\t\t{ operation, timeoutMs },\n\t\t)\n\t\tthis.name = 'WorkerTimeoutError'\n\t}\n}\n\n/**\n * Thrown when a follower tab cannot reach a live leader tab over the multi-tab\n * broadcast channel (the leader tab was closed, crashed, or is unresponsive).\n * Distinct from {@link WorkerTimeoutError}: this fails fast on a confirmed-absent\n * leader instead of waiting out the full RPC timeout.\n */\nexport class NoLeaderError extends KoraError {\n\tconstructor(\n\t\toperation: string,\n\t\tmessage = `No live leader tab is answering multi-tab storage RPC for operation \"${operation}\"`,\n\t\tcode = 'NO_LEADER',\n\t\tcontext: Record<string, unknown> = {},\n\t) {\n\t\tsuper(message, code, { operation, ...context })\n\t\tthis.name = 'NoLeaderError'\n\t}\n}\n\n/**\n * Thrown to a follower tab's pending storage requests when the leader tab that\n * was answering stopped sending heartbeats (hung, frozen, or suspended). The\n * request may or may not have been applied: retry it with the same `requestId`\n * (in the error context) and the leader de-duplicates it. Retriable.\n */\nexport class LeaderUnresponsiveError extends NoLeaderError {\n\tconstructor(operation: string, silentMs: number, requestId: string) {\n\t\tsuper(\n\t\t\toperation,\n\t\t\t`The leader tab stopped responding (no heartbeat for ${silentMs}ms) during \"${operation}\". The request may or may not have been applied; retry it with the same requestId and the leader de-duplicates it.`,\n\t\t\t'LEADER_UNRESPONSIVE',\n\t\t\t{ silentMs, requestId, retriable: true },\n\t\t)\n\t\tthis.name = 'LeaderUnresponsiveError'\n\t}\n}\n\n/**\n * Thrown to pending storage requests when their bridge was torn down, typically\n * because this tab was just promoted to leader or the leader handed over.\n * Retriable: the next attempt goes to the new leader.\n */\nexport class BridgeTerminatedError extends KoraError {\n\tconstructor(operation: string, reason: string) {\n\t\tsuper(\n\t\t\t`Storage request \"${operation}\" was interrupted (${reason}); retry it against the new storage leader.`,\n\t\t\t'BRIDGE_TERMINATED',\n\t\t\t{ operation, reason, retriable: true },\n\t\t)\n\t\tthis.name = 'BridgeTerminatedError'\n\t}\n}\n\n/** Thrown when a storage request is cancelled through its `AbortSignal`. */\nexport class RequestAbortedError extends KoraError {\n\tconstructor(operation: string, requestId: string) {\n\t\tsuper(\n\t\t\t`Storage request \"${operation}\" was aborted by the caller. A write may already have reached the leader; retry with the same requestId to stay idempotent.`,\n\t\t\t'REQUEST_ABORTED',\n\t\t\t{ operation, requestId },\n\t\t)\n\t\tthis.name = 'RequestAbortedError'\n\t}\n}\n\n/**\n * Thrown by `deleteDatabase()` when the database is open in some tab or worker\n * on this origin. Close it everywhere (or call `app.close()`) and retry.\n */\nexport class StorageInUseError extends KoraError {\n\tconstructor(dbName: string) {\n\t\tsuper(\n\t\t\t`Database \"${dbName}\" is open in a tab on this origin; close it before deleting it.`,\n\t\t\t'STORAGE_IN_USE',\n\t\t\t{ dbName },\n\t\t)\n\t\tthis.name = 'StorageInUseError'\n\t}\n}\n\n/**\n * Thrown by `deleteDatabase()` when the database still holds operations that\n * never reached the server. Sync first, or pass `{ force: true }` to discard them.\n */\nexport class UnsyncedDataError extends KoraError {\n\tconstructor(dbName: string) {\n\t\tsuper(\n\t\t\t`Database \"${dbName}\" has unsynced operations; deleting it would lose them. Sync first, or pass { force: true } to discard them deliberately.`,\n\t\t\t'UNSYNCED_DATA',\n\t\t\t{ dbName },\n\t\t)\n\t\tthis.name = 'UnsyncedDataError'\n\t}\n}\n\n/**\n * Thrown when a database's data lives in a storage backend this runtime cannot\n * read (for example it was written to OPFS, and OPFS is unavailable now). Kora\n * refuses to start an empty, disjoint copy rather than hide the existing data.\n */\nexport class StorageBackendMismatchError extends KoraError {\n\tconstructor(dbName: string, recorded: string, requested: string, reason: string) {\n\t\tsuper(\n\t\t\t`Database \"${dbName}\" is stored in ${recorded}, but this session can only use ${requested} (${reason}). Kora will not start a separate empty copy; reload in a browser profile where ${recorded} is available.`,\n\t\t\t'STORAGE_BACKEND_MISMATCH',\n\t\t\t{ dbName, recorded, requested, reason },\n\t\t)\n\t\tthis.name = 'StorageBackendMismatchError'\n\t}\n}\n\n/**\n * Thrown when IndexedDB persistence operations fail (serialize/deserialize).\n */\nexport class PersistenceError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Persistence error: ${message}`, 'PERSISTENCE_ERROR', context)\n\t\tthis.name = 'PersistenceError'\n\t}\n}\n","/**\n * The schema-version ceiling (RT-109): a build never runs against a database a newer\n * build already migrated.\n *\n * `generateFullDDL` emits `--kora:schema-ceiling <version>` right after `_kora_meta`, before\n * any other DDL. It is a SQL comment; Kora's DDL executors (the native adapter, the SQLite\n * WASM worker, the test bridge) stop there with {@link schemaAheadMessage} when the\n * database's stored schema version is newer, so not even this build's additive DDL touches\n * it. The store then maps the failure to `SchemaVersionAheadError` and checks again after\n * every open (adapters that restore data after their DDL, such as the IndexedDB fallback).\n *\n * No imports: the SQLite WASM worker bundles this file.\n */\n\nconst DIRECTIVE = /^--kora:schema-ceiling (\\d+)$/\nconst AHEAD = /KORA_SCHEMA_VERSION_AHEAD stored=(\\d+) code=(\\d+)/\n\n/** The version a `--kora:schema-ceiling <version>` DDL statement names, or null. */\nexport function parseSchemaCeiling(sql: string): number | null {\n\tconst match = DIRECTIVE.exec(sql.trim())\n\treturn match ? Number(match[1]) : null\n}\n\n/** The stored version from a `_kora_meta` row read, or 0 when none is stored. */\nexport function storedSchemaVersion(rows: ReadonlyArray<{ value: unknown }>): number {\n\tconst value = Number(rows[0]?.value ?? 0)\n\treturn Number.isFinite(value) ? value : 0\n}\n\n/** The message a DDL executor throws when the database is ahead (parsed back by the store). */\nexport function schemaAheadMessage(stored: number, code: number): string {\n\treturn `KORA_SCHEMA_VERSION_AHEAD stored=${stored} code=${code}: the database was migrated by a newer build (schema version ${stored}); this build's schema version is ${code}.`\n}\n\n/** The versions in an error raised by {@link schemaAheadMessage}, wherever it was wrapped. */\nexport function parseSchemaAhead(error: unknown): { stored: number; code: number } | null {\n\tconst seen = new Set<unknown>()\n\tlet current: unknown = error\n\twhile (current !== null && current !== undefined && !seen.has(current)) {\n\t\tseen.add(current)\n\t\tconst text =\n\t\t\tcurrent instanceof Error ? current.message : typeof current === 'string' ? current : ''\n\t\tconst match = AHEAD.exec(text)\n\t\tif (match) return { stored: Number(match[1]), code: Number(match[2]) }\n\t\tcurrent = current instanceof Error ? (current as { cause?: unknown }).cause : null\n\t}\n\treturn null\n}\n\n/** Query the stored schema version through a synchronous runner (DDL executors). */\nexport const STORED_SCHEMA_VERSION_SQL = \"SELECT value FROM _kora_meta WHERE key = 'schema_version'\"\n","/**\n * Async mutex for serializing access to a resource across `await` boundaries.\n *\n * This exists because an adapter's `transaction()` is async (its callback may\n * await), but the underlying database transaction (BEGIN…COMMIT) is not\n * reentrant: starting a second transaction while one is open is an error. When\n * a client applies a relayed remote operation while a local write is in flight,\n * the two `transaction()` calls interleave at their `await` points, and without\n * serialization the second `BEGIN` collides with the first, silently dropping\n * the operation. Serializing every transaction through this mutex guarantees at\n * most one is active at a time.\n */\nexport class Mutex {\n\tprivate locked = false\n\tprivate readonly waiters: Array<() => void> = []\n\n\t/**\n\t * Acquire the mutex. Resolves with a release function once the lock is held.\n\t * If the mutex is already held, the caller waits (FIFO) until it is released.\n\t */\n\tasync acquire(): Promise<() => void> {\n\t\tif (!this.locked) {\n\t\t\tthis.locked = true\n\t\t\treturn this.createRelease()\n\t\t}\n\n\t\treturn new Promise<() => void>((resolve) => {\n\t\t\tthis.waiters.push(() => {\n\t\t\t\tresolve(this.createRelease())\n\t\t\t})\n\t\t})\n\t}\n\n\tprivate createRelease(): () => void {\n\t\tlet released = false\n\t\treturn () => {\n\t\t\tif (released) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\treleased = true\n\t\t\tconst next = this.waiters.shift()\n\t\t\tif (next) {\n\t\t\t\tnext()\n\t\t\t} else {\n\t\t\t\tthis.locked = false\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,eAAgC;;;ACAhC,kBAA0B;AAgDnB,IAAM,eAAN,cAA2B,sBAAU;AAAA,EAC3C,YAAY,SAAiB,SAAmC;AAC/D,UAAM,SAAS,iBAAiB,OAAO;AACvC,SAAK,OAAO;AAAA,EACb;AACD;AAgCO,IAAM,0BAAN,cAAsC,sBAAU;AAAA,EACtD,YACiB,QACA,eACA,aACf;AACD;AAAA,MACC,aAAa,MAAM,0BAA0B,aAAa,4CAA4C,WAAW;AAAA,MACjH;AAAA,MACA,EAAE,QAAQ,eAAe,YAAY;AAAA,IACtC;AARgB;AACA;AACA;AAOhB,SAAK,OAAO;AAAA,EACb;AAAA,EAViB;AAAA,EACA;AAAA,EACA;AASlB;AAKO,IAAM,oBAAN,cAAgC,sBAAU;AAAA,EAChD,cAAc;AACb,UAAM,sEAAsE,gBAAgB;AAC5F,SAAK,OAAO;AAAA,EACb;AACD;;;AC9FA,IAAM,YAAY;AAIX,SAAS,mBAAmB,KAA4B;AAC9D,QAAM,QAAQ,UAAU,KAAK,IAAI,KAAK,CAAC;AACvC,SAAO,QAAQ,OAAO,MAAM,CAAC,CAAC,IAAI;AACnC;AAGO,SAAS,oBAAoB,MAAiD;AACpF,QAAM,QAAQ,OAAO,KAAK,CAAC,GAAG,SAAS,CAAC;AACxC,SAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AACzC;AAuBO,IAAM,4BAA4B;;;ACtClC,IAAM,QAAN,MAAY;AAAA,EACV,SAAS;AAAA,EACA,UAA6B,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,EAM/C,MAAM,UAA+B;AACpC,QAAI,CAAC,KAAK,QAAQ;AACjB,WAAK,SAAS;AACd,aAAO,KAAK,cAAc;AAAA,IAC3B;AAEA,WAAO,IAAI,QAAoB,CAAC,YAAY;AAC3C,WAAK,QAAQ,KAAK,MAAM;AACvB,gBAAQ,KAAK,cAAc,CAAC;AAAA,MAC7B,CAAC;AAAA,IACF,CAAC;AAAA,EACF;AAAA,EAEQ,gBAA4B;AACnC,QAAI,WAAW;AACf,WAAO,MAAM;AACZ,UAAI,UAAU;AACb;AAAA,MACD;AACA,iBAAW;AACX,YAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,UAAI,MAAM;AACT,aAAK;AAAA,MACN,OAAO;AACN,aAAK,SAAS;AAAA,MACf;AAAA,IACD;AAAA,EACD;AACD;;;AHxBA,IAAM,uBAAuB;AAEtB,IAAM,uBAAN,MAAqD;AAAA;AAAA;AAAA;AAAA,EAiB3D,YAA6B,OAAe,YAAY;AAA3B;AAAA,EAA4B;AAAA,EAA5B;AAAA,EAhBrB,KAA+B;AAAA,EACtB,aAAa,oBAAI,IAAgC;AAAA,EAC1D,eAAyC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAShC,UAAU,IAAI,MAAM;AAAA,EAOrC,MAAM,KAAK,QAAyC;AAEnD,UAAM,iBAAiB,MAAM,OAAO,gBAAgB,GAAG;AACvD,SAAK,KAAK,IAAI,cAAc,KAAK,IAAI;AAGrC,SAAK,GAAG,OAAO,oBAAoB;AAEnC,SAAK,GAAG,OAAO,mBAAmB;AAElC,UAAM,iBAAa,8BAAgB,MAAM;AACzC,eAAW,OAAO,YAAY;AAC7B,YAAM,UAAU,mBAAmB,GAAG;AACtC,UAAI,YAAY,MAAM;AAErB,cAAM,SAAS;AAAA,UACd,KAAK,GAAG,QAAQ,yBAAyB,EAAE,IAAI;AAAA,QAChD;AACA,YAAI,SAAS,SAAS;AACrB,eAAK,GAAG,MAAM;AACd,eAAK,KAAK;AACV,gBAAM,IAAI,wBAAwB,KAAK,MAAM,QAAQ,OAAO;AAAA,QAC7D;AACA;AAAA,MACD;AACA,UAAI,IAAI,WAAW,mBAAmB,GAAG;AAExC,YAAI;AACH,eAAK,GAAG,KAAK,IAAI,QAAQ,uBAAuB,EAAE,CAAC;AAAA,QACpD,SAAS,GAAG;AACX,gBAAM,MAAO,EAAY,WAAW;AAGpC,cACC,CAAC,IAAI,SAAS,uBAAuB,KACrC,CAAC,IAAI,SAAS,sDAAsD,GACnE;AACD,kBAAM;AAAA,UACP;AAAA,QACD;AAAA,MACD,OAAO;AACN,aAAK,GAAG,KAAK,GAAG;AAAA,MACjB;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAM,QAAuB;AAC5B,QAAI,KAAK,IAAI;AACZ,WAAK,WAAW,MAAM;AACtB,WAAK,GAAG,MAAM;AACd,WAAK,KAAK;AAAA,IACX;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,QAAQ,KAAa,QAAmC;AAC7D,UAAM,KAAK,KAAK,MAAM;AACtB,UAAM,UAAU,MAAM,KAAK,QAAQ,QAAQ;AAC3C,QAAI;AACH,WAAK,UAAU,IAAI,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,IAC9C,SAAS,OAAO;AACf,YAAM,IAAI,aAAa,mBAAoB,MAAgB,OAAO,IAAI;AAAA,QACrE;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF,UAAE;AACD,cAAQ;AAAA,IACT;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,MAAS,KAAa,QAAkC;AAC7D,UAAM,KAAK,KAAK,MAAM;AACtB,UAAM,UAAU,MAAM,KAAK,QAAQ,QAAQ;AAC3C,QAAI;AACH,aAAO,KAAK,UAAU,IAAI,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,IACrD,SAAS,OAAO;AACf,YAAM,IAAI,aAAa,iBAAkB,MAAgB,OAAO,IAAI;AAAA,QACnE;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF,UAAE;AACD,cAAQ;AAAA,IACT;AAAA,EACD;AAAA,EAEA,MAAM,YAAY,IAAuD;AACxE,UAAM,KAAK,KAAK,MAAM;AAGtB,UAAM,UAAU,MAAM,KAAK,QAAQ,QAAQ;AAC3C,QAAI;AAGH,SAAG,KAAK,OAAO;AACf,UAAI;AACH,cAAM,KAAkB;AAAA,UACvB,SAAS,OAAO,KAAa,WAAsC;AAClE,gBAAI;AACH,mBAAK,UAAU,IAAI,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,YAC9C,SAAS,OAAO;AACf,oBAAM,IAAI,aAAa,+BAAgC,MAAgB,OAAO,IAAI;AAAA,gBACjF;AAAA,gBACA;AAAA,cACD,CAAC;AAAA,YACF;AAAA,UACD;AAAA,UACA,OAAO,OAAU,KAAa,WAAqC;AAClE,gBAAI;AACH,qBAAO,KAAK,UAAU,IAAI,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,YACrD,SAAS,OAAO;AACf,oBAAM,IAAI,aAAa,6BAA8B,MAAgB,OAAO,IAAI;AAAA,gBAC/E;AAAA,gBACA;AAAA,cACD,CAAC;AAAA,YACF;AAAA,UACD;AAAA,QACD;AACA,cAAM,GAAG,EAAE;AACX,WAAG,KAAK,QAAQ;AAAA,MACjB,SAAS,OAAO;AACf,WAAG,KAAK,UAAU;AAClB,cAAM;AAAA,MACP;AAAA,IACD,UAAE;AACD,cAAQ;AAAA,IACT;AAAA,EACD;AAAA,EAEA,MAAM,QAAQ,MAAc,IAAY,WAAyC;AAChF,UAAM,KAAK,KAAK,MAAM;AAEtB,UAAM,UAAU,MAAM,KAAK,QAAQ,QAAQ;AAC3C,QAAI;AACH,SAAG,KAAK,OAAO;AACf,UAAI;AACH,mBAAW,OAAO,UAAU,YAAY;AACvC,aAAG,KAAK,GAAG;AAAA,QACZ;AACA,WAAG,KAAK,QAAQ;AAAA,MACjB,SAAS,OAAO;AACf,WAAG,KAAK,UAAU;AAClB,cAAM,IAAI;AAAA,UACT,mBAAmB,IAAI,QAAQ,EAAE,YAAa,MAAgB,OAAO;AAAA,UACrE;AAAA,YACC;AAAA,YACA;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAAA,IACD,UAAE;AACD,cAAQ;AAAA,IACT;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,UAAU,IAAuB,KAAiC;AAGzE,QAAI,KAAK,iBAAiB,IAAI;AAC7B,WAAK,WAAW,MAAM;AACtB,WAAK,eAAe;AAAA,IACrB;AACA,UAAM,SAAS,KAAK,WAAW,IAAI,GAAG;AACtC,QAAI,QAAQ;AACX,WAAK,WAAW,OAAO,GAAG;AAC1B,WAAK,WAAW,IAAI,KAAK,MAAM;AAC/B,aAAO;AAAA,IACR;AACA,UAAM,WAAW,GAAG,QAAQ,GAAG;AAC/B,SAAK,WAAW,IAAI,KAAK,QAAQ;AACjC,QAAI,KAAK,WAAW,OAAO,sBAAsB;AAChD,YAAM,SAAS,KAAK,WAAW,KAAK,EAAE,KAAK,EAAE;AAC7C,UAAI,WAAW,OAAW,MAAK,WAAW,OAAO,MAAM;AAAA,IACxD;AACA,WAAO;AAAA,EACR;AAAA,EAEQ,QAA2B;AAClC,QAAI,CAAC,KAAK,IAAI;AACb,YAAM,IAAI,kBAAkB;AAAA,IAC7B;AACA,WAAO,KAAK;AAAA,EACb;AACD;","names":["import_core"]}
@@ -1,20 +1,11 @@
1
1
  import { SchemaDefinition } from '@korajs/core';
2
- import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-BslDOTk0.cjs';
2
+ import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-Dh-xyUjP.cjs';
3
3
 
4
- /**
5
- * Storage adapter backed by better-sqlite3 for Node.js environments.
6
- * Used for testing and server-side usage.
7
- *
8
- * @example
9
- * ```typescript
10
- * import { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'
11
- *
12
- * const adapter = new BetterSqlite3Adapter(':memory:')
13
- * ```
14
- */
15
4
  declare class BetterSqlite3Adapter implements StorageAdapter {
16
5
  private readonly path;
17
6
  private db;
7
+ private readonly statements;
8
+ private statementsDb;
18
9
  /**
19
10
  * Serializes transactions. better-sqlite3 is synchronous, but our
20
11
  * `transaction()` is async and yields the event loop at each `await` inside
@@ -29,10 +20,28 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
29
20
  constructor(path?: string);
30
21
  open(schema: SchemaDefinition): Promise<void>;
31
22
  close(): Promise<void>;
23
+ /**
24
+ * Non-transactional write. Takes the same mutex as {@link transaction} so it
25
+ * never lands inside another caller's open transaction, where a rollback of
26
+ * that transaction would silently discard it (STORE-8). Code running inside a
27
+ * transaction callback must use the `tx` handle, never this method.
28
+ */
32
29
  execute(sql: string, params?: unknown[]): Promise<void>;
30
+ /**
31
+ * Non-transactional read. Waits for any open transaction to finish so a reader
32
+ * never observes another caller's uncommitted (possibly rolled-back) rows
33
+ * (STORE-8). Inside a transaction callback, read through the `tx` handle.
34
+ */
33
35
  query<T>(sql: string, params?: unknown[]): Promise<T[]>;
34
36
  transaction(fn: (tx: Transaction) => Promise<void>): Promise<void>;
35
37
  migrate(from: number, to: number, migration: MigrationPlan): Promise<void>;
38
+ /**
39
+ * A prepared statement for `sql`, cached by its text (bounded, least recently
40
+ * used evicted). Preparing dominated the write path: every local or remote write
41
+ * runs the same handful of statements. SQLite re-prepares a cached statement
42
+ * itself when the schema changes.
43
+ */
44
+ private statement;
36
45
  private getDb;
37
46
  }
38
47
 
@@ -1,20 +1,11 @@
1
1
  import { SchemaDefinition } from '@korajs/core';
2
- import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-BslDOTk0.js';
2
+ import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-Dh-xyUjP.js';
3
3
 
4
- /**
5
- * Storage adapter backed by better-sqlite3 for Node.js environments.
6
- * Used for testing and server-side usage.
7
- *
8
- * @example
9
- * ```typescript
10
- * import { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'
11
- *
12
- * const adapter = new BetterSqlite3Adapter(':memory:')
13
- * ```
14
- */
15
4
  declare class BetterSqlite3Adapter implements StorageAdapter {
16
5
  private readonly path;
17
6
  private db;
7
+ private readonly statements;
8
+ private statementsDb;
18
9
  /**
19
10
  * Serializes transactions. better-sqlite3 is synchronous, but our
20
11
  * `transaction()` is async and yields the event loop at each `await` inside
@@ -29,10 +20,28 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
29
20
  constructor(path?: string);
30
21
  open(schema: SchemaDefinition): Promise<void>;
31
22
  close(): Promise<void>;
23
+ /**
24
+ * Non-transactional write. Takes the same mutex as {@link transaction} so it
25
+ * never lands inside another caller's open transaction, where a rollback of
26
+ * that transaction would silently discard it (STORE-8). Code running inside a
27
+ * transaction callback must use the `tx` handle, never this method.
28
+ */
32
29
  execute(sql: string, params?: unknown[]): Promise<void>;
30
+ /**
31
+ * Non-transactional read. Waits for any open transaction to finish so a reader
32
+ * never observes another caller's uncommitted (possibly rolled-back) rows
33
+ * (STORE-8). Inside a transaction callback, read through the `tx` handle.
34
+ */
33
35
  query<T>(sql: string, params?: unknown[]): Promise<T[]>;
34
36
  transaction(fn: (tx: Transaction) => Promise<void>): Promise<void>;
35
37
  migrate(from: number, to: number, migration: MigrationPlan): Promise<void>;
38
+ /**
39
+ * A prepared statement for `sql`, cached by its text (bounded, least recently
40
+ * used evicted). Preparing dominated the write path: every local or remote write
41
+ * runs the same handful of statements. SQLite re-prepares a cached statement
42
+ * itself when the schema changes.
43
+ */
44
+ private statement;
36
45
  private getDb;
37
46
  }
38
47
 
@@ -1,13 +1,20 @@
1
+ import {
2
+ STORED_SCHEMA_VERSION_SQL,
3
+ parseSchemaCeiling,
4
+ storedSchemaVersion
5
+ } from "../chunk-A6GFH5IG.js";
1
6
  import {
2
7
  Mutex
3
8
  } from "../chunk-TVJEC5S2.js";
4
9
  import {
5
10
  AdapterError,
11
+ SchemaVersionAheadError,
6
12
  StoreNotOpenError
7
- } from "../chunk-TUR7RGEP.js";
13
+ } from "../chunk-DUKTWVZW.js";
8
14
 
9
15
  // src/adapters/better-sqlite3-adapter.ts
10
16
  import { generateFullDDL } from "@korajs/core";
17
+ var STATEMENT_CACHE_SIZE = 256;
11
18
  var BetterSqlite3Adapter = class {
12
19
  /**
13
20
  * @param path - Database file path, or ':memory:' for in-memory database
@@ -17,6 +24,8 @@ var BetterSqlite3Adapter = class {
17
24
  }
18
25
  path;
19
26
  db = null;
27
+ statements = /* @__PURE__ */ new Map();
28
+ statementsDb = null;
20
29
  /**
21
30
  * Serializes transactions. better-sqlite3 is synchronous, but our
22
31
  * `transaction()` is async and yields the event loop at each `await` inside
@@ -32,6 +41,18 @@ var BetterSqlite3Adapter = class {
32
41
  this.db.pragma("foreign_keys = ON");
33
42
  const statements = generateFullDDL(schema);
34
43
  for (const sql of statements) {
44
+ const ceiling = parseSchemaCeiling(sql);
45
+ if (ceiling !== null) {
46
+ const stored = storedSchemaVersion(
47
+ this.db.prepare(STORED_SCHEMA_VERSION_SQL).all()
48
+ );
49
+ if (stored > ceiling) {
50
+ this.db.close();
51
+ this.db = null;
52
+ throw new SchemaVersionAheadError(this.path, stored, ceiling);
53
+ }
54
+ continue;
55
+ }
35
56
  if (sql.startsWith("--kora:safe-alter")) {
36
57
  try {
37
58
  this.db.exec(sql.replace("--kora:safe-alter\n", ""));
@@ -48,30 +69,48 @@ var BetterSqlite3Adapter = class {
48
69
  }
49
70
  async close() {
50
71
  if (this.db) {
72
+ this.statements.clear();
51
73
  this.db.close();
52
74
  this.db = null;
53
75
  }
54
76
  }
77
+ /**
78
+ * Non-transactional write. Takes the same mutex as {@link transaction} so it
79
+ * never lands inside another caller's open transaction, where a rollback of
80
+ * that transaction would silently discard it (STORE-8). Code running inside a
81
+ * transaction callback must use the `tx` handle, never this method.
82
+ */
55
83
  async execute(sql, params) {
56
84
  const db = this.getDb();
85
+ const release = await this.txMutex.acquire();
57
86
  try {
58
- db.prepare(sql).run(...params ?? []);
87
+ this.statement(db, sql).run(...params ?? []);
59
88
  } catch (error) {
60
89
  throw new AdapterError(`Execute failed: ${error.message}`, {
61
90
  sql,
62
91
  params
63
92
  });
93
+ } finally {
94
+ release();
64
95
  }
65
96
  }
97
+ /**
98
+ * Non-transactional read. Waits for any open transaction to finish so a reader
99
+ * never observes another caller's uncommitted (possibly rolled-back) rows
100
+ * (STORE-8). Inside a transaction callback, read through the `tx` handle.
101
+ */
66
102
  async query(sql, params) {
67
103
  const db = this.getDb();
104
+ const release = await this.txMutex.acquire();
68
105
  try {
69
- return db.prepare(sql).all(...params ?? []);
106
+ return this.statement(db, sql).all(...params ?? []);
70
107
  } catch (error) {
71
108
  throw new AdapterError(`Query failed: ${error.message}`, {
72
109
  sql,
73
110
  params
74
111
  });
112
+ } finally {
113
+ release();
75
114
  }
76
115
  }
77
116
  async transaction(fn) {
@@ -83,7 +122,7 @@ var BetterSqlite3Adapter = class {
83
122
  const tx = {
84
123
  execute: async (sql, params) => {
85
124
  try {
86
- db.prepare(sql).run(...params ?? []);
125
+ this.statement(db, sql).run(...params ?? []);
87
126
  } catch (error) {
88
127
  throw new AdapterError(`Transaction execute failed: ${error.message}`, {
89
128
  sql,
@@ -93,7 +132,7 @@ var BetterSqlite3Adapter = class {
93
132
  },
94
133
  query: async (sql, params) => {
95
134
  try {
96
- return db.prepare(sql).all(...params ?? []);
135
+ return this.statement(db, sql).all(...params ?? []);
97
136
  } catch (error) {
98
137
  throw new AdapterError(`Transaction query failed: ${error.message}`, {
99
138
  sql,
@@ -136,6 +175,31 @@ var BetterSqlite3Adapter = class {
136
175
  release();
137
176
  }
138
177
  }
178
+ /**
179
+ * A prepared statement for `sql`, cached by its text (bounded, least recently
180
+ * used evicted). Preparing dominated the write path: every local or remote write
181
+ * runs the same handful of statements. SQLite re-prepares a cached statement
182
+ * itself when the schema changes.
183
+ */
184
+ statement(db, sql) {
185
+ if (this.statementsDb !== db) {
186
+ this.statements.clear();
187
+ this.statementsDb = db;
188
+ }
189
+ const cached = this.statements.get(sql);
190
+ if (cached) {
191
+ this.statements.delete(sql);
192
+ this.statements.set(sql, cached);
193
+ return cached;
194
+ }
195
+ const prepared = db.prepare(sql);
196
+ this.statements.set(sql, prepared);
197
+ if (this.statements.size > STATEMENT_CACHE_SIZE) {
198
+ const oldest = this.statements.keys().next().value;
199
+ if (oldest !== void 0) this.statements.delete(oldest);
200
+ }
201
+ return prepared;
202
+ }
139
203
  getDb() {
140
204
  if (!this.db) {
141
205
  throw new StoreNotOpenError();