@rotorsoft/act 0.44.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.
- package/README.md +87 -379
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +43 -5
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/console-logger.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +4 -1
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts +33 -9
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/slice-builder.d.ts +23 -8
- package/dist/@types/builders/slice-builder.d.ts.map +1 -1
- package/dist/@types/internal/build-classify.d.ts +20 -0
- package/dist/@types/internal/build-classify.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts +1 -0
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts +43 -3
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain.d.ts +3 -1
- package/dist/@types/internal/drain.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +3 -2
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/reactions.d.ts.map +1 -1
- package/dist/@types/internal/tracing.d.ts +51 -0
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/@types/ports.d.ts +10 -0
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/test/sandbox.d.ts +1 -1
- package/dist/@types/test/sandbox.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +9 -2
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +20 -2
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/{chunk-LKRNWD7C.js → chunk-PGTC7VOC.js} +46 -11
- package/dist/chunk-PGTC7VOC.js.map +1 -0
- package/dist/index.cjs +1139 -884
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1097 -876
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +45 -11
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +3 -3
- package/dist/test/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-LKRNWD7C.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
CommittedMetaSchema: () => CommittedMetaSchema,
|
|
37
37
|
ConcurrencyError: () => ConcurrencyError,
|
|
38
38
|
ConsoleLogger: () => ConsoleLogger,
|
|
39
|
+
DEFAULT_LANE: () => DEFAULT_LANE,
|
|
39
40
|
DEFAULT_MAX_SUBSCRIBED_STREAMS: () => DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
40
41
|
DEFAULT_SETTLE_DEBOUNCE_MS: () => DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
41
42
|
Environments: () => Environments,
|
|
@@ -145,6 +146,12 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
145
146
|
if (typeof objOrMsg === "string") {
|
|
146
147
|
message = objOrMsg;
|
|
147
148
|
obj = {};
|
|
149
|
+
} else if (objOrMsg instanceof Error) {
|
|
150
|
+
message = msg ?? objOrMsg.message;
|
|
151
|
+
obj = {
|
|
152
|
+
error: { message: objOrMsg.message, name: objOrMsg.name },
|
|
153
|
+
stack: objOrMsg.stack
|
|
154
|
+
};
|
|
148
155
|
} else if (objOrMsg !== null && typeof objOrMsg === "object") {
|
|
149
156
|
message = msg;
|
|
150
157
|
obj = { ...objOrMsg };
|
|
@@ -175,6 +182,9 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
175
182
|
let data;
|
|
176
183
|
if (typeof objOrMsg === "string") {
|
|
177
184
|
message = objOrMsg;
|
|
185
|
+
} else if (objOrMsg instanceof Error) {
|
|
186
|
+
message = msg ?? objOrMsg.message;
|
|
187
|
+
data = objOrMsg.stack;
|
|
178
188
|
} else {
|
|
179
189
|
message = msg ?? "";
|
|
180
190
|
if (objOrMsg !== void 0 && objOrMsg !== null) {
|
|
@@ -484,10 +494,11 @@ async function sleep(ms) {
|
|
|
484
494
|
|
|
485
495
|
// src/adapters/in-memory-store.ts
|
|
486
496
|
var InMemoryStream = class {
|
|
487
|
-
constructor(stream, source, priority = 0) {
|
|
497
|
+
constructor(stream, source, priority = 0, lane = DEFAULT_LANE) {
|
|
488
498
|
this.stream = stream;
|
|
489
499
|
this.source = source;
|
|
490
500
|
this._priority = priority;
|
|
501
|
+
this._lane = lane;
|
|
491
502
|
}
|
|
492
503
|
_at = -1;
|
|
493
504
|
_retry = -1;
|
|
@@ -496,9 +507,17 @@ var InMemoryStream = class {
|
|
|
496
507
|
_leased_by = void 0;
|
|
497
508
|
_leased_until = void 0;
|
|
498
509
|
_priority = 0;
|
|
510
|
+
_lane = DEFAULT_LANE;
|
|
499
511
|
get priority() {
|
|
500
512
|
return this._priority;
|
|
501
513
|
}
|
|
514
|
+
get lane() {
|
|
515
|
+
return this._lane;
|
|
516
|
+
}
|
|
517
|
+
/** Replace on every subscribe — current builder config wins on restart. */
|
|
518
|
+
set lane(value) {
|
|
519
|
+
this._lane = value;
|
|
520
|
+
}
|
|
502
521
|
/**
|
|
503
522
|
* Bump the priority via {@link subscribe}: keeps the maximum across
|
|
504
523
|
* reactions so the highest-priority registrant wins.
|
|
@@ -552,7 +571,8 @@ var InMemoryStream = class {
|
|
|
552
571
|
at: lease.at,
|
|
553
572
|
by: lease.by,
|
|
554
573
|
retry: this._retry,
|
|
555
|
-
lagging: lease.lagging
|
|
574
|
+
lagging: lease.lagging,
|
|
575
|
+
lane: this._lane
|
|
556
576
|
};
|
|
557
577
|
}
|
|
558
578
|
/**
|
|
@@ -571,7 +591,8 @@ var InMemoryStream = class {
|
|
|
571
591
|
at: this._at,
|
|
572
592
|
by: lease.by,
|
|
573
593
|
retry: this._retry,
|
|
574
|
-
lagging: lease.lagging
|
|
594
|
+
lagging: lease.lagging,
|
|
595
|
+
lane: this._lane
|
|
575
596
|
};
|
|
576
597
|
}
|
|
577
598
|
}
|
|
@@ -591,7 +612,8 @@ var InMemoryStream = class {
|
|
|
591
612
|
by: this._leased_by,
|
|
592
613
|
retry: this._retry,
|
|
593
614
|
error: this._error,
|
|
594
|
-
lagging: lease.lagging
|
|
615
|
+
lagging: lease.lagging,
|
|
616
|
+
lane: this._lane
|
|
595
617
|
};
|
|
596
618
|
}
|
|
597
619
|
}
|
|
@@ -767,7 +789,7 @@ var InMemoryStore = class {
|
|
|
767
789
|
* @param millis - Lease duration in milliseconds.
|
|
768
790
|
* @returns Granted leases.
|
|
769
791
|
*/
|
|
770
|
-
async claim(lagging, leading, by, millis) {
|
|
792
|
+
async claim(lagging, leading, by, millis, lane) {
|
|
771
793
|
await sleep();
|
|
772
794
|
const sourceRegex = /* @__PURE__ */ new Map();
|
|
773
795
|
const getRegex = (source) => {
|
|
@@ -788,7 +810,7 @@ var InMemoryStore = class {
|
|
|
788
810
|
return false;
|
|
789
811
|
};
|
|
790
812
|
const available = [...this._streams.values()].filter(
|
|
791
|
-
(s) => s.is_available && hasWork(s)
|
|
813
|
+
(s) => s.is_available && hasWork(s) && (lane === void 0 || s.lane === lane)
|
|
792
814
|
);
|
|
793
815
|
const lag = available.sort((a, b) => b.priority - a.priority || a.at - b.at).slice(0, lagging).map((s) => ({
|
|
794
816
|
stream: s.stream,
|
|
@@ -824,12 +846,21 @@ var InMemoryStore = class {
|
|
|
824
846
|
async subscribe(streams) {
|
|
825
847
|
await sleep();
|
|
826
848
|
let subscribed = 0;
|
|
827
|
-
for (const {
|
|
849
|
+
for (const {
|
|
850
|
+
stream,
|
|
851
|
+
source,
|
|
852
|
+
priority = 0,
|
|
853
|
+
lane = DEFAULT_LANE
|
|
854
|
+
} of streams) {
|
|
828
855
|
const existing = this._streams.get(stream);
|
|
829
856
|
if (existing) {
|
|
830
857
|
existing.bumpPriority(priority);
|
|
858
|
+
existing.lane = lane;
|
|
831
859
|
} else {
|
|
832
|
-
this._streams.set(
|
|
860
|
+
this._streams.set(
|
|
861
|
+
stream,
|
|
862
|
+
new InMemoryStream(stream, source, priority, lane)
|
|
863
|
+
);
|
|
833
864
|
subscribed++;
|
|
834
865
|
}
|
|
835
866
|
}
|
|
@@ -876,6 +907,7 @@ var InMemoryStore = class {
|
|
|
876
907
|
}
|
|
877
908
|
if (filter.blocked !== void 0 && s.blocked !== filter.blocked)
|
|
878
909
|
return false;
|
|
910
|
+
if (filter.lane !== void 0 && s.lane !== filter.lane) return false;
|
|
879
911
|
return true;
|
|
880
912
|
};
|
|
881
913
|
}
|
|
@@ -986,6 +1018,7 @@ var InMemoryStore = class {
|
|
|
986
1018
|
continue;
|
|
987
1019
|
}
|
|
988
1020
|
if (blocked !== void 0 && s.blocked !== blocked) continue;
|
|
1021
|
+
if (query?.lane !== void 0 && s.lane !== query.lane) continue;
|
|
989
1022
|
callback({
|
|
990
1023
|
stream: s.stream,
|
|
991
1024
|
source: s.source,
|
|
@@ -995,7 +1028,8 @@ var InMemoryStore = class {
|
|
|
995
1028
|
error: s.error,
|
|
996
1029
|
priority: s.priority,
|
|
997
1030
|
leased_by: s.leased_by,
|
|
998
|
-
leased_until: s.leased_until
|
|
1031
|
+
leased_until: s.leased_until,
|
|
1032
|
+
lane: s.lane
|
|
999
1033
|
});
|
|
1000
1034
|
count++;
|
|
1001
1035
|
if (count >= limit) break;
|
|
@@ -1178,6 +1212,7 @@ function dispose(disposer) {
|
|
|
1178
1212
|
}
|
|
1179
1213
|
var SNAP_EVENT = "__snapshot__";
|
|
1180
1214
|
var TOMBSTONE_EVENT = "__tombstone__";
|
|
1215
|
+
var DEFAULT_LANE = "default";
|
|
1181
1216
|
|
|
1182
1217
|
// src/signals.ts
|
|
1183
1218
|
process.once("SIGINT", async (arg) => {
|
|
@@ -1201,23 +1236,39 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
1201
1236
|
var import_node_events = __toESM(require("events"), 1);
|
|
1202
1237
|
|
|
1203
1238
|
// src/internal/build-classify.ts
|
|
1239
|
+
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
1204
1240
|
function classifyRegistry(registry, states) {
|
|
1205
1241
|
const statics = /* @__PURE__ */ new Map();
|
|
1206
1242
|
const reactiveEvents = /* @__PURE__ */ new Set();
|
|
1243
|
+
const eventToLanes = /* @__PURE__ */ new Map();
|
|
1207
1244
|
let hasDynamicResolvers = false;
|
|
1208
1245
|
for (const [name, register] of Object.entries(registry.events)) {
|
|
1209
1246
|
if (register.reactions.size > 0) reactiveEvents.add(name);
|
|
1210
1247
|
for (const reaction of register.reactions.values()) {
|
|
1211
1248
|
if (typeof reaction.resolver === "function") {
|
|
1212
1249
|
hasDynamicResolvers = true;
|
|
1250
|
+
eventToLanes.set(name, ALL_LANES);
|
|
1213
1251
|
} else {
|
|
1214
|
-
const { target, source, priority = 0 } = reaction.resolver;
|
|
1252
|
+
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
1253
|
+
const lane_name = lane ?? "default";
|
|
1254
|
+
const existing_lanes = eventToLanes.get(name);
|
|
1255
|
+
if (existing_lanes !== ALL_LANES) {
|
|
1256
|
+
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
1257
|
+
set.add(lane_name);
|
|
1258
|
+
eventToLanes.set(name, set);
|
|
1259
|
+
}
|
|
1215
1260
|
const key = `${target}|${source ?? ""}`;
|
|
1216
1261
|
const existing = statics.get(key);
|
|
1217
1262
|
if (!existing) {
|
|
1218
|
-
statics.set(key, { stream: target, source, priority });
|
|
1219
|
-
} else
|
|
1220
|
-
|
|
1263
|
+
statics.set(key, { stream: target, source, priority, lane });
|
|
1264
|
+
} else {
|
|
1265
|
+
if ((existing.lane ?? void 0) !== (lane ?? void 0))
|
|
1266
|
+
throw new Error(
|
|
1267
|
+
`Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
|
|
1268
|
+
);
|
|
1269
|
+
if (priority > existing.priority) {
|
|
1270
|
+
statics.set(key, { ...existing, priority });
|
|
1271
|
+
}
|
|
1221
1272
|
}
|
|
1222
1273
|
}
|
|
1223
1274
|
}
|
|
@@ -1232,7 +1283,8 @@ function classifyRegistry(registry, states) {
|
|
|
1232
1283
|
staticTargets: [...statics.values()],
|
|
1233
1284
|
hasDynamicResolvers,
|
|
1234
1285
|
reactiveEvents,
|
|
1235
|
-
eventToState
|
|
1286
|
+
eventToState,
|
|
1287
|
+
eventToLanes
|
|
1236
1288
|
};
|
|
1237
1289
|
}
|
|
1238
1290
|
|
|
@@ -1443,6 +1495,7 @@ var CorrelateCycle = class {
|
|
|
1443
1495
|
const entry = correlated.get(resolved.target) || {
|
|
1444
1496
|
source: resolved.source,
|
|
1445
1497
|
priority: incomingPriority,
|
|
1498
|
+
lane: resolved.lane,
|
|
1446
1499
|
payloads: []
|
|
1447
1500
|
};
|
|
1448
1501
|
if (incomingPriority > entry.priority)
|
|
@@ -1461,10 +1514,11 @@ var CorrelateCycle = class {
|
|
|
1461
1514
|
);
|
|
1462
1515
|
if (correlated.size) {
|
|
1463
1516
|
const streams = [...correlated.entries()].map(
|
|
1464
|
-
([stream, { source, priority }]) => ({
|
|
1517
|
+
([stream, { source, priority, lane }]) => ({
|
|
1465
1518
|
stream,
|
|
1466
1519
|
source,
|
|
1467
|
-
priority
|
|
1520
|
+
priority,
|
|
1521
|
+
lane
|
|
1468
1522
|
})
|
|
1469
1523
|
);
|
|
1470
1524
|
const { subscribed } = await this.cd.subscribe(streams);
|
|
@@ -1549,904 +1603,955 @@ function computeLagLeadRatio(handled, lagging, leading) {
|
|
|
1549
1603
|
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
1550
1604
|
}
|
|
1551
1605
|
|
|
1552
|
-
// src/internal/drain
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
const fetched = await ops.fetch(active, eventLimit);
|
|
1567
|
-
const fetchMap = /* @__PURE__ */ new Map();
|
|
1568
|
-
const fetch_window_at = fetched.reduce(
|
|
1569
|
-
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
1570
|
-
0
|
|
1606
|
+
// src/internal/drain.ts
|
|
1607
|
+
var claim = (lagging, leading, by, millis, lane) => store2().claim(lagging, leading, by, millis, lane);
|
|
1608
|
+
async function fetch(leased, eventLimit) {
|
|
1609
|
+
return Promise.all(
|
|
1610
|
+
leased.map(async ({ stream, source, at, lagging }) => {
|
|
1611
|
+
const events = [];
|
|
1612
|
+
await store2().query((e) => events.push(e), {
|
|
1613
|
+
stream: source,
|
|
1614
|
+
after: at,
|
|
1615
|
+
limit: eventLimit
|
|
1616
|
+
});
|
|
1617
|
+
return { stream, source, at, lagging, events };
|
|
1618
|
+
})
|
|
1571
1619
|
);
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1620
|
+
}
|
|
1621
|
+
var ack = (leases) => store2().ack(leases);
|
|
1622
|
+
var block = (leases) => store2().block(leases);
|
|
1623
|
+
var subscribe = (streams) => store2().subscribe(streams);
|
|
1624
|
+
|
|
1625
|
+
// src/internal/event-sourcing.ts
|
|
1626
|
+
var import_act_patch = require("@rotorsoft/act-patch");
|
|
1627
|
+
async function snap(snapshot) {
|
|
1628
|
+
try {
|
|
1629
|
+
const { id, stream, name, meta, version } = snapshot.event;
|
|
1630
|
+
await store2().commit(
|
|
1631
|
+
stream,
|
|
1632
|
+
[{ name: SNAP_EVENT, data: snapshot.state }],
|
|
1633
|
+
{
|
|
1634
|
+
correlation: meta.correlation,
|
|
1635
|
+
causation: { event: { id, name, stream } }
|
|
1636
|
+
},
|
|
1637
|
+
version
|
|
1638
|
+
// IMPORTANT! - state events are committed right after the snapshot event
|
|
1639
|
+
);
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
log().error(error);
|
|
1583
1642
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1643
|
+
}
|
|
1644
|
+
async function tombstone(stream, expectedVersion, correlation) {
|
|
1645
|
+
try {
|
|
1646
|
+
const [committed] = await store2().commit(
|
|
1647
|
+
stream,
|
|
1648
|
+
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
1649
|
+
{ correlation, causation: {} },
|
|
1650
|
+
expectedVersion
|
|
1651
|
+
);
|
|
1652
|
+
return committed;
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
if (error instanceof ConcurrencyError) return void 0;
|
|
1655
|
+
throw error;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
async function load(me, stream, callback, asOf) {
|
|
1659
|
+
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
1660
|
+
const cached = timeTravel ? void 0 : await cache2().get(stream);
|
|
1661
|
+
const cache_hit = !!cached;
|
|
1662
|
+
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
1663
|
+
let patches = cached?.patches ?? 0;
|
|
1664
|
+
let snaps = cached?.snaps ?? 0;
|
|
1665
|
+
let version = cached?.version ?? -1;
|
|
1666
|
+
let replayed = 0;
|
|
1667
|
+
let event;
|
|
1668
|
+
await store2().query(
|
|
1669
|
+
(e) => {
|
|
1670
|
+
event = e;
|
|
1671
|
+
version = e.version;
|
|
1672
|
+
if (e.name === SNAP_EVENT) {
|
|
1673
|
+
state2 = e.data;
|
|
1674
|
+
snaps++;
|
|
1675
|
+
patches = 0;
|
|
1676
|
+
replayed++;
|
|
1677
|
+
} else if (me.patch[e.name]) {
|
|
1678
|
+
state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](event, state2));
|
|
1679
|
+
patches++;
|
|
1680
|
+
replayed++;
|
|
1681
|
+
} else if (e.name !== TOMBSTONE_EVENT) {
|
|
1682
|
+
log().warn(
|
|
1683
|
+
`Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
|
|
1684
|
+
);
|
|
1592
1685
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1686
|
+
callback?.({
|
|
1687
|
+
event,
|
|
1688
|
+
state: state2,
|
|
1689
|
+
version,
|
|
1690
|
+
patches,
|
|
1691
|
+
snaps,
|
|
1692
|
+
cache_hit,
|
|
1693
|
+
replayed
|
|
1694
|
+
});
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
stream,
|
|
1698
|
+
stream_exact: true,
|
|
1699
|
+
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
1700
|
+
}
|
|
1601
1701
|
);
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
};
|
|
1610
|
-
var DrainController = class {
|
|
1611
|
-
constructor(deps) {
|
|
1612
|
-
this.deps = deps;
|
|
1702
|
+
if (replayed > 0 && !timeTravel && event) {
|
|
1703
|
+
await cache2().set(stream, {
|
|
1704
|
+
state: state2,
|
|
1705
|
+
version,
|
|
1706
|
+
event_id: event.id,
|
|
1707
|
+
patches,
|
|
1708
|
+
snaps
|
|
1709
|
+
});
|
|
1613
1710
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1711
|
+
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
1712
|
+
}
|
|
1713
|
+
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = defaultCorrelator) {
|
|
1714
|
+
const { stream, expectedVersion, actor } = target;
|
|
1715
|
+
if (!stream) throw new Error("Missing target stream");
|
|
1716
|
+
const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
1717
|
+
const snapshot = await load(me, stream);
|
|
1718
|
+
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
1719
|
+
throw new StreamClosedError(stream);
|
|
1720
|
+
const expected = expectedVersion ?? snapshot.event?.version;
|
|
1721
|
+
if (me.given) {
|
|
1722
|
+
const invariants = me.given[action2] || [];
|
|
1723
|
+
invariants.forEach(({ valid, description }) => {
|
|
1724
|
+
if (!valid(snapshot.state, actor))
|
|
1725
|
+
throw new InvariantError(
|
|
1726
|
+
action2,
|
|
1727
|
+
validated,
|
|
1728
|
+
target,
|
|
1729
|
+
snapshot,
|
|
1730
|
+
description
|
|
1731
|
+
);
|
|
1732
|
+
});
|
|
1633
1733
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1734
|
+
const result = me.on[action2](validated, snapshot, target);
|
|
1735
|
+
if (!result) return [snapshot];
|
|
1736
|
+
if (Array.isArray(result) && result.length === 0) {
|
|
1737
|
+
return [snapshot];
|
|
1637
1738
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
if (this._backoffTimer) clearTimeout(this._backoffTimer);
|
|
1651
|
-
let earliest = Number.POSITIVE_INFINITY;
|
|
1652
|
-
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
1653
|
-
const delay = Math.max(0, earliest - Date.now());
|
|
1654
|
-
this._backoffTimer = setTimeout(() => {
|
|
1655
|
-
this._backoffTimer = void 0;
|
|
1656
|
-
const now = Date.now();
|
|
1657
|
-
for (const [stream, at] of this._backoff) {
|
|
1658
|
-
if (at <= now) this._backoff.delete(stream);
|
|
1739
|
+
const tuples = Array.isArray(result[0]) ? result : [result];
|
|
1740
|
+
const deprecated = me._deprecated;
|
|
1741
|
+
if (deprecated && deprecated.size > 0) {
|
|
1742
|
+
const me_ = me;
|
|
1743
|
+
const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
|
|
1744
|
+
for (const [name] of tuples) {
|
|
1745
|
+
const evt = name;
|
|
1746
|
+
if (deprecated.has(evt) && !warned.has(evt)) {
|
|
1747
|
+
warned.add(evt);
|
|
1748
|
+
log().warn(
|
|
1749
|
+
`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)`
|
|
1750
|
+
);
|
|
1659
1751
|
}
|
|
1660
|
-
|
|
1661
|
-
}, delay);
|
|
1662
|
-
this._backoffTimer.unref();
|
|
1752
|
+
}
|
|
1663
1753
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
);
|
|
1688
|
-
if (!cycle) {
|
|
1689
|
-
this._armed = false;
|
|
1690
|
-
return EMPTY_DRAIN;
|
|
1691
|
-
}
|
|
1692
|
-
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1693
|
-
this._ratio = computeLagLeadRatio(handled, lagging, leading);
|
|
1694
|
-
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
1695
|
-
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
1696
|
-
for (const h of handled) {
|
|
1697
|
-
if (h.nextAttemptAt !== void 0 && !h.block) {
|
|
1698
|
-
this._backoff.set(h.lease.stream, h.nextAttemptAt);
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
if (this._backoff.size > 0) this.scheduleBackoffWake();
|
|
1702
|
-
if (acked.length) this.deps.onAcked(acked);
|
|
1703
|
-
if (blocked.length) this.deps.onBlocked(blocked);
|
|
1704
|
-
const hasErrors = handled.some(({ error }) => error);
|
|
1705
|
-
if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
|
|
1706
|
-
return { fetched, leased, acked, blocked };
|
|
1707
|
-
} catch (error) {
|
|
1708
|
-
this.deps.logger.error(error);
|
|
1709
|
-
return EMPTY_DRAIN;
|
|
1710
|
-
} finally {
|
|
1711
|
-
this._locked = false;
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
};
|
|
1715
|
-
|
|
1716
|
-
// src/internal/event-versions.ts
|
|
1717
|
-
var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
|
|
1718
|
-
function parse(name) {
|
|
1719
|
-
const m = name.match(VERSION_SUFFIX);
|
|
1720
|
-
if (m) {
|
|
1721
|
-
const v = Number.parseInt(m[2], 10);
|
|
1722
|
-
if (v >= 2) return { base: m[1], version: v };
|
|
1723
|
-
}
|
|
1724
|
-
return { base: name, version: 1 };
|
|
1725
|
-
}
|
|
1726
|
-
function deprecatedEventNames(names) {
|
|
1727
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1728
|
-
for (const name of names) {
|
|
1729
|
-
const { base, version } = parse(name);
|
|
1730
|
-
const list = groups.get(base);
|
|
1731
|
-
if (list) list.push({ version, name });
|
|
1732
|
-
else groups.set(base, [{ version, name }]);
|
|
1733
|
-
}
|
|
1734
|
-
const deprecated = /* @__PURE__ */ new Set();
|
|
1735
|
-
for (const list of groups.values()) {
|
|
1736
|
-
if (list.length < 2) continue;
|
|
1737
|
-
list.sort((a, b) => b.version - a.version);
|
|
1738
|
-
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
1739
|
-
}
|
|
1740
|
-
return deprecated;
|
|
1741
|
-
}
|
|
1742
|
-
function currentVersionOf(deprecatedName, allNames) {
|
|
1743
|
-
const target = parse(deprecatedName);
|
|
1744
|
-
let highest;
|
|
1745
|
-
for (const name of allNames) {
|
|
1746
|
-
const { base, version } = parse(name);
|
|
1747
|
-
if (base !== target.base) continue;
|
|
1748
|
-
if (!highest || version > highest.version) highest = { version, name };
|
|
1749
|
-
}
|
|
1750
|
-
return highest && highest.version > target.version ? highest.name : void 0;
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
// src/internal/merge.ts
|
|
1754
|
-
var import_zod4 = require("zod");
|
|
1755
|
-
function baseTypeName(zodType) {
|
|
1756
|
-
let t = zodType;
|
|
1757
|
-
while (typeof t.unwrap === "function") {
|
|
1758
|
-
t = t.unwrap();
|
|
1759
|
-
}
|
|
1760
|
-
return t.constructor.name;
|
|
1761
|
-
}
|
|
1762
|
-
function mergeSchemas(existing, incoming, stateName) {
|
|
1763
|
-
if (existing instanceof import_zod4.ZodObject && incoming instanceof import_zod4.ZodObject) {
|
|
1764
|
-
const existingShape = existing.shape;
|
|
1765
|
-
const incomingShape = incoming.shape;
|
|
1766
|
-
for (const key of Object.keys(incomingShape)) {
|
|
1767
|
-
if (key in existingShape) {
|
|
1768
|
-
const existingBase = baseTypeName(existingShape[key]);
|
|
1769
|
-
const incomingBase = baseTypeName(incomingShape[key]);
|
|
1770
|
-
if (existingBase !== incomingBase) {
|
|
1771
|
-
throw new Error(
|
|
1772
|
-
`Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
|
|
1773
|
-
);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
return existing.extend(incomingShape);
|
|
1778
|
-
}
|
|
1779
|
-
return existing;
|
|
1780
|
-
}
|
|
1781
|
-
function mergeInits(existing, incoming) {
|
|
1782
|
-
return () => ({ ...existing(), ...incoming() });
|
|
1783
|
-
}
|
|
1784
|
-
function registerState(state2, states, actions, events) {
|
|
1785
|
-
const existing = states.get(state2.name);
|
|
1786
|
-
if (existing) {
|
|
1787
|
-
mergeIntoExisting(state2, existing, states, actions, events);
|
|
1788
|
-
} else {
|
|
1789
|
-
registerNewState(state2, states, actions, events);
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
function registerNewState(state2, states, actions, events) {
|
|
1793
|
-
states.set(state2.name, state2);
|
|
1794
|
-
for (const name of Object.keys(state2.actions)) {
|
|
1795
|
-
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
1796
|
-
actions[name] = state2;
|
|
1797
|
-
}
|
|
1798
|
-
for (const name of Object.keys(state2.events)) {
|
|
1799
|
-
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
1800
|
-
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
1804
|
-
for (const name of Object.keys(state2.actions)) {
|
|
1805
|
-
if (existing.actions[name] === state2.actions[name]) continue;
|
|
1806
|
-
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
1807
|
-
}
|
|
1808
|
-
for (const name of Object.keys(state2.events)) {
|
|
1809
|
-
if (existing.events[name] === state2.events[name]) continue;
|
|
1810
|
-
if (existing.events[name]) {
|
|
1811
|
-
throw new Error(
|
|
1812
|
-
`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.`
|
|
1813
|
-
);
|
|
1754
|
+
const emitted = tuples.map(([name, data]) => ({
|
|
1755
|
+
name,
|
|
1756
|
+
data: skipValidation ? data : validate(name, data, me.events[name])
|
|
1757
|
+
}));
|
|
1758
|
+
const meta = {
|
|
1759
|
+
correlation: reactingTo?.meta.correlation || correlator({
|
|
1760
|
+
action: action2,
|
|
1761
|
+
state: me.name,
|
|
1762
|
+
stream,
|
|
1763
|
+
actor: target.actor
|
|
1764
|
+
}),
|
|
1765
|
+
causation: {
|
|
1766
|
+
action: {
|
|
1767
|
+
name: action2,
|
|
1768
|
+
...target
|
|
1769
|
+
// payload intentionally omitted: it can be large or contain PII,
|
|
1770
|
+
// and callers correlate via the correlation id when they need it.
|
|
1771
|
+
},
|
|
1772
|
+
event: reactingTo ? {
|
|
1773
|
+
id: reactingTo.id,
|
|
1774
|
+
name: reactingTo.name,
|
|
1775
|
+
stream: reactingTo.stream
|
|
1776
|
+
} : void 0
|
|
1814
1777
|
}
|
|
1815
|
-
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
1816
|
-
}
|
|
1817
|
-
const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
|
|
1818
|
-
const merged = {
|
|
1819
|
-
...existing,
|
|
1820
|
-
state: mergeSchemas(existing.state, state2.state, state2.name),
|
|
1821
|
-
init: mergeInits(existing.init, state2.init),
|
|
1822
|
-
events: { ...existing.events, ...state2.events },
|
|
1823
|
-
actions: { ...existing.actions, ...state2.actions },
|
|
1824
|
-
patch: mergedPatch,
|
|
1825
|
-
on: { ...existing.on, ...state2.on },
|
|
1826
|
-
given: { ...existing.given, ...state2.given },
|
|
1827
|
-
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
1828
|
-
throw new Error(
|
|
1829
|
-
`Duplicate snap strategy for state "${state2.name}"`
|
|
1830
|
-
);
|
|
1831
|
-
})() : state2.snap || existing.snap
|
|
1832
1778
|
};
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
merged[name] = incomingP;
|
|
1849
|
-
continue;
|
|
1850
|
-
}
|
|
1851
|
-
const existingIsDefault = existingP._passthrough;
|
|
1852
|
-
const incomingIsDefault = incomingP._passthrough;
|
|
1853
|
-
if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
|
|
1854
|
-
throw new Error(
|
|
1855
|
-
`Duplicate custom patch for event "${name}" in state "${stateName}"`
|
|
1856
|
-
);
|
|
1857
|
-
}
|
|
1858
|
-
if (existingIsDefault && !incomingIsDefault) {
|
|
1859
|
-
merged[name] = incomingP;
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
return merged;
|
|
1863
|
-
}
|
|
1864
|
-
function mergeEventRegister(target, source) {
|
|
1865
|
-
for (const [eventName, sourceReg] of Object.entries(source)) {
|
|
1866
|
-
const targetReg = target[eventName];
|
|
1867
|
-
if (!targetReg) continue;
|
|
1868
|
-
for (const [name, reaction] of sourceReg.reactions) {
|
|
1869
|
-
targetReg.reactions.set(name, reaction);
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
function mergeProjection(proj, events) {
|
|
1874
|
-
for (const eventName of Object.keys(proj.events)) {
|
|
1875
|
-
const projRegister = proj.events[eventName];
|
|
1876
|
-
const existing = events[eventName];
|
|
1877
|
-
if (!existing) {
|
|
1878
|
-
events[eventName] = {
|
|
1879
|
-
schema: projRegister.schema,
|
|
1880
|
-
reactions: new Map(projRegister.reactions)
|
|
1881
|
-
};
|
|
1882
|
-
} else {
|
|
1883
|
-
for (const [name, reaction] of projRegister.reactions) {
|
|
1884
|
-
let key = name;
|
|
1885
|
-
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
1886
|
-
existing.reactions.set(key, reaction);
|
|
1887
|
-
}
|
|
1779
|
+
let committed;
|
|
1780
|
+
try {
|
|
1781
|
+
committed = await store2().commit(
|
|
1782
|
+
stream,
|
|
1783
|
+
emitted,
|
|
1784
|
+
meta,
|
|
1785
|
+
// Reactions skip optimistic concurrency: they always append against the
|
|
1786
|
+
// current head. Stream leasing already serializes concurrent reactions,
|
|
1787
|
+
// and forcing version checks here would turn ordinary catch-up into
|
|
1788
|
+
// spurious retries.
|
|
1789
|
+
reactingTo ? void 0 : expected
|
|
1790
|
+
);
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
if (error instanceof ConcurrencyError) {
|
|
1793
|
+
await cache2().invalidate(stream);
|
|
1888
1794
|
}
|
|
1795
|
+
throw error;
|
|
1889
1796
|
}
|
|
1797
|
+
let { state: state2, patches } = snapshot;
|
|
1798
|
+
const snapshots = committed.map((event) => {
|
|
1799
|
+
const p = me.patch[event.name](event, state2);
|
|
1800
|
+
state2 = (0, import_act_patch.patch)(state2, p);
|
|
1801
|
+
patches++;
|
|
1802
|
+
return {
|
|
1803
|
+
event,
|
|
1804
|
+
state: state2,
|
|
1805
|
+
version: event.version,
|
|
1806
|
+
patches,
|
|
1807
|
+
snaps: snapshot.snaps,
|
|
1808
|
+
patch: p,
|
|
1809
|
+
cache_hit: snapshot.cache_hit,
|
|
1810
|
+
replayed: snapshot.replayed
|
|
1811
|
+
};
|
|
1812
|
+
});
|
|
1813
|
+
const last = snapshots.at(-1);
|
|
1814
|
+
const snapped = me.snap?.(last);
|
|
1815
|
+
cache2().set(stream, {
|
|
1816
|
+
state: last.state,
|
|
1817
|
+
version: last.event.version,
|
|
1818
|
+
event_id: last.event.id,
|
|
1819
|
+
patches: snapped ? 0 : last.patches,
|
|
1820
|
+
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
1821
|
+
}).catch((err) => log().error(err));
|
|
1822
|
+
if (snapped) void snap(last);
|
|
1823
|
+
return snapshots;
|
|
1890
1824
|
}
|
|
1891
|
-
var _this_ = ({ stream }) => ({
|
|
1892
|
-
source: stream,
|
|
1893
|
-
target: stream
|
|
1894
|
-
});
|
|
1895
1825
|
|
|
1896
|
-
// src/internal/
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1826
|
+
// src/internal/tracing.ts
|
|
1827
|
+
var PRETTY = config().env !== "production";
|
|
1828
|
+
var C_BLUE = "\x1B[38;5;39m";
|
|
1829
|
+
var C_ORANGE = "\x1B[38;5;208m";
|
|
1830
|
+
var C_GREEN = "\x1B[38;5;42m";
|
|
1831
|
+
var C_MAGENTA = "\x1B[38;5;165m";
|
|
1832
|
+
var C_DRAIN = "\x1B[38;5;244m";
|
|
1833
|
+
var C_HIT = "\x1B[38;5;82m";
|
|
1834
|
+
var C_MISS = "\x1B[38;5;220m";
|
|
1835
|
+
var C_RESET = "\x1B[0m";
|
|
1836
|
+
var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
|
|
1837
|
+
var C_LANE = "\x1B[38;5;183m";
|
|
1838
|
+
var C_DIM = "\x1B[38;5;240m";
|
|
1839
|
+
var C_ERR = "\x1B[38;5;196m";
|
|
1840
|
+
var C_STREAM = "\x1B[38;5;226m";
|
|
1841
|
+
var dim = (text) => PRETTY ? `${C_DIM}${text}${C_RESET}` : text;
|
|
1842
|
+
var hue = (color, text) => PRETTY ? `${color}${text}${C_RESET}` : text;
|
|
1843
|
+
var drain_caption = (caption, lane) => {
|
|
1844
|
+
const showLane = lane && lane !== "default";
|
|
1845
|
+
if (PRETTY) {
|
|
1846
|
+
const tag = `${C_DRAIN}>> ${caption}${C_RESET}`;
|
|
1847
|
+
return showLane ? `${tag} ${C_LANE}${lane}${C_RESET}` : tag;
|
|
1848
|
+
}
|
|
1849
|
+
return showLane ? `>> ${caption} ${lane}` : `>> ${caption}`;
|
|
1850
|
+
};
|
|
1851
|
+
var cache_marker = (hit) => {
|
|
1852
|
+
const word = hit ? "hit" : "miss";
|
|
1853
|
+
if (!PRETTY) return word;
|
|
1854
|
+
return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
|
|
1855
|
+
};
|
|
1856
|
+
var stats_marker = (version, replayed, snaps, patches) => {
|
|
1857
|
+
const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
|
|
1858
|
+
if (!PRETTY) return text;
|
|
1859
|
+
return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
|
|
1860
|
+
};
|
|
1861
|
+
var as_of_marker = (asOf) => {
|
|
1862
|
+
if (!asOf) return "";
|
|
1863
|
+
const parts = [];
|
|
1864
|
+
if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
|
|
1865
|
+
if (asOf.created_before !== void 0)
|
|
1866
|
+
parts.push(`created_before=${asOf.created_before.toISOString()}`);
|
|
1867
|
+
if (asOf.created_after !== void 0)
|
|
1868
|
+
parts.push(`created_after=${asOf.created_after.toISOString()}`);
|
|
1869
|
+
if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
|
|
1870
|
+
return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
|
|
1871
|
+
};
|
|
1872
|
+
var traced = (inner, exit, entry) => (async (...args) => {
|
|
1873
|
+
entry?.(...args);
|
|
1874
|
+
const result = await inner(...args);
|
|
1875
|
+
exit?.(result, ...args);
|
|
1876
|
+
return result;
|
|
1877
|
+
});
|
|
1878
|
+
function buildEs(logger, correlator = defaultCorrelator) {
|
|
1879
|
+
const boundAction = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
|
|
1880
|
+
me,
|
|
1881
|
+
actionName,
|
|
1882
|
+
target,
|
|
1883
|
+
payload,
|
|
1884
|
+
reactingTo,
|
|
1885
|
+
skipValidation,
|
|
1886
|
+
correlator
|
|
1887
|
+
);
|
|
1888
|
+
if (logger.level !== "trace") {
|
|
1889
|
+
return {
|
|
1890
|
+
snap,
|
|
1891
|
+
load,
|
|
1892
|
+
action: boundAction,
|
|
1893
|
+
tombstone
|
|
1894
|
+
};
|
|
1912
1895
|
}
|
|
1913
|
-
if (opts.jitter) delay = delay * (0.5 + Math.random());
|
|
1914
|
-
return Math.max(0, Math.floor(delay));
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
// src/internal/reactions.ts
|
|
1918
|
-
function finalize(lease, handled, at, error, options, logger) {
|
|
1919
|
-
if (!error) return { lease, handled, at };
|
|
1920
|
-
logger.error(error);
|
|
1921
|
-
const nonRetryable = error instanceof NonRetryableError;
|
|
1922
|
-
const block2 = options.blockOnError && (nonRetryable || lease.retry >= options.maxRetries);
|
|
1923
|
-
if (block2)
|
|
1924
|
-
logger.error(
|
|
1925
|
-
nonRetryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1926
|
-
);
|
|
1927
|
-
const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
|
|
1928
1896
|
return {
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
}
|
|
1937
|
-
function buildHandle(deps) {
|
|
1938
|
-
const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
|
|
1939
|
-
return async (lease, payloads) => {
|
|
1940
|
-
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1941
|
-
const stream = lease.stream;
|
|
1942
|
-
let at = payloads.at(0).event.id;
|
|
1943
|
-
let handled = 0;
|
|
1944
|
-
if (lease.retry > 0)
|
|
1945
|
-
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1946
|
-
const scopedApp = {
|
|
1947
|
-
do: boundDo,
|
|
1948
|
-
load: boundLoad,
|
|
1949
|
-
query: boundQuery,
|
|
1950
|
-
query_array: boundQueryArray
|
|
1951
|
-
};
|
|
1952
|
-
for (const payload of payloads) {
|
|
1953
|
-
const { event, handler } = payload;
|
|
1954
|
-
scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
|
|
1955
|
-
action2,
|
|
1956
|
-
target,
|
|
1957
|
-
actionPayload,
|
|
1958
|
-
reactingTo ?? event,
|
|
1959
|
-
skipValidation
|
|
1897
|
+
snap: traced(snap, void 0, (snapshot) => {
|
|
1898
|
+
logger.trace(
|
|
1899
|
+
es_caption(
|
|
1900
|
+
"snap",
|
|
1901
|
+
C_MAGENTA,
|
|
1902
|
+
`${snapshot.event.stream}@${snapshot.event.version}`
|
|
1903
|
+
)
|
|
1960
1904
|
);
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1905
|
+
}),
|
|
1906
|
+
load: traced(load, (result, _me, stream, _cb, asOf) => {
|
|
1907
|
+
const stats = stats_marker(
|
|
1908
|
+
result.version,
|
|
1909
|
+
result.replayed,
|
|
1910
|
+
result.snaps,
|
|
1911
|
+
result.patches
|
|
1912
|
+
);
|
|
1913
|
+
logger.trace(
|
|
1914
|
+
es_caption(
|
|
1915
|
+
"load",
|
|
1916
|
+
C_GREEN,
|
|
1917
|
+
`${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
1918
|
+
)
|
|
1919
|
+
);
|
|
1920
|
+
}),
|
|
1921
|
+
action: traced(
|
|
1922
|
+
boundAction,
|
|
1923
|
+
(snapshots, _me, _action, target) => {
|
|
1924
|
+
const committed = snapshots.filter((s) => s.event);
|
|
1925
|
+
if (committed.length) {
|
|
1926
|
+
logger.trace(
|
|
1927
|
+
committed.map((s) => s.event.data),
|
|
1928
|
+
es_caption(
|
|
1929
|
+
"committed",
|
|
1930
|
+
C_ORANGE,
|
|
1931
|
+
`${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
|
|
1932
|
+
)
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
},
|
|
1936
|
+
(_me, action2, target, payload) => {
|
|
1937
|
+
logger.trace(
|
|
1938
|
+
payload,
|
|
1939
|
+
es_caption("action", C_BLUE, `${target.stream}.${action2}`)
|
|
1973
1940
|
);
|
|
1974
1941
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1942
|
+
),
|
|
1943
|
+
tombstone: traced(tombstone, (committed, stream) => {
|
|
1944
|
+
if (committed)
|
|
1945
|
+
logger.trace(
|
|
1946
|
+
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
1947
|
+
);
|
|
1948
|
+
})
|
|
1977
1949
|
};
|
|
1978
1950
|
}
|
|
1979
|
-
function
|
|
1980
|
-
return
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
logger
|
|
1995
|
-
);
|
|
1996
|
-
} catch (error) {
|
|
1997
|
-
return finalize(lease, 0, lease.at, error, options, logger);
|
|
1998
|
-
}
|
|
1951
|
+
function buildDrain(logger) {
|
|
1952
|
+
return {
|
|
1953
|
+
claim,
|
|
1954
|
+
fetch,
|
|
1955
|
+
ack,
|
|
1956
|
+
block,
|
|
1957
|
+
subscribe: logger.level !== "trace" ? subscribe : traced(subscribe, (result, streams) => {
|
|
1958
|
+
if (!result.subscribed) return;
|
|
1959
|
+
const lanes = new Set(streams.map((s) => s.lane ?? "default"));
|
|
1960
|
+
const uniformLane = lanes.size === 1 ? streams[0]?.lane : void 0;
|
|
1961
|
+
const data = streams.map(
|
|
1962
|
+
({ stream, lane }) => uniformLane || !lane || lane === "default" ? hue(C_STREAM, stream) : `${hue(C_STREAM, stream)}${dim(`[${lane}]`)}`
|
|
1963
|
+
).join(" ");
|
|
1964
|
+
logger.trace(`${drain_caption("correlated", uniformLane)} ${data}`);
|
|
1965
|
+
})
|
|
1999
1966
|
};
|
|
2000
1967
|
}
|
|
1968
|
+
function traceCycle(logger, leased, fetched, handled, acked, blocked) {
|
|
1969
|
+
if (logger.level !== "trace" || !leased.length) return;
|
|
1970
|
+
const lane = leased[0]?.lane;
|
|
1971
|
+
const fetchByStream = new Map(fetched.map((f) => [f.stream, f]));
|
|
1972
|
+
const ackedByStream = new Map(acked.map((a) => [a.stream, a.at]));
|
|
1973
|
+
const blockedByStream = new Map(blocked.map((b) => [b.stream, b.error]));
|
|
1974
|
+
const failedByStream = new Map(
|
|
1975
|
+
handled.filter((h) => h.error).map((h) => [h.lease.stream, h])
|
|
1976
|
+
);
|
|
1977
|
+
const detail = leased.map(({ stream, at, retry }) => {
|
|
1978
|
+
const f = fetchByStream.get(stream);
|
|
1979
|
+
const key = f?.source ? `${hue(C_STREAM, stream)}${dim(`<-${f.source}`)}` : hue(C_STREAM, stream);
|
|
1980
|
+
const events = f && f.events.length ? ` ${dim(
|
|
1981
|
+
`[${f.events.map(({ id, name }) => `#${id} ${String(name)}`).join(", ")}]`
|
|
1982
|
+
)}` : "";
|
|
1983
|
+
const ackedAt = ackedByStream.get(stream);
|
|
1984
|
+
const ackPart = ackedAt !== void 0 ? hue(C_HIT, `\u2713 @${ackedAt}`) : "";
|
|
1985
|
+
const failure = failedByStream.get(stream);
|
|
1986
|
+
let failPart = "";
|
|
1987
|
+
if (failure) {
|
|
1988
|
+
const failedAt = failure.failed_at ?? at;
|
|
1989
|
+
const blockedError = blockedByStream.get(stream);
|
|
1990
|
+
if (blockedError !== void 0) {
|
|
1991
|
+
failPart = `${hue(C_ERR, `\u2717 @${failedAt}/${retry}`)} ${dim(`(${blockedError})`)}`;
|
|
1992
|
+
} else {
|
|
1993
|
+
failPart = `${hue(C_MISS, `\u26A0 @${failedAt}/${retry}`)} ${dim(`(${failure.error})`)}`;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
let tail;
|
|
1997
|
+
if (ackPart && failPart) tail = ` ${ackPart} ${failPart}`;
|
|
1998
|
+
else if (ackPart) tail = ` ${ackPart}`;
|
|
1999
|
+
else if (failPart) tail = ` ${failPart}`;
|
|
2000
|
+
else tail = ` ${dim(`\u2298 @${at}/${retry}`)}`;
|
|
2001
|
+
return `${key}${events}${tail}`;
|
|
2002
|
+
}).join(", ");
|
|
2003
|
+
logger.trace(`${drain_caption("drained", lane)} ${detail}`);
|
|
2004
|
+
}
|
|
2001
2005
|
|
|
2002
|
-
// src/internal/
|
|
2003
|
-
|
|
2004
|
-
|
|
2006
|
+
// src/internal/drain-cycle.ts
|
|
2007
|
+
async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis, isDeferred, lane) {
|
|
2008
|
+
const leased = await ops.claim(
|
|
2009
|
+
lagging,
|
|
2010
|
+
leading,
|
|
2011
|
+
(0, import_node_crypto2.randomUUID)(),
|
|
2012
|
+
leaseMillis,
|
|
2013
|
+
lane
|
|
2014
|
+
);
|
|
2015
|
+
if (!leased.length) return void 0;
|
|
2016
|
+
const active = isDeferred ? leased.filter((l) => !isDeferred(l.stream)) : leased;
|
|
2017
|
+
if (!active.length) {
|
|
2018
|
+
return {
|
|
2019
|
+
leased,
|
|
2020
|
+
fetched: [],
|
|
2021
|
+
handled: [],
|
|
2022
|
+
acked: [],
|
|
2023
|
+
blocked: []
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
const fetched = await ops.fetch(active, eventLimit);
|
|
2027
|
+
const fetchMap = /* @__PURE__ */ new Map();
|
|
2028
|
+
const fetch_window_at = fetched.reduce(
|
|
2029
|
+
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
2030
|
+
0
|
|
2031
|
+
);
|
|
2032
|
+
for (const f of fetched) {
|
|
2033
|
+
const { stream, events } = f;
|
|
2034
|
+
const payloads = events.flatMap((event) => {
|
|
2035
|
+
const register = registry.events[event.name];
|
|
2036
|
+
if (!register) return [];
|
|
2037
|
+
return [...register.reactions.values()].filter((reaction) => {
|
|
2038
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
2039
|
+
return resolved && resolved.target === stream;
|
|
2040
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
2041
|
+
});
|
|
2042
|
+
fetchMap.set(stream, { fetch: f, payloads });
|
|
2043
|
+
}
|
|
2044
|
+
const handled = await Promise.all(
|
|
2045
|
+
active.map((lease) => {
|
|
2046
|
+
const entry = fetchMap.get(lease.stream);
|
|
2047
|
+
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
2048
|
+
const { payloads } = entry;
|
|
2049
|
+
const batchHandler = batchHandlers.get(lease.stream);
|
|
2050
|
+
if (batchHandler && payloads.length > 0) {
|
|
2051
|
+
return handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
2052
|
+
}
|
|
2053
|
+
return handle({ ...lease, at }, payloads);
|
|
2054
|
+
})
|
|
2055
|
+
);
|
|
2056
|
+
const acked = await ops.ack(
|
|
2057
|
+
handled.filter((h) => h.handled > 0 || !h.error).map((h) => ({ ...h.lease, at: h.acked_at }))
|
|
2058
|
+
);
|
|
2059
|
+
const blocked = await ops.block(
|
|
2060
|
+
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
2061
|
+
);
|
|
2062
|
+
return { leased, fetched, handled, acked, blocked };
|
|
2063
|
+
}
|
|
2064
|
+
var EMPTY_DRAIN = {
|
|
2065
|
+
fetched: [],
|
|
2066
|
+
leased: [],
|
|
2067
|
+
acked: [],
|
|
2068
|
+
blocked: []
|
|
2069
|
+
};
|
|
2070
|
+
var DrainController = class {
|
|
2071
|
+
constructor(deps) {
|
|
2005
2072
|
this.deps = deps;
|
|
2006
|
-
this.defaultDebounceMs = defaultDebounceMs;
|
|
2007
2073
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2074
|
+
_armed = false;
|
|
2075
|
+
_locked = false;
|
|
2076
|
+
_ratio = 0.5;
|
|
2077
|
+
/**
|
|
2078
|
+
* Per-stream backoff: `stream → nextAttemptAt` (ms since epoch). Set by
|
|
2079
|
+
* `_finalize` via `HandleResult.nextAttemptAt`; cleared on successful
|
|
2080
|
+
* ack or terminal block. Lives in process memory — per-worker pacing
|
|
2081
|
+
* by design (see {@link BackoffOptions} for the multi-worker trade-off).
|
|
2082
|
+
*/
|
|
2083
|
+
_backoff = /* @__PURE__ */ new Map();
|
|
2084
|
+
/** Timer re-arming drain at the earliest pending `nextAttemptAt`. */
|
|
2085
|
+
_backoffTimer;
|
|
2086
|
+
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
2087
|
+
_worker;
|
|
2088
|
+
_stopped = false;
|
|
2089
|
+
/**
|
|
2090
|
+
* Signal that a commit (or reset / cold-start) may have produced work.
|
|
2091
|
+
* Subsequent `drain()` calls will run the pipeline; once the pipeline
|
|
2092
|
+
* settles to no-progress, the controller disarms itself.
|
|
2093
|
+
*/
|
|
2094
|
+
arm() {
|
|
2095
|
+
this._armed = true;
|
|
2096
|
+
}
|
|
2097
|
+
/** Read-only flag — true while a commit / reset is unprocessed. */
|
|
2098
|
+
get armed() {
|
|
2099
|
+
return this._armed;
|
|
2100
|
+
}
|
|
2101
|
+
/** Returns true when `stream` is currently within a backoff window. */
|
|
2102
|
+
isDeferred = (stream) => {
|
|
2103
|
+
const next = this._backoff.get(stream);
|
|
2104
|
+
return next !== void 0 && next > Date.now();
|
|
2105
|
+
};
|
|
2106
|
+
/**
|
|
2107
|
+
* Schedule the next drain re-arm at the earliest pending backoff
|
|
2108
|
+
* expiry. Called only when the backoff map is non-empty (caller guard).
|
|
2109
|
+
* Idempotent — collapses many simultaneously deferred streams into a
|
|
2110
|
+
* single timer.
|
|
2111
|
+
*/
|
|
2112
|
+
scheduleBackoffWake() {
|
|
2113
|
+
if (this._backoffTimer) clearTimeout(this._backoffTimer);
|
|
2114
|
+
let earliest = Number.POSITIVE_INFINITY;
|
|
2115
|
+
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
2116
|
+
const delay = Math.max(0, earliest - Date.now());
|
|
2117
|
+
this._backoffTimer = setTimeout(() => {
|
|
2118
|
+
this._backoffTimer = void 0;
|
|
2119
|
+
const now = Date.now();
|
|
2120
|
+
for (const [stream, at] of this._backoff) {
|
|
2121
|
+
if (at <= now) this._backoff.delete(stream);
|
|
2122
|
+
}
|
|
2123
|
+
this._armed = true;
|
|
2124
|
+
}, delay);
|
|
2125
|
+
this._backoffTimer.unref();
|
|
2126
|
+
}
|
|
2127
|
+
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
2128
|
+
get lane() {
|
|
2129
|
+
return this.deps.lane;
|
|
2130
|
+
}
|
|
2010
2131
|
/**
|
|
2011
|
-
*
|
|
2012
|
-
*
|
|
2013
|
-
*
|
|
2014
|
-
*
|
|
2015
|
-
*
|
|
2132
|
+
* Start a per-lane worker that drains at the lane's `cycleMs`
|
|
2133
|
+
* cadence (ACT-1103). When armed, the worker calls `drain()` on every
|
|
2134
|
+
* tick and re-schedules; when not armed, it still re-schedules at
|
|
2135
|
+
* `cycleMs` so a future `arm()` is picked up on the next tick.
|
|
2136
|
+
*
|
|
2137
|
+
* The setTimeout chain uses `unref()` so it doesn't keep the process
|
|
2138
|
+
* alive on its own.
|
|
2016
2139
|
*/
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
this.
|
|
2026
|
-
|
|
2027
|
-
if (this._running) return;
|
|
2028
|
-
this._running = true;
|
|
2029
|
-
(async () => {
|
|
2030
|
-
await this.deps.init();
|
|
2031
|
-
let lastDrain;
|
|
2032
|
-
for (let i = 0; i < maxPasses; i++) {
|
|
2033
|
-
const { subscribed } = await this.deps.correlate({
|
|
2034
|
-
...correlateQuery,
|
|
2035
|
-
after: this.deps.checkpoint()
|
|
2036
|
-
});
|
|
2037
|
-
lastDrain = await this.deps.drain(drainOptions);
|
|
2038
|
-
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
2039
|
-
if (!made_progress) break;
|
|
2040
|
-
}
|
|
2041
|
-
if (lastDrain) this.deps.onSettled(lastDrain);
|
|
2042
|
-
})().catch((err) => this.deps.logger.error(err)).finally(() => {
|
|
2043
|
-
this._running = false;
|
|
2044
|
-
});
|
|
2045
|
-
}, debounceMs);
|
|
2140
|
+
start(cycleMs) {
|
|
2141
|
+
if (this._worker || this._stopped) return;
|
|
2142
|
+
const tick = async () => {
|
|
2143
|
+
if (this._armed) await this.drain();
|
|
2144
|
+
if (this._stopped) return;
|
|
2145
|
+
this._worker = setTimeout(tick, cycleMs);
|
|
2146
|
+
this._worker.unref();
|
|
2147
|
+
};
|
|
2148
|
+
this._worker = setTimeout(tick, cycleMs);
|
|
2149
|
+
this._worker.unref();
|
|
2046
2150
|
}
|
|
2047
|
-
/**
|
|
2151
|
+
/** Stop the per-lane worker. Idempotent. */
|
|
2048
2152
|
stop() {
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
this.
|
|
2153
|
+
this._stopped = true;
|
|
2154
|
+
if (this._worker) {
|
|
2155
|
+
clearTimeout(this._worker);
|
|
2156
|
+
this._worker = void 0;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
/** Run one drain pass. Short-circuits when not armed or already running. */
|
|
2160
|
+
async drain(options = {}) {
|
|
2161
|
+
if (!this._armed) return EMPTY_DRAIN;
|
|
2162
|
+
if (this._locked) return EMPTY_DRAIN;
|
|
2163
|
+
const d = this.deps.defaults ?? {};
|
|
2164
|
+
const streamLimit = d.streamLimit ?? options.streamLimit ?? 10;
|
|
2165
|
+
const eventLimit = d.eventLimit ?? options.eventLimit ?? 10;
|
|
2166
|
+
const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? 1e4;
|
|
2167
|
+
try {
|
|
2168
|
+
this._locked = true;
|
|
2169
|
+
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
2170
|
+
const leading = streamLimit - lagging;
|
|
2171
|
+
const cycle = await runDrainCycle(
|
|
2172
|
+
this.deps.ops,
|
|
2173
|
+
this.deps.registry,
|
|
2174
|
+
this.deps.batchHandlers,
|
|
2175
|
+
this.deps.handle,
|
|
2176
|
+
this.deps.handleBatch,
|
|
2177
|
+
lagging,
|
|
2178
|
+
leading,
|
|
2179
|
+
eventLimit,
|
|
2180
|
+
leaseMillis,
|
|
2181
|
+
this._backoff.size > 0 ? this.isDeferred : void 0,
|
|
2182
|
+
this.deps.lane
|
|
2183
|
+
);
|
|
2184
|
+
if (!cycle) {
|
|
2185
|
+
this._armed = false;
|
|
2186
|
+
return EMPTY_DRAIN;
|
|
2187
|
+
}
|
|
2188
|
+
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
2189
|
+
traceCycle(this.deps.logger, leased, fetched, handled, acked, blocked);
|
|
2190
|
+
this._ratio = computeLagLeadRatio(handled, lagging, leading);
|
|
2191
|
+
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
2192
|
+
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
2193
|
+
for (const h of handled) {
|
|
2194
|
+
if (h.nextAttemptAt !== void 0 && !h.block) {
|
|
2195
|
+
this._backoff.set(h.lease.stream, h.nextAttemptAt);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
if (this._backoff.size > 0) this.scheduleBackoffWake();
|
|
2199
|
+
if (acked.length) this.deps.onAcked(acked);
|
|
2200
|
+
if (blocked.length) this.deps.onBlocked(blocked);
|
|
2201
|
+
const hasErrors = handled.some(({ error }) => error);
|
|
2202
|
+
if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
|
|
2203
|
+
return { fetched, leased, acked, blocked };
|
|
2204
|
+
} catch (error) {
|
|
2205
|
+
this.deps.logger.error(error);
|
|
2206
|
+
return EMPTY_DRAIN;
|
|
2207
|
+
} finally {
|
|
2208
|
+
this._locked = false;
|
|
2052
2209
|
}
|
|
2053
2210
|
}
|
|
2054
2211
|
};
|
|
2055
2212
|
|
|
2056
|
-
// src/internal/
|
|
2057
|
-
var
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
limit: eventLimit
|
|
2066
|
-
});
|
|
2067
|
-
return { stream, source, at, lagging, events };
|
|
2068
|
-
})
|
|
2069
|
-
);
|
|
2213
|
+
// src/internal/event-versions.ts
|
|
2214
|
+
var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
|
|
2215
|
+
function parse(name) {
|
|
2216
|
+
const m = name.match(VERSION_SUFFIX);
|
|
2217
|
+
if (m) {
|
|
2218
|
+
const v = Number.parseInt(m[2], 10);
|
|
2219
|
+
if (v >= 2) return { base: m[1], version: v };
|
|
2220
|
+
}
|
|
2221
|
+
return { base: name, version: 1 };
|
|
2070
2222
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
correlation: meta.correlation,
|
|
2085
|
-
causation: { event: { id, name, stream } }
|
|
2086
|
-
},
|
|
2087
|
-
version
|
|
2088
|
-
// IMPORTANT! - state events are committed right after the snapshot event
|
|
2089
|
-
);
|
|
2090
|
-
} catch (error) {
|
|
2091
|
-
log().error(error);
|
|
2223
|
+
function deprecatedEventNames(names) {
|
|
2224
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2225
|
+
for (const name of names) {
|
|
2226
|
+
const { base, version } = parse(name);
|
|
2227
|
+
const list = groups.get(base);
|
|
2228
|
+
if (list) list.push({ version, name });
|
|
2229
|
+
else groups.set(base, [{ version, name }]);
|
|
2230
|
+
}
|
|
2231
|
+
const deprecated = /* @__PURE__ */ new Set();
|
|
2232
|
+
for (const list of groups.values()) {
|
|
2233
|
+
if (list.length < 2) continue;
|
|
2234
|
+
list.sort((a, b) => b.version - a.version);
|
|
2235
|
+
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
2092
2236
|
}
|
|
2237
|
+
return deprecated;
|
|
2093
2238
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
);
|
|
2102
|
-
return committed;
|
|
2103
|
-
} catch (error) {
|
|
2104
|
-
if (error instanceof ConcurrencyError) return void 0;
|
|
2105
|
-
throw error;
|
|
2239
|
+
function currentVersionOf(deprecatedName, allNames) {
|
|
2240
|
+
const target = parse(deprecatedName);
|
|
2241
|
+
let highest;
|
|
2242
|
+
for (const name of allNames) {
|
|
2243
|
+
const { base, version } = parse(name);
|
|
2244
|
+
if (base !== target.base) continue;
|
|
2245
|
+
if (!highest || version > highest.version) highest = { version, name };
|
|
2106
2246
|
}
|
|
2247
|
+
return highest && highest.version > target.version ? highest.name : void 0;
|
|
2107
2248
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
let
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
} else if (e.name !== TOMBSTONE_EVENT) {
|
|
2132
|
-
log().warn(
|
|
2133
|
-
`Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
|
|
2134
|
-
);
|
|
2249
|
+
|
|
2250
|
+
// src/internal/merge.ts
|
|
2251
|
+
var import_zod4 = require("zod");
|
|
2252
|
+
function baseTypeName(zodType) {
|
|
2253
|
+
let t = zodType;
|
|
2254
|
+
while (typeof t.unwrap === "function") {
|
|
2255
|
+
t = t.unwrap();
|
|
2256
|
+
}
|
|
2257
|
+
return t.constructor.name;
|
|
2258
|
+
}
|
|
2259
|
+
function mergeSchemas(existing, incoming, stateName) {
|
|
2260
|
+
if (existing instanceof import_zod4.ZodObject && incoming instanceof import_zod4.ZodObject) {
|
|
2261
|
+
const existingShape = existing.shape;
|
|
2262
|
+
const incomingShape = incoming.shape;
|
|
2263
|
+
for (const key of Object.keys(incomingShape)) {
|
|
2264
|
+
if (key in existingShape) {
|
|
2265
|
+
const existingBase = baseTypeName(existingShape[key]);
|
|
2266
|
+
const incomingBase = baseTypeName(incomingShape[key]);
|
|
2267
|
+
if (existingBase !== incomingBase) {
|
|
2268
|
+
throw new Error(
|
|
2269
|
+
`Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2135
2272
|
}
|
|
2136
|
-
callback?.({
|
|
2137
|
-
event,
|
|
2138
|
-
state: state2,
|
|
2139
|
-
version,
|
|
2140
|
-
patches,
|
|
2141
|
-
snaps,
|
|
2142
|
-
cache_hit,
|
|
2143
|
-
replayed
|
|
2144
|
-
});
|
|
2145
|
-
},
|
|
2146
|
-
{
|
|
2147
|
-
stream,
|
|
2148
|
-
stream_exact: true,
|
|
2149
|
-
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
2150
2273
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2274
|
+
return existing.extend(incomingShape);
|
|
2275
|
+
}
|
|
2276
|
+
return existing;
|
|
2277
|
+
}
|
|
2278
|
+
function mergeInits(existing, incoming) {
|
|
2279
|
+
return () => ({ ...existing(), ...incoming() });
|
|
2280
|
+
}
|
|
2281
|
+
function registerState(state2, states, actions, events) {
|
|
2282
|
+
const existing = states.get(state2.name);
|
|
2283
|
+
if (existing) {
|
|
2284
|
+
mergeIntoExisting(state2, existing, states, actions, events);
|
|
2285
|
+
} else {
|
|
2286
|
+
registerNewState(state2, states, actions, events);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
function registerNewState(state2, states, actions, events) {
|
|
2290
|
+
states.set(state2.name, state2);
|
|
2291
|
+
for (const name of Object.keys(state2.actions)) {
|
|
2292
|
+
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
2293
|
+
actions[name] = state2;
|
|
2294
|
+
}
|
|
2295
|
+
for (const name of Object.keys(state2.events)) {
|
|
2296
|
+
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
2297
|
+
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
2160
2298
|
}
|
|
2161
|
-
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
2162
2299
|
}
|
|
2163
|
-
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
const snapshot = await load(me, stream);
|
|
2168
|
-
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
2169
|
-
throw new StreamClosedError(stream);
|
|
2170
|
-
const expected = expectedVersion ?? snapshot.event?.version;
|
|
2171
|
-
if (me.given) {
|
|
2172
|
-
const invariants = me.given[action2] || [];
|
|
2173
|
-
invariants.forEach(({ valid, description }) => {
|
|
2174
|
-
if (!valid(snapshot.state, actor))
|
|
2175
|
-
throw new InvariantError(
|
|
2176
|
-
action2,
|
|
2177
|
-
validated,
|
|
2178
|
-
target,
|
|
2179
|
-
snapshot,
|
|
2180
|
-
description
|
|
2181
|
-
);
|
|
2182
|
-
});
|
|
2300
|
+
function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
2301
|
+
for (const name of Object.keys(state2.actions)) {
|
|
2302
|
+
if (existing.actions[name] === state2.actions[name]) continue;
|
|
2303
|
+
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
2183
2304
|
}
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2305
|
+
for (const name of Object.keys(state2.events)) {
|
|
2306
|
+
if (existing.events[name] === state2.events[name]) continue;
|
|
2307
|
+
if (existing.events[name]) {
|
|
2308
|
+
throw new Error(
|
|
2309
|
+
`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.`
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
2188
2313
|
}
|
|
2189
|
-
const
|
|
2190
|
-
const
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2314
|
+
const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
|
|
2315
|
+
const merged = {
|
|
2316
|
+
...existing,
|
|
2317
|
+
state: mergeSchemas(existing.state, state2.state, state2.name),
|
|
2318
|
+
init: mergeInits(existing.init, state2.init),
|
|
2319
|
+
events: { ...existing.events, ...state2.events },
|
|
2320
|
+
actions: { ...existing.actions, ...state2.actions },
|
|
2321
|
+
patch: mergedPatch,
|
|
2322
|
+
on: { ...existing.on, ...state2.on },
|
|
2323
|
+
given: { ...existing.given, ...state2.given },
|
|
2324
|
+
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
2325
|
+
throw new Error(
|
|
2326
|
+
`Duplicate snap strategy for state "${state2.name}"`
|
|
2327
|
+
);
|
|
2328
|
+
})() : state2.snap || existing.snap
|
|
2329
|
+
};
|
|
2330
|
+
states.set(state2.name, merged);
|
|
2331
|
+
for (const name of Object.keys(merged.actions)) {
|
|
2332
|
+
actions[name] = merged;
|
|
2333
|
+
}
|
|
2334
|
+
for (const name of Object.keys(state2.events)) {
|
|
2335
|
+
if (events[name]) continue;
|
|
2336
|
+
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
function mergePatches(existing, incoming, stateName) {
|
|
2340
|
+
const merged = { ...existing };
|
|
2341
|
+
for (const name of Object.keys(incoming)) {
|
|
2342
|
+
const existingP = existing[name];
|
|
2343
|
+
const incomingP = incoming[name];
|
|
2344
|
+
if (!existingP) {
|
|
2345
|
+
merged[name] = incomingP;
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
const existingIsDefault = existingP._passthrough;
|
|
2349
|
+
const incomingIsDefault = incomingP._passthrough;
|
|
2350
|
+
if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
|
|
2351
|
+
throw new Error(
|
|
2352
|
+
`Duplicate custom patch for event "${name}" in state "${stateName}"`
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
if (existingIsDefault && !incomingIsDefault) {
|
|
2356
|
+
merged[name] = incomingP;
|
|
2202
2357
|
}
|
|
2203
2358
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
stream,
|
|
2213
|
-
actor: target.actor
|
|
2214
|
-
}),
|
|
2215
|
-
causation: {
|
|
2216
|
-
action: {
|
|
2217
|
-
name: action2,
|
|
2218
|
-
...target
|
|
2219
|
-
// payload intentionally omitted: it can be large or contain PII,
|
|
2220
|
-
// and callers correlate via the correlation id when they need it.
|
|
2221
|
-
},
|
|
2222
|
-
event: reactingTo ? {
|
|
2223
|
-
id: reactingTo.id,
|
|
2224
|
-
name: reactingTo.name,
|
|
2225
|
-
stream: reactingTo.stream
|
|
2226
|
-
} : void 0
|
|
2359
|
+
return merged;
|
|
2360
|
+
}
|
|
2361
|
+
function mergeEventRegister(target, source) {
|
|
2362
|
+
for (const [eventName, sourceReg] of Object.entries(source)) {
|
|
2363
|
+
const targetReg = target[eventName];
|
|
2364
|
+
if (!targetReg) continue;
|
|
2365
|
+
for (const [name, reaction] of sourceReg.reactions) {
|
|
2366
|
+
targetReg.reactions.set(name, reaction);
|
|
2227
2367
|
}
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
function mergeProjection(proj, events) {
|
|
2371
|
+
for (const eventName of Object.keys(proj.events)) {
|
|
2372
|
+
const projRegister = proj.events[eventName];
|
|
2373
|
+
const existing = events[eventName];
|
|
2374
|
+
if (!existing) {
|
|
2375
|
+
events[eventName] = {
|
|
2376
|
+
schema: projRegister.schema,
|
|
2377
|
+
reactions: new Map(projRegister.reactions)
|
|
2378
|
+
};
|
|
2379
|
+
} else {
|
|
2380
|
+
for (const [name, reaction] of projRegister.reactions) {
|
|
2381
|
+
let key = name;
|
|
2382
|
+
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
2383
|
+
existing.reactions.set(key, reaction);
|
|
2384
|
+
}
|
|
2244
2385
|
}
|
|
2245
|
-
throw error;
|
|
2246
2386
|
}
|
|
2247
|
-
let { state: state2, patches } = snapshot;
|
|
2248
|
-
const snapshots = committed.map((event) => {
|
|
2249
|
-
const p = me.patch[event.name](event, state2);
|
|
2250
|
-
state2 = (0, import_act_patch.patch)(state2, p);
|
|
2251
|
-
patches++;
|
|
2252
|
-
return {
|
|
2253
|
-
event,
|
|
2254
|
-
state: state2,
|
|
2255
|
-
version: event.version,
|
|
2256
|
-
patches,
|
|
2257
|
-
snaps: snapshot.snaps,
|
|
2258
|
-
patch: p,
|
|
2259
|
-
cache_hit: snapshot.cache_hit,
|
|
2260
|
-
replayed: snapshot.replayed
|
|
2261
|
-
};
|
|
2262
|
-
});
|
|
2263
|
-
const last = snapshots.at(-1);
|
|
2264
|
-
const snapped = me.snap?.(last);
|
|
2265
|
-
cache2().set(stream, {
|
|
2266
|
-
state: last.state,
|
|
2267
|
-
version: last.event.version,
|
|
2268
|
-
event_id: last.event.id,
|
|
2269
|
-
patches: snapped ? 0 : last.patches,
|
|
2270
|
-
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
2271
|
-
}).catch((err) => log().error(err));
|
|
2272
|
-
if (snapped) void snap(last);
|
|
2273
|
-
return snapshots;
|
|
2274
2387
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
var C_BLUE = "\x1B[38;5;39m";
|
|
2279
|
-
var C_ORANGE = "\x1B[38;5;208m";
|
|
2280
|
-
var C_GREEN = "\x1B[38;5;42m";
|
|
2281
|
-
var C_MAGENTA = "\x1B[38;5;165m";
|
|
2282
|
-
var C_DRAIN = "\x1B[38;5;244m";
|
|
2283
|
-
var C_HIT = "\x1B[38;5;82m";
|
|
2284
|
-
var C_MISS = "\x1B[38;5;220m";
|
|
2285
|
-
var C_RESET = "\x1B[0m";
|
|
2286
|
-
var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
|
|
2287
|
-
var drain_caption = (caption) => {
|
|
2288
|
-
const tag = `>> ${caption}`;
|
|
2289
|
-
return PRETTY ? `${C_DRAIN}${tag}${C_RESET}` : tag;
|
|
2290
|
-
};
|
|
2291
|
-
var cache_marker = (hit) => {
|
|
2292
|
-
const word = hit ? "hit" : "miss";
|
|
2293
|
-
if (!PRETTY) return word;
|
|
2294
|
-
return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
|
|
2295
|
-
};
|
|
2296
|
-
var stats_marker = (version, replayed, snaps, patches) => {
|
|
2297
|
-
const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
|
|
2298
|
-
if (!PRETTY) return text;
|
|
2299
|
-
return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
|
|
2300
|
-
};
|
|
2301
|
-
var as_of_marker = (asOf) => {
|
|
2302
|
-
if (!asOf) return "";
|
|
2303
|
-
const parts = [];
|
|
2304
|
-
if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
|
|
2305
|
-
if (asOf.created_before !== void 0)
|
|
2306
|
-
parts.push(`created_before=${asOf.created_before.toISOString()}`);
|
|
2307
|
-
if (asOf.created_after !== void 0)
|
|
2308
|
-
parts.push(`created_after=${asOf.created_after.toISOString()}`);
|
|
2309
|
-
if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
|
|
2310
|
-
return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
|
|
2311
|
-
};
|
|
2312
|
-
var traced = (inner, exit, entry) => (async (...args) => {
|
|
2313
|
-
entry?.(...args);
|
|
2314
|
-
const result = await inner(...args);
|
|
2315
|
-
exit?.(result, ...args);
|
|
2316
|
-
return result;
|
|
2388
|
+
var _this_ = ({ stream }) => ({
|
|
2389
|
+
source: stream,
|
|
2390
|
+
target: stream
|
|
2317
2391
|
});
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2392
|
+
|
|
2393
|
+
// src/internal/backoff.ts
|
|
2394
|
+
function computeBackoffDelay(retry, opts) {
|
|
2395
|
+
if (!opts || opts.baseMs <= 0) return 0;
|
|
2396
|
+
const r = Math.max(0, retry);
|
|
2397
|
+
let delay;
|
|
2398
|
+
switch (opts.strategy) {
|
|
2399
|
+
case "fixed":
|
|
2400
|
+
delay = opts.baseMs;
|
|
2401
|
+
break;
|
|
2402
|
+
case "linear":
|
|
2403
|
+
delay = opts.baseMs * (r + 1);
|
|
2404
|
+
break;
|
|
2405
|
+
case "exponential":
|
|
2406
|
+
delay = opts.baseMs * 2 ** r;
|
|
2407
|
+
if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
|
|
2408
|
+
break;
|
|
2335
2409
|
}
|
|
2410
|
+
if (opts.jitter) delay = delay * (0.5 + Math.random());
|
|
2411
|
+
return Math.max(0, Math.floor(delay));
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// src/internal/reactions.ts
|
|
2415
|
+
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
2416
|
+
if (!error) return { lease, handled, acked_at: at };
|
|
2417
|
+
logger.error(error);
|
|
2418
|
+
const nonRetryable = error instanceof NonRetryableError;
|
|
2419
|
+
const block2 = options.blockOnError && (nonRetryable || lease.retry >= options.maxRetries);
|
|
2420
|
+
if (block2)
|
|
2421
|
+
logger.error(
|
|
2422
|
+
nonRetryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
2423
|
+
);
|
|
2424
|
+
const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
|
|
2336
2425
|
return {
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
);
|
|
2345
|
-
}),
|
|
2346
|
-
load: traced(load, (result, _me, stream, _cb, asOf) => {
|
|
2347
|
-
const stats = stats_marker(
|
|
2348
|
-
result.version,
|
|
2349
|
-
result.replayed,
|
|
2350
|
-
result.snaps,
|
|
2351
|
-
result.patches
|
|
2352
|
-
);
|
|
2353
|
-
logger.trace(
|
|
2354
|
-
es_caption(
|
|
2355
|
-
"load",
|
|
2356
|
-
C_GREEN,
|
|
2357
|
-
`${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
2358
|
-
)
|
|
2359
|
-
);
|
|
2360
|
-
}),
|
|
2361
|
-
action: traced(
|
|
2362
|
-
boundAction,
|
|
2363
|
-
(snapshots, _me, _action, target) => {
|
|
2364
|
-
const committed = snapshots.filter((s) => s.event);
|
|
2365
|
-
if (committed.length) {
|
|
2366
|
-
logger.trace(
|
|
2367
|
-
committed.map((s) => s.event.data),
|
|
2368
|
-
es_caption(
|
|
2369
|
-
"committed",
|
|
2370
|
-
C_ORANGE,
|
|
2371
|
-
`${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
|
|
2372
|
-
)
|
|
2373
|
-
);
|
|
2374
|
-
}
|
|
2375
|
-
},
|
|
2376
|
-
(_me, action2, target, payload) => {
|
|
2377
|
-
logger.trace(
|
|
2378
|
-
payload,
|
|
2379
|
-
es_caption("action", C_BLUE, `${target.stream}.${action2}`)
|
|
2380
|
-
);
|
|
2381
|
-
}
|
|
2382
|
-
),
|
|
2383
|
-
tombstone: traced(tombstone, (committed, stream) => {
|
|
2384
|
-
if (committed)
|
|
2385
|
-
logger.trace(
|
|
2386
|
-
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
2387
|
-
);
|
|
2388
|
-
})
|
|
2426
|
+
lease,
|
|
2427
|
+
handled,
|
|
2428
|
+
acked_at: at,
|
|
2429
|
+
error: error.message,
|
|
2430
|
+
block: block2,
|
|
2431
|
+
nextAttemptAt,
|
|
2432
|
+
failed_at
|
|
2389
2433
|
};
|
|
2390
2434
|
}
|
|
2391
|
-
function
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2435
|
+
function buildHandle(deps) {
|
|
2436
|
+
const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
|
|
2437
|
+
return async (lease, payloads) => {
|
|
2438
|
+
if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
|
|
2439
|
+
const stream = lease.stream;
|
|
2440
|
+
let at = payloads.at(0).event.id;
|
|
2441
|
+
let handled = 0;
|
|
2442
|
+
if (lease.retry > 0)
|
|
2443
|
+
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
2444
|
+
const scopedApp = {
|
|
2445
|
+
do: boundDo,
|
|
2446
|
+
load: boundLoad,
|
|
2447
|
+
query: boundQuery,
|
|
2448
|
+
query_array: boundQueryArray
|
|
2399
2449
|
};
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
}
|
|
2409
|
-
}),
|
|
2410
|
-
fetch: traced(fetch, (fetched) => {
|
|
2411
|
-
const data = Object.fromEntries(
|
|
2412
|
-
fetched.map(({ stream, source, events }) => {
|
|
2413
|
-
const key = source ? `${stream}<-${source}` : stream;
|
|
2414
|
-
const value = Object.fromEntries(
|
|
2415
|
-
events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
|
|
2416
|
-
);
|
|
2417
|
-
return [key, value];
|
|
2418
|
-
})
|
|
2450
|
+
for (const payload of payloads) {
|
|
2451
|
+
const { event, handler } = payload;
|
|
2452
|
+
scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
|
|
2453
|
+
action2,
|
|
2454
|
+
target,
|
|
2455
|
+
actionPayload,
|
|
2456
|
+
reactingTo ?? event,
|
|
2457
|
+
skipValidation
|
|
2419
2458
|
);
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
blocked.map(({ stream, at, retry, error }) => [
|
|
2434
|
-
stream,
|
|
2435
|
-
{ at, retry, error }
|
|
2436
|
-
])
|
|
2459
|
+
try {
|
|
2460
|
+
await handler(event, stream, scopedApp);
|
|
2461
|
+
at = event.id;
|
|
2462
|
+
handled++;
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
return finalize(
|
|
2465
|
+
lease,
|
|
2466
|
+
handled,
|
|
2467
|
+
at,
|
|
2468
|
+
error,
|
|
2469
|
+
payload.options,
|
|
2470
|
+
logger,
|
|
2471
|
+
event.id
|
|
2437
2472
|
);
|
|
2438
|
-
logger.trace(data, drain_caption("blocked"));
|
|
2439
|
-
}
|
|
2440
|
-
}),
|
|
2441
|
-
subscribe: traced(subscribe, (result, streams) => {
|
|
2442
|
-
if (result.subscribed) {
|
|
2443
|
-
const data = streams.map(({ stream }) => stream).join(" ");
|
|
2444
|
-
logger.trace(`${drain_caption("correlated")} ${data}`);
|
|
2445
2473
|
}
|
|
2446
|
-
}
|
|
2474
|
+
}
|
|
2475
|
+
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
function buildHandleBatch(logger) {
|
|
2479
|
+
return async (lease, payloads, batchHandler) => {
|
|
2480
|
+
const stream = lease.stream;
|
|
2481
|
+
const events = payloads.map((p) => p.event);
|
|
2482
|
+
const options = payloads[0].options;
|
|
2483
|
+
if (lease.retry > 0)
|
|
2484
|
+
logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
|
|
2485
|
+
try {
|
|
2486
|
+
await batchHandler(events, stream);
|
|
2487
|
+
return finalize(
|
|
2488
|
+
lease,
|
|
2489
|
+
events.length,
|
|
2490
|
+
events.at(-1).id,
|
|
2491
|
+
void 0,
|
|
2492
|
+
options,
|
|
2493
|
+
logger
|
|
2494
|
+
);
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
return finalize(lease, 0, lease.at, error, options, logger);
|
|
2497
|
+
}
|
|
2447
2498
|
};
|
|
2448
2499
|
}
|
|
2449
2500
|
|
|
2501
|
+
// src/internal/settle.ts
|
|
2502
|
+
var SettleLoop = class {
|
|
2503
|
+
constructor(deps, defaultDebounceMs) {
|
|
2504
|
+
this.deps = deps;
|
|
2505
|
+
this.defaultDebounceMs = defaultDebounceMs;
|
|
2506
|
+
}
|
|
2507
|
+
_timer = void 0;
|
|
2508
|
+
_running = false;
|
|
2509
|
+
/**
|
|
2510
|
+
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
2511
|
+
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
2512
|
+
* until no progress is made (no new subscriptions, no acks, no blocks)
|
|
2513
|
+
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
2514
|
+
* via {@link SettleDeps.onSettled}.
|
|
2515
|
+
*/
|
|
2516
|
+
schedule(options = {}) {
|
|
2517
|
+
const {
|
|
2518
|
+
debounceMs = this.defaultDebounceMs,
|
|
2519
|
+
correlate: correlateQuery = { after: -1, limit: 100 },
|
|
2520
|
+
maxPasses = Infinity,
|
|
2521
|
+
...drainOptions
|
|
2522
|
+
} = options;
|
|
2523
|
+
if (this._timer) clearTimeout(this._timer);
|
|
2524
|
+
this._timer = setTimeout(() => {
|
|
2525
|
+
this._timer = void 0;
|
|
2526
|
+
if (this._running) return;
|
|
2527
|
+
this._running = true;
|
|
2528
|
+
(async () => {
|
|
2529
|
+
await this.deps.init();
|
|
2530
|
+
let lastDrain;
|
|
2531
|
+
for (let i = 0; i < maxPasses; i++) {
|
|
2532
|
+
const { subscribed } = await this.deps.correlate({
|
|
2533
|
+
...correlateQuery,
|
|
2534
|
+
after: this.deps.checkpoint()
|
|
2535
|
+
});
|
|
2536
|
+
lastDrain = await this.deps.drain(drainOptions);
|
|
2537
|
+
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
2538
|
+
if (!made_progress) break;
|
|
2539
|
+
}
|
|
2540
|
+
if (lastDrain) this.deps.onSettled(lastDrain);
|
|
2541
|
+
})().catch((err) => this.deps.logger.error(err)).finally(() => {
|
|
2542
|
+
this._running = false;
|
|
2543
|
+
});
|
|
2544
|
+
}, debounceMs);
|
|
2545
|
+
}
|
|
2546
|
+
/** Cancel any pending or active settle cycle. Idempotent. */
|
|
2547
|
+
stop() {
|
|
2548
|
+
if (this._timer) {
|
|
2549
|
+
clearTimeout(this._timer);
|
|
2550
|
+
this._timer = void 0;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
|
|
2450
2555
|
// src/act.ts
|
|
2451
2556
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
2452
2557
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
@@ -2461,11 +2566,26 @@ var Act = class {
|
|
|
2461
2566
|
* @param _states Merged map of state name → state definition
|
|
2462
2567
|
* @param batchHandlers Static-target projection batch handlers (target → handler)
|
|
2463
2568
|
* @param options Tuning knobs — see {@link ActOptions}
|
|
2569
|
+
* @param lanes Declared drain lanes (ACT-1103). The builder collects
|
|
2570
|
+
* these from `.withLane(...)` calls. Slice 1 records them on the
|
|
2571
|
+
* instance; later slices fan out one `DrainController` per lane.
|
|
2464
2572
|
*/
|
|
2465
|
-
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
|
|
2573
|
+
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
|
|
2466
2574
|
this.registry = registry;
|
|
2467
2575
|
this._states = _states;
|
|
2468
2576
|
this._batch_handlers = batchHandlers;
|
|
2577
|
+
this._lanes = lanes;
|
|
2578
|
+
if (options.onlyLanes && options.onlyLanes.length > 0) {
|
|
2579
|
+
const declared = /* @__PURE__ */ new Set([
|
|
2580
|
+
"default",
|
|
2581
|
+
...lanes.map((l) => l.name)
|
|
2582
|
+
]);
|
|
2583
|
+
const unknown = options.onlyLanes.filter((l) => !declared.has(l));
|
|
2584
|
+
if (unknown.length > 0)
|
|
2585
|
+
throw new Error(
|
|
2586
|
+
`ActOptions.onlyLanes references undeclared lane(s): ${unknown.map((l) => `"${l}"`).join(", ")}`
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2469
2589
|
this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
|
|
2470
2590
|
this._correlator = options.correlator ?? defaultCorrelator;
|
|
2471
2591
|
this._es = buildEs(this._logger, this._correlator);
|
|
@@ -2478,19 +2598,44 @@ var Act = class {
|
|
|
2478
2598
|
boundQueryArray: this._bound_query_array
|
|
2479
2599
|
});
|
|
2480
2600
|
this._handle_batch = buildHandleBatch(this._logger);
|
|
2481
|
-
const {
|
|
2601
|
+
const {
|
|
2602
|
+
staticTargets,
|
|
2603
|
+
hasDynamicResolvers,
|
|
2604
|
+
reactiveEvents,
|
|
2605
|
+
eventToState,
|
|
2606
|
+
eventToLanes
|
|
2607
|
+
} = classifyRegistry(this.registry, this._states);
|
|
2482
2608
|
this._reactive_events = reactiveEvents;
|
|
2483
2609
|
this._event_to_state = eventToState;
|
|
2484
|
-
this.
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2610
|
+
this._event_to_lanes = eventToLanes;
|
|
2611
|
+
const allLanes = ["default", ...lanes.map((l) => l.name)];
|
|
2612
|
+
const onlySet = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
2613
|
+
const activeLanes = onlySet ? allLanes.filter((n) => onlySet.has(n)) : allLanes;
|
|
2614
|
+
const singleDefaultLane = activeLanes.length === 1 && activeLanes[0] === "default";
|
|
2615
|
+
this._drain_controllers = /* @__PURE__ */ new Map();
|
|
2616
|
+
for (const name of activeLanes) {
|
|
2617
|
+
const cfg = lanes.find((l) => l.name === name);
|
|
2618
|
+
const controller = new DrainController({
|
|
2619
|
+
logger: this._logger,
|
|
2620
|
+
ops: this._cd,
|
|
2621
|
+
registry: this.registry,
|
|
2622
|
+
batchHandlers: this._batch_handlers,
|
|
2623
|
+
handle: this._handle,
|
|
2624
|
+
handleBatch: this._handle_batch,
|
|
2625
|
+
onAcked: (acked) => this.emit("acked", acked),
|
|
2626
|
+
onBlocked: (blocked) => this.emit("blocked", blocked),
|
|
2627
|
+
// Pass lane only when a true per-lane controller is active.
|
|
2628
|
+
// The all-lanes (single default) case keeps lane=undefined so
|
|
2629
|
+
// adapter SQL collapses to the pre-1103 shape.
|
|
2630
|
+
lane: singleDefaultLane ? void 0 : name,
|
|
2631
|
+
defaults: cfg && {
|
|
2632
|
+
streamLimit: cfg.streamLimit,
|
|
2633
|
+
leaseMillis: cfg.leaseMillis
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
2636
|
+
if (cfg?.cycleMs !== void 0) controller.start(cfg.cycleMs);
|
|
2637
|
+
this._drain_controllers.set(name, controller);
|
|
2638
|
+
}
|
|
2494
2639
|
this._correlate = new CorrelateCycle(
|
|
2495
2640
|
this.registry,
|
|
2496
2641
|
staticTargets,
|
|
@@ -2499,7 +2644,7 @@ var Act = class {
|
|
|
2499
2644
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2500
2645
|
// Cold start: assume drain is needed (historical events may need processing)
|
|
2501
2646
|
() => {
|
|
2502
|
-
if (this._reactive_events.size > 0) this.
|
|
2647
|
+
if (this._reactive_events.size > 0) this._armAll();
|
|
2503
2648
|
}
|
|
2504
2649
|
);
|
|
2505
2650
|
this._settle = new SettleLoop(
|
|
@@ -2519,8 +2664,8 @@ var Act = class {
|
|
|
2519
2664
|
_emitter = new import_node_events.default();
|
|
2520
2665
|
/** Event names with at least one registered reaction (computed at build time) */
|
|
2521
2666
|
_reactive_events;
|
|
2522
|
-
/**
|
|
2523
|
-
|
|
2667
|
+
/** One DrainController per active lane, keyed by lane name. */
|
|
2668
|
+
_drain_controllers;
|
|
2524
2669
|
/** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
|
|
2525
2670
|
_correlate;
|
|
2526
2671
|
/** Debounced correlate→drain catch-up loop. */
|
|
@@ -2574,6 +2719,14 @@ var Act = class {
|
|
|
2574
2719
|
* set when seeding a `restart` snapshot in multi-state apps.
|
|
2575
2720
|
*/
|
|
2576
2721
|
_event_to_state;
|
|
2722
|
+
/**
|
|
2723
|
+
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
2724
|
+
* `classifyRegistry` once per build. `"all"` means at least one of
|
|
2725
|
+
* the event's reactions is a dynamic resolver (lane opaque until
|
|
2726
|
+
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
2727
|
+
* reactions target.
|
|
2728
|
+
*/
|
|
2729
|
+
_event_to_lanes;
|
|
2577
2730
|
/** Logger resolved at construction time (after user port configuration) */
|
|
2578
2731
|
_logger = log();
|
|
2579
2732
|
/** Wraps a public-method body so internal `store()`/`cache()` resolve to the
|
|
@@ -2597,6 +2750,12 @@ var Act = class {
|
|
|
2597
2750
|
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
2598
2751
|
_handle;
|
|
2599
2752
|
_handle_batch;
|
|
2753
|
+
/** Declared drain lanes (ACT-1103). */
|
|
2754
|
+
_lanes;
|
|
2755
|
+
/** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
|
|
2756
|
+
get lanes() {
|
|
2757
|
+
return this._lanes;
|
|
2758
|
+
}
|
|
2600
2759
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
2601
2760
|
_shutdown_promise;
|
|
2602
2761
|
/**
|
|
@@ -2615,6 +2774,7 @@ var Act = class {
|
|
|
2615
2774
|
this._emitter.removeAllListeners();
|
|
2616
2775
|
this.stop_correlations();
|
|
2617
2776
|
this.stop_settling();
|
|
2777
|
+
for (const c of this._drain_controllers.values()) c.stop();
|
|
2618
2778
|
const disposer = await this._notify_disposer;
|
|
2619
2779
|
if (disposer) await disposer();
|
|
2620
2780
|
})();
|
|
@@ -2634,13 +2794,10 @@ var Act = class {
|
|
|
2634
2794
|
return await s.notify((notification) => {
|
|
2635
2795
|
try {
|
|
2636
2796
|
this.emit("notified", notification);
|
|
2637
|
-
const
|
|
2638
|
-
(e) =>
|
|
2797
|
+
const armed = this._armForEventNames(
|
|
2798
|
+
notification.events.map((e) => e.name)
|
|
2639
2799
|
);
|
|
2640
|
-
if (
|
|
2641
|
-
this._drain.arm();
|
|
2642
|
-
this._settle.schedule({ debounceMs: 0 });
|
|
2643
|
-
}
|
|
2800
|
+
if (armed) this._settle.schedule({ debounceMs: 0 });
|
|
2644
2801
|
} catch (err) {
|
|
2645
2802
|
this._logger.error(err, "notified handler threw");
|
|
2646
2803
|
}
|
|
@@ -2741,14 +2898,10 @@ var Act = class {
|
|
|
2741
2898
|
reactingTo,
|
|
2742
2899
|
skipValidation
|
|
2743
2900
|
);
|
|
2744
|
-
if (this._reactive_events.size > 0)
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
break;
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2901
|
+
if (this._reactive_events.size > 0)
|
|
2902
|
+
this._armForEventNames(
|
|
2903
|
+
snapshots.map((s) => s.event.name)
|
|
2904
|
+
);
|
|
2752
2905
|
this.emit("committed", snapshots);
|
|
2753
2906
|
return snapshots;
|
|
2754
2907
|
});
|
|
@@ -2895,7 +3048,59 @@ var Act = class {
|
|
|
2895
3048
|
* @see {@link start_correlations} for automatic correlation
|
|
2896
3049
|
*/
|
|
2897
3050
|
async drain(options = {}) {
|
|
2898
|
-
return this._scoped(() => this.
|
|
3051
|
+
return this._scoped(() => this._drainAll(options));
|
|
3052
|
+
}
|
|
3053
|
+
/** Arm every active lane controller (ACT-1103). */
|
|
3054
|
+
_armAll() {
|
|
3055
|
+
for (const c of this._drain_controllers.values()) c.arm();
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Arm only the lane controllers whose reactions match the supplied
|
|
3059
|
+
* event names (ACT-1103 selective arming). Events with any dynamic
|
|
3060
|
+
* resolver fall back to `_armAll()` via the `"all"` sentinel — the
|
|
3061
|
+
* resolver's lane isn't known until correlate runs the function.
|
|
3062
|
+
* Events with no reactions are skipped; `_event_to_lanes` doesn't
|
|
3063
|
+
* carry them. Returns true when any controller was armed (used by
|
|
3064
|
+
* the notify handler to decide whether to schedule a settle).
|
|
3065
|
+
*/
|
|
3066
|
+
_armForEventNames(names) {
|
|
3067
|
+
const to_arm = /* @__PURE__ */ new Set();
|
|
3068
|
+
for (const name of names) {
|
|
3069
|
+
const set = this._event_to_lanes.get(name);
|
|
3070
|
+
if (set === void 0) continue;
|
|
3071
|
+
if (set === ALL_LANES) {
|
|
3072
|
+
this._armAll();
|
|
3073
|
+
return true;
|
|
3074
|
+
}
|
|
3075
|
+
for (const lane of set) to_arm.add(lane);
|
|
3076
|
+
}
|
|
3077
|
+
if (to_arm.size === 0) return false;
|
|
3078
|
+
for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
|
|
3079
|
+
return true;
|
|
3080
|
+
}
|
|
3081
|
+
/** Drain every active lane controller in parallel and aggregate.
|
|
3082
|
+
*
|
|
3083
|
+
* Parallel — not sequential — so a slow lane's in-flight handler does
|
|
3084
|
+
* not block a fast lane's claim/dispatch/ack cycle. Each controller's
|
|
3085
|
+
* `claim()` is independent (filtered by lane); the store's
|
|
3086
|
+
* `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
|
|
3087
|
+
* (`acked`, `blocked`) may interleave by lane — listeners filter via
|
|
3088
|
+
* `lease.lane`. */
|
|
3089
|
+
async _drainAll(options) {
|
|
3090
|
+
const results = await Promise.all(
|
|
3091
|
+
[...this._drain_controllers.values()].map((c) => c.drain(options))
|
|
3092
|
+
);
|
|
3093
|
+
const fetched = [];
|
|
3094
|
+
const leased = [];
|
|
3095
|
+
const acked = [];
|
|
3096
|
+
const blocked = [];
|
|
3097
|
+
for (const r of results) {
|
|
3098
|
+
fetched.push(...r.fetched);
|
|
3099
|
+
leased.push(...r.leased);
|
|
3100
|
+
acked.push(...r.acked);
|
|
3101
|
+
blocked.push(...r.blocked);
|
|
3102
|
+
}
|
|
3103
|
+
return { fetched, leased, acked, blocked };
|
|
2899
3104
|
}
|
|
2900
3105
|
/**
|
|
2901
3106
|
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
@@ -3066,7 +3271,7 @@ var Act = class {
|
|
|
3066
3271
|
async reset(input) {
|
|
3067
3272
|
return this._scoped(async () => {
|
|
3068
3273
|
const count = await store2().reset(input);
|
|
3069
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
3274
|
+
if (count > 0 && this._reactive_events.size > 0) this._armAll();
|
|
3070
3275
|
return count;
|
|
3071
3276
|
});
|
|
3072
3277
|
}
|
|
@@ -3100,7 +3305,7 @@ var Act = class {
|
|
|
3100
3305
|
async unblock(input) {
|
|
3101
3306
|
return this._scoped(async () => {
|
|
3102
3307
|
const count = await store2().unblock(input);
|
|
3103
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
3308
|
+
if (count > 0 && this._reactive_events.size > 0) this._armAll();
|
|
3104
3309
|
return count;
|
|
3105
3310
|
});
|
|
3106
3311
|
}
|
|
@@ -3273,6 +3478,22 @@ function registerBatchHandler(proj, batchHandlers) {
|
|
|
3273
3478
|
}
|
|
3274
3479
|
batchHandlers.set(proj.target, proj.batchHandler);
|
|
3275
3480
|
}
|
|
3481
|
+
function validateLaneReferences(registry, lanes) {
|
|
3482
|
+
const declared = /* @__PURE__ */ new Set([DEFAULT_LANE, ...lanes.map((l) => l.name)]);
|
|
3483
|
+
for (const [eventName, def] of Object.entries(registry.events)) {
|
|
3484
|
+
const entry = def;
|
|
3485
|
+
for (const [handlerName, reaction] of entry.reactions) {
|
|
3486
|
+
const resolver = reaction.resolver;
|
|
3487
|
+
if (typeof resolver === "function") continue;
|
|
3488
|
+
const lane = resolver.lane;
|
|
3489
|
+
if (lane && !declared.has(lane)) {
|
|
3490
|
+
throw new Error(
|
|
3491
|
+
`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.`
|
|
3492
|
+
);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3276
3497
|
function act() {
|
|
3277
3498
|
const states = /* @__PURE__ */ new Map();
|
|
3278
3499
|
const registry = {
|
|
@@ -3281,6 +3502,7 @@ function act() {
|
|
|
3281
3502
|
};
|
|
3282
3503
|
const pendingProjections = [];
|
|
3283
3504
|
const batchHandlers = /* @__PURE__ */ new Map();
|
|
3505
|
+
const lanes = [];
|
|
3284
3506
|
let _built = false;
|
|
3285
3507
|
const finalizeDeprecations = () => {
|
|
3286
3508
|
const deprecationSummary = [];
|
|
@@ -3327,6 +3549,18 @@ function act() {
|
|
|
3327
3549
|
}
|
|
3328
3550
|
mergeEventRegister(registry.events, input.events);
|
|
3329
3551
|
pendingProjections.push(...input.projections);
|
|
3552
|
+
for (const sliceLane of input.lanes) {
|
|
3553
|
+
const existing = lanes.find((l) => l.name === sliceLane.name);
|
|
3554
|
+
if (!existing) {
|
|
3555
|
+
lanes.push(sliceLane);
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
if (existing.leaseMillis !== sliceLane.leaseMillis || existing.streamLimit !== sliceLane.streamLimit || existing.cycleMs !== sliceLane.cycleMs) {
|
|
3559
|
+
throw new Error(
|
|
3560
|
+
`Lane "${sliceLane.name}" was already declared with a different config`
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3330
3564
|
return builder;
|
|
3331
3565
|
},
|
|
3332
3566
|
withProjection: (proj) => {
|
|
@@ -3335,6 +3569,14 @@ function act() {
|
|
|
3335
3569
|
return builder;
|
|
3336
3570
|
},
|
|
3337
3571
|
withActor: () => builder,
|
|
3572
|
+
withLane: (config2) => {
|
|
3573
|
+
if (config2.name === DEFAULT_LANE)
|
|
3574
|
+
throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
|
|
3575
|
+
if (lanes.some((l) => l.name === config2.name))
|
|
3576
|
+
throw new Error(`Lane "${config2.name}" was already declared`);
|
|
3577
|
+
lanes.push(config2);
|
|
3578
|
+
return builder;
|
|
3579
|
+
},
|
|
3338
3580
|
on: (event) => ({
|
|
3339
3581
|
do: (handler, options) => {
|
|
3340
3582
|
const reaction = {
|
|
@@ -3366,13 +3608,15 @@ function act() {
|
|
|
3366
3608
|
registerBatchHandler(proj, batchHandlers);
|
|
3367
3609
|
}
|
|
3368
3610
|
finalizeDeprecations();
|
|
3611
|
+
validateLaneReferences(registry, lanes);
|
|
3369
3612
|
_built = true;
|
|
3370
3613
|
}
|
|
3371
3614
|
return new Act(
|
|
3372
3615
|
registry,
|
|
3373
3616
|
states,
|
|
3374
3617
|
batchHandlers,
|
|
3375
|
-
options
|
|
3618
|
+
options,
|
|
3619
|
+
lanes
|
|
3376
3620
|
);
|
|
3377
3621
|
},
|
|
3378
3622
|
events: registry.events
|
|
@@ -3453,6 +3697,7 @@ function slice() {
|
|
|
3453
3697
|
const actions = {};
|
|
3454
3698
|
const events = {};
|
|
3455
3699
|
const projections = [];
|
|
3700
|
+
const lanes = [];
|
|
3456
3701
|
const builder = {
|
|
3457
3702
|
withState: (state2) => {
|
|
3458
3703
|
registerState(state2, states, actions, events);
|
|
@@ -3462,6 +3707,14 @@ function slice() {
|
|
|
3462
3707
|
projections.push(proj);
|
|
3463
3708
|
return builder;
|
|
3464
3709
|
},
|
|
3710
|
+
withLane: (config2) => {
|
|
3711
|
+
if (config2.name === DEFAULT_LANE)
|
|
3712
|
+
throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
|
|
3713
|
+
if (lanes.some((l) => l.name === config2.name))
|
|
3714
|
+
throw new Error(`Lane "${config2.name}" was already declared`);
|
|
3715
|
+
lanes.push(config2);
|
|
3716
|
+
return builder;
|
|
3717
|
+
},
|
|
3465
3718
|
on: (event) => ({
|
|
3466
3719
|
do: (handler, options) => {
|
|
3467
3720
|
const reaction = {
|
|
@@ -3490,7 +3743,8 @@ function slice() {
|
|
|
3490
3743
|
_tag: "Slice",
|
|
3491
3744
|
states,
|
|
3492
3745
|
events,
|
|
3493
|
-
projections
|
|
3746
|
+
projections,
|
|
3747
|
+
lanes
|
|
3494
3748
|
}),
|
|
3495
3749
|
events
|
|
3496
3750
|
};
|
|
@@ -3584,6 +3838,7 @@ function action_builder(state2) {
|
|
|
3584
3838
|
CommittedMetaSchema,
|
|
3585
3839
|
ConcurrencyError,
|
|
3586
3840
|
ConsoleLogger,
|
|
3841
|
+
DEFAULT_LANE,
|
|
3587
3842
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
3588
3843
|
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
3589
3844
|
Environments,
|