@rotorsoft/act-tck 1.38.19 → 1.38.21

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
@@ -135,9 +135,9 @@ var runCacheDifferentialTck = (options) => {
135
135
 
136
136
  // src/cache-tck.ts
137
137
  import { afterEach, beforeEach as beforeEach2, describe as describe2, expect as expect2, it as it2 } from "vitest";
138
- var entry = (event_id, state = {}, stream = `s-${event_id}`) => ({
138
+ var entry = (event_id, state2 = {}, stream = `s-${event_id}`) => ({
139
139
  stream,
140
- state,
140
+ state: state2,
141
141
  version: event_id,
142
142
  event_id,
143
143
  patches: 1,
@@ -363,7 +363,7 @@ async function load_surface(entry2) {
363
363
  if (visited.has(abs)) return;
364
364
  const content = await fs.readFile(abs, "utf8");
365
365
  visited.set(abs, content);
366
- const re_export = /^\s*(?:import|export)\b[^"';]*\bfrom\s+["'](\.[^"']+)["']/gm;
366
+ const re_export = /^\s*export\b[^"';]*\bfrom\s+["'](\.[^"']+)["']/gm;
367
367
  for (const m of content.matchAll(re_export)) {
368
368
  const rel = m[1].replace(/\.js$/, ".ts");
369
369
  const target = path.resolve(path.dirname(abs), rel);
@@ -1113,10 +1113,12 @@ import {
1113
1113
  InMemoryCache,
1114
1114
  SNAP_EVENT as SNAP_EVENT2,
1115
1115
  sleep,
1116
+ state,
1116
1117
  TOMBSTONE_EVENT,
1117
1118
  ValidationError
1118
1119
  } from "@rotorsoft/act";
1119
1120
  import { afterAll as afterAll4, beforeAll as beforeAll4, describe as describe8, expect as expect8, it as it7, vi } from "vitest";
1121
+ import { z as z2 } from "zod";
1120
1122
  var runStoreTck = (options) => {
1121
1123
  describe8(`TCK / Store / ${options.name}`, () => {
1122
1124
  let store;
@@ -2150,7 +2152,7 @@ var runStoreTck = (options) => {
2150
2152
  const second = await store.claim(0, 100, `w-${uid()}`, 1);
2151
2153
  expect8(second.find((l) => l.stream === s)).toBeDefined();
2152
2154
  });
2153
- it7("does not starve a default-priority lagging stream under sustained high-priority load (ACT-1223)", async () => {
2155
+ it7("does not starve a default-priority lagging stream under sustained high-priority load", async () => {
2154
2156
  const fresh = await options.factory();
2155
2157
  try {
2156
2158
  await fresh.drop();
@@ -2552,7 +2554,7 @@ var runStoreTck = (options) => {
2552
2554
  const again = await store.claim(100, 100, `w2-${uid()}`, 1e5);
2553
2555
  expect8(again.find((l) => l.stream === s)).toBeUndefined();
2554
2556
  });
2555
- it7("returns the post-block row, not the caller's lease (#1382)", async () => {
2557
+ it7("returns the post-block row, not the caller's lease", async () => {
2556
2558
  const s = `block-returns-${uid()}`;
2557
2559
  await store.subscribe([{ stream: s }]);
2558
2560
  await store.commit(
@@ -2597,7 +2599,7 @@ var runStoreTck = (options) => {
2597
2599
  ]);
2598
2600
  expect8(blocked).toHaveLength(0);
2599
2601
  });
2600
- it7("re-blocking an already-blocked stream is a no-op (#1263)", async () => {
2602
+ it7("re-blocking an already-blocked stream is a no-op", async () => {
2601
2603
  const s = `block-twice-${uid()}`;
2602
2604
  await store.subscribe([{ stream: s }]);
2603
2605
  await store.commit(
@@ -2715,7 +2717,7 @@ var runStoreTck = (options) => {
2715
2717
  const leased = await store.claim(100, 100, `w-${uid()}`, 1e5);
2716
2718
  expect8(leased.find((l) => l.stream === s)).toBeDefined();
2717
2719
  });
2718
- it7("exposes a future deferred_at via query_streams (#1221)", async () => {
2720
+ it7("exposes a future deferred_at via query_streams", async () => {
2719
2721
  const deferred = `defer-qs-${uid()}`;
2720
2722
  const plain = `defer-qs-plain-${uid()}`;
2721
2723
  await store.subscribe([{ stream: deferred }, { stream: plain }]);
@@ -2770,7 +2772,7 @@ var runStoreTck = (options) => {
2770
2772
  expect8(await store.defer([`missing-${uid()}`], Date.now())).toBe(0);
2771
2773
  expect8(await store.defer([], Date.now())).toBe(0);
2772
2774
  });
2773
- it7("counts a duplicated stream name once (#1360)", async () => {
2775
+ it7("counts a duplicated stream name once", async () => {
2774
2776
  const s = `defer-dup-${uid()}`;
2775
2777
  await store.subscribe([{ stream: s }]);
2776
2778
  await store.commit(
@@ -2836,7 +2838,7 @@ var runStoreTck = (options) => {
2836
2838
  expect8(re.retry).toBe(0);
2837
2839
  await store.ack([{ ...re, at: 1e6 }]);
2838
2840
  });
2839
- it7("persists the lease's retry on a backoff-style due lease (#1262)", async () => {
2841
+ it7("persists the lease's retry on a backoff-style due lease", async () => {
2840
2842
  const s = `ackdefer-retry-${uid()}`;
2841
2843
  await store.subscribe([{ stream: s }]);
2842
2844
  await store.commit(
@@ -2856,7 +2858,7 @@ var runStoreTck = (options) => {
2856
2858
  expect8(re.retry).toBe(4);
2857
2859
  await store.ack([{ ...re, at: 1e6 }]);
2858
2860
  });
2859
- it7("advances the watermark to `at` while persisting the schedule on a partial-progress due lease (#1278)", async () => {
2861
+ it7("advances the watermark to `at` while persisting the schedule on a partial-progress due lease", async () => {
2860
2862
  const s = `ackadvance-${uid()}`;
2861
2863
  await store.subscribe([{ stream: s }]);
2862
2864
  const [e1] = await store.commit(
@@ -2946,7 +2948,7 @@ var runStoreTck = (options) => {
2946
2948
  expect8(await store.reset([`missing-${uid()}`])).toBe(0);
2947
2949
  expect8(await store.reset([])).toBe(0);
2948
2950
  });
2949
- it7("counts a duplicated stream name once (#1360)", async () => {
2951
+ it7("counts a duplicated stream name once", async () => {
2950
2952
  const s = `reset-dup-${uid()}`;
2951
2953
  await store.subscribe([{ stream: s }]);
2952
2954
  await store.commit(
@@ -3218,6 +3220,106 @@ var runStoreTck = (options) => {
3218
3220
  expect8(got1.priority).toBe(3);
3219
3221
  expect8(got2.priority).toBe(5);
3220
3222
  });
3223
+ it7("matches a stream pattern", async () => {
3224
+ const tag = uid();
3225
+ await store.subscribe([
3226
+ { stream: `pri-${tag}-proj-a` },
3227
+ { stream: `pri-${tag}-proj-b` },
3228
+ { stream: `pri-${tag}-audit-x` }
3229
+ ]);
3230
+ expect8(
3231
+ await store.prioritize({ stream: `^pri-${tag}-proj-.*$` }, 3)
3232
+ ).toBe(2);
3233
+ });
3234
+ it7("matches by source, skipping rows that have none", async () => {
3235
+ const tag = uid();
3236
+ await store.subscribe([
3237
+ { stream: `pri-${tag}-t1`, source: `pri-src-${tag}-users` },
3238
+ { stream: `pri-${tag}-t2`, source: `pri-src-${tag}-audit` },
3239
+ { stream: `pri-${tag}-t3` }
3240
+ ]);
3241
+ expect8(
3242
+ await store.prioritize(
3243
+ { source: `pri-src-${tag}-users`, source_exact: true },
3244
+ 4
3245
+ )
3246
+ ).toBe(1);
3247
+ expect8(
3248
+ await store.prioritize({ source: `^pri-src-${tag}-.*$` }, 6)
3249
+ ).toBe(2);
3250
+ });
3251
+ it7("matches by blocked state, both ways", async () => {
3252
+ const fresh = await options.factory();
3253
+ try {
3254
+ await fresh.drop();
3255
+ await fresh.seed();
3256
+ const tag = uid();
3257
+ const src = `pri-src-${tag}`;
3258
+ const ok = `pri-${tag}-ok`;
3259
+ const bad = `pri-${tag}-bad`;
3260
+ await fresh.commit(
3261
+ src,
3262
+ [inc(1)],
3263
+ make_meta({ stream: src })
3264
+ );
3265
+ await fresh.subscribe([
3266
+ { stream: ok, source: src },
3267
+ { stream: bad, source: src }
3268
+ ]);
3269
+ await correlate(fresh);
3270
+ const leases = await fresh.claim(2, 0, `w-${uid()}`, 5e3);
3271
+ const lease = leases.find((l) => l.stream === bad);
3272
+ await fresh.block([{ ...lease, error: "boom" }]);
3273
+ const scope = `^pri-${tag}-.*$`;
3274
+ expect8(
3275
+ await fresh.prioritize({ stream: scope, blocked: true }, 9)
3276
+ ).toBe(1);
3277
+ expect8(
3278
+ await fresh.prioritize({ stream: scope, blocked: false }, 9)
3279
+ ).toBe(1);
3280
+ } finally {
3281
+ await fresh.dispose();
3282
+ }
3283
+ });
3284
+ it7("an empty filter updates every row, and skips rows already at the value", async () => {
3285
+ const fresh = await options.factory();
3286
+ try {
3287
+ await fresh.drop();
3288
+ await fresh.seed();
3289
+ const tag = uid();
3290
+ await fresh.subscribe([
3291
+ { stream: `pri-${tag}-a` },
3292
+ { stream: `pri-${tag}-b`, priority: 5 }
3293
+ ]);
3294
+ expect8(await fresh.prioritize({}, 5)).toBe(1);
3295
+ expect8(await fresh.prioritize({}, 5)).toBe(0);
3296
+ } finally {
3297
+ await fresh.dispose();
3298
+ }
3299
+ });
3300
+ it7("claim returns higher-priority streams first under tied watermarks", async () => {
3301
+ const fresh = await options.factory();
3302
+ try {
3303
+ await fresh.drop();
3304
+ await fresh.seed();
3305
+ const tag = uid();
3306
+ const src = `pri-src-${tag}`;
3307
+ await fresh.commit(
3308
+ src,
3309
+ [inc(1), inc(2)],
3310
+ make_meta({ stream: src })
3311
+ );
3312
+ await fresh.subscribe([
3313
+ { stream: `pri-${tag}-low`, source: src, priority: 0 },
3314
+ { stream: `pri-${tag}-high`, source: src, priority: 5 }
3315
+ ]);
3316
+ await correlate(fresh);
3317
+ const leases = await fresh.claim(1, 0, `w-${uid()}`, 5e3);
3318
+ expect8(leases.map((l) => l.stream)).toEqual([`pri-${tag}-high`]);
3319
+ } finally {
3320
+ await fresh.dispose();
3321
+ }
3322
+ });
3221
3323
  });
3222
3324
  describe8("lanes", () => {
3223
3325
  it7("subscribe defaults lane to 'default' when omitted", async () => {
@@ -3431,7 +3533,7 @@ var runStoreTck = (options) => {
3431
3533
  });
3432
3534
  });
3433
3535
  describe8("long identifiers", () => {
3434
- it7("accepts a stream name well past 100 characters (#1420)", async () => {
3536
+ it7("accepts a stream name well past 100 characters", async () => {
3435
3537
  const tag = uid();
3436
3538
  const long = `${tag}-${"x".repeat(140)}`;
3437
3539
  await store.commit(
@@ -3454,7 +3556,7 @@ var runStoreTck = (options) => {
3454
3556
  });
3455
3557
  });
3456
3558
  describe8("truncate", () => {
3457
- it7("leaves subscriptions untouched for restart and retire alike (#1527)", async () => {
3559
+ it7("leaves subscriptions untouched for restart and retire alike", async () => {
3458
3560
  const tag = uid();
3459
3561
  const restarted = `trunc-restart-${tag}`;
3460
3562
  const retired = `trunc-retire-${tag}`;
@@ -3482,7 +3584,7 @@ var runStoreTck = (options) => {
3482
3584
  expect8(rows).toContain(restarted);
3483
3585
  expect8(rows).toContain(retired);
3484
3586
  });
3485
- it7("leaves a retired stream's surviving subscription unclaimable (#1527)", async () => {
3587
+ it7("leaves a retired stream's surviving subscription unclaimable", async () => {
3486
3588
  const lane = `inert-lane-${uid()}`;
3487
3589
  const s = `trunc-inert-${uid()}`;
3488
3590
  const [e] = await store.commit(
@@ -3544,7 +3646,7 @@ var runStoreTck = (options) => {
3544
3646
  );
3545
3647
  expect8(remaining[0].data).toEqual({ count: 7 });
3546
3648
  });
3547
- it7("leaves exactly one seed when a stream is listed twice (#1677)", async () => {
3649
+ it7("leaves exactly one seed when a stream is listed twice", async () => {
3548
3650
  const s = `trunc-dup-${uid()}`;
3549
3651
  await store.commit(
3550
3652
  s,
@@ -3566,7 +3668,7 @@ var runStoreTck = (options) => {
3566
3668
  expect8(remaining[0].data).toEqual({ count: 2 });
3567
3669
  expect8(result.get(s)?.deleted).toBe(1);
3568
3670
  });
3569
- it7("applies windowed targets before full ones (#1677)", async () => {
3671
+ it7("applies windowed targets before full ones", async () => {
3570
3672
  const s = `trunc-mix-${uid()}`;
3571
3673
  await store.commit(
3572
3674
  s,
@@ -3770,7 +3872,7 @@ var runStoreTck = (options) => {
3770
3872
  });
3771
3873
  });
3772
3874
  describe8("query_streams", () => {
3773
- it7("keyset pagination visits every stream regardless of name case (#1375)", async () => {
3875
+ it7("keyset pagination visits every stream regardless of name case", async () => {
3774
3876
  const tag = uid();
3775
3877
  const names = [
3776
3878
  `${tag}-Bravo`,
@@ -3965,7 +4067,7 @@ var runStoreTck = (options) => {
3965
4067
  });
3966
4068
  expect8(stats.size).toBe(0);
3967
4069
  });
3968
- it7("keyset pagination visits every stream regardless of name case (#1357)", async () => {
4070
+ it7("keyset pagination visits every stream regardless of name case", async () => {
3969
4071
  const tag = uid();
3970
4072
  const names = [
3971
4073
  `${tag}-Bravo`,
@@ -4258,6 +4360,43 @@ var runStoreTck = (options) => {
4258
4360
  });
4259
4361
  expect8([...all.keys()].sort()).toEqual([...streams].sort());
4260
4362
  });
4363
+ it7("pages the count and tail paths by limit + after too", async () => {
4364
+ const tag = uid();
4365
+ for (const s of [`qsc-${tag}-c`, `qsc-${tag}-a`, `qsc-${tag}-b`])
4366
+ await store.commit(
4367
+ s,
4368
+ [inc(1), inc(2)],
4369
+ make_meta({ stream: s })
4370
+ );
4371
+ const pattern = { stream: `qsc-${tag}-.*` };
4372
+ const counted = await store.query_stats(pattern, {
4373
+ count: true,
4374
+ names: true,
4375
+ limit: 2
4376
+ });
4377
+ expect8([...counted.keys()]).toEqual([`qsc-${tag}-a`, `qsc-${tag}-b`]);
4378
+ expect8(counted.get(`qsc-${tag}-a`)?.count).toBe(2);
4379
+ expect8(counted.get(`qsc-${tag}-a`)?.names).toEqual({ Incremented: 2 });
4380
+ const counted2 = await store.query_stats(pattern, {
4381
+ count: true,
4382
+ limit: 2,
4383
+ after: `qsc-${tag}-b`
4384
+ });
4385
+ expect8([...counted2.keys()]).toEqual([`qsc-${tag}-c`]);
4386
+ const tailed = await store.query_stats(pattern, {
4387
+ tail: true,
4388
+ limit: 2
4389
+ });
4390
+ expect8([...tailed.keys()]).toEqual([`qsc-${tag}-a`, `qsc-${tag}-b`]);
4391
+ expect8(tailed.get(`qsc-${tag}-a`)?.head.version).toBe(1);
4392
+ expect8(tailed.get(`qsc-${tag}-a`)?.tail?.version).toBe(0);
4393
+ const tailed2 = await store.query_stats(pattern, {
4394
+ tail: true,
4395
+ limit: 2,
4396
+ after: `qsc-${tag}-b`
4397
+ });
4398
+ expect8([...tailed2.keys()]).toEqual([`qsc-${tag}-c`]);
4399
+ });
4261
4400
  });
4262
4401
  describe8.skipIf(!caps.source_matches)(
4263
4402
  "query_streams source_matches (capability)",
@@ -4388,6 +4527,52 @@ var runStoreTck = (options) => {
4388
4527
  expect8(positions).toEqual([s]);
4389
4528
  });
4390
4529
  });
4530
+ describe8("autoclose end-to-end", () => {
4531
+ const Ticket = state({ TckTicket: z2.object({ open: z2.boolean() }) }).init(() => ({ open: false })).emits({
4532
+ TckTicketOpened: z2.object({ title: z2.string() }),
4533
+ TckTicketResolved: z2.object({})
4534
+ }).patch({
4535
+ TckTicketOpened: () => ({ open: true }),
4536
+ TckTicketResolved: () => ({ open: false })
4537
+ }).on({ OpenTckTicket: z2.object({ title: z2.string() }) }).emit((a) => ["TckTicketOpened", { title: a.title }]).on({ ResolveTckTicket: z2.object({}) }).emit(() => ["TckTicketResolved", {}]).autocloses({ is: "TckTicketResolved" }).build();
4538
+ const actor2 = { id: "tck", name: "tck" };
4539
+ const run = async (resolve) => {
4540
+ const fresh = await options.factory();
4541
+ await fresh.drop();
4542
+ await fresh.seed();
4543
+ const cache = new InMemoryCache();
4544
+ const app = act().withState(Ticket).build({ scoped: { store: fresh, cache } });
4545
+ const truncated = [];
4546
+ app.on("closed", (r) => truncated.push(...r.truncated.keys()));
4547
+ const stream = `ac-${uid()}`;
4548
+ try {
4549
+ await app.do("OpenTckTicket", { stream, actor: actor2 }, { title: "a" });
4550
+ if (resolve) await app.do("ResolveTckTicket", { stream, actor: actor2 }, {});
4551
+ await app.correlate();
4552
+ await app.drain();
4553
+ const surviving = [];
4554
+ await fresh.query((e) => surviving.push(String(e.name)), {
4555
+ stream,
4556
+ stream_exact: true
4557
+ });
4558
+ return { stream, truncated, surviving };
4559
+ } finally {
4560
+ await app.shutdown();
4561
+ await cache.dispose();
4562
+ await fresh.dispose();
4563
+ }
4564
+ };
4565
+ it7("closes a stream whose head is the terminal event, leaving a tombstone", async () => {
4566
+ const { stream, truncated, surviving } = await run(true);
4567
+ expect8(truncated).toContain(stream);
4568
+ expect8(surviving).toEqual([TOMBSTONE_EVENT]);
4569
+ });
4570
+ it7("leaves a stream whose head is not the terminal event", async () => {
4571
+ const { stream, truncated, surviving } = await run(false);
4572
+ expect8(truncated).not.toContain(stream);
4573
+ expect8(surviving).toEqual(["TckTicketOpened"]);
4574
+ });
4575
+ });
4391
4576
  describe8("seed_stream helper coverage", () => {
4392
4577
  it7("commits N events with monotonically increasing ids", async () => {
4393
4578
  const s = `seed-${uid()}`;
@@ -5026,7 +5211,7 @@ var runStoreTck = (options) => {
5026
5211
  expect8(seen[0].pii).toEqual({ email: "u@example.com", name: "Ursula" });
5027
5212
  expect8(seen[0].data).toEqual({ amount: 1 });
5028
5213
  });
5029
- describe8("with_pii (#1675)", () => {
5214
+ describe8("with_pii", () => {
5030
5215
  const seed_pii = async (s) => await store.commit(
5031
5216
  s,
5032
5217
  [
@@ -5090,7 +5275,7 @@ var runStoreTck = (options) => {
5090
5275
  for (const e of seen) expect8(e.pii ?? null).toBeNull();
5091
5276
  });
5092
5277
  });
5093
- it7("round-trips a Date losslessly in pii, like data/meta (#1365/#1370/#1556)", async () => {
5278
+ it7("round-trips a Date losslessly in pii, like data/meta", async () => {
5094
5279
  const s = `pii-date-${uid()}`;
5095
5280
  const when = /* @__PURE__ */ new Date("2024-03-01T10:20:30.000Z");
5096
5281
  await store.commit(
@@ -5119,7 +5304,7 @@ var runStoreTck = (options) => {
5119
5304
  expect8(recovered(born)).toBe(when.getTime());
5120
5305
  expect8(typeof at).toBe(typeof born);
5121
5306
  });
5122
- it7("query_stats head/tail never carry pii \u2014 introspection surface is pii-safe (#1294)", async () => {
5307
+ it7("query_stats head/tail never carry pii \u2014 introspection surface is pii-safe", async () => {
5123
5308
  const s = `pii-stats-${uid()}`;
5124
5309
  await store.commit(
5125
5310
  s,