@rotorsoft/act 1.32.11 → 1.32.13

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
@@ -912,17 +912,34 @@ var InMemoryStore = class {
912
912
  this._streams = /* @__PURE__ */ new Map();
913
913
  }
914
914
  in_query(query, e) {
915
- if (query.stream) {
916
- if (query.stream_exact) {
917
- if (e.stream !== query.stream) return false;
918
- } else if (!RegExp(query.stream).test(e.stream)) return false;
919
- }
915
+ if (query.stream && !this._stream_matches(query.stream, query, e.stream))
916
+ return false;
920
917
  if (query.names && !query.names.includes(e.name)) return false;
921
918
  if (query.correlation && e.meta?.correlation !== query.correlation)
922
919
  return false;
923
920
  if (e.name === SNAP_EVENT && !query.with_snaps) return false;
921
+ if (query.created_after && e.created <= query.created_after) return false;
922
+ if (query.created_before && e.created >= query.created_before) return false;
924
923
  return true;
925
924
  }
925
+ _stream_matches(pattern, query, stream) {
926
+ return query.stream_exact ? stream === pattern : RegExp(pattern).test(stream);
927
+ }
928
+ /**
929
+ * Index of the latest snapshot of the queried stream, or -1. `with_snaps`
930
+ * on an exact single stream resumes there, so earlier events aren't read.
931
+ * The orchestrator asks only for unbounded current-state loads, so the
932
+ * floor applies whenever asked; an explicit `after` wins over it.
933
+ */
934
+ _snap_floor(query) {
935
+ if (!query?.with_snaps || !query.stream_exact || query.stream === void 0 || query.after !== void 0)
936
+ return -1;
937
+ for (let j = this._events.length - 1; j >= 0; j--) {
938
+ const e = this._events[j];
939
+ if (e.stream === query.stream && e.name === SNAP_EVENT) return j;
940
+ }
941
+ return -1;
942
+ }
926
943
  /**
927
944
  * Query events in the store, optionally filtered by query options.
928
945
  * @param callback - Function to call for each event.
@@ -931,59 +948,19 @@ var InMemoryStore = class {
931
948
  */
932
949
  async query(callback, query) {
933
950
  await sleep();
951
+ const floor = this._snap_floor(query);
952
+ const lo = floor >= 0 ? floor : this._first_index_after(query?.after ?? -1);
953
+ const hi = query?.before !== void 0 ? this._first_index_after(query.before - 1) : this._events.length;
954
+ const with_pii = query?.with_pii !== false;
934
955
  let count = 0;
935
- let floor_index = -1;
936
- if (query?.with_snaps && query.stream_exact && query.stream !== void 0 && query.after === void 0) {
937
- for (let j = this._events.length - 1; j >= 0; j--) {
938
- const e = this._events[j];
939
- if (e.stream === query.stream && e.name === SNAP_EVENT) {
940
- floor_index = j;
941
- break;
942
- }
943
- }
944
- }
945
- if (query?.backward) {
946
- const floor_id = floor_index >= 0 ? this._events[floor_index].id : -1;
947
- let i = (query?.before !== void 0 ? this._first_index_after(query.before - 1) : this._events.length) - 1;
948
- while (i >= 0) {
949
- const e = this._events[i--];
950
- if (query && !this.in_query(query, e)) continue;
951
- if (query?.created_before && e.created >= query.created_before)
952
- continue;
953
- if (query.after !== void 0 && e.id <= query.after) break;
954
- if (floor_id >= 0 && e.id < floor_id) break;
955
- if (query.created_after && e.created <= query.created_after) continue;
956
- await Promise.resolve(
957
- callback(
958
- this._with_pii(
959
- e,
960
- query?.with_pii !== false
961
- )
962
- )
963
- );
964
- count++;
965
- if (query?.limit && count >= query.limit) break;
966
- }
967
- } else {
968
- let i = floor_index >= 0 ? floor_index : this._first_index_after(query?.after ?? -1);
969
- while (i < this._events.length) {
970
- const e = this._events[i++];
971
- if (query && !this.in_query(query, e)) continue;
972
- if (query?.created_after && e.created <= query.created_after) continue;
973
- if (query?.before !== void 0 && e.id >= query.before) break;
974
- if (query?.created_before && e.created >= query.created_before)
975
- continue;
976
- await Promise.resolve(
977
- callback(
978
- this._with_pii(
979
- e,
980
- query?.with_pii !== false
981
- )
982
- )
983
- );
984
- count++;
985
- if (query?.limit && count >= query.limit) break;
986
- }
956
+ for (let k = 0; k < hi - lo; k++) {
957
+ const e = this._events[query?.backward ? hi - 1 - k : lo + k];
958
+ if (query && !this.in_query(query, e)) continue;
959
+ await Promise.resolve(
960
+ callback(this._with_pii(e, with_pii))
961
+ );
962
+ count++;
963
+ if (query?.limit && count >= query.limit) break;
987
964
  }
988
965
  return count;
989
966
  }
@@ -2509,6 +2486,151 @@ var CloseSignal = class extends Error {
2509
2486
  }
2510
2487
  };
2511
2488
 
2489
+ // src/internal/config.ts
2490
+ var import_zod6 = require("zod");
2491
+ var BackoffOptionsSchema = import_zod6.z.object({
2492
+ strategy: import_zod6.z.enum(["fixed", "linear", "exponential"]),
2493
+ baseMs: import_zod6.z.number().min(0),
2494
+ maxMs: import_zod6.z.number().gt(0).optional(),
2495
+ jitter: import_zod6.z.boolean().optional()
2496
+ });
2497
+ var DEFAULT_MAX_RETRIES = 3;
2498
+ var DEFAULT_BLOCK_ON_ERROR = true;
2499
+ var ReactionOptionsSchema = import_zod6.z.object({
2500
+ blockOnError: import_zod6.z.boolean(),
2501
+ maxRetries: import_zod6.z.number().int().min(0),
2502
+ backoff: BackoffOptionsSchema.optional()
2503
+ });
2504
+ function resolveReactionConfig(options) {
2505
+ return ReactionOptionsSchema.parse(options);
2506
+ }
2507
+ var ActionOptionsSchema = import_zod6.z.object({
2508
+ maxRetries: import_zod6.z.number().int().min(0).optional(),
2509
+ backoff: BackoffOptionsSchema.optional()
2510
+ });
2511
+ function resolveActionConfig(options) {
2512
+ return ActionOptionsSchema.parse(options);
2513
+ }
2514
+ var LaneConfigSchema = import_zod6.z.object({
2515
+ name: import_zod6.z.string().min(1),
2516
+ leaseMillis: import_zod6.z.number().min(0).optional(),
2517
+ streamLimit: import_zod6.z.number().int().min(0).optional(),
2518
+ cycleMs: import_zod6.z.number().min(0).optional()
2519
+ });
2520
+ function resolveLaneConfig(options) {
2521
+ LaneConfigSchema.parse(options);
2522
+ return options;
2523
+ }
2524
+ var DEFAULT_STREAM_LIMIT = 10;
2525
+ var DEFAULT_EVENT_LIMIT = 10;
2526
+ var DEFAULT_LEASE_MILLIS = 1e4;
2527
+ var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
2528
+ var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
2529
+ var DrainOptionsSchema = import_zod6.z.object({
2530
+ streamLimit: import_zod6.z.number().int().min(0).optional(),
2531
+ eventLimit: import_zod6.z.number().int().min(0).optional(),
2532
+ leaseMillis: import_zod6.z.number().min(0).optional()
2533
+ });
2534
+ function resolveDrainConfig(options) {
2535
+ if (options === void 0) return void 0;
2536
+ DrainOptionsSchema.parse(options);
2537
+ return options;
2538
+ }
2539
+ var SettleOptionsSchema = DrainOptionsSchema.extend({
2540
+ debounceMs: import_zod6.z.number().min(0).optional(),
2541
+ // `maxPasses` defaults to Infinity (no cap) when omitted; a present value is
2542
+ // a non-negative integer — `0` is legal and means "run no passes".
2543
+ //
2544
+ // `Infinity` is accepted explicitly. It is the documented default
2545
+ // (`Act.settle`'s doc-comment and `SettleOptions.maxPasses` both name it),
2546
+ // and `settle.ts` destructures `maxPasses = Infinity`, so the one value the
2547
+ // docs advertise was the one value `z.number().int()` refused — Zod 4
2548
+ // rejects `±Infinity`, and an integer check cannot pass it either. Wrapper
2549
+ // code normalizing an optional cap (`maxPasses: opts.maxPasses ?? Infinity`)
2550
+ // is the natural way to hit it. Accepting it also honors this module's own
2551
+ // rule that a value which works today is never newly rejected (#1711).
2552
+ maxPasses: import_zod6.z.union([import_zod6.z.number().int().min(0), import_zod6.z.literal(Number.POSITIVE_INFINITY)]).optional()
2553
+ }).loose();
2554
+ function resolveSettleConfig(options) {
2555
+ if (options === void 0) return void 0;
2556
+ SettleOptionsSchema.parse(options);
2557
+ return options;
2558
+ }
2559
+ var MAX_SHUTDOWN_GRACE_MS = 3e4;
2560
+ var DEFAULT_SHUTDOWN_GRACE_MS = DEFAULT_LEASE_MILLIS;
2561
+ var ShutdownOptionsSchema = import_zod6.z.object({
2562
+ // `0` is legal and is exactly today's behavior: stop scheduling and
2563
+ // return without waiting.
2564
+ graceMs: import_zod6.z.number().min(0).optional()
2565
+ }).loose();
2566
+ function resolveShutdownConfig(options) {
2567
+ if (options === void 0) return void 0;
2568
+ ShutdownOptionsSchema.parse(options);
2569
+ return options;
2570
+ }
2571
+ var ActOptionsSchema = import_zod6.z.object({
2572
+ maxSubscribedStreams: import_zod6.z.number().int().min(1).optional(),
2573
+ settleDebounceMs: import_zod6.z.number().int().min(0).optional()
2574
+ }).loose();
2575
+ function resolveActConfig(options) {
2576
+ if (options === void 0) return void 0;
2577
+ ActOptionsSchema.parse(options);
2578
+ return options;
2579
+ }
2580
+ var DEFAULT_AUTOCLOSE_CYCLE_MINUTES = 720;
2581
+ var DEFAULT_CLOSE_BATCH_SIZE = 64;
2582
+ var DEFAULT_CLOSE_YIELD_MS = 0;
2583
+ var DEFAULT_AUTOCLOSE_WINDOW_TZ = "UTC";
2584
+ function is_valid_time_zone(tz) {
2585
+ try {
2586
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
2587
+ return true;
2588
+ } catch {
2589
+ return false;
2590
+ }
2591
+ }
2592
+ var AutocloseWindowSchema = import_zod6.z.object({
2593
+ start: import_zod6.z.number().int().min(0).max(23, { message: "autocloseWindow.start must be an hour in [0, 23]" }),
2594
+ end: import_zod6.z.number().int().min(0).max(23, { message: "autocloseWindow.end must be an hour in [0, 23]" }),
2595
+ timeZone: import_zod6.z.string().refine(is_valid_time_zone, {
2596
+ message: "autocloseWindow.timeZone must be a valid IANA time zone"
2597
+ }).default(DEFAULT_AUTOCLOSE_WINDOW_TZ)
2598
+ }).refine((w) => w.start !== w.end, {
2599
+ message: "autocloseWindow.start and end must differ \u2014 an empty window disables autoclose"
2600
+ });
2601
+ var AutocloseOptionsSchema = import_zod6.z.object({
2602
+ autocloseCycleMinutes: import_zod6.z.number().int().min(1).max(1440).default(DEFAULT_AUTOCLOSE_CYCLE_MINUTES),
2603
+ closeBatchSize: import_zod6.z.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
2604
+ closeYieldMs: import_zod6.z.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
2605
+ closeOnError: import_zod6.z.boolean().default(false),
2606
+ autocloseWindow: AutocloseWindowSchema.optional()
2607
+ });
2608
+ function resolveAutocloseConfig(options) {
2609
+ return AutocloseOptionsSchema.parse({
2610
+ autocloseCycleMinutes: options?.autocloseCycleMinutes,
2611
+ closeBatchSize: options?.closeBatchSize,
2612
+ closeYieldMs: options?.closeYieldMs,
2613
+ closeOnError: options?.closeOnError,
2614
+ autocloseWindow: options?.autocloseWindow
2615
+ });
2616
+ }
2617
+ var DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5;
2618
+ var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
2619
+ var CircuitBreakerOptionsSchema = import_zod6.z.object({
2620
+ failureThreshold: import_zod6.z.number().int().min(1).default(DEFAULT_CIRCUIT_FAILURE_THRESHOLD),
2621
+ cooldownMs: import_zod6.z.number().int().min(100).max(36e5).default(DEFAULT_CIRCUIT_COOLDOWN_MS)
2622
+ });
2623
+ var resolveCircuitBreakerConfig = (options) => CircuitBreakerOptionsSchema.parse(options ?? {});
2624
+ var DEFAULT_FOLD_FLUSH_EVERY = 1e3;
2625
+ var DEFAULT_MAX_CACHED_STATES = 1e4;
2626
+ var FoldOptionsSchema = import_zod6.z.object({
2627
+ flushEvery: import_zod6.z.number().int().min(1).default(DEFAULT_FOLD_FLUSH_EVERY),
2628
+ maxCachedStates: import_zod6.z.number().int().min(1).default(DEFAULT_MAX_CACHED_STATES)
2629
+ });
2630
+ function resolveFoldConfig(options) {
2631
+ return FoldOptionsSchema.parse(options);
2632
+ }
2633
+
2512
2634
  // src/internal/defer-signal.ts
2513
2635
  var DeferSignal = class extends Error {
2514
2636
  /**
@@ -2545,7 +2667,7 @@ function synthesize_autoclose_reactions(registry, states, read_window) {
2545
2667
  }),
2546
2668
  // Never block on autoclose: a transient query/store error should retry,
2547
2669
  // not quarantine the synthetic stream.
2548
- options: { blockOnError: false, maxRetries: 3 },
2670
+ options: { blockOnError: false, maxRetries: DEFAULT_MAX_RETRIES },
2549
2671
  handler: async (event) => {
2550
2672
  const aggregate = event.stream;
2551
2673
  const window = read_window();
@@ -3032,144 +3154,6 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
3032
3154
  return truncated;
3033
3155
  }
3034
3156
 
3035
- // src/internal/config.ts
3036
- var import_zod6 = require("zod");
3037
- var BackoffOptionsSchema = import_zod6.z.object({
3038
- strategy: import_zod6.z.enum(["fixed", "linear", "exponential"]),
3039
- baseMs: import_zod6.z.number().min(0),
3040
- maxMs: import_zod6.z.number().gt(0).optional(),
3041
- jitter: import_zod6.z.boolean().optional()
3042
- });
3043
- var ReactionOptionsSchema = import_zod6.z.object({
3044
- blockOnError: import_zod6.z.boolean(),
3045
- maxRetries: import_zod6.z.number().int().min(0),
3046
- backoff: BackoffOptionsSchema.optional()
3047
- });
3048
- function resolveReactionConfig(options) {
3049
- return ReactionOptionsSchema.parse(options);
3050
- }
3051
- var ActionOptionsSchema = import_zod6.z.object({
3052
- maxRetries: import_zod6.z.number().int().min(0).optional(),
3053
- backoff: BackoffOptionsSchema.optional()
3054
- });
3055
- function resolveActionConfig(options) {
3056
- return ActionOptionsSchema.parse(options);
3057
- }
3058
- var LaneConfigSchema = import_zod6.z.object({
3059
- name: import_zod6.z.string().min(1),
3060
- leaseMillis: import_zod6.z.number().min(0).optional(),
3061
- streamLimit: import_zod6.z.number().int().min(0).optional(),
3062
- cycleMs: import_zod6.z.number().min(0).optional()
3063
- });
3064
- function resolveLaneConfig(options) {
3065
- LaneConfigSchema.parse(options);
3066
- return options;
3067
- }
3068
- var DrainOptionsSchema = import_zod6.z.object({
3069
- streamLimit: import_zod6.z.number().int().min(0).optional(),
3070
- eventLimit: import_zod6.z.number().int().min(0).optional(),
3071
- leaseMillis: import_zod6.z.number().min(0).optional()
3072
- });
3073
- function resolveDrainConfig(options) {
3074
- if (options === void 0) return void 0;
3075
- DrainOptionsSchema.parse(options);
3076
- return options;
3077
- }
3078
- var SettleOptionsSchema = DrainOptionsSchema.extend({
3079
- debounceMs: import_zod6.z.number().min(0).optional(),
3080
- // `maxPasses` defaults to Infinity (no cap) when omitted; a present value is
3081
- // a non-negative integer — `0` is legal and means "run no passes".
3082
- //
3083
- // `Infinity` is accepted explicitly. It is the documented default
3084
- // (`Act.settle`'s doc-comment and `SettleOptions.maxPasses` both name it),
3085
- // and `settle.ts` destructures `maxPasses = Infinity`, so the one value the
3086
- // docs advertise was the one value `z.number().int()` refused — Zod 4
3087
- // rejects `±Infinity`, and an integer check cannot pass it either. Wrapper
3088
- // code normalizing an optional cap (`maxPasses: opts.maxPasses ?? Infinity`)
3089
- // is the natural way to hit it. Accepting it also honors this module's own
3090
- // rule that a value which works today is never newly rejected (#1711).
3091
- maxPasses: import_zod6.z.union([import_zod6.z.number().int().min(0), import_zod6.z.literal(Number.POSITIVE_INFINITY)]).optional()
3092
- }).loose();
3093
- function resolveSettleConfig(options) {
3094
- if (options === void 0) return void 0;
3095
- SettleOptionsSchema.parse(options);
3096
- return options;
3097
- }
3098
- var MAX_SHUTDOWN_GRACE_MS = 3e4;
3099
- var DEFAULT_SHUTDOWN_GRACE_MS = 1e4;
3100
- var ShutdownOptionsSchema = import_zod6.z.object({
3101
- // `0` is legal and is exactly today's behavior: stop scheduling and
3102
- // return without waiting.
3103
- graceMs: import_zod6.z.number().min(0).optional()
3104
- }).loose();
3105
- function resolveShutdownConfig(options) {
3106
- if (options === void 0) return void 0;
3107
- ShutdownOptionsSchema.parse(options);
3108
- return options;
3109
- }
3110
- var ActOptionsSchema = import_zod6.z.object({
3111
- maxSubscribedStreams: import_zod6.z.number().int().min(1).optional(),
3112
- settleDebounceMs: import_zod6.z.number().int().min(0).optional()
3113
- }).loose();
3114
- function resolveActConfig(options) {
3115
- if (options === void 0) return void 0;
3116
- ActOptionsSchema.parse(options);
3117
- return options;
3118
- }
3119
- var DEFAULT_AUTOCLOSE_CYCLE_MINUTES = 720;
3120
- var DEFAULT_CLOSE_BATCH_SIZE = 64;
3121
- var DEFAULT_CLOSE_YIELD_MS = 0;
3122
- var DEFAULT_AUTOCLOSE_WINDOW_TZ = "UTC";
3123
- function is_valid_time_zone(tz) {
3124
- try {
3125
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
3126
- return true;
3127
- } catch {
3128
- return false;
3129
- }
3130
- }
3131
- var AutocloseWindowSchema = import_zod6.z.object({
3132
- start: import_zod6.z.number().int().min(0).max(23, { message: "autocloseWindow.start must be an hour in [0, 23]" }),
3133
- end: import_zod6.z.number().int().min(0).max(23, { message: "autocloseWindow.end must be an hour in [0, 23]" }),
3134
- timeZone: import_zod6.z.string().refine(is_valid_time_zone, {
3135
- message: "autocloseWindow.timeZone must be a valid IANA time zone"
3136
- }).default(DEFAULT_AUTOCLOSE_WINDOW_TZ)
3137
- }).refine((w) => w.start !== w.end, {
3138
- message: "autocloseWindow.start and end must differ \u2014 an empty window disables autoclose"
3139
- });
3140
- var AutocloseOptionsSchema = import_zod6.z.object({
3141
- autocloseCycleMinutes: import_zod6.z.number().int().min(1).max(1440).default(DEFAULT_AUTOCLOSE_CYCLE_MINUTES),
3142
- closeBatchSize: import_zod6.z.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
3143
- closeYieldMs: import_zod6.z.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
3144
- closeOnError: import_zod6.z.boolean().default(false),
3145
- autocloseWindow: AutocloseWindowSchema.optional()
3146
- });
3147
- function resolveAutocloseConfig(options) {
3148
- return AutocloseOptionsSchema.parse({
3149
- autocloseCycleMinutes: options?.autocloseCycleMinutes,
3150
- closeBatchSize: options?.closeBatchSize,
3151
- closeYieldMs: options?.closeYieldMs,
3152
- closeOnError: options?.closeOnError,
3153
- autocloseWindow: options?.autocloseWindow
3154
- });
3155
- }
3156
- var DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5;
3157
- var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
3158
- var CircuitBreakerOptionsSchema = import_zod6.z.object({
3159
- failureThreshold: import_zod6.z.number().int().min(1).default(DEFAULT_CIRCUIT_FAILURE_THRESHOLD),
3160
- cooldownMs: import_zod6.z.number().int().min(100).max(36e5).default(DEFAULT_CIRCUIT_COOLDOWN_MS)
3161
- });
3162
- var resolveCircuitBreakerConfig = (options) => CircuitBreakerOptionsSchema.parse(options ?? {});
3163
- var DEFAULT_FOLD_FLUSH_EVERY = 1e3;
3164
- var DEFAULT_MAX_CACHED_STATES = 1e4;
3165
- var FoldOptionsSchema = import_zod6.z.object({
3166
- flushEvery: import_zod6.z.number().int().min(1).default(DEFAULT_FOLD_FLUSH_EVERY),
3167
- maxCachedStates: import_zod6.z.number().int().min(1).default(DEFAULT_MAX_CACHED_STATES)
3168
- });
3169
- function resolveFoldConfig(options) {
3170
- return FoldOptionsSchema.parse(options);
3171
- }
3172
-
3173
3157
  // src/internal/correlate-cycle.ts
3174
3158
  var import_node_crypto = require("crypto");
3175
3159
 
@@ -3247,7 +3231,6 @@ var CorrelateCycle = class {
3247
3231
  _armed = true;
3248
3232
  /** In-flight init, memoized for single-flight and cleared on failure. */
3249
3233
  _init_promise;
3250
- _timer = void 0;
3251
3234
  // Dynamically discovered targets → what each was last subscribed at,
3252
3235
  // bounded by `maxSubscribedStreams`. The static half lives in
3253
3236
  // `_static_subscriptions` below, which is deliberately not evictable.
@@ -3291,14 +3274,6 @@ var CorrelateCycle = class {
3291
3274
  * guarantee without a second gate on the Act side.
3292
3275
  */
3293
3276
  _on_init_async;
3294
- /**
3295
- * Scope runner (#1191). The periodic `start_polling` timer fires
3296
- * outside any caller frame, so its `correlate()` must be re-wrapped in
3297
- * the Act's `_scoped` bag or `store()`/`cache()` resolve to the
3298
- * singleton for a scoped Act. The orchestrator always threads its
3299
- * `_scoped` (identity for a non-scoped Act), so it's required.
3300
- */
3301
- _run_scoped;
3302
3277
  /**
3303
3278
  * Tail re-scan window applied to the cold-start checkpoint (ACT-1207).
3304
3279
  * See {@link DEFAULT_COLD_START_BACK_SCAN}. Constructor arg (not a
@@ -3319,7 +3294,6 @@ var CorrelateCycle = class {
3319
3294
  cd,
3320
3295
  max_subscribed_streams,
3321
3296
  declared_lanes,
3322
- run_scoped,
3323
3297
  on_init,
3324
3298
  on_init_async,
3325
3299
  cold_start_back_scan = DEFAULT_COLD_START_BACK_SCAN,
@@ -3333,7 +3307,6 @@ var CorrelateCycle = class {
3333
3307
  this._static_targets = static_targets;
3334
3308
  this._cd = cd;
3335
3309
  this._on_init = on_init;
3336
- this._run_scoped = run_scoped;
3337
3310
  this._on_init_async = on_init_async;
3338
3311
  this._cold_start_back_scan = cold_start_back_scan;
3339
3312
  }
@@ -3581,33 +3554,6 @@ var CorrelateCycle = class {
3581
3554
  this._armed = found === query.limit;
3582
3555
  return { subscribed: 0, last_id, marked: 0, scanned: true };
3583
3556
  }
3584
- /**
3585
- * Start a periodic correlation worker. Returns false if one is already
3586
- * running. Errors from `correlate()` are routed through `log()` so they
3587
- * land in the configured logger (the timer keeps running on failure).
3588
- */
3589
- start_polling(query = {}, frequency = 1e4, callback) {
3590
- if (this._timer) return false;
3591
- const limit = query.limit || 100;
3592
- this._timer = setInterval(
3593
- () => this._run_scoped(() => {
3594
- this.arm();
3595
- return this.correlate(
3596
- {
3597
- ...query,
3598
- after: this._checkpoint,
3599
- limit
3600
- },
3601
- true
3602
- );
3603
- }).then((result) => {
3604
- if (callback && result.subscribed) callback(result.subscribed);
3605
- }).catch((err) => log().error(err)),
3606
- frequency
3607
- );
3608
- return true;
3609
- }
3610
- /** Stop the periodic correlation worker. Idempotent. */
3611
3557
  /**
3612
3558
  * Hand the correlation lease back early.
3613
3559
  *
@@ -3638,12 +3584,6 @@ var CorrelateCycle = class {
3638
3584
  );
3639
3585
  }
3640
3586
  }
3641
- stop_polling() {
3642
- if (this._timer) {
3643
- clearInterval(this._timer);
3644
- this._timer = void 0;
3645
- }
3646
- }
3647
3587
  };
3648
3588
 
3649
3589
  // src/internal/correlator.ts
@@ -4741,9 +4681,9 @@ var DrainController = class {
4741
4681
  if (this._deps.breaker.state(Date.now()) === "open")
4742
4682
  return EMPTY_DRAIN;
4743
4683
  const d = this._deps.defaults ?? {};
4744
- const streamLimit = d.streamLimit ?? options.streamLimit ?? 10;
4745
- const eventLimit = d.eventLimit ?? options.eventLimit ?? 10;
4746
- const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? 1e4;
4684
+ const streamLimit = d.streamLimit ?? options.streamLimit ?? DEFAULT_STREAM_LIMIT;
4685
+ const eventLimit = d.eventLimit ?? options.eventLimit ?? DEFAULT_EVENT_LIMIT;
4686
+ const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? DEFAULT_LEASE_MILLIS;
4747
4687
  try {
4748
4688
  this._locked = true;
4749
4689
  this._inflight = new Promise((done) => {
@@ -5117,10 +5057,8 @@ function build_handle_batch(logger) {
5117
5057
  }
5118
5058
 
5119
5059
  // src/act.ts
5120
- var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
5121
5060
  var CLOSE_CATCH_UP_LIMIT = 1e3;
5122
5061
  var CLOSE_CATCH_UP_PASSES = 20;
5123
- var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
5124
5062
  function validate_only_lanes(options, lanes) {
5125
5063
  if (!options.onlyLanes || options.onlyLanes.length === 0) return;
5126
5064
  const declared = /* @__PURE__ */ new Set(["default", ...lanes.map((l) => l.name)]);
@@ -5146,6 +5084,10 @@ var Act = class {
5146
5084
  _correlate;
5147
5085
  /** Debounced correlate→drain catch-up loop. */
5148
5086
  _settle;
5087
+ /** Timer behind {@link start_correlations}. */
5088
+ _poll;
5089
+ /** The deprecated `start_correlations` callback, while the poll runs. */
5090
+ _on_discovered;
5149
5091
  /**
5150
5092
  * Disposer for the cross-process notify subscription, set up eagerly
5151
5093
  * during construction. Held as a promise because the subscription
@@ -5555,9 +5497,6 @@ var Act = class {
5555
5497
  on_init: () => {
5556
5498
  if (this._drain && this._reactive_events.size > 0) this._arm_all();
5557
5499
  },
5558
- // Re-scope the background `start_correlations` timer so its
5559
- // correlate resolves the scoped ports, not the singleton (#1191).
5560
- run_scoped: this._scoped,
5561
5500
  // Cold-start defer re-seed (#1221). Skipped on writer-only instances
5562
5501
  // (`drain: false`) — they run no local controllers to re-arm.
5563
5502
  on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
@@ -6147,86 +6086,63 @@ var Act = class {
6147
6086
  const { subscribed, last_id, marked, scanned } = await this._correlate.correlate(query, lease);
6148
6087
  if ((subscribed > 0 || marked > 0) && this._reactive_events.size > 0)
6149
6088
  this._arm_all();
6089
+ if (subscribed > 0) this._on_discovered?.(subscribed);
6150
6090
  return { subscribed, last_id, scanned };
6151
6091
  });
6152
6092
  }
6153
6093
  /**
6154
- * Starts automatic periodic correlation worker for discovering new streams.
6155
- *
6156
- * The correlation worker runs in the background, scanning for new events and identifying
6157
- * new target streams based on reaction resolvers. It maintains a sliding window that
6158
- * advances with each scan, ensuring all events are eventually correlated.
6159
- *
6160
- * This is useful for dynamic stream creation patterns where you don't know all streams
6161
- * upfront - they're discovered as events arrive.
6094
+ * Starts a background poll that settles on a timer: each tick looks for
6095
+ * new events, subscribes the streams their reactions target, and drains
6096
+ * them.
6097
+ *
6098
+ * Use it when this process can miss commits — another process writes to a
6099
+ * store without `notify`, or a notification is lost. It is the safety net
6100
+ * under `notify`: a missed wakeup costs at most one `frequency` of latency.
6101
+ *
6102
+ * Only one poll runs per Act instance. On a writer-only instance
6103
+ * (`drain: false`) each tick is a no-op, like `settle()`.
6104
+ *
6105
+ * @param query - Only `limit` is used: the most events each correlate pass
6106
+ * reads (default 100). The scan always starts at the correlation
6107
+ * checkpoint, so `after` is ignored.
6108
+ * @param frequency - Milliseconds between ticks (default: 10000)
6109
+ * @param callback - Called with the number of newly subscribed streams
6110
+ * after each correlate pass that found some.
6111
+ * @deprecated Listen to the `"settled"` lifecycle event instead.
6112
+ * @returns `true` if the poll started, `false` if one is already running
6162
6113
  *
6163
- * **Note:** Only one correlation worker can run at a time per Act instance.
6164
- *
6165
- * @param query - Query filter for correlation scans — see {@link Query}
6166
- * (typically `{ after: -1, limit: 100 }`)
6167
- * @param frequency - Correlation frequency in milliseconds (default: 10000)
6168
- * @param callback - Optional callback invoked with newly discovered streams
6169
- * @returns `true` if worker started, `false` if already running
6170
- *
6171
- * @example Start automatic correlation
6114
+ * @example
6172
6115
  * ```typescript
6173
- * // Start correlation worker scanning every 5 seconds
6174
- * app.start_correlations(
6175
- * { after: 0, limit: 100 },
6176
- * 5000,
6177
- * (leased) => {
6178
- * console.log(`Discovered ${leased.length} new streams`);
6179
- * }
6180
- * );
6181
- *
6182
- * // Later, stop it
6116
+ * app.start_correlations({ limit: 100 }, 5000);
6117
+ * // later
6183
6118
  * app.stop_correlations();
6184
6119
  * ```
6185
6120
  *
6186
- * @example With checkpoint persistence
6187
- * ```typescript
6188
- * // Load last checkpoint
6189
- * const lastId = await loadCheckpoint();
6190
- *
6191
- * app.start_correlations(
6192
- * { after: lastId, limit: 100 },
6193
- * 10000,
6194
- * async (leased) => {
6195
- * // Save checkpoint for next restart
6196
- * if (leased.length) {
6197
- * const maxId = Math.max(...leased.map(l => l.at));
6198
- * await saveCheckpoint(maxId);
6199
- * }
6200
- * }
6201
- * );
6202
- * ```
6203
- *
6204
- * @see {@link correlate} for manual one-time correlation
6205
- * @see {@link stop_correlations} to stop the worker
6121
+ * @see {@link settle} for what each tick runs
6122
+ * @see {@link stop_correlations} to stop the poll
6206
6123
  */
6207
6124
  start_correlations(query = {}, frequency = 1e4, callback) {
6208
- const started = this._correlate.start_polling(query, frequency, callback);
6209
- return started;
6125
+ if (this._poll) return false;
6126
+ this._on_discovered = callback;
6127
+ const correlate = { limit: query.limit || 100 };
6128
+ this._poll = setInterval(() => {
6129
+ this._correlate.arm();
6130
+ this.settle({ debounceMs: 0, correlate });
6131
+ }, frequency);
6132
+ return true;
6210
6133
  }
6211
6134
  /**
6212
- * Stops the automatic correlation worker.
6213
- *
6214
- * Call this to stop the background correlation worker started by {@link start_correlations}.
6215
- * This is automatically called when the Act instance is disposed.
6216
- *
6217
- * @example
6218
- * ```typescript
6219
- * // Start correlation
6220
- * app.start_correlations();
6221
- *
6222
- * // Later, stop it
6223
- * app.stop_correlations();
6224
- * ```
6135
+ * Stops the poll started by {@link start_correlations}. Idempotent, and
6136
+ * called by `shutdown()`.
6225
6137
  *
6226
6138
  * @see {@link start_correlations}
6227
6139
  */
6228
6140
  stop_correlations() {
6229
- this._correlate.stop_polling();
6141
+ if (this._poll) {
6142
+ clearInterval(this._poll);
6143
+ this._poll = void 0;
6144
+ }
6145
+ this._on_discovered = void 0;
6230
6146
  void this._scoped(() => this._correlate.release_correlation());
6231
6147
  }
6232
6148
  /**
@@ -6883,8 +6799,8 @@ function reaction_on(event, events, builder) {
6883
6799
  // `maxRetries`/`backoff`/`blockOnError` throws ZodError at build,
6884
6800
  // not a NaN gate on the first retry.
6885
6801
  options: resolveReactionConfig({
6886
- blockOnError: options?.blockOnError ?? true,
6887
- maxRetries: options?.maxRetries ?? 3,
6802
+ blockOnError: options?.blockOnError ?? DEFAULT_BLOCK_ON_ERROR,
6803
+ maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES,
6888
6804
  backoff: options?.backoff
6889
6805
  })
6890
6806
  };
@@ -7275,8 +7191,8 @@ function _projection(target) {
7275
7191
  handler,
7276
7192
  resolver: default_resolver ?? _this_,
7277
7193
  options: {
7278
- blockOnError: true,
7279
- maxRetries: 3
7194
+ blockOnError: DEFAULT_BLOCK_ON_ERROR,
7195
+ maxRetries: DEFAULT_MAX_RETRIES
7280
7196
  }
7281
7197
  };
7282
7198
  const register = events[event];
@@ -7349,7 +7265,10 @@ function _projection(target) {
7349
7265
  {
7350
7266
  handler: noop2,
7351
7267
  resolver: { target },
7352
- options: { blockOnError: true, maxRetries: 3 }
7268
+ options: {
7269
+ blockOnError: DEFAULT_BLOCK_ON_ERROR,
7270
+ maxRetries: DEFAULT_MAX_RETRIES
7271
+ }
7353
7272
  }
7354
7273
  ]
7355
7274
  ])