@rotorsoft/act-sqlite 1.12.2 → 1.12.4

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,6 +1,11 @@
1
1
  // src/sqlite-store.ts
2
2
  import { createClient } from "@libsql/client";
3
- import { is_literal_source, StoreError, ValidationError } from "@rotorsoft/act";
3
+ import {
4
+ ConcurrencyError,
5
+ is_literal_source,
6
+ StoreError,
7
+ ValidationError
8
+ } from "@rotorsoft/act";
4
9
  import {
5
10
  decrypt,
6
11
  encrypt,
@@ -9,8 +14,16 @@ import {
9
14
  var DEFAULT_CONFIG = {
10
15
  url: "file::memory:"
11
16
  };
17
+ var ISO_8601 = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\.\d+)?(Z|[+-][0-2][0-9]:[0-5][0-9])?$/;
18
+ var date_reviver = (_key, value) => typeof value === "string" && ISO_8601.test(value) ? new Date(value) : value;
19
+ var parse_json = (raw) => JSON.parse(raw, date_reviver);
20
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
21
+ var is_unique_violation = (e) => {
22
+ const err = e;
23
+ return err?.rawCode === SQLITE_CONSTRAINT_UNIQUE || typeof err?.message === "string" && err.message.includes("UNIQUE constraint failed");
24
+ };
12
25
  var PORTABLE_BODY = /^(?:\.\*|\.|[^\\^$.()[\]{}|+?*])*$/;
13
- function streamPatternToLike(input) {
26
+ function streamPatternToGlob(input) {
14
27
  let s = input;
15
28
  const start = s.startsWith("^");
16
29
  const end = s.endsWith("$");
@@ -18,27 +31,26 @@ function streamPatternToLike(input) {
18
31
  if (end) s = s.slice(0, -1);
19
32
  if (!PORTABLE_BODY.test(s))
20
33
  throw new ValidationError(
21
- `stream filter pattern "${input}" \u2014 the SQLite adapter supports only the portable subset: "^" and "$" anchors, "." (any single character), ".*" (any run), and literal characters. Escapes, character classes, grouping, alternation, and quantifiers cannot be converted to LIKE; use stream_exact/source_exact for literal names`,
34
+ `stream filter pattern "${input}" \u2014 the SQLite adapter supports only the portable subset: "^" and "$" anchors, "." (any single character), ".*" (any run), and literal characters. Escapes, character classes, grouping, alternation, and quantifiers cannot be converted to GLOB; use stream_exact/source_exact for literal names`,
22
35
  input,
23
36
  "non-portable stream filter pattern"
24
37
  );
25
38
  const tokens = [];
26
- if (!start) tokens.push("%");
39
+ if (!start) tokens.push("*");
27
40
  for (let i = 0; i < s.length; i++) {
28
41
  const c = s[i];
29
42
  if (c === ".") {
30
43
  if (s[i + 1] === "*") {
31
- tokens.push("%");
44
+ tokens.push("*");
32
45
  i++;
33
- } else tokens.push("_");
34
- } else if (c === "%" || c === "_") tokens.push(`\\${c}`);
35
- else tokens.push(c);
46
+ } else tokens.push("?");
47
+ } else tokens.push(c);
36
48
  }
37
- if (!end) tokens.push("%");
49
+ if (!end) tokens.push("*");
38
50
  let out = "";
39
51
  let prev = "";
40
52
  for (const t of tokens) {
41
- if (t === "%" && prev === "%") continue;
53
+ if (t === "*" && prev === "*") continue;
42
54
  out += t;
43
55
  prev = t;
44
56
  }
@@ -173,14 +185,14 @@ var SqliteStore = class {
173
185
  // --- commit: transaction + optimistic concurrency ---
174
186
  async commit(stream, msgs, meta, expectedVersion) {
175
187
  const tx = await this.client.transaction("write");
188
+ let current_version = -1;
176
189
  try {
177
190
  const version_row = await tx.execute({
178
191
  sql: "SELECT COALESCE(MAX(version), -1) as v FROM events WHERE stream = ?",
179
192
  args: [stream]
180
193
  });
181
- const current_version = Number(version_row.rows[0].v);
194
+ current_version = Number(version_row.rows[0].v);
182
195
  if (typeof expectedVersion === "number" && current_version !== expectedVersion) {
183
- const { ConcurrencyError } = await import("@rotorsoft/act");
184
196
  throw new ConcurrencyError(
185
197
  stream,
186
198
  current_version,
@@ -221,7 +233,15 @@ var SqliteStore = class {
221
233
  return committed;
222
234
  } catch (e) {
223
235
  await tx.rollback();
224
- throw e;
236
+ if (e instanceof ConcurrencyError) throw e;
237
+ if (is_unique_violation(e))
238
+ throw new ConcurrencyError(
239
+ stream,
240
+ current_version,
241
+ msgs,
242
+ expectedVersion ?? -1
243
+ );
244
+ throw new StoreError("commit", { cause: e });
225
245
  }
226
246
  }
227
247
  // --- query: read-only, no transaction needed ---
@@ -233,11 +253,11 @@ var SqliteStore = class {
233
253
  sql += " AND stream = ?";
234
254
  args.push(query.stream);
235
255
  } else {
236
- sql += " AND stream LIKE ? ESCAPE '\\'";
237
- args.push(streamPatternToLike(query.stream));
256
+ sql += " AND stream GLOB ?";
257
+ args.push(streamPatternToGlob(query.stream));
238
258
  }
239
259
  }
240
- if (query?.names) {
260
+ if (query?.names !== void 0) {
241
261
  sql += ` AND name IN (${query.names.map(() => "?").join(",")})`;
242
262
  args.push(...query.names);
243
263
  }
@@ -283,8 +303,8 @@ var SqliteStore = class {
283
303
  version: Number(row.version),
284
304
  created: new Date(row.created),
285
305
  name: row.name,
286
- data: JSON.parse(row.data),
287
- meta: JSON.parse(row.meta),
306
+ data: parse_json(row.data),
307
+ meta: parse_json(row.meta),
288
308
  pii: pii_value
289
309
  })
290
310
  );
@@ -299,7 +319,7 @@ var SqliteStore = class {
299
319
  async subscribe(streams) {
300
320
  for (const { source } of streams) {
301
321
  if (source !== void 0 && !is_literal_source(source))
302
- streamPatternToLike(source);
322
+ streamPatternToGlob(source);
303
323
  }
304
324
  const tx = await this.client.transaction("write");
305
325
  try {
@@ -365,8 +385,8 @@ var SqliteStore = class {
365
385
  sql: `SELECT 1 FROM events WHERE id > ? AND name != '__snapshot__' AND stream = ? LIMIT 1`,
366
386
  args: [at, source]
367
387
  }) : await tx.execute({
368
- sql: `SELECT 1 FROM events WHERE id > ? AND name != '__snapshot__' AND stream LIKE ? ESCAPE '\\' LIMIT 1`,
369
- args: [at, streamPatternToLike(source)]
388
+ sql: `SELECT 1 FROM events WHERE id > ? AND name != '__snapshot__' AND stream GLOB ? LIMIT 1`,
389
+ args: [at, streamPatternToGlob(source)]
370
390
  });
371
391
  has_events = check.rows.length > 0;
372
392
  } else {
@@ -386,7 +406,14 @@ var SqliteStore = class {
386
406
  });
387
407
  }
388
408
  }
389
- const lag = candidates.slice(0, lagging).map((c) => ({ ...c, lagging: true }));
409
+ const fair = lagging >= 2 ? Math.max(1, Math.floor(lagging / 4)) : 0;
410
+ const priority_slice = candidates.slice(0, lagging - fair);
411
+ const picked = new Set(priority_slice.map((c) => c.stream));
412
+ const fair_slice = [...candidates].sort((a, b) => a.at - b.at).filter((c) => !picked.has(c.stream)).slice(0, fair);
413
+ const lag = [...priority_slice, ...fair_slice].map((c) => ({
414
+ ...c,
415
+ lagging: true
416
+ }));
390
417
  const lead = [...candidates].sort((a, b) => b.at - a.at).slice(0, leading).map((c) => ({ ...c, lagging: false }));
391
418
  const seen = /* @__PURE__ */ new Set();
392
419
  const combined = [...lag, ...lead].filter((p) => {
@@ -507,8 +534,8 @@ var SqliteStore = class {
507
534
  conditions.push("stream = ?");
508
535
  args.push(filter.stream);
509
536
  } else {
510
- conditions.push("stream LIKE ? ESCAPE '\\'");
511
- args.push(streamPatternToLike(filter.stream));
537
+ conditions.push("stream GLOB ?");
538
+ args.push(streamPatternToGlob(filter.stream));
512
539
  }
513
540
  }
514
541
  if (filter.source !== void 0) {
@@ -517,8 +544,8 @@ var SqliteStore = class {
517
544
  conditions.push("source = ?");
518
545
  args.push(filter.source);
519
546
  } else {
520
- conditions.push("source LIKE ? ESCAPE '\\'");
521
- args.push(streamPatternToLike(filter.source));
547
+ conditions.push("source GLOB ?");
548
+ args.push(streamPatternToGlob(filter.source));
522
549
  }
523
550
  }
524
551
  if (filter.blocked !== void 0) {
@@ -600,15 +627,15 @@ var SqliteStore = class {
600
627
  // --- query_streams: read-only introspection with filters ---
601
628
  async query_streams(callback, query) {
602
629
  const limit = query?.limit ?? 100;
603
- let sql = "SELECT stream, source, at, retry, blocked, error, leased_by, leased_until, priority, lane FROM streams WHERE 1=1";
630
+ let sql = "SELECT stream, source, at, retry, blocked, error, leased_by, leased_until, priority, lane, deferred_at FROM streams WHERE 1=1";
604
631
  const args = [];
605
632
  if (query?.stream !== void 0) {
606
633
  if (query.stream_exact) {
607
634
  sql += " AND stream = ?";
608
635
  args.push(query.stream);
609
636
  } else {
610
- sql += " AND stream LIKE ? ESCAPE '\\'";
611
- args.push(streamPatternToLike(query.stream));
637
+ sql += " AND stream GLOB ?";
638
+ args.push(streamPatternToGlob(query.stream));
612
639
  }
613
640
  }
614
641
  if (query?.source !== void 0) {
@@ -617,8 +644,8 @@ var SqliteStore = class {
617
644
  sql += " AND source = ?";
618
645
  args.push(query.source);
619
646
  } else {
620
- sql += " AND source LIKE ? ESCAPE '\\'";
621
- args.push(streamPatternToLike(query.source));
647
+ sql += " AND source GLOB ?";
648
+ args.push(streamPatternToGlob(query.source));
622
649
  }
623
650
  }
624
651
  if (query?.blocked !== void 0) {
@@ -642,6 +669,7 @@ var SqliteStore = class {
642
669
  let count = 0;
643
670
  for (const row of streamsResult.rows) {
644
671
  const leased_until = row.leased_until;
672
+ const deferred_at = row.deferred_at;
645
673
  callback({
646
674
  stream: row.stream,
647
675
  source: row.source ?? void 0,
@@ -652,7 +680,8 @@ var SqliteStore = class {
652
680
  priority: Number(row.priority),
653
681
  leased_by: row.leased_by ?? void 0,
654
682
  leased_until: leased_until ? new Date(leased_until) : void 0,
655
- lane: row.lane
683
+ lane: row.lane,
684
+ deferred_at: deferred_at ? new Date(deferred_at).getTime() : void 0
656
685
  });
657
686
  count++;
658
687
  }
@@ -705,8 +734,8 @@ var SqliteStore = class {
705
734
  where.push(`e.stream = ?`);
706
735
  args.push(input.stream);
707
736
  } else {
708
- where.push(`e.stream LIKE ? ESCAPE '\\'`);
709
- args.push(streamPatternToLike(input.stream));
737
+ where.push(`e.stream GLOB ?`);
738
+ args.push(streamPatternToGlob(input.stream));
710
739
  }
711
740
  }
712
741
  if (exclude.length) {
@@ -775,8 +804,8 @@ var SqliteStore = class {
775
804
  stream: row.stream,
776
805
  version: Number(row.version),
777
806
  name: row.name,
778
- data: JSON.parse(row.data),
779
- meta: JSON.parse(row.meta),
807
+ data: parse_json(row.data),
808
+ meta: parse_json(row.meta),
780
809
  created: new Date(row.created),
781
810
  pii: await this._parse_pii_from_read(row.pii)
782
811
  });
@@ -848,8 +877,8 @@ var SqliteStore = class {
848
877
  stream,
849
878
  version: Number(version),
850
879
  name,
851
- data: JSON.parse(data),
852
- meta: JSON.parse(meta),
880
+ data: parse_json(data),
881
+ meta: parse_json(meta),
853
882
  created: new Date(created),
854
883
  pii: await this._parse_pii_from_read(pii)
855
884
  });
@@ -930,8 +959,8 @@ var SqliteStore = class {
930
959
  version: Number(row.version),
931
960
  created: new Date(row.created),
932
961
  name: row.name,
933
- data: JSON.parse(row.data),
934
- meta: JSON.parse(row.meta)
962
+ data: parse_json(row.data),
963
+ meta: parse_json(row.meta)
935
964
  },
936
965
  before
937
966
  });
@@ -1050,6 +1079,6 @@ var SqliteStore = class {
1050
1079
  };
1051
1080
  export {
1052
1081
  SqliteStore,
1053
- streamPatternToLike
1082
+ streamPatternToGlob
1054
1083
  };
1055
1084
  //# sourceMappingURL=index.js.map