@rotorsoft/act 0.39.0 → 0.41.0

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
@@ -190,7 +190,7 @@ var ConsoleLogger = class _ConsoleLogger {
190
190
  }
191
191
  };
192
192
 
193
- // src/lru-map.ts
193
+ // src/internal/lru-map.ts
194
194
  var LruMap = class {
195
195
  constructor(_maxSize) {
196
196
  this._maxSize = _maxSize;
@@ -644,7 +644,7 @@ var InMemoryStore = class {
644
644
  if (query.stream) {
645
645
  if (query.stream_exact) {
646
646
  if (e.stream !== query.stream) return false;
647
- } else if (!RegExp(`^${query.stream}$`).test(e.stream)) return false;
647
+ } else if (!RegExp(query.stream).test(e.stream)) return false;
648
648
  }
649
649
  if (query.names && !query.names.includes(e.name)) return false;
650
650
  if (query.correlation && e.meta?.correlation !== query.correlation)
@@ -860,8 +860,8 @@ var InMemoryStore = class {
860
860
  */
861
861
  async prioritize(filter, priority) {
862
862
  await sleep();
863
- const streamRe = filter.stream && !filter.stream_exact ? new RegExp(`^${filter.stream}$`) : void 0;
864
- const sourceRe = filter.source && !filter.source_exact ? new RegExp(`^${filter.source}$`) : void 0;
863
+ const streamRe = filter.stream && !filter.stream_exact ? new RegExp(filter.stream) : void 0;
864
+ const sourceRe = filter.source && !filter.source_exact ? new RegExp(filter.source) : void 0;
865
865
  let count = 0;
866
866
  for (const s of this._streams.values()) {
867
867
  if (filter.stream !== void 0) {
@@ -892,8 +892,8 @@ var InMemoryStore = class {
892
892
  const limit = query?.limit ?? 100;
893
893
  const after = query?.after;
894
894
  const blocked = query?.blocked;
895
- const streamRe = query?.stream && !query.stream_exact ? new RegExp(`^${query.stream}$`) : void 0;
896
- const sourceRe = query?.source && !query.source_exact ? new RegExp(`^${query.source}$`) : void 0;
895
+ const streamRe = query?.stream && !query.stream_exact ? new RegExp(query.stream) : void 0;
896
+ const sourceRe = query?.source && !query.source_exact ? new RegExp(query.source) : void 0;
897
897
  const sorted = [...this._streams.values()].sort(
898
898
  (a, b) => a.stream.localeCompare(b.stream)
899
899
  );
@@ -1387,10 +1387,20 @@ function computeLagLeadRatio(handled, lagging, leading) {
1387
1387
  }
1388
1388
 
1389
1389
  // src/internal/drain-cycle.ts
1390
- async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis) {
1390
+ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis, isDeferred) {
1391
1391
  const leased = await ops.claim(lagging, leading, (0, import_node_crypto2.randomUUID)(), leaseMillis);
1392
1392
  if (!leased.length) return void 0;
1393
- const fetched = await ops.fetch(leased, eventLimit);
1393
+ const active = isDeferred ? leased.filter((l) => !isDeferred(l.stream)) : leased;
1394
+ if (!active.length) {
1395
+ return {
1396
+ leased,
1397
+ fetched: [],
1398
+ handled: [],
1399
+ acked: [],
1400
+ blocked: []
1401
+ };
1402
+ }
1403
+ const fetched = await ops.fetch(active, eventLimit);
1394
1404
  const fetchMap = /* @__PURE__ */ new Map();
1395
1405
  const fetch_window_at = fetched.reduce(
1396
1406
  (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
@@ -1409,7 +1419,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
1409
1419
  fetchMap.set(stream, { fetch: f, payloads });
1410
1420
  }
1411
1421
  const handled = await Promise.all(
1412
- leased.map((lease) => {
1422
+ active.map((lease) => {
1413
1423
  const entry = fetchMap.get(lease.stream);
1414
1424
  const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
1415
1425
  const { payloads } = entry;
@@ -1441,6 +1451,15 @@ var DrainController = class {
1441
1451
  _armed = false;
1442
1452
  _locked = false;
1443
1453
  _ratio = 0.5;
1454
+ /**
1455
+ * Per-stream backoff: `stream → nextAttemptAt` (ms since epoch). Set by
1456
+ * `_finalize` via `HandleResult.nextAttemptAt`; cleared on successful
1457
+ * ack or terminal block. Lives in process memory — per-worker pacing
1458
+ * by design (see {@link BackoffOptions} for the multi-worker trade-off).
1459
+ */
1460
+ _backoff = /* @__PURE__ */ new Map();
1461
+ /** Timer re-arming drain at the earliest pending `nextAttemptAt`. */
1462
+ _backoffTimer;
1444
1463
  /**
1445
1464
  * Signal that a commit (or reset / cold-start) may have produced work.
1446
1465
  * Subsequent `drain()` calls will run the pipeline; once the pipeline
@@ -1453,6 +1472,32 @@ var DrainController = class {
1453
1472
  get armed() {
1454
1473
  return this._armed;
1455
1474
  }
1475
+ /** Returns true when `stream` is currently within a backoff window. */
1476
+ isDeferred = (stream) => {
1477
+ const next = this._backoff.get(stream);
1478
+ return next !== void 0 && next > Date.now();
1479
+ };
1480
+ /**
1481
+ * Schedule the next drain re-arm at the earliest pending backoff
1482
+ * expiry. Called only when the backoff map is non-empty (caller guard).
1483
+ * Idempotent — collapses many simultaneously deferred streams into a
1484
+ * single timer.
1485
+ */
1486
+ scheduleBackoffWake() {
1487
+ if (this._backoffTimer) clearTimeout(this._backoffTimer);
1488
+ let earliest = Number.POSITIVE_INFINITY;
1489
+ for (const t of this._backoff.values()) if (t < earliest) earliest = t;
1490
+ const delay = Math.max(0, earliest - Date.now());
1491
+ this._backoffTimer = setTimeout(() => {
1492
+ this._backoffTimer = void 0;
1493
+ const now = Date.now();
1494
+ for (const [stream, at] of this._backoff) {
1495
+ if (at <= now) this._backoff.delete(stream);
1496
+ }
1497
+ this._armed = true;
1498
+ }, delay);
1499
+ this._backoffTimer.unref();
1500
+ }
1456
1501
  /** Run one drain pass. Short-circuits when not armed or already running. */
1457
1502
  async drain({
1458
1503
  streamLimit = 10,
@@ -1474,7 +1519,8 @@ var DrainController = class {
1474
1519
  lagging,
1475
1520
  leading,
1476
1521
  eventLimit,
1477
- leaseMillis
1522
+ leaseMillis,
1523
+ this._backoff.size > 0 ? this.isDeferred : void 0
1478
1524
  );
1479
1525
  if (!cycle) {
1480
1526
  this._armed = false;
@@ -1482,6 +1528,14 @@ var DrainController = class {
1482
1528
  }
1483
1529
  const { leased, fetched, handled, acked, blocked } = cycle;
1484
1530
  this._ratio = computeLagLeadRatio(handled, lagging, leading);
1531
+ for (const lease of acked) this._backoff.delete(lease.stream);
1532
+ for (const lease of blocked) this._backoff.delete(lease.stream);
1533
+ for (const h of handled) {
1534
+ if (h.nextAttemptAt !== void 0 && !h.block) {
1535
+ this._backoff.set(h.lease.stream, h.nextAttemptAt);
1536
+ }
1537
+ }
1538
+ if (this._backoff.size > 0) this.scheduleBackoffWake();
1485
1539
  if (acked.length) this.deps.onAcked(acked);
1486
1540
  if (blocked.length) this.deps.onBlocked(blocked);
1487
1541
  const hasErrors = handled.some(({ error }) => error);
@@ -1676,6 +1730,27 @@ var _this_ = ({ stream }) => ({
1676
1730
  target: stream
1677
1731
  });
1678
1732
 
1733
+ // src/internal/backoff.ts
1734
+ function computeBackoffDelay(retry, opts) {
1735
+ if (!opts || opts.baseMs <= 0) return 0;
1736
+ const r = Math.max(0, retry);
1737
+ let delay;
1738
+ switch (opts.strategy) {
1739
+ case "fixed":
1740
+ delay = opts.baseMs;
1741
+ break;
1742
+ case "linear":
1743
+ delay = opts.baseMs * (r + 1);
1744
+ break;
1745
+ case "exponential":
1746
+ delay = opts.baseMs * 2 ** r;
1747
+ if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
1748
+ break;
1749
+ }
1750
+ if (opts.jitter) delay = delay * (0.5 + Math.random());
1751
+ return Math.max(0, Math.floor(delay));
1752
+ }
1753
+
1679
1754
  // src/internal/reactions.ts
1680
1755
  function finalize(lease, handled, at, error, options, logger) {
1681
1756
  if (!error) return { lease, handled, at };
@@ -1683,12 +1758,14 @@ function finalize(lease, handled, at, error, options, logger) {
1683
1758
  const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1684
1759
  if (block2)
1685
1760
  logger.error(`Blocking ${lease.stream} after ${lease.retry} retries.`);
1761
+ const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
1686
1762
  return {
1687
1763
  lease,
1688
1764
  handled,
1689
1765
  at,
1690
1766
  error: handled === 0 ? error.message : void 0,
1691
- block: block2
1767
+ block: block2,
1768
+ nextAttemptAt
1692
1769
  };
1693
1770
  }
1694
1771
  function buildHandle(deps) {
@@ -3008,7 +3085,8 @@ function act() {
3008
3085
  resolver: _this_,
3009
3086
  options: {
3010
3087
  blockOnError: options?.blockOnError ?? true,
3011
- maxRetries: options?.maxRetries ?? 3
3088
+ maxRetries: options?.maxRetries ?? 3,
3089
+ backoff: options?.backoff
3012
3090
  }
3013
3091
  };
3014
3092
  if (!handler.name)
@@ -3134,7 +3212,8 @@ function slice() {
3134
3212
  resolver: _this_,
3135
3213
  options: {
3136
3214
  blockOnError: options?.blockOnError ?? true,
3137
- maxRetries: options?.maxRetries ?? 3
3215
+ maxRetries: options?.maxRetries ?? 3,
3216
+ backoff: options?.backoff
3138
3217
  }
3139
3218
  };
3140
3219
  if (!handler.name)