@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.cjs CHANGED
@@ -77,7 +77,6 @@ __export(index_exports, {
77
77
  port: () => port,
78
78
  projection: () => projection,
79
79
  resolveAutocloseConfig: () => resolveAutocloseConfig,
80
- scoped: () => scoped,
81
80
  sensitive: () => sensitive,
82
81
  sleep: () => sleep,
83
82
  slice: () => slice,
@@ -87,9 +86,6 @@ __export(index_exports, {
87
86
  });
88
87
  module.exports = __toCommonJS(index_exports);
89
88
 
90
- // src/ports.ts
91
- var import_node_async_hooks = require("async_hooks");
92
-
93
89
  // src/adapters/console-logger.ts
94
90
  var LEVEL_VALUES = {
95
91
  fatal: 60,
@@ -1576,7 +1572,7 @@ var InMemoryStore = class {
1576
1572
  }
1577
1573
  };
1578
1574
 
1579
- // src/internal/disposers.ts
1575
+ // src/disposers.ts
1580
1576
  var entries = [];
1581
1577
  var prune = () => {
1582
1578
  for (let i = entries.length - 1; i >= 0; i--) {
@@ -1606,8 +1602,27 @@ var run_disposers = async () => {
1606
1602
  }
1607
1603
  };
1608
1604
 
1609
- // src/ports.ts
1605
+ // src/scoped.ts
1606
+ var import_node_async_hooks = require("async_hooks");
1610
1607
  var scoped = new import_node_async_hooks.AsyncLocalStorage();
1608
+ var reacting = new import_node_async_hooks.AsyncLocalStorage();
1609
+ function make_run_scoped(bag) {
1610
+ return bag ? (fn) => scoped.run(bag, fn) : (fn) => fn();
1611
+ }
1612
+ function current_ports() {
1613
+ return scoped.getStore();
1614
+ }
1615
+ function run_reacting(event, fn) {
1616
+ return reacting.run(event, fn);
1617
+ }
1618
+ function current_reacting() {
1619
+ return reacting.getStore();
1620
+ }
1621
+ function make_reaction_scope(app) {
1622
+ return { app, run: run_reacting };
1623
+ }
1624
+
1625
+ // src/ports.ts
1611
1626
  var ExitCodes = ["ERROR", "EXIT"];
1612
1627
  var adapters = /* @__PURE__ */ new Map();
1613
1628
  function port(injector) {
@@ -1631,13 +1646,13 @@ var _store = port(function store(adapter) {
1631
1646
  return adapter ?? new InMemoryStore();
1632
1647
  });
1633
1648
  var store2 = ((adapter) => {
1634
- return scoped.getStore()?.store ?? _store(adapter);
1649
+ return current_ports()?.store ?? _store(adapter);
1635
1650
  });
1636
1651
  var _cache = port(function cache(adapter) {
1637
1652
  return adapter ?? new InMemoryCache();
1638
1653
  });
1639
1654
  var cache2 = ((adapter) => {
1640
- return scoped.getStore()?.cache ?? _cache(adapter);
1655
+ return current_ports()?.cache ?? _cache(adapter);
1641
1656
  });
1642
1657
  async function disposeAndExit(code = "EXIT") {
1643
1658
  if (code === "ERROR" && config().env === "production") {
@@ -1683,6 +1698,58 @@ process.once("unhandledRejection", async (arg) => {
1683
1698
  // src/act.ts
1684
1699
  var import_node_events = __toESM(require("events"), 1);
1685
1700
 
1701
+ // src/builders/build-classify.ts
1702
+ var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
1703
+ function classify_registry(registry, states) {
1704
+ const statics = /* @__PURE__ */ new Map();
1705
+ const target_lanes = /* @__PURE__ */ new Map();
1706
+ const reactive_events = /* @__PURE__ */ new Set();
1707
+ const event_to_lanes = /* @__PURE__ */ new Map();
1708
+ for (const [name, register] of Object.entries(registry.events)) {
1709
+ if (register.reactions.size > 0) reactive_events.add(name);
1710
+ for (const reaction of register.reactions.values()) {
1711
+ if (typeof reaction.resolver === "function") {
1712
+ event_to_lanes.set(name, ALL_LANES);
1713
+ } else {
1714
+ const { target, source, priority = 0, lane } = reaction.resolver;
1715
+ const lane_name = lane ?? "default";
1716
+ const existing_lanes = event_to_lanes.get(name);
1717
+ if (existing_lanes !== ALL_LANES) {
1718
+ const set = existing_lanes ?? /* @__PURE__ */ new Set();
1719
+ set.add(lane_name);
1720
+ event_to_lanes.set(name, set);
1721
+ }
1722
+ if (!target_lanes.has(target)) {
1723
+ target_lanes.set(target, lane);
1724
+ } else if ((target_lanes.get(target) ?? void 0) !== (lane ?? void 0)) {
1725
+ throw new Error(
1726
+ `Stream "${target}" has conflicting lane assignments ("${target_lanes.get(target) ?? "default"}" vs "${lane ?? "default"}")`
1727
+ );
1728
+ }
1729
+ const key = `${target}|${source ?? ""}`;
1730
+ const existing = statics.get(key);
1731
+ if (!existing) {
1732
+ statics.set(key, { stream: target, source, priority, lane });
1733
+ } else if (priority > existing.priority) {
1734
+ statics.set(key, { ...existing, priority });
1735
+ }
1736
+ }
1737
+ }
1738
+ }
1739
+ const event_to_state = /* @__PURE__ */ new Map();
1740
+ for (const merged of states.values()) {
1741
+ for (const event_name of Object.keys(merged.events)) {
1742
+ event_to_state.set(event_name, merged);
1743
+ }
1744
+ }
1745
+ return {
1746
+ static_targets: [...statics.values()],
1747
+ reactive_events,
1748
+ event_to_state,
1749
+ event_to_lanes
1750
+ };
1751
+ }
1752
+
1686
1753
  // src/internal/event-versions.ts
1687
1754
  var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
1688
1755
  function parse(name) {
@@ -2434,432 +2501,43 @@ function synthesize_autoclose_reactions(registry, states, config2) {
2434
2501
  }
2435
2502
  }
2436
2503
 
2437
- // src/internal/build-classify.ts
2438
- var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
2439
- function classify_registry(registry, states) {
2440
- const statics = /* @__PURE__ */ new Map();
2441
- const target_lanes = /* @__PURE__ */ new Map();
2442
- const reactive_events = /* @__PURE__ */ new Set();
2443
- const event_to_lanes = /* @__PURE__ */ new Map();
2444
- for (const [name, register] of Object.entries(registry.events)) {
2445
- if (register.reactions.size > 0) reactive_events.add(name);
2446
- for (const reaction of register.reactions.values()) {
2447
- if (typeof reaction.resolver === "function") {
2448
- event_to_lanes.set(name, ALL_LANES);
2449
- } else {
2450
- const { target, source, priority = 0, lane } = reaction.resolver;
2451
- const lane_name = lane ?? "default";
2452
- const existing_lanes = event_to_lanes.get(name);
2453
- if (existing_lanes !== ALL_LANES) {
2454
- const set = existing_lanes ?? /* @__PURE__ */ new Set();
2455
- set.add(lane_name);
2456
- event_to_lanes.set(name, set);
2457
- }
2458
- if (!target_lanes.has(target)) {
2459
- target_lanes.set(target, lane);
2460
- } else if ((target_lanes.get(target) ?? void 0) !== (lane ?? void 0)) {
2461
- throw new Error(
2462
- `Stream "${target}" has conflicting lane assignments ("${target_lanes.get(target) ?? "default"}" vs "${lane ?? "default"}")`
2463
- );
2464
- }
2465
- const key = `${target}|${source ?? ""}`;
2466
- const existing = statics.get(key);
2467
- if (!existing) {
2468
- statics.set(key, { stream: target, source, priority, lane });
2469
- } else if (priority > existing.priority) {
2470
- statics.set(key, { ...existing, priority });
2471
- }
2472
- }
2473
- }
2474
- }
2475
- const event_to_state = /* @__PURE__ */ new Map();
2476
- for (const merged of states.values()) {
2477
- for (const event_name of Object.keys(merged.events)) {
2478
- event_to_state.set(event_name, merged);
2504
+ // src/internal/backoff.ts
2505
+ function compute_backoff_delay(retry, opts) {
2506
+ if (!opts || opts.baseMs <= 0) return 0;
2507
+ const r = Math.max(0, retry);
2508
+ let delay;
2509
+ switch (opts.strategy) {
2510
+ case "fixed":
2511
+ delay = opts.baseMs;
2512
+ break;
2513
+ case "linear":
2514
+ delay = opts.baseMs * (r + 1);
2515
+ break;
2516
+ case "exponential":
2517
+ delay = opts.baseMs * 2 ** r;
2518
+ if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
2519
+ break;
2520
+ default: {
2521
+ const _never = opts.strategy;
2522
+ throw new Error(`unknown backoff strategy: ${String(_never)}`);
2479
2523
  }
2480
2524
  }
2481
- return {
2482
- static_targets: [...statics.values()],
2483
- reactive_events,
2484
- event_to_state,
2485
- event_to_lanes
2486
- };
2525
+ if (opts.jitter) delay = delay * (0.5 + Math.random());
2526
+ return Math.max(0, Math.floor(delay));
2487
2527
  }
2488
2528
 
2489
- // src/internal/config.ts
2490
- var import_zod6 = require("zod");
2491
- var BackoffOptionsSchema = import_zod6.z.object({
2492
- strategy: import_zod6.z.enum(["fixed", "linear", "exponential"]),
2493
- baseMs: import_zod6.z.number().min(0),
2494
- maxMs: import_zod6.z.number().gt(0).optional(),
2495
- jitter: import_zod6.z.boolean().optional()
2496
- });
2497
- var ReactionOptionsSchema = import_zod6.z.object({
2498
- blockOnError: import_zod6.z.boolean(),
2499
- maxRetries: import_zod6.z.number().int().min(0),
2500
- backoff: BackoffOptionsSchema.optional()
2501
- });
2502
- function resolveReactionConfig(options) {
2503
- return ReactionOptionsSchema.parse(options);
2504
- }
2505
- var ActionOptionsSchema = import_zod6.z.object({
2506
- maxRetries: import_zod6.z.number().int().min(0).optional(),
2507
- backoff: BackoffOptionsSchema.optional()
2508
- });
2509
- function resolveActionConfig(options) {
2510
- return ActionOptionsSchema.parse(options);
2511
- }
2512
- var LaneConfigSchema = import_zod6.z.object({
2513
- name: import_zod6.z.string().min(1),
2514
- leaseMillis: import_zod6.z.number().min(0).optional(),
2515
- streamLimit: import_zod6.z.number().int().min(0).optional(),
2516
- cycleMs: import_zod6.z.number().min(0).optional()
2517
- });
2518
- function resolveLaneConfig(options) {
2519
- LaneConfigSchema.parse(options);
2520
- return options;
2521
- }
2522
- var DrainOptionsSchema = import_zod6.z.object({
2523
- streamLimit: import_zod6.z.number().int().min(0).optional(),
2524
- eventLimit: import_zod6.z.number().int().min(0).optional(),
2525
- leaseMillis: import_zod6.z.number().min(0).optional()
2526
- });
2527
- function resolveDrainConfig(options) {
2528
- if (options === void 0) return void 0;
2529
- DrainOptionsSchema.parse(options);
2530
- return options;
2531
- }
2532
- var SettleOptionsSchema = DrainOptionsSchema.extend({
2533
- debounceMs: import_zod6.z.number().min(0).optional(),
2534
- // `maxPasses` defaults to Infinity (no cap) when omitted; a present value is
2535
- // a non-negative integer — `0` is legal and means "run no passes".
2536
- maxPasses: import_zod6.z.number().int().min(0).optional()
2537
- }).loose();
2538
- function resolveSettleConfig(options) {
2539
- if (options === void 0) return void 0;
2540
- SettleOptionsSchema.parse(options);
2541
- return options;
2542
- }
2543
- var MAX_SHUTDOWN_GRACE_MS = 3e4;
2544
- var DEFAULT_SHUTDOWN_GRACE_MS = 1e4;
2545
- var ShutdownOptionsSchema = import_zod6.z.object({
2546
- // `0` is legal and is exactly today's behavior: stop scheduling and
2547
- // return without waiting.
2548
- graceMs: import_zod6.z.number().min(0).optional()
2549
- }).loose();
2550
- function resolveShutdownConfig(options) {
2551
- if (options === void 0) return void 0;
2552
- ShutdownOptionsSchema.parse(options);
2553
- return options;
2554
- }
2555
- var ActOptionsSchema = import_zod6.z.object({
2556
- maxSubscribedStreams: import_zod6.z.number().int().min(1).optional(),
2557
- settleDebounceMs: import_zod6.z.number().int().min(0).optional()
2558
- }).loose();
2559
- function resolveActConfig(options) {
2560
- if (options === void 0) return void 0;
2561
- ActOptionsSchema.parse(options);
2562
- return options;
2563
- }
2564
- var DEFAULT_AUTOCLOSE_CYCLE_MINUTES = 720;
2565
- var DEFAULT_CLOSE_BATCH_SIZE = 64;
2566
- var DEFAULT_CLOSE_YIELD_MS = 0;
2567
- var DEFAULT_AUTOCLOSE_WINDOW_TZ = "UTC";
2568
- function is_valid_time_zone(tz) {
2569
- try {
2570
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
2571
- return true;
2572
- } catch {
2573
- return false;
2574
- }
2575
- }
2576
- var AutocloseWindowSchema = import_zod6.z.object({
2577
- start: import_zod6.z.number().int().min(0).max(23, { message: "autocloseWindow.start must be an hour in [0, 23]" }),
2578
- end: import_zod6.z.number().int().min(0).max(23, { message: "autocloseWindow.end must be an hour in [0, 23]" }),
2579
- timeZone: import_zod6.z.string().refine(is_valid_time_zone, {
2580
- message: "autocloseWindow.timeZone must be a valid IANA time zone"
2581
- }).default(DEFAULT_AUTOCLOSE_WINDOW_TZ)
2582
- }).refine((w) => w.start !== w.end, {
2583
- message: "autocloseWindow.start and end must differ \u2014 an empty window disables autoclose"
2584
- });
2585
- var AutocloseOptionsSchema = import_zod6.z.object({
2586
- autocloseCycleMinutes: import_zod6.z.number().int().min(1).max(1440).default(DEFAULT_AUTOCLOSE_CYCLE_MINUTES),
2587
- closeBatchSize: import_zod6.z.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
2588
- closeYieldMs: import_zod6.z.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
2589
- closeOnError: import_zod6.z.boolean().default(false),
2590
- autocloseWindow: AutocloseWindowSchema.optional()
2591
- });
2592
- function resolveAutocloseConfig(options) {
2593
- return AutocloseOptionsSchema.parse({
2594
- autocloseCycleMinutes: options?.autocloseCycleMinutes,
2595
- closeBatchSize: options?.closeBatchSize,
2596
- closeYieldMs: options?.closeYieldMs,
2597
- closeOnError: options?.closeOnError,
2598
- autocloseWindow: options?.autocloseWindow
2599
- });
2600
- }
2601
- var DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5;
2602
- var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
2603
- var CircuitBreakerOptionsSchema = import_zod6.z.object({
2604
- failureThreshold: import_zod6.z.number().int().min(1).default(DEFAULT_CIRCUIT_FAILURE_THRESHOLD),
2605
- cooldownMs: import_zod6.z.number().int().min(100).max(36e5).default(DEFAULT_CIRCUIT_COOLDOWN_MS)
2606
- });
2607
- var resolveCircuitBreakerConfig = (options) => CircuitBreakerOptionsSchema.parse(options ?? {});
2608
- var DEFAULT_FOLD_FLUSH_EVERY = 1e3;
2609
- var DEFAULT_MAX_CACHED_STATES = 1e4;
2610
- var FoldOptionsSchema = import_zod6.z.object({
2611
- flushEvery: import_zod6.z.number().int().min(1).default(DEFAULT_FOLD_FLUSH_EVERY),
2612
- maxCachedStates: import_zod6.z.number().int().min(1).default(DEFAULT_MAX_CACHED_STATES)
2613
- });
2614
- function resolveFoldConfig(options) {
2615
- return FoldOptionsSchema.parse(options);
2616
- }
2617
-
2618
- // src/internal/defer-config.ts
2619
- var import_zod7 = require("zod");
2620
- var DeferDurationSchema = import_zod7.z.object({
2621
- days: import_zod7.z.number().positive().optional(),
2622
- hours: import_zod7.z.number().positive().optional(),
2623
- minutes: import_zod7.z.number().positive().optional()
2624
- }).strict().refine((d) => d.days != null || d.hours != null || d.minutes != null, {
2625
- message: "defer: a duration needs at least one of days, hours, or minutes"
2626
- });
2627
- var DeferWhenSchema = import_zod7.z.object({
2628
- after: DeferDurationSchema.optional(),
2629
- at: import_zod7.z.date().optional()
2630
- }).strict().refine((w) => w.after === void 0 !== (w.at === void 0), {
2631
- message: "defer: specify exactly one of `after` or `at`"
2632
- });
2633
- function duration_ms(d) {
2634
- return (d.days ?? 0) * 864e5 + (d.hours ?? 0) * 36e5 + (d.minutes ?? 0) * 6e4;
2635
- }
2636
- function resolve_defer_at(when, event) {
2637
- const parsed = DeferWhenSchema.parse(when);
2638
- if (parsed.after) return event.created.getTime() + duration_ms(parsed.after);
2639
- return parsed.at.getTime();
2640
- }
2641
- function assert_defer_when(when) {
2642
- DeferWhenSchema.parse(when);
2643
- }
2644
- function make_deferred(handler, schedule) {
2645
- const call = handler;
2646
- const deferred = async (event, stream, app) => {
2647
- const when = typeof schedule === "function" ? schedule(event) : schedule;
2648
- if (Date.now() < resolve_defer_at(when, event)) throw new DeferSignal(when);
2649
- return call(event, stream, app);
2650
- };
2651
- Object.defineProperty(deferred, "name", { value: handler.name });
2652
- return deferred;
2653
- }
2654
-
2655
- // src/internal/merge.ts
2656
- var import_zod8 = require("zod");
2657
- function base_type_name(zodType) {
2658
- let t = zodType;
2659
- while (typeof t.unwrap === "function") {
2660
- t = t.unwrap();
2661
- }
2662
- return t.constructor.name;
2663
- }
2664
- function merge_schemas(existing, incoming, state_name) {
2665
- if (existing instanceof import_zod8.ZodObject && incoming instanceof import_zod8.ZodObject) {
2666
- const existing_shape = existing.shape;
2667
- const incoming_shape = incoming.shape;
2668
- for (const key of Object.keys(incoming_shape)) {
2669
- if (key in existing_shape) {
2670
- const existing_base = base_type_name(existing_shape[key]);
2671
- const incoming_base = base_type_name(incoming_shape[key]);
2672
- if (existing_base !== incoming_base) {
2673
- throw new Error(
2674
- `Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
2675
- );
2676
- }
2677
- }
2678
- }
2679
- return existing.extend(incoming_shape);
2680
- }
2681
- return existing;
2682
- }
2683
- function merge_inits(existing, incoming) {
2684
- return () => ({ ...existing(), ...incoming() });
2685
- }
2686
- function register_state(state2, states, actions, events) {
2687
- const existing = states.get(state2.name);
2688
- if (existing) {
2689
- merge_into_existing(state2, existing, states, actions, events);
2690
- } else {
2691
- register_new_state(state2, states, actions, events);
2692
- }
2693
- }
2694
- function register_new_state(state2, states, actions, events) {
2695
- states.set(state2.name, state2);
2696
- for (const name of Object.keys(state2.actions)) {
2697
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
2698
- actions[name] = state2;
2699
- }
2700
- for (const name of Object.keys(state2.events)) {
2701
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
2702
- events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
2703
- }
2704
- }
2705
- function merge_into_existing(state2, existing, states, actions, events) {
2706
- for (const name of Object.keys(state2.actions)) {
2707
- if (existing.actions[name] === state2.actions[name]) continue;
2708
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
2709
- }
2710
- for (const name of Object.keys(state2.events)) {
2711
- if (existing.events[name] === state2.events[name]) continue;
2712
- if (existing.events[name]) {
2713
- throw new Error(
2714
- `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.`
2715
- );
2716
- }
2717
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
2718
- }
2719
- const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
2720
- const merged = {
2721
- ...existing,
2722
- state: merge_schemas(existing.state, state2.state, state2.name),
2723
- init: merge_inits(existing.init, state2.init),
2724
- events: { ...existing.events, ...state2.events },
2725
- actions: { ...existing.actions, ...state2.actions },
2726
- patch: merged_patch,
2727
- on: { ...existing.on, ...state2.on },
2728
- given: { ...existing.given, ...state2.given },
2729
- snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
2730
- throw new Error(
2731
- `Duplicate snap strategy for state "${state2.name}"`
2732
- );
2733
- })() : state2.snap || existing.snap
2734
- };
2735
- states.set(state2.name, merged);
2736
- for (const name of Object.keys(merged.actions)) {
2737
- actions[name] = merged;
2738
- }
2739
- for (const name of Object.keys(state2.events)) {
2740
- if (events[name]) continue;
2741
- events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
2742
- }
2743
- }
2744
- function merge_patches(existing, incoming, state_name) {
2745
- const merged = { ...existing };
2746
- for (const name of Object.keys(incoming)) {
2747
- const existing_p = existing[name];
2748
- const incoming_p = incoming[name];
2749
- if (!existing_p) {
2750
- merged[name] = incoming_p;
2751
- continue;
2752
- }
2753
- const existing_is_default = existing_p._passthrough;
2754
- const incoming_is_default = incoming_p._passthrough;
2755
- if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
2756
- throw new Error(
2757
- `Duplicate custom patch for event "${name}" in state "${state_name}"`
2758
- );
2759
- }
2760
- if (existing_is_default && !incoming_is_default) {
2761
- merged[name] = incoming_p;
2762
- }
2763
- }
2764
- return merged;
2765
- }
2766
- function merge_event_register(target, source) {
2767
- for (const [event_name, source_reg] of Object.entries(source)) {
2768
- const target_reg = target[event_name];
2769
- if (!target_reg) continue;
2770
- for (const [name, reaction] of source_reg.reactions) {
2771
- const existing = target_reg.reactions.get(name);
2772
- if (existing !== void 0 && existing !== reaction)
2773
- throw new Error(
2774
- `Duplicate reaction "${name}" for event "${event_name}". Reaction handlers are keyed by function name; rename one of them.`
2775
- );
2776
- target_reg.reactions.set(name, reaction);
2777
- }
2778
- }
2779
- }
2780
- function merge_projection(proj, events) {
2781
- for (const event_name of Object.keys(proj.events)) {
2782
- const proj_register = proj.events[event_name];
2783
- const existing = events[event_name];
2784
- if (!existing) {
2785
- events[event_name] = {
2786
- schema: proj_register.schema,
2787
- reactions: new Map(proj_register.reactions)
2788
- };
2789
- } else {
2790
- for (const [name, reaction] of proj_register.reactions) {
2791
- if ([...existing.reactions.values()].includes(reaction)) continue;
2792
- let key = name;
2793
- while (existing.reactions.has(key)) key = `${key}_p`;
2794
- existing.reactions.set(key, reaction);
2795
- }
2796
- }
2797
- }
2798
- }
2799
- var _this_ = ({ stream }) => ({
2800
- source: stream,
2801
- target: stream
2802
- });
2803
-
2804
- // src/internal/builder-utils.ts
2805
- function register_lane(config2, lanes) {
2806
- if (config2.name === DEFAULT_LANE)
2807
- throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
2808
- if (lanes.some((l) => l.name === config2.name))
2809
- throw new Error(`Lane "${config2.name}" was already declared`);
2810
- lanes.push(config2);
2811
- }
2812
- function reaction_on(event, events, builder) {
2813
- const register = (handler, options, schedule) => {
2814
- if (!handler.name)
2815
- throw new Error(
2816
- `Reaction handler for "${String(event)}" must be a named function`
2817
- );
2818
- if (events[event].reactions.has(handler.name))
2819
- throw new Error(
2820
- `Duplicate reaction "${handler.name}" for event "${String(event)}". Reaction handlers are keyed by function name; rename one of them.`
2821
- );
2822
- if (schedule && typeof schedule !== "function") assert_defer_when(schedule);
2823
- const reaction = {
2824
- handler: schedule ? make_deferred(handler, schedule) : handler,
2825
- resolver: _this_,
2826
- // #1269: validate the whole bag at the declaration site so a bad
2827
- // `maxRetries`/`backoff`/`blockOnError` throws ZodError at build,
2828
- // not a NaN gate on the first retry.
2829
- options: resolveReactionConfig({
2830
- blockOnError: options?.blockOnError ?? true,
2831
- maxRetries: options?.maxRetries ?? 3,
2832
- backoff: options?.backoff
2833
- })
2834
- };
2835
- events[event].reactions.set(handler.name, reaction);
2836
- return Object.assign(builder, {
2837
- to(resolver) {
2838
- reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
2839
- return builder;
2840
- }
2841
- });
2842
- };
2843
- return {
2844
- do: (handler, options) => register(handler, options),
2845
- defer: (schedule) => ({
2846
- do: (handler, options) => register(handler, options, schedule)
2847
- })
2848
- };
2849
- }
2850
-
2851
- // src/internal/circuit-breaker.ts
2852
- var CircuitBreaker = class {
2853
- _failures = 0;
2854
- _opened_at;
2855
- _wake;
2856
- _threshold;
2857
- _cooldown_ms;
2858
- _hooks;
2859
- constructor(config2, hooks = {}) {
2860
- this._threshold = config2.failureThreshold;
2861
- this._cooldown_ms = config2.cooldownMs;
2862
- this._hooks = hooks;
2529
+ // src/internal/circuit-breaker.ts
2530
+ var CircuitBreaker = class {
2531
+ _failures = 0;
2532
+ _opened_at;
2533
+ _wake;
2534
+ _threshold;
2535
+ _cooldown_ms;
2536
+ _hooks;
2537
+ constructor(config2, hooks = {}) {
2538
+ this._threshold = config2.failureThreshold;
2539
+ this._cooldown_ms = config2.cooldownMs;
2540
+ this._hooks = hooks;
2863
2541
  }
2864
2542
  /** Current state given the wall-clock `now`. */
2865
2543
  state(now) {
@@ -3218,6 +2896,135 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
3218
2896
  return truncated;
3219
2897
  }
3220
2898
 
2899
+ // src/internal/config.ts
2900
+ var import_zod6 = require("zod");
2901
+ var BackoffOptionsSchema = import_zod6.z.object({
2902
+ strategy: import_zod6.z.enum(["fixed", "linear", "exponential"]),
2903
+ baseMs: import_zod6.z.number().min(0),
2904
+ maxMs: import_zod6.z.number().gt(0).optional(),
2905
+ jitter: import_zod6.z.boolean().optional()
2906
+ });
2907
+ var ReactionOptionsSchema = import_zod6.z.object({
2908
+ blockOnError: import_zod6.z.boolean(),
2909
+ maxRetries: import_zod6.z.number().int().min(0),
2910
+ backoff: BackoffOptionsSchema.optional()
2911
+ });
2912
+ function resolveReactionConfig(options) {
2913
+ return ReactionOptionsSchema.parse(options);
2914
+ }
2915
+ var ActionOptionsSchema = import_zod6.z.object({
2916
+ maxRetries: import_zod6.z.number().int().min(0).optional(),
2917
+ backoff: BackoffOptionsSchema.optional()
2918
+ });
2919
+ function resolveActionConfig(options) {
2920
+ return ActionOptionsSchema.parse(options);
2921
+ }
2922
+ var LaneConfigSchema = import_zod6.z.object({
2923
+ name: import_zod6.z.string().min(1),
2924
+ leaseMillis: import_zod6.z.number().min(0).optional(),
2925
+ streamLimit: import_zod6.z.number().int().min(0).optional(),
2926
+ cycleMs: import_zod6.z.number().min(0).optional()
2927
+ });
2928
+ function resolveLaneConfig(options) {
2929
+ LaneConfigSchema.parse(options);
2930
+ return options;
2931
+ }
2932
+ var DrainOptionsSchema = import_zod6.z.object({
2933
+ streamLimit: import_zod6.z.number().int().min(0).optional(),
2934
+ eventLimit: import_zod6.z.number().int().min(0).optional(),
2935
+ leaseMillis: import_zod6.z.number().min(0).optional()
2936
+ });
2937
+ function resolveDrainConfig(options) {
2938
+ if (options === void 0) return void 0;
2939
+ DrainOptionsSchema.parse(options);
2940
+ return options;
2941
+ }
2942
+ var SettleOptionsSchema = DrainOptionsSchema.extend({
2943
+ debounceMs: import_zod6.z.number().min(0).optional(),
2944
+ // `maxPasses` defaults to Infinity (no cap) when omitted; a present value is
2945
+ // a non-negative integer — `0` is legal and means "run no passes".
2946
+ maxPasses: import_zod6.z.number().int().min(0).optional()
2947
+ }).loose();
2948
+ function resolveSettleConfig(options) {
2949
+ if (options === void 0) return void 0;
2950
+ SettleOptionsSchema.parse(options);
2951
+ return options;
2952
+ }
2953
+ var MAX_SHUTDOWN_GRACE_MS = 3e4;
2954
+ var DEFAULT_SHUTDOWN_GRACE_MS = 1e4;
2955
+ var ShutdownOptionsSchema = import_zod6.z.object({
2956
+ // `0` is legal and is exactly today's behavior: stop scheduling and
2957
+ // return without waiting.
2958
+ graceMs: import_zod6.z.number().min(0).optional()
2959
+ }).loose();
2960
+ function resolveShutdownConfig(options) {
2961
+ if (options === void 0) return void 0;
2962
+ ShutdownOptionsSchema.parse(options);
2963
+ return options;
2964
+ }
2965
+ var ActOptionsSchema = import_zod6.z.object({
2966
+ maxSubscribedStreams: import_zod6.z.number().int().min(1).optional(),
2967
+ settleDebounceMs: import_zod6.z.number().int().min(0).optional()
2968
+ }).loose();
2969
+ function resolveActConfig(options) {
2970
+ if (options === void 0) return void 0;
2971
+ ActOptionsSchema.parse(options);
2972
+ return options;
2973
+ }
2974
+ var DEFAULT_AUTOCLOSE_CYCLE_MINUTES = 720;
2975
+ var DEFAULT_CLOSE_BATCH_SIZE = 64;
2976
+ var DEFAULT_CLOSE_YIELD_MS = 0;
2977
+ var DEFAULT_AUTOCLOSE_WINDOW_TZ = "UTC";
2978
+ function is_valid_time_zone(tz) {
2979
+ try {
2980
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
2981
+ return true;
2982
+ } catch {
2983
+ return false;
2984
+ }
2985
+ }
2986
+ var AutocloseWindowSchema = import_zod6.z.object({
2987
+ start: import_zod6.z.number().int().min(0).max(23, { message: "autocloseWindow.start must be an hour in [0, 23]" }),
2988
+ end: import_zod6.z.number().int().min(0).max(23, { message: "autocloseWindow.end must be an hour in [0, 23]" }),
2989
+ timeZone: import_zod6.z.string().refine(is_valid_time_zone, {
2990
+ message: "autocloseWindow.timeZone must be a valid IANA time zone"
2991
+ }).default(DEFAULT_AUTOCLOSE_WINDOW_TZ)
2992
+ }).refine((w) => w.start !== w.end, {
2993
+ message: "autocloseWindow.start and end must differ \u2014 an empty window disables autoclose"
2994
+ });
2995
+ var AutocloseOptionsSchema = import_zod6.z.object({
2996
+ autocloseCycleMinutes: import_zod6.z.number().int().min(1).max(1440).default(DEFAULT_AUTOCLOSE_CYCLE_MINUTES),
2997
+ closeBatchSize: import_zod6.z.number().int().min(1).max(1024).default(DEFAULT_CLOSE_BATCH_SIZE),
2998
+ closeYieldMs: import_zod6.z.number().min(0).max(1e3).default(DEFAULT_CLOSE_YIELD_MS),
2999
+ closeOnError: import_zod6.z.boolean().default(false),
3000
+ autocloseWindow: AutocloseWindowSchema.optional()
3001
+ });
3002
+ function resolveAutocloseConfig(options) {
3003
+ return AutocloseOptionsSchema.parse({
3004
+ autocloseCycleMinutes: options?.autocloseCycleMinutes,
3005
+ closeBatchSize: options?.closeBatchSize,
3006
+ closeYieldMs: options?.closeYieldMs,
3007
+ closeOnError: options?.closeOnError,
3008
+ autocloseWindow: options?.autocloseWindow
3009
+ });
3010
+ }
3011
+ var DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5;
3012
+ var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
3013
+ var CircuitBreakerOptionsSchema = import_zod6.z.object({
3014
+ failureThreshold: import_zod6.z.number().int().min(1).default(DEFAULT_CIRCUIT_FAILURE_THRESHOLD),
3015
+ cooldownMs: import_zod6.z.number().int().min(100).max(36e5).default(DEFAULT_CIRCUIT_COOLDOWN_MS)
3016
+ });
3017
+ var resolveCircuitBreakerConfig = (options) => CircuitBreakerOptionsSchema.parse(options ?? {});
3018
+ var DEFAULT_FOLD_FLUSH_EVERY = 1e3;
3019
+ var DEFAULT_MAX_CACHED_STATES = 1e4;
3020
+ var FoldOptionsSchema = import_zod6.z.object({
3021
+ flushEvery: import_zod6.z.number().int().min(1).default(DEFAULT_FOLD_FLUSH_EVERY),
3022
+ maxCachedStates: import_zod6.z.number().int().min(1).default(DEFAULT_MAX_CACHED_STATES)
3023
+ });
3024
+ function resolveFoldConfig(options) {
3025
+ return FoldOptionsSchema.parse(options);
3026
+ }
3027
+
3221
3028
  // src/internal/correlate-cycle.ts
3222
3029
  var import_node_crypto = require("crypto");
3223
3030
  var DEFAULT_COLD_START_BACK_SCAN = 1e4;
@@ -3612,6 +3419,43 @@ function close_correlation(correlator, actor) {
3612
3419
  });
3613
3420
  }
3614
3421
 
3422
+ // src/internal/defer-config.ts
3423
+ var import_zod7 = require("zod");
3424
+ var DeferDurationSchema = import_zod7.z.object({
3425
+ days: import_zod7.z.number().positive().optional(),
3426
+ hours: import_zod7.z.number().positive().optional(),
3427
+ minutes: import_zod7.z.number().positive().optional()
3428
+ }).strict().refine((d) => d.days != null || d.hours != null || d.minutes != null, {
3429
+ message: "defer: a duration needs at least one of days, hours, or minutes"
3430
+ });
3431
+ var DeferWhenSchema = import_zod7.z.object({
3432
+ after: DeferDurationSchema.optional(),
3433
+ at: import_zod7.z.date().optional()
3434
+ }).strict().refine((w) => w.after === void 0 !== (w.at === void 0), {
3435
+ message: "defer: specify exactly one of `after` or `at`"
3436
+ });
3437
+ function duration_ms(d) {
3438
+ return (d.days ?? 0) * 864e5 + (d.hours ?? 0) * 36e5 + (d.minutes ?? 0) * 6e4;
3439
+ }
3440
+ function resolve_defer_at(when, event) {
3441
+ const parsed = DeferWhenSchema.parse(when);
3442
+ if (parsed.after) return event.created.getTime() + duration_ms(parsed.after);
3443
+ return parsed.at.getTime();
3444
+ }
3445
+ function assert_defer_when(when) {
3446
+ DeferWhenSchema.parse(when);
3447
+ }
3448
+ function make_deferred(handler, schedule) {
3449
+ const call = handler;
3450
+ const deferred = async (event, stream, app) => {
3451
+ const when = typeof schedule === "function" ? schedule(event) : schedule;
3452
+ if (Date.now() < resolve_defer_at(when, event)) throw new DeferSignal(when);
3453
+ return call(event, stream, app);
3454
+ };
3455
+ Object.defineProperty(deferred, "name", { value: handler.name });
3456
+ return deferred;
3457
+ }
3458
+
3615
3459
  // src/internal/drain-cycle.ts
3616
3460
  var import_node_crypto3 = require("crypto");
3617
3461
 
@@ -3739,37 +3583,6 @@ var subscribe = (streams, correlated_at, correlator) => store2().subscribe(strea
3739
3583
 
3740
3584
  // src/internal/event-sourcing.ts
3741
3585
  var import_act_patch = require("@rotorsoft/act-patch");
3742
-
3743
- // src/internal/backoff.ts
3744
- function compute_backoff_delay(retry, opts) {
3745
- if (!opts || opts.baseMs <= 0) return 0;
3746
- const r = Math.max(0, retry);
3747
- let delay;
3748
- switch (opts.strategy) {
3749
- case "fixed":
3750
- delay = opts.baseMs;
3751
- break;
3752
- case "linear":
3753
- delay = opts.baseMs * (r + 1);
3754
- break;
3755
- case "exponential":
3756
- delay = opts.baseMs * 2 ** r;
3757
- if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
3758
- break;
3759
- default: {
3760
- const _never = opts.strategy;
3761
- throw new Error(`unknown backoff strategy: ${String(_never)}`);
3762
- }
3763
- }
3764
- if (opts.jitter) delay = delay * (0.5 + Math.random());
3765
- return Math.max(0, Math.floor(delay));
3766
- }
3767
-
3768
- // src/internal/reacting.ts
3769
- var import_node_async_hooks2 = require("async_hooks");
3770
- var reacting = new import_node_async_hooks2.AsyncLocalStorage();
3771
-
3772
- // src/internal/event-sourcing.ts
3773
3586
  var bare_patch = (_me, state2, partial) => (0, import_act_patch.patch)(state2, partial);
3774
3587
  var validating_patch = (me, state2, partial, event) => {
3775
3588
  const next = (0, import_act_patch.patch)(state2, partial);
@@ -4026,7 +3839,7 @@ async function load(me, target, callback, patch_fn = bare_patch) {
4026
3839
  async function action(me, action2, target, payload, options, patch_fn = bare_patch) {
4027
3840
  const { stream, expectedVersion, actor } = target;
4028
3841
  if (!stream) throw new Error("Missing target stream");
4029
- const reactingTo = options?.reactingTo ?? reacting.getStore();
3842
+ const reactingTo = options?.reactingTo;
4030
3843
  const correlator = options?.correlator ?? default_correlator;
4031
3844
  const validated = validate(action2, payload, me.actions[action2]);
4032
3845
  const opts = me.options?.[action2];
@@ -4675,131 +4488,6 @@ function make_fold_handler(me, flush, config2, patch_fn = bare_patch, sensitive_
4675
4488
  return handler;
4676
4489
  }
4677
4490
 
4678
- // src/internal/reactions.ts
4679
- function finalize(lease, handled, at, error, options, logger, failed_at) {
4680
- if (!error) return { lease, handled, acked_at: at };
4681
- logger.error(error);
4682
- const non_retryable = error instanceof NonRetryableError;
4683
- const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
4684
- if (block2)
4685
- logger.error(
4686
- non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
4687
- );
4688
- const next_attempt_at = !block2 && options.backoff ? Date.now() + compute_backoff_delay(lease.retry, options.backoff) : void 0;
4689
- return {
4690
- lease,
4691
- handled,
4692
- acked_at: at,
4693
- error: error.message,
4694
- block: block2,
4695
- next_attempt_at,
4696
- failed_at
4697
- };
4698
- }
4699
- function build_handle(deps) {
4700
- const {
4701
- logger,
4702
- bound_do,
4703
- bound_load,
4704
- bound_query,
4705
- bound_query_array,
4706
- bound_forget
4707
- } = deps;
4708
- return async (lease, payloads) => {
4709
- if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
4710
- const stream = lease.stream;
4711
- const last_index_of = /* @__PURE__ */ new Map();
4712
- for (let i = 0; i < payloads.length; i++)
4713
- last_index_of.set(payloads[i].event.id, i);
4714
- let at = lease.at;
4715
- let handled = 0;
4716
- if (lease.retry > 0)
4717
- logger.warn(
4718
- `Retrying ${stream}@${payloads.at(0).event.id} (${lease.retry}).`
4719
- );
4720
- const scoped_app = {
4721
- do: bound_do,
4722
- load: bound_load,
4723
- query: bound_query,
4724
- query_array: bound_query_array,
4725
- forget: bound_forget
4726
- };
4727
- for (let i = 0; i < payloads.length; i++) {
4728
- const payload = payloads[i];
4729
- const { event, handler } = payload;
4730
- try {
4731
- await reacting.run(
4732
- event,
4733
- () => handler(event, stream, scoped_app)
4734
- );
4735
- if (last_index_of.get(event.id) === i) {
4736
- at = event.id;
4737
- handled++;
4738
- }
4739
- } catch (error) {
4740
- if (error instanceof DeferSignal)
4741
- return {
4742
- lease,
4743
- handled,
4744
- acked_at: at,
4745
- defer: resolve_defer_at(error.when, event)
4746
- };
4747
- if (error instanceof CloseSignal)
4748
- return {
4749
- lease,
4750
- handled: handled + 1,
4751
- // Advance to the live head the handler evaluated against (when
4752
- // provided) so the close-cycle guard sees this reaction caught up.
4753
- acked_at: error.at ?? event.id,
4754
- // Close the signalled stream (the autoclose reaction's aggregate,
4755
- // which differs from its synthetic lease stream); a self-closing
4756
- // user reaction omits it and closes its own lease stream. A
4757
- // carried `before` makes it a windowed close (prune, not retire).
4758
- close: {
4759
- stream: error.stream ?? stream,
4760
- archive: error.archive,
4761
- before: error.before
4762
- }
4763
- };
4764
- return finalize(
4765
- lease,
4766
- handled,
4767
- at,
4768
- error,
4769
- payload.options,
4770
- logger,
4771
- event.id
4772
- );
4773
- }
4774
- }
4775
- return finalize(lease, handled, at, void 0, payloads[0].options, logger);
4776
- };
4777
- }
4778
- function build_handle_batch(logger) {
4779
- return async (lease, payloads, batchHandler) => {
4780
- const stream = lease.stream;
4781
- const events = payloads.map(
4782
- (p) => p.event
4783
- );
4784
- const options = payloads[0].options;
4785
- if (lease.retry > 0)
4786
- logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
4787
- try {
4788
- await batchHandler(events, stream);
4789
- return finalize(
4790
- lease,
4791
- events.length,
4792
- events.at(-1).id,
4793
- void 0,
4794
- options,
4795
- logger
4796
- );
4797
- } catch (error) {
4798
- return finalize(lease, 0, lease.at, error, options, logger);
4799
- }
4800
- };
4801
- }
4802
-
4803
4491
  // src/internal/settle.ts
4804
4492
  var SettleLoop = class {
4805
4493
  _timer = void 0;
@@ -4910,8 +4598,119 @@ var SettleLoop = class {
4910
4598
  clearTimeout(this._timer);
4911
4599
  this._timer = void 0;
4912
4600
  }
4913
- }
4914
- };
4601
+ }
4602
+ };
4603
+
4604
+ // src/builders/reaction-builder.ts
4605
+ function finalize(lease, handled, at, error, options, logger, failed_at) {
4606
+ if (!error) return { lease, handled, acked_at: at };
4607
+ logger.error(error);
4608
+ const non_retryable = error instanceof NonRetryableError;
4609
+ const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
4610
+ if (block2)
4611
+ logger.error(
4612
+ non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
4613
+ );
4614
+ const next_attempt_at = !block2 && options.backoff ? Date.now() + compute_backoff_delay(lease.retry, options.backoff) : void 0;
4615
+ return {
4616
+ lease,
4617
+ handled,
4618
+ acked_at: at,
4619
+ error: error.message,
4620
+ block: block2,
4621
+ next_attempt_at,
4622
+ failed_at
4623
+ };
4624
+ }
4625
+ function build_handle(deps) {
4626
+ const { logger, reaction_scope } = deps;
4627
+ return async (lease, payloads) => {
4628
+ if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
4629
+ const stream = lease.stream;
4630
+ const last_index_of = /* @__PURE__ */ new Map();
4631
+ for (let i = 0; i < payloads.length; i++)
4632
+ last_index_of.set(payloads[i].event.id, i);
4633
+ let at = lease.at;
4634
+ let handled = 0;
4635
+ if (lease.retry > 0)
4636
+ logger.warn(
4637
+ `Retrying ${stream}@${payloads.at(0).event.id} (${lease.retry}).`
4638
+ );
4639
+ for (let i = 0; i < payloads.length; i++) {
4640
+ const payload = payloads[i];
4641
+ const { event, handler } = payload;
4642
+ try {
4643
+ await reaction_scope.run(
4644
+ event,
4645
+ () => handler(event, stream, reaction_scope.app)
4646
+ );
4647
+ if (last_index_of.get(event.id) === i) {
4648
+ at = event.id;
4649
+ handled++;
4650
+ }
4651
+ } catch (error) {
4652
+ if (error instanceof DeferSignal)
4653
+ return {
4654
+ lease,
4655
+ handled,
4656
+ acked_at: at,
4657
+ defer: resolve_defer_at(error.when, event)
4658
+ };
4659
+ if (error instanceof CloseSignal)
4660
+ return {
4661
+ lease,
4662
+ handled: handled + 1,
4663
+ // Advance to the live head the handler evaluated against (when
4664
+ // provided) so the close-cycle guard sees this reaction caught up.
4665
+ acked_at: error.at ?? event.id,
4666
+ // Close the signalled stream (the autoclose reaction's aggregate,
4667
+ // which differs from its synthetic lease stream); a self-closing
4668
+ // user reaction omits it and closes its own lease stream. A
4669
+ // carried `before` makes it a windowed close (prune, not retire).
4670
+ close: {
4671
+ stream: error.stream ?? stream,
4672
+ archive: error.archive,
4673
+ before: error.before
4674
+ }
4675
+ };
4676
+ return finalize(
4677
+ lease,
4678
+ handled,
4679
+ at,
4680
+ error,
4681
+ payload.options,
4682
+ logger,
4683
+ event.id
4684
+ );
4685
+ }
4686
+ }
4687
+ return finalize(lease, handled, at, void 0, payloads[0].options, logger);
4688
+ };
4689
+ }
4690
+ function build_handle_batch(logger) {
4691
+ return async (lease, payloads, batchHandler) => {
4692
+ const stream = lease.stream;
4693
+ const events = payloads.map(
4694
+ (p) => p.event
4695
+ );
4696
+ const options = payloads[0].options;
4697
+ if (lease.retry > 0)
4698
+ logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
4699
+ try {
4700
+ await batchHandler(events, stream);
4701
+ return finalize(
4702
+ lease,
4703
+ events.length,
4704
+ events.at(-1).id,
4705
+ void 0,
4706
+ options,
4707
+ logger
4708
+ );
4709
+ } catch (error) {
4710
+ return finalize(lease, 0, lease.at, error, options, logger);
4711
+ }
4712
+ };
4713
+ }
4915
4714
 
4916
4715
  // src/act.ts
4917
4716
  var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
@@ -5144,17 +4943,21 @@ var Act = class {
5144
4943
  this._batch_handlers = batch_handlers;
5145
4944
  this._lanes = lanes;
5146
4945
  validate_only_lanes(options, lanes);
5147
- this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
4946
+ this._scoped = make_run_scoped(options.scoped);
5148
4947
  this._correlator = options.correlator ?? default_correlator;
5149
4948
  this._es = build_es(this._logger, this._correlator, patch_fn);
5150
4949
  this._cd = build_drain(this._logger);
5151
4950
  this._handle = build_handle({
5152
4951
  logger: this._logger,
5153
- bound_do: this._bound_do,
5154
- bound_load: this._bound_load,
5155
- bound_query: this._bound_query,
5156
- bound_query_array: this._bound_query_array,
5157
- bound_forget: this._bound_forget
4952
+ // The orchestrator owns ambient context; `build_handle` only asks for
4953
+ // the triggering event to be in scope while the handler runs.
4954
+ reaction_scope: make_reaction_scope({
4955
+ do: this._bound_do,
4956
+ load: this._bound_load,
4957
+ query: this._bound_query,
4958
+ query_array: this._bound_query_array,
4959
+ forget: this._bound_forget
4960
+ })
5158
4961
  });
5159
4962
  this._handle_batch = build_handle_batch(this._logger);
5160
4963
  const classification = classify_registry(this.registry, this._states);
@@ -5573,13 +5376,15 @@ var Act = class {
5573
5376
  * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
5574
5377
  */
5575
5378
  async do(action2, target, payload, options) {
5379
+ const reacting_to = options?.reactingTo ?? current_reacting();
5380
+ const do_options = reacting_to === options?.reactingTo ? options : { ...options, reactingTo: reacting_to };
5576
5381
  return this._scoped(async () => {
5577
5382
  const snapshots = await this._es.action(
5578
5383
  this.registry.actions[action2],
5579
5384
  action2,
5580
5385
  target,
5581
5386
  payload,
5582
- options
5387
+ do_options
5583
5388
  );
5584
5389
  if (this._reactive_events.size > 0)
5585
5390
  this._arm_for_event_names(
@@ -6412,6 +6217,202 @@ var Act = class {
6412
6217
  }
6413
6218
  };
6414
6219
 
6220
+ // src/builders/merge.ts
6221
+ var import_zod8 = require("zod");
6222
+ function base_type_name(zodType) {
6223
+ let t = zodType;
6224
+ while (typeof t.unwrap === "function") {
6225
+ t = t.unwrap();
6226
+ }
6227
+ return t.constructor.name;
6228
+ }
6229
+ function merge_schemas(existing, incoming, state_name) {
6230
+ if (existing instanceof import_zod8.ZodObject && incoming instanceof import_zod8.ZodObject) {
6231
+ const existing_shape = existing.shape;
6232
+ const incoming_shape = incoming.shape;
6233
+ for (const key of Object.keys(incoming_shape)) {
6234
+ if (key in existing_shape) {
6235
+ const existing_base = base_type_name(existing_shape[key]);
6236
+ const incoming_base = base_type_name(incoming_shape[key]);
6237
+ if (existing_base !== incoming_base) {
6238
+ throw new Error(
6239
+ `Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
6240
+ );
6241
+ }
6242
+ }
6243
+ }
6244
+ return existing.extend(incoming_shape);
6245
+ }
6246
+ return existing;
6247
+ }
6248
+ function merge_inits(existing, incoming) {
6249
+ return () => ({ ...existing(), ...incoming() });
6250
+ }
6251
+ function register_state(state2, states, actions, events) {
6252
+ const existing = states.get(state2.name);
6253
+ if (existing) {
6254
+ merge_into_existing(state2, existing, states, actions, events);
6255
+ } else {
6256
+ register_new_state(state2, states, actions, events);
6257
+ }
6258
+ }
6259
+ function register_new_state(state2, states, actions, events) {
6260
+ states.set(state2.name, state2);
6261
+ for (const name of Object.keys(state2.actions)) {
6262
+ if (actions[name]) throw new Error(`Duplicate action "${name}"`);
6263
+ actions[name] = state2;
6264
+ }
6265
+ for (const name of Object.keys(state2.events)) {
6266
+ if (events[name]) throw new Error(`Duplicate event "${name}"`);
6267
+ events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
6268
+ }
6269
+ }
6270
+ function merge_into_existing(state2, existing, states, actions, events) {
6271
+ for (const name of Object.keys(state2.actions)) {
6272
+ if (existing.actions[name] === state2.actions[name]) continue;
6273
+ if (actions[name]) throw new Error(`Duplicate action "${name}"`);
6274
+ }
6275
+ for (const name of Object.keys(state2.events)) {
6276
+ if (existing.events[name] === state2.events[name]) continue;
6277
+ if (existing.events[name]) {
6278
+ throw new Error(
6279
+ `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.`
6280
+ );
6281
+ }
6282
+ if (events[name]) throw new Error(`Duplicate event "${name}"`);
6283
+ }
6284
+ const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
6285
+ const merged = {
6286
+ ...existing,
6287
+ state: merge_schemas(existing.state, state2.state, state2.name),
6288
+ init: merge_inits(existing.init, state2.init),
6289
+ events: { ...existing.events, ...state2.events },
6290
+ actions: { ...existing.actions, ...state2.actions },
6291
+ patch: merged_patch,
6292
+ on: { ...existing.on, ...state2.on },
6293
+ given: { ...existing.given, ...state2.given },
6294
+ snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
6295
+ throw new Error(
6296
+ `Duplicate snap strategy for state "${state2.name}"`
6297
+ );
6298
+ })() : state2.snap || existing.snap
6299
+ };
6300
+ states.set(state2.name, merged);
6301
+ for (const name of Object.keys(merged.actions)) {
6302
+ actions[name] = merged;
6303
+ }
6304
+ for (const name of Object.keys(state2.events)) {
6305
+ if (events[name]) continue;
6306
+ events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
6307
+ }
6308
+ }
6309
+ function merge_patches(existing, incoming, state_name) {
6310
+ const merged = { ...existing };
6311
+ for (const name of Object.keys(incoming)) {
6312
+ const existing_p = existing[name];
6313
+ const incoming_p = incoming[name];
6314
+ if (!existing_p) {
6315
+ merged[name] = incoming_p;
6316
+ continue;
6317
+ }
6318
+ const existing_is_default = existing_p._passthrough;
6319
+ const incoming_is_default = incoming_p._passthrough;
6320
+ if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
6321
+ throw new Error(
6322
+ `Duplicate custom patch for event "${name}" in state "${state_name}"`
6323
+ );
6324
+ }
6325
+ if (existing_is_default && !incoming_is_default) {
6326
+ merged[name] = incoming_p;
6327
+ }
6328
+ }
6329
+ return merged;
6330
+ }
6331
+ function merge_event_register(target, source) {
6332
+ for (const [event_name, source_reg] of Object.entries(source)) {
6333
+ const target_reg = target[event_name];
6334
+ if (!target_reg) continue;
6335
+ for (const [name, reaction] of source_reg.reactions) {
6336
+ const existing = target_reg.reactions.get(name);
6337
+ if (existing !== void 0 && existing !== reaction)
6338
+ throw new Error(
6339
+ `Duplicate reaction "${name}" for event "${event_name}". Reaction handlers are keyed by function name; rename one of them.`
6340
+ );
6341
+ target_reg.reactions.set(name, reaction);
6342
+ }
6343
+ }
6344
+ }
6345
+ function merge_projection(proj, events) {
6346
+ for (const event_name of Object.keys(proj.events)) {
6347
+ const proj_register = proj.events[event_name];
6348
+ const existing = events[event_name];
6349
+ if (!existing) {
6350
+ events[event_name] = {
6351
+ schema: proj_register.schema,
6352
+ reactions: new Map(proj_register.reactions)
6353
+ };
6354
+ } else {
6355
+ for (const [name, reaction] of proj_register.reactions) {
6356
+ if ([...existing.reactions.values()].includes(reaction)) continue;
6357
+ let key = name;
6358
+ while (existing.reactions.has(key)) key = `${key}_p`;
6359
+ existing.reactions.set(key, reaction);
6360
+ }
6361
+ }
6362
+ }
6363
+ }
6364
+ var _this_ = ({ stream }) => ({
6365
+ source: stream,
6366
+ target: stream
6367
+ });
6368
+
6369
+ // src/builders/builder-utils.ts
6370
+ function register_lane(config2, lanes) {
6371
+ if (config2.name === DEFAULT_LANE)
6372
+ throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
6373
+ if (lanes.some((l) => l.name === config2.name))
6374
+ throw new Error(`Lane "${config2.name}" was already declared`);
6375
+ lanes.push(config2);
6376
+ }
6377
+ function reaction_on(event, events, builder) {
6378
+ const register = (handler, options, schedule) => {
6379
+ if (!handler.name)
6380
+ throw new Error(
6381
+ `Reaction handler for "${String(event)}" must be a named function`
6382
+ );
6383
+ if (events[event].reactions.has(handler.name))
6384
+ throw new Error(
6385
+ `Duplicate reaction "${handler.name}" for event "${String(event)}". Reaction handlers are keyed by function name; rename one of them.`
6386
+ );
6387
+ if (schedule && typeof schedule !== "function") assert_defer_when(schedule);
6388
+ const reaction = {
6389
+ handler: schedule ? make_deferred(handler, schedule) : handler,
6390
+ resolver: _this_,
6391
+ // #1269: validate the whole bag at the declaration site so a bad
6392
+ // `maxRetries`/`backoff`/`blockOnError` throws ZodError at build,
6393
+ // not a NaN gate on the first retry.
6394
+ options: resolveReactionConfig({
6395
+ blockOnError: options?.blockOnError ?? true,
6396
+ maxRetries: options?.maxRetries ?? 3,
6397
+ backoff: options?.backoff
6398
+ })
6399
+ };
6400
+ events[event].reactions.set(handler.name, reaction);
6401
+ return Object.assign(builder, {
6402
+ to(resolver) {
6403
+ reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
6404
+ return builder;
6405
+ }
6406
+ });
6407
+ };
6408
+ return {
6409
+ do: (handler, options) => register(handler, options),
6410
+ defer: (schedule) => ({
6411
+ do: (handler, options) => register(handler, options, schedule)
6412
+ })
6413
+ };
6414
+ }
6415
+
6415
6416
  // src/builders/act-builder.ts
6416
6417
  function register_batch_handler(proj, batch_handlers) {
6417
6418
  if (!proj.batchHandler || !proj.target) return;
@@ -7197,7 +7198,6 @@ function write_line(writer, line) {
7197
7198
  port,
7198
7199
  projection,
7199
7200
  resolveAutocloseConfig,
7200
- scoped,
7201
7201
  sensitive,
7202
7202
  sleep,
7203
7203
  slice,