@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.cjs CHANGED
@@ -2834,6 +2834,31 @@ var CircuitBreaker = class {
2834
2834
  };
2835
2835
 
2836
2836
  // src/internal/close-cycle.ts
2837
+ var CLOSE_CATCH_UP_LIMIT = 1e3;
2838
+ var CLOSE_CATCH_UP_PASSES = 20;
2839
+ async function catch_up_correlation(cursor, correlate, until) {
2840
+ for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && cursor.checkpoint < until; pass++) {
2841
+ const before = cursor.checkpoint;
2842
+ cursor.arm();
2843
+ await correlate(CLOSE_CATCH_UP_LIMIT);
2844
+ if (cursor.checkpoint <= before) break;
2845
+ }
2846
+ return cursor.checkpoint;
2847
+ }
2848
+ var CloseLock = class {
2849
+ _tails = /* @__PURE__ */ new Map();
2850
+ /** Run `work` after every earlier close of `stream` has settled. */
2851
+ run(stream, work) {
2852
+ const prev = this._tails.get(stream) ?? Promise.resolve();
2853
+ const next = prev.then(work, work);
2854
+ this._tails.set(stream, next);
2855
+ const cleanup = () => {
2856
+ if (this._tails.get(stream) === next) this._tails.delete(stream);
2857
+ };
2858
+ next.then(cleanup, cleanup);
2859
+ return next;
2860
+ }
2861
+ };
2837
2862
  var SAFETY_PROBE_PAGE_SIZE = 1e3;
2838
2863
  async function run_close_cycle(targets, deps) {
2839
2864
  const target_map = new Map(targets.map((t) => [t.stream, t]));
@@ -2855,6 +2880,8 @@ async function run_close_cycle(targets, deps) {
2855
2880
  deps,
2856
2881
  () => run_full_closes(full, target_map, deps, skipped)
2857
2882
  );
2883
+ const retired = [...truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
2884
+ if (retired.length) deps.forget_subscribed?.(retired);
2858
2885
  for (const [stream, entry] of windowed_result) truncated.set(stream, entry);
2859
2886
  return { truncated, skipped };
2860
2887
  }
@@ -3013,12 +3040,12 @@ async function scan_stream_heads(streams) {
3013
3040
  }
3014
3041
  return out;
3015
3042
  }
3016
- async function partition_by_safety(stream_info, reactive_events_size, skipped, page_size, catch_up_correlation) {
3043
+ async function partition_by_safety(stream_info, reactive_events_size, skipped, page_size, catch_up_correlation2) {
3017
3044
  if (reactive_events_size === 0) return [...stream_info.keys()];
3018
3045
  let needed = -1;
3019
3046
  for (const info of stream_info.values())
3020
3047
  needed = Math.max(needed, info.max_id);
3021
- const checkpoint = await catch_up_correlation(needed);
3048
+ const checkpoint = await catch_up_correlation2(needed);
3022
3049
  const uncorrelated = /* @__PURE__ */ new Set();
3023
3050
  for (const [stream, info] of stream_info) {
3024
3051
  if (checkpoint < info.max_id) uncorrelated.add(stream);
@@ -3319,7 +3346,7 @@ var CorrelateCycle = class {
3319
3346
  this._initialized = true;
3320
3347
  }
3321
3348
  async _run_init() {
3322
- const { watermark, correlated_at } = await store2().subscribe([
3349
+ const { watermark, correlated_at } = await this._cd.subscribe([
3323
3350
  ...this._static_targets
3324
3351
  ]);
3325
3352
  this._checkpoint = correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - this._cold_start_back_scan);
@@ -4391,7 +4418,16 @@ function warn_misrouted(seen, stream, handler, event) {
4391
4418
  `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.`
4392
4419
  );
4393
4420
  }
4394
- async function run_drain_cycle(ops, registry, batch_handlers, misrouted, store_failing, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, on_blocked, lane) {
4421
+ async function run_drain_cycle(deps, cycle) {
4422
+ const { ops, registry, batch_handlers, handle, handle_batch, lane } = deps;
4423
+ const {
4424
+ lagging,
4425
+ leading,
4426
+ eventLimit,
4427
+ leaseMillis,
4428
+ misrouted,
4429
+ store_failing
4430
+ } = cycle;
4395
4431
  const leased = await ops.claim(
4396
4432
  lagging,
4397
4433
  leading,
@@ -4463,7 +4499,7 @@ async function run_drain_cycle(ops, registry, batch_handlers, misrouted, store_f
4463
4499
  const blocked = await ops.block(
4464
4500
  handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
4465
4501
  );
4466
- if (blocked.length) on_blocked(blocked);
4502
+ if (blocked.length) deps.on_blocked(blocked);
4467
4503
  const submitted = handled.flatMap((h, i) => {
4468
4504
  const advance = h.handled > 0 ? h.acked_at : leased[i].at;
4469
4505
  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 } : [];
@@ -4594,21 +4630,14 @@ var DrainController = class {
4594
4630
  });
4595
4631
  const lagging = Math.ceil(streamLimit * this._ratio);
4596
4632
  const leading = streamLimit - lagging;
4597
- const cycle = await run_drain_cycle(
4598
- this._deps.ops,
4599
- this._deps.registry,
4600
- this._deps.batch_handlers,
4601
- this._misrouted,
4602
- this._deps.breaker.failing,
4603
- this._deps.handle,
4604
- this._deps.handle_batch,
4633
+ const cycle = await run_drain_cycle(this._deps, {
4605
4634
  lagging,
4606
4635
  leading,
4607
4636
  eventLimit,
4608
4637
  leaseMillis,
4609
- (b) => this._deps.on_blocked(b),
4610
- this._deps.lane
4611
- );
4638
+ misrouted: this._misrouted,
4639
+ store_failing: this._deps.breaker.failing
4640
+ });
4612
4641
  if (!cycle) {
4613
4642
  this._deps.breaker.passed();
4614
4643
  this._armed = false;
@@ -4947,8 +4976,6 @@ function build_handle_batch(logger) {
4947
4976
  }
4948
4977
 
4949
4978
  // src/act.ts
4950
- var CLOSE_CATCH_UP_LIMIT = 1e3;
4951
- var CLOSE_CATCH_UP_PASSES = 20;
4952
4979
  function validate_only_lanes(options, lanes) {
4953
4980
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
4954
4981
  const declared = /* @__PURE__ */ new Set(["default", ...lanes.map((l) => l.name)]);
@@ -5101,28 +5128,10 @@ var Act = class {
5101
5128
  _handle_batch;
5102
5129
  /** Declared drain lanes. */
5103
5130
  _lanes;
5104
- /**
5105
- * Per-stream close serialization tails, so a manual close and an
5106
- * autoclose of the same stream never prune concurrently (the manual path
5107
- * has no drain lease to exclude it). Entries are dropped once settled.
5108
- */
5109
- _close_locks = /* @__PURE__ */ new Map();
5110
- /**
5111
- * Run `work` under the per-stream close lock. Serializes close
5112
- * critical sections for the same stream (windowed prunes and full
5113
- * closes) while letting different streams proceed in parallel.
5114
- */
5115
- _with_close_lock(stream, work) {
5116
- const prev = this._close_locks.get(stream) ?? Promise.resolve();
5117
- const next = prev.then(work, work);
5118
- this._close_locks.set(stream, next);
5119
- const cleanup = () => {
5120
- if (this._close_locks.get(stream) === next)
5121
- this._close_locks.delete(stream);
5122
- };
5123
- next.then(cleanup, cleanup);
5124
- return next;
5125
- }
5131
+ /** Serializes close critical sections per stream. */
5132
+ _close_lock = new CloseLock();
5133
+ /** Every lane a controller can exist for: `"default"` plus each declared one. */
5134
+ _declared_lanes;
5126
5135
  /** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
5127
5136
  get lanes() {
5128
5137
  return this._lanes;
@@ -5152,6 +5161,7 @@ var Act = class {
5152
5161
  this._states = states;
5153
5162
  this._batch_handlers = batch_handlers;
5154
5163
  this._lanes = lanes;
5164
+ this._declared_lanes = ["default", ...lanes.map((l) => l.name)];
5155
5165
  validate_only_lanes(options, lanes);
5156
5166
  this._ports = options.scoped ?? default_scope();
5157
5167
  const ports = this._ports;
@@ -5189,7 +5199,7 @@ var Act = class {
5189
5199
  this._drain = options.drain !== false;
5190
5200
  this._breaker = this._build_breaker(options);
5191
5201
  this._drain_controllers = this._build_drain_controllers(options, lanes);
5192
- this._advise_orphaned_lanes(options, lanes);
5202
+ this._advise_orphaned_lanes(options);
5193
5203
  this._audit_deps = this._build_audit_deps();
5194
5204
  this._correlate = this._build_correlate(options, classification);
5195
5205
  this._settle = this._build_settle(options);
@@ -5220,7 +5230,7 @@ var Act = class {
5220
5230
  * to `claim`.
5221
5231
  */
5222
5232
  _build_drain_controllers(options, lanes) {
5223
- const all_lanes = ["default", ...lanes.map((l) => l.name)];
5233
+ const all_lanes = this._declared_lanes;
5224
5234
  const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
5225
5235
  const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
5226
5236
  const single_default_lane = lanes.length === 0;
@@ -5238,19 +5248,7 @@ var Act = class {
5238
5248
  on_blocked: (blocked) => this.emit("blocked", blocked),
5239
5249
  // Reaction-requested close: the same machinery as `app.close`.
5240
5250
  on_close: async (targets) => {
5241
- const close_actor = { id: "$close", name: "close" };
5242
- const result = await run_close_cycle(targets, {
5243
- reactive_events_size: this._reactive_events.size,
5244
- catch_up_correlation: (until) => this._catch_up_correlation(until),
5245
- event_to_state: this._event_to_state,
5246
- load: this._es.load,
5247
- tombstone: this._es.tombstone,
5248
- logger: this._logger,
5249
- correlation: close_correlation(this._correlator, close_actor),
5250
- with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
5251
- });
5252
- this._forget_closed_subscriptions(result);
5253
- this.emit("closed", result);
5251
+ await this._run_close(targets);
5254
5252
  },
5255
5253
  breaker: this._breaker,
5256
5254
  run_scoped: this._scoped,
@@ -5271,12 +5269,10 @@ var Act = class {
5271
5269
  * controller here. One process can't check that the cluster covers every
5272
5270
  * lane, so it reports its own gaps.
5273
5271
  */
5274
- _advise_orphaned_lanes(options, lanes) {
5272
+ _advise_orphaned_lanes(options) {
5275
5273
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
5276
5274
  const active = new Set(this._drain_controllers.keys());
5277
- const orphaned = ["default", ...lanes.map((l) => l.name)].filter(
5278
- (name) => !active.has(name)
5279
- );
5275
+ const orphaned = this._declared_lanes.filter((name) => !active.has(name));
5280
5276
  if (orphaned.length === 0) return;
5281
5277
  const list = orphaned.map((name) => `"${name}"`).join(", ");
5282
5278
  this._logger.info(
@@ -5292,7 +5288,7 @@ var Act = class {
5292
5288
  states: this._states,
5293
5289
  // Declared lanes, not this process's controllers: a lane another
5294
5290
  // worker drains is not `unknown-lane`.
5295
- declared_lanes: /* @__PURE__ */ new Set(["default", ...this._lanes.map((l) => l.name)]),
5291
+ declared_lanes: new Set(this._declared_lanes),
5296
5292
  routed_events: new Set(this._event_to_lanes.keys())
5297
5293
  };
5298
5294
  }
@@ -5309,10 +5305,7 @@ var Act = class {
5309
5305
  max_subscribed_streams: options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
5310
5306
  // Declared lanes (not active ones): a lane another process claims is
5311
5307
  // still a valid destination.
5312
- declared_lanes: /* @__PURE__ */ new Set([
5313
- "default",
5314
- ...this._lanes.map((l) => l.name)
5315
- ]),
5308
+ declared_lanes: new Set(this._declared_lanes),
5316
5309
  on_init: () => {
5317
5310
  if (this._drain && this._reactive_events.size > 0) this._arm_all();
5318
5311
  },
@@ -6202,6 +6195,31 @@ var Act = class {
6202
6195
  async prioritize(filter, priority) {
6203
6196
  return this._scoped(() => store2().prioritize(filter, priority));
6204
6197
  }
6198
+ /**
6199
+ * Run one close: the cycle with this Act's ports, then emit `closed`. Shared by `close()` and the drain's
6200
+ * reaction-requested closes. The close actor lets a custom correlator
6201
+ * still tag tenant context or trace ids.
6202
+ */
6203
+ async _run_close(targets) {
6204
+ const close_actor = { id: "$close", name: "close" };
6205
+ const result = await run_close_cycle(targets, {
6206
+ reactive_events_size: this._reactive_events.size,
6207
+ catch_up_correlation: (until) => catch_up_correlation(
6208
+ this._correlate,
6209
+ (limit) => this.correlate({ limit }),
6210
+ until
6211
+ ),
6212
+ event_to_state: this._event_to_state,
6213
+ load: this._es.load,
6214
+ tombstone: this._es.tombstone,
6215
+ logger: this._logger,
6216
+ correlation: close_correlation(this._correlator, close_actor),
6217
+ with_stream_lock: (stream, work) => this._close_lock.run(stream, work),
6218
+ forget_subscribed: (streams) => this._correlate.forget_subscribed(streams)
6219
+ });
6220
+ this.emit("closed", result);
6221
+ return result;
6222
+ }
6205
6223
  /**
6206
6224
  * Close the books — guard, archive, truncate, and optionally restart streams.
6207
6225
  *
@@ -6260,69 +6278,11 @@ var Act = class {
6260
6278
  * ]);
6261
6279
  * ```
6262
6280
  */
6263
- /**
6264
- * After a close, forget any target whose subscription row the truncate
6265
- * removed, so a later correlate can re-subscribe it. Restart
6266
- * targets keep their row, so only fully-retired streams are forgotten.
6267
- */
6268
- /**
6269
- * Advance correlation until the read cursor reaches `until`, and report
6270
- * where it landed. Used by the close cycle's safety probe, which
6271
- * cannot judge a subscription's pending work over events correlate has
6272
- * not resolved yet.
6273
- *
6274
- * Bounded on both ends: it stops as soon as a pass makes no progress (the
6275
- * log has no more to give), and after {@link CLOSE_CATCH_UP_PASSES}
6276
- * windows, so a close behind an enormous backlog degrades to skipping the
6277
- * stream — the documented retryable outcome — instead of scanning the
6278
- * whole log inside an operator call.
6279
- */
6280
- async _catch_up_correlation(until) {
6281
- for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && this._correlate.checkpoint < until; pass++) {
6282
- const before = this._correlate.checkpoint;
6283
- this._correlate.arm();
6284
- await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
6285
- if (this._correlate.checkpoint <= before) break;
6286
- }
6287
- return this._correlate.checkpoint;
6288
- }
6289
- /**
6290
- * Drop retired streams from correlate's in-process "already subscribed"
6291
- * set, so a later scan re-issues `subscribe()` for them.
6292
- *
6293
- * A tombstone seed means the stream was retired; a snapshot seed means it
6294
- * was restarted and is still consuming.
6295
- *
6296
- * `truncate` no longer removes the subscription row, so this is no
6297
- * longer repairing damage the close itself did. It still matters, because
6298
- * the row can disappear later: reclaiming retired subscriptions is an
6299
- * operator job now, and that `DELETE` can land while this process is
6300
- * running. Forgetting here keeps the in-process view from outliving a row
6301
- * an operator removed, which is a silent-no-delivery failure — a reaction whose target is named after the stream would never
6302
- * be re-registered.
6303
- */
6304
- _forget_closed_subscriptions(result) {
6305
- const retired = [...result.truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
6306
- if (retired.length) this._correlate.forget_subscribed(retired);
6307
- }
6308
6281
  async close(targets) {
6309
6282
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
6310
6283
  return this._scoped(async () => {
6311
- await this.correlate({ limit: 1e3 });
6312
- const close_actor = { id: "$close", name: "close" };
6313
- const result = await run_close_cycle(targets, {
6314
- reactive_events_size: this._reactive_events.size,
6315
- catch_up_correlation: (until) => this._catch_up_correlation(until),
6316
- event_to_state: this._event_to_state,
6317
- load: this._es.load,
6318
- tombstone: this._es.tombstone,
6319
- logger: this._logger,
6320
- correlation: close_correlation(this._correlator, close_actor),
6321
- with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
6322
- });
6323
- this._forget_closed_subscriptions(result);
6324
- this.emit("closed", result);
6325
- return result;
6284
+ await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
6285
+ return this._run_close(targets);
6326
6286
  });
6327
6287
  }
6328
6288
  /**