@rotorsoft/act 1.31.12 → 1.31.14
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/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +5 -2
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/internal/circuit-breaker.d.ts +7 -0
- package/dist/@types/internal/circuit-breaker.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts +20 -3
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts +3 -1
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/index.cjs +49 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +49 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -916,6 +916,15 @@ var CircuitBreaker = class {
|
|
|
916
916
|
this._cooldown_ms = config2.cooldownMs;
|
|
917
917
|
this._hooks = hooks;
|
|
918
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* True when the store has failed at least once since the last pass that
|
|
921
|
+
* went all the way through. The breaker can still read "closed" here: it
|
|
922
|
+
* only opens after several failures in a row, so this catches the first
|
|
923
|
+
* one, which `state()` cannot.
|
|
924
|
+
*/
|
|
925
|
+
get failing() {
|
|
926
|
+
return this._failures > 0;
|
|
927
|
+
}
|
|
919
928
|
/** Current state given the wall-clock `now`. */
|
|
920
929
|
state(now) {
|
|
921
930
|
if (this._opened_at === void 0) return "closed";
|
|
@@ -1480,13 +1489,33 @@ var CorrelateCycle = class {
|
|
|
1480
1489
|
/** In-flight init, memoized for single-flight and cleared on failure. */
|
|
1481
1490
|
_init_promise;
|
|
1482
1491
|
_timer = void 0;
|
|
1483
|
-
//
|
|
1492
|
+
// Dynamically discovered targets → what each was last subscribed at,
|
|
1493
|
+
// bounded by `maxSubscribedStreams`. The static half lives in
|
|
1494
|
+
// `_static_subscriptions` below, which is deliberately not evictable.
|
|
1495
|
+
//
|
|
1496
|
+
// Every scan re-subscribes the
|
|
1484
1497
|
// targets it resolved (that is how the work mark lands), so this no longer
|
|
1485
1498
|
// decides *whether* a target is sent — it decides *what* is sent with it:
|
|
1486
1499
|
// a resolution raises priority/lane only when it beats the recorded floor,
|
|
1487
1500
|
// and otherwise re-sends the row's own values so the mark changes nothing
|
|
1488
1501
|
// else. See {@link Subscription}.
|
|
1489
|
-
|
|
1502
|
+
_dynamic_subscriptions;
|
|
1503
|
+
/**
|
|
1504
|
+
* What each static target was subscribed at by `init`. A plain map, never
|
|
1505
|
+
* evicted: the collection is the build-time `_static_targets` list, so it
|
|
1506
|
+
* is already bounded by the registry and costs nothing the registry does
|
|
1507
|
+
* not already hold.
|
|
1508
|
+
*
|
|
1509
|
+
* Keeping these out of the LRU is what makes the `+Infinity` floor an
|
|
1510
|
+
* invariant rather than a race (#1582). Sharing the bounded map meant a
|
|
1511
|
+
* churn of dynamic targets could evict a static record, and the next
|
|
1512
|
+
* dynamic resolution to that target found no record, took the
|
|
1513
|
+
* first-discovery branch, and re-subscribed the target with its own
|
|
1514
|
+
* priority and lane — silently re-laning a stream whose lane the
|
|
1515
|
+
* build-time subscribe owns, and starving it wherever `onlyLanes` had
|
|
1516
|
+
* provisioned a worker for the declared lane.
|
|
1517
|
+
*/
|
|
1518
|
+
_static_subscriptions = /* @__PURE__ */ new Map();
|
|
1490
1519
|
/** Compiled pattern sources, bounded by {@link PATTERN_CACHE_SIZE}. */
|
|
1491
1520
|
_patterns = new LruMap(PATTERN_CACHE_SIZE);
|
|
1492
1521
|
_registry;
|
|
@@ -1539,7 +1568,7 @@ var CorrelateCycle = class {
|
|
|
1539
1568
|
}) {
|
|
1540
1569
|
this._lease_millis = lease_millis;
|
|
1541
1570
|
this._key = registry_key(registry.events);
|
|
1542
|
-
this.
|
|
1571
|
+
this._dynamic_subscriptions = new LruMap(max_subscribed_streams);
|
|
1543
1572
|
this._registry = registry;
|
|
1544
1573
|
this._declared_lanes = declared_lanes;
|
|
1545
1574
|
this._static_targets = static_targets;
|
|
@@ -1589,7 +1618,7 @@ var CorrelateCycle = class {
|
|
|
1589
1618
|
this._checkpoint = correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - this._cold_start_back_scan);
|
|
1590
1619
|
this._on_init?.();
|
|
1591
1620
|
for (const { stream, priority = 0, lane } of this._static_targets) {
|
|
1592
|
-
this.
|
|
1621
|
+
this._static_subscriptions.set(stream, {
|
|
1593
1622
|
floor: Number.POSITIVE_INFINITY,
|
|
1594
1623
|
priority,
|
|
1595
1624
|
lane
|
|
@@ -1608,14 +1637,12 @@ var CorrelateCycle = class {
|
|
|
1608
1637
|
* the documented per-aggregate shape `.to(e => ({target: e.stream}))`
|
|
1609
1638
|
* makes those two namespaces collide by construction (#1398).
|
|
1610
1639
|
*
|
|
1611
|
-
* Static targets are left alone:
|
|
1612
|
-
*
|
|
1640
|
+
* Static targets are left alone: their record lives in
|
|
1641
|
+
* `_static_subscriptions`, which this never touches, so they stay pinned
|
|
1642
|
+
* at +Infinity and the dynamic path never re-opens them.
|
|
1613
1643
|
*/
|
|
1614
1644
|
forget_subscribed(streams) {
|
|
1615
|
-
const
|
|
1616
|
-
for (const stream of streams) {
|
|
1617
|
-
if (!statics.has(stream)) this._subscribed.delete(stream);
|
|
1618
|
-
}
|
|
1645
|
+
for (const stream of streams) this._dynamic_subscriptions.delete(stream);
|
|
1619
1646
|
}
|
|
1620
1647
|
/**
|
|
1621
1648
|
* Would an event from `stream` be fetched for a target subscribed with
|
|
@@ -1691,7 +1718,7 @@ var CorrelateCycle = class {
|
|
|
1691
1718
|
);
|
|
1692
1719
|
lane = void 0;
|
|
1693
1720
|
}
|
|
1694
|
-
const recorded = this.
|
|
1721
|
+
const recorded = this._static_subscriptions.get(resolved.target) ?? this._dynamic_subscriptions.get(resolved.target);
|
|
1695
1722
|
const priority = resolved.priority ?? 0;
|
|
1696
1723
|
const upgraded = !recorded || priority > recorded.floor;
|
|
1697
1724
|
const carried = upgraded ? { priority, lane } : { priority: recorded.priority, lane: recorded.lane };
|
|
@@ -1750,7 +1777,7 @@ var CorrelateCycle = class {
|
|
|
1750
1777
|
this._checkpoint = last_id;
|
|
1751
1778
|
for (const { stream, priority, lane } of streams) {
|
|
1752
1779
|
if (correlated.get(stream)?.upgraded)
|
|
1753
|
-
this.
|
|
1780
|
+
this._dynamic_subscriptions.set(stream, {
|
|
1754
1781
|
floor: priority,
|
|
1755
1782
|
priority,
|
|
1756
1783
|
lane
|
|
@@ -2605,10 +2632,10 @@ function trace_cycle(logger, leased, fetched, handled, acked, blocked) {
|
|
|
2605
2632
|
}
|
|
2606
2633
|
|
|
2607
2634
|
// src/internal/drain-cycle.ts
|
|
2608
|
-
function budget_exhausted(lease, options) {
|
|
2609
|
-
if (!options?.blockOnError || lease.retry <= options.maxRetries)
|
|
2635
|
+
function budget_exhausted(lease, options, store_failing) {
|
|
2636
|
+
if (store_failing || !options?.blockOnError || lease.retry <= options.maxRetries)
|
|
2610
2637
|
return void 0;
|
|
2611
|
-
const error = `Blocking ${lease.stream} after ${lease.retry} claims with no acknowledged progress \u2014 the
|
|
2638
|
+
const error = `Blocking ${lease.stream} after ${lease.retry} claims with no acknowledged progress \u2014 the retry budget (${options.maxRetries}) was spent without the handler ever reporting an error. That means every attempt lost its lease before it could ack: raise leaseMillis for this handler, then unblock the stream.`;
|
|
2612
2639
|
log().error(error);
|
|
2613
2640
|
return { lease, handled: 0, acked_at: lease.at, error, block: true };
|
|
2614
2641
|
}
|
|
@@ -2619,7 +2646,7 @@ function warn_misrouted(seen, stream, handler, event) {
|
|
|
2619
2646
|
`Reaction "${handler}" on "${event}" resolved to target "${stream}", which a projection already serves. A target is served by one batch handler or one state projection, so this reaction can never run \u2014 and delivering to it would hand the projection an aggregate from another stream. Skipping it. The equivalent static \`.to({ target })\` is rejected at build; a dynamic resolver's target is only knowable here.`
|
|
2620
2647
|
);
|
|
2621
2648
|
}
|
|
2622
|
-
async function run_drain_cycle(ops, registry, batch_handlers, misrouted, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, on_blocked, lane) {
|
|
2649
|
+
async function run_drain_cycle(ops, registry, batch_handlers, misrouted, store_failing, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, on_blocked, lane) {
|
|
2623
2650
|
const leased = await ops.claim(
|
|
2624
2651
|
lagging,
|
|
2625
2652
|
leading,
|
|
@@ -2661,7 +2688,11 @@ async function run_drain_cycle(ops, registry, batch_handlers, misrouted, handle,
|
|
|
2661
2688
|
const entry = fetch_map.get(lease.stream);
|
|
2662
2689
|
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
2663
2690
|
const { payloads } = entry;
|
|
2664
|
-
const exhausted = budget_exhausted(
|
|
2691
|
+
const exhausted = budget_exhausted(
|
|
2692
|
+
lease,
|
|
2693
|
+
payloads[0]?.options,
|
|
2694
|
+
store_failing
|
|
2695
|
+
);
|
|
2665
2696
|
if (exhausted) return Promise.resolve(exhausted);
|
|
2666
2697
|
const batchHandler = batch_handlers.get(lease.stream);
|
|
2667
2698
|
if (batchHandler && payloads.length > 0) {
|
|
@@ -2833,6 +2864,7 @@ var DrainController = class {
|
|
|
2833
2864
|
this._deps.registry,
|
|
2834
2865
|
this._deps.batch_handlers,
|
|
2835
2866
|
this._misrouted,
|
|
2867
|
+
this._deps.breaker.failing,
|
|
2836
2868
|
this._deps.handle,
|
|
2837
2869
|
this._deps.handle_batch,
|
|
2838
2870
|
lagging,
|