@rotorsoft/act-sqlite 1.22.4 → 1.22.6

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/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // src/sqlite-store.ts
2
- import { createClient } from "@libsql/client";
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.client.execute("PRAGMA journal_mode=WAL");
121
- await this.client.execute(`
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.client.execute("ALTER TABLE events ADD COLUMN pii TEXT");
137
+ await this._exec("ALTER TABLE events ADD COLUMN pii TEXT");
136
138
  } catch {
137
139
  }
138
- await this.client.execute(
140
+ await this._exec(
139
141
  "CREATE INDEX IF NOT EXISTS idx_events_stream ON events(stream)"
140
142
  );
141
- await this.client.execute(
143
+ await this._exec(
142
144
  "CREATE INDEX IF NOT EXISTS idx_events_name ON events(name)"
143
145
  );
144
- await this.client.execute(
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.client.execute(
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.client.execute(`
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.client.execute(
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.client.execute(
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.client.execute(
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.client.execute(
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.client.execute(
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.client.execute(
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.client.execute(`
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.client.execute(
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.client.execute("DROP TABLE correlated");
212
- await this.client.execute(
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.client.execute(`
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,127 @@ var SqliteStore = class {
222
218
  )
223
219
  `);
224
220
  }
225
- await this.client.execute(
226
- "INSERT OR IGNORE INTO correlated (key) VALUES ('')"
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.client.execute(
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.client.execute(
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.client.execute(
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.client.execute("DROP TABLE IF EXISTS events");
243
- await this.client.execute("DROP TABLE IF EXISTS streams");
244
- await this.client.execute("DROP TABLE IF EXISTS correlated");
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 write transaction: any failure rolls back and throws a {@link StoreError} naming the operation.
247
- async _tx(operation, work) {
248
- const tx = await this.client.transaction("write");
249
- try {
250
- const result = await work(tx);
251
- await tx.commit();
252
- return result;
253
- } catch (e) {
254
- await tx.rollback();
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
+ // Opens a write transaction: a BEGIN that fails is a {@link StoreError} too.
257
+ _begin(operation) {
258
+ return this.client.transaction("write").catch((e) => {
255
259
  throw new StoreError(operation, { cause: e });
256
- }
260
+ });
261
+ }
262
+ // One write transaction, in its turn: any failure rolls back and throws a {@link StoreError} naming the operation.
263
+ _tx(operation, work) {
264
+ return this._serial(async () => {
265
+ const tx = await this._begin(operation);
266
+ try {
267
+ const result = await work(tx);
268
+ await tx.commit();
269
+ return result;
270
+ } catch (e) {
271
+ await tx.rollback();
272
+ throw new StoreError(operation, { cause: e });
273
+ }
274
+ });
257
275
  }
258
276
  async dispose() {
259
- await Promise.resolve();
260
- this.client.close();
277
+ await this._serial(async () => this.client.close());
261
278
  }
262
279
  // --- commit: transaction + optimistic concurrency ---
263
280
  async commit(stream, msgs, meta, expectedVersion) {
264
- const tx = await this.client.transaction("write");
265
- let current_version = -1;
266
- try {
267
- const version_row = await tx.execute({
268
- sql: "SELECT COALESCE(MAX(version), -1) as v FROM events WHERE stream = ?",
269
- args: [stream]
270
- });
271
- current_version = Number(version_row.rows[0].v);
272
- if (typeof expectedVersion === "number" && current_version !== expectedVersion) {
273
- throw new ConcurrencyError(
274
- stream,
275
- current_version,
276
- msgs,
277
- expectedVersion
278
- );
279
- }
280
- const now = (/* @__PURE__ */ new Date()).toISOString();
281
- const committed = [];
282
- let version = current_version + 1;
283
- for (const { name, data, pii } of msgs) {
284
- const pii_for_write = await this._stringify_pii_for_write(pii);
285
- const result = await tx.execute({
286
- sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
287
- args: [
281
+ return this._serial(async () => {
282
+ const tx = await this._begin("commit");
283
+ let current_version = -1;
284
+ try {
285
+ const version_row = await tx.execute({
286
+ sql: "SELECT COALESCE(MAX(version), -1) as v FROM events WHERE stream = ?",
287
+ args: [stream]
288
+ });
289
+ current_version = Number(version_row.rows[0].v);
290
+ if (typeof expectedVersion === "number" && current_version !== expectedVersion) {
291
+ throw new ConcurrencyError(
292
+ stream,
293
+ current_version,
294
+ msgs,
295
+ expectedVersion
296
+ );
297
+ }
298
+ const now = (/* @__PURE__ */ new Date()).toISOString();
299
+ const committed = [];
300
+ let version = current_version + 1;
301
+ for (const { name, data, pii } of msgs) {
302
+ const pii_for_write = await this._stringify_pii_for_write(pii);
303
+ const result = await tx.execute({
304
+ sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
305
+ args: [
306
+ stream,
307
+ version,
308
+ name,
309
+ JSON.stringify(data),
310
+ JSON.stringify(meta),
311
+ now,
312
+ pii_for_write
313
+ ]
314
+ });
315
+ committed.push({
316
+ id: Number(result.lastInsertRowid),
288
317
  stream,
289
318
  version,
319
+ created: new Date(now),
290
320
  name,
291
- JSON.stringify(data),
292
- JSON.stringify(meta),
293
- now,
294
- pii_for_write
295
- ]
296
- });
297
- committed.push({
298
- id: Number(result.lastInsertRowid),
299
- stream,
300
- version,
301
- created: new Date(now),
302
- name,
303
- data,
304
- meta,
305
- ...pii == null ? {} : { pii }
306
- });
307
- version++;
321
+ data,
322
+ meta,
323
+ ...pii == null ? {} : { pii }
324
+ });
325
+ version++;
326
+ }
327
+ await tx.commit();
328
+ return committed;
329
+ } catch (e) {
330
+ await tx.rollback();
331
+ if (e instanceof ConcurrencyError) throw e;
332
+ if (is_unique_violation(e))
333
+ throw new ConcurrencyError(
334
+ stream,
335
+ current_version,
336
+ msgs,
337
+ expectedVersion ?? -1
338
+ );
339
+ throw new StoreError("commit", { cause: e });
308
340
  }
309
- await tx.commit();
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
- }
341
+ });
323
342
  }
324
343
  // --- query: read-only, no transaction needed ---
325
344
  async query(callback, query) {
@@ -369,7 +388,7 @@ var SqliteStore = class {
369
388
  sql += " LIMIT ?";
370
389
  args.push(query.limit);
371
390
  }
372
- const result = await this.client.execute({ sql, args });
391
+ const result = await this._exec({ sql, args });
373
392
  let count = 0;
374
393
  for (const row of result.rows) {
375
394
  const pii_value = query?.with_pii === false ? null : await this._parse_pii_from_read(row.pii);
@@ -740,8 +759,8 @@ var SqliteStore = class {
740
759
  sql += " ORDER BY stream LIMIT ?";
741
760
  args.push(limit);
742
761
  const [streamsResult, maxResult] = await Promise.all([
743
- this.client.execute({ sql, args }),
744
- this.client.execute("SELECT COALESCE(MAX(id), -1) AS m FROM events")
762
+ this._exec({ sql, args }),
763
+ this._exec("SELECT COALESCE(MAX(id), -1) AS m FROM events")
745
764
  ]);
746
765
  let count = 0;
747
766
  for (const row of streamsResult.rows) {
@@ -873,8 +892,8 @@ var SqliteStore = class {
873
892
  )` : null;
874
893
  const tail_args = limit !== void 0 ? [...args, ...args, limit] : [...args, ...args];
875
894
  const [headRes, tailRes] = await Promise.all([
876
- this.client.execute({ sql: head_sql, args: head_args }),
877
- tail_sql ? this.client.execute({ sql: tail_sql, args: tail_args }) : null
895
+ this._exec({ sql: head_sql, args: head_args }),
896
+ tail_sql ? this._exec({ sql: tail_sql, args: tail_args }) : null
878
897
  ]);
879
898
  const out = /* @__PURE__ */ new Map();
880
899
  for (const row of headRes.rows) {
@@ -936,7 +955,7 @@ var SqliteStore = class {
936
955
  ORDER BY h.stream
937
956
  `;
938
957
  const full_args = limit !== void 0 ? [...args, limit] : args;
939
- const res = await this.client.execute({ sql, args: full_args });
958
+ const res = await this._exec({ sql, args: full_args });
940
959
  const out = /* @__PURE__ */ new Map();
941
960
  for (const row of res.rows) {
942
961
  const r = row;
@@ -1046,33 +1065,35 @@ var SqliteStore = class {
1046
1065
  * from the source.
1047
1066
  */
1048
1067
  async restore(driver) {
1049
- const tx = await this.client.transaction("write");
1050
- try {
1051
- await tx.execute("DELETE FROM events");
1052
- await tx.execute("DELETE FROM streams");
1053
- await tx.execute("UPDATE correlated SET at = -1");
1054
- await tx.execute("DELETE FROM sqlite_sequence WHERE name = 'events'");
1055
- await driver(async (event) => {
1056
- const pii_for_write = await this._stringify_pii_for_write(event.pii);
1057
- const ins = await tx.execute({
1058
- sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
1059
- args: [
1060
- event.stream,
1061
- event.version,
1062
- event.name,
1063
- JSON.stringify(event.data),
1064
- JSON.stringify(event.meta),
1065
- event.created.toISOString(),
1066
- pii_for_write
1067
- ]
1068
+ return this._serial(async () => {
1069
+ const tx = await this._begin("restore");
1070
+ try {
1071
+ await tx.execute("DELETE FROM events");
1072
+ await tx.execute("DELETE FROM streams");
1073
+ await tx.execute("UPDATE correlated SET at = -1");
1074
+ await tx.execute("DELETE FROM sqlite_sequence WHERE name = 'events'");
1075
+ await driver(async (event) => {
1076
+ const pii_for_write = await this._stringify_pii_for_write(event.pii);
1077
+ const ins = await tx.execute({
1078
+ sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
1079
+ args: [
1080
+ event.stream,
1081
+ event.version,
1082
+ event.name,
1083
+ JSON.stringify(event.data),
1084
+ JSON.stringify(event.meta),
1085
+ event.created.toISOString(),
1086
+ pii_for_write
1087
+ ]
1088
+ });
1089
+ return Number(ins.lastInsertRowid);
1068
1090
  });
1069
- return Number(ins.lastInsertRowid);
1070
- });
1071
- await tx.commit();
1072
- } catch (error) {
1073
- await tx.rollback();
1074
- throw error;
1075
- }
1091
+ await tx.commit();
1092
+ } catch (error) {
1093
+ await tx.rollback();
1094
+ throw error;
1095
+ }
1096
+ });
1076
1097
  }
1077
1098
  /**
1078
1099
  * Wipe the sensitive-data payload for every event on the stream — the
@@ -1092,7 +1113,7 @@ var SqliteStore = class {
1092
1113
  * @returns Count of events whose `pii` was set to `NULL`
1093
1114
  */
1094
1115
  async forget_pii(stream) {
1095
- const r = await this.client.execute({
1116
+ const r = await this._exec({
1096
1117
  sql: "UPDATE events SET pii = NULL WHERE stream = ? AND pii IS NOT NULL",
1097
1118
  args: [stream]
1098
1119
  });