@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.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);
@@ -4949,8 +4976,6 @@ function build_handle_batch(logger) {
4949
4976
  }
4950
4977
 
4951
4978
  // src/act.ts
4952
- var CLOSE_CATCH_UP_LIMIT = 1e3;
4953
- var CLOSE_CATCH_UP_PASSES = 20;
4954
4979
  function validate_only_lanes(options, lanes) {
4955
4980
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
4956
4981
  const declared = /* @__PURE__ */ new Set(["default", ...lanes.map((l) => l.name)]);
@@ -5103,28 +5128,10 @@ var Act = class {
5103
5128
  _handle_batch;
5104
5129
  /** Declared drain lanes. */
5105
5130
  _lanes;
5106
- /**
5107
- * Per-stream close serialization tails, so a manual close and an
5108
- * autoclose of the same stream never prune concurrently (the manual path
5109
- * has no drain lease to exclude it). Entries are dropped once settled.
5110
- */
5111
- _close_locks = /* @__PURE__ */ new Map();
5112
- /**
5113
- * Run `work` under the per-stream close lock. Serializes close
5114
- * critical sections for the same stream (windowed prunes and full
5115
- * closes) while letting different streams proceed in parallel.
5116
- */
5117
- _with_close_lock(stream, work) {
5118
- const prev = this._close_locks.get(stream) ?? Promise.resolve();
5119
- const next = prev.then(work, work);
5120
- this._close_locks.set(stream, next);
5121
- const cleanup = () => {
5122
- if (this._close_locks.get(stream) === next)
5123
- this._close_locks.delete(stream);
5124
- };
5125
- next.then(cleanup, cleanup);
5126
- return next;
5127
- }
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;
5128
5135
  /** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
5129
5136
  get lanes() {
5130
5137
  return this._lanes;
@@ -5154,6 +5161,7 @@ var Act = class {
5154
5161
  this._states = states;
5155
5162
  this._batch_handlers = batch_handlers;
5156
5163
  this._lanes = lanes;
5164
+ this._declared_lanes = ["default", ...lanes.map((l) => l.name)];
5157
5165
  validate_only_lanes(options, lanes);
5158
5166
  this._ports = options.scoped ?? default_scope();
5159
5167
  const ports = this._ports;
@@ -5191,7 +5199,7 @@ var Act = class {
5191
5199
  this._drain = options.drain !== false;
5192
5200
  this._breaker = this._build_breaker(options);
5193
5201
  this._drain_controllers = this._build_drain_controllers(options, lanes);
5194
- this._advise_orphaned_lanes(options, lanes);
5202
+ this._advise_orphaned_lanes(options);
5195
5203
  this._audit_deps = this._build_audit_deps();
5196
5204
  this._correlate = this._build_correlate(options, classification);
5197
5205
  this._settle = this._build_settle(options);
@@ -5222,7 +5230,7 @@ var Act = class {
5222
5230
  * to `claim`.
5223
5231
  */
5224
5232
  _build_drain_controllers(options, lanes) {
5225
- const all_lanes = ["default", ...lanes.map((l) => l.name)];
5233
+ const all_lanes = this._declared_lanes;
5226
5234
  const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
5227
5235
  const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
5228
5236
  const single_default_lane = lanes.length === 0;
@@ -5240,19 +5248,7 @@ var Act = class {
5240
5248
  on_blocked: (blocked) => this.emit("blocked", blocked),
5241
5249
  // Reaction-requested close: the same machinery as `app.close`.
5242
5250
  on_close: async (targets) => {
5243
- const close_actor = { id: "$close", name: "close" };
5244
- const result = await run_close_cycle(targets, {
5245
- reactive_events_size: this._reactive_events.size,
5246
- catch_up_correlation: (until) => this._catch_up_correlation(until),
5247
- event_to_state: this._event_to_state,
5248
- load: this._es.load,
5249
- tombstone: this._es.tombstone,
5250
- logger: this._logger,
5251
- correlation: close_correlation(this._correlator, close_actor),
5252
- with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
5253
- });
5254
- this._forget_closed_subscriptions(result);
5255
- this.emit("closed", result);
5251
+ await this._run_close(targets);
5256
5252
  },
5257
5253
  breaker: this._breaker,
5258
5254
  run_scoped: this._scoped,
@@ -5273,12 +5269,10 @@ var Act = class {
5273
5269
  * controller here. One process can't check that the cluster covers every
5274
5270
  * lane, so it reports its own gaps.
5275
5271
  */
5276
- _advise_orphaned_lanes(options, lanes) {
5272
+ _advise_orphaned_lanes(options) {
5277
5273
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
5278
5274
  const active = new Set(this._drain_controllers.keys());
5279
- const orphaned = ["default", ...lanes.map((l) => l.name)].filter(
5280
- (name) => !active.has(name)
5281
- );
5275
+ const orphaned = this._declared_lanes.filter((name) => !active.has(name));
5282
5276
  if (orphaned.length === 0) return;
5283
5277
  const list = orphaned.map((name) => `"${name}"`).join(", ");
5284
5278
  this._logger.info(
@@ -5294,7 +5288,7 @@ var Act = class {
5294
5288
  states: this._states,
5295
5289
  // Declared lanes, not this process's controllers: a lane another
5296
5290
  // worker drains is not `unknown-lane`.
5297
- declared_lanes: /* @__PURE__ */ new Set(["default", ...this._lanes.map((l) => l.name)]),
5291
+ declared_lanes: new Set(this._declared_lanes),
5298
5292
  routed_events: new Set(this._event_to_lanes.keys())
5299
5293
  };
5300
5294
  }
@@ -5311,10 +5305,7 @@ var Act = class {
5311
5305
  max_subscribed_streams: options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
5312
5306
  // Declared lanes (not active ones): a lane another process claims is
5313
5307
  // still a valid destination.
5314
- declared_lanes: /* @__PURE__ */ new Set([
5315
- "default",
5316
- ...this._lanes.map((l) => l.name)
5317
- ]),
5308
+ declared_lanes: new Set(this._declared_lanes),
5318
5309
  on_init: () => {
5319
5310
  if (this._drain && this._reactive_events.size > 0) this._arm_all();
5320
5311
  },
@@ -6204,6 +6195,31 @@ var Act = class {
6204
6195
  async prioritize(filter, priority) {
6205
6196
  return this._scoped(() => store2().prioritize(filter, priority));
6206
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
+ }
6207
6223
  /**
6208
6224
  * Close the books — guard, archive, truncate, and optionally restart streams.
6209
6225
  *
@@ -6262,69 +6278,11 @@ var Act = class {
6262
6278
  * ]);
6263
6279
  * ```
6264
6280
  */
6265
- /**
6266
- * After a close, forget any target whose subscription row the truncate
6267
- * removed, so a later correlate can re-subscribe it. Restart
6268
- * targets keep their row, so only fully-retired streams are forgotten.
6269
- */
6270
- /**
6271
- * Advance correlation until the read cursor reaches `until`, and report
6272
- * where it landed. Used by the close cycle's safety probe, which
6273
- * cannot judge a subscription's pending work over events correlate has
6274
- * not resolved yet.
6275
- *
6276
- * Bounded on both ends: it stops as soon as a pass makes no progress (the
6277
- * log has no more to give), and after {@link CLOSE_CATCH_UP_PASSES}
6278
- * windows, so a close behind an enormous backlog degrades to skipping the
6279
- * stream — the documented retryable outcome — instead of scanning the
6280
- * whole log inside an operator call.
6281
- */
6282
- async _catch_up_correlation(until) {
6283
- for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && this._correlate.checkpoint < until; pass++) {
6284
- const before = this._correlate.checkpoint;
6285
- this._correlate.arm();
6286
- await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
6287
- if (this._correlate.checkpoint <= before) break;
6288
- }
6289
- return this._correlate.checkpoint;
6290
- }
6291
- /**
6292
- * Drop retired streams from correlate's in-process "already subscribed"
6293
- * set, so a later scan re-issues `subscribe()` for them.
6294
- *
6295
- * A tombstone seed means the stream was retired; a snapshot seed means it
6296
- * was restarted and is still consuming.
6297
- *
6298
- * `truncate` no longer removes the subscription row, so this is no
6299
- * longer repairing damage the close itself did. It still matters, because
6300
- * the row can disappear later: reclaiming retired subscriptions is an
6301
- * operator job now, and that `DELETE` can land while this process is
6302
- * running. Forgetting here keeps the in-process view from outliving a row
6303
- * an operator removed, which is a silent-no-delivery failure — a reaction whose target is named after the stream would never
6304
- * be re-registered.
6305
- */
6306
- _forget_closed_subscriptions(result) {
6307
- const retired = [...result.truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
6308
- if (retired.length) this._correlate.forget_subscribed(retired);
6309
- }
6310
6281
  async close(targets) {
6311
6282
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
6312
6283
  return this._scoped(async () => {
6313
- await this.correlate({ limit: 1e3 });
6314
- const close_actor = { id: "$close", name: "close" };
6315
- const result = await run_close_cycle(targets, {
6316
- reactive_events_size: this._reactive_events.size,
6317
- catch_up_correlation: (until) => this._catch_up_correlation(until),
6318
- event_to_state: this._event_to_state,
6319
- load: this._es.load,
6320
- tombstone: this._es.tombstone,
6321
- logger: this._logger,
6322
- correlation: close_correlation(this._correlator, close_actor),
6323
- with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
6324
- });
6325
- this._forget_closed_subscriptions(result);
6326
- this.emit("closed", result);
6327
- return result;
6284
+ await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
6285
+ return this._run_close(targets);
6328
6286
  });
6329
6287
  }
6330
6288
  /**