@rotorsoft/act-sqlite 1.22.4 → 1.22.5
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 +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/sqlite-store.d.ts +11 -5
- package/dist/@types/sqlite-store.d.ts.map +1 -1
- package/dist/index.cjs +149 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +152 -137
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/sqlite-store.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createClient
|
|
4
|
+
} from "@libsql/client";
|
|
3
5
|
import {
|
|
4
6
|
ConcurrencyError,
|
|
5
7
|
is_literal_source,
|
|
@@ -117,8 +119,8 @@ var SqliteStore = class {
|
|
|
117
119
|
return parsed;
|
|
118
120
|
}
|
|
119
121
|
async seed() {
|
|
120
|
-
await this.
|
|
121
|
-
await this.
|
|
122
|
+
await this._exec("PRAGMA journal_mode=WAL");
|
|
123
|
+
await this._exec(`
|
|
122
124
|
CREATE TABLE IF NOT EXISTS events (
|
|
123
125
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
124
126
|
stream TEXT NOT NULL,
|
|
@@ -132,22 +134,22 @@ var SqliteStore = class {
|
|
|
132
134
|
)
|
|
133
135
|
`);
|
|
134
136
|
try {
|
|
135
|
-
await this.
|
|
137
|
+
await this._exec("ALTER TABLE events ADD COLUMN pii TEXT");
|
|
136
138
|
} catch {
|
|
137
139
|
}
|
|
138
|
-
await this.
|
|
140
|
+
await this._exec(
|
|
139
141
|
"CREATE INDEX IF NOT EXISTS idx_events_stream ON events(stream)"
|
|
140
142
|
);
|
|
141
|
-
await this.
|
|
143
|
+
await this._exec(
|
|
142
144
|
"CREATE INDEX IF NOT EXISTS idx_events_name ON events(name)"
|
|
143
145
|
);
|
|
144
|
-
await this.
|
|
146
|
+
await this._exec(
|
|
145
147
|
"CREATE INDEX IF NOT EXISTS idx_events_snapshot ON events(stream, id) WHERE name = '__snapshot__'"
|
|
146
148
|
);
|
|
147
|
-
await this.
|
|
149
|
+
await this._exec(
|
|
148
150
|
"CREATE INDEX IF NOT EXISTS idx_events_stream_id ON events(stream, id) WHERE name <> '__snapshot__'"
|
|
149
151
|
);
|
|
150
|
-
await this.
|
|
152
|
+
await this._exec(`
|
|
151
153
|
CREATE TABLE IF NOT EXISTS streams (
|
|
152
154
|
stream TEXT PRIMARY KEY,
|
|
153
155
|
source TEXT,
|
|
@@ -164,40 +166,36 @@ var SqliteStore = class {
|
|
|
164
166
|
)
|
|
165
167
|
`);
|
|
166
168
|
try {
|
|
167
|
-
await this.
|
|
169
|
+
await this._exec(
|
|
168
170
|
"ALTER TABLE streams ADD COLUMN priority INTEGER NOT NULL DEFAULT 0"
|
|
169
171
|
);
|
|
170
172
|
} catch {
|
|
171
173
|
}
|
|
172
174
|
try {
|
|
173
|
-
await this.
|
|
174
|
-
"ALTER TABLE streams ADD COLUMN deferred_at TEXT"
|
|
175
|
-
);
|
|
175
|
+
await this._exec("ALTER TABLE streams ADD COLUMN deferred_at TEXT");
|
|
176
176
|
} catch {
|
|
177
177
|
}
|
|
178
178
|
try {
|
|
179
|
-
await this.
|
|
179
|
+
await this._exec(
|
|
180
180
|
"ALTER TABLE streams ADD COLUMN lane TEXT NOT NULL DEFAULT 'default'"
|
|
181
181
|
);
|
|
182
182
|
} catch {
|
|
183
183
|
}
|
|
184
184
|
try {
|
|
185
|
-
await this.
|
|
186
|
-
"ALTER TABLE streams ADD COLUMN correlated_at INTEGER"
|
|
187
|
-
);
|
|
185
|
+
await this._exec("ALTER TABLE streams ADD COLUMN correlated_at INTEGER");
|
|
188
186
|
} catch {
|
|
189
187
|
}
|
|
190
|
-
await this.
|
|
188
|
+
await this._exec(
|
|
191
189
|
`UPDATE streams
|
|
192
190
|
SET correlated_at = (SELECT COALESCE(MAX(id), -1) FROM events)
|
|
193
191
|
WHERE correlated_at IS NULL`
|
|
194
192
|
);
|
|
195
|
-
const correlated_columns = await this.
|
|
193
|
+
const correlated_columns = await this._exec(
|
|
196
194
|
"PRAGMA table_info(correlated)"
|
|
197
195
|
);
|
|
198
196
|
const legacy = correlated_columns.rows.some((r) => r.name === "id");
|
|
199
197
|
if (legacy) {
|
|
200
|
-
await this.
|
|
198
|
+
await this._exec(`
|
|
201
199
|
CREATE TABLE IF NOT EXISTS correlated_keyed (
|
|
202
200
|
key TEXT PRIMARY KEY,
|
|
203
201
|
at INTEGER NOT NULL DEFAULT -1,
|
|
@@ -205,15 +203,13 @@ var SqliteStore = class {
|
|
|
205
203
|
leased_until INTEGER
|
|
206
204
|
)
|
|
207
205
|
`);
|
|
208
|
-
await this.
|
|
206
|
+
await this._exec(
|
|
209
207
|
"INSERT OR IGNORE INTO correlated_keyed (key, at) SELECT '', at FROM correlated WHERE id = 0"
|
|
210
208
|
);
|
|
211
|
-
await this.
|
|
212
|
-
await this.
|
|
213
|
-
"ALTER TABLE correlated_keyed RENAME TO correlated"
|
|
214
|
-
);
|
|
209
|
+
await this._exec("DROP TABLE correlated");
|
|
210
|
+
await this._exec("ALTER TABLE correlated_keyed RENAME TO correlated");
|
|
215
211
|
} else {
|
|
216
|
-
await this.
|
|
212
|
+
await this._exec(`
|
|
217
213
|
CREATE TABLE IF NOT EXISTS correlated (
|
|
218
214
|
key TEXT PRIMARY KEY,
|
|
219
215
|
at INTEGER NOT NULL DEFAULT -1,
|
|
@@ -222,104 +218,121 @@ var SqliteStore = class {
|
|
|
222
218
|
)
|
|
223
219
|
`);
|
|
224
220
|
}
|
|
225
|
-
await this.
|
|
226
|
-
|
|
227
|
-
);
|
|
228
|
-
await this.client.execute(
|
|
221
|
+
await this._exec("INSERT OR IGNORE INTO correlated (key) VALUES ('')");
|
|
222
|
+
await this._exec(
|
|
229
223
|
"CREATE INDEX IF NOT EXISTS idx_streams_claim ON streams(blocked, priority DESC, at)"
|
|
230
224
|
);
|
|
231
|
-
await this.
|
|
225
|
+
await this._exec(
|
|
232
226
|
"CREATE INDEX IF NOT EXISTS idx_streams_correlated_at ON streams(lane, priority DESC, at) WHERE blocked = 0 AND at < correlated_at"
|
|
233
227
|
);
|
|
234
|
-
await this.
|
|
228
|
+
await this._exec(
|
|
235
229
|
"CREATE INDEX IF NOT EXISTS idx_streams_correlated_at_any_lane ON streams(priority DESC, at) WHERE blocked = 0 AND at < correlated_at"
|
|
236
230
|
);
|
|
237
|
-
await this.
|
|
231
|
+
await this._exec(
|
|
238
232
|
"CREATE INDEX IF NOT EXISTS idx_streams_lane ON streams(lane)"
|
|
239
233
|
);
|
|
240
234
|
}
|
|
241
235
|
async drop() {
|
|
242
|
-
await this.
|
|
243
|
-
await this.
|
|
244
|
-
await this.
|
|
236
|
+
await this._exec("DROP TABLE IF EXISTS events");
|
|
237
|
+
await this._exec("DROP TABLE IF EXISTS streams");
|
|
238
|
+
await this._exec("DROP TABLE IF EXISTS correlated");
|
|
245
239
|
}
|
|
246
|
-
// One
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
240
|
+
// One connection serves the whole store, and it can't run a statement
|
|
241
|
+
// while a write transaction is open on it: a second one fails at once
|
|
242
|
+
// (`TRANSACTION_ACTIVE` in memory, `SQLITE_BUSY` on a file) instead of
|
|
243
|
+
// waiting. So every statement and every transaction takes its turn here.
|
|
244
|
+
// A turn covers only the database work, never a caller's callback, so a
|
|
245
|
+
// callback that writes to the store can't wait on itself.
|
|
246
|
+
_turn = Promise.resolve();
|
|
247
|
+
_serial(work) {
|
|
248
|
+
const run = this._turn.then(work);
|
|
249
|
+
this._turn = run.catch(() => void 0);
|
|
250
|
+
return run;
|
|
251
|
+
}
|
|
252
|
+
// One statement, in its turn.
|
|
253
|
+
_exec(stmt) {
|
|
254
|
+
return this._serial(() => this.client.execute(stmt));
|
|
255
|
+
}
|
|
256
|
+
// One write transaction, in its turn: any failure rolls back and throws a {@link StoreError} naming the operation.
|
|
257
|
+
_tx(operation, work) {
|
|
258
|
+
return this._serial(async () => {
|
|
259
|
+
const tx = await this.client.transaction("write");
|
|
260
|
+
try {
|
|
261
|
+
const result = await work(tx);
|
|
262
|
+
await tx.commit();
|
|
263
|
+
return result;
|
|
264
|
+
} catch (e) {
|
|
265
|
+
await tx.rollback();
|
|
266
|
+
throw new StoreError(operation, { cause: e });
|
|
267
|
+
}
|
|
268
|
+
});
|
|
257
269
|
}
|
|
258
270
|
async dispose() {
|
|
259
|
-
await
|
|
260
|
-
this.client.close();
|
|
271
|
+
await this._serial(async () => this.client.close());
|
|
261
272
|
}
|
|
262
273
|
// --- commit: transaction + optimistic concurrency ---
|
|
263
274
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
275
|
+
return this._serial(async () => {
|
|
276
|
+
const tx = await this.client.transaction("write");
|
|
277
|
+
let current_version = -1;
|
|
278
|
+
try {
|
|
279
|
+
const version_row = await tx.execute({
|
|
280
|
+
sql: "SELECT COALESCE(MAX(version), -1) as v FROM events WHERE stream = ?",
|
|
281
|
+
args: [stream]
|
|
282
|
+
});
|
|
283
|
+
current_version = Number(version_row.rows[0].v);
|
|
284
|
+
if (typeof expectedVersion === "number" && current_version !== expectedVersion) {
|
|
285
|
+
throw new ConcurrencyError(
|
|
286
|
+
stream,
|
|
287
|
+
current_version,
|
|
288
|
+
msgs,
|
|
289
|
+
expectedVersion
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
293
|
+
const committed = [];
|
|
294
|
+
let version = current_version + 1;
|
|
295
|
+
for (const { name, data, pii } of msgs) {
|
|
296
|
+
const pii_for_write = await this._stringify_pii_for_write(pii);
|
|
297
|
+
const result = await tx.execute({
|
|
298
|
+
sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
299
|
+
args: [
|
|
300
|
+
stream,
|
|
301
|
+
version,
|
|
302
|
+
name,
|
|
303
|
+
JSON.stringify(data),
|
|
304
|
+
JSON.stringify(meta),
|
|
305
|
+
now,
|
|
306
|
+
pii_for_write
|
|
307
|
+
]
|
|
308
|
+
});
|
|
309
|
+
committed.push({
|
|
310
|
+
id: Number(result.lastInsertRowid),
|
|
288
311
|
stream,
|
|
289
312
|
version,
|
|
313
|
+
created: new Date(now),
|
|
290
314
|
name,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
315
|
+
data,
|
|
316
|
+
meta,
|
|
317
|
+
...pii == null ? {} : { pii }
|
|
318
|
+
});
|
|
319
|
+
version++;
|
|
320
|
+
}
|
|
321
|
+
await tx.commit();
|
|
322
|
+
return committed;
|
|
323
|
+
} catch (e) {
|
|
324
|
+
await tx.rollback();
|
|
325
|
+
if (e instanceof ConcurrencyError) throw e;
|
|
326
|
+
if (is_unique_violation(e))
|
|
327
|
+
throw new ConcurrencyError(
|
|
328
|
+
stream,
|
|
329
|
+
current_version,
|
|
330
|
+
msgs,
|
|
331
|
+
expectedVersion ?? -1
|
|
332
|
+
);
|
|
333
|
+
throw new StoreError("commit", { cause: e });
|
|
308
334
|
}
|
|
309
|
-
|
|
310
|
-
return committed;
|
|
311
|
-
} catch (e) {
|
|
312
|
-
await tx.rollback();
|
|
313
|
-
if (e instanceof ConcurrencyError) throw e;
|
|
314
|
-
if (is_unique_violation(e))
|
|
315
|
-
throw new ConcurrencyError(
|
|
316
|
-
stream,
|
|
317
|
-
current_version,
|
|
318
|
-
msgs,
|
|
319
|
-
expectedVersion ?? -1
|
|
320
|
-
);
|
|
321
|
-
throw new StoreError("commit", { cause: e });
|
|
322
|
-
}
|
|
335
|
+
});
|
|
323
336
|
}
|
|
324
337
|
// --- query: read-only, no transaction needed ---
|
|
325
338
|
async query(callback, query) {
|
|
@@ -369,7 +382,7 @@ var SqliteStore = class {
|
|
|
369
382
|
sql += " LIMIT ?";
|
|
370
383
|
args.push(query.limit);
|
|
371
384
|
}
|
|
372
|
-
const result = await this.
|
|
385
|
+
const result = await this._exec({ sql, args });
|
|
373
386
|
let count = 0;
|
|
374
387
|
for (const row of result.rows) {
|
|
375
388
|
const pii_value = query?.with_pii === false ? null : await this._parse_pii_from_read(row.pii);
|
|
@@ -740,8 +753,8 @@ var SqliteStore = class {
|
|
|
740
753
|
sql += " ORDER BY stream LIMIT ?";
|
|
741
754
|
args.push(limit);
|
|
742
755
|
const [streamsResult, maxResult] = await Promise.all([
|
|
743
|
-
this.
|
|
744
|
-
this.
|
|
756
|
+
this._exec({ sql, args }),
|
|
757
|
+
this._exec("SELECT COALESCE(MAX(id), -1) AS m FROM events")
|
|
745
758
|
]);
|
|
746
759
|
let count = 0;
|
|
747
760
|
for (const row of streamsResult.rows) {
|
|
@@ -873,8 +886,8 @@ var SqliteStore = class {
|
|
|
873
886
|
)` : null;
|
|
874
887
|
const tail_args = limit !== void 0 ? [...args, ...args, limit] : [...args, ...args];
|
|
875
888
|
const [headRes, tailRes] = await Promise.all([
|
|
876
|
-
this.
|
|
877
|
-
tail_sql ? this.
|
|
889
|
+
this._exec({ sql: head_sql, args: head_args }),
|
|
890
|
+
tail_sql ? this._exec({ sql: tail_sql, args: tail_args }) : null
|
|
878
891
|
]);
|
|
879
892
|
const out = /* @__PURE__ */ new Map();
|
|
880
893
|
for (const row of headRes.rows) {
|
|
@@ -936,7 +949,7 @@ var SqliteStore = class {
|
|
|
936
949
|
ORDER BY h.stream
|
|
937
950
|
`;
|
|
938
951
|
const full_args = limit !== void 0 ? [...args, limit] : args;
|
|
939
|
-
const res = await this.
|
|
952
|
+
const res = await this._exec({ sql, args: full_args });
|
|
940
953
|
const out = /* @__PURE__ */ new Map();
|
|
941
954
|
for (const row of res.rows) {
|
|
942
955
|
const r = row;
|
|
@@ -1046,33 +1059,35 @@ var SqliteStore = class {
|
|
|
1046
1059
|
* from the source.
|
|
1047
1060
|
*/
|
|
1048
1061
|
async restore(driver) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1062
|
+
return this._serial(async () => {
|
|
1063
|
+
const tx = await this.client.transaction("write");
|
|
1064
|
+
try {
|
|
1065
|
+
await tx.execute("DELETE FROM events");
|
|
1066
|
+
await tx.execute("DELETE FROM streams");
|
|
1067
|
+
await tx.execute("UPDATE correlated SET at = -1");
|
|
1068
|
+
await tx.execute("DELETE FROM sqlite_sequence WHERE name = 'events'");
|
|
1069
|
+
await driver(async (event) => {
|
|
1070
|
+
const pii_for_write = await this._stringify_pii_for_write(event.pii);
|
|
1071
|
+
const ins = await tx.execute({
|
|
1072
|
+
sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
1073
|
+
args: [
|
|
1074
|
+
event.stream,
|
|
1075
|
+
event.version,
|
|
1076
|
+
event.name,
|
|
1077
|
+
JSON.stringify(event.data),
|
|
1078
|
+
JSON.stringify(event.meta),
|
|
1079
|
+
event.created.toISOString(),
|
|
1080
|
+
pii_for_write
|
|
1081
|
+
]
|
|
1082
|
+
});
|
|
1083
|
+
return Number(ins.lastInsertRowid);
|
|
1068
1084
|
});
|
|
1069
|
-
|
|
1070
|
-
})
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1085
|
+
await tx.commit();
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
await tx.rollback();
|
|
1088
|
+
throw error;
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1076
1091
|
}
|
|
1077
1092
|
/**
|
|
1078
1093
|
* Wipe the sensitive-data payload for every event on the stream — the
|
|
@@ -1092,7 +1107,7 @@ var SqliteStore = class {
|
|
|
1092
1107
|
* @returns Count of events whose `pii` was set to `NULL`
|
|
1093
1108
|
*/
|
|
1094
1109
|
async forget_pii(stream) {
|
|
1095
|
-
const r = await this.
|
|
1110
|
+
const r = await this._exec({
|
|
1096
1111
|
sql: "UPDATE events SET pii = NULL WHERE stream = ? AND pii IS NOT NULL",
|
|
1097
1112
|
args: [stream]
|
|
1098
1113
|
});
|