@rotorsoft/act-tck 1.26.2 → 1.26.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/README.md +2 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/store-tck.d.ts +14 -5
- package/dist/@types/store-tck.d.ts.map +1 -1
- package/dist/index.cjs +215 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +215 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -7,9 +7,15 @@ import type { Store } from "@rotorsoft/act/types";
|
|
|
7
7
|
export type StoreCapabilities = {
|
|
8
8
|
/**
|
|
9
9
|
* Adapter implements {@link Store.notify}. When `true`, the TCK runs
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* the cross-instance conformance cases: a listener receives commits
|
|
11
|
+
* from a *sibling* instance produced by the same `factory`, never its
|
|
12
|
+
* own commits (the port's self-filtering MUST), and exactly one
|
|
13
|
+
* notification per commit transaction carrying the full event batch.
|
|
14
|
+
* Requires the `factory` to produce instances sharing one backing
|
|
15
|
+
* store — e.g. two PostgresStores on the same schema/table, or two
|
|
16
|
+
* `withBroker` decorators on one broker. True cross-*process*
|
|
17
|
+
* LISTEN/NOTIFY plumbing needs two processes and stays in the
|
|
18
|
+
* adapter's own suite.
|
|
13
19
|
*/
|
|
14
20
|
readonly notify?: boolean;
|
|
15
21
|
/**
|
|
@@ -90,7 +96,8 @@ export type StoreTckOptions = {
|
|
|
90
96
|
* `ValidationError` — never a silent approximation
|
|
91
97
|
* - `subscribe` — idempotent re-subscribe, watermark return value
|
|
92
98
|
* - `claim` / `ack` — lease lifecycle, dual frontiers, leased streams
|
|
93
|
-
* not double-claimed
|
|
99
|
+
* not double-claimed, exact-source has-work matching, timed-out-lease
|
|
100
|
+
* retry accounting
|
|
94
101
|
* - `block` — blocked streams hidden from claim, only same-drainer can block
|
|
95
102
|
* - `reset` — restart watermarks (including blocked), no-op for missing
|
|
96
103
|
* - `prioritize` — bulk priority updates by filter
|
|
@@ -99,7 +106,9 @@ export type StoreTckOptions = {
|
|
|
99
106
|
* behind the closest safe snapshot, tail + subscriptions kept,
|
|
100
107
|
* no-snapshot no-op, mixed full + windowed targets
|
|
101
108
|
* - `query_streams` — filters, exact-match, pagination, blocked
|
|
102
|
-
* - `notify` (capability-gated) —
|
|
109
|
+
* - `notify` (capability-gated) — cross-instance delivery, self-filtering
|
|
110
|
+
* (an instance never receives its own commits), one notification per
|
|
111
|
+
* commit transaction with the full event batch
|
|
103
112
|
*
|
|
104
113
|
* Tests namespace their streams with a per-test {@link uid} so the
|
|
105
114
|
* suite is parallel-safe against a shared backing store (e.g., a real
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-tck.d.ts","sourceRoot":"","sources":["../../src/store-tck.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAQV,KAAK,EAEN,MAAM,sBAAsB,CAAC;AAc9B;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B
|
|
1
|
+
{"version":3,"file":"store-tck.d.ts","sourceRoot":"","sources":["../../src/store-tck.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAQV,KAAK,EAEN,MAAM,sBAAsB,CAAC;AAc9B;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC/C;;OAEG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,iBAAiB,CAAC;CAC3C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,eAAe,KAAG,IAy4GtD,CAAC"}
|
package/dist/index.cjs
CHANGED
|
@@ -1481,6 +1481,93 @@ var runStoreTck = (options) => {
|
|
|
1481
1481
|
await fresh.dispose();
|
|
1482
1482
|
}
|
|
1483
1483
|
});
|
|
1484
|
+
(0, import_vitest9.it)("counts a timed-out lease reclaimed by another worker against the retry budget; ack resets it", async () => {
|
|
1485
|
+
const fresh = await options.factory();
|
|
1486
|
+
try {
|
|
1487
|
+
await fresh.drop();
|
|
1488
|
+
await fresh.seed();
|
|
1489
|
+
const s = `lease-timeout-${uid()}`;
|
|
1490
|
+
await fresh.subscribe([{ stream: s }]);
|
|
1491
|
+
const [e1] = await seed_stream(fresh, s, 1);
|
|
1492
|
+
const a = await fresh.claim(1, 0, `wA-${uid()}`, 0);
|
|
1493
|
+
(0, import_vitest9.expect)(a.find((l) => l.stream === s)?.retry).toBe(0);
|
|
1494
|
+
const b = await fresh.claim(1, 0, `wB-${uid()}`, 3e4);
|
|
1495
|
+
const b_lease = b.find((l) => l.stream === s);
|
|
1496
|
+
(0, import_vitest9.expect)(b_lease).toBeDefined();
|
|
1497
|
+
(0, import_vitest9.expect)(b_lease.retry).toBe(1);
|
|
1498
|
+
await fresh.ack([{ ...b_lease, at: e1.id }]);
|
|
1499
|
+
await seed_stream(fresh, s, 1);
|
|
1500
|
+
const c = await fresh.claim(1, 0, `wC-${uid()}`, 3e4);
|
|
1501
|
+
(0, import_vitest9.expect)(c.find((l) => l.stream === s)?.retry).toBe(0);
|
|
1502
|
+
} finally {
|
|
1503
|
+
await fresh.dispose();
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
});
|
|
1507
|
+
(0, import_vitest9.describe)("claim source matching", () => {
|
|
1508
|
+
(0, import_vitest9.it)("treats source as an exact stream name \u2014 no substring or pattern overmatch", async () => {
|
|
1509
|
+
const fresh = await options.factory();
|
|
1510
|
+
try {
|
|
1511
|
+
await fresh.drop();
|
|
1512
|
+
await fresh.seed();
|
|
1513
|
+
const base = `src-${uid()}`;
|
|
1514
|
+
const sibling = `${base}2`;
|
|
1515
|
+
const target = `agg-${uid()}`;
|
|
1516
|
+
const control = `ctl-${uid()}`;
|
|
1517
|
+
await fresh.subscribe([
|
|
1518
|
+
{ stream: target, source: base },
|
|
1519
|
+
// Control subscription sourced from the sibling — it DOES see
|
|
1520
|
+
// the sibling commit below, so the negative assertion runs
|
|
1521
|
+
// through a populated claim result.
|
|
1522
|
+
{ stream: control, source: sibling }
|
|
1523
|
+
]);
|
|
1524
|
+
const [seeded] = await seed_stream(fresh, base, 1);
|
|
1525
|
+
const [ctl_seeded] = await seed_stream(fresh, sibling, 1);
|
|
1526
|
+
const first = await fresh.claim(100, 0, `w-${uid()}`, 3e4);
|
|
1527
|
+
const mine = first.find((l) => l.stream === target);
|
|
1528
|
+
const ctl = first.find((l) => l.stream === control);
|
|
1529
|
+
(0, import_vitest9.expect)(mine).toBeDefined();
|
|
1530
|
+
(0, import_vitest9.expect)(ctl).toBeDefined();
|
|
1531
|
+
await fresh.ack([
|
|
1532
|
+
{ ...mine, at: seeded.id },
|
|
1533
|
+
{ ...ctl, at: ctl_seeded.id }
|
|
1534
|
+
]);
|
|
1535
|
+
await seed_stream(fresh, sibling, 1);
|
|
1536
|
+
const second = await fresh.claim(100, 0, `w-${uid()}`, 3e4);
|
|
1537
|
+
(0, import_vitest9.expect)(second.find((l) => l.stream === control)).toBeDefined();
|
|
1538
|
+
(0, import_vitest9.expect)(second.find((l) => l.stream === target)).toBeUndefined();
|
|
1539
|
+
} finally {
|
|
1540
|
+
await fresh.dispose();
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
(0, import_vitest9.it)("receives exactly its source stream's events", async () => {
|
|
1544
|
+
const fresh = await options.factory();
|
|
1545
|
+
try {
|
|
1546
|
+
await fresh.drop();
|
|
1547
|
+
await fresh.seed();
|
|
1548
|
+
const base = `src-${uid()}`;
|
|
1549
|
+
const target = `agg-${uid()}`;
|
|
1550
|
+
await fresh.subscribe([{ stream: target, source: base }]);
|
|
1551
|
+
const [e1] = await seed_stream(fresh, base, 1);
|
|
1552
|
+
const first = await fresh.claim(100, 0, `w-${uid()}`, 3e4);
|
|
1553
|
+
const f = first.find((l) => l.stream === target);
|
|
1554
|
+
(0, import_vitest9.expect)(f).toBeDefined();
|
|
1555
|
+
(0, import_vitest9.expect)(f.source).toBe(base);
|
|
1556
|
+
await fresh.ack([{ ...f, at: e1.id }]);
|
|
1557
|
+
const [e2] = await seed_stream(fresh, base, 1);
|
|
1558
|
+
const second = await fresh.claim(100, 0, `w-${uid()}`, 3e4);
|
|
1559
|
+
const sec = second.find((l) => l.stream === target);
|
|
1560
|
+
(0, import_vitest9.expect)(sec).toBeDefined();
|
|
1561
|
+
(0, import_vitest9.expect)(sec.at).toBe(e1.id);
|
|
1562
|
+
const events2 = await collect(fresh, {
|
|
1563
|
+
stream: sec.source,
|
|
1564
|
+
after: sec.at
|
|
1565
|
+
});
|
|
1566
|
+
(0, import_vitest9.expect)(events2.map((e) => e.id)).toEqual([e2.id]);
|
|
1567
|
+
} finally {
|
|
1568
|
+
await fresh.dispose();
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1484
1571
|
});
|
|
1485
1572
|
import_vitest9.describe.skipIf(!caps.concurrent_claim)("concurrency (capability)", () => {
|
|
1486
1573
|
(0, import_vitest9.it)("never double-leases a stream across concurrent claimers", async () => {
|
|
@@ -1512,6 +1599,51 @@ var runStoreTck = (options) => {
|
|
|
1512
1599
|
await fresh.dispose();
|
|
1513
1600
|
}
|
|
1514
1601
|
});
|
|
1602
|
+
(0, import_vitest9.it)("does not hand an unexpired lease to a competing claimer", async () => {
|
|
1603
|
+
const fresh = await options.factory();
|
|
1604
|
+
try {
|
|
1605
|
+
await fresh.drop();
|
|
1606
|
+
await fresh.seed();
|
|
1607
|
+
const s = `unexpired-${uid()}`;
|
|
1608
|
+
const other = `unexpired-other-${uid()}`;
|
|
1609
|
+
await fresh.subscribe([{ stream: s }]);
|
|
1610
|
+
await seed_stream(fresh, s, 1);
|
|
1611
|
+
const a = await fresh.claim(100, 100, `wA-${uid()}`, 6e4);
|
|
1612
|
+
(0, import_vitest9.expect)(a.find((l) => l.stream === s)).toBeDefined();
|
|
1613
|
+
await fresh.subscribe([{ stream: other }]);
|
|
1614
|
+
await seed_stream(fresh, other, 1);
|
|
1615
|
+
const b = await fresh.claim(100, 100, `wB-${uid()}`, 6e4);
|
|
1616
|
+
(0, import_vitest9.expect)(b.find((l) => l.stream === other)).toBeDefined();
|
|
1617
|
+
(0, import_vitest9.expect)(b.find((l) => l.stream === s)).toBeUndefined();
|
|
1618
|
+
} finally {
|
|
1619
|
+
await fresh.dispose();
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
(0, import_vitest9.it)("hands an expired lease to exactly one competing claimer, with retry accounting shared across workers", async () => {
|
|
1623
|
+
const fresh = await options.factory();
|
|
1624
|
+
try {
|
|
1625
|
+
await fresh.drop();
|
|
1626
|
+
await fresh.seed();
|
|
1627
|
+
const s = `expired-${uid()}`;
|
|
1628
|
+
await fresh.subscribe([{ stream: s }]);
|
|
1629
|
+
const [e1] = await seed_stream(fresh, s, 1);
|
|
1630
|
+
const dead = await fresh.claim(100, 0, `wDead-${uid()}`, 0);
|
|
1631
|
+
(0, import_vitest9.expect)(dead.find((l) => l.stream === s)?.retry).toBe(0);
|
|
1632
|
+
const [b, c] = await Promise.all([
|
|
1633
|
+
fresh.claim(100, 100, `wB-${uid()}`, 6e4),
|
|
1634
|
+
fresh.claim(100, 100, `wC-${uid()}`, 6e4)
|
|
1635
|
+
]);
|
|
1636
|
+
const winners = [...b, ...c].filter((l) => l.stream === s);
|
|
1637
|
+
(0, import_vitest9.expect)(winners).toHaveLength(1);
|
|
1638
|
+
(0, import_vitest9.expect)(winners[0].retry).toBe(1);
|
|
1639
|
+
await fresh.ack([{ ...winners[0], at: e1.id }]);
|
|
1640
|
+
await seed_stream(fresh, s, 1);
|
|
1641
|
+
const again = await fresh.claim(100, 0, `wNext-${uid()}`, 6e4);
|
|
1642
|
+
(0, import_vitest9.expect)(again.find((l) => l.stream === s)?.retry).toBe(0);
|
|
1643
|
+
} finally {
|
|
1644
|
+
await fresh.dispose();
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1515
1647
|
});
|
|
1516
1648
|
(0, import_vitest9.describe)("block", () => {
|
|
1517
1649
|
(0, import_vitest9.it)("hides blocked streams from claim", async () => {
|
|
@@ -3502,6 +3634,89 @@ var runStoreTck = (options) => {
|
|
|
3502
3634
|
await Promise.resolve(disposer());
|
|
3503
3635
|
}
|
|
3504
3636
|
});
|
|
3637
|
+
(0, import_vitest9.it)("does not deliver an instance's own commits (self-filtering)", async () => {
|
|
3638
|
+
const received = [];
|
|
3639
|
+
let resolve_sentinel;
|
|
3640
|
+
const sentinel_arrived = new Promise((res) => {
|
|
3641
|
+
resolve_sentinel = res;
|
|
3642
|
+
});
|
|
3643
|
+
const own = `notify-self-${uid()}`;
|
|
3644
|
+
const remote = `notify-remote-${uid()}`;
|
|
3645
|
+
const sentinel = `notify-sentinel-${uid()}`;
|
|
3646
|
+
const disposer = await store.notify.call(store, (n) => {
|
|
3647
|
+
received.push(n);
|
|
3648
|
+
if (n.stream === sentinel) resolve_sentinel();
|
|
3649
|
+
});
|
|
3650
|
+
const writer = await options.factory();
|
|
3651
|
+
try {
|
|
3652
|
+
await store.commit(
|
|
3653
|
+
own,
|
|
3654
|
+
[inc(1)],
|
|
3655
|
+
make_meta({ stream: own })
|
|
3656
|
+
);
|
|
3657
|
+
await writer.commit(
|
|
3658
|
+
remote,
|
|
3659
|
+
[inc(1)],
|
|
3660
|
+
make_meta({ stream: remote })
|
|
3661
|
+
);
|
|
3662
|
+
await writer.commit(
|
|
3663
|
+
sentinel,
|
|
3664
|
+
[inc(1)],
|
|
3665
|
+
make_meta({ stream: sentinel })
|
|
3666
|
+
);
|
|
3667
|
+
await sentinel_arrived;
|
|
3668
|
+
(0, import_vitest9.expect)(received.find((n) => n.stream === own)).toBeUndefined();
|
|
3669
|
+
(0, import_vitest9.expect)(received.find((n) => n.stream === remote)).toBeDefined();
|
|
3670
|
+
(0, import_vitest9.expect)(received.find((n) => n.stream === sentinel)).toBeDefined();
|
|
3671
|
+
} finally {
|
|
3672
|
+
await writer.dispose();
|
|
3673
|
+
await Promise.resolve(disposer());
|
|
3674
|
+
}
|
|
3675
|
+
});
|
|
3676
|
+
(0, import_vitest9.it)("delivers one notification per commit transaction carrying the full event batch", async () => {
|
|
3677
|
+
const received = [];
|
|
3678
|
+
let resolve_sentinel;
|
|
3679
|
+
const sentinel_arrived = new Promise((res) => {
|
|
3680
|
+
resolve_sentinel = res;
|
|
3681
|
+
});
|
|
3682
|
+
const batch = `notify-batch-${uid()}`;
|
|
3683
|
+
const sentinel = `notify-batch-sentinel-${uid()}`;
|
|
3684
|
+
const disposer = await store.notify.call(store, (n) => {
|
|
3685
|
+
received.push(n);
|
|
3686
|
+
if (n.stream === sentinel) resolve_sentinel();
|
|
3687
|
+
});
|
|
3688
|
+
const writer = await options.factory();
|
|
3689
|
+
try {
|
|
3690
|
+
await writer.commit(
|
|
3691
|
+
batch,
|
|
3692
|
+
[inc(1), inc(2), inc(3)],
|
|
3693
|
+
make_meta({ stream: batch })
|
|
3694
|
+
);
|
|
3695
|
+
await writer.commit(
|
|
3696
|
+
sentinel,
|
|
3697
|
+
[inc(1)],
|
|
3698
|
+
make_meta({ stream: sentinel })
|
|
3699
|
+
);
|
|
3700
|
+
await sentinel_arrived;
|
|
3701
|
+
const batch_notifications = received.filter(
|
|
3702
|
+
(n) => n.stream === batch
|
|
3703
|
+
);
|
|
3704
|
+
(0, import_vitest9.expect)(batch_notifications).toHaveLength(1);
|
|
3705
|
+
const events2 = batch_notifications[0].events;
|
|
3706
|
+
(0, import_vitest9.expect)(events2).toHaveLength(3);
|
|
3707
|
+
(0, import_vitest9.expect)(events2.map((e) => e.name)).toEqual([
|
|
3708
|
+
"Incremented",
|
|
3709
|
+
"Incremented",
|
|
3710
|
+
"Incremented"
|
|
3711
|
+
]);
|
|
3712
|
+
(0, import_vitest9.expect)([...events2.map((e) => e.id)].sort((a, b) => a - b)).toEqual(
|
|
3713
|
+
events2.map((e) => e.id)
|
|
3714
|
+
);
|
|
3715
|
+
} finally {
|
|
3716
|
+
await writer.dispose();
|
|
3717
|
+
await Promise.resolve(disposer());
|
|
3718
|
+
}
|
|
3719
|
+
});
|
|
3505
3720
|
});
|
|
3506
3721
|
}
|
|
3507
3722
|
});
|