@meshline/storage-node 0.0.0-stage → 0.1.0-alpha.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Meshline contributors
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 CHANGED
@@ -1,3 +1,39 @@
1
- # Temporary Holding Version
1
+ # Meshline SQLite storage for Node.js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `NodeSqliteStore` provides persistent transactional storage for a Meshline
4
+ application on **Node.js 24 or later**. The package is ESM with TypeScript declarations.
5
+
6
+ ## Install and configure
7
+
8
+ Install this adapter and the matching core together:
9
+
10
+ ```sh
11
+ npm install @meshline/sdk@0.1.0-alpha.1 @meshline/storage-node@0.1.0-alpha.1
12
+ ```
13
+
14
+ Import `NodeSqliteStore` from `@meshline/storage-node` and construct it
15
+ with a database path. Create the parent directory yourself. Construction does no
16
+ I/O; call `migrate()`, then pass the store to `MeshlineClient` and initialize the
17
+ client. Direct store consumers call `initialize({ context, accountId })` instead.
18
+
19
+ The [Node session example](https://github.com/meshline-network/sdk/blob/main/typescript/examples/node.ts)
20
+ also configures `@meshline/transport-node` and handles cleanup.
21
+
22
+ ## Persistence
23
+
24
+ One database stores one network/account/local device. Retain it with the same
25
+ persistent secret protector across restarts. WAL and full synchronization are
26
+ enabled. Transactions atomically compare revisions and commit records plus cursors.
27
+ Conflicts surface to the caller.
28
+
29
+ Query readers hold their own read-only transaction and must be disposed.
30
+ Dispose the store after the client, relay pool, and readers have stopped.
31
+ Database contents are not encrypted; the SDK's secret protector covers selected
32
+ secrets, not message text or the entire file. This database format is not
33
+ interchangeable with the .NET SDK's SQLite format.
34
+
35
+ Database calls use synchronous `node:sqlite`. Keep large workloads in an
36
+ application-owned worker when event-loop latency matters.
37
+
38
+ [Storage guide](https://github.com/meshline-network/sdk/blob/main/typescript/docs/guides/storage-and-pagination.md)
39
+ · [Platform limits](https://github.com/meshline-network/sdk/blob/main/typescript/docs/platforms.md)
@@ -0,0 +1,20 @@
1
+ import { type MeshlineStore, type QueryReader, type RecordQuery, type StorageBinding, type StoredRecord, type StoreMutation, type StoreSnapshot } from '@meshline/sdk';
2
+ /** SQLite WAL storage with explicit migration, account binding, and fixed readers. */
3
+ export declare class NodeSqliteStore implements MeshlineStore {
4
+ readonly path: string;
5
+ private database;
6
+ private initialized;
7
+ private disposed;
8
+ private disposal;
9
+ private readonly readers;
10
+ constructor(path: string);
11
+ migrate(signal?: AbortSignal): Promise<void>;
12
+ initialize(binding: StorageBinding, signal?: AbortSignal): Promise<void>;
13
+ read(queries: readonly RecordQuery[], signal?: AbortSignal): Promise<StoreSnapshot>;
14
+ commit(expectedVersion: number, mutations: readonly StoreMutation[], signal?: AbortSignal): Promise<number>;
15
+ openQuery(query: RecordQuery, signal?: AbortSignal): Promise<QueryReader<StoredRecord>>;
16
+ dispose(): Promise<void>;
17
+ private assertOpen;
18
+ private connection;
19
+ }
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAGH,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAC3E,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAC5D,MAAM,eAAe,CAAC;AAiBvB,sFAAsF;AACtF,qBAAa,eAAgB,YAAW,aAAa;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IAEnD,YAAY,IAAI,EAAE,MAAM,EAGvB;IAEK,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBjD;IAEK,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB7E;IAEK,IAAI,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAaxF;IAEK,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBhH;IAEK,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAa5F;IAED,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAWvB;IAED,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,UAAU;CAKrB"}
package/dist/index.js ADDED
@@ -0,0 +1,249 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { resolve } from 'node:path';
3
+ import { statSync } from 'node:fs';
4
+ import { StateConflictError, bindingJson, canonicalJson, parseJson, prepareCommit, requireBatchCount, requireObject, throwIfAborted, validateRecordQuery, } from '@meshline/sdk';
5
+ function decodeRow(row) {
6
+ return { collection: row.collection, key: row.key, revision: row.revision, value: requireObject(parseJson(row.value)) };
7
+ }
8
+ function selection(query) {
9
+ validateRecordQuery(query);
10
+ const parameters = [query.collection];
11
+ let where = 'collection = ?';
12
+ if (query.key !== undefined) {
13
+ where += ' AND key = ?';
14
+ parameters.push(query.key);
15
+ }
16
+ else if (query.prefix !== undefined) {
17
+ where += ' AND key >= ? AND key < ?';
18
+ parameters.push(query.prefix, query.prefix + '\uffff');
19
+ }
20
+ return { where, parameters, order: query.reverse ? 'DESC' : 'ASC' };
21
+ }
22
+ /** SQLite WAL storage with explicit migration, account binding, and fixed readers. */
23
+ export class NodeSqliteStore {
24
+ path;
25
+ database;
26
+ initialized = false;
27
+ disposed = false;
28
+ disposal;
29
+ readers = new Set();
30
+ constructor(path) {
31
+ if (!path.trim() || path === ':memory:')
32
+ throw new TypeError('A persistent SQLite file path is required.');
33
+ this.path = resolve(path);
34
+ }
35
+ async migrate(signal) {
36
+ this.assertOpen();
37
+ throwIfAborted(signal);
38
+ if (this.database)
39
+ throw new Error('Migrate before initializing the store.');
40
+ const database = new DatabaseSync(this.path);
41
+ try {
42
+ database.exec('PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL; BEGIN IMMEDIATE;');
43
+ try {
44
+ const version = database.prepare('PRAGMA user_version').get().user_version;
45
+ if (version > 1)
46
+ throw new Error(`Unsupported database schema ${version}.`);
47
+ if (version === 0)
48
+ database.exec(`
49
+ CREATE TABLE meshline_meta (id INTEGER PRIMARY KEY CHECK(id = 1), binding TEXT, version INTEGER NOT NULL CHECK(version >= 0));
50
+ INSERT INTO meshline_meta VALUES (1, NULL, 0);
51
+ CREATE TABLE meshline_records (collection TEXT NOT NULL, key TEXT NOT NULL, revision INTEGER NOT NULL, value TEXT NOT NULL,
52
+ PRIMARY KEY(collection, key)) WITHOUT ROWID;
53
+ PRAGMA user_version = 1;
54
+ `);
55
+ throwIfAborted(signal);
56
+ database.exec('COMMIT');
57
+ }
58
+ catch (error) {
59
+ database.exec('ROLLBACK');
60
+ throw error;
61
+ }
62
+ }
63
+ finally {
64
+ database.close();
65
+ }
66
+ }
67
+ async initialize(binding, signal) {
68
+ this.assertOpen();
69
+ throwIfAborted(signal);
70
+ const encoded = bindingJson(binding);
71
+ if (this.initialized) {
72
+ const existing = this.connection().prepare('SELECT binding FROM meshline_meta WHERE id = 1').get();
73
+ if (existing.binding !== encoded)
74
+ throw new Error('Database is bound to another network or account.');
75
+ return;
76
+ }
77
+ if (!statSync(this.path).isFile())
78
+ throw new Error('Database must be explicitly migrated before initialization.');
79
+ const database = new DatabaseSync(this.path, { open: true, readOnly: false });
80
+ try {
81
+ const version = database.prepare('PRAGMA user_version').get().user_version;
82
+ if (version !== 1)
83
+ throw new Error('Database must be explicitly migrated before initialization.');
84
+ database.exec('PRAGMA busy_timeout = 5000; PRAGMA synchronous = FULL; BEGIN IMMEDIATE;');
85
+ try {
86
+ const current = database.prepare('SELECT binding FROM meshline_meta WHERE id = 1').get();
87
+ if (current.binding !== null && current.binding !== encoded)
88
+ throw new Error('Database is bound to another network or account.');
89
+ if (current.binding === null)
90
+ database.prepare('UPDATE meshline_meta SET binding = ? WHERE id = 1').run(encoded);
91
+ throwIfAborted(signal);
92
+ database.exec('COMMIT');
93
+ }
94
+ catch (error) {
95
+ database.exec('ROLLBACK');
96
+ throw error;
97
+ }
98
+ this.database = database;
99
+ this.initialized = true;
100
+ }
101
+ catch (error) {
102
+ database.close();
103
+ throw error;
104
+ }
105
+ }
106
+ async read(queries, signal) {
107
+ const selections = queries.map(selection);
108
+ throwIfAborted(signal);
109
+ const database = this.connection();
110
+ database.exec('BEGIN DEFERRED');
111
+ try {
112
+ const version = database.prepare('SELECT version FROM meshline_meta WHERE id = 1').get().version;
113
+ const sets = selections.map(query => database.prepare(`SELECT * FROM meshline_records WHERE ${query.where} ORDER BY key ${query.order}`)
114
+ .all(...query.parameters).map(decodeRow));
115
+ throwIfAborted(signal);
116
+ database.exec('COMMIT');
117
+ return { version, sets };
118
+ }
119
+ catch (error) {
120
+ database.exec('ROLLBACK');
121
+ throw error;
122
+ }
123
+ }
124
+ async commit(expectedVersion, mutations, signal) {
125
+ const changes = prepareCommit(expectedVersion, mutations);
126
+ throwIfAborted(signal);
127
+ const database = this.connection();
128
+ database.exec('BEGIN IMMEDIATE');
129
+ try {
130
+ const version = database.prepare('SELECT version FROM meshline_meta WHERE id = 1').get().version;
131
+ if (version !== expectedVersion)
132
+ throw new StateConflictError('Database revision changed before commit.');
133
+ const next = expectedVersion + 1;
134
+ const put = database.prepare('INSERT INTO meshline_records VALUES (?, ?, ?, ?) ON CONFLICT(collection, key) DO UPDATE SET revision = excluded.revision, value = excluded.value');
135
+ const remove = database.prepare('DELETE FROM meshline_records WHERE collection = ? AND key = ?');
136
+ for (const change of changes) {
137
+ if (change.kind === 'put')
138
+ put.run(change.collection, change.key, next, canonicalJson(change.value));
139
+ else
140
+ remove.run(change.collection, change.key);
141
+ }
142
+ database.prepare('UPDATE meshline_meta SET version = ? WHERE id = 1').run(next);
143
+ throwIfAborted(signal);
144
+ database.exec('COMMIT');
145
+ return next;
146
+ }
147
+ catch (error) {
148
+ database.exec('ROLLBACK');
149
+ throw error;
150
+ }
151
+ }
152
+ async openQuery(query, signal) {
153
+ this.connection();
154
+ const selected = selection(query);
155
+ throwIfAborted(signal);
156
+ const database = new DatabaseSync(this.path, { readOnly: true });
157
+ try {
158
+ database.exec('PRAGMA busy_timeout = 5000; BEGIN DEFERRED;');
159
+ // Fix the WAL snapshot now, not on the first application read.
160
+ database.prepare('SELECT version FROM meshline_meta WHERE id = 1').get();
161
+ const reader = new SqliteReader(database, selected, () => this.readers.delete(reader));
162
+ this.readers.add(reader);
163
+ return reader;
164
+ }
165
+ catch (error) {
166
+ database.close();
167
+ throw error;
168
+ }
169
+ }
170
+ dispose() {
171
+ if (this.disposal)
172
+ return this.disposal;
173
+ this.disposed = true;
174
+ this.disposal = (async () => {
175
+ const errors = [];
176
+ for (const reader of this.readers) {
177
+ try {
178
+ await reader.dispose();
179
+ }
180
+ catch (error) {
181
+ errors.push(error);
182
+ }
183
+ }
184
+ try {
185
+ this.database?.close();
186
+ }
187
+ catch (error) {
188
+ errors.push(error);
189
+ }
190
+ this.database = undefined;
191
+ if (errors.length)
192
+ throw new AggregateError(errors, 'Failed to dispose SQLite storage.');
193
+ })();
194
+ return this.disposal;
195
+ }
196
+ assertOpen() { if (this.disposed)
197
+ throw new Error('Store is disposed.'); }
198
+ connection() {
199
+ this.assertOpen();
200
+ if (!this.initialized || !this.database)
201
+ throw new Error('Store is not initialized.');
202
+ return this.database;
203
+ }
204
+ }
205
+ class SqliteReader {
206
+ database;
207
+ query;
208
+ released;
209
+ offset = 0;
210
+ disposed = false;
211
+ completed = false;
212
+ constructor(database, query, released) {
213
+ this.database = database;
214
+ this.query = query;
215
+ this.released = released;
216
+ }
217
+ async readNext(count, signal) {
218
+ if (this.disposed)
219
+ throw new Error('Reader is disposed.');
220
+ requireBatchCount(count);
221
+ throwIfAborted(signal);
222
+ if (this.completed)
223
+ return [];
224
+ const rows = this.database.prepare(`SELECT * FROM meshline_records WHERE ${this.query.where} ORDER BY key ${this.query.order} LIMIT ? OFFSET ?`)
225
+ .all(...this.query.parameters, count, this.offset);
226
+ const values = rows.map(decodeRow);
227
+ throwIfAborted(signal);
228
+ this.offset += values.length;
229
+ this.completed = values.length < count;
230
+ return values;
231
+ }
232
+ async dispose() {
233
+ if (this.disposed)
234
+ return;
235
+ this.disposed = true;
236
+ try {
237
+ this.database.exec('ROLLBACK');
238
+ }
239
+ finally {
240
+ try {
241
+ this.database.close();
242
+ }
243
+ finally {
244
+ this.released();
245
+ }
246
+ }
247
+ }
248
+ }
249
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@meshline/storage-node",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Transactional SQLite storage for the Meshline TypeScript SDK",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "engines": {
9
+ "node": ">=24.0.0"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "src",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "peerDependencies": {
24
+ "@meshline/sdk": "0.1.0-alpha.1"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/meshline-network/sdk.git",
29
+ "directory": "typescript/packages/storage-node"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org"
34
+ }
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,190 @@
1
+ import { DatabaseSync, type SQLInputValue } from 'node:sqlite';
2
+ import { resolve } from 'node:path';
3
+ import { statSync } from 'node:fs';
4
+ import {
5
+ StateConflictError, bindingJson, canonicalJson, parseJson, prepareCommit,
6
+ requireBatchCount, requireObject, throwIfAborted, validateRecordQuery,
7
+ type MeshlineStore, type QueryReader, type RecordQuery, type StorageBinding,
8
+ type StoredRecord, type StoreMutation, type StoreSnapshot,
9
+ } from '@meshline/sdk';
10
+
11
+ interface Row { collection: string; key: string; revision: number; value: string }
12
+
13
+ function decodeRow(row: Row): StoredRecord {
14
+ return { collection: row.collection, key: row.key, revision: row.revision, value: requireObject(parseJson(row.value)) };
15
+ }
16
+
17
+ function selection(query: RecordQuery): { where: string; parameters: SQLInputValue[]; order: string } {
18
+ validateRecordQuery(query);
19
+ const parameters: SQLInputValue[] = [query.collection];
20
+ let where = 'collection = ?';
21
+ if (query.key !== undefined) { where += ' AND key = ?'; parameters.push(query.key); }
22
+ else if (query.prefix !== undefined) { where += ' AND key >= ? AND key < ?'; parameters.push(query.prefix, query.prefix + '\uffff'); }
23
+ return { where, parameters, order: query.reverse ? 'DESC' : 'ASC' };
24
+ }
25
+
26
+ /** SQLite WAL storage with explicit migration, account binding, and fixed readers. */
27
+ export class NodeSqliteStore implements MeshlineStore {
28
+ readonly path: string;
29
+ private database: DatabaseSync | undefined;
30
+ private initialized = false;
31
+ private disposed = false;
32
+ private disposal: Promise<void> | undefined;
33
+ private readonly readers = new Set<SqliteReader>();
34
+
35
+ constructor(path: string) {
36
+ if (!path.trim() || path === ':memory:') throw new TypeError('A persistent SQLite file path is required.');
37
+ this.path = resolve(path);
38
+ }
39
+
40
+ async migrate(signal?: AbortSignal): Promise<void> {
41
+ this.assertOpen();
42
+ throwIfAborted(signal);
43
+ if (this.database) throw new Error('Migrate before initializing the store.');
44
+ const database = new DatabaseSync(this.path);
45
+ try {
46
+ database.exec('PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL; BEGIN IMMEDIATE;');
47
+ try {
48
+ const version = (database.prepare('PRAGMA user_version').get() as { user_version: number }).user_version;
49
+ if (version > 1) throw new Error(`Unsupported database schema ${version}.`);
50
+ if (version === 0) database.exec(`
51
+ CREATE TABLE meshline_meta (id INTEGER PRIMARY KEY CHECK(id = 1), binding TEXT, version INTEGER NOT NULL CHECK(version >= 0));
52
+ INSERT INTO meshline_meta VALUES (1, NULL, 0);
53
+ CREATE TABLE meshline_records (collection TEXT NOT NULL, key TEXT NOT NULL, revision INTEGER NOT NULL, value TEXT NOT NULL,
54
+ PRIMARY KEY(collection, key)) WITHOUT ROWID;
55
+ PRAGMA user_version = 1;
56
+ `);
57
+ throwIfAborted(signal);
58
+ database.exec('COMMIT');
59
+ } catch (error) { database.exec('ROLLBACK'); throw error; }
60
+ } finally { database.close(); }
61
+ }
62
+
63
+ async initialize(binding: StorageBinding, signal?: AbortSignal): Promise<void> {
64
+ this.assertOpen();
65
+ throwIfAborted(signal);
66
+ const encoded = bindingJson(binding);
67
+ if (this.initialized) {
68
+ const existing = this.connection().prepare('SELECT binding FROM meshline_meta WHERE id = 1').get() as { binding: string };
69
+ if (existing.binding !== encoded) throw new Error('Database is bound to another network or account.');
70
+ return;
71
+ }
72
+ if (!statSync(this.path).isFile()) throw new Error('Database must be explicitly migrated before initialization.');
73
+ const database = new DatabaseSync(this.path, { open: true, readOnly: false });
74
+ try {
75
+ const version = (database.prepare('PRAGMA user_version').get() as { user_version: number }).user_version;
76
+ if (version !== 1) throw new Error('Database must be explicitly migrated before initialization.');
77
+ database.exec('PRAGMA busy_timeout = 5000; PRAGMA synchronous = FULL; BEGIN IMMEDIATE;');
78
+ try {
79
+ const current = database.prepare('SELECT binding FROM meshline_meta WHERE id = 1').get() as { binding: string | null };
80
+ if (current.binding !== null && current.binding !== encoded) throw new Error('Database is bound to another network or account.');
81
+ if (current.binding === null) database.prepare('UPDATE meshline_meta SET binding = ? WHERE id = 1').run(encoded);
82
+ throwIfAborted(signal);
83
+ database.exec('COMMIT');
84
+ } catch (error) { database.exec('ROLLBACK'); throw error; }
85
+ this.database = database;
86
+ this.initialized = true;
87
+ } catch (error) { database.close(); throw error; }
88
+ }
89
+
90
+ async read(queries: readonly RecordQuery[], signal?: AbortSignal): Promise<StoreSnapshot> {
91
+ const selections = queries.map(selection);
92
+ throwIfAborted(signal);
93
+ const database = this.connection();
94
+ database.exec('BEGIN DEFERRED');
95
+ try {
96
+ const version = (database.prepare('SELECT version FROM meshline_meta WHERE id = 1').get() as { version: number }).version;
97
+ const sets = selections.map(query => (database.prepare(`SELECT * FROM meshline_records WHERE ${query.where} ORDER BY key ${query.order}`)
98
+ .all(...query.parameters) as unknown as Row[]).map(decodeRow));
99
+ throwIfAborted(signal);
100
+ database.exec('COMMIT');
101
+ return { version, sets };
102
+ } catch (error) { database.exec('ROLLBACK'); throw error; }
103
+ }
104
+
105
+ async commit(expectedVersion: number, mutations: readonly StoreMutation[], signal?: AbortSignal): Promise<number> {
106
+ const changes = prepareCommit(expectedVersion, mutations);
107
+ throwIfAborted(signal);
108
+ const database = this.connection();
109
+ database.exec('BEGIN IMMEDIATE');
110
+ try {
111
+ const version = (database.prepare('SELECT version FROM meshline_meta WHERE id = 1').get() as { version: number }).version;
112
+ if (version !== expectedVersion) throw new StateConflictError('Database revision changed before commit.');
113
+ const next = expectedVersion + 1;
114
+ const put = database.prepare('INSERT INTO meshline_records VALUES (?, ?, ?, ?) ON CONFLICT(collection, key) DO UPDATE SET revision = excluded.revision, value = excluded.value');
115
+ const remove = database.prepare('DELETE FROM meshline_records WHERE collection = ? AND key = ?');
116
+ for (const change of changes) {
117
+ if (change.kind === 'put') put.run(change.collection, change.key, next, canonicalJson(change.value));
118
+ else remove.run(change.collection, change.key);
119
+ }
120
+ database.prepare('UPDATE meshline_meta SET version = ? WHERE id = 1').run(next);
121
+ throwIfAborted(signal);
122
+ database.exec('COMMIT');
123
+ return next;
124
+ } catch (error) { database.exec('ROLLBACK'); throw error; }
125
+ }
126
+
127
+ async openQuery(query: RecordQuery, signal?: AbortSignal): Promise<QueryReader<StoredRecord>> {
128
+ this.connection();
129
+ const selected = selection(query);
130
+ throwIfAborted(signal);
131
+ const database = new DatabaseSync(this.path, { readOnly: true });
132
+ try {
133
+ database.exec('PRAGMA busy_timeout = 5000; BEGIN DEFERRED;');
134
+ // Fix the WAL snapshot now, not on the first application read.
135
+ database.prepare('SELECT version FROM meshline_meta WHERE id = 1').get();
136
+ const reader = new SqliteReader(database, selected, () => this.readers.delete(reader));
137
+ this.readers.add(reader);
138
+ return reader;
139
+ } catch (error) { database.close(); throw error; }
140
+ }
141
+
142
+ dispose(): Promise<void> {
143
+ if (this.disposal) return this.disposal;
144
+ this.disposed = true;
145
+ this.disposal = (async () => {
146
+ const errors: unknown[] = [];
147
+ for (const reader of this.readers) { try { await reader.dispose(); } catch (error) { errors.push(error); } }
148
+ try { this.database?.close(); } catch (error) { errors.push(error); }
149
+ this.database = undefined;
150
+ if (errors.length) throw new AggregateError(errors, 'Failed to dispose SQLite storage.');
151
+ })();
152
+ return this.disposal;
153
+ }
154
+
155
+ private assertOpen(): void { if (this.disposed) throw new Error('Store is disposed.'); }
156
+ private connection(): DatabaseSync {
157
+ this.assertOpen();
158
+ if (!this.initialized || !this.database) throw new Error('Store is not initialized.');
159
+ return this.database;
160
+ }
161
+ }
162
+
163
+ class SqliteReader implements QueryReader<StoredRecord> {
164
+ private offset = 0;
165
+ private disposed = false;
166
+ private completed = false;
167
+
168
+ constructor(private readonly database: DatabaseSync, private readonly query: ReturnType<typeof selection>, private readonly released: () => void) {}
169
+
170
+ async readNext(count: number, signal?: AbortSignal): Promise<readonly StoredRecord[]> {
171
+ if (this.disposed) throw new Error('Reader is disposed.');
172
+ requireBatchCount(count);
173
+ throwIfAborted(signal);
174
+ if (this.completed) return [];
175
+ const rows = this.database.prepare(`SELECT * FROM meshline_records WHERE ${this.query.where} ORDER BY key ${this.query.order} LIMIT ? OFFSET ?`)
176
+ .all(...this.query.parameters, count, this.offset) as unknown as Row[];
177
+ const values = rows.map(decodeRow);
178
+ throwIfAborted(signal);
179
+ this.offset += values.length;
180
+ this.completed = values.length < count;
181
+ return values;
182
+ }
183
+
184
+ async dispose(): Promise<void> {
185
+ if (this.disposed) return;
186
+ this.disposed = true;
187
+ try { this.database.exec('ROLLBACK'); }
188
+ finally { try { this.database.close(); } finally { this.released(); } }
189
+ }
190
+ }