@rotorsoft/act 1.22.6 → 1.22.7
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 +31 -0
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts +11 -1
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/defer-timer.d.ts +5 -1
- package/dist/@types/internal/defer-timer.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts +13 -0
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +7 -0
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/{chunk-CWBI4NQD.js → chunk-QVHLKVS7.js} +7 -2
- package/dist/chunk-QVHLKVS7.js.map +1 -0
- package/dist/index.cjs +97 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +92 -4
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +6 -1
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-CWBI4NQD.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -703,6 +703,10 @@ var InMemoryStream = class {
|
|
|
703
703
|
get leased_until() {
|
|
704
704
|
return this._leased_until;
|
|
705
705
|
}
|
|
706
|
+
/** Persisted next-visit time (#1090/#1221), or undefined when no active defer. */
|
|
707
|
+
get deferred_at() {
|
|
708
|
+
return this._deferred_at;
|
|
709
|
+
}
|
|
706
710
|
/**
|
|
707
711
|
* Attempt to lease this stream for processing.
|
|
708
712
|
* @param lease - The lease request.
|
|
@@ -1297,7 +1301,8 @@ var InMemoryStore = class {
|
|
|
1297
1301
|
priority: s.priority,
|
|
1298
1302
|
leased_by: s.leased_by,
|
|
1299
1303
|
leased_until: s.leased_until,
|
|
1300
|
-
lane: s.lane
|
|
1304
|
+
lane: s.lane,
|
|
1305
|
+
deferred_at: s.deferred_at
|
|
1301
1306
|
});
|
|
1302
1307
|
count++;
|
|
1303
1308
|
if (count >= limit) break;
|
|
@@ -2924,6 +2929,16 @@ var CorrelateCycle = class {
|
|
|
2924
2929
|
_has_dynamic_resolvers;
|
|
2925
2930
|
_cd;
|
|
2926
2931
|
_on_init;
|
|
2932
|
+
/**
|
|
2933
|
+
* Async cold-start hook (#1221). Runs once, after the sync `on_init`,
|
|
2934
|
+
* inside the same `init()` await. The orchestrator uses it to re-seed the
|
|
2935
|
+
* process-local defer timers from the store's persisted `deferred_at` so
|
|
2936
|
+
* an idle deferred stream re-arms its drain across a restart. Kept
|
|
2937
|
+
* separate from `on_init` because seeding is an async store read; `init`
|
|
2938
|
+
* already awaits, so folding it in here preserves the "runs exactly once"
|
|
2939
|
+
* guarantee without a second gate on the Act side.
|
|
2940
|
+
*/
|
|
2941
|
+
_on_init_async;
|
|
2927
2942
|
/**
|
|
2928
2943
|
* Scope runner (#1191). The periodic `start_polling` timer fires
|
|
2929
2944
|
* outside any caller frame, so its `correlate()` must be re-wrapped in
|
|
@@ -2932,7 +2947,7 @@ var CorrelateCycle = class {
|
|
|
2932
2947
|
* `_scoped` (identity for a non-scoped Act), so it's required.
|
|
2933
2948
|
*/
|
|
2934
2949
|
_run_scoped;
|
|
2935
|
-
constructor(registry, static_targets, has_dynamic_resolvers, cd, maxSubscribedStreams, on_init, run_scoped) {
|
|
2950
|
+
constructor(registry, static_targets, has_dynamic_resolvers, cd, maxSubscribedStreams, on_init, run_scoped, on_init_async) {
|
|
2936
2951
|
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
2937
2952
|
this._registry = registry;
|
|
2938
2953
|
this._static_targets = static_targets;
|
|
@@ -2940,6 +2955,7 @@ var CorrelateCycle = class {
|
|
|
2940
2955
|
this._cd = cd;
|
|
2941
2956
|
this._on_init = on_init;
|
|
2942
2957
|
this._run_scoped = run_scoped;
|
|
2958
|
+
this._on_init_async = on_init_async;
|
|
2943
2959
|
}
|
|
2944
2960
|
/** Last correlated event id. */
|
|
2945
2961
|
get checkpoint() {
|
|
@@ -2961,6 +2977,7 @@ var CorrelateCycle = class {
|
|
|
2961
2977
|
for (const { stream } of this._static_targets) {
|
|
2962
2978
|
this._subscribed.add(stream);
|
|
2963
2979
|
}
|
|
2980
|
+
await this._on_init_async?.();
|
|
2964
2981
|
}
|
|
2965
2982
|
/**
|
|
2966
2983
|
* Discover dynamic-resolver targets in the events past the checkpoint
|
|
@@ -3834,6 +3851,22 @@ var DrainController = class {
|
|
|
3834
3851
|
arm() {
|
|
3835
3852
|
this._armed = true;
|
|
3836
3853
|
}
|
|
3854
|
+
/**
|
|
3855
|
+
* Re-seed a persisted defer schedule into the process-local timer at cold
|
|
3856
|
+
* start (#1221). The `_defer` map lives in worker memory and is empty
|
|
3857
|
+
* after a restart; a stream deferred to a future due-time (e.g. an idle
|
|
3858
|
+
* autoclose aggregate) is durable in the store's `deferred_at` but has
|
|
3859
|
+
* nothing in memory to re-arm the drain at the due-time. The orchestrator
|
|
3860
|
+
* reads the persisted `deferred_at` for this controller's lane and calls
|
|
3861
|
+
* this to park the stream + (re)schedule the shared wake — so the drain
|
|
3862
|
+
* re-arms at the due-time with no intervening commit. `schedule()`
|
|
3863
|
+
* collapses many seeds into one timer, so callers may seed in a loop and
|
|
3864
|
+
* let the earliest due-time win.
|
|
3865
|
+
*/
|
|
3866
|
+
seed_defer(stream, at) {
|
|
3867
|
+
this._defer.set(stream, at);
|
|
3868
|
+
this._defer.schedule();
|
|
3869
|
+
}
|
|
3837
3870
|
/** Read-only flag — true while a commit / reset is unprocessed. */
|
|
3838
3871
|
get armed() {
|
|
3839
3872
|
return this._armed;
|
|
@@ -4382,6 +4415,7 @@ var Act = class {
|
|
|
4382
4415
|
this._drain = options.drain !== false;
|
|
4383
4416
|
this._breaker = this._build_breaker(options);
|
|
4384
4417
|
this._drain_controllers = this._build_drain_controllers(options, lanes);
|
|
4418
|
+
this._advise_orphaned_lanes(options, lanes);
|
|
4385
4419
|
this._audit_deps = this._build_audit_deps();
|
|
4386
4420
|
this._correlate = this._build_correlate(options, classification);
|
|
4387
4421
|
this._settle = this._build_settle(options);
|
|
@@ -4474,6 +4508,31 @@ var Act = class {
|
|
|
4474
4508
|
}
|
|
4475
4509
|
return controllers;
|
|
4476
4510
|
}
|
|
4511
|
+
/**
|
|
4512
|
+
* Orphaned-lane startup advisory (#1220). When `onlyLanes` is set, this
|
|
4513
|
+
* instance builds a controller only for its slice of the declared lane
|
|
4514
|
+
* universe — every OTHER declared lane's stream is persisted (correlate
|
|
4515
|
+
* subscribes all static targets regardless of `onlyLanes`) but never
|
|
4516
|
+
* claimed here. If no peer worker deploys with those lanes in ITS
|
|
4517
|
+
* `onlyLanes`, their reactions accumulate forever, silently. A single
|
|
4518
|
+
* process can't verify the cluster invariant `∪ onlyLanes ⊇ declared
|
|
4519
|
+
* lanes`, so we surface the per-instance signal — "these declared lanes
|
|
4520
|
+
* have no controller here" — the same way the deprecated-event advisory
|
|
4521
|
+
* surfaces legacy events. No advisory when `onlyLanes` is unset (every
|
|
4522
|
+
* lane gets a controller) or covers every declared lane.
|
|
4523
|
+
*/
|
|
4524
|
+
_advise_orphaned_lanes(options, lanes) {
|
|
4525
|
+
if (!options.onlyLanes || options.onlyLanes.length === 0) return;
|
|
4526
|
+
const active = new Set(this._drain_controllers.keys());
|
|
4527
|
+
const orphaned = ["default", ...lanes.map((l) => l.name)].filter(
|
|
4528
|
+
(name) => !active.has(name)
|
|
4529
|
+
);
|
|
4530
|
+
if (orphaned.length === 0) return;
|
|
4531
|
+
const list = orphaned.map((name) => `"${name}"`).join(", ");
|
|
4532
|
+
this._logger.info(
|
|
4533
|
+
`Act declared ${orphaned.length} orphaned lane(s) on this instance: ${list}. onlyLanes excludes them, so no DrainController claims their streams here \u2014 their reactions accumulate un-drained unless a peer worker deploys with these lanes in its onlyLanes. Ensure the cluster invariant holds: the union of every worker's onlyLanes must cover every declared lane. See docs/docs/guides/production-checklist.md \xA7 Sizing lanes.`
|
|
4534
|
+
);
|
|
4535
|
+
}
|
|
4477
4536
|
/**
|
|
4478
4537
|
* Audit deps bag. Snapshotted after registry classification and
|
|
4479
4538
|
* drain-controller build so the audit module sees the finalized lane
|
|
@@ -4487,7 +4546,14 @@ var Act = class {
|
|
|
4487
4546
|
event_to_state: this._event_to_state,
|
|
4488
4547
|
states: this._states,
|
|
4489
4548
|
known_events: new Set(this._event_to_state.keys()),
|
|
4490
|
-
|
|
4549
|
+
// The DECLARED lane universe — the implicit "default" plus every
|
|
4550
|
+
// `.withLane(...)` name — NOT `_drain_controllers.keys()` (#1224).
|
|
4551
|
+
// An `onlyLanes`-filtered instance builds a controller only for its
|
|
4552
|
+
// slice of lanes, so keying off the active controller set would flag
|
|
4553
|
+
// a stream correctly assigned to an excluded-but-declared lane (one
|
|
4554
|
+
// another worker drains) as `unknown-lane`. The audit reports what's
|
|
4555
|
+
// structurally routable across the cluster, not what this process runs.
|
|
4556
|
+
declared_lanes: /* @__PURE__ */ new Set(["default", ...this._lanes.map((l) => l.name)]),
|
|
4491
4557
|
routed_events: new Set(this._event_to_lanes.keys())
|
|
4492
4558
|
};
|
|
4493
4559
|
}
|
|
@@ -4508,9 +4574,36 @@ var Act = class {
|
|
|
4508
4574
|
},
|
|
4509
4575
|
// Re-scope the background `start_correlations` timer so its
|
|
4510
4576
|
// correlate resolves the scoped ports, not the singleton (#1191).
|
|
4511
|
-
this._scoped
|
|
4577
|
+
this._scoped,
|
|
4578
|
+
// Cold-start defer re-seed (#1221). Skipped on writer-only instances
|
|
4579
|
+
// (`drain: false`) — they run no local controllers to re-arm.
|
|
4580
|
+
this._drain ? () => this._seed_persisted_defers() : void 0
|
|
4512
4581
|
);
|
|
4513
4582
|
}
|
|
4583
|
+
/**
|
|
4584
|
+
* Re-seed every active lane controller's process-local defer timer from
|
|
4585
|
+
* the store's persisted `deferred_at` (#1221). Runs once at cold start,
|
|
4586
|
+
* inside `CorrelateCycle.init`, after static targets are subscribed.
|
|
4587
|
+
*
|
|
4588
|
+
* The defer timer is worker memory: empty after a restart. A stream
|
|
4589
|
+
* deferred to a future due-time (the classic case: an idle autoclose
|
|
4590
|
+
* aggregate that deferred its terminal close) is durable in the store but
|
|
4591
|
+
* has nothing in memory to re-arm the drain — the controller disarms on
|
|
4592
|
+
* the first empty claim and, since the aggregate is idle, no commit ever
|
|
4593
|
+
* re-arms it. Reading the persisted schedule and seeding the owning lane's
|
|
4594
|
+
* timer restores the wake, so the close fires at the due-time.
|
|
4595
|
+
*
|
|
4596
|
+
* Streams whose lane has no controller on this instance (excluded by
|
|
4597
|
+
* `onlyLanes`) are skipped — a peer worker owns that lane's timer.
|
|
4598
|
+
*/
|
|
4599
|
+
async _seed_persisted_defers() {
|
|
4600
|
+
const now = Date.now();
|
|
4601
|
+
await store2().query_streams((pos) => {
|
|
4602
|
+
if (pos.deferred_at === void 0 || pos.deferred_at <= now) return;
|
|
4603
|
+
const controller = this._drain_controllers.get(pos.lane ?? "default");
|
|
4604
|
+
controller?.seed_defer(pos.stream, pos.deferred_at);
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4514
4607
|
/** Settle loop driving correlate + drain to quiescence. */
|
|
4515
4608
|
_build_settle(options) {
|
|
4516
4609
|
return new SettleLoop(
|