@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.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
sleep,
|
|
21
21
|
store,
|
|
22
22
|
validate
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-QVHLKVS7.js";
|
|
24
24
|
import {
|
|
25
25
|
ActorSchema,
|
|
26
26
|
CausationEventSchema,
|
|
@@ -89,6 +89,15 @@ function deprecated_event_names(names) {
|
|
|
89
89
|
const deprecated = /* @__PURE__ */ new Set();
|
|
90
90
|
for (const list of groups.values()) {
|
|
91
91
|
if (list.length < 2) continue;
|
|
92
|
+
const by_version = /* @__PURE__ */ new Map();
|
|
93
|
+
for (const { version, name } of list) {
|
|
94
|
+
const clash = by_version.get(version);
|
|
95
|
+
if (clash)
|
|
96
|
+
throw new Error(
|
|
97
|
+
`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`
|
|
98
|
+
);
|
|
99
|
+
by_version.set(version, name);
|
|
100
|
+
}
|
|
92
101
|
list.sort((a, b) => b.version - a.version);
|
|
93
102
|
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
94
103
|
}
|
|
@@ -534,18 +543,33 @@ function hour_in_zone(now, timeZone) {
|
|
|
534
543
|
}).formatToParts(now);
|
|
535
544
|
return Number(parts.find((p) => p.type === "hour")?.value);
|
|
536
545
|
}
|
|
546
|
+
function hour_in_range(hour, start, end) {
|
|
547
|
+
return start < end ? hour >= start && hour < end : hour >= start || hour < end;
|
|
548
|
+
}
|
|
549
|
+
function is_dst_gap_open(now, timeZone, start) {
|
|
550
|
+
const hour = hour_in_zone(now, timeZone);
|
|
551
|
+
if (hour <= start) return false;
|
|
552
|
+
const prev = hour_in_zone(new Date(now.getTime() - 36e5), timeZone);
|
|
553
|
+
return prev === start - 1 && hour === start + 1;
|
|
554
|
+
}
|
|
537
555
|
function in_autoclose_window(window, now) {
|
|
538
556
|
if (!window) return true;
|
|
539
557
|
const hour = hour_in_zone(now, window.timeZone);
|
|
540
|
-
return
|
|
558
|
+
return hour_in_range(hour, window.start, window.end) || is_dst_gap_open(now, window.timeZone, window.start);
|
|
541
559
|
}
|
|
542
560
|
function next_window_open(window, now) {
|
|
561
|
+
const start_ms = now.getTime();
|
|
543
562
|
for (let h = 0; h <= 48; h++) {
|
|
544
|
-
const candidate = new Date(
|
|
563
|
+
const candidate = new Date(start_ms + h * 36e5);
|
|
545
564
|
if (hour_in_zone(candidate, window.timeZone) === window.start)
|
|
546
565
|
return candidate;
|
|
566
|
+
const boundary = new Date(
|
|
567
|
+
candidate.getTime() - candidate.getTime() % 36e5
|
|
568
|
+
);
|
|
569
|
+
if (is_dst_gap_open(boundary, window.timeZone, window.start))
|
|
570
|
+
return boundary;
|
|
547
571
|
}
|
|
548
|
-
return new Date(
|
|
572
|
+
return new Date(start_ms + 864e5);
|
|
549
573
|
}
|
|
550
574
|
|
|
551
575
|
// src/internal/autoclose-policy.ts
|
|
@@ -1382,6 +1406,16 @@ var CorrelateCycle = class {
|
|
|
1382
1406
|
_has_dynamic_resolvers;
|
|
1383
1407
|
_cd;
|
|
1384
1408
|
_on_init;
|
|
1409
|
+
/**
|
|
1410
|
+
* Async cold-start hook (#1221). Runs once, after the sync `on_init`,
|
|
1411
|
+
* inside the same `init()` await. The orchestrator uses it to re-seed the
|
|
1412
|
+
* process-local defer timers from the store's persisted `deferred_at` so
|
|
1413
|
+
* an idle deferred stream re-arms its drain across a restart. Kept
|
|
1414
|
+
* separate from `on_init` because seeding is an async store read; `init`
|
|
1415
|
+
* already awaits, so folding it in here preserves the "runs exactly once"
|
|
1416
|
+
* guarantee without a second gate on the Act side.
|
|
1417
|
+
*/
|
|
1418
|
+
_on_init_async;
|
|
1385
1419
|
/**
|
|
1386
1420
|
* Scope runner (#1191). The periodic `start_polling` timer fires
|
|
1387
1421
|
* outside any caller frame, so its `correlate()` must be re-wrapped in
|
|
@@ -1390,7 +1424,7 @@ var CorrelateCycle = class {
|
|
|
1390
1424
|
* `_scoped` (identity for a non-scoped Act), so it's required.
|
|
1391
1425
|
*/
|
|
1392
1426
|
_run_scoped;
|
|
1393
|
-
constructor(registry, static_targets, has_dynamic_resolvers, cd, maxSubscribedStreams, on_init, run_scoped) {
|
|
1427
|
+
constructor(registry, static_targets, has_dynamic_resolvers, cd, maxSubscribedStreams, on_init, run_scoped, on_init_async) {
|
|
1394
1428
|
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
1395
1429
|
this._registry = registry;
|
|
1396
1430
|
this._static_targets = static_targets;
|
|
@@ -1398,6 +1432,7 @@ var CorrelateCycle = class {
|
|
|
1398
1432
|
this._cd = cd;
|
|
1399
1433
|
this._on_init = on_init;
|
|
1400
1434
|
this._run_scoped = run_scoped;
|
|
1435
|
+
this._on_init_async = on_init_async;
|
|
1401
1436
|
}
|
|
1402
1437
|
/** Last correlated event id. */
|
|
1403
1438
|
get checkpoint() {
|
|
@@ -1419,6 +1454,7 @@ var CorrelateCycle = class {
|
|
|
1419
1454
|
for (const { stream } of this._static_targets) {
|
|
1420
1455
|
this._subscribed.add(stream);
|
|
1421
1456
|
}
|
|
1457
|
+
await this._on_init_async?.();
|
|
1422
1458
|
}
|
|
1423
1459
|
/**
|
|
1424
1460
|
* Discover dynamic-resolver targets in the events past the checkpoint
|
|
@@ -2292,6 +2328,22 @@ var DrainController = class {
|
|
|
2292
2328
|
arm() {
|
|
2293
2329
|
this._armed = true;
|
|
2294
2330
|
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Re-seed a persisted defer schedule into the process-local timer at cold
|
|
2333
|
+
* start (#1221). The `_defer` map lives in worker memory and is empty
|
|
2334
|
+
* after a restart; a stream deferred to a future due-time (e.g. an idle
|
|
2335
|
+
* autoclose aggregate) is durable in the store's `deferred_at` but has
|
|
2336
|
+
* nothing in memory to re-arm the drain at the due-time. The orchestrator
|
|
2337
|
+
* reads the persisted `deferred_at` for this controller's lane and calls
|
|
2338
|
+
* this to park the stream + (re)schedule the shared wake — so the drain
|
|
2339
|
+
* re-arms at the due-time with no intervening commit. `schedule()`
|
|
2340
|
+
* collapses many seeds into one timer, so callers may seed in a loop and
|
|
2341
|
+
* let the earliest due-time win.
|
|
2342
|
+
*/
|
|
2343
|
+
seed_defer(stream, at) {
|
|
2344
|
+
this._defer.set(stream, at);
|
|
2345
|
+
this._defer.schedule();
|
|
2346
|
+
}
|
|
2295
2347
|
/** Read-only flag — true while a commit / reset is unprocessed. */
|
|
2296
2348
|
get armed() {
|
|
2297
2349
|
return this._armed;
|
|
@@ -2840,6 +2892,7 @@ var Act = class {
|
|
|
2840
2892
|
this._drain = options.drain !== false;
|
|
2841
2893
|
this._breaker = this._build_breaker(options);
|
|
2842
2894
|
this._drain_controllers = this._build_drain_controllers(options, lanes);
|
|
2895
|
+
this._advise_orphaned_lanes(options, lanes);
|
|
2843
2896
|
this._audit_deps = this._build_audit_deps();
|
|
2844
2897
|
this._correlate = this._build_correlate(options, classification);
|
|
2845
2898
|
this._settle = this._build_settle(options);
|
|
@@ -2932,6 +2985,31 @@ var Act = class {
|
|
|
2932
2985
|
}
|
|
2933
2986
|
return controllers;
|
|
2934
2987
|
}
|
|
2988
|
+
/**
|
|
2989
|
+
* Orphaned-lane startup advisory (#1220). When `onlyLanes` is set, this
|
|
2990
|
+
* instance builds a controller only for its slice of the declared lane
|
|
2991
|
+
* universe — every OTHER declared lane's stream is persisted (correlate
|
|
2992
|
+
* subscribes all static targets regardless of `onlyLanes`) but never
|
|
2993
|
+
* claimed here. If no peer worker deploys with those lanes in ITS
|
|
2994
|
+
* `onlyLanes`, their reactions accumulate forever, silently. A single
|
|
2995
|
+
* process can't verify the cluster invariant `∪ onlyLanes ⊇ declared
|
|
2996
|
+
* lanes`, so we surface the per-instance signal — "these declared lanes
|
|
2997
|
+
* have no controller here" — the same way the deprecated-event advisory
|
|
2998
|
+
* surfaces legacy events. No advisory when `onlyLanes` is unset (every
|
|
2999
|
+
* lane gets a controller) or covers every declared lane.
|
|
3000
|
+
*/
|
|
3001
|
+
_advise_orphaned_lanes(options, lanes) {
|
|
3002
|
+
if (!options.onlyLanes || options.onlyLanes.length === 0) return;
|
|
3003
|
+
const active = new Set(this._drain_controllers.keys());
|
|
3004
|
+
const orphaned = ["default", ...lanes.map((l) => l.name)].filter(
|
|
3005
|
+
(name) => !active.has(name)
|
|
3006
|
+
);
|
|
3007
|
+
if (orphaned.length === 0) return;
|
|
3008
|
+
const list = orphaned.map((name) => `"${name}"`).join(", ");
|
|
3009
|
+
this._logger.info(
|
|
3010
|
+
`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.`
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
2935
3013
|
/**
|
|
2936
3014
|
* Audit deps bag. Snapshotted after registry classification and
|
|
2937
3015
|
* drain-controller build so the audit module sees the finalized lane
|
|
@@ -2945,7 +3023,14 @@ var Act = class {
|
|
|
2945
3023
|
event_to_state: this._event_to_state,
|
|
2946
3024
|
states: this._states,
|
|
2947
3025
|
known_events: new Set(this._event_to_state.keys()),
|
|
2948
|
-
|
|
3026
|
+
// The DECLARED lane universe — the implicit "default" plus every
|
|
3027
|
+
// `.withLane(...)` name — NOT `_drain_controllers.keys()` (#1224).
|
|
3028
|
+
// An `onlyLanes`-filtered instance builds a controller only for its
|
|
3029
|
+
// slice of lanes, so keying off the active controller set would flag
|
|
3030
|
+
// a stream correctly assigned to an excluded-but-declared lane (one
|
|
3031
|
+
// another worker drains) as `unknown-lane`. The audit reports what's
|
|
3032
|
+
// structurally routable across the cluster, not what this process runs.
|
|
3033
|
+
declared_lanes: /* @__PURE__ */ new Set(["default", ...this._lanes.map((l) => l.name)]),
|
|
2949
3034
|
routed_events: new Set(this._event_to_lanes.keys())
|
|
2950
3035
|
};
|
|
2951
3036
|
}
|
|
@@ -2966,9 +3051,36 @@ var Act = class {
|
|
|
2966
3051
|
},
|
|
2967
3052
|
// Re-scope the background `start_correlations` timer so its
|
|
2968
3053
|
// correlate resolves the scoped ports, not the singleton (#1191).
|
|
2969
|
-
this._scoped
|
|
3054
|
+
this._scoped,
|
|
3055
|
+
// Cold-start defer re-seed (#1221). Skipped on writer-only instances
|
|
3056
|
+
// (`drain: false`) — they run no local controllers to re-arm.
|
|
3057
|
+
this._drain ? () => this._seed_persisted_defers() : void 0
|
|
2970
3058
|
);
|
|
2971
3059
|
}
|
|
3060
|
+
/**
|
|
3061
|
+
* Re-seed every active lane controller's process-local defer timer from
|
|
3062
|
+
* the store's persisted `deferred_at` (#1221). Runs once at cold start,
|
|
3063
|
+
* inside `CorrelateCycle.init`, after static targets are subscribed.
|
|
3064
|
+
*
|
|
3065
|
+
* The defer timer is worker memory: empty after a restart. A stream
|
|
3066
|
+
* deferred to a future due-time (the classic case: an idle autoclose
|
|
3067
|
+
* aggregate that deferred its terminal close) is durable in the store but
|
|
3068
|
+
* has nothing in memory to re-arm the drain — the controller disarms on
|
|
3069
|
+
* the first empty claim and, since the aggregate is idle, no commit ever
|
|
3070
|
+
* re-arms it. Reading the persisted schedule and seeding the owning lane's
|
|
3071
|
+
* timer restores the wake, so the close fires at the due-time.
|
|
3072
|
+
*
|
|
3073
|
+
* Streams whose lane has no controller on this instance (excluded by
|
|
3074
|
+
* `onlyLanes`) are skipped — a peer worker owns that lane's timer.
|
|
3075
|
+
*/
|
|
3076
|
+
async _seed_persisted_defers() {
|
|
3077
|
+
const now = Date.now();
|
|
3078
|
+
await store().query_streams((pos) => {
|
|
3079
|
+
if (pos.deferred_at === void 0 || pos.deferred_at <= now) return;
|
|
3080
|
+
const controller = this._drain_controllers.get(pos.lane ?? "default");
|
|
3081
|
+
controller?.seed_defer(pos.stream, pos.deferred_at);
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
2972
3084
|
/** Settle loop driving correlate + drain to quiescence. */
|
|
2973
3085
|
_build_settle(options) {
|
|
2974
3086
|
return new SettleLoop(
|