@rotorsoft/act 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +87 -379
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/@types/act.d.ts +43 -5
  4. package/dist/@types/act.d.ts.map +1 -1
  5. package/dist/@types/adapters/console-logger.d.ts.map +1 -1
  6. package/dist/@types/adapters/in-memory-store.d.ts +22 -2
  7. package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
  8. package/dist/@types/builders/act-builder.d.ts +33 -9
  9. package/dist/@types/builders/act-builder.d.ts.map +1 -1
  10. package/dist/@types/builders/slice-builder.d.ts +23 -8
  11. package/dist/@types/builders/slice-builder.d.ts.map +1 -1
  12. package/dist/@types/internal/build-classify.d.ts +20 -0
  13. package/dist/@types/internal/build-classify.d.ts.map +1 -1
  14. package/dist/@types/internal/correlate-cycle.d.ts +1 -0
  15. package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
  16. package/dist/@types/internal/drain-cycle.d.ts +43 -3
  17. package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
  18. package/dist/@types/internal/drain.d.ts +3 -1
  19. package/dist/@types/internal/drain.d.ts.map +1 -1
  20. package/dist/@types/internal/index.d.ts +3 -2
  21. package/dist/@types/internal/index.d.ts.map +1 -1
  22. package/dist/@types/internal/reactions.d.ts.map +1 -1
  23. package/dist/@types/internal/tracing.d.ts +51 -0
  24. package/dist/@types/internal/tracing.d.ts.map +1 -1
  25. package/dist/@types/ports.d.ts +10 -0
  26. package/dist/@types/ports.d.ts.map +1 -1
  27. package/dist/@types/test/sandbox.d.ts +1 -1
  28. package/dist/@types/test/sandbox.d.ts.map +1 -1
  29. package/dist/@types/types/ports.d.ts +203 -2
  30. package/dist/@types/types/ports.d.ts.map +1 -1
  31. package/dist/@types/types/reaction.d.ts +20 -2
  32. package/dist/@types/types/reaction.d.ts.map +1 -1
  33. package/dist/{chunk-QAB4SDOS.js → chunk-PGTC7VOC.js} +117 -11
  34. package/dist/chunk-PGTC7VOC.js.map +1 -0
  35. package/dist/index.cjs +1217 -897
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +1108 -893
  38. package/dist/index.js.map +1 -1
  39. package/dist/test/index.cjs +116 -11
  40. package/dist/test/index.cjs.map +1 -1
  41. package/dist/test/index.js +3 -3
  42. package/dist/test/index.js.map +1 -1
  43. package/package.json +2 -2
  44. package/dist/chunk-QAB4SDOS.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ConsoleLogger,
3
+ DEFAULT_LANE,
3
4
  ExitCodes,
4
5
  InMemoryCache,
5
6
  InMemoryStore,
@@ -18,7 +19,7 @@ import {
18
19
  sleep,
19
20
  store,
20
21
  validate
21
- } from "./chunk-QAB4SDOS.js";
22
+ } from "./chunk-PGTC7VOC.js";
22
23
  import {
23
24
  ActorSchema,
24
25
  CausationEventSchema,
@@ -60,23 +61,39 @@ process.once("unhandledRejection", async (arg) => {
60
61
  import EventEmitter from "events";
61
62
 
62
63
  // src/internal/build-classify.ts
64
+ var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
63
65
  function classifyRegistry(registry, states) {
64
66
  const statics = /* @__PURE__ */ new Map();
65
67
  const reactiveEvents = /* @__PURE__ */ new Set();
68
+ const eventToLanes = /* @__PURE__ */ new Map();
66
69
  let hasDynamicResolvers = false;
67
70
  for (const [name, register] of Object.entries(registry.events)) {
68
71
  if (register.reactions.size > 0) reactiveEvents.add(name);
69
72
  for (const reaction of register.reactions.values()) {
70
73
  if (typeof reaction.resolver === "function") {
71
74
  hasDynamicResolvers = true;
75
+ eventToLanes.set(name, ALL_LANES);
72
76
  } else {
73
- const { target, source, priority = 0 } = reaction.resolver;
77
+ const { target, source, priority = 0, lane } = reaction.resolver;
78
+ const lane_name = lane ?? "default";
79
+ const existing_lanes = eventToLanes.get(name);
80
+ if (existing_lanes !== ALL_LANES) {
81
+ const set = existing_lanes ?? /* @__PURE__ */ new Set();
82
+ set.add(lane_name);
83
+ eventToLanes.set(name, set);
84
+ }
74
85
  const key = `${target}|${source ?? ""}`;
75
86
  const existing = statics.get(key);
76
87
  if (!existing) {
77
- statics.set(key, { stream: target, source, priority });
78
- } else if (priority > existing.priority) {
79
- statics.set(key, { ...existing, priority });
88
+ statics.set(key, { stream: target, source, priority, lane });
89
+ } else {
90
+ if ((existing.lane ?? void 0) !== (lane ?? void 0))
91
+ throw new Error(
92
+ `Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
93
+ );
94
+ if (priority > existing.priority) {
95
+ statics.set(key, { ...existing, priority });
96
+ }
80
97
  }
81
98
  }
82
99
  }
@@ -91,7 +108,8 @@ function classifyRegistry(registry, states) {
91
108
  staticTargets: [...statics.values()],
92
109
  hasDynamicResolvers,
93
110
  reactiveEvents,
94
- eventToState
111
+ eventToState,
112
+ eventToLanes
95
113
  };
96
114
  }
97
115
 
@@ -133,24 +151,18 @@ async function runCloseCycle(targets, deps) {
133
151
  return { truncated, skipped };
134
152
  }
135
153
  async function scanStreamHeads(streams) {
154
+ const stats = await store().query_stats(streams, {
155
+ exclude: [SNAP_EVENT]
156
+ });
136
157
  const out = /* @__PURE__ */ new Map();
137
- await Promise.all(
138
- streams.map(async (s) => {
139
- let maxId = -1;
140
- let version = -1;
141
- let lastEventName = "";
142
- await store().query(
143
- (e) => {
144
- if (e.name === TOMBSTONE_EVENT || maxId !== -1) return;
145
- maxId = e.id;
146
- version = e.version;
147
- lastEventName = e.name;
148
- },
149
- { stream: s, stream_exact: true, backward: true, limit: 1 }
150
- );
151
- if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
152
- })
153
- );
158
+ for (const [stream, { head }] of stats) {
159
+ if (head.name === TOMBSTONE_EVENT) continue;
160
+ out.set(stream, {
161
+ maxId: head.id,
162
+ version: head.version,
163
+ lastEventName: head.name
164
+ });
165
+ }
154
166
  return out;
155
167
  }
156
168
  async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
@@ -308,6 +320,7 @@ var CorrelateCycle = class {
308
320
  const entry = correlated.get(resolved.target) || {
309
321
  source: resolved.source,
310
322
  priority: incomingPriority,
323
+ lane: resolved.lane,
311
324
  payloads: []
312
325
  };
313
326
  if (incomingPriority > entry.priority)
@@ -326,10 +339,11 @@ var CorrelateCycle = class {
326
339
  );
327
340
  if (correlated.size) {
328
341
  const streams = [...correlated.entries()].map(
329
- ([stream, { source, priority }]) => ({
342
+ ([stream, { source, priority, lane }]) => ({
330
343
  stream,
331
344
  source,
332
- priority
345
+ priority,
346
+ lane
333
347
  })
334
348
  );
335
349
  const { subscribed } = await this.cd.subscribe(streams);
@@ -414,904 +428,955 @@ function computeLagLeadRatio(handled, lagging, leading) {
414
428
  return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
415
429
  }
416
430
 
417
- // src/internal/drain-cycle.ts
418
- async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis, isDeferred) {
419
- const leased = await ops.claim(lagging, leading, randomUUID(), leaseMillis);
420
- if (!leased.length) return void 0;
421
- const active = isDeferred ? leased.filter((l) => !isDeferred(l.stream)) : leased;
422
- if (!active.length) {
423
- return {
424
- leased,
425
- fetched: [],
426
- handled: [],
427
- acked: [],
428
- blocked: []
429
- };
430
- }
431
- const fetched = await ops.fetch(active, eventLimit);
432
- const fetchMap = /* @__PURE__ */ new Map();
433
- const fetch_window_at = fetched.reduce(
434
- (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
435
- 0
431
+ // src/internal/drain.ts
432
+ var claim = (lagging, leading, by, millis, lane) => store().claim(lagging, leading, by, millis, lane);
433
+ async function fetch(leased, eventLimit) {
434
+ return Promise.all(
435
+ leased.map(async ({ stream, source, at, lagging }) => {
436
+ const events = [];
437
+ await store().query((e) => events.push(e), {
438
+ stream: source,
439
+ after: at,
440
+ limit: eventLimit
441
+ });
442
+ return { stream, source, at, lagging, events };
443
+ })
436
444
  );
437
- for (const f of fetched) {
438
- const { stream, events } = f;
439
- const payloads = events.flatMap((event) => {
440
- const register = registry.events[event.name];
441
- if (!register) return [];
442
- return [...register.reactions.values()].filter((reaction) => {
443
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
444
- return resolved && resolved.target === stream;
445
- }).map((reaction) => ({ ...reaction, event }));
446
- });
447
- fetchMap.set(stream, { fetch: f, payloads });
445
+ }
446
+ var ack = (leases) => store().ack(leases);
447
+ var block = (leases) => store().block(leases);
448
+ var subscribe = (streams) => store().subscribe(streams);
449
+
450
+ // src/internal/event-sourcing.ts
451
+ import { patch } from "@rotorsoft/act-patch";
452
+ async function snap(snapshot) {
453
+ try {
454
+ const { id, stream, name, meta, version } = snapshot.event;
455
+ await store().commit(
456
+ stream,
457
+ [{ name: SNAP_EVENT, data: snapshot.state }],
458
+ {
459
+ correlation: meta.correlation,
460
+ causation: { event: { id, name, stream } }
461
+ },
462
+ version
463
+ // IMPORTANT! - state events are committed right after the snapshot event
464
+ );
465
+ } catch (error) {
466
+ log().error(error);
448
467
  }
449
- const handled = await Promise.all(
450
- active.map((lease) => {
451
- const entry = fetchMap.get(lease.stream);
452
- const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
453
- const { payloads } = entry;
454
- const batchHandler = batchHandlers.get(lease.stream);
455
- if (batchHandler && payloads.length > 0) {
456
- return handleBatch({ ...lease, at }, payloads, batchHandler);
468
+ }
469
+ async function tombstone(stream, expectedVersion, correlation) {
470
+ try {
471
+ const [committed] = await store().commit(
472
+ stream,
473
+ [{ name: TOMBSTONE_EVENT, data: {} }],
474
+ { correlation, causation: {} },
475
+ expectedVersion
476
+ );
477
+ return committed;
478
+ } catch (error) {
479
+ if (error instanceof ConcurrencyError) return void 0;
480
+ throw error;
481
+ }
482
+ }
483
+ async function load(me, stream, callback, asOf) {
484
+ const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
485
+ const cached = timeTravel ? void 0 : await cache().get(stream);
486
+ const cache_hit = !!cached;
487
+ let state2 = cached?.state ?? (me.init ? me.init() : {});
488
+ let patches = cached?.patches ?? 0;
489
+ let snaps = cached?.snaps ?? 0;
490
+ let version = cached?.version ?? -1;
491
+ let replayed = 0;
492
+ let event;
493
+ await store().query(
494
+ (e) => {
495
+ event = e;
496
+ version = e.version;
497
+ if (e.name === SNAP_EVENT) {
498
+ state2 = e.data;
499
+ snaps++;
500
+ patches = 0;
501
+ replayed++;
502
+ } else if (me.patch[e.name]) {
503
+ state2 = patch(state2, me.patch[e.name](event, state2));
504
+ patches++;
505
+ replayed++;
506
+ } else if (e.name !== TOMBSTONE_EVENT) {
507
+ log().warn(
508
+ `Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
509
+ );
457
510
  }
458
- return handle({ ...lease, at }, payloads);
459
- })
460
- );
461
- const acked = await ops.ack(
462
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
463
- );
464
- const blocked = await ops.block(
465
- handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
511
+ callback?.({
512
+ event,
513
+ state: state2,
514
+ version,
515
+ patches,
516
+ snaps,
517
+ cache_hit,
518
+ replayed
519
+ });
520
+ },
521
+ {
522
+ stream,
523
+ stream_exact: true,
524
+ ...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
525
+ }
466
526
  );
467
- return { leased, fetched, handled, acked, blocked };
468
- }
469
- var EMPTY_DRAIN = {
470
- fetched: [],
471
- leased: [],
472
- acked: [],
473
- blocked: []
474
- };
475
- var DrainController = class {
476
- constructor(deps) {
477
- this.deps = deps;
527
+ if (replayed > 0 && !timeTravel && event) {
528
+ await cache().set(stream, {
529
+ state: state2,
530
+ version,
531
+ event_id: event.id,
532
+ patches,
533
+ snaps
534
+ });
478
535
  }
479
- _armed = false;
480
- _locked = false;
481
- _ratio = 0.5;
482
- /**
483
- * Per-stream backoff: `stream → nextAttemptAt` (ms since epoch). Set by
484
- * `_finalize` via `HandleResult.nextAttemptAt`; cleared on successful
485
- * ack or terminal block. Lives in process memory — per-worker pacing
486
- * by design (see {@link BackoffOptions} for the multi-worker trade-off).
487
- */
488
- _backoff = /* @__PURE__ */ new Map();
489
- /** Timer re-arming drain at the earliest pending `nextAttemptAt`. */
490
- _backoffTimer;
491
- /**
492
- * Signal that a commit (or reset / cold-start) may have produced work.
493
- * Subsequent `drain()` calls will run the pipeline; once the pipeline
494
- * settles to no-progress, the controller disarms itself.
495
- */
496
- arm() {
497
- this._armed = true;
536
+ return { event, state: state2, version, patches, snaps, cache_hit, replayed };
537
+ }
538
+ async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = defaultCorrelator) {
539
+ const { stream, expectedVersion, actor } = target;
540
+ if (!stream) throw new Error("Missing target stream");
541
+ const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
542
+ const snapshot = await load(me, stream);
543
+ if (snapshot.event?.name === TOMBSTONE_EVENT)
544
+ throw new StreamClosedError(stream);
545
+ const expected = expectedVersion ?? snapshot.event?.version;
546
+ if (me.given) {
547
+ const invariants = me.given[action2] || [];
548
+ invariants.forEach(({ valid, description }) => {
549
+ if (!valid(snapshot.state, actor))
550
+ throw new InvariantError(
551
+ action2,
552
+ validated,
553
+ target,
554
+ snapshot,
555
+ description
556
+ );
557
+ });
498
558
  }
499
- /** Read-only flag true while a commit / reset is unprocessed. */
500
- get armed() {
501
- return this._armed;
559
+ const result = me.on[action2](validated, snapshot, target);
560
+ if (!result) return [snapshot];
561
+ if (Array.isArray(result) && result.length === 0) {
562
+ return [snapshot];
502
563
  }
503
- /** Returns true when `stream` is currently within a backoff window. */
504
- isDeferred = (stream) => {
505
- const next = this._backoff.get(stream);
506
- return next !== void 0 && next > Date.now();
507
- };
508
- /**
509
- * Schedule the next drain re-arm at the earliest pending backoff
510
- * expiry. Called only when the backoff map is non-empty (caller guard).
511
- * Idempotent — collapses many simultaneously deferred streams into a
512
- * single timer.
513
- */
514
- scheduleBackoffWake() {
515
- if (this._backoffTimer) clearTimeout(this._backoffTimer);
516
- let earliest = Number.POSITIVE_INFINITY;
517
- for (const t of this._backoff.values()) if (t < earliest) earliest = t;
518
- const delay = Math.max(0, earliest - Date.now());
519
- this._backoffTimer = setTimeout(() => {
520
- this._backoffTimer = void 0;
521
- const now = Date.now();
522
- for (const [stream, at] of this._backoff) {
523
- if (at <= now) this._backoff.delete(stream);
564
+ const tuples = Array.isArray(result[0]) ? result : [result];
565
+ const deprecated = me._deprecated;
566
+ if (deprecated && deprecated.size > 0) {
567
+ const me_ = me;
568
+ const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
569
+ for (const [name] of tuples) {
570
+ const evt = name;
571
+ if (deprecated.has(evt) && !warned.has(evt)) {
572
+ warned.add(evt);
573
+ log().warn(
574
+ `Action "${String(action2)}" emitted deprecated event "${evt}". A newer version exists in the registry \u2014 update the action's .emit() to target the current version. (warned once per process)`
575
+ );
524
576
  }
525
- this._armed = true;
526
- }, delay);
527
- this._backoffTimer.unref();
577
+ }
528
578
  }
529
- /** Run one drain pass. Short-circuits when not armed or already running. */
530
- async drain({
531
- streamLimit = 10,
532
- eventLimit = 10,
533
- leaseMillis = 1e4
534
- } = {}) {
535
- if (!this._armed) return EMPTY_DRAIN;
536
- if (this._locked) return EMPTY_DRAIN;
537
- try {
538
- this._locked = true;
539
- const lagging = Math.ceil(streamLimit * this._ratio);
540
- const leading = streamLimit - lagging;
541
- const cycle = await runDrainCycle(
542
- this.deps.ops,
543
- this.deps.registry,
544
- this.deps.batchHandlers,
545
- this.deps.handle,
546
- this.deps.handleBatch,
547
- lagging,
548
- leading,
549
- eventLimit,
550
- leaseMillis,
551
- this._backoff.size > 0 ? this.isDeferred : void 0
552
- );
553
- if (!cycle) {
554
- this._armed = false;
555
- return EMPTY_DRAIN;
556
- }
557
- const { leased, fetched, handled, acked, blocked } = cycle;
558
- this._ratio = computeLagLeadRatio(handled, lagging, leading);
559
- for (const lease of acked) this._backoff.delete(lease.stream);
560
- for (const lease of blocked) this._backoff.delete(lease.stream);
561
- for (const h of handled) {
562
- if (h.nextAttemptAt !== void 0 && !h.block) {
563
- this._backoff.set(h.lease.stream, h.nextAttemptAt);
564
- }
565
- }
566
- if (this._backoff.size > 0) this.scheduleBackoffWake();
567
- if (acked.length) this.deps.onAcked(acked);
568
- if (blocked.length) this.deps.onBlocked(blocked);
569
- const hasErrors = handled.some(({ error }) => error);
570
- if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
571
- return { fetched, leased, acked, blocked };
572
- } catch (error) {
573
- this.deps.logger.error(error);
574
- return EMPTY_DRAIN;
575
- } finally {
576
- this._locked = false;
577
- }
578
- }
579
- };
580
-
581
- // src/internal/event-versions.ts
582
- var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
583
- function parse(name) {
584
- const m = name.match(VERSION_SUFFIX);
585
- if (m) {
586
- const v = Number.parseInt(m[2], 10);
587
- if (v >= 2) return { base: m[1], version: v };
588
- }
589
- return { base: name, version: 1 };
590
- }
591
- function deprecatedEventNames(names) {
592
- const groups = /* @__PURE__ */ new Map();
593
- for (const name of names) {
594
- const { base, version } = parse(name);
595
- const list = groups.get(base);
596
- if (list) list.push({ version, name });
597
- else groups.set(base, [{ version, name }]);
598
- }
599
- const deprecated = /* @__PURE__ */ new Set();
600
- for (const list of groups.values()) {
601
- if (list.length < 2) continue;
602
- list.sort((a, b) => b.version - a.version);
603
- for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
604
- }
605
- return deprecated;
606
- }
607
- function currentVersionOf(deprecatedName, allNames) {
608
- const target = parse(deprecatedName);
609
- let highest;
610
- for (const name of allNames) {
611
- const { base, version } = parse(name);
612
- if (base !== target.base) continue;
613
- if (!highest || version > highest.version) highest = { version, name };
614
- }
615
- return highest && highest.version > target.version ? highest.name : void 0;
616
- }
617
-
618
- // src/internal/merge.ts
619
- import { ZodObject } from "zod";
620
- function baseTypeName(zodType) {
621
- let t = zodType;
622
- while (typeof t.unwrap === "function") {
623
- t = t.unwrap();
624
- }
625
- return t.constructor.name;
626
- }
627
- function mergeSchemas(existing, incoming, stateName) {
628
- if (existing instanceof ZodObject && incoming instanceof ZodObject) {
629
- const existingShape = existing.shape;
630
- const incomingShape = incoming.shape;
631
- for (const key of Object.keys(incomingShape)) {
632
- if (key in existingShape) {
633
- const existingBase = baseTypeName(existingShape[key]);
634
- const incomingBase = baseTypeName(incomingShape[key]);
635
- if (existingBase !== incomingBase) {
636
- throw new Error(
637
- `Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
638
- );
639
- }
640
- }
641
- }
642
- return existing.extend(incomingShape);
643
- }
644
- return existing;
645
- }
646
- function mergeInits(existing, incoming) {
647
- return () => ({ ...existing(), ...incoming() });
648
- }
649
- function registerState(state2, states, actions, events) {
650
- const existing = states.get(state2.name);
651
- if (existing) {
652
- mergeIntoExisting(state2, existing, states, actions, events);
653
- } else {
654
- registerNewState(state2, states, actions, events);
655
- }
656
- }
657
- function registerNewState(state2, states, actions, events) {
658
- states.set(state2.name, state2);
659
- for (const name of Object.keys(state2.actions)) {
660
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
661
- actions[name] = state2;
662
- }
663
- for (const name of Object.keys(state2.events)) {
664
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
665
- events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
666
- }
667
- }
668
- function mergeIntoExisting(state2, existing, states, actions, events) {
669
- for (const name of Object.keys(state2.actions)) {
670
- if (existing.actions[name] === state2.actions[name]) continue;
671
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
672
- }
673
- for (const name of Object.keys(state2.events)) {
674
- if (existing.events[name] === state2.events[name]) continue;
675
- if (existing.events[name]) {
676
- throw new Error(
677
- `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.`
678
- );
579
+ const emitted = tuples.map(([name, data]) => ({
580
+ name,
581
+ data: skipValidation ? data : validate(name, data, me.events[name])
582
+ }));
583
+ const meta = {
584
+ correlation: reactingTo?.meta.correlation || correlator({
585
+ action: action2,
586
+ state: me.name,
587
+ stream,
588
+ actor: target.actor
589
+ }),
590
+ causation: {
591
+ action: {
592
+ name: action2,
593
+ ...target
594
+ // payload intentionally omitted: it can be large or contain PII,
595
+ // and callers correlate via the correlation id when they need it.
596
+ },
597
+ event: reactingTo ? {
598
+ id: reactingTo.id,
599
+ name: reactingTo.name,
600
+ stream: reactingTo.stream
601
+ } : void 0
679
602
  }
680
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
681
- }
682
- const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
683
- const merged = {
684
- ...existing,
685
- state: mergeSchemas(existing.state, state2.state, state2.name),
686
- init: mergeInits(existing.init, state2.init),
687
- events: { ...existing.events, ...state2.events },
688
- actions: { ...existing.actions, ...state2.actions },
689
- patch: mergedPatch,
690
- on: { ...existing.on, ...state2.on },
691
- given: { ...existing.given, ...state2.given },
692
- snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
693
- throw new Error(
694
- `Duplicate snap strategy for state "${state2.name}"`
695
- );
696
- })() : state2.snap || existing.snap
697
603
  };
698
- states.set(state2.name, merged);
699
- for (const name of Object.keys(merged.actions)) {
700
- actions[name] = merged;
701
- }
702
- for (const name of Object.keys(state2.events)) {
703
- if (events[name]) continue;
704
- events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
705
- }
706
- }
707
- function mergePatches(existing, incoming, stateName) {
708
- const merged = { ...existing };
709
- for (const name of Object.keys(incoming)) {
710
- const existingP = existing[name];
711
- const incomingP = incoming[name];
712
- if (!existingP) {
713
- merged[name] = incomingP;
714
- continue;
715
- }
716
- const existingIsDefault = existingP._passthrough;
717
- const incomingIsDefault = incomingP._passthrough;
718
- if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
719
- throw new Error(
720
- `Duplicate custom patch for event "${name}" in state "${stateName}"`
721
- );
722
- }
723
- if (existingIsDefault && !incomingIsDefault) {
724
- merged[name] = incomingP;
725
- }
726
- }
727
- return merged;
728
- }
729
- function mergeEventRegister(target, source) {
730
- for (const [eventName, sourceReg] of Object.entries(source)) {
731
- const targetReg = target[eventName];
732
- if (!targetReg) continue;
733
- for (const [name, reaction] of sourceReg.reactions) {
734
- targetReg.reactions.set(name, reaction);
735
- }
736
- }
737
- }
738
- function mergeProjection(proj, events) {
739
- for (const eventName of Object.keys(proj.events)) {
740
- const projRegister = proj.events[eventName];
741
- const existing = events[eventName];
742
- if (!existing) {
743
- events[eventName] = {
744
- schema: projRegister.schema,
745
- reactions: new Map(projRegister.reactions)
746
- };
747
- } else {
748
- for (const [name, reaction] of projRegister.reactions) {
749
- let key = name;
750
- while (existing.reactions.has(key)) key = `${key}_p`;
751
- existing.reactions.set(key, reaction);
752
- }
604
+ let committed;
605
+ try {
606
+ committed = await store().commit(
607
+ stream,
608
+ emitted,
609
+ meta,
610
+ // Reactions skip optimistic concurrency: they always append against the
611
+ // current head. Stream leasing already serializes concurrent reactions,
612
+ // and forcing version checks here would turn ordinary catch-up into
613
+ // spurious retries.
614
+ reactingTo ? void 0 : expected
615
+ );
616
+ } catch (error) {
617
+ if (error instanceof ConcurrencyError) {
618
+ await cache().invalidate(stream);
753
619
  }
620
+ throw error;
754
621
  }
622
+ let { state: state2, patches } = snapshot;
623
+ const snapshots = committed.map((event) => {
624
+ const p = me.patch[event.name](event, state2);
625
+ state2 = patch(state2, p);
626
+ patches++;
627
+ return {
628
+ event,
629
+ state: state2,
630
+ version: event.version,
631
+ patches,
632
+ snaps: snapshot.snaps,
633
+ patch: p,
634
+ cache_hit: snapshot.cache_hit,
635
+ replayed: snapshot.replayed
636
+ };
637
+ });
638
+ const last = snapshots.at(-1);
639
+ const snapped = me.snap?.(last);
640
+ cache().set(stream, {
641
+ state: last.state,
642
+ version: last.event.version,
643
+ event_id: last.event.id,
644
+ patches: snapped ? 0 : last.patches,
645
+ snaps: snapped ? last.snaps + 1 : last.snaps
646
+ }).catch((err) => log().error(err));
647
+ if (snapped) void snap(last);
648
+ return snapshots;
755
649
  }
756
- var _this_ = ({ stream }) => ({
757
- source: stream,
758
- target: stream
759
- });
760
650
 
761
- // src/internal/backoff.ts
762
- function computeBackoffDelay(retry, opts) {
763
- if (!opts || opts.baseMs <= 0) return 0;
764
- const r = Math.max(0, retry);
765
- let delay;
766
- switch (opts.strategy) {
767
- case "fixed":
768
- delay = opts.baseMs;
769
- break;
770
- case "linear":
771
- delay = opts.baseMs * (r + 1);
772
- break;
773
- case "exponential":
774
- delay = opts.baseMs * 2 ** r;
775
- if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
776
- break;
651
+ // src/internal/tracing.ts
652
+ var PRETTY = config().env !== "production";
653
+ var C_BLUE = "\x1B[38;5;39m";
654
+ var C_ORANGE = "\x1B[38;5;208m";
655
+ var C_GREEN = "\x1B[38;5;42m";
656
+ var C_MAGENTA = "\x1B[38;5;165m";
657
+ var C_DRAIN = "\x1B[38;5;244m";
658
+ var C_HIT = "\x1B[38;5;82m";
659
+ var C_MISS = "\x1B[38;5;220m";
660
+ var C_RESET = "\x1B[0m";
661
+ var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
662
+ var C_LANE = "\x1B[38;5;183m";
663
+ var C_DIM = "\x1B[38;5;240m";
664
+ var C_ERR = "\x1B[38;5;196m";
665
+ var C_STREAM = "\x1B[38;5;226m";
666
+ var dim = (text) => PRETTY ? `${C_DIM}${text}${C_RESET}` : text;
667
+ var hue = (color, text) => PRETTY ? `${color}${text}${C_RESET}` : text;
668
+ var drain_caption = (caption, lane) => {
669
+ const showLane = lane && lane !== "default";
670
+ if (PRETTY) {
671
+ const tag = `${C_DRAIN}>> ${caption}${C_RESET}`;
672
+ return showLane ? `${tag} ${C_LANE}${lane}${C_RESET}` : tag;
673
+ }
674
+ return showLane ? `>> ${caption} ${lane}` : `>> ${caption}`;
675
+ };
676
+ var cache_marker = (hit) => {
677
+ const word = hit ? "hit" : "miss";
678
+ if (!PRETTY) return word;
679
+ return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
680
+ };
681
+ var stats_marker = (version, replayed, snaps, patches) => {
682
+ const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
683
+ if (!PRETTY) return text;
684
+ return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
685
+ };
686
+ var as_of_marker = (asOf) => {
687
+ if (!asOf) return "";
688
+ const parts = [];
689
+ if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
690
+ if (asOf.created_before !== void 0)
691
+ parts.push(`created_before=${asOf.created_before.toISOString()}`);
692
+ if (asOf.created_after !== void 0)
693
+ parts.push(`created_after=${asOf.created_after.toISOString()}`);
694
+ if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
695
+ return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
696
+ };
697
+ var traced = (inner, exit, entry) => (async (...args) => {
698
+ entry?.(...args);
699
+ const result = await inner(...args);
700
+ exit?.(result, ...args);
701
+ return result;
702
+ });
703
+ function buildEs(logger, correlator = defaultCorrelator) {
704
+ const boundAction = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
705
+ me,
706
+ actionName,
707
+ target,
708
+ payload,
709
+ reactingTo,
710
+ skipValidation,
711
+ correlator
712
+ );
713
+ if (logger.level !== "trace") {
714
+ return {
715
+ snap,
716
+ load,
717
+ action: boundAction,
718
+ tombstone
719
+ };
777
720
  }
778
- if (opts.jitter) delay = delay * (0.5 + Math.random());
779
- return Math.max(0, Math.floor(delay));
780
- }
781
-
782
- // src/internal/reactions.ts
783
- function finalize(lease, handled, at, error, options, logger) {
784
- if (!error) return { lease, handled, at };
785
- logger.error(error);
786
- const nonRetryable = error instanceof NonRetryableError;
787
- const block2 = options.blockOnError && (nonRetryable || lease.retry >= options.maxRetries);
788
- if (block2)
789
- logger.error(
790
- nonRetryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
791
- );
792
- const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
793
721
  return {
794
- lease,
795
- handled,
796
- at,
797
- error: handled === 0 ? error.message : void 0,
798
- block: block2,
799
- nextAttemptAt
800
- };
801
- }
802
- function buildHandle(deps) {
803
- const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
804
- return async (lease, payloads) => {
805
- if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
806
- const stream = lease.stream;
807
- let at = payloads.at(0).event.id;
808
- let handled = 0;
809
- if (lease.retry > 0)
810
- logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
811
- const scopedApp = {
812
- do: boundDo,
813
- load: boundLoad,
814
- query: boundQuery,
815
- query_array: boundQueryArray
816
- };
817
- for (const payload of payloads) {
818
- const { event, handler } = payload;
819
- scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
820
- action2,
821
- target,
822
- actionPayload,
823
- reactingTo ?? event,
824
- skipValidation
722
+ snap: traced(snap, void 0, (snapshot) => {
723
+ logger.trace(
724
+ es_caption(
725
+ "snap",
726
+ C_MAGENTA,
727
+ `${snapshot.event.stream}@${snapshot.event.version}`
728
+ )
825
729
  );
826
- try {
827
- await handler(event, stream, scopedApp);
828
- at = event.id;
829
- handled++;
830
- } catch (error) {
831
- return finalize(
832
- lease,
833
- handled,
834
- at,
835
- error,
836
- payload.options,
837
- logger
730
+ }),
731
+ load: traced(load, (result, _me, stream, _cb, asOf) => {
732
+ const stats = stats_marker(
733
+ result.version,
734
+ result.replayed,
735
+ result.snaps,
736
+ result.patches
737
+ );
738
+ logger.trace(
739
+ es_caption(
740
+ "load",
741
+ C_GREEN,
742
+ `${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
743
+ )
744
+ );
745
+ }),
746
+ action: traced(
747
+ boundAction,
748
+ (snapshots, _me, _action, target) => {
749
+ const committed = snapshots.filter((s) => s.event);
750
+ if (committed.length) {
751
+ logger.trace(
752
+ committed.map((s) => s.event.data),
753
+ es_caption(
754
+ "committed",
755
+ C_ORANGE,
756
+ `${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
757
+ )
758
+ );
759
+ }
760
+ },
761
+ (_me, action2, target, payload) => {
762
+ logger.trace(
763
+ payload,
764
+ es_caption("action", C_BLUE, `${target.stream}.${action2}`)
838
765
  );
839
766
  }
840
- }
841
- return finalize(lease, handled, at, void 0, payloads[0].options, logger);
767
+ ),
768
+ tombstone: traced(tombstone, (committed, stream) => {
769
+ if (committed)
770
+ logger.trace(
771
+ es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
772
+ );
773
+ })
842
774
  };
843
775
  }
844
- function buildHandleBatch(logger) {
845
- return async (lease, payloads, batchHandler) => {
846
- const stream = lease.stream;
847
- const events = payloads.map((p) => p.event);
848
- const options = payloads[0].options;
849
- if (lease.retry > 0)
850
- logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
851
- try {
852
- await batchHandler(events, stream);
853
- return finalize(
854
- lease,
855
- events.length,
856
- events.at(-1).id,
857
- void 0,
858
- options,
859
- logger
860
- );
861
- } catch (error) {
862
- return finalize(lease, 0, lease.at, error, options, logger);
863
- }
776
+ function buildDrain(logger) {
777
+ return {
778
+ claim,
779
+ fetch,
780
+ ack,
781
+ block,
782
+ subscribe: logger.level !== "trace" ? subscribe : traced(subscribe, (result, streams) => {
783
+ if (!result.subscribed) return;
784
+ const lanes = new Set(streams.map((s) => s.lane ?? "default"));
785
+ const uniformLane = lanes.size === 1 ? streams[0]?.lane : void 0;
786
+ const data = streams.map(
787
+ ({ stream, lane }) => uniformLane || !lane || lane === "default" ? hue(C_STREAM, stream) : `${hue(C_STREAM, stream)}${dim(`[${lane}]`)}`
788
+ ).join(" ");
789
+ logger.trace(`${drain_caption("correlated", uniformLane)} ${data}`);
790
+ })
864
791
  };
865
792
  }
793
+ function traceCycle(logger, leased, fetched, handled, acked, blocked) {
794
+ if (logger.level !== "trace" || !leased.length) return;
795
+ const lane = leased[0]?.lane;
796
+ const fetchByStream = new Map(fetched.map((f) => [f.stream, f]));
797
+ const ackedByStream = new Map(acked.map((a) => [a.stream, a.at]));
798
+ const blockedByStream = new Map(blocked.map((b) => [b.stream, b.error]));
799
+ const failedByStream = new Map(
800
+ handled.filter((h) => h.error).map((h) => [h.lease.stream, h])
801
+ );
802
+ const detail = leased.map(({ stream, at, retry }) => {
803
+ const f = fetchByStream.get(stream);
804
+ const key = f?.source ? `${hue(C_STREAM, stream)}${dim(`<-${f.source}`)}` : hue(C_STREAM, stream);
805
+ const events = f && f.events.length ? ` ${dim(
806
+ `[${f.events.map(({ id, name }) => `#${id} ${String(name)}`).join(", ")}]`
807
+ )}` : "";
808
+ const ackedAt = ackedByStream.get(stream);
809
+ const ackPart = ackedAt !== void 0 ? hue(C_HIT, `\u2713 @${ackedAt}`) : "";
810
+ const failure = failedByStream.get(stream);
811
+ let failPart = "";
812
+ if (failure) {
813
+ const failedAt = failure.failed_at ?? at;
814
+ const blockedError = blockedByStream.get(stream);
815
+ if (blockedError !== void 0) {
816
+ failPart = `${hue(C_ERR, `\u2717 @${failedAt}/${retry}`)} ${dim(`(${blockedError})`)}`;
817
+ } else {
818
+ failPart = `${hue(C_MISS, `\u26A0 @${failedAt}/${retry}`)} ${dim(`(${failure.error})`)}`;
819
+ }
820
+ }
821
+ let tail;
822
+ if (ackPart && failPart) tail = ` ${ackPart} ${failPart}`;
823
+ else if (ackPart) tail = ` ${ackPart}`;
824
+ else if (failPart) tail = ` ${failPart}`;
825
+ else tail = ` ${dim(`\u2298 @${at}/${retry}`)}`;
826
+ return `${key}${events}${tail}`;
827
+ }).join(", ");
828
+ logger.trace(`${drain_caption("drained", lane)} ${detail}`);
829
+ }
866
830
 
867
- // src/internal/settle.ts
868
- var SettleLoop = class {
869
- constructor(deps, defaultDebounceMs) {
831
+ // src/internal/drain-cycle.ts
832
+ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis, isDeferred, lane) {
833
+ const leased = await ops.claim(
834
+ lagging,
835
+ leading,
836
+ randomUUID(),
837
+ leaseMillis,
838
+ lane
839
+ );
840
+ if (!leased.length) return void 0;
841
+ const active = isDeferred ? leased.filter((l) => !isDeferred(l.stream)) : leased;
842
+ if (!active.length) {
843
+ return {
844
+ leased,
845
+ fetched: [],
846
+ handled: [],
847
+ acked: [],
848
+ blocked: []
849
+ };
850
+ }
851
+ const fetched = await ops.fetch(active, eventLimit);
852
+ const fetchMap = /* @__PURE__ */ new Map();
853
+ const fetch_window_at = fetched.reduce(
854
+ (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
855
+ 0
856
+ );
857
+ for (const f of fetched) {
858
+ const { stream, events } = f;
859
+ const payloads = events.flatMap((event) => {
860
+ const register = registry.events[event.name];
861
+ if (!register) return [];
862
+ return [...register.reactions.values()].filter((reaction) => {
863
+ const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
864
+ return resolved && resolved.target === stream;
865
+ }).map((reaction) => ({ ...reaction, event }));
866
+ });
867
+ fetchMap.set(stream, { fetch: f, payloads });
868
+ }
869
+ const handled = await Promise.all(
870
+ active.map((lease) => {
871
+ const entry = fetchMap.get(lease.stream);
872
+ const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
873
+ const { payloads } = entry;
874
+ const batchHandler = batchHandlers.get(lease.stream);
875
+ if (batchHandler && payloads.length > 0) {
876
+ return handleBatch({ ...lease, at }, payloads, batchHandler);
877
+ }
878
+ return handle({ ...lease, at }, payloads);
879
+ })
880
+ );
881
+ const acked = await ops.ack(
882
+ handled.filter((h) => h.handled > 0 || !h.error).map((h) => ({ ...h.lease, at: h.acked_at }))
883
+ );
884
+ const blocked = await ops.block(
885
+ handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
886
+ );
887
+ return { leased, fetched, handled, acked, blocked };
888
+ }
889
+ var EMPTY_DRAIN = {
890
+ fetched: [],
891
+ leased: [],
892
+ acked: [],
893
+ blocked: []
894
+ };
895
+ var DrainController = class {
896
+ constructor(deps) {
870
897
  this.deps = deps;
871
- this.defaultDebounceMs = defaultDebounceMs;
872
898
  }
873
- _timer = void 0;
874
- _running = false;
899
+ _armed = false;
900
+ _locked = false;
901
+ _ratio = 0.5;
902
+ /**
903
+ * Per-stream backoff: `stream → nextAttemptAt` (ms since epoch). Set by
904
+ * `_finalize` via `HandleResult.nextAttemptAt`; cleared on successful
905
+ * ack or terminal block. Lives in process memory — per-worker pacing
906
+ * by design (see {@link BackoffOptions} for the multi-worker trade-off).
907
+ */
908
+ _backoff = /* @__PURE__ */ new Map();
909
+ /** Timer re-arming drain at the earliest pending `nextAttemptAt`. */
910
+ _backoffTimer;
911
+ /** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
912
+ _worker;
913
+ _stopped = false;
914
+ /**
915
+ * Signal that a commit (or reset / cold-start) may have produced work.
916
+ * Subsequent `drain()` calls will run the pipeline; once the pipeline
917
+ * settles to no-progress, the controller disarms itself.
918
+ */
919
+ arm() {
920
+ this._armed = true;
921
+ }
922
+ /** Read-only flag — true while a commit / reset is unprocessed. */
923
+ get armed() {
924
+ return this._armed;
925
+ }
926
+ /** Returns true when `stream` is currently within a backoff window. */
927
+ isDeferred = (stream) => {
928
+ const next = this._backoff.get(stream);
929
+ return next !== void 0 && next > Date.now();
930
+ };
931
+ /**
932
+ * Schedule the next drain re-arm at the earliest pending backoff
933
+ * expiry. Called only when the backoff map is non-empty (caller guard).
934
+ * Idempotent — collapses many simultaneously deferred streams into a
935
+ * single timer.
936
+ */
937
+ scheduleBackoffWake() {
938
+ if (this._backoffTimer) clearTimeout(this._backoffTimer);
939
+ let earliest = Number.POSITIVE_INFINITY;
940
+ for (const t of this._backoff.values()) if (t < earliest) earliest = t;
941
+ const delay = Math.max(0, earliest - Date.now());
942
+ this._backoffTimer = setTimeout(() => {
943
+ this._backoffTimer = void 0;
944
+ const now = Date.now();
945
+ for (const [stream, at] of this._backoff) {
946
+ if (at <= now) this._backoff.delete(stream);
947
+ }
948
+ this._armed = true;
949
+ }, delay);
950
+ this._backoffTimer.unref();
951
+ }
952
+ /** Lane this controller drains (undefined = legacy single-lane span). */
953
+ get lane() {
954
+ return this.deps.lane;
955
+ }
875
956
  /**
876
- * Schedule a settle pass. Multiple calls inside the debounce window
877
- * coalesce into one cycle. The cycle runs correlate→drain in a loop
878
- * until no progress is made (no new subscriptions, no acks, no blocks)
879
- * or `maxPasses` is reached, then emits the `"settled"` lifecycle event
880
- * via {@link SettleDeps.onSettled}.
957
+ * Start a per-lane worker that drains at the lane's `cycleMs`
958
+ * cadence (ACT-1103). When armed, the worker calls `drain()` on every
959
+ * tick and re-schedules; when not armed, it still re-schedules at
960
+ * `cycleMs` so a future `arm()` is picked up on the next tick.
961
+ *
962
+ * The setTimeout chain uses `unref()` so it doesn't keep the process
963
+ * alive on its own.
881
964
  */
882
- schedule(options = {}) {
883
- const {
884
- debounceMs = this.defaultDebounceMs,
885
- correlate: correlateQuery = { after: -1, limit: 100 },
886
- maxPasses = Infinity,
887
- ...drainOptions
888
- } = options;
889
- if (this._timer) clearTimeout(this._timer);
890
- this._timer = setTimeout(() => {
891
- this._timer = void 0;
892
- if (this._running) return;
893
- this._running = true;
894
- (async () => {
895
- await this.deps.init();
896
- let lastDrain;
897
- for (let i = 0; i < maxPasses; i++) {
898
- const { subscribed } = await this.deps.correlate({
899
- ...correlateQuery,
900
- after: this.deps.checkpoint()
901
- });
902
- lastDrain = await this.deps.drain(drainOptions);
903
- const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
904
- if (!made_progress) break;
905
- }
906
- if (lastDrain) this.deps.onSettled(lastDrain);
907
- })().catch((err) => this.deps.logger.error(err)).finally(() => {
908
- this._running = false;
909
- });
910
- }, debounceMs);
965
+ start(cycleMs) {
966
+ if (this._worker || this._stopped) return;
967
+ const tick = async () => {
968
+ if (this._armed) await this.drain();
969
+ if (this._stopped) return;
970
+ this._worker = setTimeout(tick, cycleMs);
971
+ this._worker.unref();
972
+ };
973
+ this._worker = setTimeout(tick, cycleMs);
974
+ this._worker.unref();
911
975
  }
912
- /** Cancel any pending or active settle cycle. Idempotent. */
976
+ /** Stop the per-lane worker. Idempotent. */
913
977
  stop() {
914
- if (this._timer) {
915
- clearTimeout(this._timer);
916
- this._timer = void 0;
978
+ this._stopped = true;
979
+ if (this._worker) {
980
+ clearTimeout(this._worker);
981
+ this._worker = void 0;
982
+ }
983
+ }
984
+ /** Run one drain pass. Short-circuits when not armed or already running. */
985
+ async drain(options = {}) {
986
+ if (!this._armed) return EMPTY_DRAIN;
987
+ if (this._locked) return EMPTY_DRAIN;
988
+ const d = this.deps.defaults ?? {};
989
+ const streamLimit = d.streamLimit ?? options.streamLimit ?? 10;
990
+ const eventLimit = d.eventLimit ?? options.eventLimit ?? 10;
991
+ const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? 1e4;
992
+ try {
993
+ this._locked = true;
994
+ const lagging = Math.ceil(streamLimit * this._ratio);
995
+ const leading = streamLimit - lagging;
996
+ const cycle = await runDrainCycle(
997
+ this.deps.ops,
998
+ this.deps.registry,
999
+ this.deps.batchHandlers,
1000
+ this.deps.handle,
1001
+ this.deps.handleBatch,
1002
+ lagging,
1003
+ leading,
1004
+ eventLimit,
1005
+ leaseMillis,
1006
+ this._backoff.size > 0 ? this.isDeferred : void 0,
1007
+ this.deps.lane
1008
+ );
1009
+ if (!cycle) {
1010
+ this._armed = false;
1011
+ return EMPTY_DRAIN;
1012
+ }
1013
+ const { leased, fetched, handled, acked, blocked } = cycle;
1014
+ traceCycle(this.deps.logger, leased, fetched, handled, acked, blocked);
1015
+ this._ratio = computeLagLeadRatio(handled, lagging, leading);
1016
+ for (const lease of acked) this._backoff.delete(lease.stream);
1017
+ for (const lease of blocked) this._backoff.delete(lease.stream);
1018
+ for (const h of handled) {
1019
+ if (h.nextAttemptAt !== void 0 && !h.block) {
1020
+ this._backoff.set(h.lease.stream, h.nextAttemptAt);
1021
+ }
1022
+ }
1023
+ if (this._backoff.size > 0) this.scheduleBackoffWake();
1024
+ if (acked.length) this.deps.onAcked(acked);
1025
+ if (blocked.length) this.deps.onBlocked(blocked);
1026
+ const hasErrors = handled.some(({ error }) => error);
1027
+ if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
1028
+ return { fetched, leased, acked, blocked };
1029
+ } catch (error) {
1030
+ this.deps.logger.error(error);
1031
+ return EMPTY_DRAIN;
1032
+ } finally {
1033
+ this._locked = false;
917
1034
  }
918
1035
  }
919
1036
  };
920
1037
 
921
- // src/internal/drain.ts
922
- var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
923
- async function fetch(leased, eventLimit) {
924
- return Promise.all(
925
- leased.map(async ({ stream, source, at, lagging }) => {
926
- const events = [];
927
- await store().query((e) => events.push(e), {
928
- stream: source,
929
- after: at,
930
- limit: eventLimit
931
- });
932
- return { stream, source, at, lagging, events };
933
- })
934
- );
1038
+ // src/internal/event-versions.ts
1039
+ var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
1040
+ function parse(name) {
1041
+ const m = name.match(VERSION_SUFFIX);
1042
+ if (m) {
1043
+ const v = Number.parseInt(m[2], 10);
1044
+ if (v >= 2) return { base: m[1], version: v };
1045
+ }
1046
+ return { base: name, version: 1 };
935
1047
  }
936
- var ack = (leases) => store().ack(leases);
937
- var block = (leases) => store().block(leases);
938
- var subscribe = (streams) => store().subscribe(streams);
939
-
940
- // src/internal/event-sourcing.ts
941
- import { patch } from "@rotorsoft/act-patch";
942
- async function snap(snapshot) {
943
- try {
944
- const { id, stream, name, meta, version } = snapshot.event;
945
- await store().commit(
946
- stream,
947
- [{ name: SNAP_EVENT, data: snapshot.state }],
948
- {
949
- correlation: meta.correlation,
950
- causation: { event: { id, name, stream } }
951
- },
952
- version
953
- // IMPORTANT! - state events are committed right after the snapshot event
954
- );
955
- } catch (error) {
956
- log().error(error);
1048
+ function deprecatedEventNames(names) {
1049
+ const groups = /* @__PURE__ */ new Map();
1050
+ for (const name of names) {
1051
+ const { base, version } = parse(name);
1052
+ const list = groups.get(base);
1053
+ if (list) list.push({ version, name });
1054
+ else groups.set(base, [{ version, name }]);
1055
+ }
1056
+ const deprecated = /* @__PURE__ */ new Set();
1057
+ for (const list of groups.values()) {
1058
+ if (list.length < 2) continue;
1059
+ list.sort((a, b) => b.version - a.version);
1060
+ for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
957
1061
  }
1062
+ return deprecated;
958
1063
  }
959
- async function tombstone(stream, expectedVersion, correlation) {
960
- try {
961
- const [committed] = await store().commit(
962
- stream,
963
- [{ name: TOMBSTONE_EVENT, data: {} }],
964
- { correlation, causation: {} },
965
- expectedVersion
966
- );
967
- return committed;
968
- } catch (error) {
969
- if (error instanceof ConcurrencyError) return void 0;
970
- throw error;
1064
+ function currentVersionOf(deprecatedName, allNames) {
1065
+ const target = parse(deprecatedName);
1066
+ let highest;
1067
+ for (const name of allNames) {
1068
+ const { base, version } = parse(name);
1069
+ if (base !== target.base) continue;
1070
+ if (!highest || version > highest.version) highest = { version, name };
971
1071
  }
1072
+ return highest && highest.version > target.version ? highest.name : void 0;
972
1073
  }
973
- async function load(me, stream, callback, asOf) {
974
- const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
975
- const cached = timeTravel ? void 0 : await cache().get(stream);
976
- const cache_hit = !!cached;
977
- let state2 = cached?.state ?? (me.init ? me.init() : {});
978
- let patches = cached?.patches ?? 0;
979
- let snaps = cached?.snaps ?? 0;
980
- let version = cached?.version ?? -1;
981
- let replayed = 0;
982
- let event;
983
- await store().query(
984
- (e) => {
985
- event = e;
986
- version = e.version;
987
- if (e.name === SNAP_EVENT) {
988
- state2 = e.data;
989
- snaps++;
990
- patches = 0;
991
- replayed++;
992
- } else if (me.patch[e.name]) {
993
- state2 = patch(state2, me.patch[e.name](event, state2));
994
- patches++;
995
- replayed++;
996
- } else if (e.name !== TOMBSTONE_EVENT) {
997
- log().warn(
998
- `Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
999
- );
1074
+
1075
+ // src/internal/merge.ts
1076
+ import { ZodObject } from "zod";
1077
+ function baseTypeName(zodType) {
1078
+ let t = zodType;
1079
+ while (typeof t.unwrap === "function") {
1080
+ t = t.unwrap();
1081
+ }
1082
+ return t.constructor.name;
1083
+ }
1084
+ function mergeSchemas(existing, incoming, stateName) {
1085
+ if (existing instanceof ZodObject && incoming instanceof ZodObject) {
1086
+ const existingShape = existing.shape;
1087
+ const incomingShape = incoming.shape;
1088
+ for (const key of Object.keys(incomingShape)) {
1089
+ if (key in existingShape) {
1090
+ const existingBase = baseTypeName(existingShape[key]);
1091
+ const incomingBase = baseTypeName(incomingShape[key]);
1092
+ if (existingBase !== incomingBase) {
1093
+ throw new Error(
1094
+ `Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
1095
+ );
1096
+ }
1000
1097
  }
1001
- callback?.({
1002
- event,
1003
- state: state2,
1004
- version,
1005
- patches,
1006
- snaps,
1007
- cache_hit,
1008
- replayed
1009
- });
1010
- },
1011
- {
1012
- stream,
1013
- stream_exact: true,
1014
- ...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
1015
1098
  }
1016
- );
1017
- if (replayed > 0 && !timeTravel && event) {
1018
- await cache().set(stream, {
1019
- state: state2,
1020
- version,
1021
- event_id: event.id,
1022
- patches,
1023
- snaps
1024
- });
1099
+ return existing.extend(incomingShape);
1100
+ }
1101
+ return existing;
1102
+ }
1103
+ function mergeInits(existing, incoming) {
1104
+ return () => ({ ...existing(), ...incoming() });
1105
+ }
1106
+ function registerState(state2, states, actions, events) {
1107
+ const existing = states.get(state2.name);
1108
+ if (existing) {
1109
+ mergeIntoExisting(state2, existing, states, actions, events);
1110
+ } else {
1111
+ registerNewState(state2, states, actions, events);
1025
1112
  }
1026
- return { event, state: state2, version, patches, snaps, cache_hit, replayed };
1027
1113
  }
1028
- async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = defaultCorrelator) {
1029
- const { stream, expectedVersion, actor } = target;
1030
- if (!stream) throw new Error("Missing target stream");
1031
- const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
1032
- const snapshot = await load(me, stream);
1033
- if (snapshot.event?.name === TOMBSTONE_EVENT)
1034
- throw new StreamClosedError(stream);
1035
- const expected = expectedVersion ?? snapshot.event?.version;
1036
- if (me.given) {
1037
- const invariants = me.given[action2] || [];
1038
- invariants.forEach(({ valid, description }) => {
1039
- if (!valid(snapshot.state, actor))
1040
- throw new InvariantError(
1041
- action2,
1042
- validated,
1043
- target,
1044
- snapshot,
1045
- description
1046
- );
1047
- });
1114
+ function registerNewState(state2, states, actions, events) {
1115
+ states.set(state2.name, state2);
1116
+ for (const name of Object.keys(state2.actions)) {
1117
+ if (actions[name]) throw new Error(`Duplicate action "${name}"`);
1118
+ actions[name] = state2;
1048
1119
  }
1049
- const result = me.on[action2](validated, snapshot, target);
1050
- if (!result) return [snapshot];
1051
- if (Array.isArray(result) && result.length === 0) {
1052
- return [snapshot];
1120
+ for (const name of Object.keys(state2.events)) {
1121
+ if (events[name]) throw new Error(`Duplicate event "${name}"`);
1122
+ events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
1053
1123
  }
1054
- const tuples = Array.isArray(result[0]) ? result : [result];
1055
- const deprecated = me._deprecated;
1056
- if (deprecated && deprecated.size > 0) {
1057
- const me_ = me;
1058
- const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
1059
- for (const [name] of tuples) {
1060
- const evt = name;
1061
- if (deprecated.has(evt) && !warned.has(evt)) {
1062
- warned.add(evt);
1063
- log().warn(
1064
- `Action "${String(action2)}" emitted deprecated event "${evt}". A newer version exists in the registry \u2014 update the action's .emit() to target the current version. (warned once per process)`
1065
- );
1066
- }
1067
- }
1124
+ }
1125
+ function mergeIntoExisting(state2, existing, states, actions, events) {
1126
+ for (const name of Object.keys(state2.actions)) {
1127
+ if (existing.actions[name] === state2.actions[name]) continue;
1128
+ if (actions[name]) throw new Error(`Duplicate action "${name}"`);
1068
1129
  }
1069
- const emitted = tuples.map(([name, data]) => ({
1070
- name,
1071
- data: skipValidation ? data : validate(name, data, me.events[name])
1072
- }));
1073
- const meta = {
1074
- correlation: reactingTo?.meta.correlation || correlator({
1075
- action: action2,
1076
- state: me.name,
1077
- stream,
1078
- actor: target.actor
1079
- }),
1080
- causation: {
1081
- action: {
1082
- name: action2,
1083
- ...target
1084
- // payload intentionally omitted: it can be large or contain PII,
1085
- // and callers correlate via the correlation id when they need it.
1086
- },
1087
- event: reactingTo ? {
1088
- id: reactingTo.id,
1089
- name: reactingTo.name,
1090
- stream: reactingTo.stream
1091
- } : void 0
1130
+ for (const name of Object.keys(state2.events)) {
1131
+ if (existing.events[name] === state2.events[name]) continue;
1132
+ if (existing.events[name]) {
1133
+ throw new Error(
1134
+ `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.`
1135
+ );
1092
1136
  }
1137
+ if (events[name]) throw new Error(`Duplicate event "${name}"`);
1138
+ }
1139
+ const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
1140
+ const merged = {
1141
+ ...existing,
1142
+ state: mergeSchemas(existing.state, state2.state, state2.name),
1143
+ init: mergeInits(existing.init, state2.init),
1144
+ events: { ...existing.events, ...state2.events },
1145
+ actions: { ...existing.actions, ...state2.actions },
1146
+ patch: mergedPatch,
1147
+ on: { ...existing.on, ...state2.on },
1148
+ given: { ...existing.given, ...state2.given },
1149
+ snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
1150
+ throw new Error(
1151
+ `Duplicate snap strategy for state "${state2.name}"`
1152
+ );
1153
+ })() : state2.snap || existing.snap
1093
1154
  };
1094
- let committed;
1095
- try {
1096
- committed = await store().commit(
1097
- stream,
1098
- emitted,
1099
- meta,
1100
- // Reactions skip optimistic concurrency: they always append against the
1101
- // current head. Stream leasing already serializes concurrent reactions,
1102
- // and forcing version checks here would turn ordinary catch-up into
1103
- // spurious retries.
1104
- reactingTo ? void 0 : expected
1105
- );
1106
- } catch (error) {
1107
- if (error instanceof ConcurrencyError) {
1108
- await cache().invalidate(stream);
1155
+ states.set(state2.name, merged);
1156
+ for (const name of Object.keys(merged.actions)) {
1157
+ actions[name] = merged;
1158
+ }
1159
+ for (const name of Object.keys(state2.events)) {
1160
+ if (events[name]) continue;
1161
+ events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
1162
+ }
1163
+ }
1164
+ function mergePatches(existing, incoming, stateName) {
1165
+ const merged = { ...existing };
1166
+ for (const name of Object.keys(incoming)) {
1167
+ const existingP = existing[name];
1168
+ const incomingP = incoming[name];
1169
+ if (!existingP) {
1170
+ merged[name] = incomingP;
1171
+ continue;
1172
+ }
1173
+ const existingIsDefault = existingP._passthrough;
1174
+ const incomingIsDefault = incomingP._passthrough;
1175
+ if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
1176
+ throw new Error(
1177
+ `Duplicate custom patch for event "${name}" in state "${stateName}"`
1178
+ );
1179
+ }
1180
+ if (existingIsDefault && !incomingIsDefault) {
1181
+ merged[name] = incomingP;
1109
1182
  }
1110
- throw error;
1111
1183
  }
1112
- let { state: state2, patches } = snapshot;
1113
- const snapshots = committed.map((event) => {
1114
- const p = me.patch[event.name](event, state2);
1115
- state2 = patch(state2, p);
1116
- patches++;
1117
- return {
1118
- event,
1119
- state: state2,
1120
- version: event.version,
1121
- patches,
1122
- snaps: snapshot.snaps,
1123
- patch: p,
1124
- cache_hit: snapshot.cache_hit,
1125
- replayed: snapshot.replayed
1126
- };
1127
- });
1128
- const last = snapshots.at(-1);
1129
- const snapped = me.snap?.(last);
1130
- cache().set(stream, {
1131
- state: last.state,
1132
- version: last.event.version,
1133
- event_id: last.event.id,
1134
- patches: snapped ? 0 : last.patches,
1135
- snaps: snapped ? last.snaps + 1 : last.snaps
1136
- }).catch((err) => log().error(err));
1137
- if (snapped) void snap(last);
1138
- return snapshots;
1184
+ return merged;
1139
1185
  }
1140
-
1141
- // src/internal/tracing.ts
1142
- var PRETTY = config().env !== "production";
1143
- var C_BLUE = "\x1B[38;5;39m";
1144
- var C_ORANGE = "\x1B[38;5;208m";
1145
- var C_GREEN = "\x1B[38;5;42m";
1146
- var C_MAGENTA = "\x1B[38;5;165m";
1147
- var C_DRAIN = "\x1B[38;5;244m";
1148
- var C_HIT = "\x1B[38;5;82m";
1149
- var C_MISS = "\x1B[38;5;220m";
1150
- var C_RESET = "\x1B[0m";
1151
- var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
1152
- var drain_caption = (caption) => {
1153
- const tag = `>> ${caption}`;
1154
- return PRETTY ? `${C_DRAIN}${tag}${C_RESET}` : tag;
1155
- };
1156
- var cache_marker = (hit) => {
1157
- const word = hit ? "hit" : "miss";
1158
- if (!PRETTY) return word;
1159
- return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
1160
- };
1161
- var stats_marker = (version, replayed, snaps, patches) => {
1162
- const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
1163
- if (!PRETTY) return text;
1164
- return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
1165
- };
1166
- var as_of_marker = (asOf) => {
1167
- if (!asOf) return "";
1168
- const parts = [];
1169
- if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
1170
- if (asOf.created_before !== void 0)
1171
- parts.push(`created_before=${asOf.created_before.toISOString()}`);
1172
- if (asOf.created_after !== void 0)
1173
- parts.push(`created_after=${asOf.created_after.toISOString()}`);
1174
- if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
1175
- return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
1176
- };
1177
- var traced = (inner, exit, entry) => (async (...args) => {
1178
- entry?.(...args);
1179
- const result = await inner(...args);
1180
- exit?.(result, ...args);
1181
- return result;
1186
+ function mergeEventRegister(target, source) {
1187
+ for (const [eventName, sourceReg] of Object.entries(source)) {
1188
+ const targetReg = target[eventName];
1189
+ if (!targetReg) continue;
1190
+ for (const [name, reaction] of sourceReg.reactions) {
1191
+ targetReg.reactions.set(name, reaction);
1192
+ }
1193
+ }
1194
+ }
1195
+ function mergeProjection(proj, events) {
1196
+ for (const eventName of Object.keys(proj.events)) {
1197
+ const projRegister = proj.events[eventName];
1198
+ const existing = events[eventName];
1199
+ if (!existing) {
1200
+ events[eventName] = {
1201
+ schema: projRegister.schema,
1202
+ reactions: new Map(projRegister.reactions)
1203
+ };
1204
+ } else {
1205
+ for (const [name, reaction] of projRegister.reactions) {
1206
+ let key = name;
1207
+ while (existing.reactions.has(key)) key = `${key}_p`;
1208
+ existing.reactions.set(key, reaction);
1209
+ }
1210
+ }
1211
+ }
1212
+ }
1213
+ var _this_ = ({ stream }) => ({
1214
+ source: stream,
1215
+ target: stream
1182
1216
  });
1183
- function buildEs(logger, correlator = defaultCorrelator) {
1184
- const boundAction = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
1185
- me,
1186
- actionName,
1187
- target,
1188
- payload,
1189
- reactingTo,
1190
- skipValidation,
1191
- correlator
1192
- );
1193
- if (logger.level !== "trace") {
1194
- return {
1195
- snap,
1196
- load,
1197
- action: boundAction,
1198
- tombstone
1199
- };
1217
+
1218
+ // src/internal/backoff.ts
1219
+ function computeBackoffDelay(retry, opts) {
1220
+ if (!opts || opts.baseMs <= 0) return 0;
1221
+ const r = Math.max(0, retry);
1222
+ let delay;
1223
+ switch (opts.strategy) {
1224
+ case "fixed":
1225
+ delay = opts.baseMs;
1226
+ break;
1227
+ case "linear":
1228
+ delay = opts.baseMs * (r + 1);
1229
+ break;
1230
+ case "exponential":
1231
+ delay = opts.baseMs * 2 ** r;
1232
+ if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
1233
+ break;
1200
1234
  }
1235
+ if (opts.jitter) delay = delay * (0.5 + Math.random());
1236
+ return Math.max(0, Math.floor(delay));
1237
+ }
1238
+
1239
+ // src/internal/reactions.ts
1240
+ function finalize(lease, handled, at, error, options, logger, failed_at) {
1241
+ if (!error) return { lease, handled, acked_at: at };
1242
+ logger.error(error);
1243
+ const nonRetryable = error instanceof NonRetryableError;
1244
+ const block2 = options.blockOnError && (nonRetryable || lease.retry >= options.maxRetries);
1245
+ if (block2)
1246
+ logger.error(
1247
+ nonRetryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
1248
+ );
1249
+ const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
1201
1250
  return {
1202
- snap: traced(snap, void 0, (snapshot) => {
1203
- logger.trace(
1204
- es_caption(
1205
- "snap",
1206
- C_MAGENTA,
1207
- `${snapshot.event.stream}@${snapshot.event.version}`
1208
- )
1209
- );
1210
- }),
1211
- load: traced(load, (result, _me, stream, _cb, asOf) => {
1212
- const stats = stats_marker(
1213
- result.version,
1214
- result.replayed,
1215
- result.snaps,
1216
- result.patches
1217
- );
1218
- logger.trace(
1219
- es_caption(
1220
- "load",
1221
- C_GREEN,
1222
- `${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
1223
- )
1224
- );
1225
- }),
1226
- action: traced(
1227
- boundAction,
1228
- (snapshots, _me, _action, target) => {
1229
- const committed = snapshots.filter((s) => s.event);
1230
- if (committed.length) {
1231
- logger.trace(
1232
- committed.map((s) => s.event.data),
1233
- es_caption(
1234
- "committed",
1235
- C_ORANGE,
1236
- `${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
1237
- )
1238
- );
1239
- }
1240
- },
1241
- (_me, action2, target, payload) => {
1242
- logger.trace(
1243
- payload,
1244
- es_caption("action", C_BLUE, `${target.stream}.${action2}`)
1245
- );
1246
- }
1247
- ),
1248
- tombstone: traced(tombstone, (committed, stream) => {
1249
- if (committed)
1250
- logger.trace(
1251
- es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
1252
- );
1253
- })
1251
+ lease,
1252
+ handled,
1253
+ acked_at: at,
1254
+ error: error.message,
1255
+ block: block2,
1256
+ nextAttemptAt,
1257
+ failed_at
1254
1258
  };
1255
1259
  }
1256
- function buildDrain(logger) {
1257
- if (logger.level !== "trace") {
1258
- return {
1259
- claim,
1260
- fetch,
1261
- ack,
1262
- block,
1263
- subscribe
1260
+ function buildHandle(deps) {
1261
+ const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
1262
+ return async (lease, payloads) => {
1263
+ if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
1264
+ const stream = lease.stream;
1265
+ let at = payloads.at(0).event.id;
1266
+ let handled = 0;
1267
+ if (lease.retry > 0)
1268
+ logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
1269
+ const scopedApp = {
1270
+ do: boundDo,
1271
+ load: boundLoad,
1272
+ query: boundQuery,
1273
+ query_array: boundQueryArray
1264
1274
  };
1265
- }
1266
- return {
1267
- claim: traced(claim, (leased) => {
1268
- if (leased.length) {
1269
- const data = Object.fromEntries(
1270
- leased.map(({ stream, at, retry }) => [stream, { at, retry }])
1271
- );
1272
- logger.trace(data, drain_caption("claimed"));
1273
- }
1274
- }),
1275
- fetch: traced(fetch, (fetched) => {
1276
- const data = Object.fromEntries(
1277
- fetched.map(({ stream, source, events }) => {
1278
- const key = source ? `${stream}<-${source}` : stream;
1279
- const value = Object.fromEntries(
1280
- events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
1281
- );
1282
- return [key, value];
1283
- })
1275
+ for (const payload of payloads) {
1276
+ const { event, handler } = payload;
1277
+ scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
1278
+ action2,
1279
+ target,
1280
+ actionPayload,
1281
+ reactingTo ?? event,
1282
+ skipValidation
1284
1283
  );
1285
- logger.trace(data, drain_caption("fetched"));
1286
- }),
1287
- ack: traced(ack, (acked) => {
1288
- if (acked.length) {
1289
- const data = Object.fromEntries(
1290
- acked.map(({ stream, at, retry }) => [stream, { at, retry }])
1291
- );
1292
- logger.trace(data, drain_caption("acked"));
1293
- }
1294
- }),
1295
- block: traced(block, (blocked) => {
1296
- if (blocked.length) {
1297
- const data = Object.fromEntries(
1298
- blocked.map(({ stream, at, retry, error }) => [
1299
- stream,
1300
- { at, retry, error }
1301
- ])
1284
+ try {
1285
+ await handler(event, stream, scopedApp);
1286
+ at = event.id;
1287
+ handled++;
1288
+ } catch (error) {
1289
+ return finalize(
1290
+ lease,
1291
+ handled,
1292
+ at,
1293
+ error,
1294
+ payload.options,
1295
+ logger,
1296
+ event.id
1302
1297
  );
1303
- logger.trace(data, drain_caption("blocked"));
1304
- }
1305
- }),
1306
- subscribe: traced(subscribe, (result, streams) => {
1307
- if (result.subscribed) {
1308
- const data = streams.map(({ stream }) => stream).join(" ");
1309
- logger.trace(`${drain_caption("correlated")} ${data}`);
1310
1298
  }
1311
- })
1299
+ }
1300
+ return finalize(lease, handled, at, void 0, payloads[0].options, logger);
1301
+ };
1302
+ }
1303
+ function buildHandleBatch(logger) {
1304
+ return async (lease, payloads, batchHandler) => {
1305
+ const stream = lease.stream;
1306
+ const events = payloads.map((p) => p.event);
1307
+ const options = payloads[0].options;
1308
+ if (lease.retry > 0)
1309
+ logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
1310
+ try {
1311
+ await batchHandler(events, stream);
1312
+ return finalize(
1313
+ lease,
1314
+ events.length,
1315
+ events.at(-1).id,
1316
+ void 0,
1317
+ options,
1318
+ logger
1319
+ );
1320
+ } catch (error) {
1321
+ return finalize(lease, 0, lease.at, error, options, logger);
1322
+ }
1312
1323
  };
1313
1324
  }
1314
1325
 
1326
+ // src/internal/settle.ts
1327
+ var SettleLoop = class {
1328
+ constructor(deps, defaultDebounceMs) {
1329
+ this.deps = deps;
1330
+ this.defaultDebounceMs = defaultDebounceMs;
1331
+ }
1332
+ _timer = void 0;
1333
+ _running = false;
1334
+ /**
1335
+ * Schedule a settle pass. Multiple calls inside the debounce window
1336
+ * coalesce into one cycle. The cycle runs correlate→drain in a loop
1337
+ * until no progress is made (no new subscriptions, no acks, no blocks)
1338
+ * or `maxPasses` is reached, then emits the `"settled"` lifecycle event
1339
+ * via {@link SettleDeps.onSettled}.
1340
+ */
1341
+ schedule(options = {}) {
1342
+ const {
1343
+ debounceMs = this.defaultDebounceMs,
1344
+ correlate: correlateQuery = { after: -1, limit: 100 },
1345
+ maxPasses = Infinity,
1346
+ ...drainOptions
1347
+ } = options;
1348
+ if (this._timer) clearTimeout(this._timer);
1349
+ this._timer = setTimeout(() => {
1350
+ this._timer = void 0;
1351
+ if (this._running) return;
1352
+ this._running = true;
1353
+ (async () => {
1354
+ await this.deps.init();
1355
+ let lastDrain;
1356
+ for (let i = 0; i < maxPasses; i++) {
1357
+ const { subscribed } = await this.deps.correlate({
1358
+ ...correlateQuery,
1359
+ after: this.deps.checkpoint()
1360
+ });
1361
+ lastDrain = await this.deps.drain(drainOptions);
1362
+ const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
1363
+ if (!made_progress) break;
1364
+ }
1365
+ if (lastDrain) this.deps.onSettled(lastDrain);
1366
+ })().catch((err) => this.deps.logger.error(err)).finally(() => {
1367
+ this._running = false;
1368
+ });
1369
+ }, debounceMs);
1370
+ }
1371
+ /** Cancel any pending or active settle cycle. Idempotent. */
1372
+ stop() {
1373
+ if (this._timer) {
1374
+ clearTimeout(this._timer);
1375
+ this._timer = void 0;
1376
+ }
1377
+ }
1378
+ };
1379
+
1315
1380
  // src/act.ts
1316
1381
  var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
1317
1382
  var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
@@ -1326,11 +1391,26 @@ var Act = class {
1326
1391
  * @param _states Merged map of state name → state definition
1327
1392
  * @param batchHandlers Static-target projection batch handlers (target → handler)
1328
1393
  * @param options Tuning knobs — see {@link ActOptions}
1394
+ * @param lanes Declared drain lanes (ACT-1103). The builder collects
1395
+ * these from `.withLane(...)` calls. Slice 1 records them on the
1396
+ * instance; later slices fan out one `DrainController` per lane.
1329
1397
  */
1330
- constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
1398
+ constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
1331
1399
  this.registry = registry;
1332
1400
  this._states = _states;
1333
1401
  this._batch_handlers = batchHandlers;
1402
+ this._lanes = lanes;
1403
+ if (options.onlyLanes && options.onlyLanes.length > 0) {
1404
+ const declared = /* @__PURE__ */ new Set([
1405
+ "default",
1406
+ ...lanes.map((l) => l.name)
1407
+ ]);
1408
+ const unknown = options.onlyLanes.filter((l) => !declared.has(l));
1409
+ if (unknown.length > 0)
1410
+ throw new Error(
1411
+ `ActOptions.onlyLanes references undeclared lane(s): ${unknown.map((l) => `"${l}"`).join(", ")}`
1412
+ );
1413
+ }
1334
1414
  this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
1335
1415
  this._correlator = options.correlator ?? defaultCorrelator;
1336
1416
  this._es = buildEs(this._logger, this._correlator);
@@ -1343,19 +1423,44 @@ var Act = class {
1343
1423
  boundQueryArray: this._bound_query_array
1344
1424
  });
1345
1425
  this._handle_batch = buildHandleBatch(this._logger);
1346
- const { staticTargets, hasDynamicResolvers, reactiveEvents, eventToState } = classifyRegistry(this.registry, this._states);
1426
+ const {
1427
+ staticTargets,
1428
+ hasDynamicResolvers,
1429
+ reactiveEvents,
1430
+ eventToState,
1431
+ eventToLanes
1432
+ } = classifyRegistry(this.registry, this._states);
1347
1433
  this._reactive_events = reactiveEvents;
1348
1434
  this._event_to_state = eventToState;
1349
- this._drain = new DrainController({
1350
- logger: this._logger,
1351
- ops: this._cd,
1352
- registry: this.registry,
1353
- batchHandlers: this._batch_handlers,
1354
- handle: this._handle,
1355
- handleBatch: this._handle_batch,
1356
- onAcked: (acked) => this.emit("acked", acked),
1357
- onBlocked: (blocked) => this.emit("blocked", blocked)
1358
- });
1435
+ this._event_to_lanes = eventToLanes;
1436
+ const allLanes = ["default", ...lanes.map((l) => l.name)];
1437
+ const onlySet = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
1438
+ const activeLanes = onlySet ? allLanes.filter((n) => onlySet.has(n)) : allLanes;
1439
+ const singleDefaultLane = activeLanes.length === 1 && activeLanes[0] === "default";
1440
+ this._drain_controllers = /* @__PURE__ */ new Map();
1441
+ for (const name of activeLanes) {
1442
+ const cfg = lanes.find((l) => l.name === name);
1443
+ const controller = new DrainController({
1444
+ logger: this._logger,
1445
+ ops: this._cd,
1446
+ registry: this.registry,
1447
+ batchHandlers: this._batch_handlers,
1448
+ handle: this._handle,
1449
+ handleBatch: this._handle_batch,
1450
+ onAcked: (acked) => this.emit("acked", acked),
1451
+ onBlocked: (blocked) => this.emit("blocked", blocked),
1452
+ // Pass lane only when a true per-lane controller is active.
1453
+ // The all-lanes (single default) case keeps lane=undefined so
1454
+ // adapter SQL collapses to the pre-1103 shape.
1455
+ lane: singleDefaultLane ? void 0 : name,
1456
+ defaults: cfg && {
1457
+ streamLimit: cfg.streamLimit,
1458
+ leaseMillis: cfg.leaseMillis
1459
+ }
1460
+ });
1461
+ if (cfg?.cycleMs !== void 0) controller.start(cfg.cycleMs);
1462
+ this._drain_controllers.set(name, controller);
1463
+ }
1359
1464
  this._correlate = new CorrelateCycle(
1360
1465
  this.registry,
1361
1466
  staticTargets,
@@ -1364,7 +1469,7 @@ var Act = class {
1364
1469
  options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
1365
1470
  // Cold start: assume drain is needed (historical events may need processing)
1366
1471
  () => {
1367
- if (this._reactive_events.size > 0) this._drain.arm();
1472
+ if (this._reactive_events.size > 0) this._armAll();
1368
1473
  }
1369
1474
  );
1370
1475
  this._settle = new SettleLoop(
@@ -1384,8 +1489,8 @@ var Act = class {
1384
1489
  _emitter = new EventEmitter();
1385
1490
  /** Event names with at least one registered reaction (computed at build time) */
1386
1491
  _reactive_events;
1387
- /** Drain pipeline driver: armed flag, concurrency lock, adaptive ratio. */
1388
- _drain;
1492
+ /** One DrainController per active lane, keyed by lane name. */
1493
+ _drain_controllers;
1389
1494
  /** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
1390
1495
  _correlate;
1391
1496
  /** Debounced correlate→drain catch-up loop. */
@@ -1439,6 +1544,14 @@ var Act = class {
1439
1544
  * set when seeding a `restart` snapshot in multi-state apps.
1440
1545
  */
1441
1546
  _event_to_state;
1547
+ /**
1548
+ * Event-name → lane fan-in for selective arming (ACT-1103). Built by
1549
+ * `classifyRegistry` once per build. `"all"` means at least one of
1550
+ * the event's reactions is a dynamic resolver (lane opaque until
1551
+ * runtime); a `Set<string>` lists the static lanes only that event's
1552
+ * reactions target.
1553
+ */
1554
+ _event_to_lanes;
1442
1555
  /** Logger resolved at construction time (after user port configuration) */
1443
1556
  _logger = log();
1444
1557
  /** Wraps a public-method body so internal `store()`/`cache()` resolve to the
@@ -1462,6 +1575,12 @@ var Act = class {
1462
1575
  /** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
1463
1576
  _handle;
1464
1577
  _handle_batch;
1578
+ /** Declared drain lanes (ACT-1103). */
1579
+ _lanes;
1580
+ /** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
1581
+ get lanes() {
1582
+ return this._lanes;
1583
+ }
1465
1584
  /** True after the first `shutdown()` call. Guards idempotency. */
1466
1585
  _shutdown_promise;
1467
1586
  /**
@@ -1480,6 +1599,7 @@ var Act = class {
1480
1599
  this._emitter.removeAllListeners();
1481
1600
  this.stop_correlations();
1482
1601
  this.stop_settling();
1602
+ for (const c of this._drain_controllers.values()) c.stop();
1483
1603
  const disposer = await this._notify_disposer;
1484
1604
  if (disposer) await disposer();
1485
1605
  })();
@@ -1499,13 +1619,10 @@ var Act = class {
1499
1619
  return await s.notify((notification) => {
1500
1620
  try {
1501
1621
  this.emit("notified", notification);
1502
- const hasReactive = notification.events.some(
1503
- (e) => this._reactive_events.has(e.name)
1622
+ const armed = this._armForEventNames(
1623
+ notification.events.map((e) => e.name)
1504
1624
  );
1505
- if (hasReactive) {
1506
- this._drain.arm();
1507
- this._settle.schedule({ debounceMs: 0 });
1508
- }
1625
+ if (armed) this._settle.schedule({ debounceMs: 0 });
1509
1626
  } catch (err) {
1510
1627
  this._logger.error(err, "notified handler threw");
1511
1628
  }
@@ -1606,14 +1723,10 @@ var Act = class {
1606
1723
  reactingTo,
1607
1724
  skipValidation
1608
1725
  );
1609
- if (this._reactive_events.size > 0) {
1610
- for (const snap2 of snapshots) {
1611
- if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
1612
- this._drain.arm();
1613
- break;
1614
- }
1615
- }
1616
- }
1726
+ if (this._reactive_events.size > 0)
1727
+ this._armForEventNames(
1728
+ snapshots.map((s) => s.event.name)
1729
+ );
1617
1730
  this.emit("committed", snapshots);
1618
1731
  return snapshots;
1619
1732
  });
@@ -1760,7 +1873,59 @@ var Act = class {
1760
1873
  * @see {@link start_correlations} for automatic correlation
1761
1874
  */
1762
1875
  async drain(options = {}) {
1763
- return this._scoped(() => this._drain.drain(options));
1876
+ return this._scoped(() => this._drainAll(options));
1877
+ }
1878
+ /** Arm every active lane controller (ACT-1103). */
1879
+ _armAll() {
1880
+ for (const c of this._drain_controllers.values()) c.arm();
1881
+ }
1882
+ /**
1883
+ * Arm only the lane controllers whose reactions match the supplied
1884
+ * event names (ACT-1103 selective arming). Events with any dynamic
1885
+ * resolver fall back to `_armAll()` via the `"all"` sentinel — the
1886
+ * resolver's lane isn't known until correlate runs the function.
1887
+ * Events with no reactions are skipped; `_event_to_lanes` doesn't
1888
+ * carry them. Returns true when any controller was armed (used by
1889
+ * the notify handler to decide whether to schedule a settle).
1890
+ */
1891
+ _armForEventNames(names) {
1892
+ const to_arm = /* @__PURE__ */ new Set();
1893
+ for (const name of names) {
1894
+ const set = this._event_to_lanes.get(name);
1895
+ if (set === void 0) continue;
1896
+ if (set === ALL_LANES) {
1897
+ this._armAll();
1898
+ return true;
1899
+ }
1900
+ for (const lane of set) to_arm.add(lane);
1901
+ }
1902
+ if (to_arm.size === 0) return false;
1903
+ for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
1904
+ return true;
1905
+ }
1906
+ /** Drain every active lane controller in parallel and aggregate.
1907
+ *
1908
+ * Parallel — not sequential — so a slow lane's in-flight handler does
1909
+ * not block a fast lane's claim/dispatch/ack cycle. Each controller's
1910
+ * `claim()` is independent (filtered by lane); the store's
1911
+ * `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
1912
+ * (`acked`, `blocked`) may interleave by lane — listeners filter via
1913
+ * `lease.lane`. */
1914
+ async _drainAll(options) {
1915
+ const results = await Promise.all(
1916
+ [...this._drain_controllers.values()].map((c) => c.drain(options))
1917
+ );
1918
+ const fetched = [];
1919
+ const leased = [];
1920
+ const acked = [];
1921
+ const blocked = [];
1922
+ for (const r of results) {
1923
+ fetched.push(...r.fetched);
1924
+ leased.push(...r.leased);
1925
+ acked.push(...r.acked);
1926
+ blocked.push(...r.blocked);
1927
+ }
1928
+ return { fetched, leased, acked, blocked };
1764
1929
  }
1765
1930
  /**
1766
1931
  * Discovers and registers new streams dynamically based on reaction resolvers.
@@ -1931,7 +2096,7 @@ var Act = class {
1931
2096
  async reset(input) {
1932
2097
  return this._scoped(async () => {
1933
2098
  const count = await store().reset(input);
1934
- if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
2099
+ if (count > 0 && this._reactive_events.size > 0) this._armAll();
1935
2100
  return count;
1936
2101
  });
1937
2102
  }
@@ -1965,7 +2130,7 @@ var Act = class {
1965
2130
  async unblock(input) {
1966
2131
  return this._scoped(async () => {
1967
2132
  const count = await store().unblock(input);
1968
- if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
2133
+ if (count > 0 && this._reactive_events.size > 0) this._armAll();
1969
2134
  return count;
1970
2135
  });
1971
2136
  }
@@ -2138,6 +2303,22 @@ function registerBatchHandler(proj, batchHandlers) {
2138
2303
  }
2139
2304
  batchHandlers.set(proj.target, proj.batchHandler);
2140
2305
  }
2306
+ function validateLaneReferences(registry, lanes) {
2307
+ const declared = /* @__PURE__ */ new Set([DEFAULT_LANE, ...lanes.map((l) => l.name)]);
2308
+ for (const [eventName, def] of Object.entries(registry.events)) {
2309
+ const entry = def;
2310
+ for (const [handlerName, reaction] of entry.reactions) {
2311
+ const resolver = reaction.resolver;
2312
+ if (typeof resolver === "function") continue;
2313
+ const lane = resolver.lane;
2314
+ if (lane && !declared.has(lane)) {
2315
+ throw new Error(
2316
+ `Reaction "${handlerName}" on "${eventName}" targets undeclared lane "${lane}". Declared lanes: ${[...declared].map((l) => `"${l}"`).join(", ")}. Add \`.withLane({ name: "${lane}", ... })\` to act() or correct the .to() declaration.`
2317
+ );
2318
+ }
2319
+ }
2320
+ }
2321
+ }
2141
2322
  function act() {
2142
2323
  const states = /* @__PURE__ */ new Map();
2143
2324
  const registry = {
@@ -2146,6 +2327,7 @@ function act() {
2146
2327
  };
2147
2328
  const pendingProjections = [];
2148
2329
  const batchHandlers = /* @__PURE__ */ new Map();
2330
+ const lanes = [];
2149
2331
  let _built = false;
2150
2332
  const finalizeDeprecations = () => {
2151
2333
  const deprecationSummary = [];
@@ -2192,6 +2374,18 @@ function act() {
2192
2374
  }
2193
2375
  mergeEventRegister(registry.events, input.events);
2194
2376
  pendingProjections.push(...input.projections);
2377
+ for (const sliceLane of input.lanes) {
2378
+ const existing = lanes.find((l) => l.name === sliceLane.name);
2379
+ if (!existing) {
2380
+ lanes.push(sliceLane);
2381
+ continue;
2382
+ }
2383
+ if (existing.leaseMillis !== sliceLane.leaseMillis || existing.streamLimit !== sliceLane.streamLimit || existing.cycleMs !== sliceLane.cycleMs) {
2384
+ throw new Error(
2385
+ `Lane "${sliceLane.name}" was already declared with a different config`
2386
+ );
2387
+ }
2388
+ }
2195
2389
  return builder;
2196
2390
  },
2197
2391
  withProjection: (proj) => {
@@ -2200,6 +2394,14 @@ function act() {
2200
2394
  return builder;
2201
2395
  },
2202
2396
  withActor: () => builder,
2397
+ withLane: (config2) => {
2398
+ if (config2.name === DEFAULT_LANE)
2399
+ throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
2400
+ if (lanes.some((l) => l.name === config2.name))
2401
+ throw new Error(`Lane "${config2.name}" was already declared`);
2402
+ lanes.push(config2);
2403
+ return builder;
2404
+ },
2203
2405
  on: (event) => ({
2204
2406
  do: (handler, options) => {
2205
2407
  const reaction = {
@@ -2231,13 +2433,15 @@ function act() {
2231
2433
  registerBatchHandler(proj, batchHandlers);
2232
2434
  }
2233
2435
  finalizeDeprecations();
2436
+ validateLaneReferences(registry, lanes);
2234
2437
  _built = true;
2235
2438
  }
2236
2439
  return new Act(
2237
2440
  registry,
2238
2441
  states,
2239
2442
  batchHandlers,
2240
- options
2443
+ options,
2444
+ lanes
2241
2445
  );
2242
2446
  },
2243
2447
  events: registry.events
@@ -2318,6 +2522,7 @@ function slice() {
2318
2522
  const actions = {};
2319
2523
  const events = {};
2320
2524
  const projections = [];
2525
+ const lanes = [];
2321
2526
  const builder = {
2322
2527
  withState: (state2) => {
2323
2528
  registerState(state2, states, actions, events);
@@ -2327,6 +2532,14 @@ function slice() {
2327
2532
  projections.push(proj);
2328
2533
  return builder;
2329
2534
  },
2535
+ withLane: (config2) => {
2536
+ if (config2.name === DEFAULT_LANE)
2537
+ throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
2538
+ if (lanes.some((l) => l.name === config2.name))
2539
+ throw new Error(`Lane "${config2.name}" was already declared`);
2540
+ lanes.push(config2);
2541
+ return builder;
2542
+ },
2330
2543
  on: (event) => ({
2331
2544
  do: (handler, options) => {
2332
2545
  const reaction = {
@@ -2355,7 +2568,8 @@ function slice() {
2355
2568
  _tag: "Slice",
2356
2569
  states,
2357
2570
  events,
2358
- projections
2571
+ projections,
2572
+ lanes
2359
2573
  }),
2360
2574
  events
2361
2575
  };
@@ -2448,6 +2662,7 @@ export {
2448
2662
  CommittedMetaSchema,
2449
2663
  ConcurrencyError,
2450
2664
  ConsoleLogger,
2665
+ DEFAULT_LANE,
2451
2666
  DEFAULT_MAX_SUBSCRIBED_STREAMS,
2452
2667
  DEFAULT_SETTLE_DEBOUNCE_MS,
2453
2668
  Environments,