@rotorsoft/act 1.33.7 → 1.33.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
@@ -1162,8 +1162,14 @@ var CorrelateCycle = class {
1162
1162
  _key;
1163
1163
  /** Local expiry of the held lease (0 = none); stale only costs a duplicate scan. */
1164
1164
  _lease_until = 0;
1165
- /** Raised by a commit or notification, lowered by a short page. Starts armed. */
1166
- _armed = true;
1165
+ /**
1166
+ * Armed while `_raised` (bumped by a commit or notification) is ahead of
1167
+ * `_lowered` (the raise count the last short page saw). Starts armed.
1168
+ */
1169
+ _raised = 1;
1170
+ _lowered = 0;
1171
+ /** Counts releases, so a lease reply that lands after one is dropped. */
1172
+ _releases = 0;
1167
1173
  _init_promise;
1168
1174
  // Bounded by `maxSubscribedStreams`; static records live apart so eviction
1169
1175
  // never drops their +Infinity floor.
@@ -1174,8 +1180,6 @@ var CorrelateCycle = class {
1174
1180
  _static_targets;
1175
1181
  _cd;
1176
1182
  _on_init;
1177
- /** Runs once after `on_init`; re-seeds defer timers across a restart. */
1178
- _on_init_async;
1179
1183
  _declared_lanes;
1180
1184
  /** Report-once keys; per instance because `internal/` holds no module state. */
1181
1185
  _reported = /* @__PURE__ */ new Set();
@@ -1186,8 +1190,7 @@ var CorrelateCycle = class {
1186
1190
  max_subscribed_streams,
1187
1191
  declared_lanes,
1188
1192
  shard,
1189
- on_init,
1190
- on_init_async
1193
+ on_init
1191
1194
  }) {
1192
1195
  this._key = shard ? `${registry_key(registry.events)}:${[...shard].sort().join(",")}` : registry_key(registry.events);
1193
1196
  this._dynamic_subscriptions = new LruMap(max_subscribed_streams);
@@ -1196,14 +1199,13 @@ var CorrelateCycle = class {
1196
1199
  this._static_targets = static_targets;
1197
1200
  this._cd = cd;
1198
1201
  this._on_init = on_init;
1199
- this._on_init_async = on_init_async;
1200
1202
  }
1201
1203
  get checkpoint() {
1202
1204
  return this._checkpoint;
1203
1205
  }
1204
1206
  /** A commit (local or remote) may have produced uncorrelated events. */
1205
1207
  arm() {
1206
- this._armed = true;
1208
+ this._raised++;
1207
1209
  }
1208
1210
  async init() {
1209
1211
  if (this._initialized) return;
@@ -1221,7 +1223,6 @@ var CorrelateCycle = class {
1221
1223
  ...this._static_targets
1222
1224
  ]);
1223
1225
  this._checkpoint = correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - COLD_START_BACK_SCAN);
1224
- this._on_init?.();
1225
1226
  for (const { stream, priority = 0, lane } of this._static_targets) {
1226
1227
  this._static_subscriptions.set(stream, {
1227
1228
  floor: Number.POSITIVE_INFINITY,
@@ -1229,7 +1230,7 @@ var CorrelateCycle = class {
1229
1230
  lane
1230
1231
  });
1231
1232
  }
1232
- await this._on_init_async?.();
1233
+ await this._on_init?.();
1233
1234
  }
1234
1235
  /** After `restore` replaced the log: forget everything and cold-start again. */
1235
1236
  restart() {
@@ -1238,7 +1239,7 @@ var CorrelateCycle = class {
1238
1239
  this._checkpoint = -1;
1239
1240
  this._dynamic_subscriptions.clear();
1240
1241
  this._static_subscriptions.clear();
1241
- this._armed = true;
1242
+ this._raised++;
1242
1243
  }
1243
1244
  /** Forget dynamic targets whose rows a full close deleted, so they re-subscribe. */
1244
1245
  forget_subscribed(streams) {
@@ -1258,22 +1259,26 @@ var CorrelateCycle = class {
1258
1259
  /** Scan past the checkpoint, subscribe new targets and raise their work marks. */
1259
1260
  async correlate(query = { after: -1, limit: 10 }, lease = false) {
1260
1261
  await this.init();
1261
- if (!this._armed)
1262
+ if (this._raised === this._lowered)
1262
1263
  return {
1263
1264
  subscribed: 0,
1264
1265
  last_id: this._checkpoint,
1265
1266
  marked: 0,
1266
1267
  scanned: false
1267
1268
  };
1269
+ const raised = this._raised;
1268
1270
  const now = Date.now();
1269
1271
  if (lease && now >= this._lease_until - CORRELATION_LEASE_MS / 2) {
1272
+ const releases = this._releases;
1270
1273
  const { correlating } = await this._cd.subscribe([], void 0, {
1271
1274
  key: this._key,
1272
1275
  by: this._by,
1273
1276
  millis: CORRELATION_LEASE_MS
1274
1277
  });
1275
- this._lease_until = correlating ? now + CORRELATION_LEASE_MS : 0;
1276
- if (correlating === false)
1278
+ const released = this._releases !== releases;
1279
+ if (!released)
1280
+ this._lease_until = correlating ? now + CORRELATION_LEASE_MS : 0;
1281
+ if (correlating === false || released)
1277
1282
  return {
1278
1283
  subscribed: 0,
1279
1284
  last_id: this._checkpoint,
@@ -1394,7 +1399,7 @@ var CorrelateCycle = class {
1394
1399
  return { subscribed, last_id, marked, scanned: true };
1395
1400
  }
1396
1401
  this._checkpoint = last_id;
1397
- this._armed = stopped || found === query.limit;
1402
+ if (!stopped && found !== query.limit) this._lowered = raised;
1398
1403
  return { subscribed: 0, last_id, marked: 0, scanned: true };
1399
1404
  }
1400
1405
  /**
@@ -1440,6 +1445,7 @@ var CorrelateCycle = class {
1440
1445
  async release_correlation() {
1441
1446
  try {
1442
1447
  this._lease_until = 0;
1448
+ this._releases++;
1443
1449
  await this._cd.subscribe([], void 0, {
1444
1450
  key: this._key,
1445
1451
  by: this._by,
@@ -2353,8 +2359,13 @@ var EMPTY_DRAIN = {
2353
2359
  };
2354
2360
  var RETRY = "";
2355
2361
  var DrainController = class {
2356
- _armed = false;
2357
- _locked = false;
2362
+ /**
2363
+ * Armed while `_raised` is ahead of `_lowered` (the raise count the last
2364
+ * empty claim saw), so a claim never lowers over a raise that landed
2365
+ * during it.
2366
+ */
2367
+ _raised = 0;
2368
+ _lowered = 0;
2358
2369
  _ratio = 0.5;
2359
2370
  /**
2360
2371
  * Local wake at the earliest parked (backoff or defer) visit, or at the
@@ -2364,7 +2375,7 @@ var DrainController = class {
2364
2375
  * stays until handled or until a later pass claims nothing.
2365
2376
  */
2366
2377
  _defer = new DeferTimer(() => {
2367
- this._armed = true;
2378
+ this.arm();
2368
2379
  void this._deps.run_scoped(async () => {
2369
2380
  while ((await this.drain()).leased.length > 0) ;
2370
2381
  });
@@ -2380,7 +2391,7 @@ var DrainController = class {
2380
2391
  this._deps = deps;
2381
2392
  }
2382
2393
  arm() {
2383
- this._armed = true;
2394
+ this._raised++;
2384
2395
  }
2385
2396
  /** Cold start: re-seed a persisted `deferred_at` into the local wake. */
2386
2397
  seed_defer(stream, at) {
@@ -2388,7 +2399,7 @@ var DrainController = class {
2388
2399
  this._defer.schedule();
2389
2400
  }
2390
2401
  get armed() {
2391
- return this._armed;
2402
+ return this._raised !== this._lowered;
2392
2403
  }
2393
2404
  get inflight() {
2394
2405
  return this._inflight;
@@ -2405,7 +2416,7 @@ var DrainController = class {
2405
2416
  if (this._worker || this._stopped) return;
2406
2417
  const run = this._deps.run_scoped;
2407
2418
  const tick = async () => {
2408
- if (this._armed) await run(() => this.drain());
2419
+ if (this.armed) await run(() => this.drain());
2409
2420
  if (this._stopped) return;
2410
2421
  this._worker = setTimeout(tick, cycleMs);
2411
2422
  this._worker.unref();
@@ -2422,8 +2433,8 @@ var DrainController = class {
2422
2433
  this._defer.stop();
2423
2434
  }
2424
2435
  async drain(options = {}) {
2425
- if (!this._armed || this._stopped) return EMPTY_DRAIN;
2426
- if (this._locked) return EMPTY_DRAIN;
2436
+ if (!this.armed || this._stopped || this._inflight)
2437
+ return EMPTY_DRAIN;
2427
2438
  const d = this._deps.defaults ?? {};
2428
2439
  const streamLimit = d.streamLimit ?? options.streamLimit ?? DEFAULT_STREAM_LIMIT;
2429
2440
  const eventLimit = options.eventLimit ?? DEFAULT_EVENT_LIMIT;
@@ -2431,13 +2442,13 @@ var DrainController = class {
2431
2442
  if (this._deps.breaker.state(Date.now()) === "open")
2432
2443
  return EMPTY_DRAIN;
2433
2444
  try {
2434
- this._locked = true;
2435
2445
  this._inflight = new Promise((done) => {
2436
2446
  this._inflight_done = done;
2437
2447
  });
2438
2448
  const lagging = Math.ceil(streamLimit * this._ratio);
2439
2449
  const leading = streamLimit - lagging;
2440
2450
  const started = Date.now();
2451
+ const raised = this._raised;
2441
2452
  const cycle = await run_drain_cycle(this._deps, {
2442
2453
  lagging,
2443
2454
  leading,
@@ -2449,7 +2460,7 @@ var DrainController = class {
2449
2460
  if (!cycle) {
2450
2461
  this._defer.prune(started);
2451
2462
  this._deps.breaker.passed();
2452
- this._armed = false;
2463
+ this._lowered = raised;
2453
2464
  return EMPTY_DRAIN;
2454
2465
  }
2455
2466
  const { leased, fetched, handled, acked, blocked, closeable } = cycle;
@@ -2474,7 +2485,6 @@ var DrainController = class {
2474
2485
  return EMPTY_DRAIN;
2475
2486
  } finally {
2476
2487
  if (!this._stopped) this._defer.schedule();
2477
- this._locked = false;
2478
2488
  this._inflight = void 0;
2479
2489
  this._inflight_done?.();
2480
2490
  this._inflight_done = void 0;
@@ -2716,13 +2726,23 @@ function build_handle(deps) {
2716
2726
  handled++;
2717
2727
  }
2718
2728
  } catch (error) {
2719
- if (error instanceof DeferSignal)
2720
- return {
2721
- lease,
2722
- handled,
2723
- acked_at: at,
2724
- defer: resolve_defer_at(error.when, event)
2725
- };
2729
+ if (error instanceof DeferSignal) {
2730
+ let defer;
2731
+ try {
2732
+ defer = resolve_defer_at(error.when, event);
2733
+ } catch (invalid) {
2734
+ return finalize(
2735
+ lease,
2736
+ handled,
2737
+ at,
2738
+ invalid,
2739
+ payload.options,
2740
+ logger,
2741
+ event.id
2742
+ );
2743
+ }
2744
+ return { lease, handled, acked_at: at, defer };
2745
+ }
2726
2746
  if (error instanceof CloseSignal)
2727
2747
  return {
2728
2748
  lease,
@@ -2908,7 +2928,6 @@ function validate_only_lanes(options, lanes) {
2908
2928
  var Act = class {
2909
2929
  _emitter = new EventEmitter();
2910
2930
  _breaker;
2911
- _listen;
2912
2931
  _drain;
2913
2932
  _auto_settle;
2914
2933
  /** Whether any event has a reaction; nothing arms without one. */
@@ -2989,14 +3008,6 @@ var Act = class {
2989
3008
  /** Runs a body so internal `store()`/`cache()` calls resolve to `_ports`. */
2990
3009
  _scoped;
2991
3010
  _correlator;
2992
- // Bound once, reused by every reaction payload.
2993
- _bound_do = this.do.bind(this);
2994
- _bound_load = this.load.bind(this);
2995
- _bound_query = this.query.bind(this);
2996
- _bound_query_array = this.query_array.bind(this);
2997
- _bound_forget = this.forget.bind(this);
2998
- _handle;
2999
- _handle_batch;
3000
3011
  _lanes;
3001
3012
  _close_lock = new CloseLock();
3002
3013
  /** Every lane a controller can exist for: `"default"` plus each declared one. */
@@ -3030,31 +3041,38 @@ var Act = class {
3030
3041
  this._correlator = options.correlator ?? default_correlator;
3031
3042
  this._es = build_es(this._logger, this._correlator, patch_fn);
3032
3043
  this._cd = build_drain(this._logger);
3033
- this._handle = build_handle({
3044
+ const handle = build_handle({
3034
3045
  logger: this._logger,
3035
3046
  reaction_scope: make_reaction_scope({
3036
- do: this._bound_do,
3037
- load: this._bound_load,
3038
- query: this._bound_query,
3039
- query_array: this._bound_query_array,
3040
- forget: this._bound_forget
3047
+ do: this.do.bind(this),
3048
+ load: this.load.bind(this),
3049
+ query: this.query.bind(this),
3050
+ query_array: this.query_array.bind(this),
3051
+ forget: this.forget.bind(this)
3041
3052
  })
3042
3053
  });
3043
- this._handle_batch = build_handle_batch(this._logger);
3054
+ const handle_batch = build_handle_batch(this._logger);
3044
3055
  const classification = classify_registry(this.registry, this._states);
3045
3056
  this._reactive = classification.event_to_lanes.size > 0;
3046
3057
  this._event_to_state = classification.event_to_state;
3047
3058
  this._event_to_lanes = classification.event_to_lanes;
3048
- this._listen = options.listen !== false;
3049
3059
  this._drain = options.drain !== false;
3050
3060
  this._auto_settle = options.autoSettle !== false;
3051
3061
  this._breaker = this._build_breaker(options);
3052
- this._drain_controllers = this._build_drain_controllers(options, lanes);
3062
+ this._drain_controllers = this._build_drain_controllers(
3063
+ options,
3064
+ lanes,
3065
+ handle,
3066
+ handle_batch
3067
+ );
3053
3068
  this._advise_orphaned_lanes(options);
3054
3069
  this._audit_deps = this._build_audit_deps();
3055
3070
  this._correlate = this._build_correlate(options, classification);
3056
3071
  this._settle = this._build_settle(options);
3057
- this._notify_disposer = this._wire_notify(this._ports.store);
3072
+ this._notify_disposer = this._wire_notify(
3073
+ this._ports.store,
3074
+ options.listen !== false
3075
+ );
3058
3076
  register_weak_disposer(new WeakRef(this), (self) => self.shutdown());
3059
3077
  }
3060
3078
  /** Shared by drain, settle and autoclose; bad knobs throw at build. */
@@ -3070,7 +3088,7 @@ var Act = class {
3070
3088
  );
3071
3089
  }
3072
3090
  /** Apps that declare no lanes pass no lane filter to `claim`. */
3073
- _build_drain_controllers(options, lanes) {
3091
+ _build_drain_controllers(options, lanes, handle, handle_batch) {
3074
3092
  const all_lanes = this._declared_lanes;
3075
3093
  const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
3076
3094
  const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
@@ -3083,8 +3101,8 @@ var Act = class {
3083
3101
  ops: this._cd,
3084
3102
  registry: this.registry,
3085
3103
  batch_handlers: this._batch_handlers,
3086
- handle: this._handle,
3087
- handle_batch: this._handle_batch,
3104
+ handle,
3105
+ handle_batch,
3088
3106
  on_acked: (acked) => this.emit("acked", acked),
3089
3107
  on_blocked: (blocked) => this.emit("blocked", blocked),
3090
3108
  // Reaction-requested close: the same machinery as `app.close`.
@@ -3149,8 +3167,10 @@ var Act = class {
3149
3167
  // Built after the controllers, so their keys are the active lanes.
3150
3168
  shard: this._drain_controllers.size < this._declared_lanes.length ? [...this._drain_controllers.keys()] : void 0,
3151
3169
  // Only reached on a draining app: writer-only ones never correlate.
3152
- on_init: () => this._arm_all(),
3153
- on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
3170
+ on_init: async () => {
3171
+ this._arm_all();
3172
+ await this._seed_persisted_defers();
3173
+ }
3154
3174
  });
3155
3175
  }
3156
3176
  /** Cold start: an idle deferred stream still fires on time after a restart. */
@@ -3243,10 +3263,10 @@ var Act = class {
3243
3263
  }
3244
3264
  }
3245
3265
  /** Failures are logged, never thrown: `notify` is a hint. */
3246
- async _wire_notify(s) {
3266
+ async _wire_notify(s, listen) {
3247
3267
  if (!this._reactive) return void 0;
3248
3268
  if (!s.notify) return void 0;
3249
- if (!this._listen) return void 0;
3269
+ if (!listen) return void 0;
3250
3270
  try {
3251
3271
  return await s.notify((notification) => {
3252
3272
  try {
@@ -4483,7 +4503,7 @@ function build_events(registry, states, lanes, owners) {
4483
4503
  throw new Error(
4484
4504
  `Reaction on target "${resolver.target}" conflicts with the projection that already serves it \u2014 a target is served by one batch handler or one state projection, and a reaction to it would never run`
4485
4505
  );
4486
- if (resolver.lane && !declared.has(resolver.lane))
4506
+ if (resolver.lane !== void 0 && !declared.has(resolver.lane))
4487
4507
  lane_errors.push(
4488
4508
  `Reaction "${handler_name}" on "${event_name}" targets undeclared lane "${resolver.lane}". Declared lanes: ${[...declared].map((l) => `"${l}"`).join(", ")}. Add \`.withLane({ name: "${resolver.lane}", ... })\` to act() or correct the .to() declaration.`
4489
4509
  );