@rotorsoft/act 0.30.0 → 0.31.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.js CHANGED
@@ -323,6 +323,21 @@ var InMemoryStream = class {
323
323
  get at() {
324
324
  return this._at;
325
325
  }
326
+ get retry() {
327
+ return this._retry;
328
+ }
329
+ get blocked() {
330
+ return this._blocked;
331
+ }
332
+ get error() {
333
+ return this._error;
334
+ }
335
+ get leased_by() {
336
+ return this._leased_by;
337
+ }
338
+ get leased_until() {
339
+ return this._leased_until;
340
+ }
326
341
  /**
327
342
  * Attempt to lease this stream for processing.
328
343
  * @param lease - The lease request.
@@ -603,6 +618,49 @@ var InMemoryStore = class {
603
618
  }
604
619
  return count;
605
620
  }
621
+ /**
622
+ * Streams registered subscription positions to the callback, ordered by
623
+ * stream name. Returns the highest event id in the store and the count
624
+ * of positions emitted.
625
+ */
626
+ async query_streams(callback, query) {
627
+ await sleep();
628
+ const limit = query?.limit ?? 100;
629
+ const after = query?.after;
630
+ const blocked = query?.blocked;
631
+ const streamRe = query?.stream && !query.stream_exact ? new RegExp(`^${query.stream}$`) : void 0;
632
+ const sourceRe = query?.source && !query.source_exact ? new RegExp(`^${query.source}$`) : void 0;
633
+ const sorted = [...this._streams.values()].sort(
634
+ (a, b) => a.stream.localeCompare(b.stream)
635
+ );
636
+ let count = 0;
637
+ for (const s of sorted) {
638
+ if (after !== void 0 && s.stream <= after) continue;
639
+ if (query?.stream !== void 0) {
640
+ if (query.stream_exact ? s.stream !== query.stream : !streamRe.test(s.stream))
641
+ continue;
642
+ }
643
+ if (query?.source !== void 0) {
644
+ if (s.source === void 0) continue;
645
+ if (query.source_exact ? s.source !== query.source : !sourceRe.test(s.source))
646
+ continue;
647
+ }
648
+ if (blocked !== void 0 && s.blocked !== blocked) continue;
649
+ callback({
650
+ stream: s.stream,
651
+ source: s.source,
652
+ at: s.at,
653
+ retry: s.retry,
654
+ blocked: s.blocked,
655
+ error: s.error,
656
+ leased_by: s.leased_by,
657
+ leased_until: s.leased_until
658
+ });
659
+ count++;
660
+ if (count >= limit) break;
661
+ }
662
+ return { maxEventId: this._events.length - 1, count };
663
+ }
606
664
  /**
607
665
  * Atomically truncates streams and seeds each with a snapshot or tombstone.
608
666
  * @param targets - Streams to truncate with optional snapshot state and meta.
@@ -1622,6 +1680,46 @@ var Act = class {
1622
1680
  this._settle_timer = void 0;
1623
1681
  }
1624
1682
  }
1683
+ /**
1684
+ * Reset reaction stream watermarks and request a drain on the next
1685
+ * `drain()` / `settle()` cycle.
1686
+ *
1687
+ * Use this to replay events through projections (or other reaction targets)
1688
+ * after changing handler logic. Equivalent to calling `store().reset(streams)`
1689
+ * directly, but also raises the orchestrator's internal "needs drain" flag —
1690
+ * `store().reset(...)` alone leaves the flag untouched, so a settled app
1691
+ * would short-circuit and skip the replay.
1692
+ *
1693
+ * Pair with `app.settle()` (or a single `app.drain()` for small streams).
1694
+ * `settle()` loops correlate→drain until no progress is made, so one call
1695
+ * fully catches up paginated streams without forcing callers to roll
1696
+ * their own loop.
1697
+ *
1698
+ * @param streams - Reaction target streams (e.g., projection names) to reset
1699
+ * @returns Count of streams that were actually reset
1700
+ *
1701
+ * @example Rebuild a projection (production)
1702
+ * ```typescript
1703
+ * await app.reset(["my-projection"]);
1704
+ * app.settle({ eventLimit: 1000 }); // emits "settled" when fully replayed
1705
+ * ```
1706
+ *
1707
+ * @example Rebuild a projection (tests / scripts)
1708
+ * ```typescript
1709
+ * await app.reset(["my-projection"]);
1710
+ * await app.drain({ eventLimit: 1000 }); // small streams: one pass is enough
1711
+ * ```
1712
+ *
1713
+ * @see {@link Store.reset} for the underlying store primitive
1714
+ * @see {@link settle} for the debounced full-catch-up loop
1715
+ */
1716
+ async reset(streams) {
1717
+ const count = await store().reset(streams);
1718
+ if (count > 0 && this._reactive_events.size > 0) {
1719
+ this._needs_drain = true;
1720
+ }
1721
+ return count;
1722
+ }
1625
1723
  /**
1626
1724
  * Close the books — guard, archive, truncate, and optionally restart streams.
1627
1725
  *
@@ -1789,15 +1887,19 @@ var Act = class {
1789
1887
  /**
1790
1888
  * Debounced, non-blocking correlate→drain cycle.
1791
1889
  *
1792
- * Call this after `app.do()` to schedule a background drain. Multiple rapid
1793
- * calls within the debounce window are coalesced into a single cycle. Runs
1794
- * correlate→drain in a loop until the system reaches a consistent state,
1795
- * then emits the `"settled"` lifecycle event.
1890
+ * Call this after `app.do()` (or `app.reset()`) to schedule a background
1891
+ * drain. Multiple rapid calls within the debounce window are coalesced
1892
+ * into a single cycle. Runs correlate→drain in a loop until a pass makes
1893
+ * no progress no new subscriptions, no acks, no blocks — then emits
1894
+ * the `"settled"` lifecycle event. This means a single `settle()` call
1895
+ * fully catches up paginated streams (e.g. after `reset()` on a long
1896
+ * projection) without forcing callers to loop.
1796
1897
  *
1797
1898
  * @param options - Settle configuration options
1798
1899
  * @param options.debounceMs - Debounce window in milliseconds (default: 10)
1799
1900
  * @param options.correlate - Query filter for correlation scans (default: `{ after: -1, limit: 100 }`)
1800
- * @param options.maxPasses - Maximum correlate→drain loops (default: 1)
1901
+ * @param options.maxPasses - Cap on correlate→drain loops (default: `Infinity`).
1902
+ * Early-exit on no-progress means the cap only matters in pathological cases.
1801
1903
  * @param options.streamLimit - Maximum streams per drain cycle (default: 10)
1802
1904
  * @param options.eventLimit - Maximum events per stream (default: 10)
1803
1905
  * @param options.leaseMillis - Lease duration in milliseconds (default: 10000)
@@ -1819,7 +1921,7 @@ var Act = class {
1819
1921
  const {
1820
1922
  debounceMs = 10,
1821
1923
  correlate: correlateQuery = { after: -1, limit: 100 },
1822
- maxPasses = 1,
1924
+ maxPasses = Infinity,
1823
1925
  ...drainOptions
1824
1926
  } = options;
1825
1927
  if (this._settle_timer) clearTimeout(this._settle_timer);
@@ -1835,9 +1937,9 @@ var Act = class {
1835
1937
  ...correlateQuery,
1836
1938
  after: this._correlation_checkpoint
1837
1939
  });
1838
- if (subscribed === 0 && i > 0) break;
1839
1940
  lastDrain = await this.drain(drainOptions);
1840
- if (!lastDrain.acked.length && !lastDrain.blocked.length) break;
1941
+ const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
1942
+ if (!made_progress) break;
1841
1943
  }
1842
1944
  if (lastDrain) this.emit("settled", lastDrain);
1843
1945
  })().catch((err) => logger3.error(err)).finally(() => {