@rotorsoft/act 0.19.1 → 0.21.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/dist/index.js CHANGED
@@ -1,6 +1,42 @@
1
1
  // src/ports.ts
2
2
  import { pino } from "pino";
3
3
 
4
+ // src/adapters/InMemoryCache.ts
5
+ var InMemoryCache = class {
6
+ _entries = /* @__PURE__ */ new Map();
7
+ _maxSize;
8
+ constructor(options) {
9
+ this._maxSize = options?.maxSize ?? 1e3;
10
+ }
11
+ async get(stream) {
12
+ const entry = this._entries.get(stream);
13
+ if (!entry) return void 0;
14
+ this._entries.delete(stream);
15
+ this._entries.set(stream, entry);
16
+ return entry;
17
+ }
18
+ async set(stream, entry) {
19
+ this._entries.delete(stream);
20
+ if (this._entries.size >= this._maxSize) {
21
+ const first = this._entries.keys().next().value;
22
+ this._entries.delete(first);
23
+ }
24
+ this._entries.set(stream, entry);
25
+ }
26
+ async invalidate(stream) {
27
+ this._entries.delete(stream);
28
+ }
29
+ async clear() {
30
+ this._entries.clear();
31
+ }
32
+ async dispose() {
33
+ this._entries.clear();
34
+ }
35
+ get size() {
36
+ return this._entries.size;
37
+ }
38
+ };
39
+
4
40
  // src/types/errors.ts
5
41
  var Errors = {
6
42
  ValidationError: "ERR_VALIDATION",
@@ -355,41 +391,54 @@ var InMemoryStore = class {
355
391
  });
356
392
  }
357
393
  /**
358
- * Polls the store for unblocked streams needing processing, ordered by lease watermark ascending.
359
- * @param lagging - Max number of streams to poll in ascending order.
360
- * @param leading - Max number of streams to poll in descending order.
361
- * @returns The polled streams.
394
+ * Atomically discovers and leases streams for processing.
395
+ * Fuses poll + lease into a single operation.
396
+ * @param lagging - Max streams from lagging frontier.
397
+ * @param leading - Max streams from leading frontier.
398
+ * @param by - Lease holder identifier.
399
+ * @param millis - Lease duration in milliseconds.
400
+ * @returns Granted leases.
362
401
  */
363
- async poll(lagging, leading) {
402
+ async claim(lagging, leading, by, millis) {
364
403
  await sleep();
365
- const a = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => a2.at - b2.at).slice(0, lagging).map(({ stream, source, at }) => ({
366
- stream,
367
- source,
368
- at,
404
+ const available = [...this._streams.values()].filter((s) => s.is_avaliable);
405
+ const lag = available.sort((a, b) => a.at - b.at).slice(0, lagging).map((s) => ({
406
+ stream: s.stream,
407
+ source: s.source,
408
+ at: s.at,
369
409
  lagging: true
370
410
  }));
371
- const b = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => b2.at - a2.at).slice(0, leading).map(({ stream, source, at }) => ({
372
- stream,
373
- source,
374
- at,
411
+ const lead = available.sort((a, b) => b.at - a.at).slice(0, leading).map((s) => ({
412
+ stream: s.stream,
413
+ source: s.source,
414
+ at: s.at,
375
415
  lagging: false
376
416
  }));
377
- return [...a, ...b];
417
+ const seen = /* @__PURE__ */ new Set();
418
+ const combined = [...lag, ...lead].filter((p) => {
419
+ if (seen.has(p.stream)) return false;
420
+ seen.add(p.stream);
421
+ return true;
422
+ });
423
+ return combined.map(
424
+ (p) => this._streams.get(p.stream)?.lease({ ...p, by, retry: 0 }, millis)
425
+ ).filter((l) => !!l);
378
426
  }
379
427
  /**
380
- * Lease streams for processing (e.g., for distributed consumers).
381
- * @param leases - Lease requests for streams, including end-of-lease watermark, lease holder, and source stream.
382
- * @param leaseMilis - Lease duration in milliseconds.
383
- * @returns Granted leases.
428
+ * Registers streams for event processing.
429
+ * @param streams - Streams to register with optional source.
430
+ * @returns Number of newly registered streams.
384
431
  */
385
- async lease(leases, millis) {
432
+ async subscribe(streams) {
386
433
  await sleep();
387
- return leases.map((l) => {
388
- if (!this._streams.has(l.stream)) {
389
- this._streams.set(l.stream, new InMemoryStream(l.stream, l.source));
434
+ let count = 0;
435
+ for (const { stream, source } of streams) {
436
+ if (!this._streams.has(stream)) {
437
+ this._streams.set(stream, new InMemoryStream(stream, source));
438
+ count++;
390
439
  }
391
- return this._streams.get(l.stream)?.lease(l, millis);
392
- }).filter((l) => !!l);
440
+ }
441
+ return count;
393
442
  }
394
443
  /**
395
444
  * Acknowledge completion of processing for leased streams.
@@ -455,6 +504,9 @@ var SNAP_EVENT = "__snapshot__";
455
504
  var store = port(function store2(adapter) {
456
505
  return adapter || new InMemoryStore();
457
506
  });
507
+ var cache = port(function cache2(adapter) {
508
+ return adapter || new InMemoryCache();
509
+ });
458
510
  function build_tracer(logLevel2) {
459
511
  if (logLevel2 === "trace") {
460
512
  return {
@@ -470,8 +522,8 @@ function build_tracer(logLevel2) {
470
522
  );
471
523
  logger.trace(data, "\u26A1\uFE0F fetch");
472
524
  },
473
- correlated: (leases) => {
474
- const data = leases.map(({ stream }) => stream).join(" ");
525
+ correlated: (streams) => {
526
+ const data = streams.map(({ stream }) => stream).join(" ");
475
527
  logger.trace(`\u26A1\uFE0F correlate ${data}`);
476
528
  },
477
529
  leased: (leases) => {
@@ -556,11 +608,12 @@ async function snap(snapshot) {
556
608
  }
557
609
  }
558
610
  async function load(me, stream, callback) {
559
- let state2 = me.init ? me.init() : {};
560
- let patches = 0;
561
- let snaps = 0;
611
+ const cached = await cache().get(stream);
612
+ let state2 = cached?.state ?? (me.init ? me.init() : {});
613
+ let patches = cached?.patches ?? 0;
614
+ let snaps = cached?.snaps ?? 0;
562
615
  let event;
563
- await store().query(
616
+ const count = await store().query(
564
617
  (e) => {
565
618
  event = e;
566
619
  if (e.name === SNAP_EVENT) {
@@ -573,9 +626,12 @@ async function load(me, stream, callback) {
573
626
  }
574
627
  callback && callback({ event, state: state2, patches, snaps });
575
628
  },
576
- { stream, with_snaps: true }
629
+ { stream, with_snaps: !cached, after: cached?.event_id }
630
+ );
631
+ logger.trace(
632
+ state2,
633
+ `\u{1F7E2} load ${stream}${cached && count === 0 ? " (cached)" : ""}`
577
634
  );
578
- logger.trace(state2, `\u{1F7E2} load ${stream}`);
579
635
  return { event, state: state2, patches, snaps };
580
636
  }
581
637
  async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
@@ -631,13 +687,21 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
631
687
  emitted.map((e) => e.data),
632
688
  `\u{1F534} commit ${stream}.${emitted.map((e) => e.name).join(", ")}`
633
689
  );
634
- const committed = await store().commit(
635
- stream,
636
- emitted,
637
- meta,
638
- // TODO: review reactions not enforcing expected version
639
- reactingTo ? void 0 : expected
640
- );
690
+ let committed;
691
+ try {
692
+ committed = await store().commit(
693
+ stream,
694
+ emitted,
695
+ meta,
696
+ // TODO: review reactions not enforcing expected version
697
+ reactingTo ? void 0 : expected
698
+ );
699
+ } catch (error) {
700
+ if (error.name === "ERR_CONCURRENCY") {
701
+ void cache().invalidate(stream);
702
+ }
703
+ throw error;
704
+ }
641
705
  let { state: state2, patches } = snapshot;
642
706
  const snapshots = committed.map((event) => {
643
707
  const p = me.patch[event.name](event, state2);
@@ -646,7 +710,15 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
646
710
  return { event, state: state2, patches, snaps: snapshot.snaps, patch: p };
647
711
  });
648
712
  const last = snapshots.at(-1);
649
- me.snap && me.snap(last) && void snap(last);
713
+ const snapped = me.snap && me.snap(last);
714
+ void cache().set(stream, {
715
+ state: last.state,
716
+ version: last.event.version,
717
+ event_id: last.event.id,
718
+ patches: snapped ? 0 : last.patches,
719
+ snaps: snapped ? last.snaps + 1 : last.snaps
720
+ });
721
+ if (snapped) void snap(last);
650
722
  return snapshots;
651
723
  }
652
724
 
@@ -970,9 +1042,16 @@ var Act = class {
970
1042
  this._drain_locked = true;
971
1043
  const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
972
1044
  const leading = streamLimit - lagging;
973
- const polled = await store().poll(lagging, leading);
1045
+ const leased = await store().claim(
1046
+ lagging,
1047
+ leading,
1048
+ randomUUID2(),
1049
+ leaseMillis
1050
+ );
1051
+ if (!leased.length)
1052
+ return { fetched: [], leased: [], acked: [], blocked: [] };
974
1053
  const fetched = await Promise.all(
975
- polled.map(async ({ stream, source, at, lagging: lagging2 }) => {
1054
+ leased.map(async ({ stream, source, at, lagging: lagging2 }) => {
976
1055
  const events = await this.query_array({
977
1056
  stream: source,
978
1057
  after: at,
@@ -981,71 +1060,60 @@ var Act = class {
981
1060
  return { stream, source, at, lagging: lagging2, events };
982
1061
  })
983
1062
  );
984
- if (fetched.length) {
985
- tracer.fetched(fetched);
986
- const leases = /* @__PURE__ */ new Map();
987
- const fetch_window_at = fetched.reduce(
988
- (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
989
- 0
990
- );
991
- fetched.forEach(({ stream, lagging: lagging2, events }) => {
992
- const payloads = events.flatMap((event) => {
993
- const register = this.registry.events[event.name];
994
- if (!register) return [];
995
- return [...register.reactions.values()].filter((reaction) => {
996
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
997
- return resolved && resolved.target === stream;
998
- }).map((reaction) => ({ ...reaction, event }));
999
- });
1000
- leases.set(stream, {
1001
- lease: {
1002
- stream,
1003
- by: randomUUID2(),
1004
- at: events.at(-1)?.id || fetch_window_at,
1005
- // ff when no matching events
1006
- retry: 0,
1007
- lagging: lagging2
1008
- },
1009
- payloads
1010
- });
1063
+ tracer.fetched(fetched);
1064
+ const payloadsMap = /* @__PURE__ */ new Map();
1065
+ const fetch_window_at = fetched.reduce(
1066
+ (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
1067
+ 0
1068
+ );
1069
+ fetched.forEach(({ stream, events }) => {
1070
+ const payloads = events.flatMap((event) => {
1071
+ const register = this.registry.events[event.name];
1072
+ if (!register) return [];
1073
+ return [...register.reactions.values()].filter((reaction) => {
1074
+ const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1075
+ return resolved && resolved.target === stream;
1076
+ }).map((reaction) => ({ ...reaction, event }));
1011
1077
  });
1012
- const leased = await store().lease(
1013
- [...leases.values()].map(({ lease }) => lease),
1014
- leaseMillis
1015
- );
1016
- tracer.leased(leased);
1017
- const handled = await Promise.all(
1018
- leased.map(
1019
- (lease) => this.handle(lease, leases.get(lease.stream).payloads)
1020
- )
1021
- );
1022
- const [lagging_handled, leading_handled] = handled.reduce(
1023
- ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
1024
- lagging_handled2 + (lease.lagging ? handled2 : 0),
1025
- leading_handled2 + (lease.lagging ? 0 : handled2)
1026
- ],
1027
- [0, 0]
1028
- );
1029
- const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
1030
- const leading_avg = leading > 0 ? leading_handled / leading : 0;
1031
- const total = lagging_avg + leading_avg;
1032
- this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
1033
- const acked = await store().ack(
1034
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
1035
- );
1036
- if (acked.length) {
1037
- tracer.acked(acked);
1038
- this.emit("acked", acked);
1039
- }
1040
- const blocked = await store().block(
1041
- handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
1042
- );
1043
- if (blocked.length) {
1044
- tracer.blocked(blocked);
1045
- this.emit("blocked", blocked);
1046
- }
1047
- return { fetched, leased, acked, blocked };
1078
+ payloadsMap.set(stream, payloads);
1079
+ });
1080
+ tracer.leased(leased);
1081
+ const handled = await Promise.all(
1082
+ leased.map((lease) => {
1083
+ const streamFetch = fetched.find((f) => f.stream === lease.stream);
1084
+ const at = streamFetch?.events.at(-1)?.id || fetch_window_at;
1085
+ return this.handle(
1086
+ { ...lease, at },
1087
+ payloadsMap.get(lease.stream) || []
1088
+ );
1089
+ })
1090
+ );
1091
+ const [lagging_handled, leading_handled] = handled.reduce(
1092
+ ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
1093
+ lagging_handled2 + (lease.lagging ? handled2 : 0),
1094
+ leading_handled2 + (lease.lagging ? 0 : handled2)
1095
+ ],
1096
+ [0, 0]
1097
+ );
1098
+ const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
1099
+ const leading_avg = leading > 0 ? leading_handled / leading : 0;
1100
+ const total = lagging_avg + leading_avg;
1101
+ this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
1102
+ const acked = await store().ack(
1103
+ handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
1104
+ );
1105
+ if (acked.length) {
1106
+ tracer.acked(acked);
1107
+ this.emit("acked", acked);
1108
+ }
1109
+ const blocked = await store().block(
1110
+ handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
1111
+ );
1112
+ if (blocked.length) {
1113
+ tracer.blocked(blocked);
1114
+ this.emit("blocked", blocked);
1048
1115
  }
1116
+ return { fetched, leased, acked, blocked };
1049
1117
  } catch (error) {
1050
1118
  logger.error(error);
1051
1119
  } finally {
@@ -1108,26 +1176,31 @@ var Act = class {
1108
1176
  if (register) {
1109
1177
  for (const reaction of register.reactions.values()) {
1110
1178
  const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1111
- resolved && (correlated.get(resolved.target) || correlated.set(resolved.target, []).get(resolved.target)).push({ ...reaction, source: resolved.source, event });
1179
+ if (resolved) {
1180
+ const entry = correlated.get(resolved.target) || {
1181
+ source: resolved.source,
1182
+ payloads: []
1183
+ };
1184
+ entry.payloads.push({
1185
+ ...reaction,
1186
+ source: resolved.source,
1187
+ event
1188
+ });
1189
+ correlated.set(resolved.target, entry);
1190
+ }
1112
1191
  }
1113
1192
  }
1114
1193
  }, query);
1115
1194
  if (correlated.size) {
1116
- const leases = [...correlated.entries()].map(([stream, payloads]) => ({
1195
+ const streams = [...correlated.entries()].map(([stream, { source }]) => ({
1117
1196
  stream,
1118
- // TODO: by convention, the first defined source wins (this can be tricky)
1119
- source: payloads.find((p) => p.source)?.source || void 0,
1120
- by: randomUUID2(),
1121
- at: 0,
1122
- retry: 0,
1123
- lagging: true,
1124
- payloads
1197
+ source
1125
1198
  }));
1126
- const leased = await store().lease(leases, 0);
1127
- leased.length && tracer.correlated(leased);
1128
- return { leased, last_id };
1199
+ const subscribed = await store().subscribe(streams);
1200
+ subscribed && tracer.correlated(streams);
1201
+ return { subscribed, last_id };
1129
1202
  }
1130
- return { leased: [], last_id };
1203
+ return { subscribed: 0, last_id };
1131
1204
  }
1132
1205
  /**
1133
1206
  * Starts automatic periodic correlation worker for discovering new streams.
@@ -1191,7 +1264,7 @@ var Act = class {
1191
1264
  this._correlation_timer = setInterval(
1192
1265
  () => this.correlate({ ...query, after, limit }).then((result) => {
1193
1266
  after = result.last_id;
1194
- if (callback && result.leased.length) callback(result.leased);
1267
+ if (callback && result.subscribed) callback(result.subscribed);
1195
1268
  }).catch(console.error),
1196
1269
  frequency
1197
1270
  );
@@ -1275,8 +1348,8 @@ var Act = class {
1275
1348
  (async () => {
1276
1349
  let lastDrain;
1277
1350
  for (let i = 0; i < maxPasses; i++) {
1278
- const { leased } = await this.correlate(correlateQuery);
1279
- if (leased.length === 0 && i > 0) break;
1351
+ const { subscribed } = await this.correlate(correlateQuery);
1352
+ if (subscribed === 0 && i > 0) break;
1280
1353
  lastDrain = await this.drain(drainOptions);
1281
1354
  if (!lastDrain.acked.length && !lastDrain.blocked.length) break;
1282
1355
  }
@@ -1705,6 +1778,8 @@ export {
1705
1778
  Errors,
1706
1779
  EventMetaSchema,
1707
1780
  ExitCodes,
1781
+ InMemoryCache,
1782
+ InMemoryStore,
1708
1783
  InvariantError,
1709
1784
  LogLevels,
1710
1785
  PackageSchema,
@@ -1715,6 +1790,7 @@ export {
1715
1790
  ZodEmpty,
1716
1791
  act,
1717
1792
  build_tracer,
1793
+ cache,
1718
1794
  config,
1719
1795
  dispose,
1720
1796
  disposeAndExit,