@rotorsoft/act 1.22.7 → 1.22.9

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
@@ -20,7 +20,7 @@ import {
20
20
  sleep,
21
21
  store,
22
22
  validate
23
- } from "./chunk-QVHLKVS7.js";
23
+ } from "./chunk-YVLB5MMB.js";
24
24
  import {
25
25
  ActorSchema,
26
26
  CausationEventSchema,
@@ -45,7 +45,7 @@ import {
45
45
  pii_split,
46
46
  pii_strip,
47
47
  sensitive
48
- } from "./chunk-2RZSKBZZ.js";
48
+ } from "./chunk-LDWS6I2C.js";
49
49
 
50
50
  // src/signals.ts
51
51
  process.once("SIGINT", async (arg) => {
@@ -1396,6 +1396,7 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
1396
1396
  }
1397
1397
 
1398
1398
  // src/internal/correlate-cycle.ts
1399
+ var DEFAULT_COLD_START_BACK_SCAN = 1e4;
1399
1400
  var CorrelateCycle = class {
1400
1401
  _checkpoint = -1;
1401
1402
  _initialized = false;
@@ -1424,8 +1425,24 @@ var CorrelateCycle = class {
1424
1425
  * `_scoped` (identity for a non-scoped Act), so it's required.
1425
1426
  */
1426
1427
  _run_scoped;
1427
- constructor(registry, static_targets, has_dynamic_resolvers, cd, maxSubscribedStreams, on_init, run_scoped, on_init_async) {
1428
- this._subscribed = new LruSet(maxSubscribedStreams);
1428
+ /**
1429
+ * Tail re-scan window applied to the cold-start checkpoint (ACT-1207).
1430
+ * See {@link DEFAULT_COLD_START_BACK_SCAN}. Constructor arg (not a
1431
+ * public option) so tests can shrink it; defaults otherwise.
1432
+ */
1433
+ _cold_start_back_scan;
1434
+ constructor({
1435
+ registry,
1436
+ static_targets,
1437
+ has_dynamic_resolvers,
1438
+ cd,
1439
+ max_subscribed_streams,
1440
+ run_scoped,
1441
+ on_init,
1442
+ on_init_async,
1443
+ cold_start_back_scan = DEFAULT_COLD_START_BACK_SCAN
1444
+ }) {
1445
+ this._subscribed = new LruSet(max_subscribed_streams);
1429
1446
  this._registry = registry;
1430
1447
  this._static_targets = static_targets;
1431
1448
  this._has_dynamic_resolvers = has_dynamic_resolvers;
@@ -1433,6 +1450,7 @@ var CorrelateCycle = class {
1433
1450
  this._on_init = on_init;
1434
1451
  this._run_scoped = run_scoped;
1435
1452
  this._on_init_async = on_init_async;
1453
+ this._cold_start_back_scan = cold_start_back_scan;
1436
1454
  }
1437
1455
  /** Last correlated event id. */
1438
1456
  get checkpoint() {
@@ -1440,7 +1458,10 @@ var CorrelateCycle = class {
1440
1458
  }
1441
1459
  /**
1442
1460
  * Initialize correlation state on first call.
1443
- * - Reads max(at) from store as cold-start checkpoint
1461
+ * - Reads max(at) from store, then floors the cold-start checkpoint at
1462
+ * `watermark - back_scan` when dynamic resolvers exist, so an event
1463
+ * committed-but-not-correlated before a crash is re-scanned on
1464
+ * restart instead of skipped (ACT-1207)
1444
1465
  * - Subscribes static resolver targets (idempotent upsert)
1445
1466
  * - Populates the subscribed-streams LRU
1446
1467
  * - Fires `on_init` once (Act uses this to flag a cold-start drain)
@@ -1449,7 +1470,7 @@ var CorrelateCycle = class {
1449
1470
  if (this._initialized) return;
1450
1471
  this._initialized = true;
1451
1472
  const { watermark } = await store().subscribe([...this._static_targets]);
1452
- this._checkpoint = watermark;
1473
+ this._checkpoint = this._has_dynamic_resolvers ? Math.max(-1, watermark - this._cold_start_back_scan) : watermark;
1453
1474
  this._on_init?.();
1454
1475
  for (const { stream } of this._static_targets) {
1455
1476
  this._subscribed.add(stream);
@@ -1900,6 +1921,7 @@ async function load(me, target, callback) {
1900
1921
  event,
1901
1922
  state: state2,
1902
1923
  version: event.version,
1924
+ id: event.id,
1903
1925
  patches,
1904
1926
  snaps,
1905
1927
  cache_hit,
@@ -1921,12 +1943,17 @@ async function load(me, target, callback) {
1921
1943
  event_id: event.id,
1922
1944
  patches,
1923
1945
  snaps
1924
- });
1946
+ }).catch((err) => log().error(err));
1925
1947
  }
1926
1948
  return {
1927
1949
  event,
1928
1950
  state: state2,
1929
1951
  version: event?.version ?? cached?.version ?? -1,
1952
+ // Head event id, captured atomically with `state`: the last replayed
1953
+ // event's id on a cache miss, or the cached checkpoint's event_id on a
1954
+ // warm hit with no new events. Consumers read this instead of a
1955
+ // separate cache lookup that could race a concurrent commit (ACT-1204).
1956
+ id: event?.id ?? cached?.event_id ?? -1,
1930
1957
  patches,
1931
1958
  snaps,
1932
1959
  cache_hit,
@@ -2020,6 +2047,7 @@ async function action(me, action2, target, payload, options) {
2020
2047
  event,
2021
2048
  state: state2,
2022
2049
  version: event.version,
2050
+ id: event.id,
2023
2051
  patches,
2024
2052
  snaps: snapshot.snaps,
2025
2053
  patch: p,
@@ -2047,6 +2075,7 @@ async function action(me, action2, target, payload, options) {
2047
2075
  return snapshots;
2048
2076
  } catch (error) {
2049
2077
  if (!(error instanceof ConcurrencyError)) throw error;
2078
+ if (expectedVersion !== void 0) throw error;
2050
2079
  if (attempt >= max_retries) throw error;
2051
2080
  if (opts?.backoff) {
2052
2081
  const delay_ms = compute_backoff_delay(attempt, opts.backoff);
@@ -2569,6 +2598,16 @@ function build_handle_batch(logger) {
2569
2598
  var SettleLoop = class {
2570
2599
  _timer = void 0;
2571
2600
  _running = false;
2601
+ /**
2602
+ * Set when a `schedule()` timer fires while a cycle is still running
2603
+ * (ACT-1205). The in-flight cycle's `finally` re-arms one more pass so
2604
+ * the wake-up isn't dropped — a commit landing during the final
2605
+ * no-progress drain pass would otherwise leave armed controllers with
2606
+ * nothing to re-drain on an instance with no lane `cycleMs` and no
2607
+ * polling. Carries the options of the dropped call so the re-armed
2608
+ * pass honors its `debounceMs`/`maxPasses`/drain overrides.
2609
+ */
2610
+ _pending = void 0;
2572
2611
  _deps;
2573
2612
  /** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
2574
2613
  _default_debounce_ms;
@@ -2593,7 +2632,10 @@ var SettleLoop = class {
2593
2632
  if (this._timer) clearTimeout(this._timer);
2594
2633
  this._timer = setTimeout(() => {
2595
2634
  this._timer = void 0;
2596
- if (this._running) return;
2635
+ if (this._running) {
2636
+ this._pending = options;
2637
+ return;
2638
+ }
2597
2639
  this._running = true;
2598
2640
  (async () => {
2599
2641
  await this._deps.init();
@@ -2613,11 +2655,17 @@ var SettleLoop = class {
2613
2655
  this._deps.breaker.failed(Date.now(), err);
2614
2656
  }).finally(() => {
2615
2657
  this._running = false;
2658
+ const pending = this._pending;
2659
+ if (pending !== void 0) {
2660
+ this._pending = void 0;
2661
+ this.schedule(pending);
2662
+ }
2616
2663
  });
2617
2664
  }, debounceMs);
2618
2665
  }
2619
2666
  /** Cancel any pending or active settle cycle. Idempotent. */
2620
2667
  stop() {
2668
+ this._pending = void 0;
2621
2669
  if (this._timer) {
2622
2670
  clearTimeout(this._timer);
2623
2671
  this._timer = void 0;
@@ -2668,20 +2716,13 @@ function make_fold_handler(me, flush, config2) {
2668
2716
  cache2.delete(oldest);
2669
2717
  }
2670
2718
  const snapshot = await load(me, { stream });
2671
- const entry = await cache().get(stream);
2672
- const frontier = entry ?? {
2673
- version: snapshot.event.version,
2674
- event_id: snapshot.event.id,
2675
- patches: snapshot.patches,
2676
- snaps: snapshot.snaps
2677
- };
2678
2719
  fold = {
2679
2720
  stream,
2680
2721
  state: snapshot.state,
2681
- version: frontier.version,
2682
- event_id: frontier.event_id,
2683
- patches: frontier.patches,
2684
- snaps: frontier.snaps,
2722
+ version: snapshot.version,
2723
+ event_id: snapshot.id,
2724
+ patches: snapshot.patches,
2725
+ snaps: snapshot.snaps,
2685
2726
  dirty: true
2686
2727
  };
2687
2728
  cache2.set(stream, fold);
@@ -3040,22 +3081,22 @@ var Act = class {
3040
3081
  * except on writer-only instances (`drain: false`).
3041
3082
  */
3042
3083
  _build_correlate(options, classification) {
3043
- return new CorrelateCycle(
3044
- this.registry,
3045
- classification.static_targets,
3046
- classification.has_dynamic_resolvers,
3047
- this._cd,
3048
- options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
3049
- () => {
3084
+ return new CorrelateCycle({
3085
+ registry: this.registry,
3086
+ static_targets: classification.static_targets,
3087
+ has_dynamic_resolvers: classification.has_dynamic_resolvers,
3088
+ cd: this._cd,
3089
+ max_subscribed_streams: options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
3090
+ on_init: () => {
3050
3091
  if (this._drain && this._reactive_events.size > 0) this._arm_all();
3051
3092
  },
3052
3093
  // Re-scope the background `start_correlations` timer so its
3053
3094
  // correlate resolves the scoped ports, not the singleton (#1191).
3054
- this._scoped,
3095
+ run_scoped: this._scoped,
3055
3096
  // Cold-start defer re-seed (#1221). Skipped on writer-only instances
3056
3097
  // (`drain: false`) — they run no local controllers to re-arm.
3057
- this._drain ? () => this._seed_persisted_defers() : void 0
3058
- );
3098
+ on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
3099
+ });
3059
3100
  }
3060
3101
  /**
3061
3102
  * Re-seed every active lane controller's process-local defer timer from
@@ -4703,6 +4744,7 @@ export {
4703
4744
  extend,
4704
4745
  is_literal_source,
4705
4746
  log,
4747
+ pii_fields,
4706
4748
  port,
4707
4749
  projection,
4708
4750
  resolveAutocloseConfig,