@rotorsoft/act 1.32.24 → 1.32.26
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/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +20 -155
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +10 -190
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +0 -8
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.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/{chunk-OG5O36AJ.js → chunk-QXW3JV2H.js} +21 -240
- package/dist/chunk-QXW3JV2H.js.map +1 -0
- package/dist/index.cjs +153 -488
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +135 -251
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +20 -239
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-OG5O36AJ.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-QXW3JV2H.js";
|
|
30
30
|
import {
|
|
31
31
|
ActorSchema,
|
|
32
32
|
CausationEventSchema,
|
|
@@ -1355,37 +1355,34 @@ async function windowed_prune_pending(stream, before, max_id) {
|
|
|
1355
1355
|
}
|
|
1356
1356
|
async function probe_min_watermarks(streams, page_size, checkpoint) {
|
|
1357
1357
|
const min_at = /* @__PURE__ */ new Map();
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1358
|
+
const matches = source_matcher();
|
|
1359
|
+
await walk_streams(
|
|
1360
|
+
store(),
|
|
1361
|
+
(position) => {
|
|
1362
|
+
const pending = position.correlated_at === void 0 || position.at < position.correlated_at;
|
|
1363
|
+
const cap = pending ? position.at : Math.max(position.at, checkpoint);
|
|
1364
|
+
for (const stream of streams) {
|
|
1365
|
+
if (matches(position.source, stream)) {
|
|
1366
|
+
const prev = min_at.get(stream);
|
|
1367
|
+
if (prev === void 0 || cap < prev) min_at.set(stream, cap);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1371
|
+
{ limit: page_size, source_matches: streams }
|
|
1372
|
+
);
|
|
1373
|
+
return min_at;
|
|
1374
|
+
}
|
|
1375
|
+
function source_matcher() {
|
|
1376
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
1377
|
+
return (source, stream) => {
|
|
1378
|
+
if (!source) return true;
|
|
1379
|
+
let re = compiled.get(source);
|
|
1361
1380
|
if (!re) {
|
|
1362
1381
|
re = new RegExp(source);
|
|
1363
|
-
|
|
1382
|
+
compiled.set(source, re);
|
|
1364
1383
|
}
|
|
1365
|
-
return re;
|
|
1384
|
+
return re.test(stream);
|
|
1366
1385
|
};
|
|
1367
|
-
let after;
|
|
1368
|
-
for (; ; ) {
|
|
1369
|
-
let last;
|
|
1370
|
-
const { count } = await store().query_streams(
|
|
1371
|
-
(position) => {
|
|
1372
|
-
last = position.stream;
|
|
1373
|
-
const source_re = position.source ? get_regex(position.source) : void 0;
|
|
1374
|
-
const pending = position.correlated_at === void 0 || position.at < position.correlated_at;
|
|
1375
|
-
const cap = pending ? position.at : Math.max(position.at, checkpoint);
|
|
1376
|
-
for (const stream of streams) {
|
|
1377
|
-
if (!source_re || source_re.test(stream)) {
|
|
1378
|
-
const prev = min_at.get(stream);
|
|
1379
|
-
if (prev === void 0 || cap < prev) min_at.set(stream, cap);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
},
|
|
1383
|
-
{ after, limit: page_size, source_matches: streams }
|
|
1384
|
-
);
|
|
1385
|
-
if (count < page_size) break;
|
|
1386
|
-
after = last;
|
|
1387
|
-
}
|
|
1388
|
-
return min_at;
|
|
1389
1386
|
}
|
|
1390
1387
|
async function scan_stream_heads(streams) {
|
|
1391
1388
|
const stats = await store().query_stats(streams, {
|
|
@@ -1419,36 +1416,20 @@ async function partition_by_safety(stream_info, reactive_events_size, skipped, p
|
|
|
1419
1416
|
if (checkpoint < info.max_id) uncorrelated.add(stream);
|
|
1420
1417
|
}
|
|
1421
1418
|
const pending_set = /* @__PURE__ */ new Set();
|
|
1422
|
-
const
|
|
1423
|
-
const get_regex = (source) => {
|
|
1424
|
-
let re = source_regex.get(source);
|
|
1425
|
-
if (!re) {
|
|
1426
|
-
re = new RegExp(source);
|
|
1427
|
-
source_regex.set(source, re);
|
|
1428
|
-
}
|
|
1429
|
-
return re;
|
|
1430
|
-
};
|
|
1419
|
+
const matches = source_matcher();
|
|
1431
1420
|
const targets = [...stream_info.keys()];
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
(
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
},
|
|
1447
|
-
{ after, limit: page_size, source_matches: targets }
|
|
1448
|
-
);
|
|
1449
|
-
if (count < page_size) break;
|
|
1450
|
-
after = last;
|
|
1451
|
-
}
|
|
1421
|
+
await walk_streams(
|
|
1422
|
+
store(),
|
|
1423
|
+
(position) => {
|
|
1424
|
+
const has_work = position.correlated_at !== void 0 && position.at < position.correlated_at;
|
|
1425
|
+
if (!has_work) return;
|
|
1426
|
+
for (const [stream, info] of stream_info) {
|
|
1427
|
+
if (matches(position.source, stream) && position.at < info.max_id)
|
|
1428
|
+
pending_set.add(stream);
|
|
1429
|
+
}
|
|
1430
|
+
},
|
|
1431
|
+
{ limit: page_size, source_matches: targets }
|
|
1432
|
+
);
|
|
1452
1433
|
const safe = [];
|
|
1453
1434
|
for (const [stream] of stream_info) {
|
|
1454
1435
|
if (pending_set.has(stream) || uncorrelated.has(stream))
|
|
@@ -1711,13 +1692,25 @@ var CorrelateCycle = class {
|
|
|
1711
1692
|
const after = Math.max(this._checkpoint, query.after || -1);
|
|
1712
1693
|
const correlated = /* @__PURE__ */ new Map();
|
|
1713
1694
|
let last_id = after;
|
|
1695
|
+
let stopped = false;
|
|
1714
1696
|
const found = await store().query(
|
|
1715
1697
|
(event) => {
|
|
1716
|
-
|
|
1698
|
+
if (stopped) return;
|
|
1717
1699
|
const register = this._registry.events[event.name];
|
|
1718
1700
|
if (register) {
|
|
1719
1701
|
for (const reaction of register.reactions.values()) {
|
|
1720
|
-
|
|
1702
|
+
let resolved;
|
|
1703
|
+
try {
|
|
1704
|
+
resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
stopped = true;
|
|
1707
|
+
report_once(
|
|
1708
|
+
this._reported,
|
|
1709
|
+
`resolver|${reaction.handler.name}|${event.id}`,
|
|
1710
|
+
`Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}. Correlation stops before this event until the resolver stops throwing; work already found keeps draining.`
|
|
1711
|
+
);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1721
1714
|
if (!resolved) continue;
|
|
1722
1715
|
let lane = resolved.lane;
|
|
1723
1716
|
if (lane !== void 0 && !this._declared_lanes.has(lane)) {
|
|
@@ -1766,6 +1759,7 @@ var CorrelateCycle = class {
|
|
|
1766
1759
|
correlated.set(resolved.target, entry);
|
|
1767
1760
|
}
|
|
1768
1761
|
}
|
|
1762
|
+
last_id = event.id;
|
|
1769
1763
|
},
|
|
1770
1764
|
// Resolvers see `pii: null`: correlate runs actor-less, and one
|
|
1771
1765
|
// undecryptable row would otherwise stop every stream's reactions.
|
|
@@ -1807,7 +1801,7 @@ var CorrelateCycle = class {
|
|
|
1807
1801
|
return { subscribed, last_id, marked, scanned: true };
|
|
1808
1802
|
}
|
|
1809
1803
|
this._checkpoint = last_id;
|
|
1810
|
-
this._armed = found === query.limit;
|
|
1804
|
+
this._armed = stopped || found === query.limit;
|
|
1811
1805
|
return { subscribed: 0, last_id, marked: 0, scanned: true };
|
|
1812
1806
|
}
|
|
1813
1807
|
/**
|
|
@@ -2132,6 +2126,18 @@ var validating_patch = (me, state2, partial, event) => {
|
|
|
2132
2126
|
);
|
|
2133
2127
|
};
|
|
2134
2128
|
var DEFAULT_BATCH = 500;
|
|
2129
|
+
var cache_put = (stream, entry) => cache().set(stream, entry).catch(
|
|
2130
|
+
(err) => log().warn(
|
|
2131
|
+
err,
|
|
2132
|
+
"cache set skipped; the next load re-folds from the store"
|
|
2133
|
+
)
|
|
2134
|
+
);
|
|
2135
|
+
var cache_drop = (stream) => cache().invalidate(stream).catch(
|
|
2136
|
+
(err) => log().warn(
|
|
2137
|
+
err,
|
|
2138
|
+
"cache invalidate skipped; a stale checkpoint re-folds from its event_id"
|
|
2139
|
+
)
|
|
2140
|
+
);
|
|
2135
2141
|
async function snap(snapshot) {
|
|
2136
2142
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
2137
2143
|
try {
|
|
@@ -2334,19 +2340,14 @@ async function load(me, target, callback, patch_fn = bare_patch) {
|
|
|
2334
2340
|
);
|
|
2335
2341
|
const head_is_tombstone = event?.name === TOMBSTONE_EVENT;
|
|
2336
2342
|
if (replayed > 0 && !time_travel && event && !me.pii_aware && !head_is_tombstone) {
|
|
2337
|
-
await
|
|
2343
|
+
await cache_put(stream, {
|
|
2338
2344
|
stream,
|
|
2339
2345
|
state: state2,
|
|
2340
2346
|
version: event.version,
|
|
2341
2347
|
event_id: event.id,
|
|
2342
2348
|
patches,
|
|
2343
2349
|
snaps
|
|
2344
|
-
})
|
|
2345
|
-
(err) => log().warn(
|
|
2346
|
-
err,
|
|
2347
|
-
"cache set skipped; the next load re-folds from the store"
|
|
2348
|
-
)
|
|
2349
|
-
);
|
|
2350
|
+
});
|
|
2350
2351
|
}
|
|
2351
2352
|
return {
|
|
2352
2353
|
event,
|
|
@@ -2434,14 +2435,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
|
|
|
2434
2435
|
reactingTo ? expectedVersion : expected
|
|
2435
2436
|
);
|
|
2436
2437
|
} catch (error) {
|
|
2437
|
-
if (error instanceof ConcurrencyError)
|
|
2438
|
-
cache().invalidate(stream).catch(
|
|
2439
|
-
(err) => log().warn(
|
|
2440
|
-
err,
|
|
2441
|
-
"cache invalidate skipped; a stale checkpoint re-folds from its event_id"
|
|
2442
|
-
)
|
|
2443
|
-
);
|
|
2444
|
-
}
|
|
2438
|
+
if (error instanceof ConcurrencyError) void cache_drop(stream);
|
|
2445
2439
|
throw error;
|
|
2446
2440
|
}
|
|
2447
2441
|
let { state: state2, patches } = snapshot;
|
|
@@ -2468,26 +2462,15 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
|
|
|
2468
2462
|
const snap_event = snapped ? await snap(last) : void 0;
|
|
2469
2463
|
if (!me.pii_aware) {
|
|
2470
2464
|
if (contiguous)
|
|
2471
|
-
|
|
2465
|
+
void cache_put(stream, {
|
|
2472
2466
|
stream,
|
|
2473
2467
|
state: last.state,
|
|
2474
2468
|
version: snap_event?.version ?? last.event.version,
|
|
2475
2469
|
event_id: snap_event?.id ?? last.event.id,
|
|
2476
2470
|
patches: snap_event ? 0 : last.patches,
|
|
2477
2471
|
snaps: snap_event ? last.snaps + 1 : last.snaps
|
|
2478
|
-
})
|
|
2479
|
-
|
|
2480
|
-
err,
|
|
2481
|
-
"cache set skipped; the next load re-folds from the store"
|
|
2482
|
-
)
|
|
2483
|
-
);
|
|
2484
|
-
else
|
|
2485
|
-
cache().invalidate(stream).catch(
|
|
2486
|
-
(err) => log().warn(
|
|
2487
|
-
err,
|
|
2488
|
-
"cache invalidate skipped; a stale checkpoint re-folds from its event_id"
|
|
2489
|
-
)
|
|
2490
|
-
);
|
|
2472
|
+
});
|
|
2473
|
+
else void cache_drop(stream);
|
|
2491
2474
|
}
|
|
2492
2475
|
return snapshots;
|
|
2493
2476
|
} catch (error) {
|
|
@@ -2702,6 +2685,22 @@ function fetch_failure_policy(registry) {
|
|
|
2702
2685
|
}
|
|
2703
2686
|
return { blockOnError, maxRetries };
|
|
2704
2687
|
}
|
|
2688
|
+
function resolver_failed(lease, { error, options }) {
|
|
2689
|
+
log().error(error);
|
|
2690
|
+
const block2 = options.blockOnError && lease.retry >= options.maxRetries;
|
|
2691
|
+
return {
|
|
2692
|
+
lease,
|
|
2693
|
+
handled: 0,
|
|
2694
|
+
acked_at: lease.at,
|
|
2695
|
+
error,
|
|
2696
|
+
...block2 ? { block: true } : {
|
|
2697
|
+
next_attempt_at: Date.now() + compute_backoff_delay(
|
|
2698
|
+
lease.retry,
|
|
2699
|
+
options.backoff ?? DEFAULT_BACKOFF
|
|
2700
|
+
)
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2705
2704
|
function warn_misrouted(seen, stream, handler, event) {
|
|
2706
2705
|
report_once(
|
|
2707
2706
|
seen,
|
|
@@ -2735,13 +2734,23 @@ async function run_drain_cycle(deps, cycle) {
|
|
|
2735
2734
|
);
|
|
2736
2735
|
for (const f of fetched) {
|
|
2737
2736
|
const { stream, events } = f;
|
|
2737
|
+
let failed;
|
|
2738
2738
|
const payloads = events.flatMap((event) => {
|
|
2739
2739
|
const register = registry.events[event.name];
|
|
2740
2740
|
if (!register) return [];
|
|
2741
2741
|
return [...register.reactions.values()].filter((reaction) => {
|
|
2742
2742
|
const resolver = reaction.resolver;
|
|
2743
2743
|
const dynamic = typeof resolver === "function";
|
|
2744
|
-
|
|
2744
|
+
let resolved;
|
|
2745
|
+
try {
|
|
2746
|
+
resolved = dynamic ? resolver(event) : resolver;
|
|
2747
|
+
} catch (error) {
|
|
2748
|
+
failed ??= {
|
|
2749
|
+
error: `Resolver of reaction "${reaction.handler.name}" threw on ${String(event.name)} #${event.id} (stream "${event.stream}"): ${String(error)}`,
|
|
2750
|
+
options: reaction.options
|
|
2751
|
+
};
|
|
2752
|
+
return false;
|
|
2753
|
+
}
|
|
2745
2754
|
if (!resolved || resolved.target !== stream) return false;
|
|
2746
2755
|
if (!dynamic || !batch_handlers.has(stream)) return true;
|
|
2747
2756
|
warn_misrouted(
|
|
@@ -2753,7 +2762,7 @@ async function run_drain_cycle(deps, cycle) {
|
|
|
2753
2762
|
return false;
|
|
2754
2763
|
}).map((reaction) => ({ ...reaction, event }));
|
|
2755
2764
|
});
|
|
2756
|
-
fetch_map.set(stream, { fetch: f, payloads });
|
|
2765
|
+
fetch_map.set(stream, { fetch: f, payloads, failed });
|
|
2757
2766
|
}
|
|
2758
2767
|
const handled = await Promise.all(
|
|
2759
2768
|
leased.map((lease) => {
|
|
@@ -2774,6 +2783,8 @@ async function run_drain_cycle(deps, cycle) {
|
|
|
2774
2783
|
};
|
|
2775
2784
|
return Promise.resolve(failed);
|
|
2776
2785
|
}
|
|
2786
|
+
if (entry.failed)
|
|
2787
|
+
return Promise.resolve(resolver_failed(lease, entry.failed));
|
|
2777
2788
|
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
2778
2789
|
const { payloads } = entry;
|
|
2779
2790
|
const exhausted = budget_exhausted(
|
|
@@ -3250,41 +3261,23 @@ function validate_only_lanes(options, lanes) {
|
|
|
3250
3261
|
}
|
|
3251
3262
|
var Act = class {
|
|
3252
3263
|
_emitter = new EventEmitter();
|
|
3253
|
-
/** Circuit breaker shared by all drain lanes. */
|
|
3254
3264
|
_breaker;
|
|
3255
|
-
/** Whether this instance subscribes to `Store.notify`. */
|
|
3256
3265
|
_listen;
|
|
3257
|
-
/** Whether this instance runs reactions (drain, settle, correlate). */
|
|
3258
3266
|
_drain;
|
|
3259
|
-
/** Event names with at least one registered reaction (computed at build time) */
|
|
3260
3267
|
_reactive_events;
|
|
3261
|
-
/** One DrainController per active lane, keyed by lane name. */
|
|
3262
3268
|
_drain_controllers;
|
|
3263
|
-
/** Correlation: lazy init and the scan that marks work. */
|
|
3264
3269
|
_correlate;
|
|
3265
|
-
/** Debounced correlate→drain catch-up loop. */
|
|
3266
3270
|
_settle;
|
|
3267
|
-
/** Timer behind {@link start_correlations}. */
|
|
3268
3271
|
_poll;
|
|
3269
3272
|
/** The deprecated `start_correlations` callback, while the poll runs. */
|
|
3270
3273
|
_on_discovered;
|
|
3271
3274
|
/**
|
|
3272
|
-
*
|
|
3273
|
-
*
|
|
3274
|
-
* itself may be async (the PG adapter checks out a dedicated client
|
|
3275
|
-
* and runs `LISTEN` before resolving). Resolves to `undefined` when
|
|
3276
|
-
* the store doesn't implement `notify` or there are no registered
|
|
3277
|
-
* reactions.
|
|
3278
|
-
*
|
|
3279
|
-
* **Contract:** the configured store must be injected via
|
|
3280
|
-
* {@link store}`(adapter)` *before* calling `act()...build()`. The
|
|
3281
|
-
* orchestrator wires notify against whatever store is current at
|
|
3282
|
-
* construction time — late injection after build is unsupported.
|
|
3275
|
+
* Wired at construction against the store current then, so a store
|
|
3276
|
+
* injected after `build()` is never notified.
|
|
3283
3277
|
*/
|
|
3284
3278
|
_notify_disposer;
|
|
3285
3279
|
/** Public registry — kept as-is per the no-prefix-on-public convention. */
|
|
3286
3280
|
registry;
|
|
3287
|
-
/** Map of state name → state definition; populated by the builder. */
|
|
3288
3281
|
_states;
|
|
3289
3282
|
/**
|
|
3290
3283
|
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
@@ -3310,15 +3303,9 @@ var Act = class {
|
|
|
3310
3303
|
return listeners.length > 0;
|
|
3311
3304
|
}
|
|
3312
3305
|
/**
|
|
3313
|
-
* The
|
|
3314
|
-
*
|
|
3315
|
-
*
|
|
3316
|
-
* `breaker.failed(now, error)` and never log or emit themselves.
|
|
3317
|
-
*
|
|
3318
|
-
* The emit is guarded against Node's `EventEmitter` contract that an
|
|
3319
|
-
* unhandled `"error"` emission is rethrown (which would crash the process
|
|
3320
|
-
* from inside the drain catch); logging is unconditional so failures are
|
|
3321
|
-
* never silent.
|
|
3306
|
+
* The one place a store failure is logged and emitted (the breaker's
|
|
3307
|
+
* `on_error`). The emit is guarded: an unhandled `"error"` emission
|
|
3308
|
+
* would crash the process.
|
|
3322
3309
|
*/
|
|
3323
3310
|
_emit_error(error, circuit) {
|
|
3324
3311
|
this._logger.error(error);
|
|
@@ -3340,58 +3327,29 @@ var Act = class {
|
|
|
3340
3327
|
this._emitter.off(event, listener);
|
|
3341
3328
|
return this;
|
|
3342
3329
|
}
|
|
3343
|
-
/** Batch handlers for static-target projections (target → handler) */
|
|
3344
3330
|
_batch_handlers;
|
|
3345
|
-
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
3346
3331
|
_es;
|
|
3347
|
-
/** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
|
|
3348
3332
|
_cd;
|
|
3349
|
-
/**
|
|
3350
|
-
* Event-name → owning state, computed at build time. The duplicate-event
|
|
3351
|
-
* guard in merge.ts ensures one event name maps to at most one state, so
|
|
3352
|
-
* this lookup is unambiguous. Used by `close()` to pick the right reducer
|
|
3353
|
-
* set when seeding a `restart` snapshot in multi-state apps.
|
|
3354
|
-
*/
|
|
3333
|
+
/** Unambiguous: merge rejects an event name declared by two states. */
|
|
3355
3334
|
_event_to_state;
|
|
3356
|
-
/**
|
|
3357
|
-
* Event-name → lane fan-in for selective arming. Built by
|
|
3358
|
-
* `classify_registry` once per build. `"all"` means at least one of
|
|
3359
|
-
* the event's reactions is a dynamic resolver (lane opaque until
|
|
3360
|
-
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
3361
|
-
* reactions target.
|
|
3362
|
-
*/
|
|
3335
|
+
/** Lanes to arm per event; `"all"` when a dynamic resolver hides the lane. */
|
|
3363
3336
|
_event_to_lanes;
|
|
3364
|
-
/** What `internal/audit.ts` needs, built once at construction. */
|
|
3365
3337
|
_audit_deps;
|
|
3366
|
-
/** Logger resolved at construction time (after user port configuration) */
|
|
3367
3338
|
_logger = log();
|
|
3368
3339
|
/** This Act's ports: its own bag, or the singleton adapters. */
|
|
3369
3340
|
_ports;
|
|
3370
|
-
/**
|
|
3371
|
-
* Runs a body inside this Act's ports frame, so internal
|
|
3372
|
-
* `store()`/`cache()` calls resolve to `_ports`.
|
|
3373
|
-
*/
|
|
3341
|
+
/** Runs a body so internal `store()`/`cache()` calls resolve to `_ports`. */
|
|
3374
3342
|
_scoped;
|
|
3375
|
-
/**
|
|
3376
|
-
* Correlation-id generator for originating actions. Bound at
|
|
3377
|
-
* construction from `options.correlator ?? default_correlator`. The
|
|
3378
|
-
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
3379
|
-
* uses it via {@link close_correlation}.
|
|
3380
|
-
*/
|
|
3381
3343
|
_correlator;
|
|
3382
|
-
|
|
3383
|
-
* payload (it captures the triggering event for reactingTo auto-inject). */
|
|
3344
|
+
// Bound once, reused by every reaction payload.
|
|
3384
3345
|
_bound_do = this.do.bind(this);
|
|
3385
3346
|
_bound_load = this.load.bind(this);
|
|
3386
3347
|
_bound_query = this.query.bind(this);
|
|
3387
3348
|
_bound_query_array = this.query_array.bind(this);
|
|
3388
3349
|
_bound_forget = this.forget.bind(this);
|
|
3389
|
-
/** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
|
|
3390
3350
|
_handle;
|
|
3391
3351
|
_handle_batch;
|
|
3392
|
-
/** Declared drain lanes. */
|
|
3393
3352
|
_lanes;
|
|
3394
|
-
/** Serializes close critical sections per stream. */
|
|
3395
3353
|
_close_lock = new CloseLock();
|
|
3396
3354
|
/** Every lane a controller can exist for: `"default"` plus each declared one. */
|
|
3397
3355
|
_declared_lanes;
|
|
@@ -3399,26 +3357,7 @@ var Act = class {
|
|
|
3399
3357
|
get lanes() {
|
|
3400
3358
|
return this._lanes;
|
|
3401
3359
|
}
|
|
3402
|
-
/**
|
|
3403
|
-
* Create a new Act orchestrator. Prefer the {@link act} builder over
|
|
3404
|
-
* direct construction — `act()...build()` wires the registry, merges
|
|
3405
|
-
* partial states, and collects batch handlers from registered slices
|
|
3406
|
-
* and projections in one pass.
|
|
3407
|
-
*
|
|
3408
|
-
* @param registry Schemas for every event and action across registered states
|
|
3409
|
-
* @param states Merged map of state name → state definition
|
|
3410
|
-
* @param batch_handlers Static-target projection batch handlers (target → handler)
|
|
3411
|
-
* @param options Tuning knobs — see {@link ActOptions}
|
|
3412
|
-
* @param lanes Declared drain lanes. The builder collects
|
|
3413
|
-
* these from `.withLane(...)` calls; one `DrainController` runs
|
|
3414
|
-
* per lane, plus the implicit `"default"`.
|
|
3415
|
-
* @param patch_fn The per-event patch step selected once by the builder
|
|
3416
|
-
* from `ActOptions.validateFoldedState` — `bare_patch` by
|
|
3417
|
-
* default, `validating_patch` when the flag is on. The builder uses
|
|
3418
|
-
* the same value for its projection-fold handlers, so there is a
|
|
3419
|
-
* single selection site. Defaults to `bare_patch` for direct
|
|
3420
|
-
* construction.
|
|
3421
|
-
*/
|
|
3360
|
+
/** Use the {@link act} builder rather than constructing directly. */
|
|
3422
3361
|
constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = [], patch_fn = bare_patch) {
|
|
3423
3362
|
this.registry = registry;
|
|
3424
3363
|
this._states = states;
|
|
@@ -3469,11 +3408,7 @@ var Act = class {
|
|
|
3469
3408
|
this._notify_disposer = this._wire_notify(this._ports.store);
|
|
3470
3409
|
register_weak_disposer(new WeakRef(this), (self) => self.shutdown());
|
|
3471
3410
|
}
|
|
3472
|
-
/**
|
|
3473
|
-
* Circuit breaker shared by every store-polling loop (drain, the settle
|
|
3474
|
-
* correlate, autoclose). Validates the knobs eagerly so out-of-range
|
|
3475
|
-
* values throw at build time, not on the first cycle tick.
|
|
3476
|
-
*/
|
|
3411
|
+
/** Shared by drain, settle and autoclose; bad knobs throw at build. */
|
|
3477
3412
|
_build_breaker(options) {
|
|
3478
3413
|
return new CircuitBreaker(
|
|
3479
3414
|
resolveCircuitBreakerConfig(options.circuitBreaker),
|
|
@@ -3487,11 +3422,7 @@ var Act = class {
|
|
|
3487
3422
|
}
|
|
3488
3423
|
);
|
|
3489
3424
|
}
|
|
3490
|
-
/**
|
|
3491
|
-
* One DrainController per active lane, plus the implicit "default" unless
|
|
3492
|
-
* `onlyLanes` excludes it. Apps that declare no lanes pass no lane filter
|
|
3493
|
-
* to `claim`.
|
|
3494
|
-
*/
|
|
3425
|
+
/** Apps that declare no lanes pass no lane filter to `claim`. */
|
|
3495
3426
|
_build_drain_controllers(options, lanes) {
|
|
3496
3427
|
const all_lanes = this._declared_lanes;
|
|
3497
3428
|
const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
@@ -3510,8 +3441,18 @@ var Act = class {
|
|
|
3510
3441
|
on_acked: (acked) => this.emit("acked", acked),
|
|
3511
3442
|
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
3512
3443
|
// Reaction-requested close: the same machinery as `app.close`.
|
|
3444
|
+
// A store failure reaches the breaker; anything else (the archive
|
|
3445
|
+
// callback is app code) is logged here, never counted.
|
|
3513
3446
|
on_close: async (targets) => {
|
|
3514
|
-
|
|
3447
|
+
try {
|
|
3448
|
+
await this._run_close(targets);
|
|
3449
|
+
} catch (error) {
|
|
3450
|
+
if (error instanceof StoreError) throw error;
|
|
3451
|
+
this._logger.error(
|
|
3452
|
+
error,
|
|
3453
|
+
`Reaction-requested close of ${targets.map((t) => `"${t.stream}"`).join(", ")} failed and was not retried; close it with app.close(...) once the cause is fixed.`
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3515
3456
|
},
|
|
3516
3457
|
breaker: this._breaker,
|
|
3517
3458
|
run_scoped: this._scoped,
|
|
@@ -3527,11 +3468,7 @@ var Act = class {
|
|
|
3527
3468
|
}
|
|
3528
3469
|
return controllers;
|
|
3529
3470
|
}
|
|
3530
|
-
/**
|
|
3531
|
-
* Startup advisory naming declared lanes that `onlyLanes` leaves without a
|
|
3532
|
-
* controller here. One process can't check that the cluster covers every
|
|
3533
|
-
* lane, so it reports its own gaps.
|
|
3534
|
-
*/
|
|
3471
|
+
/** One process can't see the cluster, so it reports its own lane gaps. */
|
|
3535
3472
|
_advise_orphaned_lanes(options) {
|
|
3536
3473
|
if (!options.onlyLanes || options.onlyLanes.length === 0) return;
|
|
3537
3474
|
const active = new Set(this._drain_controllers.keys());
|
|
@@ -3553,11 +3490,6 @@ var Act = class {
|
|
|
3553
3490
|
routed_events: new Set(this._event_to_lanes.keys())
|
|
3554
3491
|
};
|
|
3555
3492
|
}
|
|
3556
|
-
/**
|
|
3557
|
-
* Correlate cycle over the classified registry. The cold-start callback
|
|
3558
|
-
* arms every controller — historical events may need processing —
|
|
3559
|
-
* except on writer-only instances (`drain: false`).
|
|
3560
|
-
*/
|
|
3561
3493
|
_build_correlate(options, classification) {
|
|
3562
3494
|
return new CorrelateCycle({
|
|
3563
3495
|
registry: this.registry,
|
|
@@ -3575,12 +3507,7 @@ var Act = class {
|
|
|
3575
3507
|
on_init_async: this._drain ? () => this._seed_persisted_defers() : void 0
|
|
3576
3508
|
});
|
|
3577
3509
|
}
|
|
3578
|
-
/**
|
|
3579
|
-
* At cold start, re-seed each lane controller's local wake from the
|
|
3580
|
-
* persisted future `deferred_at`, so an idle deferred stream (an
|
|
3581
|
-
* autoclose waiting out its window) still fires on time. Streams on lanes
|
|
3582
|
-
* this instance doesn't run are skipped.
|
|
3583
|
-
*/
|
|
3510
|
+
/** Cold start: an idle deferred stream still fires on time after a restart. */
|
|
3584
3511
|
async _seed_persisted_defers() {
|
|
3585
3512
|
const now = Date.now();
|
|
3586
3513
|
await walk_streams(store(), (pos) => {
|
|
@@ -3589,7 +3516,6 @@ var Act = class {
|
|
|
3589
3516
|
controller?.seed_defer(pos.stream, pos.deferred_at);
|
|
3590
3517
|
});
|
|
3591
3518
|
}
|
|
3592
|
-
/** Settle loop driving correlate + drain to quiescence. */
|
|
3593
3519
|
_build_settle(options) {
|
|
3594
3520
|
return new SettleLoop(
|
|
3595
3521
|
{
|
|
@@ -3603,7 +3529,6 @@ var Act = class {
|
|
|
3603
3529
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
3604
3530
|
);
|
|
3605
3531
|
}
|
|
3606
|
-
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
3607
3532
|
_shutdown_promise;
|
|
3608
3533
|
/**
|
|
3609
3534
|
* Per-instance teardown: stop scheduling new work, give drain cycles
|
|
@@ -3643,14 +3568,8 @@ var Act = class {
|
|
|
3643
3568
|
return this._shutdown_promise;
|
|
3644
3569
|
}
|
|
3645
3570
|
/**
|
|
3646
|
-
*
|
|
3647
|
-
*
|
|
3648
|
-
* its own errors), so this never throws.
|
|
3649
|
-
*
|
|
3650
|
-
* An omitted budget is the largest `leaseMillis` among lanes with a cycle
|
|
3651
|
-
* in flight (the drain's fallback for lanes that pinned none, or pinned
|
|
3652
|
-
* 0, which is the same as none), with an in-flight settle counting on the
|
|
3653
|
-
* fallback, capped at the maximum. Idle lanes don't count.
|
|
3571
|
+
* The largest `leaseMillis` among lanes with a cycle in flight (the
|
|
3572
|
+
* default for none or 0, and for a running settle), capped.
|
|
3654
3573
|
*/
|
|
3655
3574
|
_derive_grace_ms(running, settling) {
|
|
3656
3575
|
let max = settling ? DEFAULT_LEASE_MILLIS : 0;
|
|
@@ -3679,12 +3598,7 @@ var Act = class {
|
|
|
3679
3598
|
clearTimeout(timer);
|
|
3680
3599
|
}
|
|
3681
3600
|
}
|
|
3682
|
-
/**
|
|
3683
|
-
* Subscribe to {@link Store.notify} when both the store and the
|
|
3684
|
-
* registry support it. Returns the disposer (or `undefined` when no
|
|
3685
|
-
* subscription was made). Errors during subscription are logged but
|
|
3686
|
-
* never thrown — `notify` is a hint, not a contract.
|
|
3687
|
-
*/
|
|
3601
|
+
/** Failures are logged, never thrown: `notify` is a hint. */
|
|
3688
3602
|
async _wire_notify(s) {
|
|
3689
3603
|
if (this._reactive_events.size === 0) return void 0;
|
|
3690
3604
|
if (!s.notify) return void 0;
|
|
@@ -3996,20 +3910,11 @@ var Act = class {
|
|
|
3996
3910
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
3997
3911
|
return this._scoped(() => this._drain_all(options));
|
|
3998
3912
|
}
|
|
3999
|
-
/** Arm every active lane controller. */
|
|
4000
3913
|
_arm_all() {
|
|
4001
3914
|
this._correlate.arm();
|
|
4002
3915
|
for (const c of this._drain_controllers.values()) c.arm();
|
|
4003
3916
|
}
|
|
4004
|
-
/**
|
|
4005
|
-
* Arm only the lane controllers whose reactions match the supplied
|
|
4006
|
-
* event names (selective arming). Events with any dynamic
|
|
4007
|
-
* resolver fall back to `_arm_all()` via the `"all"` sentinel — the
|
|
4008
|
-
* resolver's lane isn't known until correlate runs the function.
|
|
4009
|
-
* Events with no reactions are skipped; `_event_to_lanes` doesn't
|
|
4010
|
-
* carry them. Returns true when any controller was armed (used by
|
|
4011
|
-
* the notify handler to decide whether to schedule a settle).
|
|
4012
|
-
*/
|
|
3917
|
+
/** Arms only the lanes these events react on; true when any was armed. */
|
|
4013
3918
|
_arm_for_event_names(names) {
|
|
4014
3919
|
const to_arm = /* @__PURE__ */ new Set();
|
|
4015
3920
|
for (const name of names) {
|
|
@@ -4026,14 +3931,7 @@ var Act = class {
|
|
|
4026
3931
|
for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
|
|
4027
3932
|
return true;
|
|
4028
3933
|
}
|
|
4029
|
-
/**
|
|
4030
|
-
*
|
|
4031
|
-
* Parallel — not sequential — so a slow lane's in-flight handler does
|
|
4032
|
-
* not block a fast lane's claim/dispatch/ack cycle. Each controller's
|
|
4033
|
-
* `claim()` is independent (filtered by lane); the store's
|
|
4034
|
-
* `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
|
|
4035
|
-
* (`acked`, `blocked`) may interleave by lane — listeners filter via
|
|
4036
|
-
* `lease.lane`. */
|
|
3934
|
+
/** In parallel, so a slow lane never holds up a fast one. */
|
|
4037
3935
|
async _drain_all(options) {
|
|
4038
3936
|
const results = await Promise.all(
|
|
4039
3937
|
[...this._drain_controllers.values()].map((c) => c.drain(options))
|
|
@@ -4106,14 +4004,7 @@ var Act = class {
|
|
|
4106
4004
|
const { subscribed, last_id } = await this._correlate_scanned(query);
|
|
4107
4005
|
return { subscribed, last_id };
|
|
4108
4006
|
}
|
|
4109
|
-
/**
|
|
4110
|
-
* `correlate` plus whether the pass actually read the store.
|
|
4111
|
-
*
|
|
4112
|
-
* The settle loop needs that extra bit to decide whether the pass carries a
|
|
4113
|
-
* circuit-breaker health signal, and a disarmed pass carries none. It stays
|
|
4114
|
-
* internal rather than widening the public `correlate` return, which is
|
|
4115
|
-
* charter-covered and has no use for it.
|
|
4116
|
-
*/
|
|
4007
|
+
/** `correlate` plus `scanned`, which settle needs for the breaker. */
|
|
4117
4008
|
async _correlate_scanned(query, lease = false) {
|
|
4118
4009
|
if (!this._drain) return { subscribed: 0, last_id: -1, scanned: false };
|
|
4119
4010
|
return this._scoped(async () => {
|
|
@@ -4454,11 +4345,7 @@ var Act = class {
|
|
|
4454
4345
|
async prioritize(filter, priority) {
|
|
4455
4346
|
return this._scoped(() => store().prioritize(filter, priority));
|
|
4456
4347
|
}
|
|
4457
|
-
/**
|
|
4458
|
-
* Run one close: the cycle with this Act's ports, then emit `closed`. Shared by `close()` and the drain's
|
|
4459
|
-
* reaction-requested closes. The close actor lets a custom correlator
|
|
4460
|
-
* still tag tenant context or trace ids.
|
|
4461
|
-
*/
|
|
4348
|
+
/** Shared by `close()` and reaction-requested closes. */
|
|
4462
4349
|
async _run_close(targets) {
|
|
4463
4350
|
const close_actor = { id: "$close", name: "close" };
|
|
4464
4351
|
const result = await run_close_cycle(targets, {
|
|
@@ -4539,10 +4426,7 @@ var Act = class {
|
|
|
4539
4426
|
*/
|
|
4540
4427
|
async close(targets) {
|
|
4541
4428
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
4542
|
-
return this._scoped(
|
|
4543
|
-
await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
|
|
4544
|
-
return this._run_close(targets);
|
|
4545
|
-
});
|
|
4429
|
+
return this._scoped(() => this._run_close(targets));
|
|
4546
4430
|
}
|
|
4547
4431
|
/**
|
|
4548
4432
|
* Debounced, non-blocking correlate→drain cycle.
|