@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.
- package/README.md +29 -37
- package/dist/adapters/better-sqlite3.cjs +90 -4
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +21 -12
- package/dist/adapters/better-sqlite3.d.ts +21 -12
- package/dist/adapters/better-sqlite3.js +69 -5
- package/dist/adapters/better-sqlite3.js.map +1 -1
- package/dist/adapters/indexeddb.cjs +1316 -610
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +29 -3
- package/dist/adapters/indexeddb.d.ts +29 -3
- package/dist/adapters/indexeddb.js +247 -221
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs +856 -63
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +529 -63
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +1032 -344
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +239 -18
- package/dist/adapters/sqlite-wasm.d.ts +239 -18
- package/dist/adapters/sqlite-wasm.js +111 -5
- package/dist/adapters/sqlite-wasm.js.map +1 -1
- package/dist/backup-IHKVMUAM.js +25 -0
- package/dist/chunk-5AQSGMJV.js +5471 -0
- package/dist/chunk-5AQSGMJV.js.map +1 -0
- package/dist/chunk-5JMGMPUE.js +591 -0
- package/dist/chunk-5JMGMPUE.js.map +1 -0
- package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
- package/dist/chunk-A6GFH5IG.js +36 -0
- package/dist/chunk-A6GFH5IG.js.map +1 -0
- package/dist/chunk-BILSA6XZ.js +608 -0
- package/dist/chunk-BILSA6XZ.js.map +1 -0
- package/dist/chunk-BQSIL6TT.js +1095 -0
- package/dist/chunk-BQSIL6TT.js.map +1 -0
- package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
- package/dist/chunk-J5AGXF3D.js.map +1 -0
- package/dist/chunk-KK3GBONL.js +112 -0
- package/dist/chunk-KK3GBONL.js.map +1 -0
- package/dist/chunk-KTVNANUH.js +876 -0
- package/dist/chunk-KTVNANUH.js.map +1 -0
- package/dist/chunk-QW4UU7NG.js +1003 -0
- package/dist/chunk-QW4UU7NG.js.map +1 -0
- package/dist/chunk-SRHDJF5B.js +186 -0
- package/dist/chunk-SRHDJF5B.js.map +1 -0
- package/dist/chunk-UC2LGFPV.js +810 -0
- package/dist/chunk-UC2LGFPV.js.map +1 -0
- package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
- package/dist/index.cjs +9533 -4246
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1143 -110
- package/dist/index.d.ts +1143 -110
- package/dist/index.js +298 -3191
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +581 -264
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +15 -85
- package/dist/internal.d.ts +15 -85
- package/dist/internal.js +10 -8
- package/dist/restore-MNDPR557.js +299 -0
- package/dist/restore-MNDPR557.js.map +1 -0
- package/dist/sql-builder-D0tC3sv9.d.cts +615 -0
- package/dist/sql-builder-D5UMzHEo.d.ts +615 -0
- package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
- package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
- package/dist/sqlite-wasm-channel-QCCNBYCU.js +12 -0
- package/dist/store-HNGX4KTT.js +16 -0
- package/dist/store-HNGX4KTT.js.map +1 -0
- package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
- package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
- package/package.json +3 -2
- package/dist/backup-5S7K2Z35.js +0 -14
- package/dist/chunk-GSJ7EEUC.js.map +0 -1
- package/dist/chunk-HS6DBMST.js +0 -383
- package/dist/chunk-HS6DBMST.js.map +0 -1
- package/dist/chunk-RJIEKEN4.js +0 -751
- package/dist/chunk-RJIEKEN4.js.map +0 -1
- package/dist/chunk-TUR7RGEP.js +0 -84
- package/dist/chunk-TUR7RGEP.js.map +0 -1
- package/dist/chunk-VDBJQ75K.js +0 -78
- package/dist/chunk-VDBJQ75K.js.map +0 -1
- package/dist/chunk-YT7MRFCP.js +0 -687
- package/dist/chunk-YT7MRFCP.js.map +0 -1
- package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
- package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
- package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
- package/dist/types-BOO9EPt1.d.ts +0 -170
- package/dist/types-C_GDavG-.d.cts +0 -170
- /package/dist/{backup-5S7K2Z35.js.map → backup-IHKVMUAM.js.map} +0 -0
- /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
- /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-QCCNBYCU.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
# @korajs/store
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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 {
|
|
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
|
|
35
|
-
|
|
36
|
+
const app = createApp({ schema })
|
|
37
|
+
await app.ready
|
|
36
38
|
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
+
## Storage adapters
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|---------|-------------|-------------|
|
|
65
|
-
| `sqlite-wasm` | Browser (default) | OPFS |
|
|
66
|
-
| `indexeddb` | Browser (fallback) | IndexedDB |
|
|
67
|
-
| `sqlite-native` | Node.js / Electron | Filesystem |
|
|
60
|
+
## Documentation
|
|
68
61
|
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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":"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","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/**\n * Thrown by a local write while the store's pinned node id (an auth device id) belongs\n * to another user than the one signed in (F9): a browser two users shared. The write\n * could only be stored under the owner's node and would later upload as the owner's.\n */\nexport class NodeOwnedByAnotherUserError extends KoraError {\n\tconstructor(nodeId: string, principal: string | null) {\n\t\tsuper(\n\t\t\t\"This device belongs to another account, so changes made while this account is signed in here cannot be saved: they would be uploaded as the device owner's. Sign in as the device's owner, or use this account on its own device.\",\n\t\t\t'NODE_OWNED_BY_ANOTHER_USER',\n\t\t\t{ nodeId, principal },\n\t\t)\n\t\tthis.name = 'NodeOwnedByAnotherUserError'\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":"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","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-
|
|
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-
|
|
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-
|
|
13
|
+
} from "../chunk-SRHDJF5B.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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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();
|