@rotorsoft/act 1.32.20 → 1.32.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
@@ -1839,6 +1839,8 @@ var CorrelateCycle = class {
1839
1839
  priority: carried.priority,
1840
1840
  lane: carried.lane,
1841
1841
  upgraded,
1842
+ missed: !recorded,
1843
+ handler: reaction.handler.name,
1842
1844
  correlated_at: void 0
1843
1845
  };
1844
1846
  const seen_in_scan = correlated.has(resolved.target);
@@ -1857,6 +1859,7 @@ var CorrelateCycle = class {
1857
1859
  entry.priority = carried.priority;
1858
1860
  entry.lane = carried.lane;
1859
1861
  entry.upgraded = upgraded;
1862
+ entry.handler = reaction.handler.name;
1860
1863
  }
1861
1864
  if (this._in_fetch_window(resolved.source, event.stream))
1862
1865
  entry.correlated_at = event.id;
@@ -1870,6 +1873,7 @@ var CorrelateCycle = class {
1870
1873
  // actor-less, so plaintext here would be un-gated disclosure.
1871
1874
  { ...query, after, with_pii: false }
1872
1875
  );
1876
+ if (this._declared_lanes.size > 1) await this._recall(correlated);
1873
1877
  const streams = [];
1874
1878
  for (const [stream, entry] of correlated) {
1875
1879
  if (entry.upgraded || entry.correlated_at !== void 0)
@@ -1908,6 +1912,50 @@ var CorrelateCycle = class {
1908
1912
  this._armed = found === query.limit;
1909
1913
  return { subscribed: 0, last_id, marked: 0, scanned: true };
1910
1914
  }
1915
+ /**
1916
+ * Give each target this scan had no record of the record its row holds.
1917
+ *
1918
+ * A target with no record is either new or forgotten. Its row, when it
1919
+ * has one, says what lane and priority it was subscribed at; a resolution
1920
+ * that doesn't beat that priority carries the row's values, exactly as if
1921
+ * the record had never been evicted. One read per target with no record,
1922
+ * and only when more than one lane is declared: with one lane there is no
1923
+ * lane to lose, and the store already keeps the higher priority.
1924
+ */
1925
+ async _recall(correlated) {
1926
+ const missed = [...correlated].filter(([, entry]) => entry.missed);
1927
+ await Promise.all(
1928
+ missed.map(async ([stream, entry]) => {
1929
+ let row;
1930
+ await store().query_streams(
1931
+ (position) => {
1932
+ row = position;
1933
+ },
1934
+ { stream, stream_exact: true, limit: 1 }
1935
+ );
1936
+ if (!row) return;
1937
+ this._dynamic_subscriptions.set(stream, {
1938
+ floor: row.priority,
1939
+ priority: row.priority,
1940
+ lane: row.lane
1941
+ });
1942
+ if (entry.priority > row.priority) return;
1943
+ const held_lane = row.lane ?? DEFAULT_LANE;
1944
+ const resolved_lane = entry.lane ?? DEFAULT_LANE;
1945
+ if (entry.priority === row.priority && held_lane !== resolved_lane)
1946
+ report_lane_conflict(
1947
+ this._reported,
1948
+ entry.handler,
1949
+ stream,
1950
+ held_lane,
1951
+ resolved_lane
1952
+ );
1953
+ entry.priority = row.priority;
1954
+ entry.lane = row.lane;
1955
+ entry.upgraded = false;
1956
+ })
1957
+ );
1958
+ }
1911
1959
  /**
1912
1960
  * Hand the correlation lease back early.
1913
1961
  *
@@ -2067,9 +2115,8 @@ var DeferTimer = class {
2067
2115
  _timer;
2068
2116
  _on_wake;
2069
2117
  /**
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.
2118
+ * @param on_wake - invoked once each time the earliest due-time elapses.
2119
+ * The consumer re-schedules for whatever is still parked.
2073
2120
  */
2074
2121
  constructor(on_wake) {
2075
2122
  this._on_wake = on_wake;
@@ -2099,6 +2146,16 @@ var DeferTimer = class {
2099
2146
  delete(stream) {
2100
2147
  this._due.delete(stream);
2101
2148
  }
2149
+ /**
2150
+ * Drop every entry due at or before `before`. The drain calls it when a
2151
+ * pass that started at `before` claimed nothing: whatever was due by then
2152
+ * was handled elsewhere (another worker) or has no work left, so waking
2153
+ * for it again would spin.
2154
+ */
2155
+ prune(before) {
2156
+ for (const [stream, at] of this._due)
2157
+ if (at <= before) this._due.delete(stream);
2158
+ }
2102
2159
  /**
2103
2160
  * (Re)schedule the wake timer at the earliest pending due-time. Idempotent
2104
2161
  * — collapses many parked streams into a single timer. A no-op clears any
@@ -2121,9 +2178,6 @@ var DeferTimer = class {
2121
2178
  );
2122
2179
  this._timer = setTimeout(() => {
2123
2180
  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
2181
  this._on_wake();
2128
2182
  }, delay);
2129
2183
  this._timer.unref();
@@ -2892,6 +2946,9 @@ var DrainController = class {
2892
2946
  * schedule itself is persisted in the store; this runs the drain at the
2893
2947
  * earliest pending visit. A parked stream already carries its mark, so
2894
2948
  * the drain alone finds it, and on an idle app nothing else would.
2949
+ * Every drain re-aims it when it ends, and a due entry stays until a pass
2950
+ * handles it or a pass that started after it came due claims nothing, so
2951
+ * the wake keeps draining until every due stream is reached.
2895
2952
  * `stop()` cancels the timer and a stopped drain never re-schedules it,
2896
2953
  * so a wake never runs after a stop.
2897
2954
  */
@@ -2989,15 +3046,16 @@ var DrainController = class {
2989
3046
  const streamLimit = d.streamLimit ?? options.streamLimit ?? DEFAULT_STREAM_LIMIT;
2990
3047
  const eventLimit = d.eventLimit ?? options.eventLimit ?? DEFAULT_EVENT_LIMIT;
2991
3048
  const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? DEFAULT_LEASE_MILLIS;
3049
+ if (this._deps.breaker.state(Date.now()) === "open")
3050
+ return EMPTY_DRAIN;
2992
3051
  try {
2993
- if (this._deps.breaker.state(Date.now()) === "open")
2994
- return EMPTY_DRAIN;
2995
3052
  this._locked = true;
2996
3053
  this._inflight = new Promise((done) => {
2997
3054
  this._inflight_done = done;
2998
3055
  });
2999
3056
  const lagging = Math.ceil(streamLimit * this._ratio);
3000
3057
  const leading = streamLimit - lagging;
3058
+ const started = Date.now();
3001
3059
  const cycle = await run_drain_cycle(this._deps, {
3002
3060
  lagging,
3003
3061
  leading,
@@ -3007,6 +3065,7 @@ var DrainController = class {
3007
3065
  store_failing: this._deps.breaker.failing
3008
3066
  });
3009
3067
  if (!cycle) {
3068
+ this._defer.prune(started);
3010
3069
  this._deps.breaker.passed();
3011
3070
  this._armed = false;
3012
3071
  return EMPTY_DRAIN;