@rotorsoft/act 0.32.7 → 0.33.1
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/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +22 -66
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/config.d.ts +2 -0
- package/dist/@types/config.d.ts.map +1 -1
- package/dist/@types/internal/build-classify.d.ts +44 -0
- package/dist/@types/internal/build-classify.d.ts.map +1 -0
- package/dist/@types/internal/correlate-cycle.d.ts +73 -0
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -0
- package/dist/@types/internal/drain-cycle.d.ts +57 -5
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain.d.ts +2 -0
- package/dist/@types/internal/drain.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts +5 -2
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +10 -7
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/merge.d.ts.map +1 -1
- package/dist/@types/internal/reactions.d.ts +54 -0
- package/dist/@types/internal/reactions.d.ts.map +1 -0
- package/dist/@types/internal/settle.d.ts +60 -0
- package/dist/@types/internal/settle.d.ts.map +1 -0
- package/dist/@types/internal/tracing.d.ts +2 -0
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/@types/lru-map.d.ts.map +1 -0
- package/dist/@types/ports.d.ts +6 -1
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +27 -0
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/index.cjs +506 -342
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +505 -342
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/dist/@types/internal/lru-map.d.ts.map +0 -1
- /package/dist/@types/{internal/lru-map.d.ts → lru-map.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -128,7 +128,7 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
-
// src/
|
|
131
|
+
// src/lru-map.ts
|
|
132
132
|
var LruMap = class {
|
|
133
133
|
constructor(_maxSize) {
|
|
134
134
|
this._maxSize = _maxSize;
|
|
@@ -806,6 +806,37 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
806
806
|
// src/act.ts
|
|
807
807
|
import EventEmitter from "events";
|
|
808
808
|
|
|
809
|
+
// src/internal/build-classify.ts
|
|
810
|
+
function classifyRegistry(registry, states) {
|
|
811
|
+
const statics = /* @__PURE__ */ new Map();
|
|
812
|
+
const reactiveEvents = /* @__PURE__ */ new Set();
|
|
813
|
+
let hasDynamicResolvers = false;
|
|
814
|
+
for (const [name, register] of Object.entries(registry.events)) {
|
|
815
|
+
if (register.reactions.size > 0) reactiveEvents.add(name);
|
|
816
|
+
for (const reaction of register.reactions.values()) {
|
|
817
|
+
if (typeof reaction.resolver === "function") {
|
|
818
|
+
hasDynamicResolvers = true;
|
|
819
|
+
} else {
|
|
820
|
+
const { target, source } = reaction.resolver;
|
|
821
|
+
const key = `${target}|${source ?? ""}`;
|
|
822
|
+
if (!statics.has(key)) statics.set(key, { stream: target, source });
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const eventToState = /* @__PURE__ */ new Map();
|
|
827
|
+
for (const merged of states.values()) {
|
|
828
|
+
for (const eventName of Object.keys(merged.events)) {
|
|
829
|
+
eventToState.set(eventName, merged);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
staticTargets: [...statics.values()],
|
|
834
|
+
hasDynamicResolvers,
|
|
835
|
+
reactiveEvents,
|
|
836
|
+
eventToState
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
809
840
|
// src/internal/close-cycle.ts
|
|
810
841
|
import { randomUUID } from "crypto";
|
|
811
842
|
async function runCloseCycle(targets, deps) {
|
|
@@ -961,8 +992,142 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
|
|
|
961
992
|
return truncated;
|
|
962
993
|
}
|
|
963
994
|
|
|
995
|
+
// src/internal/correlate-cycle.ts
|
|
996
|
+
var CorrelateCycle = class {
|
|
997
|
+
constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
|
|
998
|
+
this.registry = registry;
|
|
999
|
+
this.staticTargets = staticTargets;
|
|
1000
|
+
this.hasDynamicResolvers = hasDynamicResolvers;
|
|
1001
|
+
this.cd = cd;
|
|
1002
|
+
this.onInit = onInit;
|
|
1003
|
+
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
1004
|
+
}
|
|
1005
|
+
_checkpoint = -1;
|
|
1006
|
+
_initialized = false;
|
|
1007
|
+
_timer = void 0;
|
|
1008
|
+
_subscribed;
|
|
1009
|
+
/** Last correlated event id. */
|
|
1010
|
+
get checkpoint() {
|
|
1011
|
+
return this._checkpoint;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Initialize correlation state on first call.
|
|
1015
|
+
* - Reads max(at) from store as cold-start checkpoint
|
|
1016
|
+
* - Subscribes static resolver targets (idempotent upsert)
|
|
1017
|
+
* - Populates the subscribed-streams LRU
|
|
1018
|
+
* - Fires `onInit` once (Act uses this to flag a cold-start drain)
|
|
1019
|
+
*/
|
|
1020
|
+
async init() {
|
|
1021
|
+
if (this._initialized) return;
|
|
1022
|
+
this._initialized = true;
|
|
1023
|
+
const { watermark } = await store().subscribe([...this.staticTargets]);
|
|
1024
|
+
this._checkpoint = watermark;
|
|
1025
|
+
this.onInit?.();
|
|
1026
|
+
for (const { stream } of this.staticTargets) {
|
|
1027
|
+
this._subscribed.add(stream);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Discover dynamic-resolver targets in the events past the checkpoint
|
|
1032
|
+
* and register any new streams via `cd.subscribe`. Static targets are
|
|
1033
|
+
* subscribed at init time, so this only walks dynamic resolvers.
|
|
1034
|
+
*/
|
|
1035
|
+
async correlate(query = { after: -1, limit: 10 }) {
|
|
1036
|
+
await this.init();
|
|
1037
|
+
if (!this.hasDynamicResolvers)
|
|
1038
|
+
return { subscribed: 0, last_id: this._checkpoint };
|
|
1039
|
+
const after = Math.max(this._checkpoint, query.after || -1);
|
|
1040
|
+
const correlated = /* @__PURE__ */ new Map();
|
|
1041
|
+
let last_id = after;
|
|
1042
|
+
await store().query(
|
|
1043
|
+
(event) => {
|
|
1044
|
+
last_id = event.id;
|
|
1045
|
+
const register = this.registry.events[event.name];
|
|
1046
|
+
if (register) {
|
|
1047
|
+
for (const reaction of register.reactions.values()) {
|
|
1048
|
+
if (typeof reaction.resolver !== "function") continue;
|
|
1049
|
+
const resolved = reaction.resolver(event);
|
|
1050
|
+
if (resolved && !this._subscribed.has(resolved.target)) {
|
|
1051
|
+
const entry = correlated.get(resolved.target) || {
|
|
1052
|
+
source: resolved.source,
|
|
1053
|
+
payloads: []
|
|
1054
|
+
};
|
|
1055
|
+
entry.payloads.push({
|
|
1056
|
+
...reaction,
|
|
1057
|
+
source: resolved.source,
|
|
1058
|
+
event
|
|
1059
|
+
});
|
|
1060
|
+
correlated.set(resolved.target, entry);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
{ ...query, after }
|
|
1066
|
+
);
|
|
1067
|
+
if (correlated.size) {
|
|
1068
|
+
const streams = [...correlated.entries()].map(([stream, { source }]) => ({
|
|
1069
|
+
stream,
|
|
1070
|
+
source
|
|
1071
|
+
}));
|
|
1072
|
+
const { subscribed } = await this.cd.subscribe(streams);
|
|
1073
|
+
this._checkpoint = last_id;
|
|
1074
|
+
if (subscribed) {
|
|
1075
|
+
for (const { stream } of streams) {
|
|
1076
|
+
this._subscribed.add(stream);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return { subscribed, last_id };
|
|
1080
|
+
}
|
|
1081
|
+
this._checkpoint = last_id;
|
|
1082
|
+
return { subscribed: 0, last_id };
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Start a periodic correlation worker. Returns false if one is already
|
|
1086
|
+
* running. Errors from `correlate()` are sent to `console.error` (matches
|
|
1087
|
+
* pre-extraction behavior; the timer keeps running on failure).
|
|
1088
|
+
*/
|
|
1089
|
+
startPolling(query = {}, frequency = 1e4, callback) {
|
|
1090
|
+
if (this._timer) return false;
|
|
1091
|
+
const limit = query.limit || 100;
|
|
1092
|
+
this._timer = setInterval(
|
|
1093
|
+
() => this.correlate({ ...query, after: this._checkpoint, limit }).then((result) => {
|
|
1094
|
+
if (callback && result.subscribed) callback(result.subscribed);
|
|
1095
|
+
}).catch(console.error),
|
|
1096
|
+
frequency
|
|
1097
|
+
);
|
|
1098
|
+
return true;
|
|
1099
|
+
}
|
|
1100
|
+
/** Stop the periodic correlation worker. Idempotent. */
|
|
1101
|
+
stopPolling() {
|
|
1102
|
+
if (this._timer) {
|
|
1103
|
+
clearInterval(this._timer);
|
|
1104
|
+
this._timer = void 0;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
|
|
964
1109
|
// src/internal/drain-cycle.ts
|
|
965
1110
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1111
|
+
|
|
1112
|
+
// src/internal/drain-ratio.ts
|
|
1113
|
+
var RATIO_MIN = 0.2;
|
|
1114
|
+
var RATIO_MAX = 0.8;
|
|
1115
|
+
var RATIO_DEFAULT = 0.5;
|
|
1116
|
+
function computeLagLeadRatio(handled, lagging, leading) {
|
|
1117
|
+
let lagging_handled = 0;
|
|
1118
|
+
let leading_handled = 0;
|
|
1119
|
+
for (const { lease, handled: count } of handled) {
|
|
1120
|
+
if (lease.lagging) lagging_handled += count;
|
|
1121
|
+
else leading_handled += count;
|
|
1122
|
+
}
|
|
1123
|
+
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1124
|
+
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1125
|
+
const total = lagging_avg + leading_avg;
|
|
1126
|
+
if (total === 0) return RATIO_DEFAULT;
|
|
1127
|
+
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/internal/drain-cycle.ts
|
|
966
1131
|
async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis) {
|
|
967
1132
|
const leased = await ops.claim(lagging, leading, randomUUID2(), leaseMillis);
|
|
968
1133
|
if (!leased.length) return void 0;
|
|
@@ -1004,24 +1169,73 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1004
1169
|
);
|
|
1005
1170
|
return { leased, fetched, handled, acked, blocked };
|
|
1006
1171
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
if (lease.lagging) lagging_handled += count;
|
|
1017
|
-
else leading_handled += count;
|
|
1172
|
+
var EMPTY_DRAIN = {
|
|
1173
|
+
fetched: [],
|
|
1174
|
+
leased: [],
|
|
1175
|
+
acked: [],
|
|
1176
|
+
blocked: []
|
|
1177
|
+
};
|
|
1178
|
+
var DrainController = class {
|
|
1179
|
+
constructor(deps) {
|
|
1180
|
+
this.deps = deps;
|
|
1018
1181
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1182
|
+
_armed = false;
|
|
1183
|
+
_locked = false;
|
|
1184
|
+
_ratio = 0.5;
|
|
1185
|
+
/**
|
|
1186
|
+
* Signal that a commit (or reset / cold-start) may have produced work.
|
|
1187
|
+
* Subsequent `drain()` calls will run the pipeline; once the pipeline
|
|
1188
|
+
* settles to no-progress, the controller disarms itself.
|
|
1189
|
+
*/
|
|
1190
|
+
arm() {
|
|
1191
|
+
this._armed = true;
|
|
1192
|
+
}
|
|
1193
|
+
/** Read-only flag — true while a commit / reset is unprocessed. */
|
|
1194
|
+
get armed() {
|
|
1195
|
+
return this._armed;
|
|
1196
|
+
}
|
|
1197
|
+
/** Run one drain pass. Short-circuits when not armed or already running. */
|
|
1198
|
+
async drain({
|
|
1199
|
+
streamLimit = 10,
|
|
1200
|
+
eventLimit = 10,
|
|
1201
|
+
leaseMillis = 1e4
|
|
1202
|
+
} = {}) {
|
|
1203
|
+
if (!this._armed) return EMPTY_DRAIN;
|
|
1204
|
+
if (this._locked) return EMPTY_DRAIN;
|
|
1205
|
+
try {
|
|
1206
|
+
this._locked = true;
|
|
1207
|
+
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
1208
|
+
const leading = streamLimit - lagging;
|
|
1209
|
+
const cycle = await runDrainCycle(
|
|
1210
|
+
this.deps.ops,
|
|
1211
|
+
this.deps.registry,
|
|
1212
|
+
this.deps.batchHandlers,
|
|
1213
|
+
this.deps.handle,
|
|
1214
|
+
this.deps.handleBatch,
|
|
1215
|
+
lagging,
|
|
1216
|
+
leading,
|
|
1217
|
+
eventLimit,
|
|
1218
|
+
leaseMillis
|
|
1219
|
+
);
|
|
1220
|
+
if (!cycle) {
|
|
1221
|
+
this._armed = false;
|
|
1222
|
+
return EMPTY_DRAIN;
|
|
1223
|
+
}
|
|
1224
|
+
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1225
|
+
this._ratio = computeLagLeadRatio(handled, lagging, leading);
|
|
1226
|
+
if (acked.length) this.deps.onAcked(acked);
|
|
1227
|
+
if (blocked.length) this.deps.onBlocked(blocked);
|
|
1228
|
+
const hasErrors = handled.some(({ error }) => error);
|
|
1229
|
+
if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
|
|
1230
|
+
return { fetched, leased, acked, blocked };
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
this.deps.logger.error(error);
|
|
1233
|
+
return EMPTY_DRAIN;
|
|
1234
|
+
} finally {
|
|
1235
|
+
this._locked = false;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1025
1239
|
|
|
1026
1240
|
// src/internal/merge.ts
|
|
1027
1241
|
import { ZodObject } from "zod";
|
|
@@ -1162,6 +1376,140 @@ var _this_ = ({ stream }) => ({
|
|
|
1162
1376
|
target: stream
|
|
1163
1377
|
});
|
|
1164
1378
|
|
|
1379
|
+
// src/internal/reactions.ts
|
|
1380
|
+
function finalize(lease, handled, at, error, options, logger) {
|
|
1381
|
+
if (!error) return { lease, handled, at };
|
|
1382
|
+
logger.error(error);
|
|
1383
|
+
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1384
|
+
if (block2)
|
|
1385
|
+
logger.error(`Blocking ${lease.stream} after ${lease.retry} retries.`);
|
|
1386
|
+
return {
|
|
1387
|
+
lease,
|
|
1388
|
+
handled,
|
|
1389
|
+
at,
|
|
1390
|
+
error: handled === 0 ? error.message : void 0,
|
|
1391
|
+
block: block2
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function buildHandle(deps) {
|
|
1395
|
+
const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
|
|
1396
|
+
return async (lease, payloads) => {
|
|
1397
|
+
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1398
|
+
const stream = lease.stream;
|
|
1399
|
+
let at = payloads.at(0).event.id;
|
|
1400
|
+
let handled = 0;
|
|
1401
|
+
if (lease.retry > 0)
|
|
1402
|
+
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1403
|
+
const scopedApp = {
|
|
1404
|
+
do: boundDo,
|
|
1405
|
+
load: boundLoad,
|
|
1406
|
+
query: boundQuery,
|
|
1407
|
+
query_array: boundQueryArray
|
|
1408
|
+
};
|
|
1409
|
+
for (const payload of payloads) {
|
|
1410
|
+
const { event, handler } = payload;
|
|
1411
|
+
scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
|
|
1412
|
+
action2,
|
|
1413
|
+
target,
|
|
1414
|
+
actionPayload,
|
|
1415
|
+
reactingTo ?? event,
|
|
1416
|
+
skipValidation
|
|
1417
|
+
);
|
|
1418
|
+
try {
|
|
1419
|
+
await handler(event, stream, scopedApp);
|
|
1420
|
+
at = event.id;
|
|
1421
|
+
handled++;
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
return finalize(
|
|
1424
|
+
lease,
|
|
1425
|
+
handled,
|
|
1426
|
+
at,
|
|
1427
|
+
error,
|
|
1428
|
+
payload.options,
|
|
1429
|
+
logger
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
function buildHandleBatch(logger) {
|
|
1437
|
+
return async (lease, payloads, batchHandler) => {
|
|
1438
|
+
const stream = lease.stream;
|
|
1439
|
+
const events = payloads.map((p) => p.event);
|
|
1440
|
+
const options = payloads[0].options;
|
|
1441
|
+
if (lease.retry > 0)
|
|
1442
|
+
logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
|
|
1443
|
+
try {
|
|
1444
|
+
await batchHandler(events, stream);
|
|
1445
|
+
return finalize(
|
|
1446
|
+
lease,
|
|
1447
|
+
events.length,
|
|
1448
|
+
events.at(-1).id,
|
|
1449
|
+
void 0,
|
|
1450
|
+
options,
|
|
1451
|
+
logger
|
|
1452
|
+
);
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
return finalize(lease, 0, lease.at, error, options, logger);
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/internal/settle.ts
|
|
1460
|
+
var SettleLoop = class {
|
|
1461
|
+
constructor(deps, defaultDebounceMs) {
|
|
1462
|
+
this.deps = deps;
|
|
1463
|
+
this.defaultDebounceMs = defaultDebounceMs;
|
|
1464
|
+
}
|
|
1465
|
+
_timer = void 0;
|
|
1466
|
+
_running = false;
|
|
1467
|
+
/**
|
|
1468
|
+
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
1469
|
+
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
1470
|
+
* until no progress is made (no new subscriptions, no acks, no blocks)
|
|
1471
|
+
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
1472
|
+
* via {@link SettleDeps.onSettled}.
|
|
1473
|
+
*/
|
|
1474
|
+
schedule(options = {}) {
|
|
1475
|
+
const {
|
|
1476
|
+
debounceMs = this.defaultDebounceMs,
|
|
1477
|
+
correlate: correlateQuery = { after: -1, limit: 100 },
|
|
1478
|
+
maxPasses = Infinity,
|
|
1479
|
+
...drainOptions
|
|
1480
|
+
} = options;
|
|
1481
|
+
if (this._timer) clearTimeout(this._timer);
|
|
1482
|
+
this._timer = setTimeout(() => {
|
|
1483
|
+
this._timer = void 0;
|
|
1484
|
+
if (this._running) return;
|
|
1485
|
+
this._running = true;
|
|
1486
|
+
(async () => {
|
|
1487
|
+
await this.deps.init();
|
|
1488
|
+
let lastDrain;
|
|
1489
|
+
for (let i = 0; i < maxPasses; i++) {
|
|
1490
|
+
const { subscribed } = await this.deps.correlate({
|
|
1491
|
+
...correlateQuery,
|
|
1492
|
+
after: this.deps.checkpoint()
|
|
1493
|
+
});
|
|
1494
|
+
lastDrain = await this.deps.drain(drainOptions);
|
|
1495
|
+
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
1496
|
+
if (!made_progress) break;
|
|
1497
|
+
}
|
|
1498
|
+
if (lastDrain) this.deps.onSettled(lastDrain);
|
|
1499
|
+
})().catch((err) => this.deps.logger.error(err)).finally(() => {
|
|
1500
|
+
this._running = false;
|
|
1501
|
+
});
|
|
1502
|
+
}, debounceMs);
|
|
1503
|
+
}
|
|
1504
|
+
/** Cancel any pending or active settle cycle. Idempotent. */
|
|
1505
|
+
stop() {
|
|
1506
|
+
if (this._timer) {
|
|
1507
|
+
clearTimeout(this._timer);
|
|
1508
|
+
this._timer = void 0;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1165
1513
|
// src/internal/drain.ts
|
|
1166
1514
|
var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
|
|
1167
1515
|
async function fetch(leased, eventLimit) {
|
|
@@ -1218,26 +1566,40 @@ async function tombstone(stream, expectedVersion, correlation) {
|
|
|
1218
1566
|
async function load(me, stream, callback, asOf) {
|
|
1219
1567
|
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
1220
1568
|
const cached = timeTravel ? void 0 : await cache().get(stream);
|
|
1569
|
+
const cache_hit = !!cached;
|
|
1221
1570
|
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
1222
1571
|
let patches = cached?.patches ?? 0;
|
|
1223
1572
|
let snaps = cached?.snaps ?? 0;
|
|
1573
|
+
let version = cached?.version ?? -1;
|
|
1574
|
+
let replayed = 0;
|
|
1224
1575
|
let event;
|
|
1225
1576
|
await store().query(
|
|
1226
1577
|
(e) => {
|
|
1227
1578
|
event = e;
|
|
1579
|
+
version = e.version;
|
|
1228
1580
|
if (e.name === SNAP_EVENT) {
|
|
1229
1581
|
state2 = e.data;
|
|
1230
1582
|
snaps++;
|
|
1231
1583
|
patches = 0;
|
|
1584
|
+
replayed++;
|
|
1232
1585
|
} else if (me.patch[e.name]) {
|
|
1233
1586
|
state2 = patch(state2, me.patch[e.name](event, state2));
|
|
1234
1587
|
patches++;
|
|
1588
|
+
replayed++;
|
|
1235
1589
|
} else if (e.name !== TOMBSTONE_EVENT) {
|
|
1236
1590
|
log().warn(
|
|
1237
1591
|
`Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
|
|
1238
1592
|
);
|
|
1239
1593
|
}
|
|
1240
|
-
callback && callback({
|
|
1594
|
+
callback && callback({
|
|
1595
|
+
event,
|
|
1596
|
+
state: state2,
|
|
1597
|
+
version,
|
|
1598
|
+
patches,
|
|
1599
|
+
snaps,
|
|
1600
|
+
cache_hit,
|
|
1601
|
+
replayed
|
|
1602
|
+
});
|
|
1241
1603
|
},
|
|
1242
1604
|
{
|
|
1243
1605
|
stream,
|
|
@@ -1245,7 +1607,16 @@ async function load(me, stream, callback, asOf) {
|
|
|
1245
1607
|
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
1246
1608
|
}
|
|
1247
1609
|
);
|
|
1248
|
-
|
|
1610
|
+
if (replayed > 0 && !timeTravel && event) {
|
|
1611
|
+
await cache().set(stream, {
|
|
1612
|
+
state: state2,
|
|
1613
|
+
version,
|
|
1614
|
+
event_id: event.id,
|
|
1615
|
+
patches,
|
|
1616
|
+
snaps
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
1249
1620
|
}
|
|
1250
1621
|
async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
|
|
1251
1622
|
const { stream, expectedVersion, actor } = target;
|
|
@@ -1317,7 +1688,16 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1317
1688
|
const p = me.patch[event.name](event, state2);
|
|
1318
1689
|
state2 = patch(state2, p);
|
|
1319
1690
|
patches++;
|
|
1320
|
-
return {
|
|
1691
|
+
return {
|
|
1692
|
+
event,
|
|
1693
|
+
state: state2,
|
|
1694
|
+
version: event.version,
|
|
1695
|
+
patches,
|
|
1696
|
+
snaps: snapshot.snaps,
|
|
1697
|
+
patch: p,
|
|
1698
|
+
cache_hit: snapshot.cache_hit,
|
|
1699
|
+
replayed: snapshot.replayed
|
|
1700
|
+
};
|
|
1321
1701
|
});
|
|
1322
1702
|
const last = snapshots.at(-1);
|
|
1323
1703
|
const snapped = me.snap && me.snap(last);
|
|
@@ -1339,12 +1719,35 @@ var C_ORANGE = "\x1B[38;5;208m";
|
|
|
1339
1719
|
var C_GREEN = "\x1B[38;5;42m";
|
|
1340
1720
|
var C_MAGENTA = "\x1B[38;5;165m";
|
|
1341
1721
|
var C_DRAIN = "\x1B[38;5;244m";
|
|
1722
|
+
var C_HIT = "\x1B[38;5;82m";
|
|
1723
|
+
var C_MISS = "\x1B[38;5;220m";
|
|
1342
1724
|
var C_RESET = "\x1B[0m";
|
|
1343
1725
|
var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
|
|
1344
1726
|
var drain_caption = (caption) => {
|
|
1345
1727
|
const tag = `>> ${caption}`;
|
|
1346
1728
|
return PRETTY ? `${C_DRAIN}${tag}${C_RESET}` : tag;
|
|
1347
1729
|
};
|
|
1730
|
+
var cache_marker = (hit) => {
|
|
1731
|
+
const word = hit ? "hit" : "miss";
|
|
1732
|
+
if (!PRETTY) return word;
|
|
1733
|
+
return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
|
|
1734
|
+
};
|
|
1735
|
+
var stats_marker = (version, replayed, snaps, patches) => {
|
|
1736
|
+
const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
|
|
1737
|
+
if (!PRETTY) return text;
|
|
1738
|
+
return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
|
|
1739
|
+
};
|
|
1740
|
+
var as_of_marker = (asOf) => {
|
|
1741
|
+
if (!asOf) return "";
|
|
1742
|
+
const parts = [];
|
|
1743
|
+
if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
|
|
1744
|
+
if (asOf.created_before !== void 0)
|
|
1745
|
+
parts.push(`created_before=${asOf.created_before.toISOString()}`);
|
|
1746
|
+
if (asOf.created_after !== void 0)
|
|
1747
|
+
parts.push(`created_after=${asOf.created_after.toISOString()}`);
|
|
1748
|
+
if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
|
|
1749
|
+
return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
|
|
1750
|
+
};
|
|
1348
1751
|
var traced = (inner, exit, entry) => (async (...args) => {
|
|
1349
1752
|
entry?.(...args);
|
|
1350
1753
|
const result = await inner(...args);
|
|
@@ -1370,9 +1773,19 @@ function buildEs(logger) {
|
|
|
1370
1773
|
)
|
|
1371
1774
|
);
|
|
1372
1775
|
}),
|
|
1373
|
-
load: traced(load,
|
|
1776
|
+
load: traced(load, (result, _me, stream, _cb, asOf) => {
|
|
1777
|
+
const stats = stats_marker(
|
|
1778
|
+
result.version,
|
|
1779
|
+
result.replayed,
|
|
1780
|
+
result.snaps,
|
|
1781
|
+
result.patches
|
|
1782
|
+
);
|
|
1374
1783
|
logger.trace(
|
|
1375
|
-
es_caption(
|
|
1784
|
+
es_caption(
|
|
1785
|
+
"load",
|
|
1786
|
+
C_GREEN,
|
|
1787
|
+
`${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
1788
|
+
)
|
|
1376
1789
|
);
|
|
1377
1790
|
}),
|
|
1378
1791
|
action: traced(
|
|
@@ -1466,37 +1879,57 @@ function buildDrain(logger) {
|
|
|
1466
1879
|
|
|
1467
1880
|
// src/act.ts
|
|
1468
1881
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
1882
|
+
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
1469
1883
|
var Act = class {
|
|
1470
1884
|
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
|
|
1471
1885
|
this.registry = registry;
|
|
1472
1886
|
this._states = _states;
|
|
1473
1887
|
this._batch_handlers = batchHandlers;
|
|
1474
|
-
this._subscribed_streams = new LruSet(
|
|
1475
|
-
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS
|
|
1476
|
-
);
|
|
1477
1888
|
this._es = buildEs(this._logger);
|
|
1478
1889
|
this._cd = buildDrain(this._logger);
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1890
|
+
this._handle = buildHandle({
|
|
1891
|
+
logger: this._logger,
|
|
1892
|
+
boundDo: this._bound_do,
|
|
1893
|
+
boundLoad: this._bound_load,
|
|
1894
|
+
boundQuery: this._bound_query,
|
|
1895
|
+
boundQueryArray: this._bound_query_array
|
|
1896
|
+
});
|
|
1897
|
+
this._handle_batch = buildHandleBatch(this._logger);
|
|
1898
|
+
const { staticTargets, hasDynamicResolvers, reactiveEvents, eventToState } = classifyRegistry(this.registry, this._states);
|
|
1899
|
+
this._reactive_events = reactiveEvents;
|
|
1900
|
+
this._event_to_state = eventToState;
|
|
1901
|
+
this._drain = new DrainController({
|
|
1902
|
+
logger: this._logger,
|
|
1903
|
+
ops: this._cd,
|
|
1904
|
+
registry: this.registry,
|
|
1905
|
+
batchHandlers: this._batch_handlers,
|
|
1906
|
+
handle: this._handle,
|
|
1907
|
+
handleBatch: this._handle_batch,
|
|
1908
|
+
onAcked: (acked) => this.emit("acked", acked),
|
|
1909
|
+
onBlocked: (blocked) => this.emit("blocked", blocked)
|
|
1910
|
+
});
|
|
1911
|
+
this._correlate = new CorrelateCycle(
|
|
1912
|
+
this.registry,
|
|
1913
|
+
staticTargets,
|
|
1914
|
+
hasDynamicResolvers,
|
|
1915
|
+
this._cd,
|
|
1916
|
+
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
1917
|
+
// Cold start: assume drain is needed (historical events may need processing)
|
|
1918
|
+
() => {
|
|
1919
|
+
if (this._reactive_events.size > 0) this._drain.arm();
|
|
1498
1920
|
}
|
|
1499
|
-
|
|
1921
|
+
);
|
|
1922
|
+
this._settle = new SettleLoop(
|
|
1923
|
+
{
|
|
1924
|
+
logger: this._logger,
|
|
1925
|
+
init: () => this._correlate.init(),
|
|
1926
|
+
checkpoint: () => this._correlate.checkpoint,
|
|
1927
|
+
correlate: (q) => this.correlate(q),
|
|
1928
|
+
drain: (o) => this.drain(o),
|
|
1929
|
+
onSettled: (drain) => this.emit("settled", drain)
|
|
1930
|
+
},
|
|
1931
|
+
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
1932
|
+
);
|
|
1500
1933
|
dispose(() => {
|
|
1501
1934
|
this._emitter.removeAllListeners();
|
|
1502
1935
|
this.stop_correlations();
|
|
@@ -1505,30 +1938,14 @@ var Act = class {
|
|
|
1505
1938
|
});
|
|
1506
1939
|
}
|
|
1507
1940
|
_emitter = new EventEmitter();
|
|
1508
|
-
_drain_locked = false;
|
|
1509
|
-
_drain_lag2lead_ratio = 0.5;
|
|
1510
|
-
_correlation_timer = void 0;
|
|
1511
|
-
_settle_timer = void 0;
|
|
1512
|
-
_settling = false;
|
|
1513
|
-
_correlation_checkpoint = -1;
|
|
1514
|
-
/**
|
|
1515
|
-
* Streams already subscribed via store.subscribe() — both the static
|
|
1516
|
-
* targets registered at init and dynamic targets discovered by
|
|
1517
|
-
* correlate(). correlate() consults this set to avoid re-subscribing
|
|
1518
|
-
* known streams.
|
|
1519
|
-
*
|
|
1520
|
-
* Bounded LRU so apps that mint millions of dynamic targets (one per
|
|
1521
|
-
* aggregate) don't grow this unbounded. Eviction costs at most one
|
|
1522
|
-
* redundant store.subscribe() call per evicted-but-still-active stream
|
|
1523
|
-
* (subscribe is idempotent). Cap configurable via {@link ActOptions}.
|
|
1524
|
-
*/
|
|
1525
|
-
_subscribed_streams;
|
|
1526
|
-
_has_dynamic_resolvers = false;
|
|
1527
|
-
_correlation_initialized = false;
|
|
1528
1941
|
/** Event names with at least one registered reaction (computed at build time) */
|
|
1529
|
-
_reactive_events
|
|
1530
|
-
/**
|
|
1531
|
-
|
|
1942
|
+
_reactive_events;
|
|
1943
|
+
/** Drain pipeline driver: armed flag, concurrency lock, adaptive ratio. */
|
|
1944
|
+
_drain;
|
|
1945
|
+
/** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
|
|
1946
|
+
_correlate;
|
|
1947
|
+
/** Debounced correlate→drain catch-up loop. */
|
|
1948
|
+
_settle;
|
|
1532
1949
|
/**
|
|
1533
1950
|
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
1534
1951
|
* via {@link ActLifecycleEvents}.
|
|
@@ -1557,8 +1974,6 @@ var Act = class {
|
|
|
1557
1974
|
* @param registry The registry of state, event, and action schemas
|
|
1558
1975
|
* @param states Map of state names to their (potentially merged) state definitions
|
|
1559
1976
|
*/
|
|
1560
|
-
/** Static resolver targets collected at build time */
|
|
1561
|
-
_static_targets;
|
|
1562
1977
|
/** Batch handlers for static-target projections (target → handler) */
|
|
1563
1978
|
_batch_handlers;
|
|
1564
1979
|
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
@@ -1571,7 +1986,7 @@ var Act = class {
|
|
|
1571
1986
|
* this lookup is unambiguous. Used by `close()` to pick the right reducer
|
|
1572
1987
|
* set when seeding a `restart` snapshot in multi-state apps.
|
|
1573
1988
|
*/
|
|
1574
|
-
_event_to_state
|
|
1989
|
+
_event_to_state;
|
|
1575
1990
|
/** Logger resolved at construction time (after user port configuration) */
|
|
1576
1991
|
_logger = log();
|
|
1577
1992
|
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
@@ -1580,9 +1995,9 @@ var Act = class {
|
|
|
1580
1995
|
_bound_load = this.load.bind(this);
|
|
1581
1996
|
_bound_query = this.query.bind(this);
|
|
1582
1997
|
_bound_query_array = this.query_array.bind(this);
|
|
1583
|
-
/**
|
|
1584
|
-
|
|
1585
|
-
|
|
1998
|
+
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
1999
|
+
_handle;
|
|
2000
|
+
_handle_batch;
|
|
1586
2001
|
/**
|
|
1587
2002
|
* Executes an action on a state instance, committing resulting events.
|
|
1588
2003
|
*
|
|
@@ -1673,10 +2088,12 @@ var Act = class {
|
|
|
1673
2088
|
reactingTo,
|
|
1674
2089
|
skipValidation
|
|
1675
2090
|
);
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
this.
|
|
1679
|
-
|
|
2091
|
+
if (this._reactive_events.size > 0) {
|
|
2092
|
+
for (const snap2 of snapshots) {
|
|
2093
|
+
if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
|
|
2094
|
+
this._drain.arm();
|
|
2095
|
+
break;
|
|
2096
|
+
}
|
|
1680
2097
|
}
|
|
1681
2098
|
}
|
|
1682
2099
|
this.emit("committed", snapshots);
|
|
@@ -1784,109 +2201,6 @@ var Act = class {
|
|
|
1784
2201
|
await store().query((e) => events.push(e), query);
|
|
1785
2202
|
return events;
|
|
1786
2203
|
}
|
|
1787
|
-
/**
|
|
1788
|
-
* Shared finalization for the two reaction-runner shapes (per-event
|
|
1789
|
-
* `handle` and bulk `handleBatch`). Centralizes the error log, retry-vs-
|
|
1790
|
-
* block decision, and the "error reported only when nothing was handled"
|
|
1791
|
-
* rule that's true in both shapes (in batch mode, `handled` is always 0
|
|
1792
|
-
* on failure, so the rule degenerates to "always reported").
|
|
1793
|
-
*/
|
|
1794
|
-
_finalize(lease, handled, at, error, options) {
|
|
1795
|
-
if (!error) return { lease, handled, at };
|
|
1796
|
-
this._logger.error(error);
|
|
1797
|
-
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1798
|
-
if (block2)
|
|
1799
|
-
this._logger.error(
|
|
1800
|
-
`Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1801
|
-
);
|
|
1802
|
-
return {
|
|
1803
|
-
lease,
|
|
1804
|
-
handled,
|
|
1805
|
-
at,
|
|
1806
|
-
error: handled === 0 ? error.message : void 0,
|
|
1807
|
-
block: block2
|
|
1808
|
-
};
|
|
1809
|
-
}
|
|
1810
|
-
/**
|
|
1811
|
-
* Handles leased reactions one event at a time.
|
|
1812
|
-
*
|
|
1813
|
-
* Called by the main `drain` loop after fetching new events. Each handler
|
|
1814
|
-
* receives a scoped `IAct` proxy that auto-injects the triggering event
|
|
1815
|
-
* as `reactingTo` when `do()` is called without it, maintaining
|
|
1816
|
-
* correlation chains by default (#587). Handlers can still pass an
|
|
1817
|
-
* explicit `reactingTo` to override.
|
|
1818
|
-
*
|
|
1819
|
-
* @internal
|
|
1820
|
-
*/
|
|
1821
|
-
async handle(lease, payloads) {
|
|
1822
|
-
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1823
|
-
const stream = lease.stream;
|
|
1824
|
-
let at = payloads.at(0).event.id;
|
|
1825
|
-
let handled = 0;
|
|
1826
|
-
if (lease.retry > 0)
|
|
1827
|
-
this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1828
|
-
const doAction = this._bound_do;
|
|
1829
|
-
const scopedApp = {
|
|
1830
|
-
do: doAction,
|
|
1831
|
-
load: this._bound_load,
|
|
1832
|
-
query: this._bound_query,
|
|
1833
|
-
query_array: this._bound_query_array
|
|
1834
|
-
};
|
|
1835
|
-
for (const payload of payloads) {
|
|
1836
|
-
const { event, handler } = payload;
|
|
1837
|
-
scopedApp.do = (action2, target, payload2, reactingTo, skipValidation) => doAction(
|
|
1838
|
-
action2,
|
|
1839
|
-
target,
|
|
1840
|
-
payload2,
|
|
1841
|
-
reactingTo ?? event,
|
|
1842
|
-
skipValidation
|
|
1843
|
-
);
|
|
1844
|
-
try {
|
|
1845
|
-
await handler(event, stream, scopedApp);
|
|
1846
|
-
at = event.id;
|
|
1847
|
-
handled++;
|
|
1848
|
-
} catch (error) {
|
|
1849
|
-
return this._finalize(
|
|
1850
|
-
lease,
|
|
1851
|
-
handled,
|
|
1852
|
-
at,
|
|
1853
|
-
error,
|
|
1854
|
-
payload.options
|
|
1855
|
-
);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
return this._finalize(lease, handled, at, void 0, payloads[0].options);
|
|
1859
|
-
}
|
|
1860
|
-
/**
|
|
1861
|
-
* Handles a batch of events for a projection with a batch handler.
|
|
1862
|
-
*
|
|
1863
|
-
* Called by `drain()` when a leased stream is a static-target projection
|
|
1864
|
-
* with a registered batch handler. All events are passed to the handler
|
|
1865
|
-
* in a single call, enabling bulk DB operations.
|
|
1866
|
-
*
|
|
1867
|
-
* @internal
|
|
1868
|
-
*/
|
|
1869
|
-
async handleBatch(lease, payloads, batchHandler) {
|
|
1870
|
-
const stream = lease.stream;
|
|
1871
|
-
const events = payloads.map((p) => p.event);
|
|
1872
|
-
const options = payloads[0].options;
|
|
1873
|
-
if (lease.retry > 0)
|
|
1874
|
-
this._logger.warn(
|
|
1875
|
-
`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
|
|
1876
|
-
);
|
|
1877
|
-
try {
|
|
1878
|
-
await batchHandler(events, stream);
|
|
1879
|
-
return this._finalize(
|
|
1880
|
-
lease,
|
|
1881
|
-
events.length,
|
|
1882
|
-
events.at(-1).id,
|
|
1883
|
-
void 0,
|
|
1884
|
-
options
|
|
1885
|
-
);
|
|
1886
|
-
} catch (error) {
|
|
1887
|
-
return this._finalize(lease, 0, lease.at, error, options);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
2204
|
/**
|
|
1891
2205
|
* Processes pending reactions by draining uncommitted events from the event store.
|
|
1892
2206
|
*
|
|
@@ -1926,54 +2240,8 @@ var Act = class {
|
|
|
1926
2240
|
* @see {@link correlate} for dynamic stream discovery
|
|
1927
2241
|
* @see {@link start_correlations} for automatic correlation
|
|
1928
2242
|
*/
|
|
1929
|
-
async drain({
|
|
1930
|
-
|
|
1931
|
-
eventLimit = 10,
|
|
1932
|
-
leaseMillis = 1e4
|
|
1933
|
-
} = {}) {
|
|
1934
|
-
if (!this._needs_drain) {
|
|
1935
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1936
|
-
}
|
|
1937
|
-
if (this._drain_locked) {
|
|
1938
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1939
|
-
}
|
|
1940
|
-
try {
|
|
1941
|
-
this._drain_locked = true;
|
|
1942
|
-
const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
|
|
1943
|
-
const leading = streamLimit - lagging;
|
|
1944
|
-
const cycle = await runDrainCycle(
|
|
1945
|
-
this._cd,
|
|
1946
|
-
this.registry,
|
|
1947
|
-
this._batch_handlers,
|
|
1948
|
-
this._bound_handle,
|
|
1949
|
-
this._bound_handle_batch,
|
|
1950
|
-
lagging,
|
|
1951
|
-
leading,
|
|
1952
|
-
eventLimit,
|
|
1953
|
-
leaseMillis
|
|
1954
|
-
);
|
|
1955
|
-
if (!cycle) {
|
|
1956
|
-
this._needs_drain = false;
|
|
1957
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1958
|
-
}
|
|
1959
|
-
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1960
|
-
this._drain_lag2lead_ratio = computeLagLeadRatio(
|
|
1961
|
-
handled,
|
|
1962
|
-
lagging,
|
|
1963
|
-
leading
|
|
1964
|
-
);
|
|
1965
|
-
if (acked.length) this.emit("acked", acked);
|
|
1966
|
-
if (blocked.length) this.emit("blocked", blocked);
|
|
1967
|
-
const hasErrors = handled.some(({ error }) => error);
|
|
1968
|
-
if (!acked.length && !blocked.length && !hasErrors)
|
|
1969
|
-
this._needs_drain = false;
|
|
1970
|
-
return { fetched, leased, acked, blocked };
|
|
1971
|
-
} catch (error) {
|
|
1972
|
-
this._logger.error(error);
|
|
1973
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1974
|
-
} finally {
|
|
1975
|
-
this._drain_locked = false;
|
|
1976
|
-
}
|
|
2243
|
+
async drain(options = {}) {
|
|
2244
|
+
return this._drain.drain(options);
|
|
1977
2245
|
}
|
|
1978
2246
|
/**
|
|
1979
2247
|
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
@@ -2020,71 +2288,8 @@ var Act = class {
|
|
|
2020
2288
|
* @see {@link start_correlations} for automatic periodic correlation
|
|
2021
2289
|
* @see {@link stop_correlations} to stop automatic correlation
|
|
2022
2290
|
*/
|
|
2023
|
-
/**
|
|
2024
|
-
* Initialize correlation state on first call.
|
|
2025
|
-
* - Reads max(at) from store as cold-start checkpoint
|
|
2026
|
-
* - Subscribes static resolver targets (idempotent upsert)
|
|
2027
|
-
* - Populates the subscribed statics set
|
|
2028
|
-
* @internal
|
|
2029
|
-
*/
|
|
2030
|
-
async _init_correlation() {
|
|
2031
|
-
if (this._correlation_initialized) return;
|
|
2032
|
-
this._correlation_initialized = true;
|
|
2033
|
-
const { watermark } = await store().subscribe(this._static_targets);
|
|
2034
|
-
this._correlation_checkpoint = watermark;
|
|
2035
|
-
if (this._reactive_events.size > 0) this._needs_drain = true;
|
|
2036
|
-
for (const { stream } of this._static_targets) {
|
|
2037
|
-
this._subscribed_streams.add(stream);
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
2291
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
2041
|
-
|
|
2042
|
-
if (!this._has_dynamic_resolvers)
|
|
2043
|
-
return { subscribed: 0, last_id: this._correlation_checkpoint };
|
|
2044
|
-
const after = Math.max(this._correlation_checkpoint, query.after || -1);
|
|
2045
|
-
const correlated = /* @__PURE__ */ new Map();
|
|
2046
|
-
let last_id = after;
|
|
2047
|
-
await store().query(
|
|
2048
|
-
(event) => {
|
|
2049
|
-
last_id = event.id;
|
|
2050
|
-
const register = this.registry.events[event.name];
|
|
2051
|
-
if (register) {
|
|
2052
|
-
for (const reaction of register.reactions.values()) {
|
|
2053
|
-
if (typeof reaction.resolver !== "function") continue;
|
|
2054
|
-
const resolved = reaction.resolver(event);
|
|
2055
|
-
if (resolved && !this._subscribed_streams.has(resolved.target)) {
|
|
2056
|
-
const entry = correlated.get(resolved.target) || {
|
|
2057
|
-
source: resolved.source,
|
|
2058
|
-
payloads: []
|
|
2059
|
-
};
|
|
2060
|
-
entry.payloads.push({
|
|
2061
|
-
...reaction,
|
|
2062
|
-
source: resolved.source,
|
|
2063
|
-
event
|
|
2064
|
-
});
|
|
2065
|
-
correlated.set(resolved.target, entry);
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
},
|
|
2070
|
-
{ ...query, after }
|
|
2071
|
-
);
|
|
2072
|
-
if (correlated.size) {
|
|
2073
|
-
const streams = [...correlated.entries()].map(([stream, { source }]) => ({
|
|
2074
|
-
stream,
|
|
2075
|
-
source
|
|
2076
|
-
}));
|
|
2077
|
-
const { subscribed } = await this._cd.subscribe(streams);
|
|
2078
|
-
this._correlation_checkpoint = last_id;
|
|
2079
|
-
if (subscribed) {
|
|
2080
|
-
for (const { stream } of streams) {
|
|
2081
|
-
this._subscribed_streams.add(stream);
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
return { subscribed, last_id };
|
|
2085
|
-
}
|
|
2086
|
-
this._correlation_checkpoint = last_id;
|
|
2087
|
-
return { subscribed: 0, last_id };
|
|
2292
|
+
return this._correlate.correlate(query);
|
|
2088
2293
|
}
|
|
2089
2294
|
/**
|
|
2090
2295
|
* Starts automatic periodic correlation worker for discovering new streams.
|
|
@@ -2142,15 +2347,7 @@ var Act = class {
|
|
|
2142
2347
|
* @see {@link stop_correlations} to stop the worker
|
|
2143
2348
|
*/
|
|
2144
2349
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
2145
|
-
|
|
2146
|
-
const limit = query.limit || 100;
|
|
2147
|
-
this._correlation_timer = setInterval(
|
|
2148
|
-
() => this.correlate({ ...query, after: this._correlation_checkpoint, limit }).then((result) => {
|
|
2149
|
-
if (callback && result.subscribed) callback(result.subscribed);
|
|
2150
|
-
}).catch(console.error),
|
|
2151
|
-
frequency
|
|
2152
|
-
);
|
|
2153
|
-
return true;
|
|
2350
|
+
return this._correlate.startPolling(query, frequency, callback);
|
|
2154
2351
|
}
|
|
2155
2352
|
/**
|
|
2156
2353
|
* Stops the automatic correlation worker.
|
|
@@ -2170,10 +2367,7 @@ var Act = class {
|
|
|
2170
2367
|
* @see {@link start_correlations}
|
|
2171
2368
|
*/
|
|
2172
2369
|
stop_correlations() {
|
|
2173
|
-
|
|
2174
|
-
clearInterval(this._correlation_timer);
|
|
2175
|
-
this._correlation_timer = void 0;
|
|
2176
|
-
}
|
|
2370
|
+
this._correlate.stopPolling();
|
|
2177
2371
|
}
|
|
2178
2372
|
/**
|
|
2179
2373
|
* Cancels any pending or active settle cycle.
|
|
@@ -2181,10 +2375,7 @@ var Act = class {
|
|
|
2181
2375
|
* @see {@link settle}
|
|
2182
2376
|
*/
|
|
2183
2377
|
stop_settling() {
|
|
2184
|
-
|
|
2185
|
-
clearTimeout(this._settle_timer);
|
|
2186
|
-
this._settle_timer = void 0;
|
|
2187
|
-
}
|
|
2378
|
+
this._settle.stop();
|
|
2188
2379
|
}
|
|
2189
2380
|
/**
|
|
2190
2381
|
* Reset reaction stream watermarks and request a drain on the next
|
|
@@ -2221,9 +2412,7 @@ var Act = class {
|
|
|
2221
2412
|
*/
|
|
2222
2413
|
async reset(streams) {
|
|
2223
2414
|
const count = await store().reset(streams);
|
|
2224
|
-
if (count > 0 && this._reactive_events.size > 0)
|
|
2225
|
-
this._needs_drain = true;
|
|
2226
|
-
}
|
|
2415
|
+
if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
|
|
2227
2416
|
return count;
|
|
2228
2417
|
}
|
|
2229
2418
|
/**
|
|
@@ -2306,34 +2495,7 @@ var Act = class {
|
|
|
2306
2495
|
* @see {@link correlate} for manual correlation
|
|
2307
2496
|
*/
|
|
2308
2497
|
settle(options = {}) {
|
|
2309
|
-
|
|
2310
|
-
debounceMs = 10,
|
|
2311
|
-
correlate: correlateQuery = { after: -1, limit: 100 },
|
|
2312
|
-
maxPasses = Infinity,
|
|
2313
|
-
...drainOptions
|
|
2314
|
-
} = options;
|
|
2315
|
-
if (this._settle_timer) clearTimeout(this._settle_timer);
|
|
2316
|
-
this._settle_timer = setTimeout(() => {
|
|
2317
|
-
this._settle_timer = void 0;
|
|
2318
|
-
if (this._settling) return;
|
|
2319
|
-
this._settling = true;
|
|
2320
|
-
(async () => {
|
|
2321
|
-
await this._init_correlation();
|
|
2322
|
-
let lastDrain;
|
|
2323
|
-
for (let i = 0; i < maxPasses; i++) {
|
|
2324
|
-
const { subscribed } = await this.correlate({
|
|
2325
|
-
...correlateQuery,
|
|
2326
|
-
after: this._correlation_checkpoint
|
|
2327
|
-
});
|
|
2328
|
-
lastDrain = await this.drain(drainOptions);
|
|
2329
|
-
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
2330
|
-
if (!made_progress) break;
|
|
2331
|
-
}
|
|
2332
|
-
if (lastDrain) this.emit("settled", lastDrain);
|
|
2333
|
-
})().catch((err) => this._logger.error(err)).finally(() => {
|
|
2334
|
-
this._settling = false;
|
|
2335
|
-
});
|
|
2336
|
-
}, debounceMs);
|
|
2498
|
+
this._settle.schedule(options);
|
|
2337
2499
|
}
|
|
2338
2500
|
};
|
|
2339
2501
|
|
|
@@ -2615,6 +2777,7 @@ export {
|
|
|
2615
2777
|
ConcurrencyError,
|
|
2616
2778
|
ConsoleLogger,
|
|
2617
2779
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2780
|
+
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
2618
2781
|
Environments,
|
|
2619
2782
|
Errors,
|
|
2620
2783
|
EventMetaSchema,
|