@rotorsoft/act 0.20.0 → 0.22.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 +16 -57
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +16 -2
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/InMemoryStore.d.ts +18 -15
- package/dist/@types/adapters/InMemoryStore.d.ts.map +1 -1
- package/dist/@types/ports.d.ts +4 -1
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +53 -47
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +0 -13
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/index.cjs +193 -125
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +193 -125
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -460,41 +460,58 @@ var InMemoryStore = class {
|
|
|
460
460
|
});
|
|
461
461
|
}
|
|
462
462
|
/**
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* @param
|
|
466
|
-
* @
|
|
463
|
+
* Atomically discovers and leases streams for processing.
|
|
464
|
+
* Fuses poll + lease into a single operation.
|
|
465
|
+
* @param lagging - Max streams from lagging frontier.
|
|
466
|
+
* @param leading - Max streams from leading frontier.
|
|
467
|
+
* @param by - Lease holder identifier.
|
|
468
|
+
* @param millis - Lease duration in milliseconds.
|
|
469
|
+
* @returns Granted leases.
|
|
467
470
|
*/
|
|
468
|
-
async
|
|
471
|
+
async claim(lagging, leading, by, millis) {
|
|
469
472
|
await sleep();
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
473
|
+
const available = [...this._streams.values()].filter((s) => s.is_avaliable);
|
|
474
|
+
const lag = available.sort((a, b) => a.at - b.at).slice(0, lagging).map((s) => ({
|
|
475
|
+
stream: s.stream,
|
|
476
|
+
source: s.source,
|
|
477
|
+
at: s.at,
|
|
474
478
|
lagging: true
|
|
475
479
|
}));
|
|
476
|
-
const
|
|
477
|
-
stream,
|
|
478
|
-
source,
|
|
479
|
-
at,
|
|
480
|
+
const lead = available.sort((a, b) => b.at - a.at).slice(0, leading).map((s) => ({
|
|
481
|
+
stream: s.stream,
|
|
482
|
+
source: s.source,
|
|
483
|
+
at: s.at,
|
|
480
484
|
lagging: false
|
|
481
485
|
}));
|
|
482
|
-
|
|
486
|
+
const seen = /* @__PURE__ */ new Set();
|
|
487
|
+
const combined = [...lag, ...lead].filter((p) => {
|
|
488
|
+
if (seen.has(p.stream)) return false;
|
|
489
|
+
seen.add(p.stream);
|
|
490
|
+
return true;
|
|
491
|
+
});
|
|
492
|
+
return combined.map(
|
|
493
|
+
(p) => this._streams.get(p.stream)?.lease({ ...p, by, retry: 0 }, millis)
|
|
494
|
+
).filter((l) => !!l);
|
|
483
495
|
}
|
|
484
496
|
/**
|
|
485
|
-
*
|
|
486
|
-
* @param
|
|
487
|
-
* @
|
|
488
|
-
* @returns Granted leases.
|
|
497
|
+
* Registers streams for event processing.
|
|
498
|
+
* @param streams - Streams to register with optional source.
|
|
499
|
+
* @returns subscribed count and current max watermark.
|
|
489
500
|
*/
|
|
490
|
-
async
|
|
501
|
+
async subscribe(streams) {
|
|
491
502
|
await sleep();
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
503
|
+
let subscribed = 0;
|
|
504
|
+
for (const { stream, source } of streams) {
|
|
505
|
+
if (!this._streams.has(stream)) {
|
|
506
|
+
this._streams.set(stream, new InMemoryStream(stream, source));
|
|
507
|
+
subscribed++;
|
|
495
508
|
}
|
|
496
|
-
|
|
497
|
-
|
|
509
|
+
}
|
|
510
|
+
let watermark = -1;
|
|
511
|
+
for (const s of this._streams.values()) {
|
|
512
|
+
if (s.at > watermark) watermark = s.at;
|
|
513
|
+
}
|
|
514
|
+
return { subscribed, watermark };
|
|
498
515
|
}
|
|
499
516
|
/**
|
|
500
517
|
* Acknowledge completion of processing for leased streams.
|
|
@@ -578,8 +595,8 @@ function build_tracer(logLevel2) {
|
|
|
578
595
|
);
|
|
579
596
|
logger.trace(data, "\u26A1\uFE0F fetch");
|
|
580
597
|
},
|
|
581
|
-
correlated: (
|
|
582
|
-
const data =
|
|
598
|
+
correlated: (streams) => {
|
|
599
|
+
const data = streams.map(({ stream }) => stream).join(" ");
|
|
583
600
|
logger.trace(`\u26A1\uFE0F correlate ${data}`);
|
|
584
601
|
},
|
|
585
602
|
leased: (leases) => {
|
|
@@ -781,15 +798,21 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
781
798
|
// src/act.ts
|
|
782
799
|
var tracer = build_tracer(config().logLevel);
|
|
783
800
|
var Act = class {
|
|
784
|
-
/**
|
|
785
|
-
* Create a new Act orchestrator.
|
|
786
|
-
*
|
|
787
|
-
* @param registry The registry of state, event, and action schemas
|
|
788
|
-
* @param states Map of state names to their (potentially merged) state definitions
|
|
789
|
-
*/
|
|
790
801
|
constructor(registry, _states = /* @__PURE__ */ new Map()) {
|
|
791
802
|
this.registry = registry;
|
|
792
803
|
this._states = _states;
|
|
804
|
+
const statics = [];
|
|
805
|
+
for (const register of Object.values(this.registry.events)) {
|
|
806
|
+
for (const reaction of register.reactions.values()) {
|
|
807
|
+
if (typeof reaction.resolver === "function") {
|
|
808
|
+
this._has_dynamic_resolvers = true;
|
|
809
|
+
} else if (reaction.resolver) {
|
|
810
|
+
const r = reaction.resolver;
|
|
811
|
+
statics.push({ stream: r.target, source: r.source });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
this._static_targets = statics;
|
|
793
816
|
dispose(() => {
|
|
794
817
|
this._emitter.removeAllListeners();
|
|
795
818
|
this.stop_correlations();
|
|
@@ -803,6 +826,10 @@ var Act = class {
|
|
|
803
826
|
_correlation_timer = void 0;
|
|
804
827
|
_settle_timer = void 0;
|
|
805
828
|
_settling = false;
|
|
829
|
+
_correlation_checkpoint = -1;
|
|
830
|
+
_subscribed_statics = /* @__PURE__ */ new Set();
|
|
831
|
+
_has_dynamic_resolvers = false;
|
|
832
|
+
_correlation_initialized = false;
|
|
806
833
|
emit(event, args) {
|
|
807
834
|
return this._emitter.emit(event, args);
|
|
808
835
|
}
|
|
@@ -814,6 +841,14 @@ var Act = class {
|
|
|
814
841
|
this._emitter.off(event, listener);
|
|
815
842
|
return this;
|
|
816
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Create a new Act orchestrator.
|
|
846
|
+
*
|
|
847
|
+
* @param registry The registry of state, event, and action schemas
|
|
848
|
+
* @param states Map of state names to their (potentially merged) state definitions
|
|
849
|
+
*/
|
|
850
|
+
/** Static resolver targets collected at build time */
|
|
851
|
+
_static_targets;
|
|
817
852
|
/**
|
|
818
853
|
* Executes an action on a state instance, committing resulting events.
|
|
819
854
|
*
|
|
@@ -1098,9 +1133,16 @@ var Act = class {
|
|
|
1098
1133
|
this._drain_locked = true;
|
|
1099
1134
|
const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
|
|
1100
1135
|
const leading = streamLimit - lagging;
|
|
1101
|
-
const
|
|
1136
|
+
const leased = await store().claim(
|
|
1137
|
+
lagging,
|
|
1138
|
+
leading,
|
|
1139
|
+
(0, import_crypto2.randomUUID)(),
|
|
1140
|
+
leaseMillis
|
|
1141
|
+
);
|
|
1142
|
+
if (!leased.length)
|
|
1143
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1102
1144
|
const fetched = await Promise.all(
|
|
1103
|
-
|
|
1145
|
+
leased.map(async ({ stream, source, at, lagging: lagging2 }) => {
|
|
1104
1146
|
const events = await this.query_array({
|
|
1105
1147
|
stream: source,
|
|
1106
1148
|
after: at,
|
|
@@ -1109,71 +1151,60 @@ var Act = class {
|
|
|
1109
1151
|
return { stream, source, at, lagging: lagging2, events };
|
|
1110
1152
|
})
|
|
1111
1153
|
);
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}).map((reaction) => ({ ...reaction, event }));
|
|
1127
|
-
});
|
|
1128
|
-
leases.set(stream, {
|
|
1129
|
-
lease: {
|
|
1130
|
-
stream,
|
|
1131
|
-
by: (0, import_crypto2.randomUUID)(),
|
|
1132
|
-
at: events.at(-1)?.id || fetch_window_at,
|
|
1133
|
-
// ff when no matching events
|
|
1134
|
-
retry: 0,
|
|
1135
|
-
lagging: lagging2
|
|
1136
|
-
},
|
|
1137
|
-
payloads
|
|
1138
|
-
});
|
|
1154
|
+
tracer.fetched(fetched);
|
|
1155
|
+
const payloadsMap = /* @__PURE__ */ new Map();
|
|
1156
|
+
const fetch_window_at = fetched.reduce(
|
|
1157
|
+
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
1158
|
+
0
|
|
1159
|
+
);
|
|
1160
|
+
fetched.forEach(({ stream, events }) => {
|
|
1161
|
+
const payloads = events.flatMap((event) => {
|
|
1162
|
+
const register = this.registry.events[event.name];
|
|
1163
|
+
if (!register) return [];
|
|
1164
|
+
return [...register.reactions.values()].filter((reaction) => {
|
|
1165
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1166
|
+
return resolved && resolved.target === stream;
|
|
1167
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
1139
1168
|
});
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
);
|
|
1171
|
-
if (blocked.length) {
|
|
1172
|
-
tracer.blocked(blocked);
|
|
1173
|
-
this.emit("blocked", blocked);
|
|
1174
|
-
}
|
|
1175
|
-
return { fetched, leased, acked, blocked };
|
|
1169
|
+
payloadsMap.set(stream, payloads);
|
|
1170
|
+
});
|
|
1171
|
+
tracer.leased(leased);
|
|
1172
|
+
const handled = await Promise.all(
|
|
1173
|
+
leased.map((lease) => {
|
|
1174
|
+
const streamFetch = fetched.find((f) => f.stream === lease.stream);
|
|
1175
|
+
const at = streamFetch?.events.at(-1)?.id || fetch_window_at;
|
|
1176
|
+
return this.handle(
|
|
1177
|
+
{ ...lease, at },
|
|
1178
|
+
payloadsMap.get(lease.stream) || []
|
|
1179
|
+
);
|
|
1180
|
+
})
|
|
1181
|
+
);
|
|
1182
|
+
const [lagging_handled, leading_handled] = handled.reduce(
|
|
1183
|
+
([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
|
|
1184
|
+
lagging_handled2 + (lease.lagging ? handled2 : 0),
|
|
1185
|
+
leading_handled2 + (lease.lagging ? 0 : handled2)
|
|
1186
|
+
],
|
|
1187
|
+
[0, 0]
|
|
1188
|
+
);
|
|
1189
|
+
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1190
|
+
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1191
|
+
const total = lagging_avg + leading_avg;
|
|
1192
|
+
this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
|
|
1193
|
+
const acked = await store().ack(
|
|
1194
|
+
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1195
|
+
);
|
|
1196
|
+
if (acked.length) {
|
|
1197
|
+
tracer.acked(acked);
|
|
1198
|
+
this.emit("acked", acked);
|
|
1176
1199
|
}
|
|
1200
|
+
const blocked = await store().block(
|
|
1201
|
+
handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
|
|
1202
|
+
);
|
|
1203
|
+
if (blocked.length) {
|
|
1204
|
+
tracer.blocked(blocked);
|
|
1205
|
+
this.emit("blocked", blocked);
|
|
1206
|
+
}
|
|
1207
|
+
return { fetched, leased, acked, blocked };
|
|
1177
1208
|
} catch (error) {
|
|
1178
1209
|
logger.error(error);
|
|
1179
1210
|
} finally {
|
|
@@ -1227,35 +1258,70 @@ var Act = class {
|
|
|
1227
1258
|
* @see {@link start_correlations} for automatic periodic correlation
|
|
1228
1259
|
* @see {@link stop_correlations} to stop automatic correlation
|
|
1229
1260
|
*/
|
|
1261
|
+
/**
|
|
1262
|
+
* Initialize correlation state on first call.
|
|
1263
|
+
* - Reads max(at) from store as cold-start checkpoint
|
|
1264
|
+
* - Subscribes static resolver targets (idempotent upsert)
|
|
1265
|
+
* - Populates the subscribed statics set
|
|
1266
|
+
* @internal
|
|
1267
|
+
*/
|
|
1268
|
+
async _init_correlation() {
|
|
1269
|
+
if (this._correlation_initialized) return;
|
|
1270
|
+
this._correlation_initialized = true;
|
|
1271
|
+
const { watermark } = await store().subscribe(this._static_targets);
|
|
1272
|
+
this._correlation_checkpoint = watermark;
|
|
1273
|
+
for (const { stream } of this._static_targets) {
|
|
1274
|
+
this._subscribed_statics.add(stream);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1230
1277
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
1278
|
+
await this._init_correlation();
|
|
1279
|
+
if (!this._has_dynamic_resolvers)
|
|
1280
|
+
return { subscribed: 0, last_id: this._correlation_checkpoint };
|
|
1281
|
+
const after = Math.max(this._correlation_checkpoint, query.after || -1);
|
|
1231
1282
|
const correlated = /* @__PURE__ */ new Map();
|
|
1232
|
-
let last_id =
|
|
1233
|
-
await store().query(
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1283
|
+
let last_id = after;
|
|
1284
|
+
await store().query(
|
|
1285
|
+
(event) => {
|
|
1286
|
+
last_id = event.id;
|
|
1287
|
+
const register = this.registry.events[event.name];
|
|
1288
|
+
if (register) {
|
|
1289
|
+
for (const reaction of register.reactions.values()) {
|
|
1290
|
+
if (typeof reaction.resolver !== "function") continue;
|
|
1291
|
+
const resolved = reaction.resolver(event);
|
|
1292
|
+
if (resolved && !this._subscribed_statics.has(resolved.target)) {
|
|
1293
|
+
const entry = correlated.get(resolved.target) || {
|
|
1294
|
+
source: resolved.source,
|
|
1295
|
+
payloads: []
|
|
1296
|
+
};
|
|
1297
|
+
entry.payloads.push({
|
|
1298
|
+
...reaction,
|
|
1299
|
+
source: resolved.source,
|
|
1300
|
+
event
|
|
1301
|
+
});
|
|
1302
|
+
correlated.set(resolved.target, entry);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1240
1305
|
}
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1306
|
+
},
|
|
1307
|
+
{ ...query, after }
|
|
1308
|
+
);
|
|
1309
|
+
this._correlation_checkpoint = last_id;
|
|
1243
1310
|
if (correlated.size) {
|
|
1244
|
-
const
|
|
1311
|
+
const streams = [...correlated.entries()].map(([stream, { source }]) => ({
|
|
1245
1312
|
stream,
|
|
1246
|
-
|
|
1247
|
-
source: payloads.find((p) => p.source)?.source || void 0,
|
|
1248
|
-
by: (0, import_crypto2.randomUUID)(),
|
|
1249
|
-
at: 0,
|
|
1250
|
-
retry: 0,
|
|
1251
|
-
lagging: true,
|
|
1252
|
-
payloads
|
|
1313
|
+
source
|
|
1253
1314
|
}));
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1315
|
+
const { subscribed } = await store().subscribe(streams);
|
|
1316
|
+
if (subscribed) {
|
|
1317
|
+
tracer.correlated(streams);
|
|
1318
|
+
for (const { stream } of streams) {
|
|
1319
|
+
this._subscribed_statics.add(stream);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return { subscribed, last_id };
|
|
1257
1323
|
}
|
|
1258
|
-
return {
|
|
1324
|
+
return { subscribed: 0, last_id };
|
|
1259
1325
|
}
|
|
1260
1326
|
/**
|
|
1261
1327
|
* Starts automatic periodic correlation worker for discovering new streams.
|
|
@@ -1315,11 +1381,9 @@ var Act = class {
|
|
|
1315
1381
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
1316
1382
|
if (this._correlation_timer) return false;
|
|
1317
1383
|
const limit = query.limit || 100;
|
|
1318
|
-
let after = query.after || -1;
|
|
1319
1384
|
this._correlation_timer = setInterval(
|
|
1320
|
-
() => this.correlate({ ...query, after, limit }).then((result) => {
|
|
1321
|
-
|
|
1322
|
-
if (callback && result.leased.length) callback(result.leased);
|
|
1385
|
+
() => this.correlate({ ...query, after: this._correlation_checkpoint, limit }).then((result) => {
|
|
1386
|
+
if (callback && result.subscribed) callback(result.subscribed);
|
|
1323
1387
|
}).catch(console.error),
|
|
1324
1388
|
frequency
|
|
1325
1389
|
);
|
|
@@ -1401,10 +1465,14 @@ var Act = class {
|
|
|
1401
1465
|
if (this._settling) return;
|
|
1402
1466
|
this._settling = true;
|
|
1403
1467
|
(async () => {
|
|
1468
|
+
await this._init_correlation();
|
|
1404
1469
|
let lastDrain;
|
|
1405
1470
|
for (let i = 0; i < maxPasses; i++) {
|
|
1406
|
-
const {
|
|
1407
|
-
|
|
1471
|
+
const { subscribed } = await this.correlate({
|
|
1472
|
+
...correlateQuery,
|
|
1473
|
+
after: this._correlation_checkpoint
|
|
1474
|
+
});
|
|
1475
|
+
if (subscribed === 0 && i > 0) break;
|
|
1408
1476
|
lastDrain = await this.drain(drainOptions);
|
|
1409
1477
|
if (!lastDrain.acked.length && !lastDrain.blocked.length) break;
|
|
1410
1478
|
}
|