@rotorsoft/act 1.32.15 → 1.32.17

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
@@ -1199,6 +1199,31 @@ var CircuitBreaker = class {
1199
1199
  };
1200
1200
 
1201
1201
  // src/internal/close-cycle.ts
1202
+ var CLOSE_CATCH_UP_LIMIT = 1e3;
1203
+ var CLOSE_CATCH_UP_PASSES = 20;
1204
+ async function catch_up_correlation(cursor, correlate, until) {
1205
+ for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && cursor.checkpoint < until; pass++) {
1206
+ const before = cursor.checkpoint;
1207
+ cursor.arm();
1208
+ await correlate(CLOSE_CATCH_UP_LIMIT);
1209
+ if (cursor.checkpoint <= before) break;
1210
+ }
1211
+ return cursor.checkpoint;
1212
+ }
1213
+ var CloseLock = class {
1214
+ _tails = /* @__PURE__ */ new Map();
1215
+ /** Run `work` after every earlier close of `stream` has settled. */
1216
+ run(stream, work) {
1217
+ const prev = this._tails.get(stream) ?? Promise.resolve();
1218
+ const next = prev.then(work, work);
1219
+ this._tails.set(stream, next);
1220
+ const cleanup = () => {
1221
+ if (this._tails.get(stream) === next) this._tails.delete(stream);
1222
+ };
1223
+ next.then(cleanup, cleanup);
1224
+ return next;
1225
+ }
1226
+ };
1202
1227
  var SAFETY_PROBE_PAGE_SIZE = 1e3;
1203
1228
  async function run_close_cycle(targets, deps) {
1204
1229
  const target_map = new Map(targets.map((t) => [t.stream, t]));
@@ -1220,6 +1245,8 @@ async function run_close_cycle(targets, deps) {
1220
1245
  deps,
1221
1246
  () => run_full_closes(full, target_map, deps, skipped)
1222
1247
  );
1248
+ const retired = [...truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
1249
+ if (retired.length) deps.forget_subscribed?.(retired);
1223
1250
  for (const [stream, entry] of windowed_result) truncated.set(stream, entry);
1224
1251
  return { truncated, skipped };
1225
1252
  }
@@ -1378,12 +1405,12 @@ async function scan_stream_heads(streams) {
1378
1405
  }
1379
1406
  return out;
1380
1407
  }
1381
- async function partition_by_safety(stream_info, reactive_events_size, skipped, page_size, catch_up_correlation) {
1408
+ async function partition_by_safety(stream_info, reactive_events_size, skipped, page_size, catch_up_correlation2) {
1382
1409
  if (reactive_events_size === 0) return [...stream_info.keys()];
1383
1410
  let needed = -1;
1384
1411
  for (const info of stream_info.values())
1385
1412
  needed = Math.max(needed, info.max_id);
1386
- const checkpoint = await catch_up_correlation(needed);
1413
+ const checkpoint = await catch_up_correlation2(needed);
1387
1414
  const uncorrelated = /* @__PURE__ */ new Set();
1388
1415
  for (const [stream, info] of stream_info) {
1389
1416
  if (checkpoint < info.max_id) uncorrelated.add(stream);
@@ -1684,7 +1711,7 @@ var CorrelateCycle = class {
1684
1711
  this._initialized = true;
1685
1712
  }
1686
1713
  async _run_init() {
1687
- const { watermark, correlated_at } = await store().subscribe([
1714
+ const { watermark, correlated_at } = await this._cd.subscribe([
1688
1715
  ...this._static_targets
1689
1716
  ]);
1690
1717
  this._checkpoint = correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - this._cold_start_back_scan);
@@ -2756,7 +2783,16 @@ function warn_misrouted(seen, stream, handler, event) {
2756
2783
  `Reaction "${handler}" on "${event}" resolved to target "${stream}", which a projection already serves. A target is served by one batch handler or one state projection, so this reaction can never run \u2014 and delivering to it would hand the projection an aggregate from another stream. Skipping it. The equivalent static \`.to({ target })\` is rejected at build; a dynamic resolver's target is only knowable here.`
2757
2784
  );
2758
2785
  }
2759
- async function run_drain_cycle(ops, registry, batch_handlers, misrouted, store_failing, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, on_blocked, lane) {
2786
+ async function run_drain_cycle(deps, cycle) {
2787
+ const { ops, registry, batch_handlers, handle, handle_batch, lane } = deps;
2788
+ const {
2789
+ lagging,
2790
+ leading,
2791
+ eventLimit,
2792
+ leaseMillis,
2793
+ misrouted,
2794
+ store_failing
2795
+ } = cycle;
2760
2796
  const leased = await ops.claim(
2761
2797
  lagging,
2762
2798
  leading,
@@ -2828,7 +2864,7 @@ async function run_drain_cycle(ops, registry, batch_handlers, misrouted, store_f
2828
2864
  const blocked = await ops.block(
2829
2865
  handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
2830
2866
  );
2831
- if (blocked.length) on_blocked(blocked);
2867
+ if (blocked.length) deps.on_blocked(blocked);
2832
2868
  const submitted = handled.flatMap((h, i) => {
2833
2869
  const advance = h.handled > 0 ? h.acked_at : leased[i].at;
2834
2870
  return h.defer !== void 0 ? { ...h.lease, at: advance, due: h.defer, retry: -1 } : h.next_attempt_at !== void 0 ? { ...h.lease, at: advance, due: h.next_attempt_at } : h.handled > 0 || !h.error ? { ...h.lease, at: h.acked_at } : [];
@@ -2959,21 +2995,14 @@ var DrainController = class {
2959
2995
  });
2960
2996
  const lagging = Math.ceil(streamLimit * this._ratio);
2961
2997
  const leading = streamLimit - lagging;
2962
- const cycle = await run_drain_cycle(
2963
- this._deps.ops,
2964
- this._deps.registry,
2965
- this._deps.batch_handlers,
2966
- this._misrouted,
2967
- this._deps.breaker.failing,
2968
- this._deps.handle,
2969
- this._deps.handle_batch,
2998
+ const cycle = await run_drain_cycle(this._deps, {
2970
2999
  lagging,
2971
3000
  leading,
2972
3001
  eventLimit,
2973
3002
  leaseMillis,
2974
- (b) => this._deps.on_blocked(b),
2975
- this._deps.lane
2976
- );
3003
+ misrouted: this._misrouted,
3004
+ store_failing: this._deps.breaker.failing
3005
+ });
2977
3006
  if (!cycle) {
2978
3007
  this._deps.breaker.passed();
2979
3008
  this._armed = false;
@@ -3312,8 +3341,6 @@ function build_handle_batch(logger) {
3312
3341
  }
3313
3342
 
3314
3343
  // src/act.ts
3315
- var CLOSE_CATCH_UP_LIMIT = 1e3;
3316
- var CLOSE_CATCH_UP_PASSES = 20;
3317
3344
  function validate_only_lanes(options, lanes) {
3318
3345
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
3319
3346
  const declared = /* @__PURE__ */ new Set(["default", ...lanes.map((l) => l.name)]);
@@ -3466,28 +3493,10 @@ var Act = class {
3466
3493
  _handle_batch;
3467
3494
  /** Declared drain lanes. */
3468
3495
  _lanes;
3469
- /**
3470
- * Per-stream close serialization tails, so a manual close and an
3471
- * autoclose of the same stream never prune concurrently (the manual path
3472
- * has no drain lease to exclude it). Entries are dropped once settled.
3473
- */
3474
- _close_locks = /* @__PURE__ */ new Map();
3475
- /**
3476
- * Run `work` under the per-stream close lock. Serializes close
3477
- * critical sections for the same stream (windowed prunes and full
3478
- * closes) while letting different streams proceed in parallel.
3479
- */
3480
- _with_close_lock(stream, work) {
3481
- const prev = this._close_locks.get(stream) ?? Promise.resolve();
3482
- const next = prev.then(work, work);
3483
- this._close_locks.set(stream, next);
3484
- const cleanup = () => {
3485
- if (this._close_locks.get(stream) === next)
3486
- this._close_locks.delete(stream);
3487
- };
3488
- next.then(cleanup, cleanup);
3489
- return next;
3490
- }
3496
+ /** Serializes close critical sections per stream. */
3497
+ _close_lock = new CloseLock();
3498
+ /** Every lane a controller can exist for: `"default"` plus each declared one. */
3499
+ _declared_lanes;
3491
3500
  /** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
3492
3501
  get lanes() {
3493
3502
  return this._lanes;
@@ -3517,6 +3526,7 @@ var Act = class {
3517
3526
  this._states = states;
3518
3527
  this._batch_handlers = batch_handlers;
3519
3528
  this._lanes = lanes;
3529
+ this._declared_lanes = ["default", ...lanes.map((l) => l.name)];
3520
3530
  validate_only_lanes(options, lanes);
3521
3531
  this._ports = options.scoped ?? default_scope();
3522
3532
  const ports = this._ports;
@@ -3554,7 +3564,7 @@ var Act = class {
3554
3564
  this._drain = options.drain !== false;
3555
3565
  this._breaker = this._build_breaker(options);
3556
3566
  this._drain_controllers = this._build_drain_controllers(options, lanes);
3557
- this._advise_orphaned_lanes(options, lanes);
3567
+ this._advise_orphaned_lanes(options);
3558
3568
  this._audit_deps = this._build_audit_deps();
3559
3569
  this._correlate = this._build_correlate(options, classification);
3560
3570
  this._settle = this._build_settle(options);
@@ -3585,7 +3595,7 @@ var Act = class {
3585
3595
  * to `claim`.
3586
3596
  */
3587
3597
  _build_drain_controllers(options, lanes) {
3588
- const all_lanes = ["default", ...lanes.map((l) => l.name)];
3598
+ const all_lanes = this._declared_lanes;
3589
3599
  const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
3590
3600
  const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
3591
3601
  const single_default_lane = lanes.length === 0;
@@ -3603,19 +3613,7 @@ var Act = class {
3603
3613
  on_blocked: (blocked) => this.emit("blocked", blocked),
3604
3614
  // Reaction-requested close: the same machinery as `app.close`.
3605
3615
  on_close: async (targets) => {
3606
- const close_actor = { id: "$close", name: "close" };
3607
- const result = await run_close_cycle(targets, {
3608
- reactive_events_size: this._reactive_events.size,
3609
- catch_up_correlation: (until) => this._catch_up_correlation(until),
3610
- event_to_state: this._event_to_state,
3611
- load: this._es.load,
3612
- tombstone: this._es.tombstone,
3613
- logger: this._logger,
3614
- correlation: close_correlation(this._correlator, close_actor),
3615
- with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
3616
- });
3617
- this._forget_closed_subscriptions(result);
3618
- this.emit("closed", result);
3616
+ await this._run_close(targets);
3619
3617
  },
3620
3618
  breaker: this._breaker,
3621
3619
  run_scoped: this._scoped,
@@ -3636,12 +3634,10 @@ var Act = class {
3636
3634
  * controller here. One process can't check that the cluster covers every
3637
3635
  * lane, so it reports its own gaps.
3638
3636
  */
3639
- _advise_orphaned_lanes(options, lanes) {
3637
+ _advise_orphaned_lanes(options) {
3640
3638
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
3641
3639
  const active = new Set(this._drain_controllers.keys());
3642
- const orphaned = ["default", ...lanes.map((l) => l.name)].filter(
3643
- (name) => !active.has(name)
3644
- );
3640
+ const orphaned = this._declared_lanes.filter((name) => !active.has(name));
3645
3641
  if (orphaned.length === 0) return;
3646
3642
  const list = orphaned.map((name) => `"${name}"`).join(", ");
3647
3643
  this._logger.info(
@@ -3657,7 +3653,7 @@ var Act = class {
3657
3653
  states: this._states,
3658
3654
  // Declared lanes, not this process's controllers: a lane another
3659
3655
  // worker drains is not `unknown-lane`.
3660
- declared_lanes: /* @__PURE__ */ new Set(["default", ...this._lanes.map((l) => l.name)]),
3656
+ declared_lanes: new Set(this._declared_lanes),
3661
3657
  routed_events: new Set(this._event_to_lanes.keys())
3662
3658
  };
3663
3659
  }
@@ -3674,10 +3670,7 @@ var Act = class {
3674
3670
  max_subscribed_streams: options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
3675
3671
  // Declared lanes (not active ones): a lane another process claims is
3676
3672
  // still a valid destination.
3677
- declared_lanes: /* @__PURE__ */ new Set([
3678
- "default",
3679
- ...this._lanes.map((l) => l.name)
3680
- ]),
3673
+ declared_lanes: new Set(this._declared_lanes),
3681
3674
  on_init: () => {
3682
3675
  if (this._drain && this._reactive_events.size > 0) this._arm_all();
3683
3676
  },
@@ -4567,6 +4560,31 @@ var Act = class {
4567
4560
  async prioritize(filter, priority) {
4568
4561
  return this._scoped(() => store().prioritize(filter, priority));
4569
4562
  }
4563
+ /**
4564
+ * Run one close: the cycle with this Act's ports, then emit `closed`. Shared by `close()` and the drain's
4565
+ * reaction-requested closes. The close actor lets a custom correlator
4566
+ * still tag tenant context or trace ids.
4567
+ */
4568
+ async _run_close(targets) {
4569
+ const close_actor = { id: "$close", name: "close" };
4570
+ const result = await run_close_cycle(targets, {
4571
+ reactive_events_size: this._reactive_events.size,
4572
+ catch_up_correlation: (until) => catch_up_correlation(
4573
+ this._correlate,
4574
+ (limit) => this.correlate({ limit }),
4575
+ until
4576
+ ),
4577
+ event_to_state: this._event_to_state,
4578
+ load: this._es.load,
4579
+ tombstone: this._es.tombstone,
4580
+ logger: this._logger,
4581
+ correlation: close_correlation(this._correlator, close_actor),
4582
+ with_stream_lock: (stream, work) => this._close_lock.run(stream, work),
4583
+ forget_subscribed: (streams) => this._correlate.forget_subscribed(streams)
4584
+ });
4585
+ this.emit("closed", result);
4586
+ return result;
4587
+ }
4570
4588
  /**
4571
4589
  * Close the books — guard, archive, truncate, and optionally restart streams.
4572
4590
  *
@@ -4625,69 +4643,11 @@ var Act = class {
4625
4643
  * ]);
4626
4644
  * ```
4627
4645
  */
4628
- /**
4629
- * After a close, forget any target whose subscription row the truncate
4630
- * removed, so a later correlate can re-subscribe it. Restart
4631
- * targets keep their row, so only fully-retired streams are forgotten.
4632
- */
4633
- /**
4634
- * Advance correlation until the read cursor reaches `until`, and report
4635
- * where it landed. Used by the close cycle's safety probe, which
4636
- * cannot judge a subscription's pending work over events correlate has
4637
- * not resolved yet.
4638
- *
4639
- * Bounded on both ends: it stops as soon as a pass makes no progress (the
4640
- * log has no more to give), and after {@link CLOSE_CATCH_UP_PASSES}
4641
- * windows, so a close behind an enormous backlog degrades to skipping the
4642
- * stream — the documented retryable outcome — instead of scanning the
4643
- * whole log inside an operator call.
4644
- */
4645
- async _catch_up_correlation(until) {
4646
- for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && this._correlate.checkpoint < until; pass++) {
4647
- const before = this._correlate.checkpoint;
4648
- this._correlate.arm();
4649
- await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
4650
- if (this._correlate.checkpoint <= before) break;
4651
- }
4652
- return this._correlate.checkpoint;
4653
- }
4654
- /**
4655
- * Drop retired streams from correlate's in-process "already subscribed"
4656
- * set, so a later scan re-issues `subscribe()` for them.
4657
- *
4658
- * A tombstone seed means the stream was retired; a snapshot seed means it
4659
- * was restarted and is still consuming.
4660
- *
4661
- * `truncate` no longer removes the subscription row, so this is no
4662
- * longer repairing damage the close itself did. It still matters, because
4663
- * the row can disappear later: reclaiming retired subscriptions is an
4664
- * operator job now, and that `DELETE` can land while this process is
4665
- * running. Forgetting here keeps the in-process view from outliving a row
4666
- * an operator removed, which is a silent-no-delivery failure — a reaction whose target is named after the stream would never
4667
- * be re-registered.
4668
- */
4669
- _forget_closed_subscriptions(result) {
4670
- const retired = [...result.truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
4671
- if (retired.length) this._correlate.forget_subscribed(retired);
4672
- }
4673
4646
  async close(targets) {
4674
4647
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
4675
4648
  return this._scoped(async () => {
4676
- await this.correlate({ limit: 1e3 });
4677
- const close_actor = { id: "$close", name: "close" };
4678
- const result = await run_close_cycle(targets, {
4679
- reactive_events_size: this._reactive_events.size,
4680
- catch_up_correlation: (until) => this._catch_up_correlation(until),
4681
- event_to_state: this._event_to_state,
4682
- load: this._es.load,
4683
- tombstone: this._es.tombstone,
4684
- logger: this._logger,
4685
- correlation: close_correlation(this._correlator, close_actor),
4686
- with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
4687
- });
4688
- this._forget_closed_subscriptions(result);
4689
- this.emit("closed", result);
4690
- return result;
4649
+ await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
4650
+ return this._run_close(targets);
4691
4651
  });
4692
4652
  }
4693
4653
  /**