@rotorsoft/act-sqlite 1.22.7 → 2.0.0

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
@@ -4,6 +4,9 @@ import {
4
4
  } from "@libsql/client";
5
5
  import {
6
6
  ConcurrencyError,
7
+ check_appends,
8
+ DEFAULT_QUERY_STREAMS_LIMIT,
9
+ fair_slots,
7
10
  is_literal_source,
8
11
  StoreError,
9
12
  ValidationError
@@ -241,8 +244,9 @@ var SqliteStore = class {
241
244
  // while a write transaction is open on it: a second one fails at once
242
245
  // (`TRANSACTION_ACTIVE` in memory, `SQLITE_BUSY` on a file) instead of
243
246
  // 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.
247
+ // A turn covers only the database work, so a `query` callback that writes to
248
+ // the store can't wait on itself. `restore` is the exception: its driver runs
249
+ // inside the turn, because the whole restore is one transaction.
246
250
  _turn = Promise.resolve();
247
251
  _serial(work) {
248
252
  const run = this._turn.then(work);
@@ -276,53 +280,63 @@ var SqliteStore = class {
276
280
  async dispose() {
277
281
  await this._serial(async () => this.client.close());
278
282
  }
279
- // --- commit: transaction + optimistic concurrency ---
280
- async commit(stream, msgs, meta, expectedVersion) {
283
+ // --- commit: one transaction across every append ---
284
+ async commit(appends, meta) {
285
+ check_appends(appends);
286
+ if (!appends.length) return [];
281
287
  return this._serial(async () => {
282
288
  const tx = await this._begin("commit");
283
- let current_version = -1;
289
+ let appending = appends[0];
290
+ let head = -1;
284
291
  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
- );
292
+ const heads = /* @__PURE__ */ new Map();
293
+ for (const { stream, msgs, expectedVersion } of appends) {
294
+ const row = await tx.execute({
295
+ sql: "SELECT COALESCE(MAX(version), -1) as v FROM events WHERE stream = ?",
296
+ args: [stream]
297
+ });
298
+ const current = Number(row.rows[0].v);
299
+ if (typeof expectedVersion === "number" && current !== expectedVersion)
300
+ throw new ConcurrencyError(
301
+ stream,
302
+ current,
303
+ msgs,
304
+ expectedVersion
305
+ );
306
+ heads.set(stream, current);
297
307
  }
298
308
  const now = (/* @__PURE__ */ new Date()).toISOString();
299
309
  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,
310
+ for (const append of appends) {
311
+ appending = append;
312
+ head = heads.get(append.stream);
313
+ let version = head + 1;
314
+ for (const { name, data, pii } of append.msgs) {
315
+ const pii_for_write = await this._stringify_pii_for_write(pii);
316
+ const result = await tx.execute({
317
+ sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
318
+ args: [
319
+ append.stream,
320
+ version,
321
+ name,
322
+ JSON.stringify(data),
323
+ JSON.stringify(meta),
324
+ now,
325
+ pii_for_write
326
+ ]
327
+ });
328
+ committed.push({
329
+ id: Number(result.lastInsertRowid),
330
+ stream: append.stream,
307
331
  version,
332
+ created: new Date(now),
308
333
  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),
317
- stream,
318
- version,
319
- created: new Date(now),
320
- name,
321
- data,
322
- meta,
323
- ...pii == null ? {} : { pii }
324
- });
325
- version++;
334
+ data,
335
+ meta,
336
+ ...pii == null ? {} : { pii }
337
+ });
338
+ version++;
339
+ }
326
340
  }
327
341
  await tx.commit();
328
342
  return committed;
@@ -331,10 +345,10 @@ var SqliteStore = class {
331
345
  if (e instanceof ConcurrencyError) throw e;
332
346
  if (is_unique_violation(e))
333
347
  throw new ConcurrencyError(
334
- stream,
335
- current_version,
336
- msgs,
337
- expectedVersion ?? -1
348
+ appending.stream,
349
+ head,
350
+ appending.msgs,
351
+ appending.expectedVersion ?? -1
338
352
  );
339
353
  throw new StoreError("commit", { cause: e });
340
354
  }
@@ -407,8 +421,8 @@ var SqliteStore = class {
407
421
  // targeting the same stream. Operator overrides go
408
422
  // through `prioritize()` instead.
409
423
  async subscribe(streams, correlated_at, correlator) {
410
- for (const { source } of streams) {
411
- if (source !== void 0 && !is_literal_source(source))
424
+ for (const { stream, source } of streams) {
425
+ if (source !== void 0 && !is_literal_source(source, stream))
412
426
  streamPatternToGlob(source);
413
427
  }
414
428
  return this._tx("subscribe", async (tx) => {
@@ -542,7 +556,7 @@ var SqliteStore = class {
542
556
  priority: Number(row.priority),
543
557
  lane: row.lane
544
558
  })).sort((a, b) => b.priority - a.priority || a.at - b.at);
545
- const fair = lagging >= 2 ? Math.max(1, Math.floor(lagging / 4)) : 0;
559
+ const fair = fair_slots(lagging);
546
560
  const priority_slice = candidates.slice(0, lagging - fair);
547
561
  const picked = new Set(priority_slice.map((c) => c.stream));
548
562
  const fair_slice = [...candidates].sort((a, b) => a.at - b.at).filter((c) => !picked.has(c.stream)).slice(0, fair);
@@ -749,7 +763,7 @@ var SqliteStore = class {
749
763
  }
750
764
  // --- query_streams: read-only introspection with filters ---
751
765
  async query_streams(callback, query) {
752
- const limit = query?.limit ?? 100;
766
+ const limit = query?.limit ?? DEFAULT_QUERY_STREAMS_LIMIT;
753
767
  const { clause, args } = this._filter_clause(query ?? {});
754
768
  let sql = `SELECT stream, source, at, retry, blocked, error, leased_by, leased_until, priority, lane, deferred_at, correlated_at FROM streams WHERE ${clause}`;
755
769
  if (query?.after !== void 0) {
@@ -1064,6 +1078,8 @@ var SqliteStore = class {
1064
1078
  * new sequence is dense from 1. `created` is preserved verbatim
1065
1079
  * from the source.
1066
1080
  */
1081
+ // The driver runs inside this store's turn, so it must read from another
1082
+ // store: a call on this one would wait for the restore to end, forever.
1067
1083
  async restore(driver) {
1068
1084
  return this._serial(async () => {
1069
1085
  const tx = await this._begin("restore");