@rotorsoft/act 1.32.18 → 1.32.20

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
@@ -859,6 +859,10 @@ var BackoffOptionsSchema = z2.object({
859
859
  maxMs: z2.number().gt(0).optional(),
860
860
  jitter: z2.boolean().optional()
861
861
  });
862
+ var DEFAULT_BACKOFF = {
863
+ strategy: "fixed",
864
+ baseMs: 1e4
865
+ };
862
866
  var DEFAULT_MAX_RETRIES = 3;
863
867
  var DEFAULT_BLOCK_ON_ERROR = true;
864
868
  var ReactionOptionsSchema = z2.object({
@@ -1661,13 +1665,14 @@ var CorrelateCycle = class {
1661
1665
  cd,
1662
1666
  max_subscribed_streams,
1663
1667
  declared_lanes,
1668
+ shard,
1664
1669
  on_init,
1665
1670
  on_init_async,
1666
1671
  cold_start_back_scan = DEFAULT_COLD_START_BACK_SCAN,
1667
1672
  lease_millis = DEFAULT_CORRELATION_LEASE_MS
1668
1673
  }) {
1669
1674
  this._lease_millis = lease_millis;
1670
- this._key = registry_key(registry.events);
1675
+ this._key = shard ? `${registry_key(registry.events)}:${[...shard].sort().join(",")}` : registry_key(registry.events);
1671
1676
  this._dynamic_subscriptions = new LruMap(max_subscribed_streams);
1672
1677
  this._registry = registry;
1673
1678
  this._declared_lanes = declared_lanes;
@@ -2063,9 +2068,8 @@ var DeferTimer = class {
2063
2068
  _on_wake;
2064
2069
  /**
2065
2070
  * @param on_wake - invoked once each time the earliest due-time elapses,
2066
- * after the come-due entries have been removed. Consumers use it to
2067
- * re-arm their loop (the drain sets its `armed` flag; autoclose runs a
2068
- * tick).
2071
+ * after the come-due entries have been removed. The consumer re-schedules
2072
+ * for whatever is still parked.
2069
2073
  */
2070
2074
  constructor(on_wake) {
2071
2075
  this._on_wake = on_wake;
@@ -2118,14 +2122,9 @@ var DeferTimer = class {
2118
2122
  this._timer = setTimeout(() => {
2119
2123
  this._timer = void 0;
2120
2124
  const now = Date.now();
2121
- let came_due = false;
2122
2125
  for (const [stream, at] of this._due)
2123
- if (at <= now) {
2124
- this._due.delete(stream);
2125
- came_due = true;
2126
- }
2126
+ if (at <= now) this._due.delete(stream);
2127
2127
  this._on_wake();
2128
- if (!came_due && this._due.size > 0) this.schedule();
2129
2128
  }, delay);
2130
2129
  this._timer.unref();
2131
2130
  }
@@ -2842,7 +2841,7 @@ async function run_drain_cycle(deps, cycle) {
2842
2841
  handled: 0,
2843
2842
  acked_at: lease.at,
2844
2843
  error,
2845
- ...block2 ? { block: true } : {}
2844
+ ...block2 ? { block: true } : { next_attempt_at: Date.now() + DEFAULT_BACKOFF.baseMs }
2846
2845
  };
2847
2846
  return Promise.resolve(failed);
2848
2847
  }
@@ -2890,11 +2889,15 @@ var DrainController = class {
2890
2889
  _ratio = 0.5;
2891
2890
  /**
2892
2891
  * Local wake for streams this worker parked (backoff or defer). The
2893
- * schedule itself is persisted in the store; this only re-arms the drain
2894
- * at the earliest pending visit.
2892
+ * schedule itself is persisted in the store; this runs the drain at the
2893
+ * earliest pending visit. A parked stream already carries its mark, so
2894
+ * the drain alone finds it, and on an idle app nothing else would.
2895
+ * `stop()` cancels the timer and a stopped drain never re-schedules it,
2896
+ * so a wake never runs after a stop.
2895
2897
  */
2896
2898
  _defer = new DeferTimer(() => {
2897
2899
  this._armed = true;
2900
+ void this._deps.run_scoped(() => this.drain());
2898
2901
  });
2899
2902
  /** Worker timer. Set when `start()` is active, undefined otherwise. */
2900
2903
  _worker;
@@ -2982,13 +2985,13 @@ var DrainController = class {
2982
2985
  async drain(options = {}) {
2983
2986
  if (!this._armed) return EMPTY_DRAIN;
2984
2987
  if (this._locked) return EMPTY_DRAIN;
2985
- if (this._deps.breaker.state(Date.now()) === "open")
2986
- return EMPTY_DRAIN;
2987
2988
  const d = this._deps.defaults ?? {};
2988
2989
  const streamLimit = d.streamLimit ?? options.streamLimit ?? DEFAULT_STREAM_LIMIT;
2989
2990
  const eventLimit = d.eventLimit ?? options.eventLimit ?? DEFAULT_EVENT_LIMIT;
2990
2991
  const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? DEFAULT_LEASE_MILLIS;
2991
2992
  try {
2993
+ if (this._deps.breaker.state(Date.now()) === "open")
2994
+ return EMPTY_DRAIN;
2992
2995
  this._locked = true;
2993
2996
  this._inflight = new Promise((done) => {
2994
2997
  this._inflight_done = done;
@@ -3006,7 +3009,6 @@ var DrainController = class {
3006
3009
  if (!cycle) {
3007
3010
  this._deps.breaker.passed();
3008
3011
  this._armed = false;
3009
- if (this._defer.size > 0) this._defer.schedule();
3010
3012
  return EMPTY_DRAIN;
3011
3013
  }
3012
3014
  const { leased, fetched, handled, acked, blocked, closeable } = cycle;
@@ -3015,11 +3017,9 @@ var DrainController = class {
3015
3017
  for (const lease of acked) this._defer.delete(lease.stream);
3016
3018
  for (const lease of blocked) this._defer.delete(lease.stream);
3017
3019
  for (const h of handled) {
3018
- const retry_at = h.error && !h.block ? h.next_attempt_at ?? Date.now() + leaseMillis : void 0;
3019
- const next = h.defer ?? retry_at;
3020
+ const next = h.defer ?? h.next_attempt_at;
3020
3021
  if (next !== void 0) this._defer.set(h.lease.stream, next);
3021
3022
  }
3022
- if (this._defer.size > 0) this._defer.schedule();
3023
3023
  if (acked.length) this._deps.on_acked(acked);
3024
3024
  if (closeable.length) await this._deps.on_close(closeable);
3025
3025
  this._deps.breaker.passed();
@@ -3030,6 +3030,7 @@ var DrainController = class {
3030
3030
  this._deps.breaker.failed(Date.now(), error);
3031
3031
  return EMPTY_DRAIN;
3032
3032
  } finally {
3033
+ if (!this._stopped) this._defer.schedule();
3033
3034
  this._locked = false;
3034
3035
  this._inflight = void 0;
3035
3036
  this._inflight_done?.();
@@ -3235,7 +3236,7 @@ function finalize(lease, handled, at, error, options, logger, failed_at) {
3235
3236
  logger.error(
3236
3237
  non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
3237
3238
  );
3238
- const next_attempt_at = !block2 && options.backoff ? Date.now() + compute_backoff_delay(lease.retry, options.backoff) : void 0;
3239
+ const next_attempt_at = block2 ? void 0 : Date.now() + compute_backoff_delay(lease.retry, options.backoff ?? DEFAULT_BACKOFF);
3239
3240
  return {
3240
3241
  lease,
3241
3242
  handled,
@@ -3671,6 +3672,8 @@ var Act = class {
3671
3672
  // Declared lanes (not active ones): a lane another process claims is
3672
3673
  // still a valid destination.
3673
3674
  declared_lanes: new Set(this._declared_lanes),
3675
+ // Built after the controllers, so their keys are the active lanes.
3676
+ shard: this._drain_controllers.size < this._declared_lanes.length ? [...this._drain_controllers.keys()] : void 0,
3674
3677
  on_init: () => {
3675
3678
  if (this._drain && this._reactive_events.size > 0) this._arm_all();
3676
3679
  },
@@ -4367,11 +4370,11 @@ var Act = class {
4367
4370
  });
4368
4371
  }
4369
4372
  /**
4370
- * Atomically wipe the store and rebuild it from an async stream of
4371
- * committed events. The framework owns iteration, validation,
4372
- * `drop_snapshots` filtering, `on_progress`, and the per-call
4373
- * `old → new` causation remap; the adapter's {@link Store.restore}
4374
- * driver supplies the transaction lifecycle and per-event insert.
4373
+ * Atomically wipe the store and rebuild it from an event source. The
4374
+ * framework owns iteration, validation, `drop_snapshots` filtering,
4375
+ * `on_progress`, and the per-call `old → new` causation remap; the
4376
+ * adapter's {@link Store.restore} driver supplies the transaction
4377
+ * lifecycle and per-event insert.
4375
4378
  *
4376
4379
  * Throws if the adapter has no restore capability. Throws on the
4377
4380
  * first invalid event (negative version, malformed `created`) with
@@ -4379,33 +4382,30 @@ var Act = class {
4379
4382
  * the adapter means a failing restore leaves the store byte-for-byte
4380
4383
  * unchanged.
4381
4384
  *
4382
- * @param source - Async stream of events in target order. Streamed
4383
- * rather than buffered so multi-million-event backups don't OOM.
4384
- * Each event's original `id` is used as a causation lookup key but
4385
- * never written through — adapters renumber densely.
4385
+ * @param source - Any {@link EventSource}: a `CsvFile` backup or
4386
+ * another store. Read in batches rather than buffered, so
4387
+ * multi-million-event backups don't OOM. Each event's original `id`
4388
+ * is used as a causation lookup key but never written through —
4389
+ * adapters renumber densely.
4386
4390
  * @param opts - {@link ScanOptions}. `drop_snapshots` skips
4387
4391
  * `__snapshot__` events (counted in the result); `on_progress`
4388
4392
  * fires once per event.
4393
+ * @param sink - Where the events land. Defaults to this Act's store.
4389
4394
  * @returns {@link ScanResult} with `kept`, `duration_ms`, and
4390
4395
  * `dropped` per-category counters.
4391
4396
  *
4392
- * @example Round-trip a CSV backup
4397
+ * @example Restore a CSV backup into the connected store
4393
4398
  * ```typescript
4394
- * async function* parseCsv(blob: string) {
4395
- * for (const line of blob.split("\n").slice(1)) {
4396
- * const [id, name, data, stream, version, created, meta] = parse(line);
4397
- * yield {
4398
- * id: +id, name, data: JSON.parse(data), stream,
4399
- * version: +version, created: new Date(created),
4400
- * meta: JSON.parse(meta),
4401
- * };
4402
- * }
4403
- * }
4404
- * const result = await app.restore(parseCsv(csvBlob), {});
4399
+ * const result = await app.restore(new CsvFile({ path: "./backup.csv" }));
4405
4400
  * console.log(`Restored ${result.kept} events in ${result.duration_ms}ms`);
4406
4401
  * await cache().clear(); // operator's responsibility
4407
4402
  * ```
4408
4403
  *
4404
+ * @example Copy one store into another
4405
+ * ```typescript
4406
+ * await app.restore(pg, {}, sqlite);
4407
+ * ```
4408
+ *
4409
4409
  * @see {@link Store.restore} for the underlying driver-pattern primitive.
4410
4410
  */
4411
4411
  async restore(source, opts = {}, sink) {