@rotorsoft/act 1.32.20 → 1.32.22

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.
Files changed (33) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act.d.ts +1 -2
  3. package/dist/@types/act.d.ts.map +1 -1
  4. package/dist/@types/adapters/in-memory-store.d.ts +2 -6
  5. package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
  6. package/dist/@types/builders/act-builder.d.ts.map +1 -1
  7. package/dist/@types/internal/audit.d.ts +1 -4
  8. package/dist/@types/internal/audit.d.ts.map +1 -1
  9. package/dist/@types/internal/config.d.ts +3 -9
  10. package/dist/@types/internal/config.d.ts.map +1 -1
  11. package/dist/@types/internal/correlate-cycle.d.ts +12 -11
  12. package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
  13. package/dist/@types/internal/defer-timer.d.ts +15 -10
  14. package/dist/@types/internal/defer-timer.d.ts.map +1 -1
  15. package/dist/@types/internal/drain-cycle.d.ts +6 -5
  16. package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
  17. package/dist/@types/internal/index.d.ts +1 -1
  18. package/dist/@types/internal/index.d.ts.map +1 -1
  19. package/dist/@types/types/ports.d.ts +10 -10
  20. package/dist/@types/types/ports.d.ts.map +1 -1
  21. package/dist/@types/utils.d.ts +3 -2
  22. package/dist/@types/utils.d.ts.map +1 -1
  23. package/dist/{chunk-QOSQI3CA.js → chunk-T3CWXVGT.js} +14 -55
  24. package/dist/chunk-T3CWXVGT.js.map +1 -0
  25. package/dist/index.cjs +106 -113
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +94 -60
  28. package/dist/index.js.map +1 -1
  29. package/dist/test/index.cjs +13 -54
  30. package/dist/test/index.cjs.map +1 -1
  31. package/dist/test/index.js +1 -1
  32. package/package.json +1 -1
  33. package/dist/chunk-QOSQI3CA.js.map +0 -1
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  sleep,
27
27
  store,
28
28
  validate
29
- } from "./chunk-QOSQI3CA.js";
29
+ } from "./chunk-T3CWXVGT.js";
30
30
  import {
31
31
  ActorSchema,
32
32
  CausationEventSchema,
@@ -926,7 +926,6 @@ function resolveSettleConfig(options) {
926
926
  return options;
927
927
  }
928
928
  var MAX_SHUTDOWN_GRACE_MS = 3e4;
929
- var DEFAULT_SHUTDOWN_GRACE_MS = DEFAULT_LEASE_MILLIS;
930
929
  var ShutdownOptionsSchema = z2.object({
931
930
  // `0` is legal and is exactly today's behavior: stop scheduling and
932
931
  // return without waiting.
@@ -1561,8 +1560,8 @@ function report_once(seen, key, message) {
1561
1560
  }
1562
1561
 
1563
1562
  // src/internal/correlate-cycle.ts
1564
- var DEFAULT_COLD_START_BACK_SCAN = 1e4;
1565
- var DEFAULT_CORRELATION_LEASE_MS = 5e3;
1563
+ var COLD_START_BACK_SCAN = 1e4;
1564
+ var CORRELATION_LEASE_MS = 5e3;
1566
1565
  var registry_key = (events) => {
1567
1566
  const shape = Object.keys(events).filter((name) => events[name].reactions.size > 0).sort().map((name) => [name, [...events[name].reactions.keys()].sort()]);
1568
1567
  return createHash("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 32);
@@ -1591,8 +1590,6 @@ var CorrelateCycle = class {
1591
1590
  * same holder and a restarted process never inherits a stale claim.
1592
1591
  */
1593
1592
  _by = randomUUID();
1594
- /** How long the correlation lease is taken for. */
1595
- _lease_millis;
1596
1593
  /**
1597
1594
  * Which correlator this is. Computed once from the registry so identical
1598
1595
  * workers share a lease and unrelated applications never do.
@@ -1645,12 +1642,6 @@ var CorrelateCycle = class {
1645
1642
  * here, so an idle deferred stream re-arms across a restart.
1646
1643
  */
1647
1644
  _on_init_async;
1648
- /**
1649
- * Tail re-scan window applied to the cold-start checkpoint.
1650
- * See {@link DEFAULT_COLD_START_BACK_SCAN}. Constructor arg (not a
1651
- * public option) so tests can shrink it; defaults otherwise.
1652
- */
1653
- _cold_start_back_scan;
1654
1645
  /** Lanes a controller exists for. See {@link CorrelateCycleDeps}. */
1655
1646
  _declared_lanes;
1656
1647
  /**
@@ -1667,11 +1658,8 @@ var CorrelateCycle = class {
1667
1658
  declared_lanes,
1668
1659
  shard,
1669
1660
  on_init,
1670
- on_init_async,
1671
- cold_start_back_scan = DEFAULT_COLD_START_BACK_SCAN,
1672
- lease_millis = DEFAULT_CORRELATION_LEASE_MS
1661
+ on_init_async
1673
1662
  }) {
1674
- this._lease_millis = lease_millis;
1675
1663
  this._key = shard ? `${registry_key(registry.events)}:${[...shard].sort().join(",")}` : registry_key(registry.events);
1676
1664
  this._dynamic_subscriptions = new LruMap(max_subscribed_streams);
1677
1665
  this._registry = registry;
@@ -1680,7 +1668,6 @@ var CorrelateCycle = class {
1680
1668
  this._cd = cd;
1681
1669
  this._on_init = on_init;
1682
1670
  this._on_init_async = on_init_async;
1683
- this._cold_start_back_scan = cold_start_back_scan;
1684
1671
  }
1685
1672
  /** Last correlated event id. */
1686
1673
  get checkpoint() {
@@ -1719,7 +1706,7 @@ var CorrelateCycle = class {
1719
1706
  const { watermark, correlated_at } = await this._cd.subscribe([
1720
1707
  ...this._static_targets
1721
1708
  ]);
1722
- this._checkpoint = correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - this._cold_start_back_scan);
1709
+ this._checkpoint = correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - COLD_START_BACK_SCAN);
1723
1710
  this._on_init?.();
1724
1711
  for (const { stream, priority = 0, lane } of this._static_targets) {
1725
1712
  this._static_subscriptions.set(stream, {
@@ -1793,13 +1780,13 @@ var CorrelateCycle = class {
1793
1780
  scanned: false
1794
1781
  };
1795
1782
  const now = Date.now();
1796
- if (lease && now >= this._lease_until - this._lease_millis / 2) {
1783
+ if (lease && now >= this._lease_until - CORRELATION_LEASE_MS / 2) {
1797
1784
  const { correlating } = await this._cd.subscribe([], void 0, {
1798
1785
  key: this._key,
1799
1786
  by: this._by,
1800
- millis: this._lease_millis
1787
+ millis: CORRELATION_LEASE_MS
1801
1788
  });
1802
- this._lease_until = correlating ? now + this._lease_millis : 0;
1789
+ this._lease_until = correlating ? now + CORRELATION_LEASE_MS : 0;
1803
1790
  if (correlating === false)
1804
1791
  return {
1805
1792
  subscribed: 0,
@@ -1839,6 +1826,8 @@ var CorrelateCycle = class {
1839
1826
  priority: carried.priority,
1840
1827
  lane: carried.lane,
1841
1828
  upgraded,
1829
+ missed: !recorded,
1830
+ handler: reaction.handler.name,
1842
1831
  correlated_at: void 0
1843
1832
  };
1844
1833
  const seen_in_scan = correlated.has(resolved.target);
@@ -1857,6 +1846,7 @@ var CorrelateCycle = class {
1857
1846
  entry.priority = carried.priority;
1858
1847
  entry.lane = carried.lane;
1859
1848
  entry.upgraded = upgraded;
1849
+ entry.handler = reaction.handler.name;
1860
1850
  }
1861
1851
  if (this._in_fetch_window(resolved.source, event.stream))
1862
1852
  entry.correlated_at = event.id;
@@ -1870,6 +1860,7 @@ var CorrelateCycle = class {
1870
1860
  // actor-less, so plaintext here would be un-gated disclosure.
1871
1861
  { ...query, after, with_pii: false }
1872
1862
  );
1863
+ if (this._declared_lanes.size > 1) await this._recall(correlated);
1873
1864
  const streams = [];
1874
1865
  for (const [stream, entry] of correlated) {
1875
1866
  if (entry.upgraded || entry.correlated_at !== void 0)
@@ -1886,10 +1877,10 @@ var CorrelateCycle = class {
1886
1877
  const { subscribed, correlating } = await this._cd.subscribe(
1887
1878
  streams,
1888
1879
  last_id,
1889
- lease ? { key: this._key, by: this._by, millis: this._lease_millis } : void 0
1880
+ lease ? { key: this._key, by: this._by, millis: CORRELATION_LEASE_MS } : void 0
1890
1881
  );
1891
1882
  if (lease && correlating !== false)
1892
- this._lease_until = renewed_at + this._lease_millis;
1883
+ this._lease_until = renewed_at + CORRELATION_LEASE_MS;
1893
1884
  const marked = streams.filter(
1894
1885
  (entry) => entry.correlated_at !== void 0
1895
1886
  ).length;
@@ -1908,6 +1899,50 @@ var CorrelateCycle = class {
1908
1899
  this._armed = found === query.limit;
1909
1900
  return { subscribed: 0, last_id, marked: 0, scanned: true };
1910
1901
  }
1902
+ /**
1903
+ * Give each target this scan had no record of the record its row holds.
1904
+ *
1905
+ * A target with no record is either new or forgotten. Its row, when it
1906
+ * has one, says what lane and priority it was subscribed at; a resolution
1907
+ * that doesn't beat that priority carries the row's values, exactly as if
1908
+ * the record had never been evicted. One read per target with no record,
1909
+ * and only when more than one lane is declared: with one lane there is no
1910
+ * lane to lose, and the store already keeps the higher priority.
1911
+ */
1912
+ async _recall(correlated) {
1913
+ const missed = [...correlated].filter(([, entry]) => entry.missed);
1914
+ await Promise.all(
1915
+ missed.map(async ([stream, entry]) => {
1916
+ let row;
1917
+ await store().query_streams(
1918
+ (position) => {
1919
+ row = position;
1920
+ },
1921
+ { stream, stream_exact: true, limit: 1 }
1922
+ );
1923
+ if (!row) return;
1924
+ this._dynamic_subscriptions.set(stream, {
1925
+ floor: row.priority,
1926
+ priority: row.priority,
1927
+ lane: row.lane
1928
+ });
1929
+ if (entry.priority > row.priority) return;
1930
+ const held_lane = row.lane ?? DEFAULT_LANE;
1931
+ const resolved_lane = entry.lane ?? DEFAULT_LANE;
1932
+ if (entry.priority === row.priority && held_lane !== resolved_lane)
1933
+ report_lane_conflict(
1934
+ this._reported,
1935
+ entry.handler,
1936
+ stream,
1937
+ held_lane,
1938
+ resolved_lane
1939
+ );
1940
+ entry.priority = row.priority;
1941
+ entry.lane = row.lane;
1942
+ entry.upgraded = false;
1943
+ })
1944
+ );
1945
+ }
1911
1946
  /**
1912
1947
  * Hand the correlation lease back early.
1913
1948
  *
@@ -1925,7 +1960,7 @@ var CorrelateCycle = class {
1925
1960
  });
1926
1961
  } catch (error) {
1927
1962
  log().warn(
1928
- `Could not hand back the correlation lease during shutdown; it expires on its own within ${this._lease_millis}ms and correlation resumes normally after that. On a single-writer store this usually means another connection holds the database. Cause: ${String(error)}`
1963
+ `Could not hand back the correlation lease during shutdown; it expires on its own within ${CORRELATION_LEASE_MS}ms and correlation resumes normally after that. On a single-writer store this usually means another connection holds the database. Cause: ${String(error)}`
1929
1964
  );
1930
1965
  }
1931
1966
  }
@@ -2067,9 +2102,8 @@ var DeferTimer = class {
2067
2102
  _timer;
2068
2103
  _on_wake;
2069
2104
  /**
2070
- * @param on_wake - invoked once each time the earliest due-time elapses,
2071
- * after the come-due entries have been removed. The consumer re-schedules
2072
- * for whatever is still parked.
2105
+ * @param on_wake - invoked once each time the earliest due-time elapses.
2106
+ * The consumer re-schedules for whatever is still parked.
2073
2107
  */
2074
2108
  constructor(on_wake) {
2075
2109
  this._on_wake = on_wake;
@@ -2078,14 +2112,6 @@ var DeferTimer = class {
2078
2112
  get size() {
2079
2113
  return this._due.size;
2080
2114
  }
2081
- /**
2082
- * True while `stream` is parked with a due-time still in the future.
2083
- * Consumers skip work for deferred streams until their window elapses.
2084
- */
2085
- is_deferred = (stream) => {
2086
- const next = this._due.get(stream);
2087
- return next !== void 0 && next > Date.now();
2088
- };
2089
2115
  /**
2090
2116
  * Park `stream` for a re-visit at `at` (ms since epoch). A plain
2091
2117
  * overwrite: the caller computes the authoritative next-visit for the
@@ -2099,6 +2125,16 @@ var DeferTimer = class {
2099
2125
  delete(stream) {
2100
2126
  this._due.delete(stream);
2101
2127
  }
2128
+ /**
2129
+ * Drop every entry due at or before `before`. The drain calls it when a
2130
+ * pass that started at `before` claimed nothing: whatever was due by then
2131
+ * was handled elsewhere (another worker) or has no work left, so waking
2132
+ * for it again would spin.
2133
+ */
2134
+ prune(before) {
2135
+ for (const [stream, at] of this._due)
2136
+ if (at <= before) this._due.delete(stream);
2137
+ }
2102
2138
  /**
2103
2139
  * (Re)schedule the wake timer at the earliest pending due-time. Idempotent
2104
2140
  * — collapses many parked streams into a single timer. A no-op clears any
@@ -2121,9 +2157,6 @@ var DeferTimer = class {
2121
2157
  );
2122
2158
  this._timer = setTimeout(() => {
2123
2159
  this._timer = void 0;
2124
- const now = Date.now();
2125
- for (const [stream, at] of this._due)
2126
- if (at <= now) this._due.delete(stream);
2127
2160
  this._on_wake();
2128
2161
  }, delay);
2129
2162
  this._timer.unref();
@@ -2761,7 +2794,7 @@ function trace_cycle(logger, leased, fetched, handled, acked, blocked) {
2761
2794
  function budget_exhausted(lease, options, store_failing) {
2762
2795
  if (store_failing || !options?.blockOnError || lease.retry <= options.maxRetries)
2763
2796
  return void 0;
2764
- 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.`;
2797
+ 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: move the reaction to a lane whose leaseMillis covers the handler, then unblock the stream.`;
2765
2798
  log().error(error);
2766
2799
  return { lease, handled: 0, acked_at: lease.at, error, block: true };
2767
2800
  }
@@ -2841,7 +2874,9 @@ async function run_drain_cycle(deps, cycle) {
2841
2874
  handled: 0,
2842
2875
  acked_at: lease.at,
2843
2876
  error,
2844
- ...block2 ? { block: true } : { next_attempt_at: Date.now() + DEFAULT_BACKOFF.baseMs }
2877
+ ...block2 ? { block: true } : {
2878
+ next_attempt_at: Date.now() + compute_backoff_delay(lease.retry, DEFAULT_BACKOFF)
2879
+ }
2845
2880
  };
2846
2881
  return Promise.resolve(failed);
2847
2882
  }
@@ -2892,6 +2927,9 @@ var DrainController = class {
2892
2927
  * schedule itself is persisted in the store; this runs the drain at the
2893
2928
  * earliest pending visit. A parked stream already carries its mark, so
2894
2929
  * the drain alone finds it, and on an idle app nothing else would.
2930
+ * Every drain re-aims it when it ends, and a due entry stays until a pass
2931
+ * handles it or a pass that started after it came due claims nothing, so
2932
+ * the wake keeps draining until every due stream is reached.
2895
2933
  * `stop()` cancels the timer and a stopped drain never re-schedules it,
2896
2934
  * so a wake never runs after a stop.
2897
2935
  */
@@ -2987,17 +3025,18 @@ var DrainController = class {
2987
3025
  if (this._locked) return EMPTY_DRAIN;
2988
3026
  const d = this._deps.defaults ?? {};
2989
3027
  const streamLimit = d.streamLimit ?? options.streamLimit ?? DEFAULT_STREAM_LIMIT;
2990
- const eventLimit = d.eventLimit ?? options.eventLimit ?? DEFAULT_EVENT_LIMIT;
3028
+ const eventLimit = options.eventLimit ?? DEFAULT_EVENT_LIMIT;
2991
3029
  const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? DEFAULT_LEASE_MILLIS;
3030
+ if (this._deps.breaker.state(Date.now()) === "open")
3031
+ return EMPTY_DRAIN;
2992
3032
  try {
2993
- if (this._deps.breaker.state(Date.now()) === "open")
2994
- return EMPTY_DRAIN;
2995
3033
  this._locked = true;
2996
3034
  this._inflight = new Promise((done) => {
2997
3035
  this._inflight_done = done;
2998
3036
  });
2999
3037
  const lagging = Math.ceil(streamLimit * this._ratio);
3000
3038
  const leading = streamLimit - lagging;
3039
+ const started = Date.now();
3001
3040
  const cycle = await run_drain_cycle(this._deps, {
3002
3041
  lagging,
3003
3042
  leading,
@@ -3007,6 +3046,7 @@ var DrainController = class {
3007
3046
  store_failing: this._deps.breaker.failing
3008
3047
  });
3009
3048
  if (!cycle) {
3049
+ this._defer.prune(started);
3010
3050
  this._deps.breaker.passed();
3011
3051
  this._armed = false;
3012
3052
  return EMPTY_DRAIN;
@@ -3649,9 +3689,7 @@ var Act = class {
3649
3689
  _build_audit_deps() {
3650
3690
  return {
3651
3691
  store,
3652
- logger: this._logger,
3653
3692
  event_to_state: this._event_to_state,
3654
- states: this._states,
3655
3693
  // Declared lanes, not this process's controllers: a lane another
3656
3694
  // worker drains is not `unknown-lane`.
3657
3695
  declared_lanes: new Set(this._declared_lanes),
@@ -3758,9 +3796,9 @@ var Act = class {
3758
3796
  * fallback, capped at the maximum. Idle lanes don't count.
3759
3797
  */
3760
3798
  _derive_grace_ms(running, settling) {
3761
- let max = settling ? DEFAULT_SHUTDOWN_GRACE_MS : 0;
3799
+ let max = settling ? DEFAULT_LEASE_MILLIS : 0;
3762
3800
  for (const c of running)
3763
- max = Math.max(max, c.lease_millis || DEFAULT_SHUTDOWN_GRACE_MS);
3801
+ max = Math.max(max, c.lease_millis || DEFAULT_LEASE_MILLIS);
3764
3802
  return Math.min(max, MAX_SHUTDOWN_GRACE_MS);
3765
3803
  }
3766
3804
  async _await_inflight(grace_ms) {
@@ -4070,8 +4108,7 @@ var Act = class {
4070
4108
  * Call `correlate()` before `drain()`. It is not only how dynamic targets
4071
4109
  * are discovered: a stream is claimable while `at < correlated_at`, and
4072
4110
  * `correlate` is the only component that raises that mark, so a
4073
- * commit no correlate has seen is not drainable — including for static
4074
- * targets, which were served by a probe of the event log before. For a
4111
+ * commit no correlate has seen is not drainable, static targets included. For a
4075
4112
  * higher-level API that handles debouncing, correlation, and signaling
4076
4113
  * automatically, use {@link settle}.
4077
4114
  *
@@ -5077,6 +5114,12 @@ function act() {
5077
5114
  };
5078
5115
  const fold_projections = [];
5079
5116
  const batch_handlers = /* @__PURE__ */ new Map();
5117
+ const add_projection = (proj) => {
5118
+ record_projection_reactions(proj);
5119
+ merge_projection(proj, registry.events);
5120
+ register_batch_handler(proj, batch_handlers);
5121
+ if (proj.fold) fold_projections.push(proj);
5122
+ };
5080
5123
  const fold_specs = [];
5081
5124
  const lanes = [];
5082
5125
  let _built = false;
@@ -5176,11 +5219,7 @@ function act() {
5176
5219
  },
5177
5220
  withProjection: (proj) => {
5178
5221
  if (_built) closed("withProjection");
5179
- record_projection_reactions(proj);
5180
- merge_projection(proj, registry.events);
5181
- register_batch_handler(proj, batch_handlers);
5182
- if (proj.fold)
5183
- fold_projections.push(proj);
5222
+ add_projection(proj);
5184
5223
  return builder;
5185
5224
  },
5186
5225
  withActor: () => builder,
@@ -5197,12 +5236,7 @@ function act() {
5197
5236
  resolveActConfig(options);
5198
5237
  const patch_fn = options?.validateFoldedState === true ? validating_patch : bare_patch;
5199
5238
  if (!_built) {
5200
- for (const proj of pending_projections) {
5201
- record_projection_reactions(proj);
5202
- merge_projection(proj, registry.events);
5203
- register_batch_handler(proj, batch_handlers);
5204
- if (proj.fold) fold_projections.push(proj);
5205
- }
5239
+ for (const proj of pending_projections) add_projection(proj);
5206
5240
  for (const proj of fold_projections) {
5207
5241
  const fold = proj.fold;
5208
5242
  const merged = states.get(fold.name);