@rotorsoft/act 0.44.0 → 0.46.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 +93 -5
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/console-logger.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +4 -1
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts +33 -9
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/slice-builder.d.ts +23 -8
- package/dist/@types/builders/slice-builder.d.ts.map +1 -1
- package/dist/@types/internal/audit.d.ts +95 -0
- package/dist/@types/internal/audit.d.ts.map +1 -0
- 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 +4 -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/audit.d.ts +126 -0
- package/dist/@types/types/audit.d.ts.map +1 -0
- package/dist/@types/types/index.d.ts +1 -0
- package/dist/@types/types/index.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +9 -2
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +20 -2
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/{chunk-VMX7RPTC.js → chunk-TZWDSNSN.js} +1 -1
- package/dist/{chunk-VMX7RPTC.js.map → chunk-TZWDSNSN.js.map} +1 -1
- package/dist/{chunk-LKRNWD7C.js → chunk-VC6MSVC3.js} +47 -12
- package/dist/chunk-VC6MSVC3.js.map +1 -0
- package/dist/index.cjs +1584 -886
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1538 -874
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +52 -18
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +11 -11
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-LKRNWD7C.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
CommittedMetaSchema: () => CommittedMetaSchema,
|
|
37
37
|
ConcurrencyError: () => ConcurrencyError,
|
|
38
38
|
ConsoleLogger: () => ConsoleLogger,
|
|
39
|
+
DEFAULT_LANE: () => DEFAULT_LANE,
|
|
39
40
|
DEFAULT_MAX_SUBSCRIBED_STREAMS: () => DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
40
41
|
DEFAULT_SETTLE_DEBOUNCE_MS: () => DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
41
42
|
Environments: () => Environments,
|
|
@@ -145,6 +146,12 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
145
146
|
if (typeof objOrMsg === "string") {
|
|
146
147
|
message = objOrMsg;
|
|
147
148
|
obj = {};
|
|
149
|
+
} else if (objOrMsg instanceof Error) {
|
|
150
|
+
message = msg ?? objOrMsg.message;
|
|
151
|
+
obj = {
|
|
152
|
+
error: { message: objOrMsg.message, name: objOrMsg.name },
|
|
153
|
+
stack: objOrMsg.stack
|
|
154
|
+
};
|
|
148
155
|
} else if (objOrMsg !== null && typeof objOrMsg === "object") {
|
|
149
156
|
message = msg;
|
|
150
157
|
obj = { ...objOrMsg };
|
|
@@ -175,6 +182,9 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
175
182
|
let data;
|
|
176
183
|
if (typeof objOrMsg === "string") {
|
|
177
184
|
message = objOrMsg;
|
|
185
|
+
} else if (objOrMsg instanceof Error) {
|
|
186
|
+
message = msg ?? objOrMsg.message;
|
|
187
|
+
data = objOrMsg.stack;
|
|
178
188
|
} else {
|
|
179
189
|
message = msg ?? "";
|
|
180
190
|
if (objOrMsg !== void 0 && objOrMsg !== null) {
|
|
@@ -484,10 +494,11 @@ async function sleep(ms) {
|
|
|
484
494
|
|
|
485
495
|
// src/adapters/in-memory-store.ts
|
|
486
496
|
var InMemoryStream = class {
|
|
487
|
-
constructor(stream, source, priority = 0) {
|
|
497
|
+
constructor(stream, source, priority = 0, lane = DEFAULT_LANE) {
|
|
488
498
|
this.stream = stream;
|
|
489
499
|
this.source = source;
|
|
490
500
|
this._priority = priority;
|
|
501
|
+
this._lane = lane;
|
|
491
502
|
}
|
|
492
503
|
_at = -1;
|
|
493
504
|
_retry = -1;
|
|
@@ -496,9 +507,17 @@ var InMemoryStream = class {
|
|
|
496
507
|
_leased_by = void 0;
|
|
497
508
|
_leased_until = void 0;
|
|
498
509
|
_priority = 0;
|
|
510
|
+
_lane = DEFAULT_LANE;
|
|
499
511
|
get priority() {
|
|
500
512
|
return this._priority;
|
|
501
513
|
}
|
|
514
|
+
get lane() {
|
|
515
|
+
return this._lane;
|
|
516
|
+
}
|
|
517
|
+
/** Replace on every subscribe — current builder config wins on restart. */
|
|
518
|
+
set lane(value) {
|
|
519
|
+
this._lane = value;
|
|
520
|
+
}
|
|
502
521
|
/**
|
|
503
522
|
* Bump the priority via {@link subscribe}: keeps the maximum across
|
|
504
523
|
* reactions so the highest-priority registrant wins.
|
|
@@ -552,7 +571,8 @@ var InMemoryStream = class {
|
|
|
552
571
|
at: lease.at,
|
|
553
572
|
by: lease.by,
|
|
554
573
|
retry: this._retry,
|
|
555
|
-
lagging: lease.lagging
|
|
574
|
+
lagging: lease.lagging,
|
|
575
|
+
lane: this._lane
|
|
556
576
|
};
|
|
557
577
|
}
|
|
558
578
|
/**
|
|
@@ -571,7 +591,8 @@ var InMemoryStream = class {
|
|
|
571
591
|
at: this._at,
|
|
572
592
|
by: lease.by,
|
|
573
593
|
retry: this._retry,
|
|
574
|
-
lagging: lease.lagging
|
|
594
|
+
lagging: lease.lagging,
|
|
595
|
+
lane: this._lane
|
|
575
596
|
};
|
|
576
597
|
}
|
|
577
598
|
}
|
|
@@ -591,7 +612,8 @@ var InMemoryStream = class {
|
|
|
591
612
|
by: this._leased_by,
|
|
592
613
|
retry: this._retry,
|
|
593
614
|
error: this._error,
|
|
594
|
-
lagging: lease.lagging
|
|
615
|
+
lagging: lease.lagging,
|
|
616
|
+
lane: this._lane
|
|
595
617
|
};
|
|
596
618
|
}
|
|
597
619
|
}
|
|
@@ -767,7 +789,7 @@ var InMemoryStore = class {
|
|
|
767
789
|
* @param millis - Lease duration in milliseconds.
|
|
768
790
|
* @returns Granted leases.
|
|
769
791
|
*/
|
|
770
|
-
async claim(lagging, leading, by, millis) {
|
|
792
|
+
async claim(lagging, leading, by, millis, lane) {
|
|
771
793
|
await sleep();
|
|
772
794
|
const sourceRegex = /* @__PURE__ */ new Map();
|
|
773
795
|
const getRegex = (source) => {
|
|
@@ -788,7 +810,7 @@ var InMemoryStore = class {
|
|
|
788
810
|
return false;
|
|
789
811
|
};
|
|
790
812
|
const available = [...this._streams.values()].filter(
|
|
791
|
-
(s) => s.is_available && hasWork(s)
|
|
813
|
+
(s) => s.is_available && hasWork(s) && (lane === void 0 || s.lane === lane)
|
|
792
814
|
);
|
|
793
815
|
const lag = available.sort((a, b) => b.priority - a.priority || a.at - b.at).slice(0, lagging).map((s) => ({
|
|
794
816
|
stream: s.stream,
|
|
@@ -824,12 +846,21 @@ var InMemoryStore = class {
|
|
|
824
846
|
async subscribe(streams) {
|
|
825
847
|
await sleep();
|
|
826
848
|
let subscribed = 0;
|
|
827
|
-
for (const {
|
|
849
|
+
for (const {
|
|
850
|
+
stream,
|
|
851
|
+
source,
|
|
852
|
+
priority = 0,
|
|
853
|
+
lane = DEFAULT_LANE
|
|
854
|
+
} of streams) {
|
|
828
855
|
const existing = this._streams.get(stream);
|
|
829
856
|
if (existing) {
|
|
830
857
|
existing.bumpPriority(priority);
|
|
858
|
+
existing.lane = lane;
|
|
831
859
|
} else {
|
|
832
|
-
this._streams.set(
|
|
860
|
+
this._streams.set(
|
|
861
|
+
stream,
|
|
862
|
+
new InMemoryStream(stream, source, priority, lane)
|
|
863
|
+
);
|
|
833
864
|
subscribed++;
|
|
834
865
|
}
|
|
835
866
|
}
|
|
@@ -876,6 +907,7 @@ var InMemoryStore = class {
|
|
|
876
907
|
}
|
|
877
908
|
if (filter.blocked !== void 0 && s.blocked !== filter.blocked)
|
|
878
909
|
return false;
|
|
910
|
+
if (filter.lane !== void 0 && s.lane !== filter.lane) return false;
|
|
879
911
|
return true;
|
|
880
912
|
};
|
|
881
913
|
}
|
|
@@ -986,6 +1018,7 @@ var InMemoryStore = class {
|
|
|
986
1018
|
continue;
|
|
987
1019
|
}
|
|
988
1020
|
if (blocked !== void 0 && s.blocked !== blocked) continue;
|
|
1021
|
+
if (query?.lane !== void 0 && s.lane !== query.lane) continue;
|
|
989
1022
|
callback({
|
|
990
1023
|
stream: s.stream,
|
|
991
1024
|
source: s.source,
|
|
@@ -995,7 +1028,8 @@ var InMemoryStore = class {
|
|
|
995
1028
|
error: s.error,
|
|
996
1029
|
priority: s.priority,
|
|
997
1030
|
leased_by: s.leased_by,
|
|
998
|
-
leased_until: s.leased_until
|
|
1031
|
+
leased_until: s.leased_until,
|
|
1032
|
+
lane: s.lane
|
|
999
1033
|
});
|
|
1000
1034
|
count++;
|
|
1001
1035
|
if (count >= limit) break;
|
|
@@ -1178,6 +1212,7 @@ function dispose(disposer) {
|
|
|
1178
1212
|
}
|
|
1179
1213
|
var SNAP_EVENT = "__snapshot__";
|
|
1180
1214
|
var TOMBSTONE_EVENT = "__tombstone__";
|
|
1215
|
+
var DEFAULT_LANE = "default";
|
|
1181
1216
|
|
|
1182
1217
|
// src/signals.ts
|
|
1183
1218
|
process.once("SIGINT", async (arg) => {
|
|
@@ -1200,24 +1235,459 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
1200
1235
|
// src/act.ts
|
|
1201
1236
|
var import_node_events = __toESM(require("events"), 1);
|
|
1202
1237
|
|
|
1238
|
+
// src/internal/event-versions.ts
|
|
1239
|
+
var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
|
|
1240
|
+
function parse(name) {
|
|
1241
|
+
const m = name.match(VERSION_SUFFIX);
|
|
1242
|
+
if (m) {
|
|
1243
|
+
const v = Number.parseInt(m[2], 10);
|
|
1244
|
+
if (v >= 2) return { base: m[1], version: v };
|
|
1245
|
+
}
|
|
1246
|
+
return { base: name, version: 1 };
|
|
1247
|
+
}
|
|
1248
|
+
function deprecatedEventNames(names) {
|
|
1249
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1250
|
+
for (const name of names) {
|
|
1251
|
+
const { base, version } = parse(name);
|
|
1252
|
+
const list = groups.get(base);
|
|
1253
|
+
if (list) list.push({ version, name });
|
|
1254
|
+
else groups.set(base, [{ version, name }]);
|
|
1255
|
+
}
|
|
1256
|
+
const deprecated = /* @__PURE__ */ new Set();
|
|
1257
|
+
for (const list of groups.values()) {
|
|
1258
|
+
if (list.length < 2) continue;
|
|
1259
|
+
list.sort((a, b) => b.version - a.version);
|
|
1260
|
+
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
1261
|
+
}
|
|
1262
|
+
return deprecated;
|
|
1263
|
+
}
|
|
1264
|
+
function currentVersionOf(deprecatedName, allNames) {
|
|
1265
|
+
const target = parse(deprecatedName);
|
|
1266
|
+
let highest;
|
|
1267
|
+
for (const name of allNames) {
|
|
1268
|
+
const { base, version } = parse(name);
|
|
1269
|
+
if (base !== target.base) continue;
|
|
1270
|
+
if (!highest || version > highest.version) highest = { version, name };
|
|
1271
|
+
}
|
|
1272
|
+
return highest && highest.version > target.version ? highest.name : void 0;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/internal/audit.ts
|
|
1276
|
+
var DEFAULTS = {
|
|
1277
|
+
idle_days: 90,
|
|
1278
|
+
restart_min: 1e4,
|
|
1279
|
+
stuck_minutes: 30,
|
|
1280
|
+
deprecated_min: 0.1,
|
|
1281
|
+
drift_min: 500,
|
|
1282
|
+
near_block: 3
|
|
1283
|
+
};
|
|
1284
|
+
var ALL_CATEGORIES = [
|
|
1285
|
+
"schema",
|
|
1286
|
+
"close-candidate",
|
|
1287
|
+
"restart-candidate",
|
|
1288
|
+
"deprecated-load",
|
|
1289
|
+
"reaction-health",
|
|
1290
|
+
"snapshot-drift",
|
|
1291
|
+
"routing-health",
|
|
1292
|
+
"correlation-gaps",
|
|
1293
|
+
"clock-anomalies"
|
|
1294
|
+
];
|
|
1295
|
+
async function* audit(deps, categories, options = {}) {
|
|
1296
|
+
const requested = new Set(categories ?? [...ALL_CATEGORIES]);
|
|
1297
|
+
const orderedCategories = ALL_CATEGORIES.filter((c) => requested.has(c));
|
|
1298
|
+
const passes = orderedCategories.map(
|
|
1299
|
+
(c) => PASS_FACTORIES[c](deps, options)
|
|
1300
|
+
);
|
|
1301
|
+
const needStats = passes.some((p) => p.onStat !== void 0);
|
|
1302
|
+
const needStreams = passes.some((p) => p.onStream !== void 0);
|
|
1303
|
+
const needEvents = passes.some((p) => p.onEvent !== void 0);
|
|
1304
|
+
if (needStats) {
|
|
1305
|
+
const stats = await deps.store().query_stats({}, { count: true, names: true });
|
|
1306
|
+
for (const [stream, s] of stats) {
|
|
1307
|
+
for (const p of passes) p.onStat?.(stream, s);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (needStreams) {
|
|
1311
|
+
await deps.store().query_streams((pos) => {
|
|
1312
|
+
for (const p of passes) p.onStream?.(pos);
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
if (needEvents) {
|
|
1316
|
+
await deps.store().query((event) => {
|
|
1317
|
+
for (const p of passes) p.onEvent?.(event);
|
|
1318
|
+
}, options.query);
|
|
1319
|
+
}
|
|
1320
|
+
for (const p of passes) await p.finalize?.(deps);
|
|
1321
|
+
for (const p of passes) {
|
|
1322
|
+
for (const f of p.drain()) yield f;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
var makeSchemaPass = (deps) => {
|
|
1326
|
+
const findings = [];
|
|
1327
|
+
return {
|
|
1328
|
+
category: "schema",
|
|
1329
|
+
onEvent(event) {
|
|
1330
|
+
const name = String(event.name);
|
|
1331
|
+
const state2 = deps.event_to_state.get(name);
|
|
1332
|
+
if (!state2) {
|
|
1333
|
+
if (name.startsWith("__")) return;
|
|
1334
|
+
findings.push({
|
|
1335
|
+
category: "schema",
|
|
1336
|
+
stream: event.stream,
|
|
1337
|
+
event_id: event.id,
|
|
1338
|
+
name,
|
|
1339
|
+
reason: "unknown_event_name"
|
|
1340
|
+
});
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const schema = state2.events[name];
|
|
1344
|
+
const parsed = schema.safeParse(event.data);
|
|
1345
|
+
if (!parsed.success) {
|
|
1346
|
+
findings.push({
|
|
1347
|
+
category: "schema",
|
|
1348
|
+
stream: event.stream,
|
|
1349
|
+
event_id: event.id,
|
|
1350
|
+
name,
|
|
1351
|
+
reason: "schema_validation_failed",
|
|
1352
|
+
zod_error: parsed.error
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
drain: () => findings
|
|
1357
|
+
};
|
|
1358
|
+
};
|
|
1359
|
+
var makeDeprecatedLoadPass = (deps, options) => {
|
|
1360
|
+
const share_min = options.thresholds?.deprecated_min ?? DEFAULTS.deprecated_min;
|
|
1361
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1362
|
+
const perStream = /* @__PURE__ */ new Map();
|
|
1363
|
+
return {
|
|
1364
|
+
category: "deprecated-load",
|
|
1365
|
+
onStat(stream, { names }) {
|
|
1366
|
+
for (const [name, count] of Object.entries(names)) {
|
|
1367
|
+
totals.set(name, (totals.get(name) ?? 0) + count);
|
|
1368
|
+
let m = perStream.get(name);
|
|
1369
|
+
if (!m) {
|
|
1370
|
+
m = /* @__PURE__ */ new Map();
|
|
1371
|
+
perStream.set(name, m);
|
|
1372
|
+
}
|
|
1373
|
+
m.set(stream, count);
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1376
|
+
drain() {
|
|
1377
|
+
const findings = [];
|
|
1378
|
+
const grand = [...totals.values()].reduce((s, n) => s + n, 0);
|
|
1379
|
+
if (grand === 0) return findings;
|
|
1380
|
+
const deprecated = deprecatedEventNames(deps.known_events);
|
|
1381
|
+
const sorted = [...deprecated].map((name) => ({ name, count: totals.get(name) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
1382
|
+
for (const { name, count } of sorted) {
|
|
1383
|
+
if (count === 0) continue;
|
|
1384
|
+
if (count / grand < share_min) continue;
|
|
1385
|
+
const currentVersion = currentVersionOf(name, deps.known_events);
|
|
1386
|
+
const topStreams = [...perStream.get(name).entries()].map(([stream, c]) => ({ stream, count: c })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1387
|
+
findings.push({
|
|
1388
|
+
category: "deprecated-load",
|
|
1389
|
+
name,
|
|
1390
|
+
current_version: currentVersion,
|
|
1391
|
+
total: count,
|
|
1392
|
+
top_streams: topStreams
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
return findings;
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
};
|
|
1399
|
+
var makeCloseCandidatePass = (deps, options) => {
|
|
1400
|
+
const idle_days = options.thresholds?.idle_days ?? DEFAULTS.idle_days;
|
|
1401
|
+
const terminal_events = new Set(options.thresholds?.terminal_events ?? []);
|
|
1402
|
+
const idle_cutoff = Date.now() - idle_days * 24 * 60 * 60 * 1e3;
|
|
1403
|
+
const findings = [];
|
|
1404
|
+
return {
|
|
1405
|
+
category: "close-candidate",
|
|
1406
|
+
onStat(stream, { head }) {
|
|
1407
|
+
const head_name = String(head.name);
|
|
1408
|
+
if (head_name.startsWith("__")) return;
|
|
1409
|
+
const head_time = head.created.getTime();
|
|
1410
|
+
const is_idle = head_time < idle_cutoff;
|
|
1411
|
+
const is_terminal = terminal_events.has(head_name);
|
|
1412
|
+
if (!is_idle && !is_terminal) return;
|
|
1413
|
+
findings.push({
|
|
1414
|
+
category: "close-candidate",
|
|
1415
|
+
stream,
|
|
1416
|
+
last_event_at: head.created.toISOString(),
|
|
1417
|
+
reason: is_terminal ? "terminal" : "idle",
|
|
1418
|
+
idle_days: is_idle ? Math.floor((Date.now() - head_time) / (24 * 60 * 60 * 1e3)) : void 0,
|
|
1419
|
+
restart_supported: restartIsSupported(deps, head_name)
|
|
1420
|
+
});
|
|
1421
|
+
},
|
|
1422
|
+
drain: () => findings
|
|
1423
|
+
};
|
|
1424
|
+
};
|
|
1425
|
+
var makeRestartCandidatePass = (deps, options) => {
|
|
1426
|
+
const threshold = options.thresholds?.restart_min ?? DEFAULTS.restart_min;
|
|
1427
|
+
const findings = [];
|
|
1428
|
+
return {
|
|
1429
|
+
category: "restart-candidate",
|
|
1430
|
+
onStat(stream, { head, count, names }) {
|
|
1431
|
+
if (count < threshold) return;
|
|
1432
|
+
const head_name = String(head.name);
|
|
1433
|
+
if (head_name.startsWith("__")) return;
|
|
1434
|
+
if (!restartIsSupported(deps, head_name)) return;
|
|
1435
|
+
findings.push({
|
|
1436
|
+
category: "restart-candidate",
|
|
1437
|
+
stream,
|
|
1438
|
+
count,
|
|
1439
|
+
// names map is sparse — `__snapshot__` key absent when the
|
|
1440
|
+
// stream has never been snapshotted (a common case for the
|
|
1441
|
+
// restart-candidate signal).
|
|
1442
|
+
snaps: names["__snapshot__"] ?? 0
|
|
1443
|
+
});
|
|
1444
|
+
},
|
|
1445
|
+
drain: () => findings
|
|
1446
|
+
};
|
|
1447
|
+
};
|
|
1448
|
+
var makeReactionHealthPass = (_deps, options) => {
|
|
1449
|
+
const near_block = options.thresholds?.near_block ?? DEFAULTS.near_block;
|
|
1450
|
+
const stuck_minutes = options.thresholds?.stuck_minutes ?? DEFAULTS.stuck_minutes;
|
|
1451
|
+
const stuck_cutoff = Date.now() - stuck_minutes * 60 * 1e3;
|
|
1452
|
+
const findings = [];
|
|
1453
|
+
return {
|
|
1454
|
+
category: "reaction-health",
|
|
1455
|
+
onStream(p) {
|
|
1456
|
+
if (p.blocked) {
|
|
1457
|
+
findings.push({
|
|
1458
|
+
category: "reaction-health",
|
|
1459
|
+
stream: p.stream,
|
|
1460
|
+
status: "blocked",
|
|
1461
|
+
retry: p.retry,
|
|
1462
|
+
reason: p.error || "blocked without recorded error"
|
|
1463
|
+
});
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (p.retry >= near_block) {
|
|
1467
|
+
findings.push({
|
|
1468
|
+
category: "reaction-health",
|
|
1469
|
+
stream: p.stream,
|
|
1470
|
+
status: "near-block",
|
|
1471
|
+
retry: p.retry,
|
|
1472
|
+
reason: `retry ${p.retry} \u2265 near-block threshold ${near_block}`
|
|
1473
|
+
});
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (p.leased_by && p.leased_until && p.leased_until.getTime() < stuck_cutoff) {
|
|
1477
|
+
const minutes = Math.floor(
|
|
1478
|
+
(Date.now() - p.leased_until.getTime()) / (60 * 1e3)
|
|
1479
|
+
);
|
|
1480
|
+
findings.push({
|
|
1481
|
+
category: "reaction-health",
|
|
1482
|
+
stream: p.stream,
|
|
1483
|
+
status: "stuck-backoff",
|
|
1484
|
+
retry: p.retry,
|
|
1485
|
+
reason: `lease expired ${minutes}m ago without release`
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
drain: () => findings
|
|
1490
|
+
};
|
|
1491
|
+
};
|
|
1492
|
+
var makeSnapshotDriftPass = (deps, options) => {
|
|
1493
|
+
const drift_min = options.thresholds?.drift_min ?? DEFAULTS.drift_min;
|
|
1494
|
+
const candidates = [];
|
|
1495
|
+
const findings = [];
|
|
1496
|
+
return {
|
|
1497
|
+
category: "snapshot-drift",
|
|
1498
|
+
onStat(stream, { head, count, names }) {
|
|
1499
|
+
if (!restartIsSupported(deps, String(head.name))) return;
|
|
1500
|
+
if (count < drift_min) return;
|
|
1501
|
+
candidates.push({
|
|
1502
|
+
stream,
|
|
1503
|
+
total: count,
|
|
1504
|
+
snaps: names["__snapshot__"] ?? 0
|
|
1505
|
+
});
|
|
1506
|
+
},
|
|
1507
|
+
async finalize(deps2) {
|
|
1508
|
+
for (const { stream, total, snaps } of candidates) {
|
|
1509
|
+
let events_since_snap = total;
|
|
1510
|
+
let snap_at;
|
|
1511
|
+
if (snaps > 0) {
|
|
1512
|
+
const collected = [];
|
|
1513
|
+
await deps2.store().query(
|
|
1514
|
+
(e) => {
|
|
1515
|
+
collected.push({ id: e.id });
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
stream,
|
|
1519
|
+
stream_exact: true,
|
|
1520
|
+
names: ["__snapshot__"],
|
|
1521
|
+
backward: true,
|
|
1522
|
+
limit: 1,
|
|
1523
|
+
with_snaps: true
|
|
1524
|
+
}
|
|
1525
|
+
);
|
|
1526
|
+
snap_at = collected[0].id;
|
|
1527
|
+
let after = 0;
|
|
1528
|
+
await deps2.store().query(
|
|
1529
|
+
() => {
|
|
1530
|
+
after++;
|
|
1531
|
+
},
|
|
1532
|
+
{ stream, stream_exact: true, after: snap_at }
|
|
1533
|
+
);
|
|
1534
|
+
events_since_snap = after;
|
|
1535
|
+
}
|
|
1536
|
+
if (events_since_snap < drift_min) continue;
|
|
1537
|
+
findings.push({
|
|
1538
|
+
category: "snapshot-drift",
|
|
1539
|
+
stream,
|
|
1540
|
+
events_since_snap,
|
|
1541
|
+
snap_at
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
drain: () => findings
|
|
1546
|
+
};
|
|
1547
|
+
};
|
|
1548
|
+
var makeRoutingHealthPass = (deps) => {
|
|
1549
|
+
const findings = [];
|
|
1550
|
+
const seenEventNames = /* @__PURE__ */ new Set();
|
|
1551
|
+
return {
|
|
1552
|
+
category: "routing-health",
|
|
1553
|
+
onStream(p) {
|
|
1554
|
+
if (!p.lane) return;
|
|
1555
|
+
if (deps.declared_lanes.has(p.lane)) return;
|
|
1556
|
+
findings.push({
|
|
1557
|
+
category: "routing-health",
|
|
1558
|
+
stream: p.stream,
|
|
1559
|
+
reason: "unknown-lane",
|
|
1560
|
+
lane: p.lane
|
|
1561
|
+
});
|
|
1562
|
+
},
|
|
1563
|
+
onStat(_stream, { names }) {
|
|
1564
|
+
for (const name of Object.keys(names)) {
|
|
1565
|
+
seenEventNames.add(name);
|
|
1566
|
+
}
|
|
1567
|
+
},
|
|
1568
|
+
finalize() {
|
|
1569
|
+
for (const name of seenEventNames) {
|
|
1570
|
+
if (name.startsWith("__")) continue;
|
|
1571
|
+
if (deps.routed_events.has(name)) continue;
|
|
1572
|
+
findings.push({
|
|
1573
|
+
category: "routing-health",
|
|
1574
|
+
stream: "*",
|
|
1575
|
+
reason: "unrouted"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
return Promise.resolve();
|
|
1579
|
+
},
|
|
1580
|
+
drain: () => findings
|
|
1581
|
+
};
|
|
1582
|
+
};
|
|
1583
|
+
var makeCorrelationGapsPass = () => {
|
|
1584
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1585
|
+
const checks = [];
|
|
1586
|
+
return {
|
|
1587
|
+
category: "correlation-gaps",
|
|
1588
|
+
onEvent(e) {
|
|
1589
|
+
seenIds.add(e.id);
|
|
1590
|
+
const causation = e.meta?.causation;
|
|
1591
|
+
const parentId = causation?.event?.id;
|
|
1592
|
+
if (parentId !== void 0) {
|
|
1593
|
+
checks.push({ stream: e.stream, id: e.id, parentId });
|
|
1594
|
+
}
|
|
1595
|
+
},
|
|
1596
|
+
drain() {
|
|
1597
|
+
const findings = [];
|
|
1598
|
+
for (const { stream, id, parentId } of checks) {
|
|
1599
|
+
if (!seenIds.has(parentId)) {
|
|
1600
|
+
findings.push({
|
|
1601
|
+
category: "correlation-gaps",
|
|
1602
|
+
stream,
|
|
1603
|
+
event_id: id,
|
|
1604
|
+
reason: "orphan-parent"
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return findings;
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
};
|
|
1612
|
+
var makeClockAnomaliesPass = () => {
|
|
1613
|
+
const findings = [];
|
|
1614
|
+
const lastPerStream = /* @__PURE__ */ new Map();
|
|
1615
|
+
return {
|
|
1616
|
+
category: "clock-anomalies",
|
|
1617
|
+
onEvent(e) {
|
|
1618
|
+
const created = e.created.getTime();
|
|
1619
|
+
if (created > Date.now()) {
|
|
1620
|
+
findings.push({
|
|
1621
|
+
category: "clock-anomalies",
|
|
1622
|
+
stream: e.stream,
|
|
1623
|
+
event_id: e.id,
|
|
1624
|
+
reason: "future-created"
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
const prev = lastPerStream.get(e.stream);
|
|
1628
|
+
if (prev !== void 0 && created < prev) {
|
|
1629
|
+
findings.push({
|
|
1630
|
+
category: "clock-anomalies",
|
|
1631
|
+
stream: e.stream,
|
|
1632
|
+
event_id: e.id,
|
|
1633
|
+
reason: "out-of-order"
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
lastPerStream.set(e.stream, created);
|
|
1637
|
+
},
|
|
1638
|
+
drain: () => findings
|
|
1639
|
+
};
|
|
1640
|
+
};
|
|
1641
|
+
function restartIsSupported(deps, headEventName) {
|
|
1642
|
+
const state2 = deps.event_to_state.get(headEventName);
|
|
1643
|
+
return state2?.snap !== void 0;
|
|
1644
|
+
}
|
|
1645
|
+
var PASS_FACTORIES = {
|
|
1646
|
+
schema: makeSchemaPass,
|
|
1647
|
+
"deprecated-load": makeDeprecatedLoadPass,
|
|
1648
|
+
"close-candidate": makeCloseCandidatePass,
|
|
1649
|
+
"restart-candidate": makeRestartCandidatePass,
|
|
1650
|
+
"reaction-health": makeReactionHealthPass,
|
|
1651
|
+
"snapshot-drift": makeSnapshotDriftPass,
|
|
1652
|
+
"routing-health": makeRoutingHealthPass,
|
|
1653
|
+
"correlation-gaps": makeCorrelationGapsPass,
|
|
1654
|
+
"clock-anomalies": makeClockAnomaliesPass
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1203
1657
|
// src/internal/build-classify.ts
|
|
1658
|
+
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
1204
1659
|
function classifyRegistry(registry, states) {
|
|
1205
1660
|
const statics = /* @__PURE__ */ new Map();
|
|
1206
1661
|
const reactiveEvents = /* @__PURE__ */ new Set();
|
|
1662
|
+
const eventToLanes = /* @__PURE__ */ new Map();
|
|
1207
1663
|
let hasDynamicResolvers = false;
|
|
1208
1664
|
for (const [name, register] of Object.entries(registry.events)) {
|
|
1209
1665
|
if (register.reactions.size > 0) reactiveEvents.add(name);
|
|
1210
1666
|
for (const reaction of register.reactions.values()) {
|
|
1211
1667
|
if (typeof reaction.resolver === "function") {
|
|
1212
1668
|
hasDynamicResolvers = true;
|
|
1669
|
+
eventToLanes.set(name, ALL_LANES);
|
|
1213
1670
|
} else {
|
|
1214
|
-
const { target, source, priority = 0 } = reaction.resolver;
|
|
1671
|
+
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
1672
|
+
const lane_name = lane ?? "default";
|
|
1673
|
+
const existing_lanes = eventToLanes.get(name);
|
|
1674
|
+
if (existing_lanes !== ALL_LANES) {
|
|
1675
|
+
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
1676
|
+
set.add(lane_name);
|
|
1677
|
+
eventToLanes.set(name, set);
|
|
1678
|
+
}
|
|
1215
1679
|
const key = `${target}|${source ?? ""}`;
|
|
1216
1680
|
const existing = statics.get(key);
|
|
1217
1681
|
if (!existing) {
|
|
1218
|
-
statics.set(key, { stream: target, source, priority });
|
|
1219
|
-
} else
|
|
1220
|
-
|
|
1682
|
+
statics.set(key, { stream: target, source, priority, lane });
|
|
1683
|
+
} else {
|
|
1684
|
+
if ((existing.lane ?? void 0) !== (lane ?? void 0))
|
|
1685
|
+
throw new Error(
|
|
1686
|
+
`Stream "${target}" has conflicting lane assignments ("${existing.lane ?? "default"}" vs "${lane ?? "default"}")`
|
|
1687
|
+
);
|
|
1688
|
+
if (priority > existing.priority) {
|
|
1689
|
+
statics.set(key, { ...existing, priority });
|
|
1690
|
+
}
|
|
1221
1691
|
}
|
|
1222
1692
|
}
|
|
1223
1693
|
}
|
|
@@ -1232,7 +1702,8 @@ function classifyRegistry(registry, states) {
|
|
|
1232
1702
|
staticTargets: [...statics.values()],
|
|
1233
1703
|
hasDynamicResolvers,
|
|
1234
1704
|
reactiveEvents,
|
|
1235
|
-
eventToState
|
|
1705
|
+
eventToState,
|
|
1706
|
+
eventToLanes
|
|
1236
1707
|
};
|
|
1237
1708
|
}
|
|
1238
1709
|
|
|
@@ -1443,6 +1914,7 @@ var CorrelateCycle = class {
|
|
|
1443
1914
|
const entry = correlated.get(resolved.target) || {
|
|
1444
1915
|
source: resolved.source,
|
|
1445
1916
|
priority: incomingPriority,
|
|
1917
|
+
lane: resolved.lane,
|
|
1446
1918
|
payloads: []
|
|
1447
1919
|
};
|
|
1448
1920
|
if (incomingPriority > entry.priority)
|
|
@@ -1461,10 +1933,11 @@ var CorrelateCycle = class {
|
|
|
1461
1933
|
);
|
|
1462
1934
|
if (correlated.size) {
|
|
1463
1935
|
const streams = [...correlated.entries()].map(
|
|
1464
|
-
([stream, { source, priority }]) => ({
|
|
1936
|
+
([stream, { source, priority, lane }]) => ({
|
|
1465
1937
|
stream,
|
|
1466
1938
|
source,
|
|
1467
|
-
priority
|
|
1939
|
+
priority,
|
|
1940
|
+
lane
|
|
1468
1941
|
})
|
|
1469
1942
|
);
|
|
1470
1943
|
const { subscribed } = await this.cd.subscribe(streams);
|
|
@@ -1549,904 +2022,918 @@ function computeLagLeadRatio(handled, lagging, leading) {
|
|
|
1549
2022
|
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
1550
2023
|
}
|
|
1551
2024
|
|
|
1552
|
-
// src/internal/drain
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
const fetched = await ops.fetch(active, eventLimit);
|
|
1567
|
-
const fetchMap = /* @__PURE__ */ new Map();
|
|
1568
|
-
const fetch_window_at = fetched.reduce(
|
|
1569
|
-
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
1570
|
-
0
|
|
1571
|
-
);
|
|
1572
|
-
for (const f of fetched) {
|
|
1573
|
-
const { stream, events } = f;
|
|
1574
|
-
const payloads = events.flatMap((event) => {
|
|
1575
|
-
const register = registry.events[event.name];
|
|
1576
|
-
if (!register) return [];
|
|
1577
|
-
return [...register.reactions.values()].filter((reaction) => {
|
|
1578
|
-
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1579
|
-
return resolved && resolved.target === stream;
|
|
1580
|
-
}).map((reaction) => ({ ...reaction, event }));
|
|
1581
|
-
});
|
|
1582
|
-
fetchMap.set(stream, { fetch: f, payloads });
|
|
1583
|
-
}
|
|
1584
|
-
const handled = await Promise.all(
|
|
1585
|
-
active.map((lease) => {
|
|
1586
|
-
const entry = fetchMap.get(lease.stream);
|
|
1587
|
-
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1588
|
-
const { payloads } = entry;
|
|
1589
|
-
const batchHandler = batchHandlers.get(lease.stream);
|
|
1590
|
-
if (batchHandler && payloads.length > 0) {
|
|
1591
|
-
return handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
1592
|
-
}
|
|
1593
|
-
return handle({ ...lease, at }, payloads);
|
|
2025
|
+
// src/internal/drain.ts
|
|
2026
|
+
var claim = (lagging, leading, by, millis, lane) => store2().claim(lagging, leading, by, millis, lane);
|
|
2027
|
+
async function fetch(leased, eventLimit) {
|
|
2028
|
+
return Promise.all(
|
|
2029
|
+
leased.map(async ({ stream, source, at, lagging }) => {
|
|
2030
|
+
const events = [];
|
|
2031
|
+
await store2().query((e) => events.push(e), {
|
|
2032
|
+
stream: source,
|
|
2033
|
+
after: at,
|
|
2034
|
+
limit: eventLimit
|
|
2035
|
+
});
|
|
2036
|
+
return { stream, source, at, lagging, events };
|
|
1594
2037
|
})
|
|
1595
2038
|
);
|
|
1596
|
-
const acked = await ops.ack(
|
|
1597
|
-
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1598
|
-
);
|
|
1599
|
-
const blocked = await ops.block(
|
|
1600
|
-
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
1601
|
-
);
|
|
1602
|
-
return { leased, fetched, handled, acked, blocked };
|
|
1603
2039
|
}
|
|
1604
|
-
var
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
_backoffTimer;
|
|
1626
|
-
/**
|
|
1627
|
-
* Signal that a commit (or reset / cold-start) may have produced work.
|
|
1628
|
-
* Subsequent `drain()` calls will run the pipeline; once the pipeline
|
|
1629
|
-
* settles to no-progress, the controller disarms itself.
|
|
1630
|
-
*/
|
|
1631
|
-
arm() {
|
|
1632
|
-
this._armed = true;
|
|
2040
|
+
var ack = (leases) => store2().ack(leases);
|
|
2041
|
+
var block = (leases) => store2().block(leases);
|
|
2042
|
+
var subscribe = (streams) => store2().subscribe(streams);
|
|
2043
|
+
|
|
2044
|
+
// src/internal/event-sourcing.ts
|
|
2045
|
+
var import_act_patch = require("@rotorsoft/act-patch");
|
|
2046
|
+
async function snap(snapshot) {
|
|
2047
|
+
try {
|
|
2048
|
+
const { id, stream, name, meta, version } = snapshot.event;
|
|
2049
|
+
await store2().commit(
|
|
2050
|
+
stream,
|
|
2051
|
+
[{ name: SNAP_EVENT, data: snapshot.state }],
|
|
2052
|
+
{
|
|
2053
|
+
correlation: meta.correlation,
|
|
2054
|
+
causation: { event: { id, name, stream } }
|
|
2055
|
+
},
|
|
2056
|
+
version
|
|
2057
|
+
// IMPORTANT! - state events are committed right after the snapshot event
|
|
2058
|
+
);
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
log().error(error);
|
|
1633
2061
|
}
|
|
1634
|
-
/** Read-only flag — true while a commit / reset is unprocessed. */
|
|
1635
|
-
get armed() {
|
|
1636
|
-
return this._armed;
|
|
1637
|
-
}
|
|
1638
|
-
/** Returns true when `stream` is currently within a backoff window. */
|
|
1639
|
-
isDeferred = (stream) => {
|
|
1640
|
-
const next = this._backoff.get(stream);
|
|
1641
|
-
return next !== void 0 && next > Date.now();
|
|
1642
|
-
};
|
|
1643
|
-
/**
|
|
1644
|
-
* Schedule the next drain re-arm at the earliest pending backoff
|
|
1645
|
-
* expiry. Called only when the backoff map is non-empty (caller guard).
|
|
1646
|
-
* Idempotent — collapses many simultaneously deferred streams into a
|
|
1647
|
-
* single timer.
|
|
1648
|
-
*/
|
|
1649
|
-
scheduleBackoffWake() {
|
|
1650
|
-
if (this._backoffTimer) clearTimeout(this._backoffTimer);
|
|
1651
|
-
let earliest = Number.POSITIVE_INFINITY;
|
|
1652
|
-
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
1653
|
-
const delay = Math.max(0, earliest - Date.now());
|
|
1654
|
-
this._backoffTimer = setTimeout(() => {
|
|
1655
|
-
this._backoffTimer = void 0;
|
|
1656
|
-
const now = Date.now();
|
|
1657
|
-
for (const [stream, at] of this._backoff) {
|
|
1658
|
-
if (at <= now) this._backoff.delete(stream);
|
|
1659
|
-
}
|
|
1660
|
-
this._armed = true;
|
|
1661
|
-
}, delay);
|
|
1662
|
-
this._backoffTimer.unref();
|
|
1663
|
-
}
|
|
1664
|
-
/** Run one drain pass. Short-circuits when not armed or already running. */
|
|
1665
|
-
async drain({
|
|
1666
|
-
streamLimit = 10,
|
|
1667
|
-
eventLimit = 10,
|
|
1668
|
-
leaseMillis = 1e4
|
|
1669
|
-
} = {}) {
|
|
1670
|
-
if (!this._armed) return EMPTY_DRAIN;
|
|
1671
|
-
if (this._locked) return EMPTY_DRAIN;
|
|
1672
|
-
try {
|
|
1673
|
-
this._locked = true;
|
|
1674
|
-
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
1675
|
-
const leading = streamLimit - lagging;
|
|
1676
|
-
const cycle = await runDrainCycle(
|
|
1677
|
-
this.deps.ops,
|
|
1678
|
-
this.deps.registry,
|
|
1679
|
-
this.deps.batchHandlers,
|
|
1680
|
-
this.deps.handle,
|
|
1681
|
-
this.deps.handleBatch,
|
|
1682
|
-
lagging,
|
|
1683
|
-
leading,
|
|
1684
|
-
eventLimit,
|
|
1685
|
-
leaseMillis,
|
|
1686
|
-
this._backoff.size > 0 ? this.isDeferred : void 0
|
|
1687
|
-
);
|
|
1688
|
-
if (!cycle) {
|
|
1689
|
-
this._armed = false;
|
|
1690
|
-
return EMPTY_DRAIN;
|
|
1691
|
-
}
|
|
1692
|
-
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1693
|
-
this._ratio = computeLagLeadRatio(handled, lagging, leading);
|
|
1694
|
-
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
1695
|
-
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
1696
|
-
for (const h of handled) {
|
|
1697
|
-
if (h.nextAttemptAt !== void 0 && !h.block) {
|
|
1698
|
-
this._backoff.set(h.lease.stream, h.nextAttemptAt);
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
if (this._backoff.size > 0) this.scheduleBackoffWake();
|
|
1702
|
-
if (acked.length) this.deps.onAcked(acked);
|
|
1703
|
-
if (blocked.length) this.deps.onBlocked(blocked);
|
|
1704
|
-
const hasErrors = handled.some(({ error }) => error);
|
|
1705
|
-
if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
|
|
1706
|
-
return { fetched, leased, acked, blocked };
|
|
1707
|
-
} catch (error) {
|
|
1708
|
-
this.deps.logger.error(error);
|
|
1709
|
-
return EMPTY_DRAIN;
|
|
1710
|
-
} finally {
|
|
1711
|
-
this._locked = false;
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
};
|
|
1715
|
-
|
|
1716
|
-
// src/internal/event-versions.ts
|
|
1717
|
-
var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
|
|
1718
|
-
function parse(name) {
|
|
1719
|
-
const m = name.match(VERSION_SUFFIX);
|
|
1720
|
-
if (m) {
|
|
1721
|
-
const v = Number.parseInt(m[2], 10);
|
|
1722
|
-
if (v >= 2) return { base: m[1], version: v };
|
|
1723
|
-
}
|
|
1724
|
-
return { base: name, version: 1 };
|
|
1725
|
-
}
|
|
1726
|
-
function deprecatedEventNames(names) {
|
|
1727
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1728
|
-
for (const name of names) {
|
|
1729
|
-
const { base, version } = parse(name);
|
|
1730
|
-
const list = groups.get(base);
|
|
1731
|
-
if (list) list.push({ version, name });
|
|
1732
|
-
else groups.set(base, [{ version, name }]);
|
|
1733
|
-
}
|
|
1734
|
-
const deprecated = /* @__PURE__ */ new Set();
|
|
1735
|
-
for (const list of groups.values()) {
|
|
1736
|
-
if (list.length < 2) continue;
|
|
1737
|
-
list.sort((a, b) => b.version - a.version);
|
|
1738
|
-
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
1739
|
-
}
|
|
1740
|
-
return deprecated;
|
|
1741
|
-
}
|
|
1742
|
-
function currentVersionOf(deprecatedName, allNames) {
|
|
1743
|
-
const target = parse(deprecatedName);
|
|
1744
|
-
let highest;
|
|
1745
|
-
for (const name of allNames) {
|
|
1746
|
-
const { base, version } = parse(name);
|
|
1747
|
-
if (base !== target.base) continue;
|
|
1748
|
-
if (!highest || version > highest.version) highest = { version, name };
|
|
1749
|
-
}
|
|
1750
|
-
return highest && highest.version > target.version ? highest.name : void 0;
|
|
1751
2062
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2063
|
+
async function tombstone(stream, expectedVersion, correlation) {
|
|
2064
|
+
try {
|
|
2065
|
+
const [committed] = await store2().commit(
|
|
2066
|
+
stream,
|
|
2067
|
+
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
2068
|
+
{ correlation, causation: {} },
|
|
2069
|
+
expectedVersion
|
|
2070
|
+
);
|
|
2071
|
+
return committed;
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
if (error instanceof ConcurrencyError) return void 0;
|
|
2074
|
+
throw error;
|
|
1759
2075
|
}
|
|
1760
|
-
return t.constructor.name;
|
|
1761
2076
|
}
|
|
1762
|
-
function
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2077
|
+
async function load(me, stream, callback, asOf) {
|
|
2078
|
+
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
2079
|
+
const cached = timeTravel ? void 0 : await cache2().get(stream);
|
|
2080
|
+
const cache_hit = !!cached;
|
|
2081
|
+
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
2082
|
+
let patches = cached?.patches ?? 0;
|
|
2083
|
+
let snaps = cached?.snaps ?? 0;
|
|
2084
|
+
let version = cached?.version ?? -1;
|
|
2085
|
+
let replayed = 0;
|
|
2086
|
+
let event;
|
|
2087
|
+
await store2().query(
|
|
2088
|
+
(e) => {
|
|
2089
|
+
event = e;
|
|
2090
|
+
version = e.version;
|
|
2091
|
+
if (e.name === SNAP_EVENT) {
|
|
2092
|
+
state2 = e.data;
|
|
2093
|
+
snaps++;
|
|
2094
|
+
patches = 0;
|
|
2095
|
+
replayed++;
|
|
2096
|
+
} else if (me.patch[e.name]) {
|
|
2097
|
+
state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](event, state2));
|
|
2098
|
+
patches++;
|
|
2099
|
+
replayed++;
|
|
2100
|
+
} else if (e.name !== TOMBSTONE_EVENT) {
|
|
2101
|
+
log().warn(
|
|
2102
|
+
`Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
|
|
2103
|
+
);
|
|
1775
2104
|
}
|
|
2105
|
+
callback?.({
|
|
2106
|
+
event,
|
|
2107
|
+
state: state2,
|
|
2108
|
+
version,
|
|
2109
|
+
patches,
|
|
2110
|
+
snaps,
|
|
2111
|
+
cache_hit,
|
|
2112
|
+
replayed
|
|
2113
|
+
});
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
stream,
|
|
2117
|
+
stream_exact: true,
|
|
2118
|
+
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
1776
2119
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
if (existing) {
|
|
1787
|
-
mergeIntoExisting(state2, existing, states, actions, events);
|
|
1788
|
-
} else {
|
|
1789
|
-
registerNewState(state2, states, actions, events);
|
|
2120
|
+
);
|
|
2121
|
+
if (replayed > 0 && !timeTravel && event) {
|
|
2122
|
+
await cache2().set(stream, {
|
|
2123
|
+
state: state2,
|
|
2124
|
+
version,
|
|
2125
|
+
event_id: event.id,
|
|
2126
|
+
patches,
|
|
2127
|
+
snaps
|
|
2128
|
+
});
|
|
1790
2129
|
}
|
|
2130
|
+
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
1791
2131
|
}
|
|
1792
|
-
function
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2132
|
+
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = defaultCorrelator) {
|
|
2133
|
+
const { stream, expectedVersion, actor } = target;
|
|
2134
|
+
if (!stream) throw new Error("Missing target stream");
|
|
2135
|
+
const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
2136
|
+
const snapshot = await load(me, stream);
|
|
2137
|
+
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
2138
|
+
throw new StreamClosedError(stream);
|
|
2139
|
+
const expected = expectedVersion ?? snapshot.event?.version;
|
|
2140
|
+
if (me.given) {
|
|
2141
|
+
const invariants = me.given[action2] || [];
|
|
2142
|
+
invariants.forEach(({ valid, description }) => {
|
|
2143
|
+
if (!valid(snapshot.state, actor))
|
|
2144
|
+
throw new InvariantError(
|
|
2145
|
+
action2,
|
|
2146
|
+
validated,
|
|
2147
|
+
target,
|
|
2148
|
+
snapshot,
|
|
2149
|
+
description
|
|
2150
|
+
);
|
|
2151
|
+
});
|
|
1797
2152
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2153
|
+
const result = me.on[action2](validated, snapshot, target);
|
|
2154
|
+
if (!result) return [snapshot];
|
|
2155
|
+
if (Array.isArray(result) && result.length === 0) {
|
|
2156
|
+
return [snapshot];
|
|
1801
2157
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2158
|
+
const tuples = Array.isArray(result[0]) ? result : [result];
|
|
2159
|
+
const deprecated = me._deprecated;
|
|
2160
|
+
if (deprecated && deprecated.size > 0) {
|
|
2161
|
+
const me_ = me;
|
|
2162
|
+
const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
|
|
2163
|
+
for (const [name] of tuples) {
|
|
2164
|
+
const evt = name;
|
|
2165
|
+
if (deprecated.has(evt) && !warned.has(evt)) {
|
|
2166
|
+
warned.add(evt);
|
|
2167
|
+
log().warn(
|
|
2168
|
+
`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)`
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
1807
2172
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2173
|
+
const emitted = tuples.map(([name, data]) => ({
|
|
2174
|
+
name,
|
|
2175
|
+
data: skipValidation ? data : validate(name, data, me.events[name])
|
|
2176
|
+
}));
|
|
2177
|
+
const meta = {
|
|
2178
|
+
correlation: reactingTo?.meta.correlation || correlator({
|
|
2179
|
+
action: action2,
|
|
2180
|
+
state: me.name,
|
|
2181
|
+
stream,
|
|
2182
|
+
actor: target.actor
|
|
2183
|
+
}),
|
|
2184
|
+
causation: {
|
|
2185
|
+
action: {
|
|
2186
|
+
name: action2,
|
|
2187
|
+
...target
|
|
2188
|
+
// payload intentionally omitted: it can be large or contain PII,
|
|
2189
|
+
// and callers correlate via the correlation id when they need it.
|
|
2190
|
+
},
|
|
2191
|
+
event: reactingTo ? {
|
|
2192
|
+
id: reactingTo.id,
|
|
2193
|
+
name: reactingTo.name,
|
|
2194
|
+
stream: reactingTo.stream
|
|
2195
|
+
} : void 0
|
|
1814
2196
|
}
|
|
1815
|
-
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
1816
|
-
}
|
|
1817
|
-
const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
|
|
1818
|
-
const merged = {
|
|
1819
|
-
...existing,
|
|
1820
|
-
state: mergeSchemas(existing.state, state2.state, state2.name),
|
|
1821
|
-
init: mergeInits(existing.init, state2.init),
|
|
1822
|
-
events: { ...existing.events, ...state2.events },
|
|
1823
|
-
actions: { ...existing.actions, ...state2.actions },
|
|
1824
|
-
patch: mergedPatch,
|
|
1825
|
-
on: { ...existing.on, ...state2.on },
|
|
1826
|
-
given: { ...existing.given, ...state2.given },
|
|
1827
|
-
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
1828
|
-
throw new Error(
|
|
1829
|
-
`Duplicate snap strategy for state "${state2.name}"`
|
|
1830
|
-
);
|
|
1831
|
-
})() : state2.snap || existing.snap
|
|
1832
2197
|
};
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
merged[name] = incomingP;
|
|
1849
|
-
continue;
|
|
1850
|
-
}
|
|
1851
|
-
const existingIsDefault = existingP._passthrough;
|
|
1852
|
-
const incomingIsDefault = incomingP._passthrough;
|
|
1853
|
-
if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
|
|
1854
|
-
throw new Error(
|
|
1855
|
-
`Duplicate custom patch for event "${name}" in state "${stateName}"`
|
|
1856
|
-
);
|
|
1857
|
-
}
|
|
1858
|
-
if (existingIsDefault && !incomingIsDefault) {
|
|
1859
|
-
merged[name] = incomingP;
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
return merged;
|
|
1863
|
-
}
|
|
1864
|
-
function mergeEventRegister(target, source) {
|
|
1865
|
-
for (const [eventName, sourceReg] of Object.entries(source)) {
|
|
1866
|
-
const targetReg = target[eventName];
|
|
1867
|
-
if (!targetReg) continue;
|
|
1868
|
-
for (const [name, reaction] of sourceReg.reactions) {
|
|
1869
|
-
targetReg.reactions.set(name, reaction);
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
function mergeProjection(proj, events) {
|
|
1874
|
-
for (const eventName of Object.keys(proj.events)) {
|
|
1875
|
-
const projRegister = proj.events[eventName];
|
|
1876
|
-
const existing = events[eventName];
|
|
1877
|
-
if (!existing) {
|
|
1878
|
-
events[eventName] = {
|
|
1879
|
-
schema: projRegister.schema,
|
|
1880
|
-
reactions: new Map(projRegister.reactions)
|
|
1881
|
-
};
|
|
1882
|
-
} else {
|
|
1883
|
-
for (const [name, reaction] of projRegister.reactions) {
|
|
1884
|
-
let key = name;
|
|
1885
|
-
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
1886
|
-
existing.reactions.set(key, reaction);
|
|
1887
|
-
}
|
|
2198
|
+
let committed;
|
|
2199
|
+
try {
|
|
2200
|
+
committed = await store2().commit(
|
|
2201
|
+
stream,
|
|
2202
|
+
emitted,
|
|
2203
|
+
meta,
|
|
2204
|
+
// Reactions skip optimistic concurrency: they always append against the
|
|
2205
|
+
// current head. Stream leasing already serializes concurrent reactions,
|
|
2206
|
+
// and forcing version checks here would turn ordinary catch-up into
|
|
2207
|
+
// spurious retries.
|
|
2208
|
+
reactingTo ? void 0 : expected
|
|
2209
|
+
);
|
|
2210
|
+
} catch (error) {
|
|
2211
|
+
if (error instanceof ConcurrencyError) {
|
|
2212
|
+
await cache2().invalidate(stream);
|
|
1888
2213
|
}
|
|
2214
|
+
throw error;
|
|
1889
2215
|
}
|
|
2216
|
+
let { state: state2, patches } = snapshot;
|
|
2217
|
+
const snapshots = committed.map((event) => {
|
|
2218
|
+
const p = me.patch[event.name](event, state2);
|
|
2219
|
+
state2 = (0, import_act_patch.patch)(state2, p);
|
|
2220
|
+
patches++;
|
|
2221
|
+
return {
|
|
2222
|
+
event,
|
|
2223
|
+
state: state2,
|
|
2224
|
+
version: event.version,
|
|
2225
|
+
patches,
|
|
2226
|
+
snaps: snapshot.snaps,
|
|
2227
|
+
patch: p,
|
|
2228
|
+
cache_hit: snapshot.cache_hit,
|
|
2229
|
+
replayed: snapshot.replayed
|
|
2230
|
+
};
|
|
2231
|
+
});
|
|
2232
|
+
const last = snapshots.at(-1);
|
|
2233
|
+
const snapped = me.snap?.(last);
|
|
2234
|
+
cache2().set(stream, {
|
|
2235
|
+
state: last.state,
|
|
2236
|
+
version: last.event.version,
|
|
2237
|
+
event_id: last.event.id,
|
|
2238
|
+
patches: snapped ? 0 : last.patches,
|
|
2239
|
+
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
2240
|
+
}).catch((err) => log().error(err));
|
|
2241
|
+
if (snapped) void snap(last);
|
|
2242
|
+
return snapshots;
|
|
1890
2243
|
}
|
|
1891
|
-
var _this_ = ({ stream }) => ({
|
|
1892
|
-
source: stream,
|
|
1893
|
-
target: stream
|
|
1894
|
-
});
|
|
1895
2244
|
|
|
1896
|
-
// src/internal/
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2245
|
+
// src/internal/tracing.ts
|
|
2246
|
+
var PRETTY = config().env !== "production";
|
|
2247
|
+
var C_BLUE = "\x1B[38;5;39m";
|
|
2248
|
+
var C_ORANGE = "\x1B[38;5;208m";
|
|
2249
|
+
var C_GREEN = "\x1B[38;5;42m";
|
|
2250
|
+
var C_MAGENTA = "\x1B[38;5;165m";
|
|
2251
|
+
var C_DRAIN = "\x1B[38;5;244m";
|
|
2252
|
+
var C_HIT = "\x1B[38;5;82m";
|
|
2253
|
+
var C_MISS = "\x1B[38;5;220m";
|
|
2254
|
+
var C_RESET = "\x1B[0m";
|
|
2255
|
+
var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
|
|
2256
|
+
var C_LANE = "\x1B[38;5;183m";
|
|
2257
|
+
var C_DIM = "\x1B[38;5;240m";
|
|
2258
|
+
var C_ERR = "\x1B[38;5;196m";
|
|
2259
|
+
var C_STREAM = "\x1B[38;5;226m";
|
|
2260
|
+
var dim = (text) => PRETTY ? `${C_DIM}${text}${C_RESET}` : text;
|
|
2261
|
+
var hue = (color, text) => PRETTY ? `${color}${text}${C_RESET}` : text;
|
|
2262
|
+
var drain_caption = (caption, lane) => {
|
|
2263
|
+
const showLane = lane && lane !== "default";
|
|
2264
|
+
if (PRETTY) {
|
|
2265
|
+
const tag = `${C_DRAIN}>> ${caption}${C_RESET}`;
|
|
2266
|
+
return showLane ? `${tag} ${C_LANE}${lane}${C_RESET}` : tag;
|
|
2267
|
+
}
|
|
2268
|
+
return showLane ? `>> ${caption} ${lane}` : `>> ${caption}`;
|
|
2269
|
+
};
|
|
2270
|
+
var cache_marker = (hit) => {
|
|
2271
|
+
const word = hit ? "hit" : "miss";
|
|
2272
|
+
if (!PRETTY) return word;
|
|
2273
|
+
return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
|
|
2274
|
+
};
|
|
2275
|
+
var stats_marker = (version, replayed, snaps, patches) => {
|
|
2276
|
+
const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
|
|
2277
|
+
if (!PRETTY) return text;
|
|
2278
|
+
return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
|
|
2279
|
+
};
|
|
2280
|
+
var as_of_marker = (asOf) => {
|
|
2281
|
+
if (!asOf) return "";
|
|
2282
|
+
const parts = [];
|
|
2283
|
+
if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
|
|
2284
|
+
if (asOf.created_before !== void 0)
|
|
2285
|
+
parts.push(`created_before=${asOf.created_before.toISOString()}`);
|
|
2286
|
+
if (asOf.created_after !== void 0)
|
|
2287
|
+
parts.push(`created_after=${asOf.created_after.toISOString()}`);
|
|
2288
|
+
if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
|
|
2289
|
+
return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
|
|
2290
|
+
};
|
|
2291
|
+
var traced = (inner, exit, entry) => (async (...args) => {
|
|
2292
|
+
entry?.(...args);
|
|
2293
|
+
const result = await inner(...args);
|
|
2294
|
+
exit?.(result, ...args);
|
|
2295
|
+
return result;
|
|
2296
|
+
});
|
|
2297
|
+
function buildEs(logger, correlator = defaultCorrelator) {
|
|
2298
|
+
const boundAction = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
|
|
2299
|
+
me,
|
|
2300
|
+
actionName,
|
|
2301
|
+
target,
|
|
2302
|
+
payload,
|
|
2303
|
+
reactingTo,
|
|
2304
|
+
skipValidation,
|
|
2305
|
+
correlator
|
|
2306
|
+
);
|
|
2307
|
+
if (logger.level !== "trace") {
|
|
2308
|
+
return {
|
|
2309
|
+
snap,
|
|
2310
|
+
load,
|
|
2311
|
+
action: boundAction,
|
|
2312
|
+
tombstone
|
|
2313
|
+
};
|
|
1912
2314
|
}
|
|
1913
|
-
if (opts.jitter) delay = delay * (0.5 + Math.random());
|
|
1914
|
-
return Math.max(0, Math.floor(delay));
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
// src/internal/reactions.ts
|
|
1918
|
-
function finalize(lease, handled, at, error, options, logger) {
|
|
1919
|
-
if (!error) return { lease, handled, at };
|
|
1920
|
-
logger.error(error);
|
|
1921
|
-
const nonRetryable = error instanceof NonRetryableError;
|
|
1922
|
-
const block2 = options.blockOnError && (nonRetryable || lease.retry >= options.maxRetries);
|
|
1923
|
-
if (block2)
|
|
1924
|
-
logger.error(
|
|
1925
|
-
nonRetryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1926
|
-
);
|
|
1927
|
-
const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
|
|
1928
2315
|
return {
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
}
|
|
1937
|
-
function buildHandle(deps) {
|
|
1938
|
-
const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
|
|
1939
|
-
return async (lease, payloads) => {
|
|
1940
|
-
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1941
|
-
const stream = lease.stream;
|
|
1942
|
-
let at = payloads.at(0).event.id;
|
|
1943
|
-
let handled = 0;
|
|
1944
|
-
if (lease.retry > 0)
|
|
1945
|
-
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1946
|
-
const scopedApp = {
|
|
1947
|
-
do: boundDo,
|
|
1948
|
-
load: boundLoad,
|
|
1949
|
-
query: boundQuery,
|
|
1950
|
-
query_array: boundQueryArray
|
|
1951
|
-
};
|
|
1952
|
-
for (const payload of payloads) {
|
|
1953
|
-
const { event, handler } = payload;
|
|
1954
|
-
scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
|
|
1955
|
-
action2,
|
|
1956
|
-
target,
|
|
1957
|
-
actionPayload,
|
|
1958
|
-
reactingTo ?? event,
|
|
1959
|
-
skipValidation
|
|
2316
|
+
snap: traced(snap, void 0, (snapshot) => {
|
|
2317
|
+
logger.trace(
|
|
2318
|
+
es_caption(
|
|
2319
|
+
"snap",
|
|
2320
|
+
C_MAGENTA,
|
|
2321
|
+
`${snapshot.event.stream}@${snapshot.event.version}`
|
|
2322
|
+
)
|
|
1960
2323
|
);
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2324
|
+
}),
|
|
2325
|
+
load: traced(load, (result, _me, stream, _cb, asOf) => {
|
|
2326
|
+
const stats = stats_marker(
|
|
2327
|
+
result.version,
|
|
2328
|
+
result.replayed,
|
|
2329
|
+
result.snaps,
|
|
2330
|
+
result.patches
|
|
2331
|
+
);
|
|
2332
|
+
logger.trace(
|
|
2333
|
+
es_caption(
|
|
2334
|
+
"load",
|
|
2335
|
+
C_GREEN,
|
|
2336
|
+
`${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
2337
|
+
)
|
|
2338
|
+
);
|
|
2339
|
+
}),
|
|
2340
|
+
action: traced(
|
|
2341
|
+
boundAction,
|
|
2342
|
+
(snapshots, _me, _action, target) => {
|
|
2343
|
+
const committed = snapshots.filter((s) => s.event);
|
|
2344
|
+
if (committed.length) {
|
|
2345
|
+
logger.trace(
|
|
2346
|
+
committed.map((s) => s.event.data),
|
|
2347
|
+
es_caption(
|
|
2348
|
+
"committed",
|
|
2349
|
+
C_ORANGE,
|
|
2350
|
+
`${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
|
|
2351
|
+
)
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
},
|
|
2355
|
+
(_me, action2, target, payload) => {
|
|
2356
|
+
logger.trace(
|
|
2357
|
+
payload,
|
|
2358
|
+
es_caption("action", C_BLUE, `${target.stream}.${action2}`)
|
|
1973
2359
|
);
|
|
1974
2360
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
2361
|
+
),
|
|
2362
|
+
tombstone: traced(tombstone, (committed, stream) => {
|
|
2363
|
+
if (committed)
|
|
2364
|
+
logger.trace(
|
|
2365
|
+
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
2366
|
+
);
|
|
2367
|
+
})
|
|
1977
2368
|
};
|
|
1978
2369
|
}
|
|
1979
|
-
function
|
|
1980
|
-
return
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
logger
|
|
1995
|
-
);
|
|
1996
|
-
} catch (error) {
|
|
1997
|
-
return finalize(lease, 0, lease.at, error, options, logger);
|
|
1998
|
-
}
|
|
2370
|
+
function buildDrain(logger) {
|
|
2371
|
+
return {
|
|
2372
|
+
claim,
|
|
2373
|
+
fetch,
|
|
2374
|
+
ack,
|
|
2375
|
+
block,
|
|
2376
|
+
subscribe: logger.level !== "trace" ? subscribe : traced(subscribe, (result, streams) => {
|
|
2377
|
+
if (!result.subscribed) return;
|
|
2378
|
+
const lanes = new Set(streams.map((s) => s.lane ?? "default"));
|
|
2379
|
+
const uniformLane = lanes.size === 1 ? streams[0]?.lane : void 0;
|
|
2380
|
+
const data = streams.map(
|
|
2381
|
+
({ stream, lane }) => uniformLane || !lane || lane === "default" ? hue(C_STREAM, stream) : `${hue(C_STREAM, stream)}${dim(`[${lane}]`)}`
|
|
2382
|
+
).join(" ");
|
|
2383
|
+
logger.trace(`${drain_caption("correlated", uniformLane)} ${data}`);
|
|
2384
|
+
})
|
|
1999
2385
|
};
|
|
2000
2386
|
}
|
|
2387
|
+
function traceCycle(logger, leased, fetched, handled, acked, blocked) {
|
|
2388
|
+
if (logger.level !== "trace" || !leased.length) return;
|
|
2389
|
+
const lane = leased[0]?.lane;
|
|
2390
|
+
const fetchByStream = new Map(fetched.map((f) => [f.stream, f]));
|
|
2391
|
+
const ackedByStream = new Map(acked.map((a) => [a.stream, a.at]));
|
|
2392
|
+
const blockedByStream = new Map(blocked.map((b) => [b.stream, b.error]));
|
|
2393
|
+
const failedByStream = new Map(
|
|
2394
|
+
handled.filter((h) => h.error).map((h) => [h.lease.stream, h])
|
|
2395
|
+
);
|
|
2396
|
+
const detail = leased.map(({ stream, at, retry }) => {
|
|
2397
|
+
const f = fetchByStream.get(stream);
|
|
2398
|
+
const key = f?.source ? `${hue(C_STREAM, stream)}${dim(`<-${f.source}`)}` : hue(C_STREAM, stream);
|
|
2399
|
+
const events = f && f.events.length ? ` ${dim(
|
|
2400
|
+
`[${f.events.map(({ id, name }) => `#${id} ${String(name)}`).join(", ")}]`
|
|
2401
|
+
)}` : "";
|
|
2402
|
+
const ackedAt = ackedByStream.get(stream);
|
|
2403
|
+
const ackPart = ackedAt !== void 0 ? hue(C_HIT, `\u2713 @${ackedAt}`) : "";
|
|
2404
|
+
const failure = failedByStream.get(stream);
|
|
2405
|
+
let failPart = "";
|
|
2406
|
+
if (failure) {
|
|
2407
|
+
const failedAt = failure.failed_at ?? at;
|
|
2408
|
+
const blockedError = blockedByStream.get(stream);
|
|
2409
|
+
if (blockedError !== void 0) {
|
|
2410
|
+
failPart = `${hue(C_ERR, `\u2717 @${failedAt}/${retry}`)} ${dim(`(${blockedError})`)}`;
|
|
2411
|
+
} else {
|
|
2412
|
+
failPart = `${hue(C_MISS, `\u26A0 @${failedAt}/${retry}`)} ${dim(`(${failure.error})`)}`;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
let tail;
|
|
2416
|
+
if (ackPart && failPart) tail = ` ${ackPart} ${failPart}`;
|
|
2417
|
+
else if (ackPart) tail = ` ${ackPart}`;
|
|
2418
|
+
else if (failPart) tail = ` ${failPart}`;
|
|
2419
|
+
else tail = ` ${dim(`\u2298 @${at}/${retry}`)}`;
|
|
2420
|
+
return `${key}${events}${tail}`;
|
|
2421
|
+
}).join(", ");
|
|
2422
|
+
logger.trace(`${drain_caption("drained", lane)} ${detail}`);
|
|
2423
|
+
}
|
|
2001
2424
|
|
|
2002
|
-
// src/internal/
|
|
2003
|
-
|
|
2004
|
-
|
|
2425
|
+
// src/internal/drain-cycle.ts
|
|
2426
|
+
async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis, isDeferred, lane) {
|
|
2427
|
+
const leased = await ops.claim(
|
|
2428
|
+
lagging,
|
|
2429
|
+
leading,
|
|
2430
|
+
(0, import_node_crypto2.randomUUID)(),
|
|
2431
|
+
leaseMillis,
|
|
2432
|
+
lane
|
|
2433
|
+
);
|
|
2434
|
+
if (!leased.length) return void 0;
|
|
2435
|
+
const active = isDeferred ? leased.filter((l) => !isDeferred(l.stream)) : leased;
|
|
2436
|
+
if (!active.length) {
|
|
2437
|
+
return {
|
|
2438
|
+
leased,
|
|
2439
|
+
fetched: [],
|
|
2440
|
+
handled: [],
|
|
2441
|
+
acked: [],
|
|
2442
|
+
blocked: []
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
const fetched = await ops.fetch(active, eventLimit);
|
|
2446
|
+
const fetchMap = /* @__PURE__ */ new Map();
|
|
2447
|
+
const fetch_window_at = fetched.reduce(
|
|
2448
|
+
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
2449
|
+
0
|
|
2450
|
+
);
|
|
2451
|
+
for (const f of fetched) {
|
|
2452
|
+
const { stream, events } = f;
|
|
2453
|
+
const payloads = events.flatMap((event) => {
|
|
2454
|
+
const register = registry.events[event.name];
|
|
2455
|
+
if (!register) return [];
|
|
2456
|
+
return [...register.reactions.values()].filter((reaction) => {
|
|
2457
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
2458
|
+
return resolved && resolved.target === stream;
|
|
2459
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
2460
|
+
});
|
|
2461
|
+
fetchMap.set(stream, { fetch: f, payloads });
|
|
2462
|
+
}
|
|
2463
|
+
const handled = await Promise.all(
|
|
2464
|
+
active.map((lease) => {
|
|
2465
|
+
const entry = fetchMap.get(lease.stream);
|
|
2466
|
+
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
2467
|
+
const { payloads } = entry;
|
|
2468
|
+
const batchHandler = batchHandlers.get(lease.stream);
|
|
2469
|
+
if (batchHandler && payloads.length > 0) {
|
|
2470
|
+
return handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
2471
|
+
}
|
|
2472
|
+
return handle({ ...lease, at }, payloads);
|
|
2473
|
+
})
|
|
2474
|
+
);
|
|
2475
|
+
const acked = await ops.ack(
|
|
2476
|
+
handled.filter((h) => h.handled > 0 || !h.error).map((h) => ({ ...h.lease, at: h.acked_at }))
|
|
2477
|
+
);
|
|
2478
|
+
const blocked = await ops.block(
|
|
2479
|
+
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
2480
|
+
);
|
|
2481
|
+
return { leased, fetched, handled, acked, blocked };
|
|
2482
|
+
}
|
|
2483
|
+
var EMPTY_DRAIN = {
|
|
2484
|
+
fetched: [],
|
|
2485
|
+
leased: [],
|
|
2486
|
+
acked: [],
|
|
2487
|
+
blocked: []
|
|
2488
|
+
};
|
|
2489
|
+
var DrainController = class {
|
|
2490
|
+
constructor(deps) {
|
|
2005
2491
|
this.deps = deps;
|
|
2006
|
-
this.defaultDebounceMs = defaultDebounceMs;
|
|
2007
2492
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2493
|
+
_armed = false;
|
|
2494
|
+
_locked = false;
|
|
2495
|
+
_ratio = 0.5;
|
|
2496
|
+
/**
|
|
2497
|
+
* Per-stream backoff: `stream → nextAttemptAt` (ms since epoch). Set by
|
|
2498
|
+
* `_finalize` via `HandleResult.nextAttemptAt`; cleared on successful
|
|
2499
|
+
* ack or terminal block. Lives in process memory — per-worker pacing
|
|
2500
|
+
* by design (see {@link BackoffOptions} for the multi-worker trade-off).
|
|
2501
|
+
*/
|
|
2502
|
+
_backoff = /* @__PURE__ */ new Map();
|
|
2503
|
+
/** Timer re-arming drain at the earliest pending `nextAttemptAt`. */
|
|
2504
|
+
_backoffTimer;
|
|
2505
|
+
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
2506
|
+
_worker;
|
|
2507
|
+
_stopped = false;
|
|
2508
|
+
/**
|
|
2509
|
+
* Signal that a commit (or reset / cold-start) may have produced work.
|
|
2510
|
+
* Subsequent `drain()` calls will run the pipeline; once the pipeline
|
|
2511
|
+
* settles to no-progress, the controller disarms itself.
|
|
2512
|
+
*/
|
|
2513
|
+
arm() {
|
|
2514
|
+
this._armed = true;
|
|
2515
|
+
}
|
|
2516
|
+
/** Read-only flag — true while a commit / reset is unprocessed. */
|
|
2517
|
+
get armed() {
|
|
2518
|
+
return this._armed;
|
|
2519
|
+
}
|
|
2520
|
+
/** Returns true when `stream` is currently within a backoff window. */
|
|
2521
|
+
isDeferred = (stream) => {
|
|
2522
|
+
const next = this._backoff.get(stream);
|
|
2523
|
+
return next !== void 0 && next > Date.now();
|
|
2524
|
+
};
|
|
2525
|
+
/**
|
|
2526
|
+
* Schedule the next drain re-arm at the earliest pending backoff
|
|
2527
|
+
* expiry. Called only when the backoff map is non-empty (caller guard).
|
|
2528
|
+
* Idempotent — collapses many simultaneously deferred streams into a
|
|
2529
|
+
* single timer.
|
|
2530
|
+
*/
|
|
2531
|
+
scheduleBackoffWake() {
|
|
2532
|
+
if (this._backoffTimer) clearTimeout(this._backoffTimer);
|
|
2533
|
+
let earliest = Number.POSITIVE_INFINITY;
|
|
2534
|
+
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
2535
|
+
const delay = Math.max(0, earliest - Date.now());
|
|
2536
|
+
this._backoffTimer = setTimeout(() => {
|
|
2537
|
+
this._backoffTimer = void 0;
|
|
2538
|
+
const now = Date.now();
|
|
2539
|
+
for (const [stream, at] of this._backoff) {
|
|
2540
|
+
if (at <= now) this._backoff.delete(stream);
|
|
2541
|
+
}
|
|
2542
|
+
this._armed = true;
|
|
2543
|
+
}, delay);
|
|
2544
|
+
this._backoffTimer.unref();
|
|
2545
|
+
}
|
|
2546
|
+
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
2547
|
+
get lane() {
|
|
2548
|
+
return this.deps.lane;
|
|
2549
|
+
}
|
|
2010
2550
|
/**
|
|
2011
|
-
*
|
|
2012
|
-
*
|
|
2013
|
-
*
|
|
2014
|
-
*
|
|
2015
|
-
*
|
|
2551
|
+
* Start a per-lane worker that drains at the lane's `cycleMs`
|
|
2552
|
+
* cadence (ACT-1103). When armed, the worker calls `drain()` on every
|
|
2553
|
+
* tick and re-schedules; when not armed, it still re-schedules at
|
|
2554
|
+
* `cycleMs` so a future `arm()` is picked up on the next tick.
|
|
2555
|
+
*
|
|
2556
|
+
* The setTimeout chain uses `unref()` so it doesn't keep the process
|
|
2557
|
+
* alive on its own.
|
|
2016
2558
|
*/
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
this.
|
|
2026
|
-
|
|
2027
|
-
if (this._running) return;
|
|
2028
|
-
this._running = true;
|
|
2029
|
-
(async () => {
|
|
2030
|
-
await this.deps.init();
|
|
2031
|
-
let lastDrain;
|
|
2032
|
-
for (let i = 0; i < maxPasses; i++) {
|
|
2033
|
-
const { subscribed } = await this.deps.correlate({
|
|
2034
|
-
...correlateQuery,
|
|
2035
|
-
after: this.deps.checkpoint()
|
|
2036
|
-
});
|
|
2037
|
-
lastDrain = await this.deps.drain(drainOptions);
|
|
2038
|
-
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
2039
|
-
if (!made_progress) break;
|
|
2040
|
-
}
|
|
2041
|
-
if (lastDrain) this.deps.onSettled(lastDrain);
|
|
2042
|
-
})().catch((err) => this.deps.logger.error(err)).finally(() => {
|
|
2043
|
-
this._running = false;
|
|
2044
|
-
});
|
|
2045
|
-
}, debounceMs);
|
|
2559
|
+
start(cycleMs) {
|
|
2560
|
+
if (this._worker || this._stopped) return;
|
|
2561
|
+
const tick = async () => {
|
|
2562
|
+
if (this._armed) await this.drain();
|
|
2563
|
+
if (this._stopped) return;
|
|
2564
|
+
this._worker = setTimeout(tick, cycleMs);
|
|
2565
|
+
this._worker.unref();
|
|
2566
|
+
};
|
|
2567
|
+
this._worker = setTimeout(tick, cycleMs);
|
|
2568
|
+
this._worker.unref();
|
|
2046
2569
|
}
|
|
2047
|
-
/**
|
|
2570
|
+
/** Stop the per-lane worker. Idempotent. */
|
|
2048
2571
|
stop() {
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
this.
|
|
2572
|
+
this._stopped = true;
|
|
2573
|
+
if (this._worker) {
|
|
2574
|
+
clearTimeout(this._worker);
|
|
2575
|
+
this._worker = void 0;
|
|
2052
2576
|
}
|
|
2053
2577
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
[{ name: SNAP_EVENT, data: snapshot.state }],
|
|
2083
|
-
{
|
|
2084
|
-
correlation: meta.correlation,
|
|
2085
|
-
causation: { event: { id, name, stream } }
|
|
2086
|
-
},
|
|
2087
|
-
version
|
|
2088
|
-
// IMPORTANT! - state events are committed right after the snapshot event
|
|
2089
|
-
);
|
|
2090
|
-
} catch (error) {
|
|
2091
|
-
log().error(error);
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
async function tombstone(stream, expectedVersion, correlation) {
|
|
2095
|
-
try {
|
|
2096
|
-
const [committed] = await store2().commit(
|
|
2097
|
-
stream,
|
|
2098
|
-
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
2099
|
-
{ correlation, causation: {} },
|
|
2100
|
-
expectedVersion
|
|
2101
|
-
);
|
|
2102
|
-
return committed;
|
|
2103
|
-
} catch (error) {
|
|
2104
|
-
if (error instanceof ConcurrencyError) return void 0;
|
|
2105
|
-
throw error;
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
async function load(me, stream, callback, asOf) {
|
|
2109
|
-
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
2110
|
-
const cached = timeTravel ? void 0 : await cache2().get(stream);
|
|
2111
|
-
const cache_hit = !!cached;
|
|
2112
|
-
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
2113
|
-
let patches = cached?.patches ?? 0;
|
|
2114
|
-
let snaps = cached?.snaps ?? 0;
|
|
2115
|
-
let version = cached?.version ?? -1;
|
|
2116
|
-
let replayed = 0;
|
|
2117
|
-
let event;
|
|
2118
|
-
await store2().query(
|
|
2119
|
-
(e) => {
|
|
2120
|
-
event = e;
|
|
2121
|
-
version = e.version;
|
|
2122
|
-
if (e.name === SNAP_EVENT) {
|
|
2123
|
-
state2 = e.data;
|
|
2124
|
-
snaps++;
|
|
2125
|
-
patches = 0;
|
|
2126
|
-
replayed++;
|
|
2127
|
-
} else if (me.patch[e.name]) {
|
|
2128
|
-
state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](event, state2));
|
|
2129
|
-
patches++;
|
|
2130
|
-
replayed++;
|
|
2131
|
-
} else if (e.name !== TOMBSTONE_EVENT) {
|
|
2132
|
-
log().warn(
|
|
2133
|
-
`Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
|
|
2134
|
-
);
|
|
2578
|
+
/** Run one drain pass. Short-circuits when not armed or already running. */
|
|
2579
|
+
async drain(options = {}) {
|
|
2580
|
+
if (!this._armed) return EMPTY_DRAIN;
|
|
2581
|
+
if (this._locked) return EMPTY_DRAIN;
|
|
2582
|
+
const d = this.deps.defaults ?? {};
|
|
2583
|
+
const streamLimit = d.streamLimit ?? options.streamLimit ?? 10;
|
|
2584
|
+
const eventLimit = d.eventLimit ?? options.eventLimit ?? 10;
|
|
2585
|
+
const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? 1e4;
|
|
2586
|
+
try {
|
|
2587
|
+
this._locked = true;
|
|
2588
|
+
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
2589
|
+
const leading = streamLimit - lagging;
|
|
2590
|
+
const cycle = await runDrainCycle(
|
|
2591
|
+
this.deps.ops,
|
|
2592
|
+
this.deps.registry,
|
|
2593
|
+
this.deps.batchHandlers,
|
|
2594
|
+
this.deps.handle,
|
|
2595
|
+
this.deps.handleBatch,
|
|
2596
|
+
lagging,
|
|
2597
|
+
leading,
|
|
2598
|
+
eventLimit,
|
|
2599
|
+
leaseMillis,
|
|
2600
|
+
this._backoff.size > 0 ? this.isDeferred : void 0,
|
|
2601
|
+
this.deps.lane
|
|
2602
|
+
);
|
|
2603
|
+
if (!cycle) {
|
|
2604
|
+
this._armed = false;
|
|
2605
|
+
return EMPTY_DRAIN;
|
|
2135
2606
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2607
|
+
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
2608
|
+
traceCycle(this.deps.logger, leased, fetched, handled, acked, blocked);
|
|
2609
|
+
this._ratio = computeLagLeadRatio(handled, lagging, leading);
|
|
2610
|
+
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
2611
|
+
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
2612
|
+
for (const h of handled) {
|
|
2613
|
+
if (h.nextAttemptAt !== void 0 && !h.block) {
|
|
2614
|
+
this._backoff.set(h.lease.stream, h.nextAttemptAt);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (this._backoff.size > 0) this.scheduleBackoffWake();
|
|
2618
|
+
if (acked.length) this.deps.onAcked(acked);
|
|
2619
|
+
if (blocked.length) this.deps.onBlocked(blocked);
|
|
2620
|
+
const hasErrors = handled.some(({ error }) => error);
|
|
2621
|
+
if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
|
|
2622
|
+
return { fetched, leased, acked, blocked };
|
|
2623
|
+
} catch (error) {
|
|
2624
|
+
this.deps.logger.error(error);
|
|
2625
|
+
return EMPTY_DRAIN;
|
|
2626
|
+
} finally {
|
|
2627
|
+
this._locked = false;
|
|
2150
2628
|
}
|
|
2151
|
-
);
|
|
2152
|
-
if (replayed > 0 && !timeTravel && event) {
|
|
2153
|
-
await cache2().set(stream, {
|
|
2154
|
-
state: state2,
|
|
2155
|
-
version,
|
|
2156
|
-
event_id: event.id,
|
|
2157
|
-
patches,
|
|
2158
|
-
snaps
|
|
2159
|
-
});
|
|
2160
|
-
}
|
|
2161
|
-
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
2162
|
-
}
|
|
2163
|
-
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = defaultCorrelator) {
|
|
2164
|
-
const { stream, expectedVersion, actor } = target;
|
|
2165
|
-
if (!stream) throw new Error("Missing target stream");
|
|
2166
|
-
const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
2167
|
-
const snapshot = await load(me, stream);
|
|
2168
|
-
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
2169
|
-
throw new StreamClosedError(stream);
|
|
2170
|
-
const expected = expectedVersion ?? snapshot.event?.version;
|
|
2171
|
-
if (me.given) {
|
|
2172
|
-
const invariants = me.given[action2] || [];
|
|
2173
|
-
invariants.forEach(({ valid, description }) => {
|
|
2174
|
-
if (!valid(snapshot.state, actor))
|
|
2175
|
-
throw new InvariantError(
|
|
2176
|
-
action2,
|
|
2177
|
-
validated,
|
|
2178
|
-
target,
|
|
2179
|
-
snapshot,
|
|
2180
|
-
description
|
|
2181
|
-
);
|
|
2182
|
-
});
|
|
2183
2629
|
}
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2630
|
+
};
|
|
2631
|
+
|
|
2632
|
+
// src/internal/merge.ts
|
|
2633
|
+
var import_zod4 = require("zod");
|
|
2634
|
+
function baseTypeName(zodType) {
|
|
2635
|
+
let t = zodType;
|
|
2636
|
+
while (typeof t.unwrap === "function") {
|
|
2637
|
+
t = t.unwrap();
|
|
2188
2638
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
if (
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2639
|
+
return t.constructor.name;
|
|
2640
|
+
}
|
|
2641
|
+
function mergeSchemas(existing, incoming, stateName) {
|
|
2642
|
+
if (existing instanceof import_zod4.ZodObject && incoming instanceof import_zod4.ZodObject) {
|
|
2643
|
+
const existingShape = existing.shape;
|
|
2644
|
+
const incomingShape = incoming.shape;
|
|
2645
|
+
for (const key of Object.keys(incomingShape)) {
|
|
2646
|
+
if (key in existingShape) {
|
|
2647
|
+
const existingBase = baseTypeName(existingShape[key]);
|
|
2648
|
+
const incomingBase = baseTypeName(incomingShape[key]);
|
|
2649
|
+
if (existingBase !== incomingBase) {
|
|
2650
|
+
throw new Error(
|
|
2651
|
+
`Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2201
2654
|
}
|
|
2202
2655
|
}
|
|
2656
|
+
return existing.extend(incomingShape);
|
|
2203
2657
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
})
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
causation: {
|
|
2216
|
-
action: {
|
|
2217
|
-
name: action2,
|
|
2218
|
-
...target
|
|
2219
|
-
// payload intentionally omitted: it can be large or contain PII,
|
|
2220
|
-
// and callers correlate via the correlation id when they need it.
|
|
2221
|
-
},
|
|
2222
|
-
event: reactingTo ? {
|
|
2223
|
-
id: reactingTo.id,
|
|
2224
|
-
name: reactingTo.name,
|
|
2225
|
-
stream: reactingTo.stream
|
|
2226
|
-
} : void 0
|
|
2227
|
-
}
|
|
2228
|
-
};
|
|
2229
|
-
let committed;
|
|
2230
|
-
try {
|
|
2231
|
-
committed = await store2().commit(
|
|
2232
|
-
stream,
|
|
2233
|
-
emitted,
|
|
2234
|
-
meta,
|
|
2235
|
-
// Reactions skip optimistic concurrency: they always append against the
|
|
2236
|
-
// current head. Stream leasing already serializes concurrent reactions,
|
|
2237
|
-
// and forcing version checks here would turn ordinary catch-up into
|
|
2238
|
-
// spurious retries.
|
|
2239
|
-
reactingTo ? void 0 : expected
|
|
2240
|
-
);
|
|
2241
|
-
} catch (error) {
|
|
2242
|
-
if (error instanceof ConcurrencyError) {
|
|
2243
|
-
await cache2().invalidate(stream);
|
|
2244
|
-
}
|
|
2245
|
-
throw error;
|
|
2658
|
+
return existing;
|
|
2659
|
+
}
|
|
2660
|
+
function mergeInits(existing, incoming) {
|
|
2661
|
+
return () => ({ ...existing(), ...incoming() });
|
|
2662
|
+
}
|
|
2663
|
+
function registerState(state2, states, actions, events) {
|
|
2664
|
+
const existing = states.get(state2.name);
|
|
2665
|
+
if (existing) {
|
|
2666
|
+
mergeIntoExisting(state2, existing, states, actions, events);
|
|
2667
|
+
} else {
|
|
2668
|
+
registerNewState(state2, states, actions, events);
|
|
2246
2669
|
}
|
|
2247
|
-
let { state: state2, patches } = snapshot;
|
|
2248
|
-
const snapshots = committed.map((event) => {
|
|
2249
|
-
const p = me.patch[event.name](event, state2);
|
|
2250
|
-
state2 = (0, import_act_patch.patch)(state2, p);
|
|
2251
|
-
patches++;
|
|
2252
|
-
return {
|
|
2253
|
-
event,
|
|
2254
|
-
state: state2,
|
|
2255
|
-
version: event.version,
|
|
2256
|
-
patches,
|
|
2257
|
-
snaps: snapshot.snaps,
|
|
2258
|
-
patch: p,
|
|
2259
|
-
cache_hit: snapshot.cache_hit,
|
|
2260
|
-
replayed: snapshot.replayed
|
|
2261
|
-
};
|
|
2262
|
-
});
|
|
2263
|
-
const last = snapshots.at(-1);
|
|
2264
|
-
const snapped = me.snap?.(last);
|
|
2265
|
-
cache2().set(stream, {
|
|
2266
|
-
state: last.state,
|
|
2267
|
-
version: last.event.version,
|
|
2268
|
-
event_id: last.event.id,
|
|
2269
|
-
patches: snapped ? 0 : last.patches,
|
|
2270
|
-
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
2271
|
-
}).catch((err) => log().error(err));
|
|
2272
|
-
if (snapped) void snap(last);
|
|
2273
|
-
return snapshots;
|
|
2274
2670
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
var C_GREEN = "\x1B[38;5;42m";
|
|
2281
|
-
var C_MAGENTA = "\x1B[38;5;165m";
|
|
2282
|
-
var C_DRAIN = "\x1B[38;5;244m";
|
|
2283
|
-
var C_HIT = "\x1B[38;5;82m";
|
|
2284
|
-
var C_MISS = "\x1B[38;5;220m";
|
|
2285
|
-
var C_RESET = "\x1B[0m";
|
|
2286
|
-
var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
|
|
2287
|
-
var drain_caption = (caption) => {
|
|
2288
|
-
const tag = `>> ${caption}`;
|
|
2289
|
-
return PRETTY ? `${C_DRAIN}${tag}${C_RESET}` : tag;
|
|
2290
|
-
};
|
|
2291
|
-
var cache_marker = (hit) => {
|
|
2292
|
-
const word = hit ? "hit" : "miss";
|
|
2293
|
-
if (!PRETTY) return word;
|
|
2294
|
-
return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
|
|
2295
|
-
};
|
|
2296
|
-
var stats_marker = (version, replayed, snaps, patches) => {
|
|
2297
|
-
const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
|
|
2298
|
-
if (!PRETTY) return text;
|
|
2299
|
-
return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
|
|
2300
|
-
};
|
|
2301
|
-
var as_of_marker = (asOf) => {
|
|
2302
|
-
if (!asOf) return "";
|
|
2303
|
-
const parts = [];
|
|
2304
|
-
if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
|
|
2305
|
-
if (asOf.created_before !== void 0)
|
|
2306
|
-
parts.push(`created_before=${asOf.created_before.toISOString()}`);
|
|
2307
|
-
if (asOf.created_after !== void 0)
|
|
2308
|
-
parts.push(`created_after=${asOf.created_after.toISOString()}`);
|
|
2309
|
-
if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
|
|
2310
|
-
return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
|
|
2311
|
-
};
|
|
2312
|
-
var traced = (inner, exit, entry) => (async (...args) => {
|
|
2313
|
-
entry?.(...args);
|
|
2314
|
-
const result = await inner(...args);
|
|
2315
|
-
exit?.(result, ...args);
|
|
2316
|
-
return result;
|
|
2317
|
-
});
|
|
2318
|
-
function buildEs(logger, correlator = defaultCorrelator) {
|
|
2319
|
-
const boundAction = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
|
|
2320
|
-
me,
|
|
2321
|
-
actionName,
|
|
2322
|
-
target,
|
|
2323
|
-
payload,
|
|
2324
|
-
reactingTo,
|
|
2325
|
-
skipValidation,
|
|
2326
|
-
correlator
|
|
2327
|
-
);
|
|
2328
|
-
if (logger.level !== "trace") {
|
|
2329
|
-
return {
|
|
2330
|
-
snap,
|
|
2331
|
-
load,
|
|
2332
|
-
action: boundAction,
|
|
2333
|
-
tombstone
|
|
2334
|
-
};
|
|
2671
|
+
function registerNewState(state2, states, actions, events) {
|
|
2672
|
+
states.set(state2.name, state2);
|
|
2673
|
+
for (const name of Object.keys(state2.actions)) {
|
|
2674
|
+
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
2675
|
+
actions[name] = state2;
|
|
2335
2676
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2677
|
+
for (const name of Object.keys(state2.events)) {
|
|
2678
|
+
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
2679
|
+
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
2683
|
+
for (const name of Object.keys(state2.actions)) {
|
|
2684
|
+
if (existing.actions[name] === state2.actions[name]) continue;
|
|
2685
|
+
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
2686
|
+
}
|
|
2687
|
+
for (const name of Object.keys(state2.events)) {
|
|
2688
|
+
if (existing.events[name] === state2.events[name]) continue;
|
|
2689
|
+
if (existing.events[name]) {
|
|
2690
|
+
throw new Error(
|
|
2691
|
+
`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.`
|
|
2344
2692
|
);
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2693
|
+
}
|
|
2694
|
+
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
2695
|
+
}
|
|
2696
|
+
const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
|
|
2697
|
+
const merged = {
|
|
2698
|
+
...existing,
|
|
2699
|
+
state: mergeSchemas(existing.state, state2.state, state2.name),
|
|
2700
|
+
init: mergeInits(existing.init, state2.init),
|
|
2701
|
+
events: { ...existing.events, ...state2.events },
|
|
2702
|
+
actions: { ...existing.actions, ...state2.actions },
|
|
2703
|
+
patch: mergedPatch,
|
|
2704
|
+
on: { ...existing.on, ...state2.on },
|
|
2705
|
+
given: { ...existing.given, ...state2.given },
|
|
2706
|
+
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
2707
|
+
throw new Error(
|
|
2708
|
+
`Duplicate snap strategy for state "${state2.name}"`
|
|
2352
2709
|
);
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2710
|
+
})() : state2.snap || existing.snap
|
|
2711
|
+
};
|
|
2712
|
+
states.set(state2.name, merged);
|
|
2713
|
+
for (const name of Object.keys(merged.actions)) {
|
|
2714
|
+
actions[name] = merged;
|
|
2715
|
+
}
|
|
2716
|
+
for (const name of Object.keys(state2.events)) {
|
|
2717
|
+
if (events[name]) continue;
|
|
2718
|
+
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
function mergePatches(existing, incoming, stateName) {
|
|
2722
|
+
const merged = { ...existing };
|
|
2723
|
+
for (const name of Object.keys(incoming)) {
|
|
2724
|
+
const existingP = existing[name];
|
|
2725
|
+
const incomingP = incoming[name];
|
|
2726
|
+
if (!existingP) {
|
|
2727
|
+
merged[name] = incomingP;
|
|
2728
|
+
continue;
|
|
2729
|
+
}
|
|
2730
|
+
const existingIsDefault = existingP._passthrough;
|
|
2731
|
+
const incomingIsDefault = incomingP._passthrough;
|
|
2732
|
+
if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
|
|
2733
|
+
throw new Error(
|
|
2734
|
+
`Duplicate custom patch for event "${name}" in state "${stateName}"`
|
|
2359
2735
|
);
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2736
|
+
}
|
|
2737
|
+
if (existingIsDefault && !incomingIsDefault) {
|
|
2738
|
+
merged[name] = incomingP;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
return merged;
|
|
2742
|
+
}
|
|
2743
|
+
function mergeEventRegister(target, source) {
|
|
2744
|
+
for (const [eventName, sourceReg] of Object.entries(source)) {
|
|
2745
|
+
const targetReg = target[eventName];
|
|
2746
|
+
if (!targetReg) continue;
|
|
2747
|
+
for (const [name, reaction] of sourceReg.reactions) {
|
|
2748
|
+
targetReg.reactions.set(name, reaction);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
function mergeProjection(proj, events) {
|
|
2753
|
+
for (const eventName of Object.keys(proj.events)) {
|
|
2754
|
+
const projRegister = proj.events[eventName];
|
|
2755
|
+
const existing = events[eventName];
|
|
2756
|
+
if (!existing) {
|
|
2757
|
+
events[eventName] = {
|
|
2758
|
+
schema: projRegister.schema,
|
|
2759
|
+
reactions: new Map(projRegister.reactions)
|
|
2760
|
+
};
|
|
2761
|
+
} else {
|
|
2762
|
+
for (const [name, reaction] of projRegister.reactions) {
|
|
2763
|
+
let key = name;
|
|
2764
|
+
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
2765
|
+
existing.reactions.set(key, reaction);
|
|
2381
2766
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
if (committed)
|
|
2385
|
-
logger.trace(
|
|
2386
|
-
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
2387
|
-
);
|
|
2388
|
-
})
|
|
2389
|
-
};
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2390
2769
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2770
|
+
var _this_ = ({ stream }) => ({
|
|
2771
|
+
source: stream,
|
|
2772
|
+
target: stream
|
|
2773
|
+
});
|
|
2774
|
+
|
|
2775
|
+
// src/internal/backoff.ts
|
|
2776
|
+
function computeBackoffDelay(retry, opts) {
|
|
2777
|
+
if (!opts || opts.baseMs <= 0) return 0;
|
|
2778
|
+
const r = Math.max(0, retry);
|
|
2779
|
+
let delay;
|
|
2780
|
+
switch (opts.strategy) {
|
|
2781
|
+
case "fixed":
|
|
2782
|
+
delay = opts.baseMs;
|
|
2783
|
+
break;
|
|
2784
|
+
case "linear":
|
|
2785
|
+
delay = opts.baseMs * (r + 1);
|
|
2786
|
+
break;
|
|
2787
|
+
case "exponential":
|
|
2788
|
+
delay = opts.baseMs * 2 ** r;
|
|
2789
|
+
if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
|
|
2790
|
+
break;
|
|
2400
2791
|
}
|
|
2792
|
+
if (opts.jitter) delay = delay * (0.5 + Math.random());
|
|
2793
|
+
return Math.max(0, Math.floor(delay));
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
// src/internal/reactions.ts
|
|
2797
|
+
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
2798
|
+
if (!error) return { lease, handled, acked_at: at };
|
|
2799
|
+
logger.error(error);
|
|
2800
|
+
const nonRetryable = error instanceof NonRetryableError;
|
|
2801
|
+
const block2 = options.blockOnError && (nonRetryable || lease.retry >= options.maxRetries);
|
|
2802
|
+
if (block2)
|
|
2803
|
+
logger.error(
|
|
2804
|
+
nonRetryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
2805
|
+
);
|
|
2806
|
+
const nextAttemptAt = !block2 && options.backoff ? Date.now() + computeBackoffDelay(lease.retry, options.backoff) : void 0;
|
|
2401
2807
|
return {
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2808
|
+
lease,
|
|
2809
|
+
handled,
|
|
2810
|
+
acked_at: at,
|
|
2811
|
+
error: error.message,
|
|
2812
|
+
block: block2,
|
|
2813
|
+
nextAttemptAt,
|
|
2814
|
+
failed_at
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
function buildHandle(deps) {
|
|
2818
|
+
const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
|
|
2819
|
+
return async (lease, payloads) => {
|
|
2820
|
+
if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
|
|
2821
|
+
const stream = lease.stream;
|
|
2822
|
+
let at = payloads.at(0).event.id;
|
|
2823
|
+
let handled = 0;
|
|
2824
|
+
if (lease.retry > 0)
|
|
2825
|
+
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
2826
|
+
const scopedApp = {
|
|
2827
|
+
do: boundDo,
|
|
2828
|
+
load: boundLoad,
|
|
2829
|
+
query: boundQuery,
|
|
2830
|
+
query_array: boundQueryArray
|
|
2831
|
+
};
|
|
2832
|
+
for (const payload of payloads) {
|
|
2833
|
+
const { event, handler } = payload;
|
|
2834
|
+
scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
|
|
2835
|
+
action2,
|
|
2836
|
+
target,
|
|
2837
|
+
actionPayload,
|
|
2838
|
+
reactingTo ?? event,
|
|
2839
|
+
skipValidation
|
|
2419
2840
|
);
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
blocked.map(({ stream, at, retry, error }) => [
|
|
2434
|
-
stream,
|
|
2435
|
-
{ at, retry, error }
|
|
2436
|
-
])
|
|
2841
|
+
try {
|
|
2842
|
+
await handler(event, stream, scopedApp);
|
|
2843
|
+
at = event.id;
|
|
2844
|
+
handled++;
|
|
2845
|
+
} catch (error) {
|
|
2846
|
+
return finalize(
|
|
2847
|
+
lease,
|
|
2848
|
+
handled,
|
|
2849
|
+
at,
|
|
2850
|
+
error,
|
|
2851
|
+
payload.options,
|
|
2852
|
+
logger,
|
|
2853
|
+
event.id
|
|
2437
2854
|
);
|
|
2438
|
-
logger.trace(data, drain_caption("blocked"));
|
|
2439
|
-
}
|
|
2440
|
-
}),
|
|
2441
|
-
subscribe: traced(subscribe, (result, streams) => {
|
|
2442
|
-
if (result.subscribed) {
|
|
2443
|
-
const data = streams.map(({ stream }) => stream).join(" ");
|
|
2444
|
-
logger.trace(`${drain_caption("correlated")} ${data}`);
|
|
2445
2855
|
}
|
|
2446
|
-
}
|
|
2856
|
+
}
|
|
2857
|
+
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
function buildHandleBatch(logger) {
|
|
2861
|
+
return async (lease, payloads, batchHandler) => {
|
|
2862
|
+
const stream = lease.stream;
|
|
2863
|
+
const events = payloads.map((p) => p.event);
|
|
2864
|
+
const options = payloads[0].options;
|
|
2865
|
+
if (lease.retry > 0)
|
|
2866
|
+
logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
|
|
2867
|
+
try {
|
|
2868
|
+
await batchHandler(events, stream);
|
|
2869
|
+
return finalize(
|
|
2870
|
+
lease,
|
|
2871
|
+
events.length,
|
|
2872
|
+
events.at(-1).id,
|
|
2873
|
+
void 0,
|
|
2874
|
+
options,
|
|
2875
|
+
logger
|
|
2876
|
+
);
|
|
2877
|
+
} catch (error) {
|
|
2878
|
+
return finalize(lease, 0, lease.at, error, options, logger);
|
|
2879
|
+
}
|
|
2447
2880
|
};
|
|
2448
2881
|
}
|
|
2449
2882
|
|
|
2883
|
+
// src/internal/settle.ts
|
|
2884
|
+
var SettleLoop = class {
|
|
2885
|
+
constructor(deps, defaultDebounceMs) {
|
|
2886
|
+
this.deps = deps;
|
|
2887
|
+
this.defaultDebounceMs = defaultDebounceMs;
|
|
2888
|
+
}
|
|
2889
|
+
_timer = void 0;
|
|
2890
|
+
_running = false;
|
|
2891
|
+
/**
|
|
2892
|
+
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
2893
|
+
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
2894
|
+
* until no progress is made (no new subscriptions, no acks, no blocks)
|
|
2895
|
+
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
2896
|
+
* via {@link SettleDeps.onSettled}.
|
|
2897
|
+
*/
|
|
2898
|
+
schedule(options = {}) {
|
|
2899
|
+
const {
|
|
2900
|
+
debounceMs = this.defaultDebounceMs,
|
|
2901
|
+
correlate: correlateQuery = { after: -1, limit: 100 },
|
|
2902
|
+
maxPasses = Infinity,
|
|
2903
|
+
...drainOptions
|
|
2904
|
+
} = options;
|
|
2905
|
+
if (this._timer) clearTimeout(this._timer);
|
|
2906
|
+
this._timer = setTimeout(() => {
|
|
2907
|
+
this._timer = void 0;
|
|
2908
|
+
if (this._running) return;
|
|
2909
|
+
this._running = true;
|
|
2910
|
+
(async () => {
|
|
2911
|
+
await this.deps.init();
|
|
2912
|
+
let lastDrain;
|
|
2913
|
+
for (let i = 0; i < maxPasses; i++) {
|
|
2914
|
+
const { subscribed } = await this.deps.correlate({
|
|
2915
|
+
...correlateQuery,
|
|
2916
|
+
after: this.deps.checkpoint()
|
|
2917
|
+
});
|
|
2918
|
+
lastDrain = await this.deps.drain(drainOptions);
|
|
2919
|
+
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
2920
|
+
if (!made_progress) break;
|
|
2921
|
+
}
|
|
2922
|
+
if (lastDrain) this.deps.onSettled(lastDrain);
|
|
2923
|
+
})().catch((err) => this.deps.logger.error(err)).finally(() => {
|
|
2924
|
+
this._running = false;
|
|
2925
|
+
});
|
|
2926
|
+
}, debounceMs);
|
|
2927
|
+
}
|
|
2928
|
+
/** Cancel any pending or active settle cycle. Idempotent. */
|
|
2929
|
+
stop() {
|
|
2930
|
+
if (this._timer) {
|
|
2931
|
+
clearTimeout(this._timer);
|
|
2932
|
+
this._timer = void 0;
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
};
|
|
2936
|
+
|
|
2450
2937
|
// src/act.ts
|
|
2451
2938
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
2452
2939
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
@@ -2461,11 +2948,26 @@ var Act = class {
|
|
|
2461
2948
|
* @param _states Merged map of state name → state definition
|
|
2462
2949
|
* @param batchHandlers Static-target projection batch handlers (target → handler)
|
|
2463
2950
|
* @param options Tuning knobs — see {@link ActOptions}
|
|
2951
|
+
* @param lanes Declared drain lanes (ACT-1103). The builder collects
|
|
2952
|
+
* these from `.withLane(...)` calls. Slice 1 records them on the
|
|
2953
|
+
* instance; later slices fan out one `DrainController` per lane.
|
|
2464
2954
|
*/
|
|
2465
|
-
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
|
|
2955
|
+
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
|
|
2466
2956
|
this.registry = registry;
|
|
2467
2957
|
this._states = _states;
|
|
2468
2958
|
this._batch_handlers = batchHandlers;
|
|
2959
|
+
this._lanes = lanes;
|
|
2960
|
+
if (options.onlyLanes && options.onlyLanes.length > 0) {
|
|
2961
|
+
const declared = /* @__PURE__ */ new Set([
|
|
2962
|
+
"default",
|
|
2963
|
+
...lanes.map((l) => l.name)
|
|
2964
|
+
]);
|
|
2965
|
+
const unknown = options.onlyLanes.filter((l) => !declared.has(l));
|
|
2966
|
+
if (unknown.length > 0)
|
|
2967
|
+
throw new Error(
|
|
2968
|
+
`ActOptions.onlyLanes references undeclared lane(s): ${unknown.map((l) => `"${l}"`).join(", ")}`
|
|
2969
|
+
);
|
|
2970
|
+
}
|
|
2469
2971
|
this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
|
|
2470
2972
|
this._correlator = options.correlator ?? defaultCorrelator;
|
|
2471
2973
|
this._es = buildEs(this._logger, this._correlator);
|
|
@@ -2478,19 +2980,53 @@ var Act = class {
|
|
|
2478
2980
|
boundQueryArray: this._bound_query_array
|
|
2479
2981
|
});
|
|
2480
2982
|
this._handle_batch = buildHandleBatch(this._logger);
|
|
2481
|
-
const {
|
|
2983
|
+
const {
|
|
2984
|
+
staticTargets,
|
|
2985
|
+
hasDynamicResolvers,
|
|
2986
|
+
reactiveEvents,
|
|
2987
|
+
eventToState,
|
|
2988
|
+
eventToLanes
|
|
2989
|
+
} = classifyRegistry(this.registry, this._states);
|
|
2482
2990
|
this._reactive_events = reactiveEvents;
|
|
2483
2991
|
this._event_to_state = eventToState;
|
|
2484
|
-
this.
|
|
2992
|
+
this._event_to_lanes = eventToLanes;
|
|
2993
|
+
const allLanes = ["default", ...lanes.map((l) => l.name)];
|
|
2994
|
+
const onlySet = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
2995
|
+
const activeLanes = onlySet ? allLanes.filter((n) => onlySet.has(n)) : allLanes;
|
|
2996
|
+
const singleDefaultLane = activeLanes.length === 1 && activeLanes[0] === "default";
|
|
2997
|
+
this._drain_controllers = /* @__PURE__ */ new Map();
|
|
2998
|
+
for (const name of activeLanes) {
|
|
2999
|
+
const cfg = lanes.find((l) => l.name === name);
|
|
3000
|
+
const controller = new DrainController({
|
|
3001
|
+
logger: this._logger,
|
|
3002
|
+
ops: this._cd,
|
|
3003
|
+
registry: this.registry,
|
|
3004
|
+
batchHandlers: this._batch_handlers,
|
|
3005
|
+
handle: this._handle,
|
|
3006
|
+
handleBatch: this._handle_batch,
|
|
3007
|
+
onAcked: (acked) => this.emit("acked", acked),
|
|
3008
|
+
onBlocked: (blocked) => this.emit("blocked", blocked),
|
|
3009
|
+
// Pass lane only when a true per-lane controller is active.
|
|
3010
|
+
// The all-lanes (single default) case keeps lane=undefined so
|
|
3011
|
+
// adapter SQL collapses to the pre-1103 shape.
|
|
3012
|
+
lane: singleDefaultLane ? void 0 : name,
|
|
3013
|
+
defaults: cfg && {
|
|
3014
|
+
streamLimit: cfg.streamLimit,
|
|
3015
|
+
leaseMillis: cfg.leaseMillis
|
|
3016
|
+
}
|
|
3017
|
+
});
|
|
3018
|
+
if (cfg?.cycleMs !== void 0) controller.start(cfg.cycleMs);
|
|
3019
|
+
this._drain_controllers.set(name, controller);
|
|
3020
|
+
}
|
|
3021
|
+
this._audit_deps = {
|
|
3022
|
+
store: store2,
|
|
2485
3023
|
logger: this._logger,
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
onBlocked: (blocked) => this.emit("blocked", blocked)
|
|
2493
|
-
});
|
|
3024
|
+
event_to_state: eventToState,
|
|
3025
|
+
states: this._states,
|
|
3026
|
+
known_events: new Set(eventToState.keys()),
|
|
3027
|
+
declared_lanes: new Set(this._drain_controllers.keys()),
|
|
3028
|
+
routed_events: new Set(eventToLanes.keys())
|
|
3029
|
+
};
|
|
2494
3030
|
this._correlate = new CorrelateCycle(
|
|
2495
3031
|
this.registry,
|
|
2496
3032
|
staticTargets,
|
|
@@ -2499,7 +3035,7 @@ var Act = class {
|
|
|
2499
3035
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2500
3036
|
// Cold start: assume drain is needed (historical events may need processing)
|
|
2501
3037
|
() => {
|
|
2502
|
-
if (this._reactive_events.size > 0) this.
|
|
3038
|
+
if (this._reactive_events.size > 0) this._armAll();
|
|
2503
3039
|
}
|
|
2504
3040
|
);
|
|
2505
3041
|
this._settle = new SettleLoop(
|
|
@@ -2519,8 +3055,8 @@ var Act = class {
|
|
|
2519
3055
|
_emitter = new import_node_events.default();
|
|
2520
3056
|
/** Event names with at least one registered reaction (computed at build time) */
|
|
2521
3057
|
_reactive_events;
|
|
2522
|
-
/**
|
|
2523
|
-
|
|
3058
|
+
/** One DrainController per active lane, keyed by lane name. */
|
|
3059
|
+
_drain_controllers;
|
|
2524
3060
|
/** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
|
|
2525
3061
|
_correlate;
|
|
2526
3062
|
/** Debounced correlate→drain catch-up loop. */
|
|
@@ -2574,6 +3110,22 @@ var Act = class {
|
|
|
2574
3110
|
* set when seeding a `restart` snapshot in multi-state apps.
|
|
2575
3111
|
*/
|
|
2576
3112
|
_event_to_state;
|
|
3113
|
+
/**
|
|
3114
|
+
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
3115
|
+
* `classifyRegistry` once per build. `"all"` means at least one of
|
|
3116
|
+
* the event's reactions is a dynamic resolver (lane opaque until
|
|
3117
|
+
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
3118
|
+
* reactions target.
|
|
3119
|
+
*/
|
|
3120
|
+
_event_to_lanes;
|
|
3121
|
+
/**
|
|
3122
|
+
* Audit dependency bag (#723). Built once at construction; held as
|
|
3123
|
+
* an immutable snapshot of the registry state the audit module
|
|
3124
|
+
* needs. Lives in `internal/audit.ts` — this orchestrator never
|
|
3125
|
+
* carries audit logic, only the deps + a one-liner that hands them
|
|
3126
|
+
* over.
|
|
3127
|
+
*/
|
|
3128
|
+
_audit_deps;
|
|
2577
3129
|
/** Logger resolved at construction time (after user port configuration) */
|
|
2578
3130
|
_logger = log();
|
|
2579
3131
|
/** Wraps a public-method body so internal `store()`/`cache()` resolve to the
|
|
@@ -2597,6 +3149,12 @@ var Act = class {
|
|
|
2597
3149
|
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
2598
3150
|
_handle;
|
|
2599
3151
|
_handle_batch;
|
|
3152
|
+
/** Declared drain lanes (ACT-1103). */
|
|
3153
|
+
_lanes;
|
|
3154
|
+
/** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
|
|
3155
|
+
get lanes() {
|
|
3156
|
+
return this._lanes;
|
|
3157
|
+
}
|
|
2600
3158
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
2601
3159
|
_shutdown_promise;
|
|
2602
3160
|
/**
|
|
@@ -2615,6 +3173,7 @@ var Act = class {
|
|
|
2615
3173
|
this._emitter.removeAllListeners();
|
|
2616
3174
|
this.stop_correlations();
|
|
2617
3175
|
this.stop_settling();
|
|
3176
|
+
for (const c of this._drain_controllers.values()) c.stop();
|
|
2618
3177
|
const disposer = await this._notify_disposer;
|
|
2619
3178
|
if (disposer) await disposer();
|
|
2620
3179
|
})();
|
|
@@ -2634,13 +3193,10 @@ var Act = class {
|
|
|
2634
3193
|
return await s.notify((notification) => {
|
|
2635
3194
|
try {
|
|
2636
3195
|
this.emit("notified", notification);
|
|
2637
|
-
const
|
|
2638
|
-
(e) =>
|
|
3196
|
+
const armed = this._armForEventNames(
|
|
3197
|
+
notification.events.map((e) => e.name)
|
|
2639
3198
|
);
|
|
2640
|
-
if (
|
|
2641
|
-
this._drain.arm();
|
|
2642
|
-
this._settle.schedule({ debounceMs: 0 });
|
|
2643
|
-
}
|
|
3199
|
+
if (armed) this._settle.schedule({ debounceMs: 0 });
|
|
2644
3200
|
} catch (err) {
|
|
2645
3201
|
this._logger.error(err, "notified handler threw");
|
|
2646
3202
|
}
|
|
@@ -2741,14 +3297,10 @@ var Act = class {
|
|
|
2741
3297
|
reactingTo,
|
|
2742
3298
|
skipValidation
|
|
2743
3299
|
);
|
|
2744
|
-
if (this._reactive_events.size > 0)
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
break;
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
3300
|
+
if (this._reactive_events.size > 0)
|
|
3301
|
+
this._armForEventNames(
|
|
3302
|
+
snapshots.map((s) => s.event.name)
|
|
3303
|
+
);
|
|
2752
3304
|
this.emit("committed", snapshots);
|
|
2753
3305
|
return snapshots;
|
|
2754
3306
|
});
|
|
@@ -2895,7 +3447,59 @@ var Act = class {
|
|
|
2895
3447
|
* @see {@link start_correlations} for automatic correlation
|
|
2896
3448
|
*/
|
|
2897
3449
|
async drain(options = {}) {
|
|
2898
|
-
return this._scoped(() => this.
|
|
3450
|
+
return this._scoped(() => this._drainAll(options));
|
|
3451
|
+
}
|
|
3452
|
+
/** Arm every active lane controller (ACT-1103). */
|
|
3453
|
+
_armAll() {
|
|
3454
|
+
for (const c of this._drain_controllers.values()) c.arm();
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Arm only the lane controllers whose reactions match the supplied
|
|
3458
|
+
* event names (ACT-1103 selective arming). Events with any dynamic
|
|
3459
|
+
* resolver fall back to `_armAll()` via the `"all"` sentinel — the
|
|
3460
|
+
* resolver's lane isn't known until correlate runs the function.
|
|
3461
|
+
* Events with no reactions are skipped; `_event_to_lanes` doesn't
|
|
3462
|
+
* carry them. Returns true when any controller was armed (used by
|
|
3463
|
+
* the notify handler to decide whether to schedule a settle).
|
|
3464
|
+
*/
|
|
3465
|
+
_armForEventNames(names) {
|
|
3466
|
+
const to_arm = /* @__PURE__ */ new Set();
|
|
3467
|
+
for (const name of names) {
|
|
3468
|
+
const set = this._event_to_lanes.get(name);
|
|
3469
|
+
if (set === void 0) continue;
|
|
3470
|
+
if (set === ALL_LANES) {
|
|
3471
|
+
this._armAll();
|
|
3472
|
+
return true;
|
|
3473
|
+
}
|
|
3474
|
+
for (const lane of set) to_arm.add(lane);
|
|
3475
|
+
}
|
|
3476
|
+
if (to_arm.size === 0) return false;
|
|
3477
|
+
for (const lane of to_arm) this._drain_controllers.get(lane)?.arm();
|
|
3478
|
+
return true;
|
|
3479
|
+
}
|
|
3480
|
+
/** Drain every active lane controller in parallel and aggregate.
|
|
3481
|
+
*
|
|
3482
|
+
* Parallel — not sequential — so a slow lane's in-flight handler does
|
|
3483
|
+
* not block a fast lane's claim/dispatch/ack cycle. Each controller's
|
|
3484
|
+
* `claim()` is independent (filtered by lane); the store's
|
|
3485
|
+
* `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
|
|
3486
|
+
* (`acked`, `blocked`) may interleave by lane — listeners filter via
|
|
3487
|
+
* `lease.lane`. */
|
|
3488
|
+
async _drainAll(options) {
|
|
3489
|
+
const results = await Promise.all(
|
|
3490
|
+
[...this._drain_controllers.values()].map((c) => c.drain(options))
|
|
3491
|
+
);
|
|
3492
|
+
const fetched = [];
|
|
3493
|
+
const leased = [];
|
|
3494
|
+
const acked = [];
|
|
3495
|
+
const blocked = [];
|
|
3496
|
+
for (const r of results) {
|
|
3497
|
+
fetched.push(...r.fetched);
|
|
3498
|
+
leased.push(...r.leased);
|
|
3499
|
+
acked.push(...r.acked);
|
|
3500
|
+
blocked.push(...r.blocked);
|
|
3501
|
+
}
|
|
3502
|
+
return { fetched, leased, acked, blocked };
|
|
2899
3503
|
}
|
|
2900
3504
|
/**
|
|
2901
3505
|
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
@@ -3066,7 +3670,7 @@ var Act = class {
|
|
|
3066
3670
|
async reset(input) {
|
|
3067
3671
|
return this._scoped(async () => {
|
|
3068
3672
|
const count = await store2().reset(input);
|
|
3069
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
3673
|
+
if (count > 0 && this._reactive_events.size > 0) this._armAll();
|
|
3070
3674
|
return count;
|
|
3071
3675
|
});
|
|
3072
3676
|
}
|
|
@@ -3100,7 +3704,7 @@ var Act = class {
|
|
|
3100
3704
|
async unblock(input) {
|
|
3101
3705
|
return this._scoped(async () => {
|
|
3102
3706
|
const count = await store2().unblock(input);
|
|
3103
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
3707
|
+
if (count > 0 && this._reactive_events.size > 0) this._armAll();
|
|
3104
3708
|
return count;
|
|
3105
3709
|
});
|
|
3106
3710
|
}
|
|
@@ -3138,6 +3742,50 @@ var Act = class {
|
|
|
3138
3742
|
return positions;
|
|
3139
3743
|
});
|
|
3140
3744
|
}
|
|
3745
|
+
/**
|
|
3746
|
+
* Operator-driven store audit (#723).
|
|
3747
|
+
*
|
|
3748
|
+
* Walks the connected store and yields per-category findings —
|
|
3749
|
+
* each tagged with the remediation it suggests. Same operator-
|
|
3750
|
+
* driven category as `app.close()` / `app.reset()` /
|
|
3751
|
+
* `app.unblock()` / `app.blocked_streams()`: never auto-invoked by
|
|
3752
|
+
* the framework; the operator decides when to run it (CI gate,
|
|
3753
|
+
* scheduled job, ad-hoc forensics) and what to do with the
|
|
3754
|
+
* findings.
|
|
3755
|
+
*
|
|
3756
|
+
* Categories are independent — pass a subset to scope the work,
|
|
3757
|
+
* or omit to run everything:
|
|
3758
|
+
*
|
|
3759
|
+
* ```typescript
|
|
3760
|
+
* // Targeted: schema drift + deprecated-event load only
|
|
3761
|
+
* for await (const f of app.audit(["schema", "deprecated-load"], {
|
|
3762
|
+
* query: { created_after: lastScan },
|
|
3763
|
+
* thresholds: { deprecatedLoadShareMin: 0.10 },
|
|
3764
|
+
* })) {
|
|
3765
|
+
* await escalate(f);
|
|
3766
|
+
* }
|
|
3767
|
+
*
|
|
3768
|
+
* // Full audit, default thresholds
|
|
3769
|
+
* for await (const f of app.audit()) console.log(f);
|
|
3770
|
+
* ```
|
|
3771
|
+
*
|
|
3772
|
+
* Returns an `AsyncIterable` so callers can `break` early — the
|
|
3773
|
+
* underlying store paginations respect the iterator protocol and
|
|
3774
|
+
* stop cleanly. Each finding is emitted independently, so
|
|
3775
|
+
* pipelining into Slack / persistence / further analysis works
|
|
3776
|
+
* without buffering the full report in memory.
|
|
3777
|
+
*
|
|
3778
|
+
* Findings shape — see {@link AuditFinding}. The discriminated
|
|
3779
|
+
* union carries enough context for the operator to act on each
|
|
3780
|
+
* finding directly: stream id, event id, recommendation hints.
|
|
3781
|
+
*
|
|
3782
|
+
* @param categories - Subset of categories to run (default: all).
|
|
3783
|
+
* @param options - Query window + per-category thresholds.
|
|
3784
|
+
* @returns Async iterable of {@link AuditFinding}.
|
|
3785
|
+
*/
|
|
3786
|
+
audit(categories, options) {
|
|
3787
|
+
return audit(this._audit_deps, categories, options);
|
|
3788
|
+
}
|
|
3141
3789
|
/**
|
|
3142
3790
|
* Bulk-update scheduling priority for streams matching `filter`.
|
|
3143
3791
|
*
|
|
@@ -3273,6 +3921,22 @@ function registerBatchHandler(proj, batchHandlers) {
|
|
|
3273
3921
|
}
|
|
3274
3922
|
batchHandlers.set(proj.target, proj.batchHandler);
|
|
3275
3923
|
}
|
|
3924
|
+
function validateLaneReferences(registry, lanes) {
|
|
3925
|
+
const declared = /* @__PURE__ */ new Set([DEFAULT_LANE, ...lanes.map((l) => l.name)]);
|
|
3926
|
+
for (const [eventName, def] of Object.entries(registry.events)) {
|
|
3927
|
+
const entry = def;
|
|
3928
|
+
for (const [handlerName, reaction] of entry.reactions) {
|
|
3929
|
+
const resolver = reaction.resolver;
|
|
3930
|
+
if (typeof resolver === "function") continue;
|
|
3931
|
+
const lane = resolver.lane;
|
|
3932
|
+
if (lane && !declared.has(lane)) {
|
|
3933
|
+
throw new Error(
|
|
3934
|
+
`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.`
|
|
3935
|
+
);
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3276
3940
|
function act() {
|
|
3277
3941
|
const states = /* @__PURE__ */ new Map();
|
|
3278
3942
|
const registry = {
|
|
@@ -3281,6 +3945,7 @@ function act() {
|
|
|
3281
3945
|
};
|
|
3282
3946
|
const pendingProjections = [];
|
|
3283
3947
|
const batchHandlers = /* @__PURE__ */ new Map();
|
|
3948
|
+
const lanes = [];
|
|
3284
3949
|
let _built = false;
|
|
3285
3950
|
const finalizeDeprecations = () => {
|
|
3286
3951
|
const deprecationSummary = [];
|
|
@@ -3327,6 +3992,18 @@ function act() {
|
|
|
3327
3992
|
}
|
|
3328
3993
|
mergeEventRegister(registry.events, input.events);
|
|
3329
3994
|
pendingProjections.push(...input.projections);
|
|
3995
|
+
for (const sliceLane of input.lanes) {
|
|
3996
|
+
const existing = lanes.find((l) => l.name === sliceLane.name);
|
|
3997
|
+
if (!existing) {
|
|
3998
|
+
lanes.push(sliceLane);
|
|
3999
|
+
continue;
|
|
4000
|
+
}
|
|
4001
|
+
if (existing.leaseMillis !== sliceLane.leaseMillis || existing.streamLimit !== sliceLane.streamLimit || existing.cycleMs !== sliceLane.cycleMs) {
|
|
4002
|
+
throw new Error(
|
|
4003
|
+
`Lane "${sliceLane.name}" was already declared with a different config`
|
|
4004
|
+
);
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
3330
4007
|
return builder;
|
|
3331
4008
|
},
|
|
3332
4009
|
withProjection: (proj) => {
|
|
@@ -3335,6 +4012,14 @@ function act() {
|
|
|
3335
4012
|
return builder;
|
|
3336
4013
|
},
|
|
3337
4014
|
withActor: () => builder,
|
|
4015
|
+
withLane: (config2) => {
|
|
4016
|
+
if (config2.name === DEFAULT_LANE)
|
|
4017
|
+
throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
|
|
4018
|
+
if (lanes.some((l) => l.name === config2.name))
|
|
4019
|
+
throw new Error(`Lane "${config2.name}" was already declared`);
|
|
4020
|
+
lanes.push(config2);
|
|
4021
|
+
return builder;
|
|
4022
|
+
},
|
|
3338
4023
|
on: (event) => ({
|
|
3339
4024
|
do: (handler, options) => {
|
|
3340
4025
|
const reaction = {
|
|
@@ -3366,13 +4051,15 @@ function act() {
|
|
|
3366
4051
|
registerBatchHandler(proj, batchHandlers);
|
|
3367
4052
|
}
|
|
3368
4053
|
finalizeDeprecations();
|
|
4054
|
+
validateLaneReferences(registry, lanes);
|
|
3369
4055
|
_built = true;
|
|
3370
4056
|
}
|
|
3371
4057
|
return new Act(
|
|
3372
4058
|
registry,
|
|
3373
4059
|
states,
|
|
3374
4060
|
batchHandlers,
|
|
3375
|
-
options
|
|
4061
|
+
options,
|
|
4062
|
+
lanes
|
|
3376
4063
|
);
|
|
3377
4064
|
},
|
|
3378
4065
|
events: registry.events
|
|
@@ -3453,6 +4140,7 @@ function slice() {
|
|
|
3453
4140
|
const actions = {};
|
|
3454
4141
|
const events = {};
|
|
3455
4142
|
const projections = [];
|
|
4143
|
+
const lanes = [];
|
|
3456
4144
|
const builder = {
|
|
3457
4145
|
withState: (state2) => {
|
|
3458
4146
|
registerState(state2, states, actions, events);
|
|
@@ -3462,6 +4150,14 @@ function slice() {
|
|
|
3462
4150
|
projections.push(proj);
|
|
3463
4151
|
return builder;
|
|
3464
4152
|
},
|
|
4153
|
+
withLane: (config2) => {
|
|
4154
|
+
if (config2.name === DEFAULT_LANE)
|
|
4155
|
+
throw new Error(`Lane "${DEFAULT_LANE}" is reserved`);
|
|
4156
|
+
if (lanes.some((l) => l.name === config2.name))
|
|
4157
|
+
throw new Error(`Lane "${config2.name}" was already declared`);
|
|
4158
|
+
lanes.push(config2);
|
|
4159
|
+
return builder;
|
|
4160
|
+
},
|
|
3465
4161
|
on: (event) => ({
|
|
3466
4162
|
do: (handler, options) => {
|
|
3467
4163
|
const reaction = {
|
|
@@ -3490,7 +4186,8 @@ function slice() {
|
|
|
3490
4186
|
_tag: "Slice",
|
|
3491
4187
|
states,
|
|
3492
4188
|
events,
|
|
3493
|
-
projections
|
|
4189
|
+
projections,
|
|
4190
|
+
lanes
|
|
3494
4191
|
}),
|
|
3495
4192
|
events
|
|
3496
4193
|
};
|
|
@@ -3584,6 +4281,7 @@ function action_builder(state2) {
|
|
|
3584
4281
|
CommittedMetaSchema,
|
|
3585
4282
|
ConcurrencyError,
|
|
3586
4283
|
ConsoleLogger,
|
|
4284
|
+
DEFAULT_LANE,
|
|
3587
4285
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
3588
4286
|
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
3589
4287
|
Environments,
|