@rotorsoft/act 1.32.16 → 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);
@@ -3314,8 +3341,6 @@ function build_handle_batch(logger) {
3314
3341
  }
3315
3342
 
3316
3343
  // src/act.ts
3317
- var CLOSE_CATCH_UP_LIMIT = 1e3;
3318
- var CLOSE_CATCH_UP_PASSES = 20;
3319
3344
  function validate_only_lanes(options, lanes) {
3320
3345
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
3321
3346
  const declared = /* @__PURE__ */ new Set(["default", ...lanes.map((l) => l.name)]);
@@ -3468,28 +3493,10 @@ var Act = class {
3468
3493
  _handle_batch;
3469
3494
  /** Declared drain lanes. */
3470
3495
  _lanes;
3471
- /**
3472
- * Per-stream close serialization tails, so a manual close and an
3473
- * autoclose of the same stream never prune concurrently (the manual path
3474
- * has no drain lease to exclude it). Entries are dropped once settled.
3475
- */
3476
- _close_locks = /* @__PURE__ */ new Map();
3477
- /**
3478
- * Run `work` under the per-stream close lock. Serializes close
3479
- * critical sections for the same stream (windowed prunes and full
3480
- * closes) while letting different streams proceed in parallel.
3481
- */
3482
- _with_close_lock(stream, work) {
3483
- const prev = this._close_locks.get(stream) ?? Promise.resolve();
3484
- const next = prev.then(work, work);
3485
- this._close_locks.set(stream, next);
3486
- const cleanup = () => {
3487
- if (this._close_locks.get(stream) === next)
3488
- this._close_locks.delete(stream);
3489
- };
3490
- next.then(cleanup, cleanup);
3491
- return next;
3492
- }
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;
3493
3500
  /** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
3494
3501
  get lanes() {
3495
3502
  return this._lanes;
@@ -3519,6 +3526,7 @@ var Act = class {
3519
3526
  this._states = states;
3520
3527
  this._batch_handlers = batch_handlers;
3521
3528
  this._lanes = lanes;
3529
+ this._declared_lanes = ["default", ...lanes.map((l) => l.name)];
3522
3530
  validate_only_lanes(options, lanes);
3523
3531
  this._ports = options.scoped ?? default_scope();
3524
3532
  const ports = this._ports;
@@ -3556,7 +3564,7 @@ var Act = class {
3556
3564
  this._drain = options.drain !== false;
3557
3565
  this._breaker = this._build_breaker(options);
3558
3566
  this._drain_controllers = this._build_drain_controllers(options, lanes);
3559
- this._advise_orphaned_lanes(options, lanes);
3567
+ this._advise_orphaned_lanes(options);
3560
3568
  this._audit_deps = this._build_audit_deps();
3561
3569
  this._correlate = this._build_correlate(options, classification);
3562
3570
  this._settle = this._build_settle(options);
@@ -3587,7 +3595,7 @@ var Act = class {
3587
3595
  * to `claim`.
3588
3596
  */
3589
3597
  _build_drain_controllers(options, lanes) {
3590
- const all_lanes = ["default", ...lanes.map((l) => l.name)];
3598
+ const all_lanes = this._declared_lanes;
3591
3599
  const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
3592
3600
  const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
3593
3601
  const single_default_lane = lanes.length === 0;
@@ -3605,19 +3613,7 @@ var Act = class {
3605
3613
  on_blocked: (blocked) => this.emit("blocked", blocked),
3606
3614
  // Reaction-requested close: the same machinery as `app.close`.
3607
3615
  on_close: async (targets) => {
3608
- const close_actor = { id: "$close", name: "close" };
3609
- const result = await run_close_cycle(targets, {
3610
- reactive_events_size: this._reactive_events.size,
3611
- catch_up_correlation: (until) => this._catch_up_correlation(until),
3612
- event_to_state: this._event_to_state,
3613
- load: this._es.load,
3614
- tombstone: this._es.tombstone,
3615
- logger: this._logger,
3616
- correlation: close_correlation(this._correlator, close_actor),
3617
- with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
3618
- });
3619
- this._forget_closed_subscriptions(result);
3620
- this.emit("closed", result);
3616
+ await this._run_close(targets);
3621
3617
  },
3622
3618
  breaker: this._breaker,
3623
3619
  run_scoped: this._scoped,
@@ -3638,12 +3634,10 @@ var Act = class {
3638
3634
  * controller here. One process can't check that the cluster covers every
3639
3635
  * lane, so it reports its own gaps.
3640
3636
  */
3641
- _advise_orphaned_lanes(options, lanes) {
3637
+ _advise_orphaned_lanes(options) {
3642
3638
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
3643
3639
  const active = new Set(this._drain_controllers.keys());
3644
- const orphaned = ["default", ...lanes.map((l) => l.name)].filter(
3645
- (name) => !active.has(name)
3646
- );
3640
+ const orphaned = this._declared_lanes.filter((name) => !active.has(name));
3647
3641
  if (orphaned.length === 0) return;
3648
3642
  const list = orphaned.map((name) => `"${name}"`).join(", ");
3649
3643
  this._logger.info(
@@ -3659,7 +3653,7 @@ var Act = class {
3659
3653
  states: this._states,
3660
3654
  // Declared lanes, not this process's controllers: a lane another
3661
3655
  // worker drains is not `unknown-lane`.
3662
- declared_lanes: /* @__PURE__ */ new Set(["default", ...this._lanes.map((l) => l.name)]),
3656
+ declared_lanes: new Set(this._declared_lanes),
3663
3657
  routed_events: new Set(this._event_to_lanes.keys())
3664
3658
  };
3665
3659
  }
@@ -3676,10 +3670,7 @@ var Act = class {
3676
3670
  max_subscribed_streams: options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
3677
3671
  // Declared lanes (not active ones): a lane another process claims is
3678
3672
  // still a valid destination.
3679
- declared_lanes: /* @__PURE__ */ new Set([
3680
- "default",
3681
- ...this._lanes.map((l) => l.name)
3682
- ]),
3673
+ declared_lanes: new Set(this._declared_lanes),
3683
3674
  on_init: () => {
3684
3675
  if (this._drain && this._reactive_events.size > 0) this._arm_all();
3685
3676
  },
@@ -4569,6 +4560,31 @@ var Act = class {
4569
4560
  async prioritize(filter, priority) {
4570
4561
  return this._scoped(() => store().prioritize(filter, priority));
4571
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
+ }
4572
4588
  /**
4573
4589
  * Close the books — guard, archive, truncate, and optionally restart streams.
4574
4590
  *
@@ -4627,69 +4643,11 @@ var Act = class {
4627
4643
  * ]);
4628
4644
  * ```
4629
4645
  */
4630
- /**
4631
- * After a close, forget any target whose subscription row the truncate
4632
- * removed, so a later correlate can re-subscribe it. Restart
4633
- * targets keep their row, so only fully-retired streams are forgotten.
4634
- */
4635
- /**
4636
- * Advance correlation until the read cursor reaches `until`, and report
4637
- * where it landed. Used by the close cycle's safety probe, which
4638
- * cannot judge a subscription's pending work over events correlate has
4639
- * not resolved yet.
4640
- *
4641
- * Bounded on both ends: it stops as soon as a pass makes no progress (the
4642
- * log has no more to give), and after {@link CLOSE_CATCH_UP_PASSES}
4643
- * windows, so a close behind an enormous backlog degrades to skipping the
4644
- * stream — the documented retryable outcome — instead of scanning the
4645
- * whole log inside an operator call.
4646
- */
4647
- async _catch_up_correlation(until) {
4648
- for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && this._correlate.checkpoint < until; pass++) {
4649
- const before = this._correlate.checkpoint;
4650
- this._correlate.arm();
4651
- await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
4652
- if (this._correlate.checkpoint <= before) break;
4653
- }
4654
- return this._correlate.checkpoint;
4655
- }
4656
- /**
4657
- * Drop retired streams from correlate's in-process "already subscribed"
4658
- * set, so a later scan re-issues `subscribe()` for them.
4659
- *
4660
- * A tombstone seed means the stream was retired; a snapshot seed means it
4661
- * was restarted and is still consuming.
4662
- *
4663
- * `truncate` no longer removes the subscription row, so this is no
4664
- * longer repairing damage the close itself did. It still matters, because
4665
- * the row can disappear later: reclaiming retired subscriptions is an
4666
- * operator job now, and that `DELETE` can land while this process is
4667
- * running. Forgetting here keeps the in-process view from outliving a row
4668
- * an operator removed, which is a silent-no-delivery failure — a reaction whose target is named after the stream would never
4669
- * be re-registered.
4670
- */
4671
- _forget_closed_subscriptions(result) {
4672
- const retired = [...result.truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
4673
- if (retired.length) this._correlate.forget_subscribed(retired);
4674
- }
4675
4646
  async close(targets) {
4676
4647
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
4677
4648
  return this._scoped(async () => {
4678
- await this.correlate({ limit: 1e3 });
4679
- const close_actor = { id: "$close", name: "close" };
4680
- const result = await run_close_cycle(targets, {
4681
- reactive_events_size: this._reactive_events.size,
4682
- catch_up_correlation: (until) => this._catch_up_correlation(until),
4683
- event_to_state: this._event_to_state,
4684
- load: this._es.load,
4685
- tombstone: this._es.tombstone,
4686
- logger: this._logger,
4687
- correlation: close_correlation(this._correlator, close_actor),
4688
- with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
4689
- });
4690
- this._forget_closed_subscriptions(result);
4691
- this.emit("closed", result);
4692
- return result;
4649
+ await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
4650
+ return this._run_close(targets);
4693
4651
  });
4694
4652
  }
4695
4653
  /**