@rotorsoft/act 1.22.5 → 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/autoclose-config.d.ts +13 -0
- package/dist/@types/internal/autoclose-config.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/internal/event-versions.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 +124 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +119 -7
- 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;
|
|
@@ -1607,6 +1612,15 @@ function deprecated_event_names(names) {
|
|
|
1607
1612
|
const deprecated = /* @__PURE__ */ new Set();
|
|
1608
1613
|
for (const list of groups.values()) {
|
|
1609
1614
|
if (list.length < 2) continue;
|
|
1615
|
+
const by_version = /* @__PURE__ */ new Map();
|
|
1616
|
+
for (const { version, name } of list) {
|
|
1617
|
+
const clash = by_version.get(version);
|
|
1618
|
+
if (clash)
|
|
1619
|
+
throw new Error(
|
|
1620
|
+
`duplicate event version: ${clash} and ${name} both map to version ${version} \u2014 leading zeros in a \`_v<n>\` suffix are not allowed; use a single canonical spelling`
|
|
1621
|
+
);
|
|
1622
|
+
by_version.set(version, name);
|
|
1623
|
+
}
|
|
1610
1624
|
list.sort((a, b) => b.version - a.version);
|
|
1611
1625
|
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
1612
1626
|
}
|
|
@@ -2052,18 +2066,33 @@ function hour_in_zone(now, timeZone) {
|
|
|
2052
2066
|
}).formatToParts(now);
|
|
2053
2067
|
return Number(parts.find((p) => p.type === "hour")?.value);
|
|
2054
2068
|
}
|
|
2069
|
+
function hour_in_range(hour, start, end) {
|
|
2070
|
+
return start < end ? hour >= start && hour < end : hour >= start || hour < end;
|
|
2071
|
+
}
|
|
2072
|
+
function is_dst_gap_open(now, timeZone, start) {
|
|
2073
|
+
const hour = hour_in_zone(now, timeZone);
|
|
2074
|
+
if (hour <= start) return false;
|
|
2075
|
+
const prev = hour_in_zone(new Date(now.getTime() - 36e5), timeZone);
|
|
2076
|
+
return prev === start - 1 && hour === start + 1;
|
|
2077
|
+
}
|
|
2055
2078
|
function in_autoclose_window(window, now) {
|
|
2056
2079
|
if (!window) return true;
|
|
2057
2080
|
const hour = hour_in_zone(now, window.timeZone);
|
|
2058
|
-
return
|
|
2081
|
+
return hour_in_range(hour, window.start, window.end) || is_dst_gap_open(now, window.timeZone, window.start);
|
|
2059
2082
|
}
|
|
2060
2083
|
function next_window_open(window, now) {
|
|
2084
|
+
const start_ms = now.getTime();
|
|
2061
2085
|
for (let h = 0; h <= 48; h++) {
|
|
2062
|
-
const candidate = new Date(
|
|
2086
|
+
const candidate = new Date(start_ms + h * 36e5);
|
|
2063
2087
|
if (hour_in_zone(candidate, window.timeZone) === window.start)
|
|
2064
2088
|
return candidate;
|
|
2089
|
+
const boundary = new Date(
|
|
2090
|
+
candidate.getTime() - candidate.getTime() % 36e5
|
|
2091
|
+
);
|
|
2092
|
+
if (is_dst_gap_open(boundary, window.timeZone, window.start))
|
|
2093
|
+
return boundary;
|
|
2065
2094
|
}
|
|
2066
|
-
return new Date(
|
|
2095
|
+
return new Date(start_ms + 864e5);
|
|
2067
2096
|
}
|
|
2068
2097
|
|
|
2069
2098
|
// src/internal/autoclose-policy.ts
|
|
@@ -2900,6 +2929,16 @@ var CorrelateCycle = class {
|
|
|
2900
2929
|
_has_dynamic_resolvers;
|
|
2901
2930
|
_cd;
|
|
2902
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;
|
|
2903
2942
|
/**
|
|
2904
2943
|
* Scope runner (#1191). The periodic `start_polling` timer fires
|
|
2905
2944
|
* outside any caller frame, so its `correlate()` must be re-wrapped in
|
|
@@ -2908,7 +2947,7 @@ var CorrelateCycle = class {
|
|
|
2908
2947
|
* `_scoped` (identity for a non-scoped Act), so it's required.
|
|
2909
2948
|
*/
|
|
2910
2949
|
_run_scoped;
|
|
2911
|
-
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) {
|
|
2912
2951
|
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
2913
2952
|
this._registry = registry;
|
|
2914
2953
|
this._static_targets = static_targets;
|
|
@@ -2916,6 +2955,7 @@ var CorrelateCycle = class {
|
|
|
2916
2955
|
this._cd = cd;
|
|
2917
2956
|
this._on_init = on_init;
|
|
2918
2957
|
this._run_scoped = run_scoped;
|
|
2958
|
+
this._on_init_async = on_init_async;
|
|
2919
2959
|
}
|
|
2920
2960
|
/** Last correlated event id. */
|
|
2921
2961
|
get checkpoint() {
|
|
@@ -2937,6 +2977,7 @@ var CorrelateCycle = class {
|
|
|
2937
2977
|
for (const { stream } of this._static_targets) {
|
|
2938
2978
|
this._subscribed.add(stream);
|
|
2939
2979
|
}
|
|
2980
|
+
await this._on_init_async?.();
|
|
2940
2981
|
}
|
|
2941
2982
|
/**
|
|
2942
2983
|
* Discover dynamic-resolver targets in the events past the checkpoint
|
|
@@ -3810,6 +3851,22 @@ var DrainController = class {
|
|
|
3810
3851
|
arm() {
|
|
3811
3852
|
this._armed = true;
|
|
3812
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
|
+
}
|
|
3813
3870
|
/** Read-only flag — true while a commit / reset is unprocessed. */
|
|
3814
3871
|
get armed() {
|
|
3815
3872
|
return this._armed;
|
|
@@ -4358,6 +4415,7 @@ var Act = class {
|
|
|
4358
4415
|
this._drain = options.drain !== false;
|
|
4359
4416
|
this._breaker = this._build_breaker(options);
|
|
4360
4417
|
this._drain_controllers = this._build_drain_controllers(options, lanes);
|
|
4418
|
+
this._advise_orphaned_lanes(options, lanes);
|
|
4361
4419
|
this._audit_deps = this._build_audit_deps();
|
|
4362
4420
|
this._correlate = this._build_correlate(options, classification);
|
|
4363
4421
|
this._settle = this._build_settle(options);
|
|
@@ -4450,6 +4508,31 @@ var Act = class {
|
|
|
4450
4508
|
}
|
|
4451
4509
|
return controllers;
|
|
4452
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
|
+
}
|
|
4453
4536
|
/**
|
|
4454
4537
|
* Audit deps bag. Snapshotted after registry classification and
|
|
4455
4538
|
* drain-controller build so the audit module sees the finalized lane
|
|
@@ -4463,7 +4546,14 @@ var Act = class {
|
|
|
4463
4546
|
event_to_state: this._event_to_state,
|
|
4464
4547
|
states: this._states,
|
|
4465
4548
|
known_events: new Set(this._event_to_state.keys()),
|
|
4466
|
-
|
|
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)]),
|
|
4467
4557
|
routed_events: new Set(this._event_to_lanes.keys())
|
|
4468
4558
|
};
|
|
4469
4559
|
}
|
|
@@ -4484,9 +4574,36 @@ var Act = class {
|
|
|
4484
4574
|
},
|
|
4485
4575
|
// Re-scope the background `start_correlations` timer so its
|
|
4486
4576
|
// correlate resolves the scoped ports, not the singleton (#1191).
|
|
4487
|
-
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
|
|
4488
4581
|
);
|
|
4489
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
|
+
}
|
|
4490
4607
|
/** Settle loop driving correlate + drain to quiescence. */
|
|
4491
4608
|
_build_settle(options) {
|
|
4492
4609
|
return new SettleLoop(
|