@rotorsoft/act 1.31.2 → 1.31.3

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.
Files changed (40) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act.d.ts.map +1 -1
  3. package/dist/@types/builders/act-builder.d.ts.map +1 -1
  4. package/dist/@types/{internal → builders}/build-classify.d.ts +1 -1
  5. package/dist/@types/{internal → builders}/build-classify.d.ts.map +1 -1
  6. package/dist/@types/builders/builder-base.d.ts +1 -1
  7. package/dist/@types/{internal → builders}/builder-utils.d.ts +1 -1
  8. package/dist/@types/{internal → builders}/builder-utils.d.ts.map +1 -1
  9. package/dist/@types/{internal → builders}/merge.d.ts +1 -1
  10. package/dist/@types/{internal → builders}/merge.d.ts.map +1 -1
  11. package/dist/@types/builders/projection-builder.d.ts.map +1 -1
  12. package/dist/@types/{internal/reactions.d.ts → builders/reaction-builder.d.ts} +15 -14
  13. package/dist/@types/builders/reaction-builder.d.ts.map +1 -0
  14. package/dist/@types/builders/slice-builder.d.ts +7 -0
  15. package/dist/@types/builders/slice-builder.d.ts.map +1 -1
  16. package/dist/@types/{internal/disposers.d.ts → disposers.d.ts} +1 -1
  17. package/dist/@types/disposers.d.ts.map +1 -0
  18. package/dist/@types/internal/defer-signal.d.ts +1 -1
  19. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  20. package/dist/@types/internal/index.d.ts +4 -7
  21. package/dist/@types/internal/index.d.ts.map +1 -1
  22. package/dist/@types/ports.d.ts +2 -10
  23. package/dist/@types/ports.d.ts.map +1 -1
  24. package/dist/@types/scoped.d.ts +86 -0
  25. package/dist/@types/scoped.d.ts.map +1 -0
  26. package/dist/{chunk-YUJ7ACSP.js → chunk-IBLR35N5.js} +27 -9
  27. package/dist/chunk-IBLR35N5.js.map +1 -0
  28. package/dist/index.cjs +597 -597
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +578 -591
  31. package/dist/index.js.map +1 -1
  32. package/dist/test/index.cjs +5 -4
  33. package/dist/test/index.cjs.map +1 -1
  34. package/dist/test/index.js +1 -1
  35. package/package.json +2 -2
  36. package/dist/@types/internal/disposers.d.ts.map +0 -1
  37. package/dist/@types/internal/reacting.d.ts +0 -17
  38. package/dist/@types/internal/reacting.d.ts.map +0 -1
  39. package/dist/@types/internal/reactions.d.ts.map +0 -1
  40. package/dist/chunk-YUJ7ACSP.js.map +0 -1
package/dist/index.js CHANGED
@@ -10,19 +10,21 @@ import {
10
10
  TOMBSTONE_EVENT,
11
11
  cache,
12
12
  config,
13
+ current_reacting,
13
14
  dateReviver,
14
15
  dispose,
15
16
  disposeAndExit,
16
17
  extend,
17
18
  is_literal_source,
18
19
  log,
20
+ make_reaction_scope,
21
+ make_run_scoped,
19
22
  port,
20
23
  register_weak_disposer,
21
- scoped,
22
24
  sleep,
23
25
  store,
24
26
  validate
25
- } from "./chunk-YUJ7ACSP.js";
27
+ } from "./chunk-IBLR35N5.js";
26
28
  import {
27
29
  ActorSchema,
28
30
  CausationEventSchema,
@@ -71,6 +73,58 @@ process.once("unhandledRejection", async (arg) => {
71
73
  // src/act.ts
72
74
  import EventEmitter from "events";
73
75
 
76
+ // src/builders/build-classify.ts
77
+ var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
78
+ function classify_registry(registry, states) {
79
+ const statics = /* @__PURE__ */ new Map();
80
+ const target_lanes = /* @__PURE__ */ new Map();
81
+ const reactive_events = /* @__PURE__ */ new Set();
82
+ const event_to_lanes = /* @__PURE__ */ new Map();
83
+ for (const [name, register] of Object.entries(registry.events)) {
84
+ if (register.reactions.size > 0) reactive_events.add(name);
85
+ for (const reaction of register.reactions.values()) {
86
+ if (typeof reaction.resolver === "function") {
87
+ event_to_lanes.set(name, ALL_LANES);
88
+ } else {
89
+ const { target, source, priority = 0, lane } = reaction.resolver;
90
+ const lane_name = lane ?? "default";
91
+ const existing_lanes = event_to_lanes.get(name);
92
+ if (existing_lanes !== ALL_LANES) {
93
+ const set = existing_lanes ?? /* @__PURE__ */ new Set();
94
+ set.add(lane_name);
95
+ event_to_lanes.set(name, set);
96
+ }
97
+ if (!target_lanes.has(target)) {
98
+ target_lanes.set(target, lane);
99
+ } else if ((target_lanes.get(target) ?? void 0) !== (lane ?? void 0)) {
100
+ throw new Error(
101
+ `Stream "${target}" has conflicting lane assignments ("${target_lanes.get(target) ?? "default"}" vs "${lane ?? "default"}")`
102
+ );
103
+ }
104
+ const key = `${target}|${source ?? ""}`;
105
+ const existing = statics.get(key);
106
+ if (!existing) {
107
+ statics.set(key, { stream: target, source, priority, lane });
108
+ } else if (priority > existing.priority) {
109
+ statics.set(key, { ...existing, priority });
110
+ }
111
+ }
112
+ }
113
+ }
114
+ const event_to_state = /* @__PURE__ */ new Map();
115
+ for (const merged of states.values()) {
116
+ for (const event_name of Object.keys(merged.events)) {
117
+ event_to_state.set(event_name, merged);
118
+ }
119
+ }
120
+ return {
121
+ static_targets: [...statics.values()],
122
+ reactive_events,
123
+ event_to_state,
124
+ event_to_lanes
125
+ };
126
+ }
127
+
74
128
  // src/internal/event-versions.ts
75
129
  var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
76
130
  function parse(name) {
@@ -822,432 +876,43 @@ function synthesize_autoclose_reactions(registry, states, config2) {
822
876
  }
823
877
  }
824
878
 
825
- // src/internal/build-classify.ts
826
- var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
827
- function classify_registry(registry, states) {
828
- const statics = /* @__PURE__ */ new Map();
829
- const target_lanes = /* @__PURE__ */ new Map();
830
- const reactive_events = /* @__PURE__ */ new Set();
831
- const event_to_lanes = /* @__PURE__ */ new Map();
832
- for (const [name, register] of Object.entries(registry.events)) {
833
- if (register.reactions.size > 0) reactive_events.add(name);
834
- for (const reaction of register.reactions.values()) {
835
- if (typeof reaction.resolver === "function") {
836
- event_to_lanes.set(name, ALL_LANES);
837
- } else {
838
- const { target, source, priority = 0, lane } = reaction.resolver;
839
- const lane_name = lane ?? "default";
840
- const existing_lanes = event_to_lanes.get(name);
841
- if (existing_lanes !== ALL_LANES) {
842
- const set = existing_lanes ?? /* @__PURE__ */ new Set();
843
- set.add(lane_name);
844
- event_to_lanes.set(name, set);
845
- }
846
- if (!target_lanes.has(target)) {
847
- target_lanes.set(target, lane);
848
- } else if ((target_lanes.get(target) ?? void 0) !== (lane ?? void 0)) {
849
- throw new Error(
850
- `Stream "${target}" has conflicting lane assignments ("${target_lanes.get(target) ?? "default"}" vs "${lane ?? "default"}")`
851
- );
852
- }
853
- const key = `${target}|${source ?? ""}`;
854
- const existing = statics.get(key);
855
- if (!existing) {
856
- statics.set(key, { stream: target, source, priority, lane });
857
- } else if (priority > existing.priority) {
858
- statics.set(key, { ...existing, priority });
859
- }
860
- }
861
- }
862
- }
863
- const event_to_state = /* @__PURE__ */ new Map();
864
- for (const merged of states.values()) {
865
- for (const event_name of Object.keys(merged.events)) {
866
- event_to_state.set(event_name, merged);
879
+ // src/internal/backoff.ts
880
+ function compute_backoff_delay(retry, opts) {
881
+ if (!opts || opts.baseMs <= 0) return 0;
882
+ const r = Math.max(0, retry);
883
+ let delay;
884
+ switch (opts.strategy) {
885
+ case "fixed":
886
+ delay = opts.baseMs;
887
+ break;
888
+ case "linear":
889
+ delay = opts.baseMs * (r + 1);
890
+ break;
891
+ case "exponential":
892
+ delay = opts.baseMs * 2 ** r;
893
+ if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
894
+ break;
895
+ default: {
896
+ const _never = opts.strategy;
897
+ throw new Error(`unknown backoff strategy: ${String(_never)}`);
867
898
  }
868
899
  }
869
- return {
870
- static_targets: [...statics.values()],
871
- reactive_events,
872
- event_to_state,
873
- event_to_lanes
874
- };
900
+ if (opts.jitter) delay = delay * (0.5 + Math.random());
901
+ return Math.max(0, Math.floor(delay));
875
902
  }
876
903
 
877
- // src/internal/config.ts
878
- import { z as z2 } from "zod";
879
- var BackoffOptionsSchema = z2.object({
880
- strategy: z2.enum(["fixed", "linear", "exponential"]),
881
- baseMs: z2.number().min(0),
882
- maxMs: z2.number().gt(0).optional(),
883
- jitter: z2.boolean().optional()
884
- });
885
- var ReactionOptionsSchema = z2.object({
886
- blockOnError: z2.boolean(),
887
- maxRetries: z2.number().int().min(0),
888
- backoff: BackoffOptionsSchema.optional()
889
- });
890
- function resolveReactionConfig(options) {
891
- return ReactionOptionsSchema.parse(options);
892
- }
893
- var ActionOptionsSchema = z2.object({
894
- maxRetries: z2.number().int().min(0).optional(),
895
- backoff: BackoffOptionsSchema.optional()
896
- });
897
- function resolveActionConfig(options) {
898
- return ActionOptionsSchema.parse(options);
899
- }
900
- var LaneConfigSchema = z2.object({
901
- name: z2.string().min(1),
902
- leaseMillis: z2.number().min(0).optional(),
903
- streamLimit: z2.number().int().min(0).optional(),
904
- cycleMs: z2.number().min(0).optional()
905
- });
906
- function resolveLaneConfig(options) {
907
- LaneConfigSchema.parse(options);
908
- return options;
909
- }
910
- var DrainOptionsSchema = z2.object({
911
- streamLimit: z2.number().int().min(0).optional(),
912
- eventLimit: z2.number().int().min(0).optional(),
913
- leaseMillis: z2.number().min(0).optional()
914
- });
915
- function resolveDrainConfig(options) {
916
- if (options === void 0) return void 0;
917
- DrainOptionsSchema.parse(options);
918
- return options;
919
- }
920
- var SettleOptionsSchema = DrainOptionsSchema.extend({
921
- debounceMs: z2.number().min(0).optional(),
922
- // `maxPasses` defaults to Infinity (no cap) when omitted; a present value is
923
- // a non-negative integer — `0` is legal and means "run no passes".
924
- maxPasses: z2.number().int().min(0).optional()
925
- }).loose();
926
- function resolveSettleConfig(options) {
927
- if (options === void 0) return void 0;
928
- SettleOptionsSchema.parse(options);
929
- return options;
930
- }
931
- var MAX_SHUTDOWN_GRACE_MS = 3e4;
932
- var DEFAULT_SHUTDOWN_GRACE_MS = 1e4;
933
- var ShutdownOptionsSchema = z2.object({
934
- // `0` is legal and is exactly today's behavior: stop scheduling and
935
- // return without waiting.
936
- graceMs: z2.number().min(0).optional()
937
- }).loose();
938
- function resolveShutdownConfig(options) {
939
- if (options === void 0) return void 0;
940
- ShutdownOptionsSchema.parse(options);
941
- return options;
942
- }
943
- var ActOptionsSchema = z2.object({
944
- maxSubscribedStreams: z2.number().int().min(1).optional(),
945
- settleDebounceMs: z2.number().int().min(0).optional()
946
- }).loose();
947
- function resolveActConfig(options) {
948
- if (options === void 0) return void 0;
949
- ActOptionsSchema.parse(options);
950
- return options;
951
- }
952
- var DEFAULT_AUTOCLOSE_CYCLE_MINUTES = 720;
953
- var DEFAULT_CLOSE_BATCH_SIZE = 64;
954
- var DEFAULT_CLOSE_YIELD_MS = 0;
955
- var DEFAULT_AUTOCLOSE_WINDOW_TZ = "UTC";
956
- function is_valid_time_zone(tz) {
957
- try {
958
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
959
- return true;
960
- } catch {
961
- return false;
962
- }
963
- }
964
- var AutocloseWindowSchema = z2.object({
965
- start: z2.number().int().min(0).max(23, { message: "autocloseWindow.start must be an hour in [0, 23]" }),
966
- end: z2.number().int().min(0).max(23, { message: "autocloseWindow.end must be an hour in [0, 23]" }),
967
- timeZone: z2.string().refine(is_valid_time_zone, {
968
- message: "autocloseWindow.timeZone must be a valid IANA time zone"
969
- }).default(DEFAULT_AUTOCLOSE_WINDOW_TZ)
970
- }).refine((w) => w.start !== w.end, {
971
- message: "autocloseWindow.start and end must differ \u2014 an empty window disables autoclose"
972
- });
973
- var AutocloseOptionsSchema = z2.object({
974
- autocloseCycleMinutes: z2.number().int().min(1).max(1440).default(DEFAULT_AUTOCLOSE_CYCLE_MINUTES),
975
- closeBatchSize: z2.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
976
- closeYieldMs: z2.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
977
- closeOnError: z2.boolean().default(false),
978
- autocloseWindow: AutocloseWindowSchema.optional()
979
- });
980
- function resolveAutocloseConfig(options) {
981
- return AutocloseOptionsSchema.parse({
982
- autocloseCycleMinutes: options?.autocloseCycleMinutes,
983
- closeBatchSize: options?.closeBatchSize,
984
- closeYieldMs: options?.closeYieldMs,
985
- closeOnError: options?.closeOnError,
986
- autocloseWindow: options?.autocloseWindow
987
- });
988
- }
989
- var DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5;
990
- var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
991
- var CircuitBreakerOptionsSchema = z2.object({
992
- failureThreshold: z2.number().int().min(1).default(DEFAULT_CIRCUIT_FAILURE_THRESHOLD),
993
- cooldownMs: z2.number().int().min(100).max(36e5).default(DEFAULT_CIRCUIT_COOLDOWN_MS)
994
- });
995
- var resolveCircuitBreakerConfig = (options) => CircuitBreakerOptionsSchema.parse(options ?? {});
996
- var DEFAULT_FOLD_FLUSH_EVERY = 1e3;
997
- var DEFAULT_MAX_CACHED_STATES = 1e4;
998
- var FoldOptionsSchema = z2.object({
999
- flushEvery: z2.number().int().min(1).default(DEFAULT_FOLD_FLUSH_EVERY),
1000
- maxCachedStates: z2.number().int().min(1).default(DEFAULT_MAX_CACHED_STATES)
1001
- });
1002
- function resolveFoldConfig(options) {
1003
- return FoldOptionsSchema.parse(options);
1004
- }
1005
-
1006
- // src/internal/defer-config.ts
1007
- import { z as z3 } from "zod";
1008
- var DeferDurationSchema = z3.object({
1009
- days: z3.number().positive().optional(),
1010
- hours: z3.number().positive().optional(),
1011
- minutes: z3.number().positive().optional()
1012
- }).strict().refine((d) => d.days != null || d.hours != null || d.minutes != null, {
1013
- message: "defer: a duration needs at least one of days, hours, or minutes"
1014
- });
1015
- var DeferWhenSchema = z3.object({
1016
- after: DeferDurationSchema.optional(),
1017
- at: z3.date().optional()
1018
- }).strict().refine((w) => w.after === void 0 !== (w.at === void 0), {
1019
- message: "defer: specify exactly one of `after` or `at`"
1020
- });
1021
- function duration_ms(d) {
1022
- return (d.days ?? 0) * 864e5 + (d.hours ?? 0) * 36e5 + (d.minutes ?? 0) * 6e4;
1023
- }
1024
- function resolve_defer_at(when, event) {
1025
- const parsed = DeferWhenSchema.parse(when);
1026
- if (parsed.after) return event.created.getTime() + duration_ms(parsed.after);
1027
- return parsed.at.getTime();
1028
- }
1029
- function assert_defer_when(when) {
1030
- DeferWhenSchema.parse(when);
1031
- }
1032
- function make_deferred(handler, schedule) {
1033
- const call = handler;
1034
- const deferred = async (event, stream, app) => {
1035
- const when = typeof schedule === "function" ? schedule(event) : schedule;
1036
- if (Date.now() < resolve_defer_at(when, event)) throw new DeferSignal(when);
1037
- return call(event, stream, app);
1038
- };
1039
- Object.defineProperty(deferred, "name", { value: handler.name });
1040
- return deferred;
1041
- }
1042
-
1043
- // src/internal/merge.ts
1044
- import { ZodObject } from "zod";
1045
- function base_type_name(zodType) {
1046
- let t = zodType;
1047
- while (typeof t.unwrap === "function") {
1048
- t = t.unwrap();
1049
- }
1050
- return t.constructor.name;
1051
- }
1052
- function merge_schemas(existing, incoming, state_name) {
1053
- if (existing instanceof ZodObject && incoming instanceof ZodObject) {
1054
- const existing_shape = existing.shape;
1055
- const incoming_shape = incoming.shape;
1056
- for (const key of Object.keys(incoming_shape)) {
1057
- if (key in existing_shape) {
1058
- const existing_base = base_type_name(existing_shape[key]);
1059
- const incoming_base = base_type_name(incoming_shape[key]);
1060
- if (existing_base !== incoming_base) {
1061
- throw new Error(
1062
- `Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
1063
- );
1064
- }
1065
- }
1066
- }
1067
- return existing.extend(incoming_shape);
1068
- }
1069
- return existing;
1070
- }
1071
- function merge_inits(existing, incoming) {
1072
- return () => ({ ...existing(), ...incoming() });
1073
- }
1074
- function register_state(state2, states, actions, events) {
1075
- const existing = states.get(state2.name);
1076
- if (existing) {
1077
- merge_into_existing(state2, existing, states, actions, events);
1078
- } else {
1079
- register_new_state(state2, states, actions, events);
1080
- }
1081
- }
1082
- function register_new_state(state2, states, actions, events) {
1083
- states.set(state2.name, state2);
1084
- for (const name of Object.keys(state2.actions)) {
1085
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
1086
- actions[name] = state2;
1087
- }
1088
- for (const name of Object.keys(state2.events)) {
1089
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
1090
- events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
1091
- }
1092
- }
1093
- function merge_into_existing(state2, existing, states, actions, events) {
1094
- for (const name of Object.keys(state2.actions)) {
1095
- if (existing.actions[name] === state2.actions[name]) continue;
1096
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
1097
- }
1098
- for (const name of Object.keys(state2.events)) {
1099
- if (existing.events[name] === state2.events[name]) continue;
1100
- if (existing.events[name]) {
1101
- throw new Error(
1102
- `Event "${name}" in state "${state2.name}" is declared with different Zod schemas across slices. Cross-slice event schemas must reference the same instance \u2014 extract a shared schema (e.g. \`export const ${name} = z.object({ ... })\` in a shared module) and import it in every slice that declares it.`
1103
- );
1104
- }
1105
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
1106
- }
1107
- const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
1108
- const merged = {
1109
- ...existing,
1110
- state: merge_schemas(existing.state, state2.state, state2.name),
1111
- init: merge_inits(existing.init, state2.init),
1112
- events: { ...existing.events, ...state2.events },
1113
- actions: { ...existing.actions, ...state2.actions },
1114
- patch: merged_patch,
1115
- on: { ...existing.on, ...state2.on },
1116
- given: { ...existing.given, ...state2.given },
1117
- snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
1118
- throw new Error(
1119
- `Duplicate snap strategy for state "${state2.name}"`
1120
- );
1121
- })() : state2.snap || existing.snap
1122
- };
1123
- states.set(state2.name, merged);
1124
- for (const name of Object.keys(merged.actions)) {
1125
- actions[name] = merged;
1126
- }
1127
- for (const name of Object.keys(state2.events)) {
1128
- if (events[name]) continue;
1129
- events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
1130
- }
1131
- }
1132
- function merge_patches(existing, incoming, state_name) {
1133
- const merged = { ...existing };
1134
- for (const name of Object.keys(incoming)) {
1135
- const existing_p = existing[name];
1136
- const incoming_p = incoming[name];
1137
- if (!existing_p) {
1138
- merged[name] = incoming_p;
1139
- continue;
1140
- }
1141
- const existing_is_default = existing_p._passthrough;
1142
- const incoming_is_default = incoming_p._passthrough;
1143
- if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
1144
- throw new Error(
1145
- `Duplicate custom patch for event "${name}" in state "${state_name}"`
1146
- );
1147
- }
1148
- if (existing_is_default && !incoming_is_default) {
1149
- merged[name] = incoming_p;
1150
- }
1151
- }
1152
- return merged;
1153
- }
1154
- function merge_event_register(target, source) {
1155
- for (const [event_name, source_reg] of Object.entries(source)) {
1156
- const target_reg = target[event_name];
1157
- if (!target_reg) continue;
1158
- for (const [name, reaction] of source_reg.reactions) {
1159
- const existing = target_reg.reactions.get(name);
1160
- if (existing !== void 0 && existing !== reaction)
1161
- throw new Error(
1162
- `Duplicate reaction "${name}" for event "${event_name}". Reaction handlers are keyed by function name; rename one of them.`
1163
- );
1164
- target_reg.reactions.set(name, reaction);
1165
- }
1166
- }
1167
- }
1168
- function merge_projection(proj, events) {
1169
- for (const event_name of Object.keys(proj.events)) {
1170
- const proj_register = proj.events[event_name];
1171
- const existing = events[event_name];
1172
- if (!existing) {
1173
- events[event_name] = {
1174
- schema: proj_register.schema,
1175
- reactions: new Map(proj_register.reactions)
1176
- };
1177
- } else {
1178
- for (const [name, reaction] of proj_register.reactions) {
1179
- if ([...existing.reactions.values()].includes(reaction)) continue;
1180
- let key = name;
1181
- while (existing.reactions.has(key)) key = `${key}_p`;
1182
- existing.reactions.set(key, reaction);
1183
- }
1184
- }
1185
- }
1186
- }
1187
- var _this_ = ({ stream }) => ({
1188
- source: stream,
1189
- target: stream
1190
- });
1191
-
1192
- // src/internal/builder-utils.ts
1193
- function register_lane(config2, lanes) {
1194
- if (config2.name === DEFAULT_LANE)
1195
- throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
1196
- if (lanes.some((l) => l.name === config2.name))
1197
- throw new Error(`Lane "${config2.name}" was already declared`);
1198
- lanes.push(config2);
1199
- }
1200
- function reaction_on(event, events, builder) {
1201
- const register = (handler, options, schedule) => {
1202
- if (!handler.name)
1203
- throw new Error(
1204
- `Reaction handler for "${String(event)}" must be a named function`
1205
- );
1206
- if (events[event].reactions.has(handler.name))
1207
- throw new Error(
1208
- `Duplicate reaction "${handler.name}" for event "${String(event)}". Reaction handlers are keyed by function name; rename one of them.`
1209
- );
1210
- if (schedule && typeof schedule !== "function") assert_defer_when(schedule);
1211
- const reaction = {
1212
- handler: schedule ? make_deferred(handler, schedule) : handler,
1213
- resolver: _this_,
1214
- // #1269: validate the whole bag at the declaration site so a bad
1215
- // `maxRetries`/`backoff`/`blockOnError` throws ZodError at build,
1216
- // not a NaN gate on the first retry.
1217
- options: resolveReactionConfig({
1218
- blockOnError: options?.blockOnError ?? true,
1219
- maxRetries: options?.maxRetries ?? 3,
1220
- backoff: options?.backoff
1221
- })
1222
- };
1223
- events[event].reactions.set(handler.name, reaction);
1224
- return Object.assign(builder, {
1225
- to(resolver) {
1226
- reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
1227
- return builder;
1228
- }
1229
- });
1230
- };
1231
- return {
1232
- do: (handler, options) => register(handler, options),
1233
- defer: (schedule) => ({
1234
- do: (handler, options) => register(handler, options, schedule)
1235
- })
1236
- };
1237
- }
1238
-
1239
- // src/internal/circuit-breaker.ts
1240
- var CircuitBreaker = class {
1241
- _failures = 0;
1242
- _opened_at;
1243
- _wake;
1244
- _threshold;
1245
- _cooldown_ms;
1246
- _hooks;
1247
- constructor(config2, hooks = {}) {
1248
- this._threshold = config2.failureThreshold;
1249
- this._cooldown_ms = config2.cooldownMs;
1250
- this._hooks = hooks;
904
+ // src/internal/circuit-breaker.ts
905
+ var CircuitBreaker = class {
906
+ _failures = 0;
907
+ _opened_at;
908
+ _wake;
909
+ _threshold;
910
+ _cooldown_ms;
911
+ _hooks;
912
+ constructor(config2, hooks = {}) {
913
+ this._threshold = config2.failureThreshold;
914
+ this._cooldown_ms = config2.cooldownMs;
915
+ this._hooks = hooks;
1251
916
  }
1252
917
  /** Current state given the wall-clock `now`. */
1253
918
  state(now) {
@@ -1606,6 +1271,135 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
1606
1271
  return truncated;
1607
1272
  }
1608
1273
 
1274
+ // src/internal/config.ts
1275
+ import { z as z2 } from "zod";
1276
+ var BackoffOptionsSchema = z2.object({
1277
+ strategy: z2.enum(["fixed", "linear", "exponential"]),
1278
+ baseMs: z2.number().min(0),
1279
+ maxMs: z2.number().gt(0).optional(),
1280
+ jitter: z2.boolean().optional()
1281
+ });
1282
+ var ReactionOptionsSchema = z2.object({
1283
+ blockOnError: z2.boolean(),
1284
+ maxRetries: z2.number().int().min(0),
1285
+ backoff: BackoffOptionsSchema.optional()
1286
+ });
1287
+ function resolveReactionConfig(options) {
1288
+ return ReactionOptionsSchema.parse(options);
1289
+ }
1290
+ var ActionOptionsSchema = z2.object({
1291
+ maxRetries: z2.number().int().min(0).optional(),
1292
+ backoff: BackoffOptionsSchema.optional()
1293
+ });
1294
+ function resolveActionConfig(options) {
1295
+ return ActionOptionsSchema.parse(options);
1296
+ }
1297
+ var LaneConfigSchema = z2.object({
1298
+ name: z2.string().min(1),
1299
+ leaseMillis: z2.number().min(0).optional(),
1300
+ streamLimit: z2.number().int().min(0).optional(),
1301
+ cycleMs: z2.number().min(0).optional()
1302
+ });
1303
+ function resolveLaneConfig(options) {
1304
+ LaneConfigSchema.parse(options);
1305
+ return options;
1306
+ }
1307
+ var DrainOptionsSchema = z2.object({
1308
+ streamLimit: z2.number().int().min(0).optional(),
1309
+ eventLimit: z2.number().int().min(0).optional(),
1310
+ leaseMillis: z2.number().min(0).optional()
1311
+ });
1312
+ function resolveDrainConfig(options) {
1313
+ if (options === void 0) return void 0;
1314
+ DrainOptionsSchema.parse(options);
1315
+ return options;
1316
+ }
1317
+ var SettleOptionsSchema = DrainOptionsSchema.extend({
1318
+ debounceMs: z2.number().min(0).optional(),
1319
+ // `maxPasses` defaults to Infinity (no cap) when omitted; a present value is
1320
+ // a non-negative integer — `0` is legal and means "run no passes".
1321
+ maxPasses: z2.number().int().min(0).optional()
1322
+ }).loose();
1323
+ function resolveSettleConfig(options) {
1324
+ if (options === void 0) return void 0;
1325
+ SettleOptionsSchema.parse(options);
1326
+ return options;
1327
+ }
1328
+ var MAX_SHUTDOWN_GRACE_MS = 3e4;
1329
+ var DEFAULT_SHUTDOWN_GRACE_MS = 1e4;
1330
+ var ShutdownOptionsSchema = z2.object({
1331
+ // `0` is legal and is exactly today's behavior: stop scheduling and
1332
+ // return without waiting.
1333
+ graceMs: z2.number().min(0).optional()
1334
+ }).loose();
1335
+ function resolveShutdownConfig(options) {
1336
+ if (options === void 0) return void 0;
1337
+ ShutdownOptionsSchema.parse(options);
1338
+ return options;
1339
+ }
1340
+ var ActOptionsSchema = z2.object({
1341
+ maxSubscribedStreams: z2.number().int().min(1).optional(),
1342
+ settleDebounceMs: z2.number().int().min(0).optional()
1343
+ }).loose();
1344
+ function resolveActConfig(options) {
1345
+ if (options === void 0) return void 0;
1346
+ ActOptionsSchema.parse(options);
1347
+ return options;
1348
+ }
1349
+ var DEFAULT_AUTOCLOSE_CYCLE_MINUTES = 720;
1350
+ var DEFAULT_CLOSE_BATCH_SIZE = 64;
1351
+ var DEFAULT_CLOSE_YIELD_MS = 0;
1352
+ var DEFAULT_AUTOCLOSE_WINDOW_TZ = "UTC";
1353
+ function is_valid_time_zone(tz) {
1354
+ try {
1355
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
1356
+ return true;
1357
+ } catch {
1358
+ return false;
1359
+ }
1360
+ }
1361
+ var AutocloseWindowSchema = z2.object({
1362
+ start: z2.number().int().min(0).max(23, { message: "autocloseWindow.start must be an hour in [0, 23]" }),
1363
+ end: z2.number().int().min(0).max(23, { message: "autocloseWindow.end must be an hour in [0, 23]" }),
1364
+ timeZone: z2.string().refine(is_valid_time_zone, {
1365
+ message: "autocloseWindow.timeZone must be a valid IANA time zone"
1366
+ }).default(DEFAULT_AUTOCLOSE_WINDOW_TZ)
1367
+ }).refine((w) => w.start !== w.end, {
1368
+ message: "autocloseWindow.start and end must differ \u2014 an empty window disables autoclose"
1369
+ });
1370
+ var AutocloseOptionsSchema = z2.object({
1371
+ autocloseCycleMinutes: z2.number().int().min(1).max(1440).default(DEFAULT_AUTOCLOSE_CYCLE_MINUTES),
1372
+ closeBatchSize: z2.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
1373
+ closeYieldMs: z2.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
1374
+ closeOnError: z2.boolean().default(false),
1375
+ autocloseWindow: AutocloseWindowSchema.optional()
1376
+ });
1377
+ function resolveAutocloseConfig(options) {
1378
+ return AutocloseOptionsSchema.parse({
1379
+ autocloseCycleMinutes: options?.autocloseCycleMinutes,
1380
+ closeBatchSize: options?.closeBatchSize,
1381
+ closeYieldMs: options?.closeYieldMs,
1382
+ closeOnError: options?.closeOnError,
1383
+ autocloseWindow: options?.autocloseWindow
1384
+ });
1385
+ }
1386
+ var DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5;
1387
+ var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
1388
+ var CircuitBreakerOptionsSchema = z2.object({
1389
+ failureThreshold: z2.number().int().min(1).default(DEFAULT_CIRCUIT_FAILURE_THRESHOLD),
1390
+ cooldownMs: z2.number().int().min(100).max(36e5).default(DEFAULT_CIRCUIT_COOLDOWN_MS)
1391
+ });
1392
+ var resolveCircuitBreakerConfig = (options) => CircuitBreakerOptionsSchema.parse(options ?? {});
1393
+ var DEFAULT_FOLD_FLUSH_EVERY = 1e3;
1394
+ var DEFAULT_MAX_CACHED_STATES = 1e4;
1395
+ var FoldOptionsSchema = z2.object({
1396
+ flushEvery: z2.number().int().min(1).default(DEFAULT_FOLD_FLUSH_EVERY),
1397
+ maxCachedStates: z2.number().int().min(1).default(DEFAULT_MAX_CACHED_STATES)
1398
+ });
1399
+ function resolveFoldConfig(options) {
1400
+ return FoldOptionsSchema.parse(options);
1401
+ }
1402
+
1609
1403
  // src/internal/correlate-cycle.ts
1610
1404
  import { createHash, randomUUID } from "crypto";
1611
1405
  var DEFAULT_COLD_START_BACK_SCAN = 1e4;
@@ -2000,6 +1794,43 @@ function close_correlation(correlator, actor) {
2000
1794
  });
2001
1795
  }
2002
1796
 
1797
+ // src/internal/defer-config.ts
1798
+ import { z as z3 } from "zod";
1799
+ var DeferDurationSchema = z3.object({
1800
+ days: z3.number().positive().optional(),
1801
+ hours: z3.number().positive().optional(),
1802
+ minutes: z3.number().positive().optional()
1803
+ }).strict().refine((d) => d.days != null || d.hours != null || d.minutes != null, {
1804
+ message: "defer: a duration needs at least one of days, hours, or minutes"
1805
+ });
1806
+ var DeferWhenSchema = z3.object({
1807
+ after: DeferDurationSchema.optional(),
1808
+ at: z3.date().optional()
1809
+ }).strict().refine((w) => w.after === void 0 !== (w.at === void 0), {
1810
+ message: "defer: specify exactly one of `after` or `at`"
1811
+ });
1812
+ function duration_ms(d) {
1813
+ return (d.days ?? 0) * 864e5 + (d.hours ?? 0) * 36e5 + (d.minutes ?? 0) * 6e4;
1814
+ }
1815
+ function resolve_defer_at(when, event) {
1816
+ const parsed = DeferWhenSchema.parse(when);
1817
+ if (parsed.after) return event.created.getTime() + duration_ms(parsed.after);
1818
+ return parsed.at.getTime();
1819
+ }
1820
+ function assert_defer_when(when) {
1821
+ DeferWhenSchema.parse(when);
1822
+ }
1823
+ function make_deferred(handler, schedule) {
1824
+ const call = handler;
1825
+ const deferred = async (event, stream, app) => {
1826
+ const when = typeof schedule === "function" ? schedule(event) : schedule;
1827
+ if (Date.now() < resolve_defer_at(when, event)) throw new DeferSignal(when);
1828
+ return call(event, stream, app);
1829
+ };
1830
+ Object.defineProperty(deferred, "name", { value: handler.name });
1831
+ return deferred;
1832
+ }
1833
+
2003
1834
  // src/internal/drain-cycle.ts
2004
1835
  import { randomUUID as randomUUID2 } from "crypto";
2005
1836
 
@@ -2127,37 +1958,6 @@ var subscribe = (streams, correlated_at, correlator) => store().subscribe(stream
2127
1958
 
2128
1959
  // src/internal/event-sourcing.ts
2129
1960
  import { patch } from "@rotorsoft/act-patch";
2130
-
2131
- // src/internal/backoff.ts
2132
- function compute_backoff_delay(retry, opts) {
2133
- if (!opts || opts.baseMs <= 0) return 0;
2134
- const r = Math.max(0, retry);
2135
- let delay;
2136
- switch (opts.strategy) {
2137
- case "fixed":
2138
- delay = opts.baseMs;
2139
- break;
2140
- case "linear":
2141
- delay = opts.baseMs * (r + 1);
2142
- break;
2143
- case "exponential":
2144
- delay = opts.baseMs * 2 ** r;
2145
- if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
2146
- break;
2147
- default: {
2148
- const _never = opts.strategy;
2149
- throw new Error(`unknown backoff strategy: ${String(_never)}`);
2150
- }
2151
- }
2152
- if (opts.jitter) delay = delay * (0.5 + Math.random());
2153
- return Math.max(0, Math.floor(delay));
2154
- }
2155
-
2156
- // src/internal/reacting.ts
2157
- import { AsyncLocalStorage } from "async_hooks";
2158
- var reacting = new AsyncLocalStorage();
2159
-
2160
- // src/internal/event-sourcing.ts
2161
1961
  var bare_patch = (_me, state2, partial) => patch(state2, partial);
2162
1962
  var validating_patch = (me, state2, partial, event) => {
2163
1963
  const next = patch(state2, partial);
@@ -2414,7 +2214,7 @@ async function load(me, target, callback, patch_fn = bare_patch) {
2414
2214
  async function action(me, action2, target, payload, options, patch_fn = bare_patch) {
2415
2215
  const { stream, expectedVersion, actor } = target;
2416
2216
  if (!stream) throw new Error("Missing target stream");
2417
- const reactingTo = options?.reactingTo ?? reacting.getStore();
2217
+ const reactingTo = options?.reactingTo;
2418
2218
  const correlator = options?.correlator ?? default_correlator;
2419
2219
  const validated = validate(action2, payload, me.actions[action2]);
2420
2220
  const opts = me.options?.[action2];
@@ -3063,131 +2863,6 @@ function make_fold_handler(me, flush, config2, patch_fn = bare_patch, sensitive_
3063
2863
  return handler;
3064
2864
  }
3065
2865
 
3066
- // src/internal/reactions.ts
3067
- function finalize(lease, handled, at, error, options, logger, failed_at) {
3068
- if (!error) return { lease, handled, acked_at: at };
3069
- logger.error(error);
3070
- const non_retryable = error instanceof NonRetryableError;
3071
- const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
3072
- if (block2)
3073
- logger.error(
3074
- non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
3075
- );
3076
- const next_attempt_at = !block2 && options.backoff ? Date.now() + compute_backoff_delay(lease.retry, options.backoff) : void 0;
3077
- return {
3078
- lease,
3079
- handled,
3080
- acked_at: at,
3081
- error: error.message,
3082
- block: block2,
3083
- next_attempt_at,
3084
- failed_at
3085
- };
3086
- }
3087
- function build_handle(deps) {
3088
- const {
3089
- logger,
3090
- bound_do,
3091
- bound_load,
3092
- bound_query,
3093
- bound_query_array,
3094
- bound_forget
3095
- } = deps;
3096
- return async (lease, payloads) => {
3097
- if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
3098
- const stream = lease.stream;
3099
- const last_index_of = /* @__PURE__ */ new Map();
3100
- for (let i = 0; i < payloads.length; i++)
3101
- last_index_of.set(payloads[i].event.id, i);
3102
- let at = lease.at;
3103
- let handled = 0;
3104
- if (lease.retry > 0)
3105
- logger.warn(
3106
- `Retrying ${stream}@${payloads.at(0).event.id} (${lease.retry}).`
3107
- );
3108
- const scoped_app = {
3109
- do: bound_do,
3110
- load: bound_load,
3111
- query: bound_query,
3112
- query_array: bound_query_array,
3113
- forget: bound_forget
3114
- };
3115
- for (let i = 0; i < payloads.length; i++) {
3116
- const payload = payloads[i];
3117
- const { event, handler } = payload;
3118
- try {
3119
- await reacting.run(
3120
- event,
3121
- () => handler(event, stream, scoped_app)
3122
- );
3123
- if (last_index_of.get(event.id) === i) {
3124
- at = event.id;
3125
- handled++;
3126
- }
3127
- } catch (error) {
3128
- if (error instanceof DeferSignal)
3129
- return {
3130
- lease,
3131
- handled,
3132
- acked_at: at,
3133
- defer: resolve_defer_at(error.when, event)
3134
- };
3135
- if (error instanceof CloseSignal)
3136
- return {
3137
- lease,
3138
- handled: handled + 1,
3139
- // Advance to the live head the handler evaluated against (when
3140
- // provided) so the close-cycle guard sees this reaction caught up.
3141
- acked_at: error.at ?? event.id,
3142
- // Close the signalled stream (the autoclose reaction's aggregate,
3143
- // which differs from its synthetic lease stream); a self-closing
3144
- // user reaction omits it and closes its own lease stream. A
3145
- // carried `before` makes it a windowed close (prune, not retire).
3146
- close: {
3147
- stream: error.stream ?? stream,
3148
- archive: error.archive,
3149
- before: error.before
3150
- }
3151
- };
3152
- return finalize(
3153
- lease,
3154
- handled,
3155
- at,
3156
- error,
3157
- payload.options,
3158
- logger,
3159
- event.id
3160
- );
3161
- }
3162
- }
3163
- return finalize(lease, handled, at, void 0, payloads[0].options, logger);
3164
- };
3165
- }
3166
- function build_handle_batch(logger) {
3167
- return async (lease, payloads, batchHandler) => {
3168
- const stream = lease.stream;
3169
- const events = payloads.map(
3170
- (p) => p.event
3171
- );
3172
- const options = payloads[0].options;
3173
- if (lease.retry > 0)
3174
- logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
3175
- try {
3176
- await batchHandler(events, stream);
3177
- return finalize(
3178
- lease,
3179
- events.length,
3180
- events.at(-1).id,
3181
- void 0,
3182
- options,
3183
- logger
3184
- );
3185
- } catch (error) {
3186
- return finalize(lease, 0, lease.at, error, options, logger);
3187
- }
3188
- };
3189
- }
3190
-
3191
2866
  // src/internal/settle.ts
3192
2867
  var SettleLoop = class {
3193
2868
  _timer = void 0;
@@ -3298,8 +2973,119 @@ var SettleLoop = class {
3298
2973
  clearTimeout(this._timer);
3299
2974
  this._timer = void 0;
3300
2975
  }
3301
- }
3302
- };
2976
+ }
2977
+ };
2978
+
2979
+ // src/builders/reaction-builder.ts
2980
+ function finalize(lease, handled, at, error, options, logger, failed_at) {
2981
+ if (!error) return { lease, handled, acked_at: at };
2982
+ logger.error(error);
2983
+ const non_retryable = error instanceof NonRetryableError;
2984
+ const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
2985
+ if (block2)
2986
+ logger.error(
2987
+ non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
2988
+ );
2989
+ const next_attempt_at = !block2 && options.backoff ? Date.now() + compute_backoff_delay(lease.retry, options.backoff) : void 0;
2990
+ return {
2991
+ lease,
2992
+ handled,
2993
+ acked_at: at,
2994
+ error: error.message,
2995
+ block: block2,
2996
+ next_attempt_at,
2997
+ failed_at
2998
+ };
2999
+ }
3000
+ function build_handle(deps) {
3001
+ const { logger, reaction_scope } = deps;
3002
+ return async (lease, payloads) => {
3003
+ if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
3004
+ const stream = lease.stream;
3005
+ const last_index_of = /* @__PURE__ */ new Map();
3006
+ for (let i = 0; i < payloads.length; i++)
3007
+ last_index_of.set(payloads[i].event.id, i);
3008
+ let at = lease.at;
3009
+ let handled = 0;
3010
+ if (lease.retry > 0)
3011
+ logger.warn(
3012
+ `Retrying ${stream}@${payloads.at(0).event.id} (${lease.retry}).`
3013
+ );
3014
+ for (let i = 0; i < payloads.length; i++) {
3015
+ const payload = payloads[i];
3016
+ const { event, handler } = payload;
3017
+ try {
3018
+ await reaction_scope.run(
3019
+ event,
3020
+ () => handler(event, stream, reaction_scope.app)
3021
+ );
3022
+ if (last_index_of.get(event.id) === i) {
3023
+ at = event.id;
3024
+ handled++;
3025
+ }
3026
+ } catch (error) {
3027
+ if (error instanceof DeferSignal)
3028
+ return {
3029
+ lease,
3030
+ handled,
3031
+ acked_at: at,
3032
+ defer: resolve_defer_at(error.when, event)
3033
+ };
3034
+ if (error instanceof CloseSignal)
3035
+ return {
3036
+ lease,
3037
+ handled: handled + 1,
3038
+ // Advance to the live head the handler evaluated against (when
3039
+ // provided) so the close-cycle guard sees this reaction caught up.
3040
+ acked_at: error.at ?? event.id,
3041
+ // Close the signalled stream (the autoclose reaction's aggregate,
3042
+ // which differs from its synthetic lease stream); a self-closing
3043
+ // user reaction omits it and closes its own lease stream. A
3044
+ // carried `before` makes it a windowed close (prune, not retire).
3045
+ close: {
3046
+ stream: error.stream ?? stream,
3047
+ archive: error.archive,
3048
+ before: error.before
3049
+ }
3050
+ };
3051
+ return finalize(
3052
+ lease,
3053
+ handled,
3054
+ at,
3055
+ error,
3056
+ payload.options,
3057
+ logger,
3058
+ event.id
3059
+ );
3060
+ }
3061
+ }
3062
+ return finalize(lease, handled, at, void 0, payloads[0].options, logger);
3063
+ };
3064
+ }
3065
+ function build_handle_batch(logger) {
3066
+ return async (lease, payloads, batchHandler) => {
3067
+ const stream = lease.stream;
3068
+ const events = payloads.map(
3069
+ (p) => p.event
3070
+ );
3071
+ const options = payloads[0].options;
3072
+ if (lease.retry > 0)
3073
+ logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
3074
+ try {
3075
+ await batchHandler(events, stream);
3076
+ return finalize(
3077
+ lease,
3078
+ events.length,
3079
+ events.at(-1).id,
3080
+ void 0,
3081
+ options,
3082
+ logger
3083
+ );
3084
+ } catch (error) {
3085
+ return finalize(lease, 0, lease.at, error, options, logger);
3086
+ }
3087
+ };
3088
+ }
3303
3089
 
3304
3090
  // src/act.ts
3305
3091
  var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
@@ -3532,17 +3318,21 @@ var Act = class {
3532
3318
  this._batch_handlers = batch_handlers;
3533
3319
  this._lanes = lanes;
3534
3320
  validate_only_lanes(options, lanes);
3535
- this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
3321
+ this._scoped = make_run_scoped(options.scoped);
3536
3322
  this._correlator = options.correlator ?? default_correlator;
3537
3323
  this._es = build_es(this._logger, this._correlator, patch_fn);
3538
3324
  this._cd = build_drain(this._logger);
3539
3325
  this._handle = build_handle({
3540
3326
  logger: this._logger,
3541
- bound_do: this._bound_do,
3542
- bound_load: this._bound_load,
3543
- bound_query: this._bound_query,
3544
- bound_query_array: this._bound_query_array,
3545
- bound_forget: this._bound_forget
3327
+ // The orchestrator owns ambient context; `build_handle` only asks for
3328
+ // the triggering event to be in scope while the handler runs.
3329
+ reaction_scope: make_reaction_scope({
3330
+ do: this._bound_do,
3331
+ load: this._bound_load,
3332
+ query: this._bound_query,
3333
+ query_array: this._bound_query_array,
3334
+ forget: this._bound_forget
3335
+ })
3546
3336
  });
3547
3337
  this._handle_batch = build_handle_batch(this._logger);
3548
3338
  const classification = classify_registry(this.registry, this._states);
@@ -3961,13 +3751,15 @@ var Act = class {
3961
3751
  * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
3962
3752
  */
3963
3753
  async do(action2, target, payload, options) {
3754
+ const reacting_to = options?.reactingTo ?? current_reacting();
3755
+ const do_options = reacting_to === options?.reactingTo ? options : { ...options, reactingTo: reacting_to };
3964
3756
  return this._scoped(async () => {
3965
3757
  const snapshots = await this._es.action(
3966
3758
  this.registry.actions[action2],
3967
3759
  action2,
3968
3760
  target,
3969
3761
  payload,
3970
- options
3762
+ do_options
3971
3763
  );
3972
3764
  if (this._reactive_events.size > 0)
3973
3765
  this._arm_for_event_names(
@@ -4800,6 +4592,202 @@ var Act = class {
4800
4592
  }
4801
4593
  };
4802
4594
 
4595
+ // src/builders/merge.ts
4596
+ import { ZodObject } from "zod";
4597
+ function base_type_name(zodType) {
4598
+ let t = zodType;
4599
+ while (typeof t.unwrap === "function") {
4600
+ t = t.unwrap();
4601
+ }
4602
+ return t.constructor.name;
4603
+ }
4604
+ function merge_schemas(existing, incoming, state_name) {
4605
+ if (existing instanceof ZodObject && incoming instanceof ZodObject) {
4606
+ const existing_shape = existing.shape;
4607
+ const incoming_shape = incoming.shape;
4608
+ for (const key of Object.keys(incoming_shape)) {
4609
+ if (key in existing_shape) {
4610
+ const existing_base = base_type_name(existing_shape[key]);
4611
+ const incoming_base = base_type_name(incoming_shape[key]);
4612
+ if (existing_base !== incoming_base) {
4613
+ throw new Error(
4614
+ `Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
4615
+ );
4616
+ }
4617
+ }
4618
+ }
4619
+ return existing.extend(incoming_shape);
4620
+ }
4621
+ return existing;
4622
+ }
4623
+ function merge_inits(existing, incoming) {
4624
+ return () => ({ ...existing(), ...incoming() });
4625
+ }
4626
+ function register_state(state2, states, actions, events) {
4627
+ const existing = states.get(state2.name);
4628
+ if (existing) {
4629
+ merge_into_existing(state2, existing, states, actions, events);
4630
+ } else {
4631
+ register_new_state(state2, states, actions, events);
4632
+ }
4633
+ }
4634
+ function register_new_state(state2, states, actions, events) {
4635
+ states.set(state2.name, state2);
4636
+ for (const name of Object.keys(state2.actions)) {
4637
+ if (actions[name]) throw new Error(`Duplicate action "${name}"`);
4638
+ actions[name] = state2;
4639
+ }
4640
+ for (const name of Object.keys(state2.events)) {
4641
+ if (events[name]) throw new Error(`Duplicate event "${name}"`);
4642
+ events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
4643
+ }
4644
+ }
4645
+ function merge_into_existing(state2, existing, states, actions, events) {
4646
+ for (const name of Object.keys(state2.actions)) {
4647
+ if (existing.actions[name] === state2.actions[name]) continue;
4648
+ if (actions[name]) throw new Error(`Duplicate action "${name}"`);
4649
+ }
4650
+ for (const name of Object.keys(state2.events)) {
4651
+ if (existing.events[name] === state2.events[name]) continue;
4652
+ if (existing.events[name]) {
4653
+ throw new Error(
4654
+ `Event "${name}" in state "${state2.name}" is declared with different Zod schemas across slices. Cross-slice event schemas must reference the same instance \u2014 extract a shared schema (e.g. \`export const ${name} = z.object({ ... })\` in a shared module) and import it in every slice that declares it.`
4655
+ );
4656
+ }
4657
+ if (events[name]) throw new Error(`Duplicate event "${name}"`);
4658
+ }
4659
+ const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
4660
+ const merged = {
4661
+ ...existing,
4662
+ state: merge_schemas(existing.state, state2.state, state2.name),
4663
+ init: merge_inits(existing.init, state2.init),
4664
+ events: { ...existing.events, ...state2.events },
4665
+ actions: { ...existing.actions, ...state2.actions },
4666
+ patch: merged_patch,
4667
+ on: { ...existing.on, ...state2.on },
4668
+ given: { ...existing.given, ...state2.given },
4669
+ snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
4670
+ throw new Error(
4671
+ `Duplicate snap strategy for state "${state2.name}"`
4672
+ );
4673
+ })() : state2.snap || existing.snap
4674
+ };
4675
+ states.set(state2.name, merged);
4676
+ for (const name of Object.keys(merged.actions)) {
4677
+ actions[name] = merged;
4678
+ }
4679
+ for (const name of Object.keys(state2.events)) {
4680
+ if (events[name]) continue;
4681
+ events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
4682
+ }
4683
+ }
4684
+ function merge_patches(existing, incoming, state_name) {
4685
+ const merged = { ...existing };
4686
+ for (const name of Object.keys(incoming)) {
4687
+ const existing_p = existing[name];
4688
+ const incoming_p = incoming[name];
4689
+ if (!existing_p) {
4690
+ merged[name] = incoming_p;
4691
+ continue;
4692
+ }
4693
+ const existing_is_default = existing_p._passthrough;
4694
+ const incoming_is_default = incoming_p._passthrough;
4695
+ if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
4696
+ throw new Error(
4697
+ `Duplicate custom patch for event "${name}" in state "${state_name}"`
4698
+ );
4699
+ }
4700
+ if (existing_is_default && !incoming_is_default) {
4701
+ merged[name] = incoming_p;
4702
+ }
4703
+ }
4704
+ return merged;
4705
+ }
4706
+ function merge_event_register(target, source) {
4707
+ for (const [event_name, source_reg] of Object.entries(source)) {
4708
+ const target_reg = target[event_name];
4709
+ if (!target_reg) continue;
4710
+ for (const [name, reaction] of source_reg.reactions) {
4711
+ const existing = target_reg.reactions.get(name);
4712
+ if (existing !== void 0 && existing !== reaction)
4713
+ throw new Error(
4714
+ `Duplicate reaction "${name}" for event "${event_name}". Reaction handlers are keyed by function name; rename one of them.`
4715
+ );
4716
+ target_reg.reactions.set(name, reaction);
4717
+ }
4718
+ }
4719
+ }
4720
+ function merge_projection(proj, events) {
4721
+ for (const event_name of Object.keys(proj.events)) {
4722
+ const proj_register = proj.events[event_name];
4723
+ const existing = events[event_name];
4724
+ if (!existing) {
4725
+ events[event_name] = {
4726
+ schema: proj_register.schema,
4727
+ reactions: new Map(proj_register.reactions)
4728
+ };
4729
+ } else {
4730
+ for (const [name, reaction] of proj_register.reactions) {
4731
+ if ([...existing.reactions.values()].includes(reaction)) continue;
4732
+ let key = name;
4733
+ while (existing.reactions.has(key)) key = `${key}_p`;
4734
+ existing.reactions.set(key, reaction);
4735
+ }
4736
+ }
4737
+ }
4738
+ }
4739
+ var _this_ = ({ stream }) => ({
4740
+ source: stream,
4741
+ target: stream
4742
+ });
4743
+
4744
+ // src/builders/builder-utils.ts
4745
+ function register_lane(config2, lanes) {
4746
+ if (config2.name === DEFAULT_LANE)
4747
+ throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
4748
+ if (lanes.some((l) => l.name === config2.name))
4749
+ throw new Error(`Lane "${config2.name}" was already declared`);
4750
+ lanes.push(config2);
4751
+ }
4752
+ function reaction_on(event, events, builder) {
4753
+ const register = (handler, options, schedule) => {
4754
+ if (!handler.name)
4755
+ throw new Error(
4756
+ `Reaction handler for "${String(event)}" must be a named function`
4757
+ );
4758
+ if (events[event].reactions.has(handler.name))
4759
+ throw new Error(
4760
+ `Duplicate reaction "${handler.name}" for event "${String(event)}". Reaction handlers are keyed by function name; rename one of them.`
4761
+ );
4762
+ if (schedule && typeof schedule !== "function") assert_defer_when(schedule);
4763
+ const reaction = {
4764
+ handler: schedule ? make_deferred(handler, schedule) : handler,
4765
+ resolver: _this_,
4766
+ // #1269: validate the whole bag at the declaration site so a bad
4767
+ // `maxRetries`/`backoff`/`blockOnError` throws ZodError at build,
4768
+ // not a NaN gate on the first retry.
4769
+ options: resolveReactionConfig({
4770
+ blockOnError: options?.blockOnError ?? true,
4771
+ maxRetries: options?.maxRetries ?? 3,
4772
+ backoff: options?.backoff
4773
+ })
4774
+ };
4775
+ events[event].reactions.set(handler.name, reaction);
4776
+ return Object.assign(builder, {
4777
+ to(resolver) {
4778
+ reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
4779
+ return builder;
4780
+ }
4781
+ });
4782
+ };
4783
+ return {
4784
+ do: (handler, options) => register(handler, options),
4785
+ defer: (schedule) => ({
4786
+ do: (handler, options) => register(handler, options, schedule)
4787
+ })
4788
+ };
4789
+ }
4790
+
4803
4791
  // src/builders/act-builder.ts
4804
4792
  function register_batch_handler(proj, batch_handlers) {
4805
4793
  if (!proj.batchHandler || !proj.target) return;
@@ -5584,7 +5572,6 @@ export {
5584
5572
  port,
5585
5573
  projection,
5586
5574
  resolveAutocloseConfig,
5587
- scoped,
5588
5575
  sensitive,
5589
5576
  sleep,
5590
5577
  slice,