@korajs/store 0.0.0-canary-20261008174129
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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/adapters/better-sqlite3.cjs +317 -0
- package/dist/adapters/better-sqlite3.cjs.map +1 -0
- package/dist/adapters/better-sqlite3.d.cts +48 -0
- package/dist/adapters/better-sqlite3.d.ts +48 -0
- package/dist/adapters/better-sqlite3.js +213 -0
- package/dist/adapters/better-sqlite3.js.map +1 -0
- package/dist/adapters/indexeddb.cjs +2037 -0
- package/dist/adapters/indexeddb.cjs.map +1 -0
- package/dist/adapters/indexeddb.d.cts +98 -0
- package/dist/adapters/indexeddb.d.ts +98 -0
- package/dist/adapters/indexeddb.js +423 -0
- package/dist/adapters/indexeddb.js.map +1 -0
- package/dist/adapters/sqlite-wasm-worker.cjs +1076 -0
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -0
- package/dist/adapters/sqlite-wasm-worker.d.cts +2 -0
- package/dist/adapters/sqlite-wasm-worker.d.ts +2 -0
- package/dist/adapters/sqlite-wasm-worker.js +725 -0
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -0
- package/dist/adapters/sqlite-wasm.cjs +1634 -0
- package/dist/adapters/sqlite-wasm.cjs.map +1 -0
- package/dist/adapters/sqlite-wasm.d.cts +361 -0
- package/dist/adapters/sqlite-wasm.d.ts +361 -0
- package/dist/adapters/sqlite-wasm.js +122 -0
- package/dist/adapters/sqlite-wasm.js.map +1 -0
- package/dist/audit-trace-store-AM4EWQNZ.js +15 -0
- package/dist/audit-trace-store-AM4EWQNZ.js.map +1 -0
- package/dist/backup-IHKVMUAM.js +25 -0
- package/dist/backup-IHKVMUAM.js.map +1 -0
- package/dist/blob-fs.cjs +166 -0
- package/dist/blob-fs.cjs.map +1 -0
- package/dist/blob-fs.d.cts +29 -0
- package/dist/blob-fs.d.ts +29 -0
- package/dist/blob-fs.js +128 -0
- package/dist/blob-fs.js.map +1 -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-5ZRRF6R2.js +189 -0
- package/dist/chunk-5ZRRF6R2.js.map +1 -0
- 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-J5AGXF3D.js +306 -0
- 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-MYSJN3DF.js +117 -0
- package/dist/chunk-MYSJN3DF.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-TVJEC5S2.js +40 -0
- package/dist/chunk-TVJEC5S2.js.map +1 -0
- package/dist/chunk-UC2LGFPV.js +810 -0
- package/dist/chunk-UC2LGFPV.js.map +1 -0
- package/dist/chunk-W4MTTNEZ.js +55 -0
- package/dist/chunk-W4MTTNEZ.js.map +1 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
- package/dist/export-audit-J2QXRHD2.js +15 -0
- package/dist/export-audit-J2QXRHD2.js.map +1 -0
- package/dist/index.cjs +11272 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2843 -0
- package/dist/index.d.ts +2843 -0
- package/dist/index.js +1334 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.cjs +1202 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +84 -0
- package/dist/internal.d.ts +84 -0
- package/dist/internal.js +50 -0
- package/dist/internal.js.map +1 -0
- 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/sqlite-wasm-channel-QCCNBYCU.js.map +1 -0
- package/dist/store-HNGX4KTT.js +16 -0
- package/dist/store-HNGX4KTT.js.map +1 -0
- package/dist/types-Dh-xyUjP.d.cts +356 -0
- package/dist/types-Dh-xyUjP.d.ts +356 -0
- package/package.json +120 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dr. Obed Ehoneah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @korajs/store
|
|
2
|
+
|
|
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.
|
|
6
|
+
|
|
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>`.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @korajs/store@beta
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage through createApp
|
|
18
|
+
|
|
19
|
+
<!-- docs-check: standalone -->
|
|
20
|
+
```typescript
|
|
21
|
+
import { createApp, defineSchema, t } from 'korajs'
|
|
22
|
+
|
|
23
|
+
const schema = defineSchema({
|
|
24
|
+
version: 1,
|
|
25
|
+
collections: {
|
|
26
|
+
todos: {
|
|
27
|
+
fields: {
|
|
28
|
+
title: t.string(),
|
|
29
|
+
completed: t.boolean().default(false),
|
|
30
|
+
createdAt: t.timestamp().auto(),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const app = createApp({ schema })
|
|
37
|
+
await app.ready
|
|
38
|
+
|
|
39
|
+
await app.todos.insert({ title: 'Ship Kora v1' })
|
|
40
|
+
const active = await app.todos.where({ completed: false }).orderBy('createdAt', 'desc').limit(10).exec()
|
|
41
|
+
|
|
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)
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Storage adapters
|
|
49
|
+
|
|
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 |
|
|
55
|
+
|
|
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`.
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
[Storage Configuration](https://korajs.dev/guide/storage-configuration) and the
|
|
63
|
+
[Store API reference](https://korajs.dev/api/store).
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/adapters/better-sqlite3.ts
|
|
31
|
+
var better_sqlite3_exports = {};
|
|
32
|
+
__export(better_sqlite3_exports, {
|
|
33
|
+
BetterSqlite3Adapter: () => BetterSqlite3Adapter
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(better_sqlite3_exports);
|
|
36
|
+
|
|
37
|
+
// src/adapters/better-sqlite3-adapter.ts
|
|
38
|
+
var import_core2 = require("@korajs/core");
|
|
39
|
+
|
|
40
|
+
// src/errors.ts
|
|
41
|
+
var import_core = require("@korajs/core");
|
|
42
|
+
var AdapterError = class extends import_core.KoraError {
|
|
43
|
+
constructor(message, context) {
|
|
44
|
+
super(message, "ADAPTER_ERROR", context);
|
|
45
|
+
this.name = "AdapterError";
|
|
46
|
+
}
|
|
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
|
+
};
|
|
64
|
+
var StoreNotOpenError = class extends import_core.KoraError {
|
|
65
|
+
constructor() {
|
|
66
|
+
super("Store is not open. Call store.open() before performing operations.", "STORE_NOT_OPEN");
|
|
67
|
+
this.name = "StoreNotOpenError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
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
|
+
|
|
83
|
+
// src/adapters/mutex.ts
|
|
84
|
+
var Mutex = class {
|
|
85
|
+
locked = false;
|
|
86
|
+
waiters = [];
|
|
87
|
+
/**
|
|
88
|
+
* Acquire the mutex. Resolves with a release function once the lock is held.
|
|
89
|
+
* If the mutex is already held, the caller waits (FIFO) until it is released.
|
|
90
|
+
*/
|
|
91
|
+
async acquire() {
|
|
92
|
+
if (!this.locked) {
|
|
93
|
+
this.locked = true;
|
|
94
|
+
return this.createRelease();
|
|
95
|
+
}
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
this.waiters.push(() => {
|
|
98
|
+
resolve(this.createRelease());
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
createRelease() {
|
|
103
|
+
let released = false;
|
|
104
|
+
return () => {
|
|
105
|
+
if (released) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
released = true;
|
|
109
|
+
const next = this.waiters.shift();
|
|
110
|
+
if (next) {
|
|
111
|
+
next();
|
|
112
|
+
} else {
|
|
113
|
+
this.locked = false;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/adapters/better-sqlite3-adapter.ts
|
|
120
|
+
var STATEMENT_CACHE_SIZE = 256;
|
|
121
|
+
var BetterSqlite3Adapter = class {
|
|
122
|
+
/**
|
|
123
|
+
* @param path - Database file path, or ':memory:' for in-memory database
|
|
124
|
+
*/
|
|
125
|
+
constructor(path = ":memory:") {
|
|
126
|
+
this.path = path;
|
|
127
|
+
}
|
|
128
|
+
path;
|
|
129
|
+
db = null;
|
|
130
|
+
statements = /* @__PURE__ */ new Map();
|
|
131
|
+
statementsDb = null;
|
|
132
|
+
/**
|
|
133
|
+
* Serializes transactions. better-sqlite3 is synchronous, but our
|
|
134
|
+
* `transaction()` is async and yields the event loop at each `await` inside
|
|
135
|
+
* the callback. Without this mutex, a transaction started while another is
|
|
136
|
+
* mid-flight (e.g. a relayed remote operation applied during a local write)
|
|
137
|
+
* issues a nested `BEGIN`, which SQLite rejects, and the operation is lost.
|
|
138
|
+
*/
|
|
139
|
+
txMutex = new Mutex();
|
|
140
|
+
async open(schema) {
|
|
141
|
+
const BetterSqlite3 = (await import("better-sqlite3")).default;
|
|
142
|
+
this.db = new BetterSqlite3(this.path);
|
|
143
|
+
this.db.pragma("journal_mode = WAL");
|
|
144
|
+
this.db.pragma("foreign_keys = ON");
|
|
145
|
+
const statements = (0, import_core2.generateFullDDL)(schema);
|
|
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
|
+
}
|
|
159
|
+
if (sql.startsWith("--kora:safe-alter")) {
|
|
160
|
+
try {
|
|
161
|
+
this.db.exec(sql.replace("--kora:safe-alter\n", ""));
|
|
162
|
+
} catch (e) {
|
|
163
|
+
const msg = e.message || "";
|
|
164
|
+
if (!msg.includes("duplicate column name") && !msg.includes("Cannot add a NOT NULL column with default value NULL")) {
|
|
165
|
+
throw e;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
this.db.exec(sql);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async close() {
|
|
174
|
+
if (this.db) {
|
|
175
|
+
this.statements.clear();
|
|
176
|
+
this.db.close();
|
|
177
|
+
this.db = null;
|
|
178
|
+
}
|
|
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
|
+
*/
|
|
186
|
+
async execute(sql, params) {
|
|
187
|
+
const db = this.getDb();
|
|
188
|
+
const release = await this.txMutex.acquire();
|
|
189
|
+
try {
|
|
190
|
+
this.statement(db, sql).run(...params ?? []);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw new AdapterError(`Execute failed: ${error.message}`, {
|
|
193
|
+
sql,
|
|
194
|
+
params
|
|
195
|
+
});
|
|
196
|
+
} finally {
|
|
197
|
+
release();
|
|
198
|
+
}
|
|
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
|
+
*/
|
|
205
|
+
async query(sql, params) {
|
|
206
|
+
const db = this.getDb();
|
|
207
|
+
const release = await this.txMutex.acquire();
|
|
208
|
+
try {
|
|
209
|
+
return this.statement(db, sql).all(...params ?? []);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new AdapterError(`Query failed: ${error.message}`, {
|
|
212
|
+
sql,
|
|
213
|
+
params
|
|
214
|
+
});
|
|
215
|
+
} finally {
|
|
216
|
+
release();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async transaction(fn) {
|
|
220
|
+
const db = this.getDb();
|
|
221
|
+
const release = await this.txMutex.acquire();
|
|
222
|
+
try {
|
|
223
|
+
db.exec("BEGIN");
|
|
224
|
+
try {
|
|
225
|
+
const tx = {
|
|
226
|
+
execute: async (sql, params) => {
|
|
227
|
+
try {
|
|
228
|
+
this.statement(db, sql).run(...params ?? []);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
throw new AdapterError(`Transaction execute failed: ${error.message}`, {
|
|
231
|
+
sql,
|
|
232
|
+
params
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
query: async (sql, params) => {
|
|
237
|
+
try {
|
|
238
|
+
return this.statement(db, sql).all(...params ?? []);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
throw new AdapterError(`Transaction query failed: ${error.message}`, {
|
|
241
|
+
sql,
|
|
242
|
+
params
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
await fn(tx);
|
|
248
|
+
db.exec("COMMIT");
|
|
249
|
+
} catch (error) {
|
|
250
|
+
db.exec("ROLLBACK");
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
} finally {
|
|
254
|
+
release();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async migrate(from, to, migration) {
|
|
258
|
+
const db = this.getDb();
|
|
259
|
+
const release = await this.txMutex.acquire();
|
|
260
|
+
try {
|
|
261
|
+
db.exec("BEGIN");
|
|
262
|
+
try {
|
|
263
|
+
for (const sql of migration.statements) {
|
|
264
|
+
db.exec(sql);
|
|
265
|
+
}
|
|
266
|
+
db.exec("COMMIT");
|
|
267
|
+
} catch (error) {
|
|
268
|
+
db.exec("ROLLBACK");
|
|
269
|
+
throw new AdapterError(
|
|
270
|
+
`Migration from v${from} to v${to} failed: ${error.message}`,
|
|
271
|
+
{
|
|
272
|
+
from,
|
|
273
|
+
to
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
release();
|
|
279
|
+
}
|
|
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
|
+
}
|
|
306
|
+
getDb() {
|
|
307
|
+
if (!this.db) {
|
|
308
|
+
throw new StoreNotOpenError();
|
|
309
|
+
}
|
|
310
|
+
return this.db;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
314
|
+
0 && (module.exports = {
|
|
315
|
+
BetterSqlite3Adapter
|
|
316
|
+
});
|
|
317
|
+
//# sourceMappingURL=better-sqlite3.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SchemaDefinition } from '@korajs/core';
|
|
2
|
+
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-Dh-xyUjP.cjs';
|
|
3
|
+
|
|
4
|
+
declare class BetterSqlite3Adapter implements StorageAdapter {
|
|
5
|
+
private readonly path;
|
|
6
|
+
private db;
|
|
7
|
+
private readonly statements;
|
|
8
|
+
private statementsDb;
|
|
9
|
+
/**
|
|
10
|
+
* Serializes transactions. better-sqlite3 is synchronous, but our
|
|
11
|
+
* `transaction()` is async and yields the event loop at each `await` inside
|
|
12
|
+
* the callback. Without this mutex, a transaction started while another is
|
|
13
|
+
* mid-flight (e.g. a relayed remote operation applied during a local write)
|
|
14
|
+
* issues a nested `BEGIN`, which SQLite rejects, and the operation is lost.
|
|
15
|
+
*/
|
|
16
|
+
private readonly txMutex;
|
|
17
|
+
/**
|
|
18
|
+
* @param path - Database file path, or ':memory:' for in-memory database
|
|
19
|
+
*/
|
|
20
|
+
constructor(path?: string);
|
|
21
|
+
open(schema: SchemaDefinition): Promise<void>;
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
35
|
+
query<T>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
36
|
+
transaction(fn: (tx: Transaction) => Promise<void>): Promise<void>;
|
|
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;
|
|
45
|
+
private getDb;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { BetterSqlite3Adapter };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SchemaDefinition } from '@korajs/core';
|
|
2
|
+
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-Dh-xyUjP.js';
|
|
3
|
+
|
|
4
|
+
declare class BetterSqlite3Adapter implements StorageAdapter {
|
|
5
|
+
private readonly path;
|
|
6
|
+
private db;
|
|
7
|
+
private readonly statements;
|
|
8
|
+
private statementsDb;
|
|
9
|
+
/**
|
|
10
|
+
* Serializes transactions. better-sqlite3 is synchronous, but our
|
|
11
|
+
* `transaction()` is async and yields the event loop at each `await` inside
|
|
12
|
+
* the callback. Without this mutex, a transaction started while another is
|
|
13
|
+
* mid-flight (e.g. a relayed remote operation applied during a local write)
|
|
14
|
+
* issues a nested `BEGIN`, which SQLite rejects, and the operation is lost.
|
|
15
|
+
*/
|
|
16
|
+
private readonly txMutex;
|
|
17
|
+
/**
|
|
18
|
+
* @param path - Database file path, or ':memory:' for in-memory database
|
|
19
|
+
*/
|
|
20
|
+
constructor(path?: string);
|
|
21
|
+
open(schema: SchemaDefinition): Promise<void>;
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
35
|
+
query<T>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
36
|
+
transaction(fn: (tx: Transaction) => Promise<void>): Promise<void>;
|
|
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;
|
|
45
|
+
private getDb;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { BetterSqlite3Adapter };
|