@rotorsoft/act 1.31.11 → 1.31.13

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
@@ -1420,18 +1420,18 @@ var registry_key = (events) => {
1420
1420
  return createHash("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 32);
1421
1421
  };
1422
1422
  var PATTERN_CACHE_SIZE = 32;
1423
- function report_undeclared_lane(seen, target, lane, declared) {
1423
+ function report_undeclared_lane(seen, handler, target, lane, declared) {
1424
1424
  report_once(
1425
1425
  seen,
1426
- `lane|${target}|${lane}`,
1427
- `Reaction resolved target "${target}" onto undeclared lane "${lane}". Declared lanes: ${[...declared].map((l) => `"${l}"`).join(", ")}. No controller claims it, so the stream would never drain \u2014 running it on "default" instead. The equivalent static \`.to({ lane })\` is rejected at build; a dynamic resolver's lane is only knowable here.`
1426
+ `lane|${handler}|${lane}`,
1427
+ `Reaction "${handler}" resolved onto undeclared lane "${lane}" \u2014 for example target "${target}". Declared lanes: ${[...declared].map((l) => `"${l}"`).join(", ")}. No controller claims it, so the stream would never drain \u2014 running it on "default" instead. The equivalent static \`.to({ lane })\` is rejected at build; a dynamic resolver's lane is only knowable here.`
1428
1428
  );
1429
1429
  }
1430
- function report_lane_conflict(seen, target, kept, dropped) {
1430
+ function report_lane_conflict(seen, handler, target, kept, dropped) {
1431
1431
  report_once(
1432
1432
  seen,
1433
- `conflict|${target}|${kept}|${dropped}`,
1434
- `Stream "${target}" has conflicting lane assignments ("${kept}" vs "${dropped}") from two dynamic resolutions at equal priority. Keeping "${kept}", the lane it was first discovered on \u2014 re-laning a live stream would move it out from under a worker holding its lease. The reaction resolving "${dropped}" runs inside the "${kept}" lane's budget, and a process restricted to "${dropped}" via onlyLanes never runs it. The equivalent static declaration is rejected at build; align the resolvers, or split the target.`
1433
+ `conflict|${handler}|${kept}|${dropped}`,
1434
+ `Reaction "${handler}" resolved lane "${dropped}" for a stream already on "${kept}" \u2014 for example "${target}". These are conflicting lane assignments from two dynamic resolutions at equal priority. Keeping "${kept}", the lane it was first discovered on \u2014 re-laning a live stream would move it out from under a worker holding its lease. The reaction resolving "${dropped}" runs inside the "${kept}" lane's budget, and a process restricted to "${dropped}" via onlyLanes never runs it. The equivalent static declaration is rejected at build; align the resolvers, or split the target.`
1435
1435
  );
1436
1436
  }
1437
1437
  var CorrelateCycle = class {
@@ -1480,13 +1480,33 @@ var CorrelateCycle = class {
1480
1480
  /** In-flight init, memoized for single-flight and cleared on failure. */
1481
1481
  _init_promise;
1482
1482
  _timer = void 0;
1483
- // target → what it was last subscribed at. Every scan re-subscribes the
1483
+ // Dynamically discovered targets → what each was last subscribed at,
1484
+ // bounded by `maxSubscribedStreams`. The static half lives in
1485
+ // `_static_subscriptions` below, which is deliberately not evictable.
1486
+ //
1487
+ // Every scan re-subscribes the
1484
1488
  // targets it resolved (that is how the work mark lands), so this no longer
1485
1489
  // decides *whether* a target is sent — it decides *what* is sent with it:
1486
1490
  // a resolution raises priority/lane only when it beats the recorded floor,
1487
1491
  // and otherwise re-sends the row's own values so the mark changes nothing
1488
1492
  // else. See {@link Subscription}.
1489
- _subscribed;
1493
+ _dynamic_subscriptions;
1494
+ /**
1495
+ * What each static target was subscribed at by `init`. A plain map, never
1496
+ * evicted: the collection is the build-time `_static_targets` list, so it
1497
+ * is already bounded by the registry and costs nothing the registry does
1498
+ * not already hold.
1499
+ *
1500
+ * Keeping these out of the LRU is what makes the `+Infinity` floor an
1501
+ * invariant rather than a race (#1582). Sharing the bounded map meant a
1502
+ * churn of dynamic targets could evict a static record, and the next
1503
+ * dynamic resolution to that target found no record, took the
1504
+ * first-discovery branch, and re-subscribed the target with its own
1505
+ * priority and lane — silently re-laning a stream whose lane the
1506
+ * build-time subscribe owns, and starving it wherever `onlyLanes` had
1507
+ * provisioned a worker for the declared lane.
1508
+ */
1509
+ _static_subscriptions = /* @__PURE__ */ new Map();
1490
1510
  /** Compiled pattern sources, bounded by {@link PATTERN_CACHE_SIZE}. */
1491
1511
  _patterns = new LruMap(PATTERN_CACHE_SIZE);
1492
1512
  _registry;
@@ -1539,7 +1559,7 @@ var CorrelateCycle = class {
1539
1559
  }) {
1540
1560
  this._lease_millis = lease_millis;
1541
1561
  this._key = registry_key(registry.events);
1542
- this._subscribed = new LruMap(max_subscribed_streams);
1562
+ this._dynamic_subscriptions = new LruMap(max_subscribed_streams);
1543
1563
  this._registry = registry;
1544
1564
  this._declared_lanes = declared_lanes;
1545
1565
  this._static_targets = static_targets;
@@ -1589,7 +1609,7 @@ var CorrelateCycle = class {
1589
1609
  this._checkpoint = correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - this._cold_start_back_scan);
1590
1610
  this._on_init?.();
1591
1611
  for (const { stream, priority = 0, lane } of this._static_targets) {
1592
- this._subscribed.set(stream, {
1612
+ this._static_subscriptions.set(stream, {
1593
1613
  floor: Number.POSITIVE_INFINITY,
1594
1614
  priority,
1595
1615
  lane
@@ -1608,14 +1628,12 @@ var CorrelateCycle = class {
1608
1628
  * the documented per-aggregate shape `.to(e => ({target: e.stream}))`
1609
1629
  * makes those two namespaces collide by construction (#1398).
1610
1630
  *
1611
- * Static targets are left alone: they are subscribed once at init and
1612
- * recorded at +Infinity so the dynamic path never re-opens them.
1631
+ * Static targets are left alone: their record lives in
1632
+ * `_static_subscriptions`, which this never touches, so they stay pinned
1633
+ * at +Infinity and the dynamic path never re-opens them.
1613
1634
  */
1614
1635
  forget_subscribed(streams) {
1615
- const statics = new Set(this._static_targets.map((t) => t.stream));
1616
- for (const stream of streams) {
1617
- if (!statics.has(stream)) this._subscribed.delete(stream);
1618
- }
1636
+ for (const stream of streams) this._dynamic_subscriptions.delete(stream);
1619
1637
  }
1620
1638
  /**
1621
1639
  * Would an event from `stream` be fetched for a target subscribed with
@@ -1684,13 +1702,14 @@ var CorrelateCycle = class {
1684
1702
  if (lane !== void 0 && !this._declared_lanes.has(lane)) {
1685
1703
  report_undeclared_lane(
1686
1704
  this._reported,
1705
+ reaction.handler.name,
1687
1706
  resolved.target,
1688
1707
  lane,
1689
1708
  this._declared_lanes
1690
1709
  );
1691
1710
  lane = void 0;
1692
1711
  }
1693
- const recorded = this._subscribed.get(resolved.target);
1712
+ const recorded = this._static_subscriptions.get(resolved.target) ?? this._dynamic_subscriptions.get(resolved.target);
1694
1713
  const priority = resolved.priority ?? 0;
1695
1714
  const upgraded = !recorded || priority > recorded.floor;
1696
1715
  const carried = upgraded ? { priority, lane } : { priority: recorded.priority, lane: recorded.lane };
@@ -1703,7 +1722,13 @@ var CorrelateCycle = class {
1703
1722
  };
1704
1723
  const held = correlated.has(resolved.target) ? entry.lane : recorded?.lane;
1705
1724
  if (held !== void 0 && lane !== void 0 && held !== lane && priority === entry.priority)
1706
- report_lane_conflict(this._reported, resolved.target, held, lane);
1725
+ report_lane_conflict(
1726
+ this._reported,
1727
+ reaction.handler.name,
1728
+ resolved.target,
1729
+ held,
1730
+ lane
1731
+ );
1707
1732
  if (carried.priority > entry.priority) {
1708
1733
  entry.priority = carried.priority;
1709
1734
  entry.lane = carried.lane;
@@ -1743,7 +1768,7 @@ var CorrelateCycle = class {
1743
1768
  this._checkpoint = last_id;
1744
1769
  for (const { stream, priority, lane } of streams) {
1745
1770
  if (correlated.get(stream)?.upgraded)
1746
- this._subscribed.set(stream, {
1771
+ this._dynamic_subscriptions.set(stream, {
1747
1772
  floor: priority,
1748
1773
  priority,
1749
1774
  lane