@rotorsoft/act 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +22 -2
- 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 +203 -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-QAB4SDOS.js → chunk-PGTC7VOC.js} +117 -11
- package/dist/chunk-PGTC7VOC.js.map +1 -0
- package/dist/index.cjs +1217 -897
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1108 -893
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +116 -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-QAB4SDOS.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,13 +1028,85 @@ 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;
|
|
1002
1036
|
}
|
|
1003
1037
|
return { maxEventId: this._events.length - 1, count };
|
|
1004
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Per-stream aggregated stats — see {@link Store.query_stats}.
|
|
1041
|
+
*
|
|
1042
|
+
* Single forward scan over the in-memory event list, accumulating per
|
|
1043
|
+
* stream. The "cheap heads" cost tier from durable adapters doesn't
|
|
1044
|
+
* apply here (InMemory has no indexes); correctness is the goal, perf
|
|
1045
|
+
* is a non-issue.
|
|
1046
|
+
*
|
|
1047
|
+
* Scope rules:
|
|
1048
|
+
* - Array `input` — explicit stream names, regardless of subscription.
|
|
1049
|
+
* - Filter `input` — `stream`/`stream_exact` match against event-bearing
|
|
1050
|
+
* stream names; `source`/`source_exact`/`blocked` require a
|
|
1051
|
+
* corresponding subscription in `_streams` (those are subscription
|
|
1052
|
+
* concepts, not event concepts). Empty filter `{}` matches every
|
|
1053
|
+
* event-bearing stream.
|
|
1054
|
+
*/
|
|
1055
|
+
async query_stats(input, options) {
|
|
1056
|
+
await sleep();
|
|
1057
|
+
const exclude = new Set(options?.exclude ?? []);
|
|
1058
|
+
const wantTail = options?.tail ?? false;
|
|
1059
|
+
const wantCount = options?.count ?? false;
|
|
1060
|
+
const wantNames = options?.names ?? false;
|
|
1061
|
+
const before = options?.before;
|
|
1062
|
+
const arrayTargets = Array.isArray(input) ? new Set(input) : null;
|
|
1063
|
+
const filter = Array.isArray(input) ? null : input;
|
|
1064
|
+
const streamRe = filter?.stream && !filter.stream_exact ? new RegExp(filter.stream) : void 0;
|
|
1065
|
+
const scopeCache = /* @__PURE__ */ new Map();
|
|
1066
|
+
const inScope = (stream) => {
|
|
1067
|
+
const cached = scopeCache.get(stream);
|
|
1068
|
+
if (cached !== void 0) return cached;
|
|
1069
|
+
let ok = true;
|
|
1070
|
+
if (arrayTargets) {
|
|
1071
|
+
ok = arrayTargets.has(stream);
|
|
1072
|
+
} else if (filter?.stream !== void 0) {
|
|
1073
|
+
ok = filter.stream_exact ? stream === filter.stream : (
|
|
1074
|
+
// biome-ignore lint/style/noNonNullAssertion: streamRe set when stream is regex
|
|
1075
|
+
streamRe.test(stream)
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
scopeCache.set(stream, ok);
|
|
1079
|
+
return ok;
|
|
1080
|
+
};
|
|
1081
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1082
|
+
for (const e of this._events) {
|
|
1083
|
+
if (before !== void 0 && e.id >= before) continue;
|
|
1084
|
+
if (!inScope(e.stream)) continue;
|
|
1085
|
+
if (exclude.has(e.name)) continue;
|
|
1086
|
+
let a = acc.get(e.stream);
|
|
1087
|
+
if (!a) {
|
|
1088
|
+
a = { head: e, count: 0 };
|
|
1089
|
+
if (wantTail) a.tail = e;
|
|
1090
|
+
if (wantNames) a.names = {};
|
|
1091
|
+
acc.set(e.stream, a);
|
|
1092
|
+
}
|
|
1093
|
+
a.head = e;
|
|
1094
|
+
a.count++;
|
|
1095
|
+
if (wantNames) {
|
|
1096
|
+
const n = String(e.name);
|
|
1097
|
+
a.names[n] = (a.names[n] ?? 0) + 1;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
const out = /* @__PURE__ */ new Map();
|
|
1101
|
+
for (const [stream, a] of acc) {
|
|
1102
|
+
const stats = { head: a.head };
|
|
1103
|
+
if (wantTail) stats.tail = a.tail;
|
|
1104
|
+
if (wantCount) stats.count = a.count;
|
|
1105
|
+
if (wantNames) stats.names = a.names;
|
|
1106
|
+
out.set(stream, stats);
|
|
1107
|
+
}
|
|
1108
|
+
return out;
|
|
1109
|
+
}
|
|
1005
1110
|
/**
|
|
1006
1111
|
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|
|
1007
1112
|
* @param targets - Streams to truncate with optional snapshot state and meta.
|
|
@@ -1107,6 +1212,7 @@ function dispose(disposer) {
|
|
|
1107
1212
|
}
|
|
1108
1213
|
var SNAP_EVENT = "__snapshot__";
|
|
1109
1214
|
var TOMBSTONE_EVENT = "__tombstone__";
|
|
1215
|
+
var DEFAULT_LANE = "default";
|
|
1110
1216
|
|
|
1111
1217
|
// src/signals.ts
|
|
1112
1218
|
process.once("SIGINT", async (arg) => {
|
|
@@ -1130,23 +1236,39 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
1130
1236
|
var import_node_events = __toESM(require("events"), 1);
|
|
1131
1237
|
|
|
1132
1238
|
// src/internal/build-classify.ts
|
|
1239
|
+
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
1133
1240
|
function classifyRegistry(registry, states) {
|
|
1134
1241
|
const statics = /* @__PURE__ */ new Map();
|
|
1135
1242
|
const reactiveEvents = /* @__PURE__ */ new Set();
|
|
1243
|
+
const eventToLanes = /* @__PURE__ */ new Map();
|
|
1136
1244
|
let hasDynamicResolvers = false;
|
|
1137
1245
|
for (const [name, register] of Object.entries(registry.events)) {
|
|
1138
1246
|
if (register.reactions.size > 0) reactiveEvents.add(name);
|
|
1139
1247
|
for (const reaction of register.reactions.values()) {
|
|
1140
1248
|
if (typeof reaction.resolver === "function") {
|
|
1141
1249
|
hasDynamicResolvers = true;
|
|
1250
|
+
eventToLanes.set(name, ALL_LANES);
|
|
1142
1251
|
} else {
|
|
1143
|
-
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
|
+
}
|
|
1144
1260
|
const key = `${target}|${source ?? ""}`;
|
|
1145
1261
|
const existing = statics.get(key);
|
|
1146
1262
|
if (!existing) {
|
|
1147
|
-
statics.set(key, { stream: target, source, priority });
|
|
1148
|
-
} else
|
|
1149
|
-
|
|
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
|
+
}
|
|
1150
1272
|
}
|
|
1151
1273
|
}
|
|
1152
1274
|
}
|
|
@@ -1161,7 +1283,8 @@ function classifyRegistry(registry, states) {
|
|
|
1161
1283
|
staticTargets: [...statics.values()],
|
|
1162
1284
|
hasDynamicResolvers,
|
|
1163
1285
|
reactiveEvents,
|
|
1164
|
-
eventToState
|
|
1286
|
+
eventToState,
|
|
1287
|
+
eventToLanes
|
|
1165
1288
|
};
|
|
1166
1289
|
}
|
|
1167
1290
|
|
|
@@ -1203,24 +1326,18 @@ async function runCloseCycle(targets, deps) {
|
|
|
1203
1326
|
return { truncated, skipped };
|
|
1204
1327
|
}
|
|
1205
1328
|
async function scanStreamHeads(streams) {
|
|
1329
|
+
const stats = await store2().query_stats(streams, {
|
|
1330
|
+
exclude: [SNAP_EVENT]
|
|
1331
|
+
});
|
|
1206
1332
|
const out = /* @__PURE__ */ new Map();
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
maxId = e.id;
|
|
1216
|
-
version = e.version;
|
|
1217
|
-
lastEventName = e.name;
|
|
1218
|
-
},
|
|
1219
|
-
{ stream: s, stream_exact: true, backward: true, limit: 1 }
|
|
1220
|
-
);
|
|
1221
|
-
if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
|
|
1222
|
-
})
|
|
1223
|
-
);
|
|
1333
|
+
for (const [stream, { head }] of stats) {
|
|
1334
|
+
if (head.name === TOMBSTONE_EVENT) continue;
|
|
1335
|
+
out.set(stream, {
|
|
1336
|
+
maxId: head.id,
|
|
1337
|
+
version: head.version,
|
|
1338
|
+
lastEventName: head.name
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1224
1341
|
return out;
|
|
1225
1342
|
}
|
|
1226
1343
|
async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
|
|
@@ -1378,6 +1495,7 @@ var CorrelateCycle = class {
|
|
|
1378
1495
|
const entry = correlated.get(resolved.target) || {
|
|
1379
1496
|
source: resolved.source,
|
|
1380
1497
|
priority: incomingPriority,
|
|
1498
|
+
lane: resolved.lane,
|
|
1381
1499
|
payloads: []
|
|
1382
1500
|
};
|
|
1383
1501
|
if (incomingPriority > entry.priority)
|
|
@@ -1396,10 +1514,11 @@ var CorrelateCycle = class {
|
|
|
1396
1514
|
);
|
|
1397
1515
|
if (correlated.size) {
|
|
1398
1516
|
const streams = [...correlated.entries()].map(
|
|
1399
|
-
([stream, { source, priority }]) => ({
|
|
1517
|
+
([stream, { source, priority, lane }]) => ({
|
|
1400
1518
|
stream,
|
|
1401
1519
|
source,
|
|
1402
|
-
priority
|
|
1520
|
+
priority,
|
|
1521
|
+
lane
|
|
1403
1522
|
})
|
|
1404
1523
|
);
|
|
1405
1524
|
const { subscribed } = await this.cd.subscribe(streams);
|
|
@@ -1484,904 +1603,955 @@ function computeLagLeadRatio(handled, lagging, leading) {
|
|
|
1484
1603
|
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
1485
1604
|
}
|
|
1486
1605
|
|
|
1487
|
-
// src/internal/drain
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
const fetched = await ops.fetch(active, eventLimit);
|
|
1502
|
-
const fetchMap = /* @__PURE__ */ new Map();
|
|
1503
|
-
const fetch_window_at = fetched.reduce(
|
|
1504
|
-
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
1505
|
-
0
|
|
1506
|
-
);
|
|
1507
|
-
for (const f of fetched) {
|
|
1508
|
-
const { stream, events } = f;
|
|
1509
|
-
const payloads = events.flatMap((event) => {
|
|
1510
|
-
const register = registry.events[event.name];
|
|
1511
|
-
if (!register) return [];
|
|
1512
|
-
return [...register.reactions.values()].filter((reaction) => {
|
|
1513
|
-
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1514
|
-
return resolved && resolved.target === stream;
|
|
1515
|
-
}).map((reaction) => ({ ...reaction, event }));
|
|
1516
|
-
});
|
|
1517
|
-
fetchMap.set(stream, { fetch: f, payloads });
|
|
1518
|
-
}
|
|
1519
|
-
const handled = await Promise.all(
|
|
1520
|
-
active.map((lease) => {
|
|
1521
|
-
const entry = fetchMap.get(lease.stream);
|
|
1522
|
-
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1523
|
-
const { payloads } = entry;
|
|
1524
|
-
const batchHandler = batchHandlers.get(lease.stream);
|
|
1525
|
-
if (batchHandler && payloads.length > 0) {
|
|
1526
|
-
return handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
1527
|
-
}
|
|
1528
|
-
return handle({ ...lease, at }, payloads);
|
|
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 };
|
|
1529
1618
|
})
|
|
1530
1619
|
);
|
|
1531
|
-
const acked = await ops.ack(
|
|
1532
|
-
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1533
|
-
);
|
|
1534
|
-
const blocked = await ops.block(
|
|
1535
|
-
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
1536
|
-
);
|
|
1537
|
-
return { leased, fetched, handled, acked, blocked };
|
|
1538
1620
|
}
|
|
1539
|
-
var
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
acked: [],
|
|
1543
|
-
blocked: []
|
|
1544
|
-
};
|
|
1545
|
-
var DrainController = class {
|
|
1546
|
-
constructor(deps) {
|
|
1547
|
-
this.deps = deps;
|
|
1548
|
-
}
|
|
1549
|
-
_armed = false;
|
|
1550
|
-
_locked = false;
|
|
1551
|
-
_ratio = 0.5;
|
|
1552
|
-
/**
|
|
1553
|
-
* Per-stream backoff: `stream → nextAttemptAt` (ms since epoch). Set by
|
|
1554
|
-
* `_finalize` via `HandleResult.nextAttemptAt`; cleared on successful
|
|
1555
|
-
* ack or terminal block. Lives in process memory — per-worker pacing
|
|
1556
|
-
* by design (see {@link BackoffOptions} for the multi-worker trade-off).
|
|
1557
|
-
*/
|
|
1558
|
-
_backoff = /* @__PURE__ */ new Map();
|
|
1559
|
-
/** Timer re-arming drain at the earliest pending `nextAttemptAt`. */
|
|
1560
|
-
_backoffTimer;
|
|
1561
|
-
/**
|
|
1562
|
-
* Signal that a commit (or reset / cold-start) may have produced work.
|
|
1563
|
-
* Subsequent `drain()` calls will run the pipeline; once the pipeline
|
|
1564
|
-
* settles to no-progress, the controller disarms itself.
|
|
1565
|
-
*/
|
|
1566
|
-
arm() {
|
|
1567
|
-
this._armed = true;
|
|
1568
|
-
}
|
|
1569
|
-
/** Read-only flag — true while a commit / reset is unprocessed. */
|
|
1570
|
-
get armed() {
|
|
1571
|
-
return this._armed;
|
|
1572
|
-
}
|
|
1573
|
-
/** Returns true when `stream` is currently within a backoff window. */
|
|
1574
|
-
isDeferred = (stream) => {
|
|
1575
|
-
const next = this._backoff.get(stream);
|
|
1576
|
-
return next !== void 0 && next > Date.now();
|
|
1577
|
-
};
|
|
1578
|
-
/**
|
|
1579
|
-
* Schedule the next drain re-arm at the earliest pending backoff
|
|
1580
|
-
* expiry. Called only when the backoff map is non-empty (caller guard).
|
|
1581
|
-
* Idempotent — collapses many simultaneously deferred streams into a
|
|
1582
|
-
* single timer.
|
|
1583
|
-
*/
|
|
1584
|
-
scheduleBackoffWake() {
|
|
1585
|
-
if (this._backoffTimer) clearTimeout(this._backoffTimer);
|
|
1586
|
-
let earliest = Number.POSITIVE_INFINITY;
|
|
1587
|
-
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
1588
|
-
const delay = Math.max(0, earliest - Date.now());
|
|
1589
|
-
this._backoffTimer = setTimeout(() => {
|
|
1590
|
-
this._backoffTimer = void 0;
|
|
1591
|
-
const now = Date.now();
|
|
1592
|
-
for (const [stream, at] of this._backoff) {
|
|
1593
|
-
if (at <= now) this._backoff.delete(stream);
|
|
1594
|
-
}
|
|
1595
|
-
this._armed = true;
|
|
1596
|
-
}, delay);
|
|
1597
|
-
this._backoffTimer.unref();
|
|
1598
|
-
}
|
|
1599
|
-
/** Run one drain pass. Short-circuits when not armed or already running. */
|
|
1600
|
-
async drain({
|
|
1601
|
-
streamLimit = 10,
|
|
1602
|
-
eventLimit = 10,
|
|
1603
|
-
leaseMillis = 1e4
|
|
1604
|
-
} = {}) {
|
|
1605
|
-
if (!this._armed) return EMPTY_DRAIN;
|
|
1606
|
-
if (this._locked) return EMPTY_DRAIN;
|
|
1607
|
-
try {
|
|
1608
|
-
this._locked = true;
|
|
1609
|
-
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
1610
|
-
const leading = streamLimit - lagging;
|
|
1611
|
-
const cycle = await runDrainCycle(
|
|
1612
|
-
this.deps.ops,
|
|
1613
|
-
this.deps.registry,
|
|
1614
|
-
this.deps.batchHandlers,
|
|
1615
|
-
this.deps.handle,
|
|
1616
|
-
this.deps.handleBatch,
|
|
1617
|
-
lagging,
|
|
1618
|
-
leading,
|
|
1619
|
-
eventLimit,
|
|
1620
|
-
leaseMillis,
|
|
1621
|
-
this._backoff.size > 0 ? this.isDeferred : void 0
|
|
1622
|
-
);
|
|
1623
|
-
if (!cycle) {
|
|
1624
|
-
this._armed = false;
|
|
1625
|
-
return EMPTY_DRAIN;
|
|
1626
|
-
}
|
|
1627
|
-
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1628
|
-
this._ratio = computeLagLeadRatio(handled, lagging, leading);
|
|
1629
|
-
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
1630
|
-
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
1631
|
-
for (const h of handled) {
|
|
1632
|
-
if (h.nextAttemptAt !== void 0 && !h.block) {
|
|
1633
|
-
this._backoff.set(h.lease.stream, h.nextAttemptAt);
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
if (this._backoff.size > 0) this.scheduleBackoffWake();
|
|
1637
|
-
if (acked.length) this.deps.onAcked(acked);
|
|
1638
|
-
if (blocked.length) this.deps.onBlocked(blocked);
|
|
1639
|
-
const hasErrors = handled.some(({ error }) => error);
|
|
1640
|
-
if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
|
|
1641
|
-
return { fetched, leased, acked, blocked };
|
|
1642
|
-
} catch (error) {
|
|
1643
|
-
this.deps.logger.error(error);
|
|
1644
|
-
return EMPTY_DRAIN;
|
|
1645
|
-
} finally {
|
|
1646
|
-
this._locked = false;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
};
|
|
1621
|
+
var ack = (leases) => store2().ack(leases);
|
|
1622
|
+
var block = (leases) => store2().block(leases);
|
|
1623
|
+
var subscribe = (streams) => store2().subscribe(streams);
|
|
1650
1624
|
|
|
1651
|
-
// src/internal/event-
|
|
1652
|
-
var
|
|
1653
|
-
function
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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);
|
|
1658
1642
|
}
|
|
1659
|
-
return { base: name, version: 1 };
|
|
1660
1643
|
}
|
|
1661
|
-
function
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
if (
|
|
1672
|
-
|
|
1673
|
-
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
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;
|
|
1674
1656
|
}
|
|
1675
|
-
return deprecated;
|
|
1676
1657
|
}
|
|
1677
|
-
function
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
const incomingBase = baseTypeName(incomingShape[key]);
|
|
1705
|
-
if (existingBase !== incomingBase) {
|
|
1706
|
-
throw new Error(
|
|
1707
|
-
`Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
|
|
1708
|
-
);
|
|
1709
|
-
}
|
|
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
|
+
);
|
|
1710
1685
|
}
|
|
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 }
|
|
1711
1700
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
if (existing) {
|
|
1722
|
-
mergeIntoExisting(state2, existing, states, actions, events);
|
|
1723
|
-
} else {
|
|
1724
|
-
registerNewState(state2, states, actions, events);
|
|
1701
|
+
);
|
|
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
|
+
});
|
|
1725
1710
|
}
|
|
1711
|
+
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
1726
1712
|
}
|
|
1727
|
-
function
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
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
|
+
});
|
|
1736
1733
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
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];
|
|
1742
1738
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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
|
+
);
|
|
1751
|
+
}
|
|
1749
1752
|
}
|
|
1750
|
-
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
1751
|
-
}
|
|
1752
|
-
const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
|
|
1753
|
-
const merged = {
|
|
1754
|
-
...existing,
|
|
1755
|
-
state: mergeSchemas(existing.state, state2.state, state2.name),
|
|
1756
|
-
init: mergeInits(existing.init, state2.init),
|
|
1757
|
-
events: { ...existing.events, ...state2.events },
|
|
1758
|
-
actions: { ...existing.actions, ...state2.actions },
|
|
1759
|
-
patch: mergedPatch,
|
|
1760
|
-
on: { ...existing.on, ...state2.on },
|
|
1761
|
-
given: { ...existing.given, ...state2.given },
|
|
1762
|
-
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
1763
|
-
throw new Error(
|
|
1764
|
-
`Duplicate snap strategy for state "${state2.name}"`
|
|
1765
|
-
);
|
|
1766
|
-
})() : state2.snap || existing.snap
|
|
1767
|
-
};
|
|
1768
|
-
states.set(state2.name, merged);
|
|
1769
|
-
for (const name of Object.keys(merged.actions)) {
|
|
1770
|
-
actions[name] = merged;
|
|
1771
|
-
}
|
|
1772
|
-
for (const name of Object.keys(state2.events)) {
|
|
1773
|
-
if (events[name]) continue;
|
|
1774
|
-
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
1775
1753
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
|
1792
1777
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1778
|
+
};
|
|
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);
|
|
1795
1794
|
}
|
|
1795
|
+
throw error;
|
|
1796
1796
|
}
|
|
1797
|
-
|
|
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;
|
|
1798
1824
|
}
|
|
1799
|
-
function mergeEventRegister(target, source) {
|
|
1800
|
-
for (const [eventName, sourceReg] of Object.entries(source)) {
|
|
1801
|
-
const targetReg = target[eventName];
|
|
1802
|
-
if (!targetReg) continue;
|
|
1803
|
-
for (const [name, reaction] of sourceReg.reactions) {
|
|
1804
|
-
targetReg.reactions.set(name, reaction);
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
function mergeProjection(proj, events) {
|
|
1809
|
-
for (const eventName of Object.keys(proj.events)) {
|
|
1810
|
-
const projRegister = proj.events[eventName];
|
|
1811
|
-
const existing = events[eventName];
|
|
1812
|
-
if (!existing) {
|
|
1813
|
-
events[eventName] = {
|
|
1814
|
-
schema: projRegister.schema,
|
|
1815
|
-
reactions: new Map(projRegister.reactions)
|
|
1816
|
-
};
|
|
1817
|
-
} else {
|
|
1818
|
-
for (const [name, reaction] of projRegister.reactions) {
|
|
1819
|
-
let key = name;
|
|
1820
|
-
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
1821
|
-
existing.reactions.set(key, reaction);
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
var _this_ = ({ stream }) => ({
|
|
1827
|
-
source: stream,
|
|
1828
|
-
target: stream
|
|
1829
|
-
});
|
|
1830
1825
|
|
|
1831
|
-
// src/internal/
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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
|
+
};
|
|
1847
1895
|
}
|
|
1848
|
-
if (opts.jitter) delay = delay * (0.5 + Math.random());
|
|
1849
|
-
return Math.max(0, Math.floor(delay));
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
// src/internal/reactions.ts
|
|
1853
|
-
function finalize(lease, handled, at, error, options, logger) {
|
|
1854
|
-
if (!error) return { lease, handled, at };
|
|
1855
|
-
logger.error(error);
|
|
1856
|
-
const nonRetryable = error instanceof NonRetryableError;
|
|
1857
|
-
const block2 = options.blockOnError && (nonRetryable || lease.retry >= options.maxRetries);
|
|
1858
|
-
if (block2)
|
|
1859
|
-
logger.error(
|
|
1860
|
-
nonRetryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1861
|
-
);
|
|
1862
|
-
const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
|
|
1863
1896
|
return {
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
}
|
|
1872
|
-
function buildHandle(deps) {
|
|
1873
|
-
const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
|
|
1874
|
-
return async (lease, payloads) => {
|
|
1875
|
-
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1876
|
-
const stream = lease.stream;
|
|
1877
|
-
let at = payloads.at(0).event.id;
|
|
1878
|
-
let handled = 0;
|
|
1879
|
-
if (lease.retry > 0)
|
|
1880
|
-
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1881
|
-
const scopedApp = {
|
|
1882
|
-
do: boundDo,
|
|
1883
|
-
load: boundLoad,
|
|
1884
|
-
query: boundQuery,
|
|
1885
|
-
query_array: boundQueryArray
|
|
1886
|
-
};
|
|
1887
|
-
for (const payload of payloads) {
|
|
1888
|
-
const { event, handler } = payload;
|
|
1889
|
-
scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
|
|
1890
|
-
action2,
|
|
1891
|
-
target,
|
|
1892
|
-
actionPayload,
|
|
1893
|
-
reactingTo ?? event,
|
|
1894
|
-
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
|
+
)
|
|
1895
1904
|
);
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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}`)
|
|
1908
1940
|
);
|
|
1909
1941
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1942
|
+
),
|
|
1943
|
+
tombstone: traced(tombstone, (committed, stream) => {
|
|
1944
|
+
if (committed)
|
|
1945
|
+
logger.trace(
|
|
1946
|
+
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
1947
|
+
);
|
|
1948
|
+
})
|
|
1912
1949
|
};
|
|
1913
1950
|
}
|
|
1914
|
-
function
|
|
1915
|
-
return
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
logger
|
|
1930
|
-
);
|
|
1931
|
-
} catch (error) {
|
|
1932
|
-
return finalize(lease, 0, lease.at, error, options, logger);
|
|
1933
|
-
}
|
|
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
|
+
})
|
|
1934
1966
|
};
|
|
1935
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
|
+
}
|
|
1936
2005
|
|
|
1937
|
-
// src/internal/
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
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
|
+
};
|
|
1942
2025
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
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) {
|
|
2072
|
+
this.deps = deps;
|
|
2073
|
+
}
|
|
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).
|
|
1951
2082
|
*/
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
await this.deps.init();
|
|
1966
|
-
let lastDrain;
|
|
1967
|
-
for (let i = 0; i < maxPasses; i++) {
|
|
1968
|
-
const { subscribed } = await this.deps.correlate({
|
|
1969
|
-
...correlateQuery,
|
|
1970
|
-
after: this.deps.checkpoint()
|
|
1971
|
-
});
|
|
1972
|
-
lastDrain = await this.deps.drain(drainOptions);
|
|
1973
|
-
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
1974
|
-
if (!made_progress) break;
|
|
1975
|
-
}
|
|
1976
|
-
if (lastDrain) this.deps.onSettled(lastDrain);
|
|
1977
|
-
})().catch((err) => this.deps.logger.error(err)).finally(() => {
|
|
1978
|
-
this._running = false;
|
|
1979
|
-
});
|
|
1980
|
-
}, debounceMs);
|
|
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;
|
|
1981
2096
|
}
|
|
1982
|
-
/**
|
|
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
|
+
}
|
|
2131
|
+
/**
|
|
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.
|
|
2139
|
+
*/
|
|
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();
|
|
2150
|
+
}
|
|
2151
|
+
/** Stop the per-lane worker. Idempotent. */
|
|
1983
2152
|
stop() {
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
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;
|
|
1987
2209
|
}
|
|
1988
2210
|
}
|
|
1989
2211
|
};
|
|
1990
2212
|
|
|
1991
|
-
// src/internal/
|
|
1992
|
-
var
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
limit: eventLimit
|
|
2001
|
-
});
|
|
2002
|
-
return { stream, source, at, lagging, events };
|
|
2003
|
-
})
|
|
2004
|
-
);
|
|
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 };
|
|
2005
2222
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
try {
|
|
2014
|
-
const { id, stream, name, meta, version } = snapshot.event;
|
|
2015
|
-
await store2().commit(
|
|
2016
|
-
stream,
|
|
2017
|
-
[{ name: SNAP_EVENT, data: snapshot.state }],
|
|
2018
|
-
{
|
|
2019
|
-
correlation: meta.correlation,
|
|
2020
|
-
causation: { event: { id, name, stream } }
|
|
2021
|
-
},
|
|
2022
|
-
version
|
|
2023
|
-
// IMPORTANT! - state events are committed right after the snapshot event
|
|
2024
|
-
);
|
|
2025
|
-
} catch (error) {
|
|
2026
|
-
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 }]);
|
|
2027
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);
|
|
2236
|
+
}
|
|
2237
|
+
return deprecated;
|
|
2028
2238
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
);
|
|
2037
|
-
return committed;
|
|
2038
|
-
} catch (error) {
|
|
2039
|
-
if (error instanceof ConcurrencyError) return void 0;
|
|
2040
|
-
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 };
|
|
2041
2246
|
}
|
|
2247
|
+
return highest && highest.version > target.version ? highest.name : void 0;
|
|
2042
2248
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
let
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
} else if (e.name !== TOMBSTONE_EVENT) {
|
|
2067
|
-
log().warn(
|
|
2068
|
-
`Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
|
|
2069
|
-
);
|
|
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
|
+
}
|
|
2070
2272
|
}
|
|
2071
|
-
callback?.({
|
|
2072
|
-
event,
|
|
2073
|
-
state: state2,
|
|
2074
|
-
version,
|
|
2075
|
-
patches,
|
|
2076
|
-
snaps,
|
|
2077
|
-
cache_hit,
|
|
2078
|
-
replayed
|
|
2079
|
-
});
|
|
2080
|
-
},
|
|
2081
|
-
{
|
|
2082
|
-
stream,
|
|
2083
|
-
stream_exact: true,
|
|
2084
|
-
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
2085
2273
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
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() };
|
|
2095
2298
|
}
|
|
2096
|
-
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
2097
2299
|
}
|
|
2098
|
-
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
const snapshot = await load(me, stream);
|
|
2103
|
-
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
2104
|
-
throw new StreamClosedError(stream);
|
|
2105
|
-
const expected = expectedVersion ?? snapshot.event?.version;
|
|
2106
|
-
if (me.given) {
|
|
2107
|
-
const invariants = me.given[action2] || [];
|
|
2108
|
-
invariants.forEach(({ valid, description }) => {
|
|
2109
|
-
if (!valid(snapshot.state, actor))
|
|
2110
|
-
throw new InvariantError(
|
|
2111
|
-
action2,
|
|
2112
|
-
validated,
|
|
2113
|
-
target,
|
|
2114
|
-
snapshot,
|
|
2115
|
-
description
|
|
2116
|
-
);
|
|
2117
|
-
});
|
|
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}"`);
|
|
2118
2304
|
}
|
|
2119
|
-
const
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
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}"`);
|
|
2123
2313
|
}
|
|
2124
|
-
const
|
|
2125
|
-
const
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
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;
|
|
2137
2357
|
}
|
|
2138
2358
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
stream,
|
|
2148
|
-
actor: target.actor
|
|
2149
|
-
}),
|
|
2150
|
-
causation: {
|
|
2151
|
-
action: {
|
|
2152
|
-
name: action2,
|
|
2153
|
-
...target
|
|
2154
|
-
// payload intentionally omitted: it can be large or contain PII,
|
|
2155
|
-
// and callers correlate via the correlation id when they need it.
|
|
2156
|
-
},
|
|
2157
|
-
event: reactingTo ? {
|
|
2158
|
-
id: reactingTo.id,
|
|
2159
|
-
name: reactingTo.name,
|
|
2160
|
-
stream: reactingTo.stream
|
|
2161
|
-
} : 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);
|
|
2162
2367
|
}
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
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
|
+
}
|
|
2179
2385
|
}
|
|
2180
|
-
throw error;
|
|
2181
2386
|
}
|
|
2182
|
-
let { state: state2, patches } = snapshot;
|
|
2183
|
-
const snapshots = committed.map((event) => {
|
|
2184
|
-
const p = me.patch[event.name](event, state2);
|
|
2185
|
-
state2 = (0, import_act_patch.patch)(state2, p);
|
|
2186
|
-
patches++;
|
|
2187
|
-
return {
|
|
2188
|
-
event,
|
|
2189
|
-
state: state2,
|
|
2190
|
-
version: event.version,
|
|
2191
|
-
patches,
|
|
2192
|
-
snaps: snapshot.snaps,
|
|
2193
|
-
patch: p,
|
|
2194
|
-
cache_hit: snapshot.cache_hit,
|
|
2195
|
-
replayed: snapshot.replayed
|
|
2196
|
-
};
|
|
2197
|
-
});
|
|
2198
|
-
const last = snapshots.at(-1);
|
|
2199
|
-
const snapped = me.snap?.(last);
|
|
2200
|
-
cache2().set(stream, {
|
|
2201
|
-
state: last.state,
|
|
2202
|
-
version: last.event.version,
|
|
2203
|
-
event_id: last.event.id,
|
|
2204
|
-
patches: snapped ? 0 : last.patches,
|
|
2205
|
-
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
2206
|
-
}).catch((err) => log().error(err));
|
|
2207
|
-
if (snapped) void snap(last);
|
|
2208
|
-
return snapshots;
|
|
2209
2387
|
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
var C_BLUE = "\x1B[38;5;39m";
|
|
2214
|
-
var C_ORANGE = "\x1B[38;5;208m";
|
|
2215
|
-
var C_GREEN = "\x1B[38;5;42m";
|
|
2216
|
-
var C_MAGENTA = "\x1B[38;5;165m";
|
|
2217
|
-
var C_DRAIN = "\x1B[38;5;244m";
|
|
2218
|
-
var C_HIT = "\x1B[38;5;82m";
|
|
2219
|
-
var C_MISS = "\x1B[38;5;220m";
|
|
2220
|
-
var C_RESET = "\x1B[0m";
|
|
2221
|
-
var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
|
|
2222
|
-
var drain_caption = (caption) => {
|
|
2223
|
-
const tag = `>> ${caption}`;
|
|
2224
|
-
return PRETTY ? `${C_DRAIN}${tag}${C_RESET}` : tag;
|
|
2225
|
-
};
|
|
2226
|
-
var cache_marker = (hit) => {
|
|
2227
|
-
const word = hit ? "hit" : "miss";
|
|
2228
|
-
if (!PRETTY) return word;
|
|
2229
|
-
return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
|
|
2230
|
-
};
|
|
2231
|
-
var stats_marker = (version, replayed, snaps, patches) => {
|
|
2232
|
-
const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
|
|
2233
|
-
if (!PRETTY) return text;
|
|
2234
|
-
return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
|
|
2235
|
-
};
|
|
2236
|
-
var as_of_marker = (asOf) => {
|
|
2237
|
-
if (!asOf) return "";
|
|
2238
|
-
const parts = [];
|
|
2239
|
-
if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
|
|
2240
|
-
if (asOf.created_before !== void 0)
|
|
2241
|
-
parts.push(`created_before=${asOf.created_before.toISOString()}`);
|
|
2242
|
-
if (asOf.created_after !== void 0)
|
|
2243
|
-
parts.push(`created_after=${asOf.created_after.toISOString()}`);
|
|
2244
|
-
if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
|
|
2245
|
-
return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
|
|
2246
|
-
};
|
|
2247
|
-
var traced = (inner, exit, entry) => (async (...args) => {
|
|
2248
|
-
entry?.(...args);
|
|
2249
|
-
const result = await inner(...args);
|
|
2250
|
-
exit?.(result, ...args);
|
|
2251
|
-
return result;
|
|
2388
|
+
var _this_ = ({ stream }) => ({
|
|
2389
|
+
source: stream,
|
|
2390
|
+
target: stream
|
|
2252
2391
|
});
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
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;
|
|
2270
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;
|
|
2271
2425
|
return {
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
);
|
|
2280
|
-
}),
|
|
2281
|
-
load: traced(load, (result, _me, stream, _cb, asOf) => {
|
|
2282
|
-
const stats = stats_marker(
|
|
2283
|
-
result.version,
|
|
2284
|
-
result.replayed,
|
|
2285
|
-
result.snaps,
|
|
2286
|
-
result.patches
|
|
2287
|
-
);
|
|
2288
|
-
logger.trace(
|
|
2289
|
-
es_caption(
|
|
2290
|
-
"load",
|
|
2291
|
-
C_GREEN,
|
|
2292
|
-
`${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
2293
|
-
)
|
|
2294
|
-
);
|
|
2295
|
-
}),
|
|
2296
|
-
action: traced(
|
|
2297
|
-
boundAction,
|
|
2298
|
-
(snapshots, _me, _action, target) => {
|
|
2299
|
-
const committed = snapshots.filter((s) => s.event);
|
|
2300
|
-
if (committed.length) {
|
|
2301
|
-
logger.trace(
|
|
2302
|
-
committed.map((s) => s.event.data),
|
|
2303
|
-
es_caption(
|
|
2304
|
-
"committed",
|
|
2305
|
-
C_ORANGE,
|
|
2306
|
-
`${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
|
|
2307
|
-
)
|
|
2308
|
-
);
|
|
2309
|
-
}
|
|
2310
|
-
},
|
|
2311
|
-
(_me, action2, target, payload) => {
|
|
2312
|
-
logger.trace(
|
|
2313
|
-
payload,
|
|
2314
|
-
es_caption("action", C_BLUE, `${target.stream}.${action2}`)
|
|
2315
|
-
);
|
|
2316
|
-
}
|
|
2317
|
-
),
|
|
2318
|
-
tombstone: traced(tombstone, (committed, stream) => {
|
|
2319
|
-
if (committed)
|
|
2320
|
-
logger.trace(
|
|
2321
|
-
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
2322
|
-
);
|
|
2323
|
-
})
|
|
2426
|
+
lease,
|
|
2427
|
+
handled,
|
|
2428
|
+
acked_at: at,
|
|
2429
|
+
error: error.message,
|
|
2430
|
+
block: block2,
|
|
2431
|
+
nextAttemptAt,
|
|
2432
|
+
failed_at
|
|
2324
2433
|
};
|
|
2325
2434
|
}
|
|
2326
|
-
function
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
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
|
|
2334
2449
|
};
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
}
|
|
2344
|
-
}),
|
|
2345
|
-
fetch: traced(fetch, (fetched) => {
|
|
2346
|
-
const data = Object.fromEntries(
|
|
2347
|
-
fetched.map(({ stream, source, events }) => {
|
|
2348
|
-
const key = source ? `${stream}<-${source}` : stream;
|
|
2349
|
-
const value = Object.fromEntries(
|
|
2350
|
-
events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
|
|
2351
|
-
);
|
|
2352
|
-
return [key, value];
|
|
2353
|
-
})
|
|
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
|
|
2354
2458
|
);
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
blocked.map(({ stream, at, retry, error }) => [
|
|
2369
|
-
stream,
|
|
2370
|
-
{ at, retry, error }
|
|
2371
|
-
])
|
|
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
|
|
2372
2472
|
);
|
|
2373
|
-
logger.trace(data, drain_caption("blocked"));
|
|
2374
|
-
}
|
|
2375
|
-
}),
|
|
2376
|
-
subscribe: traced(subscribe, (result, streams) => {
|
|
2377
|
-
if (result.subscribed) {
|
|
2378
|
-
const data = streams.map(({ stream }) => stream).join(" ");
|
|
2379
|
-
logger.trace(`${drain_caption("correlated")} ${data}`);
|
|
2380
2473
|
}
|
|
2381
|
-
}
|
|
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
|
+
}
|
|
2382
2498
|
};
|
|
2383
2499
|
}
|
|
2384
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
|
+
|
|
2385
2555
|
// src/act.ts
|
|
2386
2556
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
2387
2557
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
@@ -2396,11 +2566,26 @@ var Act = class {
|
|
|
2396
2566
|
* @param _states Merged map of state name → state definition
|
|
2397
2567
|
* @param batchHandlers Static-target projection batch handlers (target → handler)
|
|
2398
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.
|
|
2399
2572
|
*/
|
|
2400
|
-
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
|
|
2573
|
+
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
|
|
2401
2574
|
this.registry = registry;
|
|
2402
2575
|
this._states = _states;
|
|
2403
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
|
+
}
|
|
2404
2589
|
this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
|
|
2405
2590
|
this._correlator = options.correlator ?? defaultCorrelator;
|
|
2406
2591
|
this._es = buildEs(this._logger, this._correlator);
|
|
@@ -2413,19 +2598,44 @@ var Act = class {
|
|
|
2413
2598
|
boundQueryArray: this._bound_query_array
|
|
2414
2599
|
});
|
|
2415
2600
|
this._handle_batch = buildHandleBatch(this._logger);
|
|
2416
|
-
const {
|
|
2601
|
+
const {
|
|
2602
|
+
staticTargets,
|
|
2603
|
+
hasDynamicResolvers,
|
|
2604
|
+
reactiveEvents,
|
|
2605
|
+
eventToState,
|
|
2606
|
+
eventToLanes
|
|
2607
|
+
} = classifyRegistry(this.registry, this._states);
|
|
2417
2608
|
this._reactive_events = reactiveEvents;
|
|
2418
2609
|
this._event_to_state = eventToState;
|
|
2419
|
-
this.
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
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
|
+
}
|
|
2429
2639
|
this._correlate = new CorrelateCycle(
|
|
2430
2640
|
this.registry,
|
|
2431
2641
|
staticTargets,
|
|
@@ -2434,7 +2644,7 @@ var Act = class {
|
|
|
2434
2644
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2435
2645
|
// Cold start: assume drain is needed (historical events may need processing)
|
|
2436
2646
|
() => {
|
|
2437
|
-
if (this._reactive_events.size > 0) this.
|
|
2647
|
+
if (this._reactive_events.size > 0) this._armAll();
|
|
2438
2648
|
}
|
|
2439
2649
|
);
|
|
2440
2650
|
this._settle = new SettleLoop(
|
|
@@ -2454,8 +2664,8 @@ var Act = class {
|
|
|
2454
2664
|
_emitter = new import_node_events.default();
|
|
2455
2665
|
/** Event names with at least one registered reaction (computed at build time) */
|
|
2456
2666
|
_reactive_events;
|
|
2457
|
-
/**
|
|
2458
|
-
|
|
2667
|
+
/** One DrainController per active lane, keyed by lane name. */
|
|
2668
|
+
_drain_controllers;
|
|
2459
2669
|
/** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
|
|
2460
2670
|
_correlate;
|
|
2461
2671
|
/** Debounced correlate→drain catch-up loop. */
|
|
@@ -2509,6 +2719,14 @@ var Act = class {
|
|
|
2509
2719
|
* set when seeding a `restart` snapshot in multi-state apps.
|
|
2510
2720
|
*/
|
|
2511
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;
|
|
2512
2730
|
/** Logger resolved at construction time (after user port configuration) */
|
|
2513
2731
|
_logger = log();
|
|
2514
2732
|
/** Wraps a public-method body so internal `store()`/`cache()` resolve to the
|
|
@@ -2532,6 +2750,12 @@ var Act = class {
|
|
|
2532
2750
|
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
2533
2751
|
_handle;
|
|
2534
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
|
+
}
|
|
2535
2759
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
2536
2760
|
_shutdown_promise;
|
|
2537
2761
|
/**
|
|
@@ -2550,6 +2774,7 @@ var Act = class {
|
|
|
2550
2774
|
this._emitter.removeAllListeners();
|
|
2551
2775
|
this.stop_correlations();
|
|
2552
2776
|
this.stop_settling();
|
|
2777
|
+
for (const c of this._drain_controllers.values()) c.stop();
|
|
2553
2778
|
const disposer = await this._notify_disposer;
|
|
2554
2779
|
if (disposer) await disposer();
|
|
2555
2780
|
})();
|
|
@@ -2569,13 +2794,10 @@ var Act = class {
|
|
|
2569
2794
|
return await s.notify((notification) => {
|
|
2570
2795
|
try {
|
|
2571
2796
|
this.emit("notified", notification);
|
|
2572
|
-
const
|
|
2573
|
-
(e) =>
|
|
2797
|
+
const armed = this._armForEventNames(
|
|
2798
|
+
notification.events.map((e) => e.name)
|
|
2574
2799
|
);
|
|
2575
|
-
if (
|
|
2576
|
-
this._drain.arm();
|
|
2577
|
-
this._settle.schedule({ debounceMs: 0 });
|
|
2578
|
-
}
|
|
2800
|
+
if (armed) this._settle.schedule({ debounceMs: 0 });
|
|
2579
2801
|
} catch (err) {
|
|
2580
2802
|
this._logger.error(err, "notified handler threw");
|
|
2581
2803
|
}
|
|
@@ -2676,14 +2898,10 @@ var Act = class {
|
|
|
2676
2898
|
reactingTo,
|
|
2677
2899
|
skipValidation
|
|
2678
2900
|
);
|
|
2679
|
-
if (this._reactive_events.size > 0)
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
break;
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2901
|
+
if (this._reactive_events.size > 0)
|
|
2902
|
+
this._armForEventNames(
|
|
2903
|
+
snapshots.map((s) => s.event.name)
|
|
2904
|
+
);
|
|
2687
2905
|
this.emit("committed", snapshots);
|
|
2688
2906
|
return snapshots;
|
|
2689
2907
|
});
|
|
@@ -2830,7 +3048,59 @@ var Act = class {
|
|
|
2830
3048
|
* @see {@link start_correlations} for automatic correlation
|
|
2831
3049
|
*/
|
|
2832
3050
|
async drain(options = {}) {
|
|
2833
|
-
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 };
|
|
2834
3104
|
}
|
|
2835
3105
|
/**
|
|
2836
3106
|
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
@@ -3001,7 +3271,7 @@ var Act = class {
|
|
|
3001
3271
|
async reset(input) {
|
|
3002
3272
|
return this._scoped(async () => {
|
|
3003
3273
|
const count = await store2().reset(input);
|
|
3004
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
3274
|
+
if (count > 0 && this._reactive_events.size > 0) this._armAll();
|
|
3005
3275
|
return count;
|
|
3006
3276
|
});
|
|
3007
3277
|
}
|
|
@@ -3035,7 +3305,7 @@ var Act = class {
|
|
|
3035
3305
|
async unblock(input) {
|
|
3036
3306
|
return this._scoped(async () => {
|
|
3037
3307
|
const count = await store2().unblock(input);
|
|
3038
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
3308
|
+
if (count > 0 && this._reactive_events.size > 0) this._armAll();
|
|
3039
3309
|
return count;
|
|
3040
3310
|
});
|
|
3041
3311
|
}
|
|
@@ -3208,6 +3478,22 @@ function registerBatchHandler(proj, batchHandlers) {
|
|
|
3208
3478
|
}
|
|
3209
3479
|
batchHandlers.set(proj.target, proj.batchHandler);
|
|
3210
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
|
+
}
|
|
3211
3497
|
function act() {
|
|
3212
3498
|
const states = /* @__PURE__ */ new Map();
|
|
3213
3499
|
const registry = {
|
|
@@ -3216,6 +3502,7 @@ function act() {
|
|
|
3216
3502
|
};
|
|
3217
3503
|
const pendingProjections = [];
|
|
3218
3504
|
const batchHandlers = /* @__PURE__ */ new Map();
|
|
3505
|
+
const lanes = [];
|
|
3219
3506
|
let _built = false;
|
|
3220
3507
|
const finalizeDeprecations = () => {
|
|
3221
3508
|
const deprecationSummary = [];
|
|
@@ -3262,6 +3549,18 @@ function act() {
|
|
|
3262
3549
|
}
|
|
3263
3550
|
mergeEventRegister(registry.events, input.events);
|
|
3264
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
|
+
}
|
|
3265
3564
|
return builder;
|
|
3266
3565
|
},
|
|
3267
3566
|
withProjection: (proj) => {
|
|
@@ -3270,6 +3569,14 @@ function act() {
|
|
|
3270
3569
|
return builder;
|
|
3271
3570
|
},
|
|
3272
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
|
+
},
|
|
3273
3580
|
on: (event) => ({
|
|
3274
3581
|
do: (handler, options) => {
|
|
3275
3582
|
const reaction = {
|
|
@@ -3301,13 +3608,15 @@ function act() {
|
|
|
3301
3608
|
registerBatchHandler(proj, batchHandlers);
|
|
3302
3609
|
}
|
|
3303
3610
|
finalizeDeprecations();
|
|
3611
|
+
validateLaneReferences(registry, lanes);
|
|
3304
3612
|
_built = true;
|
|
3305
3613
|
}
|
|
3306
3614
|
return new Act(
|
|
3307
3615
|
registry,
|
|
3308
3616
|
states,
|
|
3309
3617
|
batchHandlers,
|
|
3310
|
-
options
|
|
3618
|
+
options,
|
|
3619
|
+
lanes
|
|
3311
3620
|
);
|
|
3312
3621
|
},
|
|
3313
3622
|
events: registry.events
|
|
@@ -3388,6 +3697,7 @@ function slice() {
|
|
|
3388
3697
|
const actions = {};
|
|
3389
3698
|
const events = {};
|
|
3390
3699
|
const projections = [];
|
|
3700
|
+
const lanes = [];
|
|
3391
3701
|
const builder = {
|
|
3392
3702
|
withState: (state2) => {
|
|
3393
3703
|
registerState(state2, states, actions, events);
|
|
@@ -3397,6 +3707,14 @@ function slice() {
|
|
|
3397
3707
|
projections.push(proj);
|
|
3398
3708
|
return builder;
|
|
3399
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
|
+
},
|
|
3400
3718
|
on: (event) => ({
|
|
3401
3719
|
do: (handler, options) => {
|
|
3402
3720
|
const reaction = {
|
|
@@ -3425,7 +3743,8 @@ function slice() {
|
|
|
3425
3743
|
_tag: "Slice",
|
|
3426
3744
|
states,
|
|
3427
3745
|
events,
|
|
3428
|
-
projections
|
|
3746
|
+
projections,
|
|
3747
|
+
lanes
|
|
3429
3748
|
}),
|
|
3430
3749
|
events
|
|
3431
3750
|
};
|
|
@@ -3519,6 +3838,7 @@ function action_builder(state2) {
|
|
|
3519
3838
|
CommittedMetaSchema,
|
|
3520
3839
|
ConcurrencyError,
|
|
3521
3840
|
ConsoleLogger,
|
|
3841
|
+
DEFAULT_LANE,
|
|
3522
3842
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
3523
3843
|
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
3524
3844
|
Environments,
|