@rotorsoft/act 1.33.0 → 1.33.2

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
@@ -40,7 +40,7 @@ import {
40
40
  sleep,
41
41
  store,
42
42
  validate
43
- } from "./chunk-JABYJGT7.js";
43
+ } from "./chunk-NNTF627K.js";
44
44
  import {
45
45
  ActorSchema,
46
46
  CausationEventSchema,
@@ -70,7 +70,7 @@ import {
70
70
  pii_fields,
71
71
  pii_schemas,
72
72
  sensitive
73
- } from "./chunk-2PMUONGF.js";
73
+ } from "./chunk-KHFMCJDR.js";
74
74
 
75
75
  // src/signals.ts
76
76
  process.once("SIGINT", async (arg) => {
@@ -2848,7 +2848,7 @@ var SettleLoop = class {
2848
2848
  (async () => {
2849
2849
  await this._deps.init();
2850
2850
  let settled_drain;
2851
- for (let i = 0; i < maxPasses; i++) {
2851
+ for (let i = 0; i < maxPasses && !this._stopped; i++) {
2852
2852
  const after_before = this._deps.checkpoint();
2853
2853
  const { subscribed, last_id, scanned } = await this._deps.correlate({
2854
2854
  ...correlate_query,
@@ -3187,21 +3187,14 @@ var Act = class {
3187
3187
  }
3188
3188
  _shutdown_promise;
3189
3189
  /**
3190
- * Per-instance teardown: stop scheduling new work, give drain cycles
3191
- * already in flight a bounded chance to finish, then remove lifecycle
3192
- * listeners and tear down the cross-process notify subscription.
3190
+ * Per-instance teardown: stops scheduling, gives the cycles in flight up
3191
+ * to `graceMs` to reach their `ack` (an abandoned handler holds its lease
3192
+ * until it expires), then removes listeners and the notify subscription.
3193
+ * The budget is a ceiling, not a delay.
3193
3194
  *
3194
- * Scheduling stops first, then in-flight cycles get up to `graceMs` to
3195
- * reach their `ack` (an abandoned handler holds its lease until it
3196
- * expires). Listeners come off after the wait, so they still see work that
3197
- * finished during it. The budget is a ceiling, not a delay; when it runs
3198
- * out, teardown proceeds anyway.
3199
- *
3200
- * Idempotent — repeated calls return the same promise, and the first
3201
- * call's `graceMs` is the one that applies. Registered automatically with
3202
- * the global `dispose()` registry at construction, so process-wide
3203
- * `dispose()()` covers it; test helpers (or operators that mint
3204
- * short-lived Acts) call it explicitly for prompt cleanup.
3195
+ * Idempotent: repeated calls return the same promise, with the first
3196
+ * call's `graceMs`. Registered with the global `dispose()` registry, so
3197
+ * `dispose()()` covers it; call it directly for prompt cleanup.
3205
3198
  *
3206
3199
  * @param options - See {@link ShutdownOptions}. Defaults the grace budget
3207
3200
  * to the largest lane `leaseMillis` (capped at 30s).
@@ -3279,15 +3272,10 @@ var Act = class {
3279
3272
  }
3280
3273
  }
3281
3274
  /**
3282
- * Executes an action on a state instance, committing resulting events.
3283
- *
3284
- * This is the primary method for modifying state. It:
3285
- * 1. Validates the action payload against the schema
3286
- * 2. Loads the current state snapshot
3287
- * 3. Checks invariants (business rules)
3288
- * 4. Executes the action handler to generate events
3289
- * 5. Applies events to create new state
3290
- * 6. Commits events to the store with optimistic concurrency control
3275
+ * Executes an action: validates the payload, loads the state, checks its
3276
+ * invariants, and commits the events its handler emits, with optimistic
3277
+ * concurrency on the stream. The reactions to those events settle on
3278
+ * their own (unless `autoSettle: false`).
3291
3279
  *
3292
3280
  * @template TKey - Action name from registered actions
3293
3281
  * @param action - The name of the action to execute
@@ -3366,9 +3354,7 @@ var Act = class {
3366
3354
  *
3367
3355
  * For small result sets, consider using {@link query_array} instead.
3368
3356
  *
3369
- * @param query - Filter criteria — see {@link Query} for available fields
3370
- * (`stream`, `name`, `after`, `before`, `created_after`, `created_before`,
3371
- * `limit`, `with_snaps`, `stream_exact`)
3357
+ * @param query - Filter criteria; see {@link Query} for the fields
3372
3358
  * @param callback - Optional callback invoked for each matching event
3373
3359
  * @returns Object with first event, last event, and total count
3374
3360
  *
@@ -3452,31 +3438,23 @@ var Act = class {
3452
3438
  });
3453
3439
  }
3454
3440
  /**
3455
- * Processes pending reactions by draining uncommitted events from the event store.
3456
- *
3457
- * Runs a single drain cycle:
3458
- * 1. Polls the store for streams with uncommitted events
3459
- * 2. Leases streams to prevent concurrent processing
3460
- * 3. Fetches events for each leased stream
3461
- * 4. Executes matching reaction handlers
3462
- * 5. Acknowledges successful reactions or blocks failing ones
3441
+ * Runs one drain cycle: claims streams with marked work (leasing them),
3442
+ * fetches their events, runs the matching reactions, and acks or blocks
3443
+ * each stream.
3463
3444
  *
3464
- * Drain uses a dual-frontier strategy to balance processing of new streams (lagging)
3465
- * vs active streams (leading). The ratio adapts based on event pressure.
3466
- *
3467
- * Call `correlate()` before `drain()`. It is not only how dynamic targets
3468
- * are discovered: a stream is claimable while `at < correlated_at`, and
3469
- * `correlate` is the only component that raises that mark, so a
3470
- * commit no correlate has seen is not drainable, static targets included. For a
3471
- * higher-level API that handles debouncing, correlation, and signaling
3472
- * automatically, use {@link settle}.
3445
+ * A stream is claimable only once `correlate()` has marked it, static
3446
+ * targets included, so a commit no correlate has seen is not drainable.
3447
+ * Apps rarely call this: each commit settles its reactions on its own.
3448
+ * With `autoSettle: false`, tests walk the pipeline with `correlate()`
3449
+ * then `drain()` (see the testing guide).
3473
3450
  *
3474
3451
  * @param options - Drain configuration — see {@link DrainOptions} for fields
3475
3452
  * (`streamLimit`, `eventLimit`, `leaseMillis`).
3476
3453
  * @returns Drain statistics with fetched, leased, acked, and blocked counts
3477
3454
  *
3478
- * @example In tests and scripts
3455
+ * @example Walking the pipeline in a test
3479
3456
  * ```typescript
3457
+ * const app = act().withState(User).withProjection(Users).build({ autoSettle: false });
3480
3458
  * await app.do("createUser", target, payload);
3481
3459
  * await app.correlate();
3482
3460
  * await app.drain();
@@ -3484,7 +3462,6 @@ var Act = class {
3484
3462
  *
3485
3463
  * @see {@link settle} for debounced correlate→drain with lifecycle events
3486
3464
  * @see {@link correlate} for dynamic stream discovery
3487
- * @see {@link start_correlations} for automatic correlation
3488
3465
  */
3489
3466
  async drain(options = {}) {
3490
3467
  resolveDrainConfig(options);
@@ -3551,32 +3528,20 @@ var Act = class {
3551
3528
  return { fetched, leased, acked, blocked };
3552
3529
  }
3553
3530
  /**
3554
- * Discovers and registers new streams dynamically based on reaction resolvers.
3555
- *
3556
- * Correlation enables "dynamic reactions" where target streams are determined at runtime
3557
- * based on event content. For example, you might create a stats stream for each user
3558
- * when they perform certain actions.
3559
- *
3560
- * This method scans events matching the query and identifies new target streams based
3561
- * on reaction resolvers. It then registers these streams so they'll be picked up by
3562
- * the next drain cycle.
3531
+ * Scans committed events from the correlation checkpoint, subscribes the
3532
+ * target streams their reactions resolve to (static or computed from
3533
+ * the event), and marks their work so the next drain can claim it.
3563
3534
  *
3564
3535
  * @param query - Scan filter — see {@link Query} for fields (typically
3565
- * `{ after: <event-id>, limit: <count> }`)
3536
+ * `{ limit: <count> }`; the scan starts at the checkpoint)
3566
3537
  * @returns The number of newly subscribed streams and the last scanned event id
3567
3538
  *
3568
3539
  * @example Manual correlation
3569
3540
  * ```typescript
3570
- * // Scan for new streams
3571
- * const { subscribed, last_id } = await app.correlate({ after: 0, limit: 100 });
3572
- * console.log(`Subscribed ${subscribed} new streams`);
3573
- *
3574
- * // Save last_id for next scan
3575
- * await saveCheckpoint(last_id);
3541
+ * const { subscribed } = await app.correlate({ limit: 100 });
3576
3542
  * ```
3577
3543
  *
3578
3544
  * @see {@link start_correlations} for automatic periodic correlation
3579
- * @see {@link stop_correlations} to stop automatic correlation
3580
3545
  */
3581
3546
  async correlate(query = { after: -1, limit: 10 }) {
3582
3547
  const { subscribed, last_id } = await this._correlate_scanned(query);
@@ -3594,7 +3559,7 @@ var Act = class {
3594
3559
  /**
3595
3560
  * Starts a background poll that settles on a timer: each tick looks for
3596
3561
  * new events, subscribes the streams their reactions target, and drains
3597
- * them.
3562
+ * them, along with any stream whose lease a dead worker let expire.
3598
3563
  *
3599
3564
  * Use it when this process can miss commits — another process writes to a
3600
3565
  * store without `notify`, or a notification is lost. It is the safety net
@@ -3623,7 +3588,7 @@ var Act = class {
3623
3588
  if (this._poll) return false;
3624
3589
  this._poll_limit = query.limit || void 0;
3625
3590
  this._poll = setInterval(() => {
3626
- this._correlate.arm();
3591
+ this._arm_all();
3627
3592
  this._schedule();
3628
3593
  }, frequency);
3629
3594
  return true;
@@ -3656,15 +3621,13 @@ var Act = class {
3656
3621
  * events through their reactions.
3657
3622
  *
3658
3623
  * Use this to replay events through projections (or other reaction targets)
3659
- * after changing handler logic. Equivalent to calling `store().reset(streams)`
3660
- * directly, but also raises the orchestrator's internal "needs drain" flag —
3661
- * `store().reset(...)` alone leaves the flag untouched, so a settled app
3662
- * would short-circuit and skip the replay.
3663
- *
3664
- * The reset schedules a settle on its own, which loops correlate→drain
3665
- * until the replay catches up. Call `app.settle(options)` right after to
3666
- * give that pass batch options, or
3667
- * `correlate()` / `drain()` to drive the replay by hand.
3624
+ * after changing handler logic. It rewinds the same watermarks as
3625
+ * `store().reset(streams)`, and also wakes the drain and schedules a
3626
+ * settle, which loops correlate→drain until the replay catches up;
3627
+ * `store().reset(...)` alone leaves a settled app idle. Call
3628
+ * `app.settle(options)` right after to give that pass batch options, or,
3629
+ * with `autoSettle: false`, `correlate()` / `drain()` to drive the replay
3630
+ * by hand.
3668
3631
  *
3669
3632
  * @param input - Reaction target streams (e.g., projection names) to reset, or a {@link StreamFilter} for bulk operations
3670
3633
  * @returns Count of streams that were actually reset
@@ -3823,45 +3786,20 @@ var Act = class {
3823
3786
  });
3824
3787
  }
3825
3788
  /**
3826
- * Operator-driven store audit.
3827
- *
3828
- * Walks the connected store and yields per-category findings —
3829
- * each tagged with the remediation it suggests. Same operator-
3830
- * driven category as `app.close()` / `app.reset()` /
3831
- * `app.unblock()` / `app.blocked_streams()`: never auto-invoked by
3832
- * the framework; the operator decides when to run it (CI gate,
3833
- * scheduled job, ad-hoc forensics) and what to do with the
3834
- * findings.
3789
+ * Operator-driven store audit: walks the store and yields findings per
3790
+ * category, each with the remediation it suggests. Never run by the
3791
+ * framework. Pass a subset of categories to scope the work; the iterable
3792
+ * can be broken early, and findings stream without buffering the report.
3835
3793
  *
3836
- * Categories are independent — pass a subset to scope the work,
3837
- * or omit to run everything:
3794
+ * @param categories - Subset of categories to run (default: all).
3795
+ * @param options - Query window + per-category thresholds.
3796
+ * @returns Async iterable of {@link AuditFinding}.
3838
3797
  *
3798
+ * @example Schema drift since the last scan
3839
3799
  * ```typescript
3840
- * // Targeted: schema drift + deprecated-event load only
3841
- * for await (const f of app.audit(["schema", "deprecated-load"], {
3842
- * query: { created_after: lastScan },
3843
- * thresholds: { deprecatedLoadShareMin: 0.10 },
3844
- * })) {
3800
+ * for await (const f of app.audit(["schema"], { query: { created_after: lastScan } }))
3845
3801
  * await escalate(f);
3846
- * }
3847
- *
3848
- * // Full audit, default thresholds
3849
- * for await (const f of app.audit()) console.log(f);
3850
3802
  * ```
3851
- *
3852
- * Returns an `AsyncIterable` so callers can `break` early — the
3853
- * underlying store paginations respect the iterator protocol and
3854
- * stop cleanly. Each finding is emitted independently, so
3855
- * pipelining into Slack / persistence / further analysis works
3856
- * without buffering the full report in memory.
3857
- *
3858
- * Findings shape — see {@link AuditFinding}. The discriminated
3859
- * union carries enough context for the operator to act on each
3860
- * finding directly: stream id, event id, recommendation hints.
3861
- *
3862
- * @param categories - Subset of categories to run (default: all).
3863
- * @param options - Query window + per-category thresholds.
3864
- * @returns Async iterable of {@link AuditFinding}.
3865
3803
  */
3866
3804
  async *audit(categories, options) {
3867
3805
  const it = audit(this._audit_deps, categories, options)[Symbol.asyncIterator]();
@@ -3872,22 +3810,13 @@ var Act = class {
3872
3810
  }
3873
3811
  }
3874
3812
  /**
3875
- * Bulk-update scheduling priority for streams matching `filter`.
3876
- *
3877
- * Operator-grade override of the `claim()` lagging-frontier
3878
- * ordering. Useful when a long-running replay needs to
3879
- * jump ahead of other lagging streams, or when a no-longer-urgent
3880
- * job should yield slots back to the rest. Build-time priorities
3881
- * (set via the resolver's `priority` field) are subject to a
3882
- * `max()` invariant across reactions; this API ignores that and
3883
- * sets the priority outright on every matching row.
3884
- *
3885
- * Filter shape mirrors {@link query} / {@link Store.query_streams}:
3886
- * `stream` / `source` are regex by default, exact with the
3887
- * `*_exact` flags; `blocked` restricts to blocked or unblocked
3888
- * rows. **An empty filter (`{}`) updates every registered stream.**
3813
+ * Sets the scheduling priority of every stream matching `filter`, as-is:
3814
+ * unlike build-time priorities (the highest across reactions wins), this
3815
+ * overrides them. Useful to let a long replay jump ahead of other lagging
3816
+ * streams, or to yield slots back. **An empty filter (`{}`) updates every
3817
+ * registered stream.**
3889
3818
  *
3890
- * @param filter - Selection criteria (regex by default).
3819
+ * @param filter - Selection criteria (regex by default, like {@link query}).
3891
3820
  * @param priority - New priority value. Set as-is — no clamp.
3892
3821
  * @returns Count of streams whose priority changed.
3893
3822
  *
@@ -3897,7 +3826,6 @@ var Act = class {
3897
3826
  * ```
3898
3827
  *
3899
3828
  * @see {@link Store.prioritize} for the underlying primitive
3900
- * @see {@link claim} for how priority biases scheduling
3901
3829
  */
3902
3830
  async prioritize(filter, priority) {
3903
3831
  return this._scoped(() => store().prioritize(filter, priority));
@@ -3924,30 +3852,12 @@ var Act = class {
3924
3852
  return result;
3925
3853
  }
3926
3854
  /**
3927
- * Close the books — guard, archive, truncate, and optionally restart streams.
3928
- *
3929
- * Safely removes historical events from the operational store:
3930
- *
3931
- * 1. **Correlate** — discover pending reaction targets
3932
- * 2. **Safety check** — skip streams with pending reactions (skipped when no reactive events)
3933
- * 3. **Guard** — commit `__tombstone__` with `expectedVersion` to block concurrent writes
3934
- * 4. **Load state** — for streams in `snapshots`, load final state while guarded (no races)
3935
- * 5. **Archive** — user callback per stream (abort-all on failure, streams are guarded)
3936
- * 6. **Truncate + seed** — atomic: delete all events, insert `__snapshot__` or `__tombstone__`
3937
- * 7. **Cache** — invalidate (tombstoned) or warm (restarted)
3938
- * 8. **Emit "closed"** — lifecycle event with results
3939
- *
3940
- * Targets carrying `before` take the **windowed** branch instead —
3941
- * close the books on a rolling window: probe the min consumer
3942
- * watermark (so the boundary never rises past a lagging reaction),
3943
- * run the archive callback against the cutoff, then prune the prefix
3944
- * below the closest safe `__snapshot__`. No tombstone guard (the
3945
- * pre-cutoff prefix is immutable), no seed, cache untouched — the
3946
- * stream stays live and keeps accepting actions. Requires the state
3947
- * to snapshot via `.snap(...)`; no qualifying snapshot ⇒ the stream
3948
- * lands in `skipped` (retry after the next snapshot). Windowed
3949
- * entries in the result echo `before` and carry the surviving
3950
- * boundary snapshot as `committed`.
3855
+ * Close the books: guard, archive, truncate, and optionally restart
3856
+ * streams. A stream with pending reactions is skipped; an archive
3857
+ * failure aborts the whole close while the streams are still guarded.
3858
+ * Targets carrying `before` prune a rolling window instead and leave the
3859
+ * stream live (needs `.snap(...)`). The phases and their failure
3860
+ * semantics are in the close-cycle architecture page.
3951
3861
  *
3952
3862
  * @param targets - Per-stream close options (stream, restart?, archive?, before?)
3953
3863
  * @returns `{ truncated: TruncateResult, skipped: string[] }`
@@ -3982,7 +3892,8 @@ var Act = class {
3982
3892
  * `debounceMs` (default 10), `correlate.limit` (default 100, or the
3983
3893
  * poll's limit while `start_correlations` runs; `after` is always the
3984
3894
  * checkpoint),
3985
- * `maxPasses` (default `Infinity` — kill-switch for runaway loops),
3895
+ * `maxPasses` (default `Infinity`; caps this settle's passes, not the
3896
+ * passes a reaction chain's own commits schedule),
3986
3897
  * `streamLimit` (default 10), `eventLimit` (default 10),
3987
3898
  * `leaseMillis` (default 10000).
3988
3899
  *