@rotorsoft/act 1.30.0 → 1.31.1

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
@@ -22,7 +22,7 @@ import {
22
22
  sleep,
23
23
  store,
24
24
  validate
25
- } from "./chunk-H2IDTW6B.js";
25
+ } from "./chunk-YUJ7ACSP.js";
26
26
  import {
27
27
  ActorSchema,
28
28
  CausationEventSchema,
@@ -1607,11 +1607,46 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
1607
1607
  }
1608
1608
 
1609
1609
  // src/internal/correlate-cycle.ts
1610
+ import { createHash, randomUUID } from "crypto";
1610
1611
  var DEFAULT_COLD_START_BACK_SCAN = 1e4;
1612
+ var DEFAULT_CORRELATION_LEASE_MS = 5e3;
1613
+ var registry_key = (events) => {
1614
+ const shape = Object.keys(events).filter((name) => events[name].reactions.size > 0).sort().map((name) => [name, [...events[name].reactions.keys()].sort()]);
1615
+ return createHash("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 32);
1616
+ };
1611
1617
  var PATTERN_CACHE_SIZE = 32;
1612
1618
  var CorrelateCycle = class {
1613
1619
  _checkpoint = -1;
1614
1620
  _initialized = false;
1621
+ /**
1622
+ * This worker's identity for the correlation lease (#1532). A per-instance
1623
+ * UUID, matching the drain's convention, so a renewal is recognised as the
1624
+ * same holder and a restarted process never inherits a stale claim.
1625
+ */
1626
+ _by = randomUUID();
1627
+ /** How long the correlation lease is taken for. */
1628
+ _lease_millis;
1629
+ /**
1630
+ * Which correlator this is. Computed once from the registry so identical
1631
+ * workers share a lease and unrelated applications never do.
1632
+ */
1633
+ _key;
1634
+ /**
1635
+ * When this worker's correlation lease runs out, as a local clock reading,
1636
+ * or 0 when it holds none.
1637
+ *
1638
+ * Asking the store on every pass costs a round trip that the holder — which
1639
+ * is the *only* worker in a single-node deployment, and the steady-state
1640
+ * one everywhere else — gains nothing from: it already knows the answer.
1641
+ * The act-sqlite perf gate caught that as a 1.5x regression on
1642
+ * correlate+drain, which is the shape an embedded app runs constantly.
1643
+ *
1644
+ * Believing this while the store disagrees is safe in the one direction it
1645
+ * can fail. A worker that scans without really holding the lease produces
1646
+ * the duplicate scan that existed before the lease, and the marks are
1647
+ * idempotent — so a stale belief costs work, never correctness.
1648
+ */
1649
+ _lease_until = 0;
1615
1650
  /**
1616
1651
  * Whether a scan might find anything. The drain has carried the same flag
1617
1652
  * since it was written — a commit raises it, an empty claim lowers it, and
@@ -1671,8 +1706,11 @@ var CorrelateCycle = class {
1671
1706
  run_scoped,
1672
1707
  on_init,
1673
1708
  on_init_async,
1674
- cold_start_back_scan = DEFAULT_COLD_START_BACK_SCAN
1709
+ cold_start_back_scan = DEFAULT_COLD_START_BACK_SCAN,
1710
+ lease_millis = DEFAULT_CORRELATION_LEASE_MS
1675
1711
  }) {
1712
+ this._lease_millis = lease_millis;
1713
+ this._key = registry_key(registry.events);
1676
1714
  this._subscribed = new LruMap(max_subscribed_streams);
1677
1715
  this._registry = registry;
1678
1716
  this._static_targets = static_targets;
@@ -1777,7 +1815,7 @@ var CorrelateCycle = class {
1777
1815
  * at init, but marking it is what makes it claimable without probing the
1778
1816
  * event log, so the scan runs for every app.
1779
1817
  */
1780
- async correlate(query = { after: -1, limit: 10 }) {
1818
+ async correlate(query = { after: -1, limit: 10 }, lease = false) {
1781
1819
  await this.init();
1782
1820
  if (!this._armed)
1783
1821
  return {
@@ -1786,6 +1824,22 @@ var CorrelateCycle = class {
1786
1824
  marked: 0,
1787
1825
  scanned: false
1788
1826
  };
1827
+ const now = Date.now();
1828
+ if (lease && now >= this._lease_until - this._lease_millis / 2) {
1829
+ const { correlating } = await this._cd.subscribe([], void 0, {
1830
+ key: this._key,
1831
+ by: this._by,
1832
+ millis: this._lease_millis
1833
+ });
1834
+ this._lease_until = correlating ? now + this._lease_millis : 0;
1835
+ if (correlating === false)
1836
+ return {
1837
+ subscribed: 0,
1838
+ last_id: this._checkpoint,
1839
+ marked: 0,
1840
+ scanned: false
1841
+ };
1842
+ }
1789
1843
  const after = Math.max(this._checkpoint, query.after || -1);
1790
1844
  const correlated = /* @__PURE__ */ new Map();
1791
1845
  let last_id = after;
@@ -1833,7 +1887,14 @@ var CorrelateCycle = class {
1833
1887
  });
1834
1888
  }
1835
1889
  if (streams.length) {
1836
- const { subscribed } = await this._cd.subscribe(streams, last_id);
1890
+ const renewed_at = Date.now();
1891
+ const { subscribed, correlating } = await this._cd.subscribe(
1892
+ streams,
1893
+ last_id,
1894
+ lease ? { key: this._key, by: this._by, millis: this._lease_millis } : void 0
1895
+ );
1896
+ if (lease && correlating !== false)
1897
+ this._lease_until = renewed_at + this._lease_millis;
1837
1898
  const marked = streams.filter(
1838
1899
  (entry) => entry.correlated_at !== void 0
1839
1900
  ).length;
@@ -1863,11 +1924,14 @@ var CorrelateCycle = class {
1863
1924
  this._timer = setInterval(
1864
1925
  () => this._run_scoped(() => {
1865
1926
  this.arm();
1866
- return this.correlate({
1867
- ...query,
1868
- after: this._checkpoint,
1869
- limit
1870
- });
1927
+ return this.correlate(
1928
+ {
1929
+ ...query,
1930
+ after: this._checkpoint,
1931
+ limit
1932
+ },
1933
+ true
1934
+ );
1871
1935
  }).then((result) => {
1872
1936
  if (callback && result.subscribed) callback(result.subscribed);
1873
1937
  }).catch((err) => log().error(err)),
@@ -1876,6 +1940,34 @@ var CorrelateCycle = class {
1876
1940
  return true;
1877
1941
  }
1878
1942
  /** Stop the periodic correlation worker. Idempotent. */
1943
+ /**
1944
+ * Hand the correlation lease back early.
1945
+ *
1946
+ * There is no release verb on the port, deliberately — expiry is the only
1947
+ * path, which keeps a crash and a clean stop on the same code path. A
1948
+ * holder can still shorten its own lease, because re-acquiring as the same
1949
+ * holder renews, and renewing to a millisecond is a release in all but
1950
+ * name.
1951
+ *
1952
+ * Without this a worker that stops cleanly still blocks discovery for the
1953
+ * rest of its lease — invisible in a long-lived deployment, very visible
1954
+ * anywhere Acts are created and dropped inside one process.
1955
+ *
1956
+ * Best-effort: failing here costs the lease's remaining lifetime, which is
1957
+ * what would have happened had the process died instead.
1958
+ */
1959
+ async release_correlation() {
1960
+ try {
1961
+ this._lease_until = 0;
1962
+ await this._cd.subscribe([], void 0, {
1963
+ key: this._key,
1964
+ by: this._by,
1965
+ millis: 0
1966
+ });
1967
+ } catch (error) {
1968
+ log().error(error);
1969
+ }
1970
+ }
1879
1971
  stop_polling() {
1880
1972
  if (this._timer) {
1881
1973
  clearInterval(this._timer);
@@ -1909,7 +2001,7 @@ function close_correlation(correlator, actor) {
1909
2001
  }
1910
2002
 
1911
2003
  // src/internal/drain-cycle.ts
1912
- import { randomUUID } from "crypto";
2004
+ import { randomUUID as randomUUID2 } from "crypto";
1913
2005
 
1914
2006
  // src/internal/defer-timer.ts
1915
2007
  var MAX_TIMER_DELAY_MS = 2147483647;
@@ -2031,7 +2123,7 @@ async function fetch(leased, eventLimit) {
2031
2123
  }
2032
2124
  var ack = (leases) => store().ack(leases);
2033
2125
  var block = (leases) => store().block(leases);
2034
- var subscribe = (streams, correlated_at) => store().subscribe(streams, correlated_at);
2126
+ var subscribe = (streams, correlated_at, correlator) => store().subscribe(streams, correlated_at, correlator);
2035
2127
 
2036
2128
  // src/internal/event-sourcing.ts
2037
2129
  import { patch } from "@rotorsoft/act-patch";
@@ -2061,6 +2153,10 @@ function compute_backoff_delay(retry, opts) {
2061
2153
  return Math.max(0, Math.floor(delay));
2062
2154
  }
2063
2155
 
2156
+ // src/internal/reacting.ts
2157
+ import { AsyncLocalStorage } from "async_hooks";
2158
+ var reacting = new AsyncLocalStorage();
2159
+
2064
2160
  // src/internal/event-sourcing.ts
2065
2161
  var bare_patch = (_me, state2, partial) => patch(state2, partial);
2066
2162
  var validating_patch = (me, state2, partial, event) => {
@@ -2318,7 +2414,7 @@ async function load(me, target, callback, patch_fn = bare_patch) {
2318
2414
  async function action(me, action2, target, payload, options, patch_fn = bare_patch) {
2319
2415
  const { stream, expectedVersion, actor } = target;
2320
2416
  if (!stream) throw new Error("Missing target stream");
2321
- const reactingTo = options?.reactingTo;
2417
+ const reactingTo = options?.reactingTo ?? reacting.getStore();
2322
2418
  const correlator = options?.correlator ?? default_correlator;
2323
2419
  const validated = validate(action2, payload, me.actions[action2]);
2324
2420
  const opts = me.options?.[action2];
@@ -2385,11 +2481,15 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
2385
2481
  stream,
2386
2482
  emitted,
2387
2483
  meta,
2388
- // Reactions skip optimistic concurrency: they always append against the
2484
+ // Reactions skip the INFERRED guard: they always append against the
2389
2485
  // current head. Stream leasing already serializes concurrent reactions,
2390
2486
  // and forcing version checks here would turn ordinary catch-up into
2391
- // spurious retries.
2392
- reactingTo ? void 0 : expected
2487
+ // spurious retries. An expectedVersion the caller passed explicitly is
2488
+ // still honored — dropping a guard the caller asked for is silent data
2489
+ // loss, and the reaction context can reach further than the handler
2490
+ // (detached timers, settle cycles) where the caller never intended a
2491
+ // reaction's semantics at all.
2492
+ reactingTo ? expectedVersion : expected
2393
2493
  );
2394
2494
  } catch (error) {
2395
2495
  if (error instanceof ConcurrencyError) {
@@ -2639,7 +2739,7 @@ async function run_drain_cycle(ops, registry, batch_handlers, handle, handle_bat
2639
2739
  const leased = await ops.claim(
2640
2740
  lagging,
2641
2741
  leading,
2642
- randomUUID(),
2742
+ randomUUID2(),
2643
2743
  leaseMillis,
2644
2744
  lane
2645
2745
  );
@@ -3015,12 +3115,11 @@ function build_handle(deps) {
3015
3115
  for (let i = 0; i < payloads.length; i++) {
3016
3116
  const payload = payloads[i];
3017
3117
  const { event, handler } = payload;
3018
- scoped_app.do = (action2, target, action_payload, options) => bound_do(action2, target, action_payload, {
3019
- ...options,
3020
- reactingTo: options?.reactingTo ?? event
3021
- });
3022
3118
  try {
3023
- await handler(event, stream, scoped_app);
3119
+ await reacting.run(
3120
+ event,
3121
+ () => handler(event, stream, scoped_app)
3122
+ );
3024
3123
  if (last_index_of.get(event.id) === i) {
3025
3124
  at = event.id;
3026
3125
  handled++;
@@ -3655,7 +3754,7 @@ var Act = class {
3655
3754
  // the scoped store and `_initialized` then blocks a retry (#1191).
3656
3755
  init: () => this._scoped(() => this._correlate.init()),
3657
3756
  checkpoint: () => this._correlate.checkpoint,
3658
- correlate: (q) => this._correlate_scanned(q),
3757
+ correlate: (q) => this._correlate_scanned(q, true),
3659
3758
  drain: (o) => this.drain(o),
3660
3759
  on_settled: (drain) => this.emit("settled", drain),
3661
3760
  breaker: this._breaker
@@ -3698,6 +3797,7 @@ var Act = class {
3698
3797
  resolveShutdownConfig(options);
3699
3798
  this._shutdown_promise = (async () => {
3700
3799
  this.stop_correlations();
3800
+ await this._scoped(() => this._correlate.release_correlation());
3701
3801
  this.stop_settling();
3702
3802
  this._breaker.stop();
3703
3803
  for (const c of this._drain_controllers.values()) c.stop();
@@ -4177,10 +4277,10 @@ var Act = class {
4177
4277
  * internal rather than widening the public `correlate` return, which is
4178
4278
  * charter-covered and has no use for it.
4179
4279
  */
4180
- async _correlate_scanned(query) {
4280
+ async _correlate_scanned(query, lease = false) {
4181
4281
  if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
4182
4282
  return this._scoped(async () => {
4183
- const { subscribed, last_id, marked, scanned } = await this._correlate.correlate(query);
4283
+ const { subscribed, last_id, marked, scanned } = await this._correlate.correlate(query, lease);
4184
4284
  if ((subscribed > 0 || marked > 0) && this._reactive_events.size > 0)
4185
4285
  this._arm_all();
4186
4286
  return { subscribed, last_id, scanned };
@@ -4263,6 +4363,7 @@ var Act = class {
4263
4363
  */
4264
4364
  stop_correlations() {
4265
4365
  this._correlate.stop_polling();
4366
+ void this._scoped(() => this._correlate.release_correlation());
4266
4367
  }
4267
4368
  /**
4268
4369
  * Cancels any pending or active settle cycle.