@rotorsoft/act 1.10.2 → 1.12.0
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 +61 -2
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/state-builder.d.ts +117 -1
- package/dist/@types/builders/state-builder.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-config.d.ts +72 -0
- package/dist/@types/internal/autoclose-config.d.ts.map +1 -0
- package/dist/@types/internal/autoclose-cycle.d.ts +171 -0
- package/dist/@types/internal/autoclose-cycle.d.ts.map +1 -0
- package/dist/@types/internal/autoclose-policy.d.ts +121 -0
- package/dist/@types/internal/autoclose-policy.d.ts.map +1 -0
- package/dist/@types/internal/index.d.ts +3 -0
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +63 -0
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/registry.d.ts +24 -4
- package/dist/@types/types/registry.d.ts.map +1 -1
- package/dist/index.cjs +398 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +392 -77
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -486,57 +486,24 @@ var PASS_FACTORIES = {
|
|
|
486
486
|
"clock-anomalies": make_clock_anomalies_pass
|
|
487
487
|
};
|
|
488
488
|
|
|
489
|
-
// src/internal/
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
508
|
-
set.add(lane_name);
|
|
509
|
-
event_to_lanes.set(name, set);
|
|
510
|
-
}
|
|
511
|
-
const key = `${target}|${source ?? ""}`;
|
|
512
|
-
const existing = statics.get(key);
|
|
513
|
-
if (!existing) {
|
|
514
|
-
statics.set(key, { stream: target, source, priority, lane });
|
|
515
|
-
} else {
|
|
516
|
-
if ((existing.lane ?? void 0) !== (lane ?? void 0))
|
|
517
|
-
throw new Error(
|
|
518
|
-
`Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
|
|
519
|
-
);
|
|
520
|
-
if (priority > existing.priority) {
|
|
521
|
-
statics.set(key, { ...existing, priority });
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
const event_to_state = /* @__PURE__ */ new Map();
|
|
528
|
-
for (const merged of states.values()) {
|
|
529
|
-
for (const event_name of Object.keys(merged.events)) {
|
|
530
|
-
event_to_state.set(event_name, merged);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
return {
|
|
534
|
-
static_targets: [...statics.values()],
|
|
535
|
-
has_dynamic_resolvers,
|
|
536
|
-
reactive_events,
|
|
537
|
-
event_to_state,
|
|
538
|
-
event_to_lanes
|
|
539
|
-
};
|
|
489
|
+
// src/internal/autoclose-config.ts
|
|
490
|
+
import { z } from "zod";
|
|
491
|
+
var DEFAULT_AUTOCLOSE_CYCLE_MS = 6e4;
|
|
492
|
+
var DEFAULT_CLOSE_BATCH_SIZE = 64;
|
|
493
|
+
var DEFAULT_CLOSE_YIELD_MS = 0;
|
|
494
|
+
var AutocloseOptionsSchema = z.object({
|
|
495
|
+
autocloseCycleMs: z.number().min(1e4).max(36e5).default(DEFAULT_AUTOCLOSE_CYCLE_MS),
|
|
496
|
+
closeBatchSize: z.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
|
|
497
|
+
closeYieldMs: z.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
|
|
498
|
+
closeOnError: z.boolean().default(false)
|
|
499
|
+
});
|
|
500
|
+
function resolveAutocloseConfig(options) {
|
|
501
|
+
return AutocloseOptionsSchema.parse({
|
|
502
|
+
autocloseCycleMs: options?.autocloseCycleMs,
|
|
503
|
+
closeBatchSize: options?.closeBatchSize,
|
|
504
|
+
closeYieldMs: options?.closeYieldMs,
|
|
505
|
+
closeOnError: options?.closeOnError
|
|
506
|
+
});
|
|
540
507
|
}
|
|
541
508
|
|
|
542
509
|
// src/internal/close-cycle.ts
|
|
@@ -686,6 +653,303 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
|
|
|
686
653
|
return truncated;
|
|
687
654
|
}
|
|
688
655
|
|
|
656
|
+
// src/internal/correlator.ts
|
|
657
|
+
import { randomInt } from "crypto";
|
|
658
|
+
var BASE = 36;
|
|
659
|
+
var SEG_WIDTH = 4;
|
|
660
|
+
var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
661
|
+
function seg(n) {
|
|
662
|
+
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
663
|
+
}
|
|
664
|
+
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
665
|
+
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
666
|
+
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
667
|
+
const ts = seg(Date.now() % SEG_SPACE);
|
|
668
|
+
const rnd = seg(randomInt(SEG_SPACE));
|
|
669
|
+
return `${s}-${a}-${ts}${rnd}`;
|
|
670
|
+
};
|
|
671
|
+
function close_correlation(correlator, actor) {
|
|
672
|
+
return correlator({
|
|
673
|
+
state: "$close",
|
|
674
|
+
action: "close",
|
|
675
|
+
stream: "$close",
|
|
676
|
+
actor
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/internal/autoclose-cycle.ts
|
|
681
|
+
function yield_between_batches(ms) {
|
|
682
|
+
if (ms <= 0) return Promise.resolve();
|
|
683
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
684
|
+
}
|
|
685
|
+
async function run_autoclose_cycle(deps) {
|
|
686
|
+
const s = store();
|
|
687
|
+
let inspected = 0;
|
|
688
|
+
let evaluated = 0;
|
|
689
|
+
let predicate_errors = 0;
|
|
690
|
+
const truncated = /* @__PURE__ */ new Map();
|
|
691
|
+
const skipped = [];
|
|
692
|
+
const stats = await s.query_stats(
|
|
693
|
+
{},
|
|
694
|
+
{ count: true, exclude: [TOMBSTONE_EVENT] }
|
|
695
|
+
);
|
|
696
|
+
const flush_batch = async (candidates2) => {
|
|
697
|
+
if (candidates2.length === 0) return;
|
|
698
|
+
const result = await run_close_cycle(candidates2, {
|
|
699
|
+
reactive_events_size: deps.reactive_events_size,
|
|
700
|
+
event_to_state: deps.event_to_state,
|
|
701
|
+
load: deps.load,
|
|
702
|
+
tombstone: deps.tombstone,
|
|
703
|
+
logger: deps.logger,
|
|
704
|
+
correlation: deps.correlation
|
|
705
|
+
});
|
|
706
|
+
for (const [stream, entry] of result.truncated) {
|
|
707
|
+
truncated.set(stream, entry);
|
|
708
|
+
}
|
|
709
|
+
for (const stream of result.skipped) skipped.push(stream);
|
|
710
|
+
await yield_between_batches(deps.config.closeYieldMs);
|
|
711
|
+
};
|
|
712
|
+
let candidates = [];
|
|
713
|
+
for (const [stream, { head, count }] of stats) {
|
|
714
|
+
inspected += 1;
|
|
715
|
+
const owner = deps.event_to_state.get(head.name);
|
|
716
|
+
if (!owner) continue;
|
|
717
|
+
const predicate = deps.autoclose_policy(owner.name);
|
|
718
|
+
if (!predicate) continue;
|
|
719
|
+
let close = false;
|
|
720
|
+
try {
|
|
721
|
+
close = predicate(stream, head, count ?? 0);
|
|
722
|
+
evaluated += 1;
|
|
723
|
+
} catch (err) {
|
|
724
|
+
predicate_errors += 1;
|
|
725
|
+
deps.logger.error(
|
|
726
|
+
`Autoclose predicate for state "${owner.name}" threw on stream "${stream}": ${err instanceof Error ? err.message : String(err)}`
|
|
727
|
+
);
|
|
728
|
+
close = deps.config.closeOnError;
|
|
729
|
+
}
|
|
730
|
+
if (!close) continue;
|
|
731
|
+
const archiver = deps.autoclose_archiver(owner.name);
|
|
732
|
+
const archive = archiver ? () => archiver(stream, head) : void 0;
|
|
733
|
+
candidates.push({ stream, archive });
|
|
734
|
+
if (candidates.length >= deps.config.closeBatchSize) {
|
|
735
|
+
await flush_batch(candidates);
|
|
736
|
+
candidates = [];
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
await flush_batch(candidates);
|
|
740
|
+
return {
|
|
741
|
+
inspected,
|
|
742
|
+
evaluated,
|
|
743
|
+
predicate_errors,
|
|
744
|
+
close_result: { truncated, skipped }
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
var AutocloseController = class {
|
|
748
|
+
deps;
|
|
749
|
+
_timer;
|
|
750
|
+
/**
|
|
751
|
+
* Reentrancy guard. The interval fires the next tick on cadence
|
|
752
|
+
* regardless of how long the previous one took; this flag drops
|
|
753
|
+
* overlapping ticks so the cycle stays sequential per controller.
|
|
754
|
+
*/
|
|
755
|
+
_running = false;
|
|
756
|
+
constructor(deps) {
|
|
757
|
+
this.deps = deps;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Start the cycle ticker. Returns `false` if already running so
|
|
761
|
+
* accidental double-start doesn't stack timers (matches
|
|
762
|
+
* `_correlate.start_polling` semantics).
|
|
763
|
+
*/
|
|
764
|
+
start() {
|
|
765
|
+
if (this._timer) return false;
|
|
766
|
+
this._timer = setInterval(() => {
|
|
767
|
+
this.run_once().catch((err) => {
|
|
768
|
+
this.deps.logger.error(
|
|
769
|
+
`Autoclose cycle errored: ${err instanceof Error ? err.message : String(err)}`
|
|
770
|
+
);
|
|
771
|
+
});
|
|
772
|
+
}, this.deps.config.autocloseCycleMs);
|
|
773
|
+
this._timer.unref();
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Stop the cycle ticker. Idempotent.
|
|
778
|
+
*/
|
|
779
|
+
stop() {
|
|
780
|
+
if (this._timer) {
|
|
781
|
+
clearInterval(this._timer);
|
|
782
|
+
this._timer = void 0;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Whether the ticker is currently running.
|
|
787
|
+
*/
|
|
788
|
+
get is_running() {
|
|
789
|
+
return this._timer !== void 0;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Run one cycle synchronously. The controller's interval callback
|
|
793
|
+
* delegates here; tests can invoke it directly to deterministically
|
|
794
|
+
* exercise the cycle without spinning real timers.
|
|
795
|
+
*
|
|
796
|
+
* Overlapping invocations short-circuit: if a previous tick is
|
|
797
|
+
* still running, this one drops and returns `null`. The orchestrator
|
|
798
|
+
* never sees a queue of pending ticks regardless of how slow the
|
|
799
|
+
* predicate / archive / truncate are.
|
|
800
|
+
*/
|
|
801
|
+
async run_once() {
|
|
802
|
+
if (this._running) return null;
|
|
803
|
+
this._running = true;
|
|
804
|
+
try {
|
|
805
|
+
const correlation = close_correlation(
|
|
806
|
+
this.deps.correlator,
|
|
807
|
+
this.deps.close_actor
|
|
808
|
+
);
|
|
809
|
+
const result = await run_autoclose_cycle({
|
|
810
|
+
autoclose_policy: this.deps.autoclose_policy,
|
|
811
|
+
autoclose_archiver: this.deps.autoclose_archiver,
|
|
812
|
+
event_to_state: this.deps.event_to_state,
|
|
813
|
+
reactive_events_size: this.deps.reactive_events_size,
|
|
814
|
+
load: this.deps.load,
|
|
815
|
+
tombstone: this.deps.tombstone,
|
|
816
|
+
logger: this.deps.logger,
|
|
817
|
+
config: this.deps.config,
|
|
818
|
+
correlation
|
|
819
|
+
});
|
|
820
|
+
if (result.close_result.truncated.size > 0) {
|
|
821
|
+
this.deps.on_closed(result.close_result);
|
|
822
|
+
}
|
|
823
|
+
return result;
|
|
824
|
+
} finally {
|
|
825
|
+
this._running = false;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// src/internal/autoclose-policy.ts
|
|
831
|
+
import { z as z2 } from "zod";
|
|
832
|
+
var MIN_AFTER_MS = 6e4;
|
|
833
|
+
var AfterSchema = z2.object({
|
|
834
|
+
days: z2.number({ message: "autocloses: after.days must be a number" }).positive("autocloses: after.days must be > 0")
|
|
835
|
+
}).refine((o) => o.days * 864e5 >= MIN_AFTER_MS, {
|
|
836
|
+
message: `autocloses: after resolves to < ${MIN_AFTER_MS}ms (one minute), which is too short to be a meaningful retention window`
|
|
837
|
+
});
|
|
838
|
+
var IsSchema = z2.union([
|
|
839
|
+
z2.string().min(1, "autocloses: is must be a non-empty event name"),
|
|
840
|
+
z2.array(
|
|
841
|
+
z2.string().min(1, "autocloses: is entries must be non-empty event names")
|
|
842
|
+
).min(1, "autocloses: is must include at least one event name").readonly()
|
|
843
|
+
]);
|
|
844
|
+
var ReachesSchema = z2.number({ message: "autocloses: reaches must be a number" }).int("autocloses: reaches must be an integer").min(1, "autocloses: reaches must be >= 1");
|
|
845
|
+
var OrBlockSchema = z2.object({
|
|
846
|
+
after: AfterSchema.optional(),
|
|
847
|
+
is: IsSchema.optional(),
|
|
848
|
+
reaches: ReachesSchema.optional()
|
|
849
|
+
}).strict().refine(
|
|
850
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0,
|
|
851
|
+
{
|
|
852
|
+
message: "autocloses: `or` block must include at least one of after / is / reaches"
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
var AutoclosePolicySchema = z2.object({
|
|
856
|
+
after: AfterSchema.optional(),
|
|
857
|
+
is: IsSchema.optional(),
|
|
858
|
+
reaches: ReachesSchema.optional(),
|
|
859
|
+
or: OrBlockSchema.optional()
|
|
860
|
+
}).strict().refine(
|
|
861
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0,
|
|
862
|
+
{
|
|
863
|
+
message: "autocloses: at least one of after / is / reaches / or must be specified \u2014 empty `{}` is a misconfiguration"
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
function compile_after(after) {
|
|
867
|
+
const after_ms = after.days * 864e5;
|
|
868
|
+
return (_stream, head) => Date.now() - head.created.getTime() >= after_ms;
|
|
869
|
+
}
|
|
870
|
+
function compile_is(is) {
|
|
871
|
+
const set = new Set(typeof is === "string" ? [is] : is);
|
|
872
|
+
return (_stream, head) => set.has(head.name);
|
|
873
|
+
}
|
|
874
|
+
function compile_reaches(reaches) {
|
|
875
|
+
return (_stream, _head, count) => count >= reaches;
|
|
876
|
+
}
|
|
877
|
+
function compile_autoclose_policy(options) {
|
|
878
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
879
|
+
const and_preds = [];
|
|
880
|
+
if (parsed.after) and_preds.push(compile_after(parsed.after));
|
|
881
|
+
if (parsed.is) and_preds.push(compile_is(parsed.is));
|
|
882
|
+
if (parsed.reaches) and_preds.push(compile_reaches(parsed.reaches));
|
|
883
|
+
const or_preds = [];
|
|
884
|
+
if (parsed.or) {
|
|
885
|
+
if (parsed.or.after) or_preds.push(compile_after(parsed.or.after));
|
|
886
|
+
if (parsed.or.is) or_preds.push(compile_is(parsed.or.is));
|
|
887
|
+
if (parsed.or.reaches) or_preds.push(compile_reaches(parsed.or.reaches));
|
|
888
|
+
}
|
|
889
|
+
return (stream, head, count) => {
|
|
890
|
+
if (and_preds.length > 0 && and_preds.every((p) => p(stream, head, count))) {
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
if (or_preds.some((p) => p(stream, head, count))) {
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
return false;
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/internal/build-classify.ts
|
|
901
|
+
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
902
|
+
function classify_registry(registry, states) {
|
|
903
|
+
const statics = /* @__PURE__ */ new Map();
|
|
904
|
+
const reactive_events = /* @__PURE__ */ new Set();
|
|
905
|
+
const event_to_lanes = /* @__PURE__ */ new Map();
|
|
906
|
+
let has_dynamic_resolvers = false;
|
|
907
|
+
for (const [name, register] of Object.entries(registry.events)) {
|
|
908
|
+
if (register.reactions.size > 0) reactive_events.add(name);
|
|
909
|
+
for (const reaction of register.reactions.values()) {
|
|
910
|
+
if (typeof reaction.resolver === "function") {
|
|
911
|
+
has_dynamic_resolvers = true;
|
|
912
|
+
event_to_lanes.set(name, ALL_LANES);
|
|
913
|
+
} else {
|
|
914
|
+
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
915
|
+
const lane_name = lane ?? "default";
|
|
916
|
+
const existing_lanes = event_to_lanes.get(name);
|
|
917
|
+
if (existing_lanes !== ALL_LANES) {
|
|
918
|
+
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
919
|
+
set.add(lane_name);
|
|
920
|
+
event_to_lanes.set(name, set);
|
|
921
|
+
}
|
|
922
|
+
const key = `${target}|${source ?? ""}`;
|
|
923
|
+
const existing = statics.get(key);
|
|
924
|
+
if (!existing) {
|
|
925
|
+
statics.set(key, { stream: target, source, priority, lane });
|
|
926
|
+
} else {
|
|
927
|
+
if ((existing.lane ?? void 0) !== (lane ?? void 0))
|
|
928
|
+
throw new Error(
|
|
929
|
+
`Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
|
|
930
|
+
);
|
|
931
|
+
if (priority > existing.priority) {
|
|
932
|
+
statics.set(key, { ...existing, priority });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const event_to_state = /* @__PURE__ */ new Map();
|
|
939
|
+
for (const merged of states.values()) {
|
|
940
|
+
for (const event_name of Object.keys(merged.events)) {
|
|
941
|
+
event_to_state.set(event_name, merged);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
static_targets: [...statics.values()],
|
|
946
|
+
has_dynamic_resolvers,
|
|
947
|
+
reactive_events,
|
|
948
|
+
event_to_state,
|
|
949
|
+
event_to_lanes
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
689
953
|
// src/internal/correlate-cycle.ts
|
|
690
954
|
var CorrelateCycle = class {
|
|
691
955
|
_checkpoint = -1;
|
|
@@ -814,30 +1078,6 @@ var CorrelateCycle = class {
|
|
|
814
1078
|
}
|
|
815
1079
|
};
|
|
816
1080
|
|
|
817
|
-
// src/internal/correlator.ts
|
|
818
|
-
import { randomInt } from "crypto";
|
|
819
|
-
var BASE = 36;
|
|
820
|
-
var SEG_WIDTH = 4;
|
|
821
|
-
var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
822
|
-
function seg(n) {
|
|
823
|
-
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
824
|
-
}
|
|
825
|
-
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
826
|
-
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
827
|
-
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
828
|
-
const ts = seg(Date.now() % SEG_SPACE);
|
|
829
|
-
const rnd = seg(randomInt(SEG_SPACE));
|
|
830
|
-
return `${s}-${a}-${ts}${rnd}`;
|
|
831
|
-
};
|
|
832
|
-
function close_correlation(correlator, actor) {
|
|
833
|
-
return correlator({
|
|
834
|
-
state: "$close",
|
|
835
|
-
action: "close",
|
|
836
|
-
stream: "$close",
|
|
837
|
-
actor
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
|
|
841
1081
|
// src/internal/drain-cycle.ts
|
|
842
1082
|
import { randomUUID } from "crypto";
|
|
843
1083
|
|
|
@@ -1941,6 +2181,22 @@ var Act = class {
|
|
|
1941
2181
|
registry;
|
|
1942
2182
|
/** Map of state name → state definition; populated by the builder. */
|
|
1943
2183
|
_states;
|
|
2184
|
+
/**
|
|
2185
|
+
* Resolved autoclose configuration (#837 / epic #802). Frozen at
|
|
2186
|
+
* `act().build()` via {@link resolveAutocloseConfig}.
|
|
2187
|
+
*
|
|
2188
|
+
* @internal
|
|
2189
|
+
*/
|
|
2190
|
+
_autoclose_config;
|
|
2191
|
+
/**
|
|
2192
|
+
* App-level autoclose controller. `undefined` when no state declares
|
|
2193
|
+
* `.autocloses(...)` — the controller is never constructed, so
|
|
2194
|
+
* `start_correlations()` / `stop_correlations()` skip the autoclose
|
|
2195
|
+
* lifecycle and the orchestrator pays zero per-tick cost.
|
|
2196
|
+
*
|
|
2197
|
+
* @internal
|
|
2198
|
+
*/
|
|
2199
|
+
_autoclose;
|
|
1944
2200
|
/**
|
|
1945
2201
|
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
1946
2202
|
* via {@link ActLifecycleEvents}.
|
|
@@ -2075,6 +2331,7 @@ var Act = class {
|
|
|
2075
2331
|
this._reactive_events = reactive_events;
|
|
2076
2332
|
this._listen = options.listen !== false;
|
|
2077
2333
|
this._drain = options.drain !== false;
|
|
2334
|
+
this._autoclose_config = resolveAutocloseConfig(options);
|
|
2078
2335
|
this._event_to_state = event_to_state;
|
|
2079
2336
|
this._event_to_lanes = event_to_lanes;
|
|
2080
2337
|
const all_lanes = ["default", ...lanes.map((l) => l.name)];
|
|
@@ -2138,6 +2395,26 @@ var Act = class {
|
|
|
2138
2395
|
},
|
|
2139
2396
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
2140
2397
|
);
|
|
2398
|
+
const states_with_policy = [...this._states.values()].filter(
|
|
2399
|
+
(s) => s.autoclose
|
|
2400
|
+
);
|
|
2401
|
+
this._autoclose = states_with_policy.length ? new AutocloseController({
|
|
2402
|
+
// Internal cycle takes the loose `(string) => …` signature; the
|
|
2403
|
+
// public `registry.autoclose_policy` narrows to
|
|
2404
|
+
// `keyof TStateMap & string`. Thin lambdas bridge across the
|
|
2405
|
+
// narrowing — runtime semantics are unchanged.
|
|
2406
|
+
autoclose_policy: (name) => this.registry.autoclose_policy(name),
|
|
2407
|
+
autoclose_archiver: (name) => this.registry.autoclose_archiver(name),
|
|
2408
|
+
event_to_state: this._event_to_state,
|
|
2409
|
+
reactive_events_size: this._reactive_events.size,
|
|
2410
|
+
load: this._es.load,
|
|
2411
|
+
tombstone: this._es.tombstone,
|
|
2412
|
+
logger: this._logger,
|
|
2413
|
+
config: this._autoclose_config,
|
|
2414
|
+
correlator: this._correlator,
|
|
2415
|
+
close_actor: { id: "$autoclose", name: "autoclose" },
|
|
2416
|
+
on_closed: (result) => this.emit("closed", result)
|
|
2417
|
+
}) : void 0;
|
|
2141
2418
|
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store());
|
|
2142
2419
|
dispose(() => this.shutdown());
|
|
2143
2420
|
}
|
|
@@ -2629,7 +2906,9 @@ var Act = class {
|
|
|
2629
2906
|
* @see {@link stop_correlations} to stop the worker
|
|
2630
2907
|
*/
|
|
2631
2908
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
2632
|
-
|
|
2909
|
+
const started = this._correlate.start_polling(query, frequency, callback);
|
|
2910
|
+
this._autoclose?.start();
|
|
2911
|
+
return started;
|
|
2633
2912
|
}
|
|
2634
2913
|
/**
|
|
2635
2914
|
* Stops the automatic correlation worker.
|
|
@@ -2650,6 +2929,7 @@ var Act = class {
|
|
|
2650
2929
|
*/
|
|
2651
2930
|
stop_correlations() {
|
|
2652
2931
|
this._correlate.stop_polling();
|
|
2932
|
+
this._autoclose?.stop();
|
|
2653
2933
|
}
|
|
2654
2934
|
/**
|
|
2655
2935
|
* Cancels any pending or active settle cycle.
|
|
@@ -3034,13 +3314,17 @@ function act() {
|
|
|
3034
3314
|
const _sf = /* @__PURE__ */ new Map();
|
|
3035
3315
|
const _dp = /* @__PURE__ */ new Map();
|
|
3036
3316
|
const _de = /* @__PURE__ */ new Map();
|
|
3317
|
+
const _ac = /* @__PURE__ */ new Map();
|
|
3318
|
+
const _aa = /* @__PURE__ */ new Map();
|
|
3037
3319
|
const EMPTY_DEPRECATED = /* @__PURE__ */ new Set();
|
|
3038
3320
|
const registry = {
|
|
3039
3321
|
actions: {},
|
|
3040
3322
|
events: {},
|
|
3041
3323
|
sensitive_fields: (event_name) => _sf.get(event_name) ?? [],
|
|
3042
3324
|
disclosure_predicate: (state_name) => _dp.get(state_name) ?? null,
|
|
3043
|
-
deprecated_events: (state_name) => _de.get(state_name) ?? EMPTY_DEPRECATED
|
|
3325
|
+
deprecated_events: (state_name) => _de.get(state_name) ?? EMPTY_DEPRECATED,
|
|
3326
|
+
autoclose_policy: (state_name) => _ac.get(state_name) ?? null,
|
|
3327
|
+
autoclose_archiver: (state_name) => _aa.get(state_name) ?? null
|
|
3044
3328
|
};
|
|
3045
3329
|
const pending_projections = [];
|
|
3046
3330
|
const batch_handlers = /* @__PURE__ */ new Map();
|
|
@@ -3167,6 +3451,8 @@ function act() {
|
|
|
3167
3451
|
}
|
|
3168
3452
|
for (const state2 of states.values()) {
|
|
3169
3453
|
if (state2.disclose) _dp.set(state2.name, state2.disclose);
|
|
3454
|
+
if (state2.autoclose) _ac.set(state2.name, state2.autoclose);
|
|
3455
|
+
if (state2.archive) _aa.set(state2.name, state2.archive);
|
|
3170
3456
|
const fields_by_event = /* @__PURE__ */ new Map();
|
|
3171
3457
|
for (const event_name of Object.keys(state2.events)) {
|
|
3172
3458
|
const fields = _sf.get(event_name);
|
|
@@ -3434,6 +3720,31 @@ function action_builder(state2) {
|
|
|
3434
3720
|
internal.disclose = disclose;
|
|
3435
3721
|
return builder;
|
|
3436
3722
|
},
|
|
3723
|
+
autocloses(predicate_or_policy) {
|
|
3724
|
+
let predicate;
|
|
3725
|
+
if (typeof predicate_or_policy === "function") {
|
|
3726
|
+
predicate = predicate_or_policy;
|
|
3727
|
+
} else if (predicate_or_policy !== null && typeof predicate_or_policy === "object") {
|
|
3728
|
+
predicate = compile_autoclose_policy(
|
|
3729
|
+
predicate_or_policy
|
|
3730
|
+
);
|
|
3731
|
+
} else {
|
|
3732
|
+
throw new Error(
|
|
3733
|
+
".autocloses(...) requires a function or a policy object; got " + typeof predicate_or_policy
|
|
3734
|
+
);
|
|
3735
|
+
}
|
|
3736
|
+
internal.autoclose = predicate;
|
|
3737
|
+
return builder;
|
|
3738
|
+
},
|
|
3739
|
+
archives(archive) {
|
|
3740
|
+
if (typeof archive !== "function") {
|
|
3741
|
+
throw new Error(
|
|
3742
|
+
".archives(archive) requires a function; got " + typeof archive
|
|
3743
|
+
);
|
|
3744
|
+
}
|
|
3745
|
+
internal.archive = archive;
|
|
3746
|
+
return builder;
|
|
3747
|
+
},
|
|
3437
3748
|
build() {
|
|
3438
3749
|
return internal;
|
|
3439
3750
|
}
|
|
@@ -3609,6 +3920,9 @@ export {
|
|
|
3609
3920
|
ConcurrencyError,
|
|
3610
3921
|
ConsoleLogger,
|
|
3611
3922
|
CsvFile,
|
|
3923
|
+
DEFAULT_AUTOCLOSE_CYCLE_MS,
|
|
3924
|
+
DEFAULT_CLOSE_BATCH_SIZE,
|
|
3925
|
+
DEFAULT_CLOSE_YIELD_MS,
|
|
3612
3926
|
DEFAULT_LANE,
|
|
3613
3927
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
3614
3928
|
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
@@ -3640,6 +3954,7 @@ export {
|
|
|
3640
3954
|
log,
|
|
3641
3955
|
port,
|
|
3642
3956
|
projection,
|
|
3957
|
+
resolveAutocloseConfig,
|
|
3643
3958
|
scoped,
|
|
3644
3959
|
sensitive,
|
|
3645
3960
|
sleep,
|