@rotorsoft/act 1.32.26 → 1.32.28
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/README.md +2 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +7 -152
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +0 -21
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts +0 -25
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/build-classify.d.ts +4 -4
- package/dist/@types/builders/build-classify.d.ts.map +1 -1
- package/dist/@types/builders/projection-builder.d.ts.map +1 -1
- package/dist/@types/builders/reaction-builder.d.ts +3 -3
- package/dist/@types/builders/reaction-builder.d.ts.map +1 -1
- package/dist/@types/builders/slice-builder.d.ts +1 -12
- package/dist/@types/builders/slice-builder.d.ts.map +1 -1
- package/dist/@types/builders/state-builder.d.ts +0 -189
- package/dist/@types/builders/state-builder.d.ts.map +1 -1
- package/dist/@types/config.d.ts +0 -27
- package/dist/@types/config.d.ts.map +1 -1
- package/dist/@types/internal/circuit-breaker.d.ts +2 -0
- package/dist/@types/internal/circuit-breaker.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +2 -1
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts +13 -4
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +1 -1
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/settle.d.ts +2 -1
- package/dist/@types/internal/settle.d.ts.map +1 -1
- package/dist/@types/ports.d.ts +0 -7
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/test/sandbox.d.ts +0 -14
- package/dist/@types/test/sandbox.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +0 -27
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/errors.d.ts +1 -92
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +0 -70
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +1 -34
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/{chunk-QXW3JV2H.js → chunk-5FXT63KF.js} +2 -2
- package/dist/chunk-5FXT63KF.js.map +1 -0
- package/dist/{chunk-XSG2NW4J.js → chunk-F7K7SIG6.js} +1 -1
- package/dist/chunk-F7K7SIG6.js.map +1 -0
- package/dist/index.cjs +83 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +85 -217
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +2 -2
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +1 -4
- package/dist/chunk-QXW3JV2H.js.map +0 -1
- package/dist/chunk-XSG2NW4J.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
sleep,
|
|
27
27
|
store,
|
|
28
28
|
validate
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-5FXT63KF.js";
|
|
30
30
|
import {
|
|
31
31
|
ActorSchema,
|
|
32
32
|
CausationEventSchema,
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
pii_split,
|
|
54
54
|
pii_strip,
|
|
55
55
|
sensitive
|
|
56
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-F7K7SIG6.js";
|
|
57
57
|
|
|
58
58
|
// src/signals.ts
|
|
59
59
|
process.once("SIGINT", async (arg) => {
|
|
@@ -81,10 +81,8 @@ var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
|
81
81
|
function classify_registry(registry, states) {
|
|
82
82
|
const statics = /* @__PURE__ */ new Map();
|
|
83
83
|
const target_lanes = /* @__PURE__ */ new Map();
|
|
84
|
-
const reactive_events = /* @__PURE__ */ new Set();
|
|
85
84
|
const event_to_lanes = /* @__PURE__ */ new Map();
|
|
86
85
|
for (const [name, register] of Object.entries(registry.events)) {
|
|
87
|
-
if (register.reactions.size > 0) reactive_events.add(name);
|
|
88
86
|
for (const reaction of register.reactions.values()) {
|
|
89
87
|
if (typeof reaction.resolver === "function") {
|
|
90
88
|
event_to_lanes.set(name, ALL_LANES);
|
|
@@ -122,7 +120,6 @@ function classify_registry(registry, states) {
|
|
|
122
120
|
}
|
|
123
121
|
return {
|
|
124
122
|
static_targets: [...statics.values()],
|
|
125
|
-
reactive_events,
|
|
126
123
|
event_to_state,
|
|
127
124
|
event_to_lanes
|
|
128
125
|
};
|
|
@@ -1135,6 +1132,10 @@ var CircuitBreaker = class {
|
|
|
1135
1132
|
get failing() {
|
|
1136
1133
|
return this._failures > 0;
|
|
1137
1134
|
}
|
|
1135
|
+
/** How long the store is left alone after a failure before a retry. */
|
|
1136
|
+
get cooldown_ms() {
|
|
1137
|
+
return this._cooldown_ms;
|
|
1138
|
+
}
|
|
1138
1139
|
/** Current state given the wall-clock `now`. */
|
|
1139
1140
|
state(now) {
|
|
1140
1141
|
if (this._opened_at === void 0) return "closed";
|
|
@@ -1279,7 +1280,7 @@ async function run_full_closes(full, target_map, deps, skipped) {
|
|
|
1279
1280
|
}
|
|
1280
1281
|
const safe = await partition_by_safety(
|
|
1281
1282
|
stream_info,
|
|
1282
|
-
deps.
|
|
1283
|
+
deps.reactive,
|
|
1283
1284
|
skipped,
|
|
1284
1285
|
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
|
|
1285
1286
|
deps.catch_up_correlation
|
|
@@ -1316,8 +1317,8 @@ async function run_full_closes(full, target_map, deps, skipped) {
|
|
|
1316
1317
|
);
|
|
1317
1318
|
}
|
|
1318
1319
|
async function run_windowed_closes(windowed, deps, skipped) {
|
|
1319
|
-
const checkpoint = deps.
|
|
1320
|
-
const min_at = deps.
|
|
1320
|
+
const checkpoint = deps.reactive ? await deps.catch_up_correlation(Number.MAX_SAFE_INTEGER) : -1;
|
|
1321
|
+
const min_at = deps.reactive ? await probe_min_watermarks(
|
|
1321
1322
|
windowed.map((t) => t.stream),
|
|
1322
1323
|
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
|
|
1323
1324
|
checkpoint
|
|
@@ -1405,8 +1406,8 @@ async function scan_stream_heads(streams) {
|
|
|
1405
1406
|
}
|
|
1406
1407
|
return out;
|
|
1407
1408
|
}
|
|
1408
|
-
async function partition_by_safety(stream_info,
|
|
1409
|
-
if (
|
|
1409
|
+
async function partition_by_safety(stream_info, reactive, skipped, page_size, catch_up_correlation2) {
|
|
1410
|
+
if (!reactive) return [...stream_info.keys()];
|
|
1410
1411
|
let needed = -1;
|
|
1411
1412
|
for (const info of stream_info.values())
|
|
1412
1413
|
needed = Math.max(needed, info.max_id);
|
|
@@ -2685,20 +2686,24 @@ function fetch_failure_policy(registry) {
|
|
|
2685
2686
|
}
|
|
2686
2687
|
return { blockOnError, maxRetries };
|
|
2687
2688
|
}
|
|
2688
|
-
function
|
|
2689
|
-
|
|
2690
|
-
|
|
2689
|
+
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
2690
|
+
if (!error) return { lease, handled, acked_at: at };
|
|
2691
|
+
logger.error(error);
|
|
2692
|
+
const non_retryable = error instanceof NonRetryableError;
|
|
2693
|
+
const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
|
|
2694
|
+
if (block2)
|
|
2695
|
+
logger.error(
|
|
2696
|
+
non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
2697
|
+
);
|
|
2698
|
+
const next_attempt_at = block2 ? void 0 : Date.now() + compute_backoff_delay(lease.retry, options.backoff ?? DEFAULT_BACKOFF);
|
|
2691
2699
|
return {
|
|
2692
2700
|
lease,
|
|
2693
|
-
handled
|
|
2694
|
-
acked_at:
|
|
2695
|
-
error,
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
options.backoff ?? DEFAULT_BACKOFF
|
|
2700
|
-
)
|
|
2701
|
-
}
|
|
2701
|
+
handled,
|
|
2702
|
+
acked_at: at,
|
|
2703
|
+
error: error.message,
|
|
2704
|
+
block: block2,
|
|
2705
|
+
next_attempt_at,
|
|
2706
|
+
failed_at
|
|
2702
2707
|
};
|
|
2703
2708
|
}
|
|
2704
2709
|
function warn_misrouted(seen, stream, handler, event) {
|
|
@@ -2746,7 +2751,9 @@ async function run_drain_cycle(deps, cycle) {
|
|
|
2746
2751
|
resolved = dynamic ? resolver(event) : resolver;
|
|
2747
2752
|
} catch (error) {
|
|
2748
2753
|
failed ??= {
|
|
2749
|
-
error:
|
|
2754
|
+
error: new Error(
|
|
2755
|
+
`Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`
|
|
2756
|
+
),
|
|
2750
2757
|
options: reaction.options
|
|
2751
2758
|
};
|
|
2752
2759
|
return false;
|
|
@@ -2768,23 +2775,29 @@ async function run_drain_cycle(deps, cycle) {
|
|
|
2768
2775
|
leased.map((lease) => {
|
|
2769
2776
|
const entry = fetch_map.get(lease.stream);
|
|
2770
2777
|
if (entry.fetch.error !== void 0) {
|
|
2771
|
-
const error = `Fetch failed for ${lease.stream}: ${entry.fetch.error}`;
|
|
2772
|
-
log().error(error);
|
|
2773
2778
|
const policy = fetch_failure_policy(registry);
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
return Promise.resolve(failed);
|
|
2779
|
+
return Promise.resolve(
|
|
2780
|
+
finalize(
|
|
2781
|
+
lease,
|
|
2782
|
+
0,
|
|
2783
|
+
lease.at,
|
|
2784
|
+
new Error(`Fetch failed for ${lease.stream}: ${entry.fetch.error}`),
|
|
2785
|
+
store_failing ? { ...policy, blockOnError: false } : policy,
|
|
2786
|
+
deps.logger
|
|
2787
|
+
)
|
|
2788
|
+
);
|
|
2785
2789
|
}
|
|
2786
2790
|
if (entry.failed)
|
|
2787
|
-
return Promise.resolve(
|
|
2791
|
+
return Promise.resolve(
|
|
2792
|
+
finalize(
|
|
2793
|
+
lease,
|
|
2794
|
+
0,
|
|
2795
|
+
lease.at,
|
|
2796
|
+
entry.failed.error,
|
|
2797
|
+
entry.failed.options,
|
|
2798
|
+
deps.logger
|
|
2799
|
+
)
|
|
2800
|
+
);
|
|
2788
2801
|
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
2789
2802
|
const { payloads } = entry;
|
|
2790
2803
|
const exhausted = budget_exhausted(
|
|
@@ -2823,18 +2836,23 @@ var EMPTY_DRAIN = {
|
|
|
2823
2836
|
acked: [],
|
|
2824
2837
|
blocked: []
|
|
2825
2838
|
};
|
|
2839
|
+
var RETRY = "";
|
|
2826
2840
|
var DrainController = class {
|
|
2827
2841
|
_armed = false;
|
|
2828
2842
|
_locked = false;
|
|
2829
2843
|
_ratio = 0.5;
|
|
2830
2844
|
/**
|
|
2831
|
-
* Local wake at the earliest parked (backoff or defer) visit
|
|
2832
|
-
*
|
|
2833
|
-
*
|
|
2845
|
+
* Local wake at the earliest parked (backoff or defer) visit, or at the
|
|
2846
|
+
* lease expiry after a failed pass (the {@link RETRY} key); the schedule
|
|
2847
|
+
* itself is persisted. On an idle app nothing else would drain it. It
|
|
2848
|
+
* drains until a pass leases nothing, and an entry stays until handled or
|
|
2849
|
+
* until a later pass claims nothing.
|
|
2834
2850
|
*/
|
|
2835
2851
|
_defer = new DeferTimer(() => {
|
|
2836
2852
|
this._armed = true;
|
|
2837
|
-
void this._deps.run_scoped(() =>
|
|
2853
|
+
void this._deps.run_scoped(async () => {
|
|
2854
|
+
while ((await this.drain()).leased.length > 0) ;
|
|
2855
|
+
});
|
|
2838
2856
|
});
|
|
2839
2857
|
_worker;
|
|
2840
2858
|
_misrouted = /* @__PURE__ */ new Set();
|
|
@@ -2931,10 +2949,12 @@ var DrainController = class {
|
|
|
2931
2949
|
if (acked.length) this._deps.on_acked(acked);
|
|
2932
2950
|
if (closeable.length) await this._deps.on_close(closeable);
|
|
2933
2951
|
this._deps.breaker.passed();
|
|
2934
|
-
const has_errors = handled.some(({ error }) => error);
|
|
2935
|
-
if (!acked.length && !blocked.length && !has_errors) this._armed = false;
|
|
2936
2952
|
return { fetched, leased, acked, blocked };
|
|
2937
2953
|
} catch (error) {
|
|
2954
|
+
this._defer.set(
|
|
2955
|
+
RETRY,
|
|
2956
|
+
Date.now() + Math.max(leaseMillis, this._deps.breaker.cooldown_ms)
|
|
2957
|
+
);
|
|
2938
2958
|
this._deps.breaker.failed(Date.now(), error);
|
|
2939
2959
|
return EMPTY_DRAIN;
|
|
2940
2960
|
} finally {
|
|
@@ -3058,7 +3078,8 @@ var SettleLoop = class {
|
|
|
3058
3078
|
/**
|
|
3059
3079
|
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
3060
3080
|
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
3061
|
-
* until no progress is made (no new subscriptions, no
|
|
3081
|
+
* until no progress is made (no new subscriptions, no events read, no
|
|
3082
|
+
* streams leased)
|
|
3062
3083
|
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
3063
3084
|
* via {@link SettleDeps.on_settled}.
|
|
3064
3085
|
*/
|
|
@@ -3098,7 +3119,7 @@ var SettleLoop = class {
|
|
|
3098
3119
|
acked: [...settled_drain.acked, ...drain.acked],
|
|
3099
3120
|
blocked: [...settled_drain.blocked, ...drain.blocked]
|
|
3100
3121
|
} : drain;
|
|
3101
|
-
const made_progress = subscribed > 0 || drain.
|
|
3122
|
+
const made_progress = subscribed > 0 || drain.leased.length > 0 || last_id > after_before;
|
|
3102
3123
|
if (!made_progress) break;
|
|
3103
3124
|
}
|
|
3104
3125
|
if (settled_drain) this._deps.on_settled(settled_drain);
|
|
@@ -3135,26 +3156,6 @@ var SettleLoop = class {
|
|
|
3135
3156
|
};
|
|
3136
3157
|
|
|
3137
3158
|
// src/builders/reaction-builder.ts
|
|
3138
|
-
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
3139
|
-
if (!error) return { lease, handled, acked_at: at };
|
|
3140
|
-
logger.error(error);
|
|
3141
|
-
const non_retryable = error instanceof NonRetryableError;
|
|
3142
|
-
const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
|
|
3143
|
-
if (block2)
|
|
3144
|
-
logger.error(
|
|
3145
|
-
non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
3146
|
-
);
|
|
3147
|
-
const next_attempt_at = block2 ? void 0 : Date.now() + compute_backoff_delay(lease.retry, options.backoff ?? DEFAULT_BACKOFF);
|
|
3148
|
-
return {
|
|
3149
|
-
lease,
|
|
3150
|
-
handled,
|
|
3151
|
-
acked_at: at,
|
|
3152
|
-
error: error.message,
|
|
3153
|
-
block: block2,
|
|
3154
|
-
next_attempt_at,
|
|
3155
|
-
failed_at
|
|
3156
|
-
};
|
|
3157
|
-
}
|
|
3158
3159
|
function build_handle(deps) {
|
|
3159
3160
|
const { logger, reaction_scope } = deps;
|
|
3160
3161
|
return async (lease, payloads) => {
|
|
@@ -3264,7 +3265,8 @@ var Act = class {
|
|
|
3264
3265
|
_breaker;
|
|
3265
3266
|
_listen;
|
|
3266
3267
|
_drain;
|
|
3267
|
-
|
|
3268
|
+
/** Whether any event has a reaction; nothing arms without one. */
|
|
3269
|
+
_reactive;
|
|
3268
3270
|
_drain_controllers;
|
|
3269
3271
|
_correlate;
|
|
3270
3272
|
_settle;
|
|
@@ -3394,7 +3396,7 @@ var Act = class {
|
|
|
3394
3396
|
});
|
|
3395
3397
|
this._handle_batch = build_handle_batch(this._logger);
|
|
3396
3398
|
const classification = classify_registry(this.registry, this._states);
|
|
3397
|
-
this.
|
|
3399
|
+
this._reactive = classification.event_to_lanes.size > 0;
|
|
3398
3400
|
this._event_to_state = classification.event_to_state;
|
|
3399
3401
|
this._event_to_lanes = classification.event_to_lanes;
|
|
3400
3402
|
this._listen = options.listen !== false;
|
|
@@ -3501,9 +3503,8 @@ var Act = class {
|
|
|
3501
3503
|
declared_lanes: new Set(this._declared_lanes),
|
|
3502
3504
|
// Built after the controllers, so their keys are the active lanes.
|
|
3503
3505
|
shard: this._drain_controllers.size < this._declared_lanes.length ? [...this._drain_controllers.keys()] : void 0,
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
},
|
|
3506
|
+
// Only reached on a draining app: writer-only ones never correlate.
|
|
3507
|
+
on_init: () => this._arm_all(),
|
|
3507
3508
|
on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
|
|
3508
3509
|
});
|
|
3509
3510
|
}
|
|
@@ -3600,7 +3601,7 @@ var Act = class {
|
|
|
3600
3601
|
}
|
|
3601
3602
|
/** Failures are logged, never thrown: `notify` is a hint. */
|
|
3602
3603
|
async _wire_notify(s) {
|
|
3603
|
-
if (this.
|
|
3604
|
+
if (!this._reactive) return void 0;
|
|
3604
3605
|
if (!s.notify) return void 0;
|
|
3605
3606
|
if (!this._listen) return void 0;
|
|
3606
3607
|
try {
|
|
@@ -3660,46 +3661,6 @@ var Act = class {
|
|
|
3660
3661
|
* console.log(snapshots[0].state.count); // Current count after increment
|
|
3661
3662
|
* ```
|
|
3662
3663
|
*
|
|
3663
|
-
* @example With error handling
|
|
3664
|
-
* ```typescript
|
|
3665
|
-
* try {
|
|
3666
|
-
* await app.do(
|
|
3667
|
-
* "withdraw",
|
|
3668
|
-
* { stream: "account-123", actor: { id: "user1", name: "Alice" } },
|
|
3669
|
-
* { amount: 1000 }
|
|
3670
|
-
* );
|
|
3671
|
-
* } catch (error) {
|
|
3672
|
-
* if (error instanceof InvariantError) {
|
|
3673
|
-
* console.error("Business rule violated:", error.description);
|
|
3674
|
-
* } else if (error instanceof ConcurrencyError) {
|
|
3675
|
-
* console.error("Concurrent modification detected, retry...");
|
|
3676
|
-
* } else if (error instanceof ValidationError) {
|
|
3677
|
-
* console.error("Invalid payload:", error.details);
|
|
3678
|
-
* }
|
|
3679
|
-
* }
|
|
3680
|
-
* ```
|
|
3681
|
-
*
|
|
3682
|
-
* @example Reaction triggering another action (reactingTo auto-injected)
|
|
3683
|
-
* ```typescript
|
|
3684
|
-
* const app = act()
|
|
3685
|
-
* .withState(Order)
|
|
3686
|
-
* .withState(Inventory)
|
|
3687
|
-
* .on("OrderPlaced")
|
|
3688
|
-
* .do(async function reduceInventory(event, _stream, app) {
|
|
3689
|
-
* // Inside reaction handlers, reactingTo is auto-injected when omitted.
|
|
3690
|
-
* // The triggering event is used by default, maintaining the correlation chain.
|
|
3691
|
-
* await app.do(
|
|
3692
|
-
* "reduceStock",
|
|
3693
|
-
* { stream: "inventory-1", actor: { id: "sys", name: "system" } },
|
|
3694
|
-
* { amount: event.data.items.length }
|
|
3695
|
-
* );
|
|
3696
|
-
* // To use a different correlation, pass reactingTo explicitly:
|
|
3697
|
-
* // await app.do("reduceStock", target, payload, { reactingTo: customEvent });
|
|
3698
|
-
* })
|
|
3699
|
-
* .to("inventory-1")
|
|
3700
|
-
* .build();
|
|
3701
|
-
* ```
|
|
3702
|
-
*
|
|
3703
3664
|
* @see {@link Target} for target structure
|
|
3704
3665
|
* @see {@link Snapshot} for return value structure
|
|
3705
3666
|
* @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
|
|
@@ -3715,10 +3676,9 @@ var Act = class {
|
|
|
3715
3676
|
payload,
|
|
3716
3677
|
do_options
|
|
3717
3678
|
);
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
);
|
|
3679
|
+
this._arm_for_event_names(
|
|
3680
|
+
snapshots.map((s) => s.event.name)
|
|
3681
|
+
);
|
|
3722
3682
|
this.emit("committed", snapshots);
|
|
3723
3683
|
return snapshots;
|
|
3724
3684
|
});
|
|
@@ -3765,26 +3725,6 @@ var Act = class {
|
|
|
3765
3725
|
* console.log(`Found ${count} events from ${first?.id} to ${last?.id}`);
|
|
3766
3726
|
* ```
|
|
3767
3727
|
*
|
|
3768
|
-
* @example Query specific event types
|
|
3769
|
-
* ```typescript
|
|
3770
|
-
* const { count } = await app.query(
|
|
3771
|
-
* { name: "UserCreated", limit: 100 },
|
|
3772
|
-
* (event) => {
|
|
3773
|
-
* console.log("User created:", event.data.email);
|
|
3774
|
-
* }
|
|
3775
|
-
* );
|
|
3776
|
-
* ```
|
|
3777
|
-
*
|
|
3778
|
-
* @example Query events in time range
|
|
3779
|
-
* ```typescript
|
|
3780
|
-
* const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
3781
|
-
* const { count } = await app.query({
|
|
3782
|
-
* created_after: yesterday,
|
|
3783
|
-
* stream: "user-123"
|
|
3784
|
-
* });
|
|
3785
|
-
* console.log(`User had ${count} events in last 24 hours`);
|
|
3786
|
-
* ```
|
|
3787
|
-
*
|
|
3788
3728
|
* @see {@link query_array} for loading events into memory
|
|
3789
3729
|
*/
|
|
3790
3730
|
async query(query, callback) {
|
|
@@ -3816,14 +3756,6 @@ var Act = class {
|
|
|
3816
3756
|
* events.forEach(event => console.log(event.name, event.data));
|
|
3817
3757
|
* ```
|
|
3818
3758
|
*
|
|
3819
|
-
* @example Get recent events
|
|
3820
|
-
* ```typescript
|
|
3821
|
-
* const recent = await app.query_array({
|
|
3822
|
-
* stream: "user-123",
|
|
3823
|
-
* limit: 10
|
|
3824
|
-
* });
|
|
3825
|
-
* ```
|
|
3826
|
-
*
|
|
3827
3759
|
* @see {@link query} for large result sets
|
|
3828
3760
|
*/
|
|
3829
3761
|
async query_array(query) {
|
|
@@ -3894,12 +3826,6 @@ var Act = class {
|
|
|
3894
3826
|
* await app.drain();
|
|
3895
3827
|
* ```
|
|
3896
3828
|
*
|
|
3897
|
-
* @example In production, prefer settle()
|
|
3898
|
-
* ```typescript
|
|
3899
|
-
* await app.do("CreateItem", target, input);
|
|
3900
|
-
* app.settle(); // debounced correlate→drain, emits "settled"
|
|
3901
|
-
* ```
|
|
3902
|
-
*
|
|
3903
3829
|
* @see {@link settle} for debounced correlate→drain with lifecycle events
|
|
3904
3830
|
* @see {@link correlate} for dynamic stream discovery
|
|
3905
3831
|
* @see {@link start_correlations} for automatic correlation
|
|
@@ -3911,6 +3837,7 @@ var Act = class {
|
|
|
3911
3837
|
return this._scoped(() => this._drain_all(options));
|
|
3912
3838
|
}
|
|
3913
3839
|
_arm_all() {
|
|
3840
|
+
if (!this._reactive) return;
|
|
3914
3841
|
this._correlate.arm();
|
|
3915
3842
|
for (const c of this._drain_controllers.values()) c.arm();
|
|
3916
3843
|
}
|
|
@@ -3966,37 +3893,20 @@ var Act = class {
|
|
|
3966
3893
|
* on reaction resolvers. It then registers these streams so they'll be picked up by
|
|
3967
3894
|
* the next drain cycle.
|
|
3968
3895
|
*
|
|
3969
|
-
* @param query - Query filter to scan for new correlations
|
|
3970
3896
|
* @param query - Scan filter — see {@link Query} for fields (typically
|
|
3971
3897
|
* `{ after: <event-id>, limit: <count> }`)
|
|
3972
|
-
* @returns
|
|
3898
|
+
* @returns The number of newly subscribed streams and the last scanned event id
|
|
3973
3899
|
*
|
|
3974
3900
|
* @example Manual correlation
|
|
3975
3901
|
* ```typescript
|
|
3976
3902
|
* // Scan for new streams
|
|
3977
|
-
* const {
|
|
3978
|
-
* console.log(`
|
|
3903
|
+
* const { subscribed, last_id } = await app.correlate({ after: 0, limit: 100 });
|
|
3904
|
+
* console.log(`Subscribed ${subscribed} new streams`);
|
|
3979
3905
|
*
|
|
3980
3906
|
* // Save last_id for next scan
|
|
3981
3907
|
* await saveCheckpoint(last_id);
|
|
3982
3908
|
* ```
|
|
3983
3909
|
*
|
|
3984
|
-
* @example Dynamic stream creation
|
|
3985
|
-
* ```typescript
|
|
3986
|
-
* const app = act()
|
|
3987
|
-
* .withState(User)
|
|
3988
|
-
* .withState(UserStats)
|
|
3989
|
-
* .on("UserLoggedIn")
|
|
3990
|
-
* .do(async (event) => ["incrementLoginCount", {}])
|
|
3991
|
-
* .to((event) => ({
|
|
3992
|
-
* target: `stats-${event.stream}` // Dynamic target per user
|
|
3993
|
-
* }))
|
|
3994
|
-
* .build();
|
|
3995
|
-
*
|
|
3996
|
-
* // Discover stats streams as users log in
|
|
3997
|
-
* await app.correlate();
|
|
3998
|
-
* ```
|
|
3999
|
-
*
|
|
4000
3910
|
* @see {@link start_correlations} for automatic periodic correlation
|
|
4001
3911
|
* @see {@link stop_correlations} to stop automatic correlation
|
|
4002
3912
|
*/
|
|
@@ -4009,8 +3919,7 @@ var Act = class {
|
|
|
4009
3919
|
if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
|
|
4010
3920
|
return this._scoped(async () => {
|
|
4011
3921
|
const { subscribed, last_id, marked, scanned } = await this._correlate.correlate(query, lease);
|
|
4012
|
-
if (
|
|
4013
|
-
this._arm_all();
|
|
3922
|
+
if (subscribed > 0 || marked > 0) this._arm_all();
|
|
4014
3923
|
if (subscribed > 0) this._on_discovered?.(subscribed);
|
|
4015
3924
|
return { subscribed, last_id, scanned };
|
|
4016
3925
|
});
|
|
@@ -4102,12 +4011,6 @@ var Act = class {
|
|
|
4102
4011
|
* app.settle({ eventLimit: 1000 }); // emits "settled" when fully replayed
|
|
4103
4012
|
* ```
|
|
4104
4013
|
*
|
|
4105
|
-
* @example Rebuild a projection (tests / scripts)
|
|
4106
|
-
* ```typescript
|
|
4107
|
-
* await app.reset(["my-projection"]);
|
|
4108
|
-
* await app.drain({ eventLimit: 1000 }); // small streams: one pass is enough
|
|
4109
|
-
* ```
|
|
4110
|
-
*
|
|
4111
4014
|
* @see {@link Store.reset} for the underlying store primitive
|
|
4112
4015
|
* @see {@link settle} for the debounced full-catch-up loop
|
|
4113
4016
|
*/
|
|
@@ -4116,7 +4019,7 @@ var Act = class {
|
|
|
4116
4019
|
const count = await store().reset(input);
|
|
4117
4020
|
for (const handler of this._batch_handlers.values())
|
|
4118
4021
|
handler[FOLD_RESET]?.();
|
|
4119
|
-
if (count > 0
|
|
4022
|
+
if (count > 0) this._arm_all();
|
|
4120
4023
|
return count;
|
|
4121
4024
|
});
|
|
4122
4025
|
}
|
|
@@ -4150,7 +4053,7 @@ var Act = class {
|
|
|
4150
4053
|
async unblock(input) {
|
|
4151
4054
|
return this._scoped(async () => {
|
|
4152
4055
|
const count = await store().unblock(input);
|
|
4153
|
-
if (count > 0
|
|
4056
|
+
if (count > 0) this._arm_all();
|
|
4154
4057
|
return count;
|
|
4155
4058
|
});
|
|
4156
4059
|
}
|
|
@@ -4186,11 +4089,6 @@ var Act = class {
|
|
|
4186
4089
|
* await cache().clear(); // operator's responsibility
|
|
4187
4090
|
* ```
|
|
4188
4091
|
*
|
|
4189
|
-
* @example Copy one store into another
|
|
4190
|
-
* ```typescript
|
|
4191
|
-
* await app.restore(pg, {}, sqlite);
|
|
4192
|
-
* ```
|
|
4193
|
-
*
|
|
4194
4092
|
* @see {@link Store.restore} for the underlying driver-pattern primitive.
|
|
4195
4093
|
*/
|
|
4196
4094
|
async restore(source, opts = {}, sink) {
|
|
@@ -4329,16 +4227,6 @@ var Act = class {
|
|
|
4329
4227
|
* await app.prioritize({ stream: "^proj-orders$", stream_exact: false }, 10);
|
|
4330
4228
|
* ```
|
|
4331
4229
|
*
|
|
4332
|
-
* @example Drop all audit projections to background
|
|
4333
|
-
* ```typescript
|
|
4334
|
-
* await app.prioritize({ source: "^audit-" }, -5);
|
|
4335
|
-
* ```
|
|
4336
|
-
*
|
|
4337
|
-
* @example Reset everyone to default
|
|
4338
|
-
* ```typescript
|
|
4339
|
-
* await app.prioritize({}, 0);
|
|
4340
|
-
* ```
|
|
4341
|
-
*
|
|
4342
4230
|
* @see {@link Store.prioritize} for the underlying primitive
|
|
4343
4231
|
* @see {@link claim} for how priority biases scheduling
|
|
4344
4232
|
*/
|
|
@@ -4349,7 +4237,7 @@ var Act = class {
|
|
|
4349
4237
|
async _run_close(targets) {
|
|
4350
4238
|
const close_actor = { id: "$close", name: "close" };
|
|
4351
4239
|
const result = await run_close_cycle(targets, {
|
|
4352
|
-
|
|
4240
|
+
reactive: this._reactive,
|
|
4353
4241
|
catch_up_correlation: (until) => catch_up_correlation(
|
|
4354
4242
|
this._correlate,
|
|
4355
4243
|
(limit) => this.correlate({ limit }),
|
|
@@ -4402,27 +4290,6 @@ var Act = class {
|
|
|
4402
4290
|
* { stream: "order-456" },
|
|
4403
4291
|
* ]);
|
|
4404
4292
|
* ```
|
|
4405
|
-
*
|
|
4406
|
-
* @example Close with restart (state loaded automatically after guard)
|
|
4407
|
-
* ```typescript
|
|
4408
|
-
* await app.close([
|
|
4409
|
-
* { stream: "counter-1", restart: true },
|
|
4410
|
-
* { stream: "counter-2" }, // tombstoned
|
|
4411
|
-
* ]);
|
|
4412
|
-
* ```
|
|
4413
|
-
*
|
|
4414
|
-
* @example Windowed close — keep the last 180 days of real events
|
|
4415
|
-
* ```typescript
|
|
4416
|
-
* const cutoff = new Date();
|
|
4417
|
-
* cutoff.setDate(cutoff.getDate() - 180);
|
|
4418
|
-
* await app.close([
|
|
4419
|
-
* {
|
|
4420
|
-
* stream: "ledger-acme",
|
|
4421
|
-
* before: cutoff,
|
|
4422
|
-
* archive: async () => { await archiveToS3("ledger-acme", cutoff); },
|
|
4423
|
-
* },
|
|
4424
|
-
* ]);
|
|
4425
|
-
* ```
|
|
4426
4293
|
*/
|
|
4427
4294
|
async close(targets) {
|
|
4428
4295
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
@@ -4434,7 +4301,8 @@ var Act = class {
|
|
|
4434
4301
|
* Call this after `app.do()` (or `app.reset()`) to schedule a background
|
|
4435
4302
|
* drain. Multiple rapid calls within the debounce window are coalesced
|
|
4436
4303
|
* into a single cycle. Runs correlate→drain in a loop until a pass makes
|
|
4437
|
-
* no progress — no new subscriptions, no
|
|
4304
|
+
* no progress — no new subscriptions, no events read, no streams leased —
|
|
4305
|
+
* then emits
|
|
4438
4306
|
* the `"settled"` lifecycle event. This means a single `settle()` call
|
|
4439
4307
|
* fully catches up paginated streams (e.g. after `reset()` on a long
|
|
4440
4308
|
* projection) without forcing callers to loop.
|