@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.cjs
CHANGED
|
@@ -37,6 +37,7 @@ __export(index_exports, {
|
|
|
37
37
|
ConcurrencyError: () => ConcurrencyError,
|
|
38
38
|
ConsoleLogger: () => ConsoleLogger,
|
|
39
39
|
DEFAULT_MAX_SUBSCRIBED_STREAMS: () => DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
40
|
+
DEFAULT_SETTLE_DEBOUNCE_MS: () => DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
40
41
|
Environments: () => Environments,
|
|
41
42
|
Errors: () => Errors,
|
|
42
43
|
EventMetaSchema: () => EventMetaSchema,
|
|
@@ -183,7 +184,7 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
183
184
|
}
|
|
184
185
|
};
|
|
185
186
|
|
|
186
|
-
// src/
|
|
187
|
+
// src/lru-map.ts
|
|
187
188
|
var LruMap = class {
|
|
188
189
|
constructor(_maxSize) {
|
|
189
190
|
this._maxSize = _maxSize;
|
|
@@ -973,6 +974,37 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
973
974
|
// src/act.ts
|
|
974
975
|
var import_events = __toESM(require("events"), 1);
|
|
975
976
|
|
|
977
|
+
// src/internal/build-classify.ts
|
|
978
|
+
function classifyRegistry(registry, states) {
|
|
979
|
+
const statics = /* @__PURE__ */ new Map();
|
|
980
|
+
const reactiveEvents = /* @__PURE__ */ new Set();
|
|
981
|
+
let hasDynamicResolvers = false;
|
|
982
|
+
for (const [name, register] of Object.entries(registry.events)) {
|
|
983
|
+
if (register.reactions.size > 0) reactiveEvents.add(name);
|
|
984
|
+
for (const reaction of register.reactions.values()) {
|
|
985
|
+
if (typeof reaction.resolver === "function") {
|
|
986
|
+
hasDynamicResolvers = true;
|
|
987
|
+
} else {
|
|
988
|
+
const { target, source } = reaction.resolver;
|
|
989
|
+
const key = `${target}|${source ?? ""}`;
|
|
990
|
+
if (!statics.has(key)) statics.set(key, { stream: target, source });
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const eventToState = /* @__PURE__ */ new Map();
|
|
995
|
+
for (const merged of states.values()) {
|
|
996
|
+
for (const eventName of Object.keys(merged.events)) {
|
|
997
|
+
eventToState.set(eventName, merged);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
staticTargets: [...statics.values()],
|
|
1002
|
+
hasDynamicResolvers,
|
|
1003
|
+
reactiveEvents,
|
|
1004
|
+
eventToState
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
976
1008
|
// src/internal/close-cycle.ts
|
|
977
1009
|
var import_crypto = require("crypto");
|
|
978
1010
|
async function runCloseCycle(targets, deps) {
|
|
@@ -1128,8 +1160,142 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
|
|
|
1128
1160
|
return truncated;
|
|
1129
1161
|
}
|
|
1130
1162
|
|
|
1163
|
+
// src/internal/correlate-cycle.ts
|
|
1164
|
+
var CorrelateCycle = class {
|
|
1165
|
+
constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
|
|
1166
|
+
this.registry = registry;
|
|
1167
|
+
this.staticTargets = staticTargets;
|
|
1168
|
+
this.hasDynamicResolvers = hasDynamicResolvers;
|
|
1169
|
+
this.cd = cd;
|
|
1170
|
+
this.onInit = onInit;
|
|
1171
|
+
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
1172
|
+
}
|
|
1173
|
+
_checkpoint = -1;
|
|
1174
|
+
_initialized = false;
|
|
1175
|
+
_timer = void 0;
|
|
1176
|
+
_subscribed;
|
|
1177
|
+
/** Last correlated event id. */
|
|
1178
|
+
get checkpoint() {
|
|
1179
|
+
return this._checkpoint;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Initialize correlation state on first call.
|
|
1183
|
+
* - Reads max(at) from store as cold-start checkpoint
|
|
1184
|
+
* - Subscribes static resolver targets (idempotent upsert)
|
|
1185
|
+
* - Populates the subscribed-streams LRU
|
|
1186
|
+
* - Fires `onInit` once (Act uses this to flag a cold-start drain)
|
|
1187
|
+
*/
|
|
1188
|
+
async init() {
|
|
1189
|
+
if (this._initialized) return;
|
|
1190
|
+
this._initialized = true;
|
|
1191
|
+
const { watermark } = await store().subscribe([...this.staticTargets]);
|
|
1192
|
+
this._checkpoint = watermark;
|
|
1193
|
+
this.onInit?.();
|
|
1194
|
+
for (const { stream } of this.staticTargets) {
|
|
1195
|
+
this._subscribed.add(stream);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Discover dynamic-resolver targets in the events past the checkpoint
|
|
1200
|
+
* and register any new streams via `cd.subscribe`. Static targets are
|
|
1201
|
+
* subscribed at init time, so this only walks dynamic resolvers.
|
|
1202
|
+
*/
|
|
1203
|
+
async correlate(query = { after: -1, limit: 10 }) {
|
|
1204
|
+
await this.init();
|
|
1205
|
+
if (!this.hasDynamicResolvers)
|
|
1206
|
+
return { subscribed: 0, last_id: this._checkpoint };
|
|
1207
|
+
const after = Math.max(this._checkpoint, query.after || -1);
|
|
1208
|
+
const correlated = /* @__PURE__ */ new Map();
|
|
1209
|
+
let last_id = after;
|
|
1210
|
+
await store().query(
|
|
1211
|
+
(event) => {
|
|
1212
|
+
last_id = event.id;
|
|
1213
|
+
const register = this.registry.events[event.name];
|
|
1214
|
+
if (register) {
|
|
1215
|
+
for (const reaction of register.reactions.values()) {
|
|
1216
|
+
if (typeof reaction.resolver !== "function") continue;
|
|
1217
|
+
const resolved = reaction.resolver(event);
|
|
1218
|
+
if (resolved && !this._subscribed.has(resolved.target)) {
|
|
1219
|
+
const entry = correlated.get(resolved.target) || {
|
|
1220
|
+
source: resolved.source,
|
|
1221
|
+
payloads: []
|
|
1222
|
+
};
|
|
1223
|
+
entry.payloads.push({
|
|
1224
|
+
...reaction,
|
|
1225
|
+
source: resolved.source,
|
|
1226
|
+
event
|
|
1227
|
+
});
|
|
1228
|
+
correlated.set(resolved.target, entry);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
},
|
|
1233
|
+
{ ...query, after }
|
|
1234
|
+
);
|
|
1235
|
+
if (correlated.size) {
|
|
1236
|
+
const streams = [...correlated.entries()].map(([stream, { source }]) => ({
|
|
1237
|
+
stream,
|
|
1238
|
+
source
|
|
1239
|
+
}));
|
|
1240
|
+
const { subscribed } = await this.cd.subscribe(streams);
|
|
1241
|
+
this._checkpoint = last_id;
|
|
1242
|
+
if (subscribed) {
|
|
1243
|
+
for (const { stream } of streams) {
|
|
1244
|
+
this._subscribed.add(stream);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return { subscribed, last_id };
|
|
1248
|
+
}
|
|
1249
|
+
this._checkpoint = last_id;
|
|
1250
|
+
return { subscribed: 0, last_id };
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Start a periodic correlation worker. Returns false if one is already
|
|
1254
|
+
* running. Errors from `correlate()` are sent to `console.error` (matches
|
|
1255
|
+
* pre-extraction behavior; the timer keeps running on failure).
|
|
1256
|
+
*/
|
|
1257
|
+
startPolling(query = {}, frequency = 1e4, callback) {
|
|
1258
|
+
if (this._timer) return false;
|
|
1259
|
+
const limit = query.limit || 100;
|
|
1260
|
+
this._timer = setInterval(
|
|
1261
|
+
() => this.correlate({ ...query, after: this._checkpoint, limit }).then((result) => {
|
|
1262
|
+
if (callback && result.subscribed) callback(result.subscribed);
|
|
1263
|
+
}).catch(console.error),
|
|
1264
|
+
frequency
|
|
1265
|
+
);
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
/** Stop the periodic correlation worker. Idempotent. */
|
|
1269
|
+
stopPolling() {
|
|
1270
|
+
if (this._timer) {
|
|
1271
|
+
clearInterval(this._timer);
|
|
1272
|
+
this._timer = void 0;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1131
1277
|
// src/internal/drain-cycle.ts
|
|
1132
1278
|
var import_crypto2 = require("crypto");
|
|
1279
|
+
|
|
1280
|
+
// src/internal/drain-ratio.ts
|
|
1281
|
+
var RATIO_MIN = 0.2;
|
|
1282
|
+
var RATIO_MAX = 0.8;
|
|
1283
|
+
var RATIO_DEFAULT = 0.5;
|
|
1284
|
+
function computeLagLeadRatio(handled, lagging, leading) {
|
|
1285
|
+
let lagging_handled = 0;
|
|
1286
|
+
let leading_handled = 0;
|
|
1287
|
+
for (const { lease, handled: count } of handled) {
|
|
1288
|
+
if (lease.lagging) lagging_handled += count;
|
|
1289
|
+
else leading_handled += count;
|
|
1290
|
+
}
|
|
1291
|
+
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1292
|
+
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1293
|
+
const total = lagging_avg + leading_avg;
|
|
1294
|
+
if (total === 0) return RATIO_DEFAULT;
|
|
1295
|
+
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// src/internal/drain-cycle.ts
|
|
1133
1299
|
async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis) {
|
|
1134
1300
|
const leased = await ops.claim(lagging, leading, (0, import_crypto2.randomUUID)(), leaseMillis);
|
|
1135
1301
|
if (!leased.length) return void 0;
|
|
@@ -1171,24 +1337,73 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1171
1337
|
);
|
|
1172
1338
|
return { leased, fetched, handled, acked, blocked };
|
|
1173
1339
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
if (lease.lagging) lagging_handled += count;
|
|
1184
|
-
else leading_handled += count;
|
|
1340
|
+
var EMPTY_DRAIN = {
|
|
1341
|
+
fetched: [],
|
|
1342
|
+
leased: [],
|
|
1343
|
+
acked: [],
|
|
1344
|
+
blocked: []
|
|
1345
|
+
};
|
|
1346
|
+
var DrainController = class {
|
|
1347
|
+
constructor(deps) {
|
|
1348
|
+
this.deps = deps;
|
|
1185
1349
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1350
|
+
_armed = false;
|
|
1351
|
+
_locked = false;
|
|
1352
|
+
_ratio = 0.5;
|
|
1353
|
+
/**
|
|
1354
|
+
* Signal that a commit (or reset / cold-start) may have produced work.
|
|
1355
|
+
* Subsequent `drain()` calls will run the pipeline; once the pipeline
|
|
1356
|
+
* settles to no-progress, the controller disarms itself.
|
|
1357
|
+
*/
|
|
1358
|
+
arm() {
|
|
1359
|
+
this._armed = true;
|
|
1360
|
+
}
|
|
1361
|
+
/** Read-only flag — true while a commit / reset is unprocessed. */
|
|
1362
|
+
get armed() {
|
|
1363
|
+
return this._armed;
|
|
1364
|
+
}
|
|
1365
|
+
/** Run one drain pass. Short-circuits when not armed or already running. */
|
|
1366
|
+
async drain({
|
|
1367
|
+
streamLimit = 10,
|
|
1368
|
+
eventLimit = 10,
|
|
1369
|
+
leaseMillis = 1e4
|
|
1370
|
+
} = {}) {
|
|
1371
|
+
if (!this._armed) return EMPTY_DRAIN;
|
|
1372
|
+
if (this._locked) return EMPTY_DRAIN;
|
|
1373
|
+
try {
|
|
1374
|
+
this._locked = true;
|
|
1375
|
+
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
1376
|
+
const leading = streamLimit - lagging;
|
|
1377
|
+
const cycle = await runDrainCycle(
|
|
1378
|
+
this.deps.ops,
|
|
1379
|
+
this.deps.registry,
|
|
1380
|
+
this.deps.batchHandlers,
|
|
1381
|
+
this.deps.handle,
|
|
1382
|
+
this.deps.handleBatch,
|
|
1383
|
+
lagging,
|
|
1384
|
+
leading,
|
|
1385
|
+
eventLimit,
|
|
1386
|
+
leaseMillis
|
|
1387
|
+
);
|
|
1388
|
+
if (!cycle) {
|
|
1389
|
+
this._armed = false;
|
|
1390
|
+
return EMPTY_DRAIN;
|
|
1391
|
+
}
|
|
1392
|
+
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1393
|
+
this._ratio = computeLagLeadRatio(handled, lagging, leading);
|
|
1394
|
+
if (acked.length) this.deps.onAcked(acked);
|
|
1395
|
+
if (blocked.length) this.deps.onBlocked(blocked);
|
|
1396
|
+
const hasErrors = handled.some(({ error }) => error);
|
|
1397
|
+
if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
|
|
1398
|
+
return { fetched, leased, acked, blocked };
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
this.deps.logger.error(error);
|
|
1401
|
+
return EMPTY_DRAIN;
|
|
1402
|
+
} finally {
|
|
1403
|
+
this._locked = false;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1192
1407
|
|
|
1193
1408
|
// src/internal/merge.ts
|
|
1194
1409
|
var import_zod4 = require("zod");
|
|
@@ -1329,6 +1544,140 @@ var _this_ = ({ stream }) => ({
|
|
|
1329
1544
|
target: stream
|
|
1330
1545
|
});
|
|
1331
1546
|
|
|
1547
|
+
// src/internal/reactions.ts
|
|
1548
|
+
function finalize(lease, handled, at, error, options, logger) {
|
|
1549
|
+
if (!error) return { lease, handled, at };
|
|
1550
|
+
logger.error(error);
|
|
1551
|
+
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1552
|
+
if (block2)
|
|
1553
|
+
logger.error(`Blocking ${lease.stream} after ${lease.retry} retries.`);
|
|
1554
|
+
return {
|
|
1555
|
+
lease,
|
|
1556
|
+
handled,
|
|
1557
|
+
at,
|
|
1558
|
+
error: handled === 0 ? error.message : void 0,
|
|
1559
|
+
block: block2
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
function buildHandle(deps) {
|
|
1563
|
+
const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
|
|
1564
|
+
return async (lease, payloads) => {
|
|
1565
|
+
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1566
|
+
const stream = lease.stream;
|
|
1567
|
+
let at = payloads.at(0).event.id;
|
|
1568
|
+
let handled = 0;
|
|
1569
|
+
if (lease.retry > 0)
|
|
1570
|
+
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1571
|
+
const scopedApp = {
|
|
1572
|
+
do: boundDo,
|
|
1573
|
+
load: boundLoad,
|
|
1574
|
+
query: boundQuery,
|
|
1575
|
+
query_array: boundQueryArray
|
|
1576
|
+
};
|
|
1577
|
+
for (const payload of payloads) {
|
|
1578
|
+
const { event, handler } = payload;
|
|
1579
|
+
scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
|
|
1580
|
+
action2,
|
|
1581
|
+
target,
|
|
1582
|
+
actionPayload,
|
|
1583
|
+
reactingTo ?? event,
|
|
1584
|
+
skipValidation
|
|
1585
|
+
);
|
|
1586
|
+
try {
|
|
1587
|
+
await handler(event, stream, scopedApp);
|
|
1588
|
+
at = event.id;
|
|
1589
|
+
handled++;
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
return finalize(
|
|
1592
|
+
lease,
|
|
1593
|
+
handled,
|
|
1594
|
+
at,
|
|
1595
|
+
error,
|
|
1596
|
+
payload.options,
|
|
1597
|
+
logger
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
function buildHandleBatch(logger) {
|
|
1605
|
+
return async (lease, payloads, batchHandler) => {
|
|
1606
|
+
const stream = lease.stream;
|
|
1607
|
+
const events = payloads.map((p) => p.event);
|
|
1608
|
+
const options = payloads[0].options;
|
|
1609
|
+
if (lease.retry > 0)
|
|
1610
|
+
logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
|
|
1611
|
+
try {
|
|
1612
|
+
await batchHandler(events, stream);
|
|
1613
|
+
return finalize(
|
|
1614
|
+
lease,
|
|
1615
|
+
events.length,
|
|
1616
|
+
events.at(-1).id,
|
|
1617
|
+
void 0,
|
|
1618
|
+
options,
|
|
1619
|
+
logger
|
|
1620
|
+
);
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
return finalize(lease, 0, lease.at, error, options, logger);
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// src/internal/settle.ts
|
|
1628
|
+
var SettleLoop = class {
|
|
1629
|
+
constructor(deps, defaultDebounceMs) {
|
|
1630
|
+
this.deps = deps;
|
|
1631
|
+
this.defaultDebounceMs = defaultDebounceMs;
|
|
1632
|
+
}
|
|
1633
|
+
_timer = void 0;
|
|
1634
|
+
_running = false;
|
|
1635
|
+
/**
|
|
1636
|
+
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
1637
|
+
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
1638
|
+
* until no progress is made (no new subscriptions, no acks, no blocks)
|
|
1639
|
+
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
1640
|
+
* via {@link SettleDeps.onSettled}.
|
|
1641
|
+
*/
|
|
1642
|
+
schedule(options = {}) {
|
|
1643
|
+
const {
|
|
1644
|
+
debounceMs = this.defaultDebounceMs,
|
|
1645
|
+
correlate: correlateQuery = { after: -1, limit: 100 },
|
|
1646
|
+
maxPasses = Infinity,
|
|
1647
|
+
...drainOptions
|
|
1648
|
+
} = options;
|
|
1649
|
+
if (this._timer) clearTimeout(this._timer);
|
|
1650
|
+
this._timer = setTimeout(() => {
|
|
1651
|
+
this._timer = void 0;
|
|
1652
|
+
if (this._running) return;
|
|
1653
|
+
this._running = true;
|
|
1654
|
+
(async () => {
|
|
1655
|
+
await this.deps.init();
|
|
1656
|
+
let lastDrain;
|
|
1657
|
+
for (let i = 0; i < maxPasses; i++) {
|
|
1658
|
+
const { subscribed } = await this.deps.correlate({
|
|
1659
|
+
...correlateQuery,
|
|
1660
|
+
after: this.deps.checkpoint()
|
|
1661
|
+
});
|
|
1662
|
+
lastDrain = await this.deps.drain(drainOptions);
|
|
1663
|
+
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
1664
|
+
if (!made_progress) break;
|
|
1665
|
+
}
|
|
1666
|
+
if (lastDrain) this.deps.onSettled(lastDrain);
|
|
1667
|
+
})().catch((err) => this.deps.logger.error(err)).finally(() => {
|
|
1668
|
+
this._running = false;
|
|
1669
|
+
});
|
|
1670
|
+
}, debounceMs);
|
|
1671
|
+
}
|
|
1672
|
+
/** Cancel any pending or active settle cycle. Idempotent. */
|
|
1673
|
+
stop() {
|
|
1674
|
+
if (this._timer) {
|
|
1675
|
+
clearTimeout(this._timer);
|
|
1676
|
+
this._timer = void 0;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1332
1681
|
// src/internal/drain.ts
|
|
1333
1682
|
var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
|
|
1334
1683
|
async function fetch(leased, eventLimit) {
|
|
@@ -1385,26 +1734,40 @@ async function tombstone(stream, expectedVersion, correlation) {
|
|
|
1385
1734
|
async function load(me, stream, callback, asOf) {
|
|
1386
1735
|
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
1387
1736
|
const cached = timeTravel ? void 0 : await cache().get(stream);
|
|
1737
|
+
const cache_hit = !!cached;
|
|
1388
1738
|
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
1389
1739
|
let patches = cached?.patches ?? 0;
|
|
1390
1740
|
let snaps = cached?.snaps ?? 0;
|
|
1741
|
+
let version = cached?.version ?? -1;
|
|
1742
|
+
let replayed = 0;
|
|
1391
1743
|
let event;
|
|
1392
1744
|
await store().query(
|
|
1393
1745
|
(e) => {
|
|
1394
1746
|
event = e;
|
|
1747
|
+
version = e.version;
|
|
1395
1748
|
if (e.name === SNAP_EVENT) {
|
|
1396
1749
|
state2 = e.data;
|
|
1397
1750
|
snaps++;
|
|
1398
1751
|
patches = 0;
|
|
1752
|
+
replayed++;
|
|
1399
1753
|
} else if (me.patch[e.name]) {
|
|
1400
1754
|
state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](event, state2));
|
|
1401
1755
|
patches++;
|
|
1756
|
+
replayed++;
|
|
1402
1757
|
} else if (e.name !== TOMBSTONE_EVENT) {
|
|
1403
1758
|
log().warn(
|
|
1404
1759
|
`Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
|
|
1405
1760
|
);
|
|
1406
1761
|
}
|
|
1407
|
-
callback && callback({
|
|
1762
|
+
callback && callback({
|
|
1763
|
+
event,
|
|
1764
|
+
state: state2,
|
|
1765
|
+
version,
|
|
1766
|
+
patches,
|
|
1767
|
+
snaps,
|
|
1768
|
+
cache_hit,
|
|
1769
|
+
replayed
|
|
1770
|
+
});
|
|
1408
1771
|
},
|
|
1409
1772
|
{
|
|
1410
1773
|
stream,
|
|
@@ -1412,7 +1775,16 @@ async function load(me, stream, callback, asOf) {
|
|
|
1412
1775
|
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
1413
1776
|
}
|
|
1414
1777
|
);
|
|
1415
|
-
|
|
1778
|
+
if (replayed > 0 && !timeTravel && event) {
|
|
1779
|
+
await cache().set(stream, {
|
|
1780
|
+
state: state2,
|
|
1781
|
+
version,
|
|
1782
|
+
event_id: event.id,
|
|
1783
|
+
patches,
|
|
1784
|
+
snaps
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
1416
1788
|
}
|
|
1417
1789
|
async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
|
|
1418
1790
|
const { stream, expectedVersion, actor } = target;
|
|
@@ -1484,7 +1856,16 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1484
1856
|
const p = me.patch[event.name](event, state2);
|
|
1485
1857
|
state2 = (0, import_act_patch.patch)(state2, p);
|
|
1486
1858
|
patches++;
|
|
1487
|
-
return {
|
|
1859
|
+
return {
|
|
1860
|
+
event,
|
|
1861
|
+
state: state2,
|
|
1862
|
+
version: event.version,
|
|
1863
|
+
patches,
|
|
1864
|
+
snaps: snapshot.snaps,
|
|
1865
|
+
patch: p,
|
|
1866
|
+
cache_hit: snapshot.cache_hit,
|
|
1867
|
+
replayed: snapshot.replayed
|
|
1868
|
+
};
|
|
1488
1869
|
});
|
|
1489
1870
|
const last = snapshots.at(-1);
|
|
1490
1871
|
const snapped = me.snap && me.snap(last);
|
|
@@ -1506,12 +1887,35 @@ var C_ORANGE = "\x1B[38;5;208m";
|
|
|
1506
1887
|
var C_GREEN = "\x1B[38;5;42m";
|
|
1507
1888
|
var C_MAGENTA = "\x1B[38;5;165m";
|
|
1508
1889
|
var C_DRAIN = "\x1B[38;5;244m";
|
|
1890
|
+
var C_HIT = "\x1B[38;5;82m";
|
|
1891
|
+
var C_MISS = "\x1B[38;5;220m";
|
|
1509
1892
|
var C_RESET = "\x1B[0m";
|
|
1510
1893
|
var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
|
|
1511
1894
|
var drain_caption = (caption) => {
|
|
1512
1895
|
const tag = `>> ${caption}`;
|
|
1513
1896
|
return PRETTY ? `${C_DRAIN}${tag}${C_RESET}` : tag;
|
|
1514
1897
|
};
|
|
1898
|
+
var cache_marker = (hit) => {
|
|
1899
|
+
const word = hit ? "hit" : "miss";
|
|
1900
|
+
if (!PRETTY) return word;
|
|
1901
|
+
return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
|
|
1902
|
+
};
|
|
1903
|
+
var stats_marker = (version, replayed, snaps, patches) => {
|
|
1904
|
+
const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
|
|
1905
|
+
if (!PRETTY) return text;
|
|
1906
|
+
return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
|
|
1907
|
+
};
|
|
1908
|
+
var as_of_marker = (asOf) => {
|
|
1909
|
+
if (!asOf) return "";
|
|
1910
|
+
const parts = [];
|
|
1911
|
+
if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
|
|
1912
|
+
if (asOf.created_before !== void 0)
|
|
1913
|
+
parts.push(`created_before=${asOf.created_before.toISOString()}`);
|
|
1914
|
+
if (asOf.created_after !== void 0)
|
|
1915
|
+
parts.push(`created_after=${asOf.created_after.toISOString()}`);
|
|
1916
|
+
if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
|
|
1917
|
+
return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
|
|
1918
|
+
};
|
|
1515
1919
|
var traced = (inner, exit, entry) => (async (...args) => {
|
|
1516
1920
|
entry?.(...args);
|
|
1517
1921
|
const result = await inner(...args);
|
|
@@ -1537,9 +1941,19 @@ function buildEs(logger) {
|
|
|
1537
1941
|
)
|
|
1538
1942
|
);
|
|
1539
1943
|
}),
|
|
1540
|
-
load: traced(load,
|
|
1944
|
+
load: traced(load, (result, _me, stream, _cb, asOf) => {
|
|
1945
|
+
const stats = stats_marker(
|
|
1946
|
+
result.version,
|
|
1947
|
+
result.replayed,
|
|
1948
|
+
result.snaps,
|
|
1949
|
+
result.patches
|
|
1950
|
+
);
|
|
1541
1951
|
logger.trace(
|
|
1542
|
-
es_caption(
|
|
1952
|
+
es_caption(
|
|
1953
|
+
"load",
|
|
1954
|
+
C_GREEN,
|
|
1955
|
+
`${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
1956
|
+
)
|
|
1543
1957
|
);
|
|
1544
1958
|
}),
|
|
1545
1959
|
action: traced(
|
|
@@ -1633,37 +2047,57 @@ function buildDrain(logger) {
|
|
|
1633
2047
|
|
|
1634
2048
|
// src/act.ts
|
|
1635
2049
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
2050
|
+
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
1636
2051
|
var Act = class {
|
|
1637
2052
|
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
|
|
1638
2053
|
this.registry = registry;
|
|
1639
2054
|
this._states = _states;
|
|
1640
2055
|
this._batch_handlers = batchHandlers;
|
|
1641
|
-
this._subscribed_streams = new LruSet(
|
|
1642
|
-
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS
|
|
1643
|
-
);
|
|
1644
2056
|
this._es = buildEs(this._logger);
|
|
1645
2057
|
this._cd = buildDrain(this._logger);
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
2058
|
+
this._handle = buildHandle({
|
|
2059
|
+
logger: this._logger,
|
|
2060
|
+
boundDo: this._bound_do,
|
|
2061
|
+
boundLoad: this._bound_load,
|
|
2062
|
+
boundQuery: this._bound_query,
|
|
2063
|
+
boundQueryArray: this._bound_query_array
|
|
2064
|
+
});
|
|
2065
|
+
this._handle_batch = buildHandleBatch(this._logger);
|
|
2066
|
+
const { staticTargets, hasDynamicResolvers, reactiveEvents, eventToState } = classifyRegistry(this.registry, this._states);
|
|
2067
|
+
this._reactive_events = reactiveEvents;
|
|
2068
|
+
this._event_to_state = eventToState;
|
|
2069
|
+
this._drain = new DrainController({
|
|
2070
|
+
logger: this._logger,
|
|
2071
|
+
ops: this._cd,
|
|
2072
|
+
registry: this.registry,
|
|
2073
|
+
batchHandlers: this._batch_handlers,
|
|
2074
|
+
handle: this._handle,
|
|
2075
|
+
handleBatch: this._handle_batch,
|
|
2076
|
+
onAcked: (acked) => this.emit("acked", acked),
|
|
2077
|
+
onBlocked: (blocked) => this.emit("blocked", blocked)
|
|
2078
|
+
});
|
|
2079
|
+
this._correlate = new CorrelateCycle(
|
|
2080
|
+
this.registry,
|
|
2081
|
+
staticTargets,
|
|
2082
|
+
hasDynamicResolvers,
|
|
2083
|
+
this._cd,
|
|
2084
|
+
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2085
|
+
// Cold start: assume drain is needed (historical events may need processing)
|
|
2086
|
+
() => {
|
|
2087
|
+
if (this._reactive_events.size > 0) this._drain.arm();
|
|
1665
2088
|
}
|
|
1666
|
-
|
|
2089
|
+
);
|
|
2090
|
+
this._settle = new SettleLoop(
|
|
2091
|
+
{
|
|
2092
|
+
logger: this._logger,
|
|
2093
|
+
init: () => this._correlate.init(),
|
|
2094
|
+
checkpoint: () => this._correlate.checkpoint,
|
|
2095
|
+
correlate: (q) => this.correlate(q),
|
|
2096
|
+
drain: (o) => this.drain(o),
|
|
2097
|
+
onSettled: (drain) => this.emit("settled", drain)
|
|
2098
|
+
},
|
|
2099
|
+
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
2100
|
+
);
|
|
1667
2101
|
dispose(() => {
|
|
1668
2102
|
this._emitter.removeAllListeners();
|
|
1669
2103
|
this.stop_correlations();
|
|
@@ -1672,30 +2106,14 @@ var Act = class {
|
|
|
1672
2106
|
});
|
|
1673
2107
|
}
|
|
1674
2108
|
_emitter = new import_events.default();
|
|
1675
|
-
_drain_locked = false;
|
|
1676
|
-
_drain_lag2lead_ratio = 0.5;
|
|
1677
|
-
_correlation_timer = void 0;
|
|
1678
|
-
_settle_timer = void 0;
|
|
1679
|
-
_settling = false;
|
|
1680
|
-
_correlation_checkpoint = -1;
|
|
1681
|
-
/**
|
|
1682
|
-
* Streams already subscribed via store.subscribe() — both the static
|
|
1683
|
-
* targets registered at init and dynamic targets discovered by
|
|
1684
|
-
* correlate(). correlate() consults this set to avoid re-subscribing
|
|
1685
|
-
* known streams.
|
|
1686
|
-
*
|
|
1687
|
-
* Bounded LRU so apps that mint millions of dynamic targets (one per
|
|
1688
|
-
* aggregate) don't grow this unbounded. Eviction costs at most one
|
|
1689
|
-
* redundant store.subscribe() call per evicted-but-still-active stream
|
|
1690
|
-
* (subscribe is idempotent). Cap configurable via {@link ActOptions}.
|
|
1691
|
-
*/
|
|
1692
|
-
_subscribed_streams;
|
|
1693
|
-
_has_dynamic_resolvers = false;
|
|
1694
|
-
_correlation_initialized = false;
|
|
1695
2109
|
/** Event names with at least one registered reaction (computed at build time) */
|
|
1696
|
-
_reactive_events
|
|
1697
|
-
/**
|
|
1698
|
-
|
|
2110
|
+
_reactive_events;
|
|
2111
|
+
/** Drain pipeline driver: armed flag, concurrency lock, adaptive ratio. */
|
|
2112
|
+
_drain;
|
|
2113
|
+
/** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
|
|
2114
|
+
_correlate;
|
|
2115
|
+
/** Debounced correlate→drain catch-up loop. */
|
|
2116
|
+
_settle;
|
|
1699
2117
|
/**
|
|
1700
2118
|
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
1701
2119
|
* via {@link ActLifecycleEvents}.
|
|
@@ -1724,8 +2142,6 @@ var Act = class {
|
|
|
1724
2142
|
* @param registry The registry of state, event, and action schemas
|
|
1725
2143
|
* @param states Map of state names to their (potentially merged) state definitions
|
|
1726
2144
|
*/
|
|
1727
|
-
/** Static resolver targets collected at build time */
|
|
1728
|
-
_static_targets;
|
|
1729
2145
|
/** Batch handlers for static-target projections (target → handler) */
|
|
1730
2146
|
_batch_handlers;
|
|
1731
2147
|
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
@@ -1738,7 +2154,7 @@ var Act = class {
|
|
|
1738
2154
|
* this lookup is unambiguous. Used by `close()` to pick the right reducer
|
|
1739
2155
|
* set when seeding a `restart` snapshot in multi-state apps.
|
|
1740
2156
|
*/
|
|
1741
|
-
_event_to_state
|
|
2157
|
+
_event_to_state;
|
|
1742
2158
|
/** Logger resolved at construction time (after user port configuration) */
|
|
1743
2159
|
_logger = log();
|
|
1744
2160
|
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
@@ -1747,9 +2163,9 @@ var Act = class {
|
|
|
1747
2163
|
_bound_load = this.load.bind(this);
|
|
1748
2164
|
_bound_query = this.query.bind(this);
|
|
1749
2165
|
_bound_query_array = this.query_array.bind(this);
|
|
1750
|
-
/**
|
|
1751
|
-
|
|
1752
|
-
|
|
2166
|
+
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
2167
|
+
_handle;
|
|
2168
|
+
_handle_batch;
|
|
1753
2169
|
/**
|
|
1754
2170
|
* Executes an action on a state instance, committing resulting events.
|
|
1755
2171
|
*
|
|
@@ -1840,10 +2256,12 @@ var Act = class {
|
|
|
1840
2256
|
reactingTo,
|
|
1841
2257
|
skipValidation
|
|
1842
2258
|
);
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
this.
|
|
1846
|
-
|
|
2259
|
+
if (this._reactive_events.size > 0) {
|
|
2260
|
+
for (const snap2 of snapshots) {
|
|
2261
|
+
if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
|
|
2262
|
+
this._drain.arm();
|
|
2263
|
+
break;
|
|
2264
|
+
}
|
|
1847
2265
|
}
|
|
1848
2266
|
}
|
|
1849
2267
|
this.emit("committed", snapshots);
|
|
@@ -1951,109 +2369,6 @@ var Act = class {
|
|
|
1951
2369
|
await store().query((e) => events.push(e), query);
|
|
1952
2370
|
return events;
|
|
1953
2371
|
}
|
|
1954
|
-
/**
|
|
1955
|
-
* Shared finalization for the two reaction-runner shapes (per-event
|
|
1956
|
-
* `handle` and bulk `handleBatch`). Centralizes the error log, retry-vs-
|
|
1957
|
-
* block decision, and the "error reported only when nothing was handled"
|
|
1958
|
-
* rule that's true in both shapes (in batch mode, `handled` is always 0
|
|
1959
|
-
* on failure, so the rule degenerates to "always reported").
|
|
1960
|
-
*/
|
|
1961
|
-
_finalize(lease, handled, at, error, options) {
|
|
1962
|
-
if (!error) return { lease, handled, at };
|
|
1963
|
-
this._logger.error(error);
|
|
1964
|
-
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1965
|
-
if (block2)
|
|
1966
|
-
this._logger.error(
|
|
1967
|
-
`Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1968
|
-
);
|
|
1969
|
-
return {
|
|
1970
|
-
lease,
|
|
1971
|
-
handled,
|
|
1972
|
-
at,
|
|
1973
|
-
error: handled === 0 ? error.message : void 0,
|
|
1974
|
-
block: block2
|
|
1975
|
-
};
|
|
1976
|
-
}
|
|
1977
|
-
/**
|
|
1978
|
-
* Handles leased reactions one event at a time.
|
|
1979
|
-
*
|
|
1980
|
-
* Called by the main `drain` loop after fetching new events. Each handler
|
|
1981
|
-
* receives a scoped `IAct` proxy that auto-injects the triggering event
|
|
1982
|
-
* as `reactingTo` when `do()` is called without it, maintaining
|
|
1983
|
-
* correlation chains by default (#587). Handlers can still pass an
|
|
1984
|
-
* explicit `reactingTo` to override.
|
|
1985
|
-
*
|
|
1986
|
-
* @internal
|
|
1987
|
-
*/
|
|
1988
|
-
async handle(lease, payloads) {
|
|
1989
|
-
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1990
|
-
const stream = lease.stream;
|
|
1991
|
-
let at = payloads.at(0).event.id;
|
|
1992
|
-
let handled = 0;
|
|
1993
|
-
if (lease.retry > 0)
|
|
1994
|
-
this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1995
|
-
const doAction = this._bound_do;
|
|
1996
|
-
const scopedApp = {
|
|
1997
|
-
do: doAction,
|
|
1998
|
-
load: this._bound_load,
|
|
1999
|
-
query: this._bound_query,
|
|
2000
|
-
query_array: this._bound_query_array
|
|
2001
|
-
};
|
|
2002
|
-
for (const payload of payloads) {
|
|
2003
|
-
const { event, handler } = payload;
|
|
2004
|
-
scopedApp.do = (action2, target, payload2, reactingTo, skipValidation) => doAction(
|
|
2005
|
-
action2,
|
|
2006
|
-
target,
|
|
2007
|
-
payload2,
|
|
2008
|
-
reactingTo ?? event,
|
|
2009
|
-
skipValidation
|
|
2010
|
-
);
|
|
2011
|
-
try {
|
|
2012
|
-
await handler(event, stream, scopedApp);
|
|
2013
|
-
at = event.id;
|
|
2014
|
-
handled++;
|
|
2015
|
-
} catch (error) {
|
|
2016
|
-
return this._finalize(
|
|
2017
|
-
lease,
|
|
2018
|
-
handled,
|
|
2019
|
-
at,
|
|
2020
|
-
error,
|
|
2021
|
-
payload.options
|
|
2022
|
-
);
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
return this._finalize(lease, handled, at, void 0, payloads[0].options);
|
|
2026
|
-
}
|
|
2027
|
-
/**
|
|
2028
|
-
* Handles a batch of events for a projection with a batch handler.
|
|
2029
|
-
*
|
|
2030
|
-
* Called by `drain()` when a leased stream is a static-target projection
|
|
2031
|
-
* with a registered batch handler. All events are passed to the handler
|
|
2032
|
-
* in a single call, enabling bulk DB operations.
|
|
2033
|
-
*
|
|
2034
|
-
* @internal
|
|
2035
|
-
*/
|
|
2036
|
-
async handleBatch(lease, payloads, batchHandler) {
|
|
2037
|
-
const stream = lease.stream;
|
|
2038
|
-
const events = payloads.map((p) => p.event);
|
|
2039
|
-
const options = payloads[0].options;
|
|
2040
|
-
if (lease.retry > 0)
|
|
2041
|
-
this._logger.warn(
|
|
2042
|
-
`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
|
|
2043
|
-
);
|
|
2044
|
-
try {
|
|
2045
|
-
await batchHandler(events, stream);
|
|
2046
|
-
return this._finalize(
|
|
2047
|
-
lease,
|
|
2048
|
-
events.length,
|
|
2049
|
-
events.at(-1).id,
|
|
2050
|
-
void 0,
|
|
2051
|
-
options
|
|
2052
|
-
);
|
|
2053
|
-
} catch (error) {
|
|
2054
|
-
return this._finalize(lease, 0, lease.at, error, options);
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
2372
|
/**
|
|
2058
2373
|
* Processes pending reactions by draining uncommitted events from the event store.
|
|
2059
2374
|
*
|
|
@@ -2093,54 +2408,8 @@ var Act = class {
|
|
|
2093
2408
|
* @see {@link correlate} for dynamic stream discovery
|
|
2094
2409
|
* @see {@link start_correlations} for automatic correlation
|
|
2095
2410
|
*/
|
|
2096
|
-
async drain({
|
|
2097
|
-
|
|
2098
|
-
eventLimit = 10,
|
|
2099
|
-
leaseMillis = 1e4
|
|
2100
|
-
} = {}) {
|
|
2101
|
-
if (!this._needs_drain) {
|
|
2102
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2103
|
-
}
|
|
2104
|
-
if (this._drain_locked) {
|
|
2105
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2106
|
-
}
|
|
2107
|
-
try {
|
|
2108
|
-
this._drain_locked = true;
|
|
2109
|
-
const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
|
|
2110
|
-
const leading = streamLimit - lagging;
|
|
2111
|
-
const cycle = await runDrainCycle(
|
|
2112
|
-
this._cd,
|
|
2113
|
-
this.registry,
|
|
2114
|
-
this._batch_handlers,
|
|
2115
|
-
this._bound_handle,
|
|
2116
|
-
this._bound_handle_batch,
|
|
2117
|
-
lagging,
|
|
2118
|
-
leading,
|
|
2119
|
-
eventLimit,
|
|
2120
|
-
leaseMillis
|
|
2121
|
-
);
|
|
2122
|
-
if (!cycle) {
|
|
2123
|
-
this._needs_drain = false;
|
|
2124
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2125
|
-
}
|
|
2126
|
-
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
2127
|
-
this._drain_lag2lead_ratio = computeLagLeadRatio(
|
|
2128
|
-
handled,
|
|
2129
|
-
lagging,
|
|
2130
|
-
leading
|
|
2131
|
-
);
|
|
2132
|
-
if (acked.length) this.emit("acked", acked);
|
|
2133
|
-
if (blocked.length) this.emit("blocked", blocked);
|
|
2134
|
-
const hasErrors = handled.some(({ error }) => error);
|
|
2135
|
-
if (!acked.length && !blocked.length && !hasErrors)
|
|
2136
|
-
this._needs_drain = false;
|
|
2137
|
-
return { fetched, leased, acked, blocked };
|
|
2138
|
-
} catch (error) {
|
|
2139
|
-
this._logger.error(error);
|
|
2140
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2141
|
-
} finally {
|
|
2142
|
-
this._drain_locked = false;
|
|
2143
|
-
}
|
|
2411
|
+
async drain(options = {}) {
|
|
2412
|
+
return this._drain.drain(options);
|
|
2144
2413
|
}
|
|
2145
2414
|
/**
|
|
2146
2415
|
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
@@ -2187,71 +2456,8 @@ var Act = class {
|
|
|
2187
2456
|
* @see {@link start_correlations} for automatic periodic correlation
|
|
2188
2457
|
* @see {@link stop_correlations} to stop automatic correlation
|
|
2189
2458
|
*/
|
|
2190
|
-
/**
|
|
2191
|
-
* Initialize correlation state on first call.
|
|
2192
|
-
* - Reads max(at) from store as cold-start checkpoint
|
|
2193
|
-
* - Subscribes static resolver targets (idempotent upsert)
|
|
2194
|
-
* - Populates the subscribed statics set
|
|
2195
|
-
* @internal
|
|
2196
|
-
*/
|
|
2197
|
-
async _init_correlation() {
|
|
2198
|
-
if (this._correlation_initialized) return;
|
|
2199
|
-
this._correlation_initialized = true;
|
|
2200
|
-
const { watermark } = await store().subscribe(this._static_targets);
|
|
2201
|
-
this._correlation_checkpoint = watermark;
|
|
2202
|
-
if (this._reactive_events.size > 0) this._needs_drain = true;
|
|
2203
|
-
for (const { stream } of this._static_targets) {
|
|
2204
|
-
this._subscribed_streams.add(stream);
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
2459
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
2208
|
-
|
|
2209
|
-
if (!this._has_dynamic_resolvers)
|
|
2210
|
-
return { subscribed: 0, last_id: this._correlation_checkpoint };
|
|
2211
|
-
const after = Math.max(this._correlation_checkpoint, query.after || -1);
|
|
2212
|
-
const correlated = /* @__PURE__ */ new Map();
|
|
2213
|
-
let last_id = after;
|
|
2214
|
-
await store().query(
|
|
2215
|
-
(event) => {
|
|
2216
|
-
last_id = event.id;
|
|
2217
|
-
const register = this.registry.events[event.name];
|
|
2218
|
-
if (register) {
|
|
2219
|
-
for (const reaction of register.reactions.values()) {
|
|
2220
|
-
if (typeof reaction.resolver !== "function") continue;
|
|
2221
|
-
const resolved = reaction.resolver(event);
|
|
2222
|
-
if (resolved && !this._subscribed_streams.has(resolved.target)) {
|
|
2223
|
-
const entry = correlated.get(resolved.target) || {
|
|
2224
|
-
source: resolved.source,
|
|
2225
|
-
payloads: []
|
|
2226
|
-
};
|
|
2227
|
-
entry.payloads.push({
|
|
2228
|
-
...reaction,
|
|
2229
|
-
source: resolved.source,
|
|
2230
|
-
event
|
|
2231
|
-
});
|
|
2232
|
-
correlated.set(resolved.target, entry);
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
},
|
|
2237
|
-
{ ...query, after }
|
|
2238
|
-
);
|
|
2239
|
-
if (correlated.size) {
|
|
2240
|
-
const streams = [...correlated.entries()].map(([stream, { source }]) => ({
|
|
2241
|
-
stream,
|
|
2242
|
-
source
|
|
2243
|
-
}));
|
|
2244
|
-
const { subscribed } = await this._cd.subscribe(streams);
|
|
2245
|
-
this._correlation_checkpoint = last_id;
|
|
2246
|
-
if (subscribed) {
|
|
2247
|
-
for (const { stream } of streams) {
|
|
2248
|
-
this._subscribed_streams.add(stream);
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
return { subscribed, last_id };
|
|
2252
|
-
}
|
|
2253
|
-
this._correlation_checkpoint = last_id;
|
|
2254
|
-
return { subscribed: 0, last_id };
|
|
2460
|
+
return this._correlate.correlate(query);
|
|
2255
2461
|
}
|
|
2256
2462
|
/**
|
|
2257
2463
|
* Starts automatic periodic correlation worker for discovering new streams.
|
|
@@ -2309,15 +2515,7 @@ var Act = class {
|
|
|
2309
2515
|
* @see {@link stop_correlations} to stop the worker
|
|
2310
2516
|
*/
|
|
2311
2517
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
2312
|
-
|
|
2313
|
-
const limit = query.limit || 100;
|
|
2314
|
-
this._correlation_timer = setInterval(
|
|
2315
|
-
() => this.correlate({ ...query, after: this._correlation_checkpoint, limit }).then((result) => {
|
|
2316
|
-
if (callback && result.subscribed) callback(result.subscribed);
|
|
2317
|
-
}).catch(console.error),
|
|
2318
|
-
frequency
|
|
2319
|
-
);
|
|
2320
|
-
return true;
|
|
2518
|
+
return this._correlate.startPolling(query, frequency, callback);
|
|
2321
2519
|
}
|
|
2322
2520
|
/**
|
|
2323
2521
|
* Stops the automatic correlation worker.
|
|
@@ -2337,10 +2535,7 @@ var Act = class {
|
|
|
2337
2535
|
* @see {@link start_correlations}
|
|
2338
2536
|
*/
|
|
2339
2537
|
stop_correlations() {
|
|
2340
|
-
|
|
2341
|
-
clearInterval(this._correlation_timer);
|
|
2342
|
-
this._correlation_timer = void 0;
|
|
2343
|
-
}
|
|
2538
|
+
this._correlate.stopPolling();
|
|
2344
2539
|
}
|
|
2345
2540
|
/**
|
|
2346
2541
|
* Cancels any pending or active settle cycle.
|
|
@@ -2348,10 +2543,7 @@ var Act = class {
|
|
|
2348
2543
|
* @see {@link settle}
|
|
2349
2544
|
*/
|
|
2350
2545
|
stop_settling() {
|
|
2351
|
-
|
|
2352
|
-
clearTimeout(this._settle_timer);
|
|
2353
|
-
this._settle_timer = void 0;
|
|
2354
|
-
}
|
|
2546
|
+
this._settle.stop();
|
|
2355
2547
|
}
|
|
2356
2548
|
/**
|
|
2357
2549
|
* Reset reaction stream watermarks and request a drain on the next
|
|
@@ -2388,9 +2580,7 @@ var Act = class {
|
|
|
2388
2580
|
*/
|
|
2389
2581
|
async reset(streams) {
|
|
2390
2582
|
const count = await store().reset(streams);
|
|
2391
|
-
if (count > 0 && this._reactive_events.size > 0)
|
|
2392
|
-
this._needs_drain = true;
|
|
2393
|
-
}
|
|
2583
|
+
if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
|
|
2394
2584
|
return count;
|
|
2395
2585
|
}
|
|
2396
2586
|
/**
|
|
@@ -2473,34 +2663,7 @@ var Act = class {
|
|
|
2473
2663
|
* @see {@link correlate} for manual correlation
|
|
2474
2664
|
*/
|
|
2475
2665
|
settle(options = {}) {
|
|
2476
|
-
|
|
2477
|
-
debounceMs = 10,
|
|
2478
|
-
correlate: correlateQuery = { after: -1, limit: 100 },
|
|
2479
|
-
maxPasses = Infinity,
|
|
2480
|
-
...drainOptions
|
|
2481
|
-
} = options;
|
|
2482
|
-
if (this._settle_timer) clearTimeout(this._settle_timer);
|
|
2483
|
-
this._settle_timer = setTimeout(() => {
|
|
2484
|
-
this._settle_timer = void 0;
|
|
2485
|
-
if (this._settling) return;
|
|
2486
|
-
this._settling = true;
|
|
2487
|
-
(async () => {
|
|
2488
|
-
await this._init_correlation();
|
|
2489
|
-
let lastDrain;
|
|
2490
|
-
for (let i = 0; i < maxPasses; i++) {
|
|
2491
|
-
const { subscribed } = await this.correlate({
|
|
2492
|
-
...correlateQuery,
|
|
2493
|
-
after: this._correlation_checkpoint
|
|
2494
|
-
});
|
|
2495
|
-
lastDrain = await this.drain(drainOptions);
|
|
2496
|
-
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
2497
|
-
if (!made_progress) break;
|
|
2498
|
-
}
|
|
2499
|
-
if (lastDrain) this.emit("settled", lastDrain);
|
|
2500
|
-
})().catch((err) => this._logger.error(err)).finally(() => {
|
|
2501
|
-
this._settling = false;
|
|
2502
|
-
});
|
|
2503
|
-
}, debounceMs);
|
|
2666
|
+
this._settle.schedule(options);
|
|
2504
2667
|
}
|
|
2505
2668
|
};
|
|
2506
2669
|
|
|
@@ -2783,6 +2946,7 @@ function action_builder(state2) {
|
|
|
2783
2946
|
ConcurrencyError,
|
|
2784
2947
|
ConsoleLogger,
|
|
2785
2948
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2949
|
+
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
2786
2950
|
Environments,
|
|
2787
2951
|
Errors,
|
|
2788
2952
|
EventMetaSchema,
|