@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/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,121 @@ 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();
255
- throw new StoreError(operation, { cause: e });
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 Promise.resolve();
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
- 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: [
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
- 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++;
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
- 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
- }
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.client.execute({ sql, args });
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.client.execute({ sql, args }),
744
- this.client.execute("SELECT COALESCE(MAX(id), -1) AS m FROM events")
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.client.execute({ sql: head_sql, args: head_args }),
877
- tail_sql ? this.client.execute({ sql: tail_sql, args: tail_args }) : null
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.client.execute({ sql, args: full_args });
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
- 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
- ]
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
- return Number(ins.lastInsertRowid);
1070
- });
1071
- await tx.commit();
1072
- } catch (error) {
1073
- await tx.rollback();
1074
- throw error;
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.client.execute({
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
  });