@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.cjs CHANGED
@@ -39,6 +39,8 @@ __export(index_exports, {
39
39
  Errors: () => Errors,
40
40
  EventMetaSchema: () => EventMetaSchema,
41
41
  ExitCodes: () => ExitCodes,
42
+ InMemoryCache: () => InMemoryCache,
43
+ InMemoryStore: () => InMemoryStore,
42
44
  InvariantError: () => InvariantError,
43
45
  LogLevels: () => LogLevels,
44
46
  PackageSchema: () => PackageSchema,
@@ -49,6 +51,7 @@ __export(index_exports, {
49
51
  ZodEmpty: () => ZodEmpty,
50
52
  act: () => act,
51
53
  build_tracer: () => build_tracer,
54
+ cache: () => cache,
52
55
  config: () => config,
53
56
  dispose: () => dispose,
54
57
  disposeAndExit: () => disposeAndExit,
@@ -67,6 +70,42 @@ module.exports = __toCommonJS(index_exports);
67
70
  // src/ports.ts
68
71
  var import_pino = require("pino");
69
72
 
73
+ // src/adapters/InMemoryCache.ts
74
+ var InMemoryCache = class {
75
+ _entries = /* @__PURE__ */ new Map();
76
+ _maxSize;
77
+ constructor(options) {
78
+ this._maxSize = options?.maxSize ?? 1e3;
79
+ }
80
+ async get(stream) {
81
+ const entry = this._entries.get(stream);
82
+ if (!entry) return void 0;
83
+ this._entries.delete(stream);
84
+ this._entries.set(stream, entry);
85
+ return entry;
86
+ }
87
+ async set(stream, entry) {
88
+ this._entries.delete(stream);
89
+ if (this._entries.size >= this._maxSize) {
90
+ const first = this._entries.keys().next().value;
91
+ this._entries.delete(first);
92
+ }
93
+ this._entries.set(stream, entry);
94
+ }
95
+ async invalidate(stream) {
96
+ this._entries.delete(stream);
97
+ }
98
+ async clear() {
99
+ this._entries.clear();
100
+ }
101
+ async dispose() {
102
+ this._entries.clear();
103
+ }
104
+ get size() {
105
+ return this._entries.size;
106
+ }
107
+ };
108
+
70
109
  // src/types/errors.ts
71
110
  var Errors = {
72
111
  ValidationError: "ERR_VALIDATION",
@@ -421,41 +460,54 @@ var InMemoryStore = class {
421
460
  });
422
461
  }
423
462
  /**
424
- * Polls the store for unblocked streams needing processing, ordered by lease watermark ascending.
425
- * @param lagging - Max number of streams to poll in ascending order.
426
- * @param leading - Max number of streams to poll in descending order.
427
- * @returns The polled streams.
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.
428
470
  */
429
- async poll(lagging, leading) {
471
+ async claim(lagging, leading, by, millis) {
430
472
  await sleep();
431
- const a = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => a2.at - b2.at).slice(0, lagging).map(({ stream, source, at }) => ({
432
- stream,
433
- source,
434
- at,
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,
435
478
  lagging: true
436
479
  }));
437
- const b = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => b2.at - a2.at).slice(0, leading).map(({ stream, source, at }) => ({
438
- stream,
439
- source,
440
- 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,
441
484
  lagging: false
442
485
  }));
443
- return [...a, ...b];
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);
444
495
  }
445
496
  /**
446
- * Lease streams for processing (e.g., for distributed consumers).
447
- * @param leases - Lease requests for streams, including end-of-lease watermark, lease holder, and source stream.
448
- * @param leaseMilis - Lease duration in milliseconds.
449
- * @returns Granted leases.
497
+ * Registers streams for event processing.
498
+ * @param streams - Streams to register with optional source.
499
+ * @returns Number of newly registered streams.
450
500
  */
451
- async lease(leases, millis) {
501
+ async subscribe(streams) {
452
502
  await sleep();
453
- return leases.map((l) => {
454
- if (!this._streams.has(l.stream)) {
455
- this._streams.set(l.stream, new InMemoryStream(l.stream, l.source));
503
+ let count = 0;
504
+ for (const { stream, source } of streams) {
505
+ if (!this._streams.has(stream)) {
506
+ this._streams.set(stream, new InMemoryStream(stream, source));
507
+ count++;
456
508
  }
457
- return this._streams.get(l.stream)?.lease(l, millis);
458
- }).filter((l) => !!l);
509
+ }
510
+ return count;
459
511
  }
460
512
  /**
461
513
  * Acknowledge completion of processing for leased streams.
@@ -521,6 +573,9 @@ var SNAP_EVENT = "__snapshot__";
521
573
  var store = port(function store2(adapter) {
522
574
  return adapter || new InMemoryStore();
523
575
  });
576
+ var cache = port(function cache2(adapter) {
577
+ return adapter || new InMemoryCache();
578
+ });
524
579
  function build_tracer(logLevel2) {
525
580
  if (logLevel2 === "trace") {
526
581
  return {
@@ -536,8 +591,8 @@ function build_tracer(logLevel2) {
536
591
  );
537
592
  logger.trace(data, "\u26A1\uFE0F fetch");
538
593
  },
539
- correlated: (leases) => {
540
- const data = leases.map(({ stream }) => stream).join(" ");
594
+ correlated: (streams) => {
595
+ const data = streams.map(({ stream }) => stream).join(" ");
541
596
  logger.trace(`\u26A1\uFE0F correlate ${data}`);
542
597
  },
543
598
  leased: (leases) => {
@@ -622,11 +677,12 @@ async function snap(snapshot) {
622
677
  }
623
678
  }
624
679
  async function load(me, stream, callback) {
625
- let state2 = me.init ? me.init() : {};
626
- let patches = 0;
627
- let snaps = 0;
680
+ const cached = await cache().get(stream);
681
+ let state2 = cached?.state ?? (me.init ? me.init() : {});
682
+ let patches = cached?.patches ?? 0;
683
+ let snaps = cached?.snaps ?? 0;
628
684
  let event;
629
- await store().query(
685
+ const count = await store().query(
630
686
  (e) => {
631
687
  event = e;
632
688
  if (e.name === SNAP_EVENT) {
@@ -639,9 +695,12 @@ async function load(me, stream, callback) {
639
695
  }
640
696
  callback && callback({ event, state: state2, patches, snaps });
641
697
  },
642
- { stream, with_snaps: true }
698
+ { stream, with_snaps: !cached, after: cached?.event_id }
699
+ );
700
+ logger.trace(
701
+ state2,
702
+ `\u{1F7E2} load ${stream}${cached && count === 0 ? " (cached)" : ""}`
643
703
  );
644
- logger.trace(state2, `\u{1F7E2} load ${stream}`);
645
704
  return { event, state: state2, patches, snaps };
646
705
  }
647
706
  async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
@@ -697,13 +756,21 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
697
756
  emitted.map((e) => e.data),
698
757
  `\u{1F534} commit ${stream}.${emitted.map((e) => e.name).join(", ")}`
699
758
  );
700
- const committed = await store().commit(
701
- stream,
702
- emitted,
703
- meta,
704
- // TODO: review reactions not enforcing expected version
705
- reactingTo ? void 0 : expected
706
- );
759
+ let committed;
760
+ try {
761
+ committed = await store().commit(
762
+ stream,
763
+ emitted,
764
+ meta,
765
+ // TODO: review reactions not enforcing expected version
766
+ reactingTo ? void 0 : expected
767
+ );
768
+ } catch (error) {
769
+ if (error.name === "ERR_CONCURRENCY") {
770
+ void cache().invalidate(stream);
771
+ }
772
+ throw error;
773
+ }
707
774
  let { state: state2, patches } = snapshot;
708
775
  const snapshots = committed.map((event) => {
709
776
  const p = me.patch[event.name](event, state2);
@@ -712,7 +779,15 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
712
779
  return { event, state: state2, patches, snaps: snapshot.snaps, patch: p };
713
780
  });
714
781
  const last = snapshots.at(-1);
715
- me.snap && me.snap(last) && void snap(last);
782
+ const snapped = me.snap && me.snap(last);
783
+ void cache().set(stream, {
784
+ state: last.state,
785
+ version: last.event.version,
786
+ event_id: last.event.id,
787
+ patches: snapped ? 0 : last.patches,
788
+ snaps: snapped ? last.snaps + 1 : last.snaps
789
+ });
790
+ if (snapped) void snap(last);
716
791
  return snapshots;
717
792
  }
718
793
 
@@ -1036,9 +1111,16 @@ var Act = class {
1036
1111
  this._drain_locked = true;
1037
1112
  const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
1038
1113
  const leading = streamLimit - lagging;
1039
- const polled = await store().poll(lagging, leading);
1114
+ const leased = await store().claim(
1115
+ lagging,
1116
+ leading,
1117
+ (0, import_crypto2.randomUUID)(),
1118
+ leaseMillis
1119
+ );
1120
+ if (!leased.length)
1121
+ return { fetched: [], leased: [], acked: [], blocked: [] };
1040
1122
  const fetched = await Promise.all(
1041
- polled.map(async ({ stream, source, at, lagging: lagging2 }) => {
1123
+ leased.map(async ({ stream, source, at, lagging: lagging2 }) => {
1042
1124
  const events = await this.query_array({
1043
1125
  stream: source,
1044
1126
  after: at,
@@ -1047,71 +1129,60 @@ var Act = class {
1047
1129
  return { stream, source, at, lagging: lagging2, events };
1048
1130
  })
1049
1131
  );
1050
- if (fetched.length) {
1051
- tracer.fetched(fetched);
1052
- const leases = /* @__PURE__ */ new Map();
1053
- const fetch_window_at = fetched.reduce(
1054
- (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
1055
- 0
1056
- );
1057
- fetched.forEach(({ stream, lagging: lagging2, events }) => {
1058
- const payloads = events.flatMap((event) => {
1059
- const register = this.registry.events[event.name];
1060
- if (!register) return [];
1061
- return [...register.reactions.values()].filter((reaction) => {
1062
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1063
- return resolved && resolved.target === stream;
1064
- }).map((reaction) => ({ ...reaction, event }));
1065
- });
1066
- leases.set(stream, {
1067
- lease: {
1068
- stream,
1069
- by: (0, import_crypto2.randomUUID)(),
1070
- at: events.at(-1)?.id || fetch_window_at,
1071
- // ff when no matching events
1072
- retry: 0,
1073
- lagging: lagging2
1074
- },
1075
- payloads
1076
- });
1132
+ tracer.fetched(fetched);
1133
+ const payloadsMap = /* @__PURE__ */ new Map();
1134
+ const fetch_window_at = fetched.reduce(
1135
+ (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
1136
+ 0
1137
+ );
1138
+ fetched.forEach(({ stream, events }) => {
1139
+ const payloads = events.flatMap((event) => {
1140
+ const register = this.registry.events[event.name];
1141
+ if (!register) return [];
1142
+ return [...register.reactions.values()].filter((reaction) => {
1143
+ const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1144
+ return resolved && resolved.target === stream;
1145
+ }).map((reaction) => ({ ...reaction, event }));
1077
1146
  });
1078
- const leased = await store().lease(
1079
- [...leases.values()].map(({ lease }) => lease),
1080
- leaseMillis
1081
- );
1082
- tracer.leased(leased);
1083
- const handled = await Promise.all(
1084
- leased.map(
1085
- (lease) => this.handle(lease, leases.get(lease.stream).payloads)
1086
- )
1087
- );
1088
- const [lagging_handled, leading_handled] = handled.reduce(
1089
- ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
1090
- lagging_handled2 + (lease.lagging ? handled2 : 0),
1091
- leading_handled2 + (lease.lagging ? 0 : handled2)
1092
- ],
1093
- [0, 0]
1094
- );
1095
- const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
1096
- const leading_avg = leading > 0 ? leading_handled / leading : 0;
1097
- const total = lagging_avg + leading_avg;
1098
- this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
1099
- const acked = await store().ack(
1100
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
1101
- );
1102
- if (acked.length) {
1103
- tracer.acked(acked);
1104
- this.emit("acked", acked);
1105
- }
1106
- const blocked = await store().block(
1107
- handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
1108
- );
1109
- if (blocked.length) {
1110
- tracer.blocked(blocked);
1111
- this.emit("blocked", blocked);
1112
- }
1113
- return { fetched, leased, acked, blocked };
1147
+ payloadsMap.set(stream, payloads);
1148
+ });
1149
+ tracer.leased(leased);
1150
+ const handled = await Promise.all(
1151
+ leased.map((lease) => {
1152
+ const streamFetch = fetched.find((f) => f.stream === lease.stream);
1153
+ const at = streamFetch?.events.at(-1)?.id || fetch_window_at;
1154
+ return this.handle(
1155
+ { ...lease, at },
1156
+ payloadsMap.get(lease.stream) || []
1157
+ );
1158
+ })
1159
+ );
1160
+ const [lagging_handled, leading_handled] = handled.reduce(
1161
+ ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
1162
+ lagging_handled2 + (lease.lagging ? handled2 : 0),
1163
+ leading_handled2 + (lease.lagging ? 0 : handled2)
1164
+ ],
1165
+ [0, 0]
1166
+ );
1167
+ const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
1168
+ const leading_avg = leading > 0 ? leading_handled / leading : 0;
1169
+ const total = lagging_avg + leading_avg;
1170
+ this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
1171
+ const acked = await store().ack(
1172
+ handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
1173
+ );
1174
+ if (acked.length) {
1175
+ tracer.acked(acked);
1176
+ this.emit("acked", acked);
1177
+ }
1178
+ const blocked = await store().block(
1179
+ handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
1180
+ );
1181
+ if (blocked.length) {
1182
+ tracer.blocked(blocked);
1183
+ this.emit("blocked", blocked);
1114
1184
  }
1185
+ return { fetched, leased, acked, blocked };
1115
1186
  } catch (error) {
1116
1187
  logger.error(error);
1117
1188
  } finally {
@@ -1174,26 +1245,31 @@ var Act = class {
1174
1245
  if (register) {
1175
1246
  for (const reaction of register.reactions.values()) {
1176
1247
  const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1177
- resolved && (correlated.get(resolved.target) || correlated.set(resolved.target, []).get(resolved.target)).push({ ...reaction, source: resolved.source, event });
1248
+ if (resolved) {
1249
+ const entry = correlated.get(resolved.target) || {
1250
+ source: resolved.source,
1251
+ payloads: []
1252
+ };
1253
+ entry.payloads.push({
1254
+ ...reaction,
1255
+ source: resolved.source,
1256
+ event
1257
+ });
1258
+ correlated.set(resolved.target, entry);
1259
+ }
1178
1260
  }
1179
1261
  }
1180
1262
  }, query);
1181
1263
  if (correlated.size) {
1182
- const leases = [...correlated.entries()].map(([stream, payloads]) => ({
1264
+ const streams = [...correlated.entries()].map(([stream, { source }]) => ({
1183
1265
  stream,
1184
- // TODO: by convention, the first defined source wins (this can be tricky)
1185
- source: payloads.find((p) => p.source)?.source || void 0,
1186
- by: (0, import_crypto2.randomUUID)(),
1187
- at: 0,
1188
- retry: 0,
1189
- lagging: true,
1190
- payloads
1266
+ source
1191
1267
  }));
1192
- const leased = await store().lease(leases, 0);
1193
- leased.length && tracer.correlated(leased);
1194
- return { leased, last_id };
1268
+ const subscribed = await store().subscribe(streams);
1269
+ subscribed && tracer.correlated(streams);
1270
+ return { subscribed, last_id };
1195
1271
  }
1196
- return { leased: [], last_id };
1272
+ return { subscribed: 0, last_id };
1197
1273
  }
1198
1274
  /**
1199
1275
  * Starts automatic periodic correlation worker for discovering new streams.
@@ -1257,7 +1333,7 @@ var Act = class {
1257
1333
  this._correlation_timer = setInterval(
1258
1334
  () => this.correlate({ ...query, after, limit }).then((result) => {
1259
1335
  after = result.last_id;
1260
- if (callback && result.leased.length) callback(result.leased);
1336
+ if (callback && result.subscribed) callback(result.subscribed);
1261
1337
  }).catch(console.error),
1262
1338
  frequency
1263
1339
  );
@@ -1341,8 +1417,8 @@ var Act = class {
1341
1417
  (async () => {
1342
1418
  let lastDrain;
1343
1419
  for (let i = 0; i < maxPasses; i++) {
1344
- const { leased } = await this.correlate(correlateQuery);
1345
- if (leased.length === 0 && i > 0) break;
1420
+ const { subscribed } = await this.correlate(correlateQuery);
1421
+ if (subscribed === 0 && i > 0) break;
1346
1422
  lastDrain = await this.drain(drainOptions);
1347
1423
  if (!lastDrain.acked.length && !lastDrain.blocked.length) break;
1348
1424
  }
@@ -1772,6 +1848,8 @@ function action_builder(state2) {
1772
1848
  Errors,
1773
1849
  EventMetaSchema,
1774
1850
  ExitCodes,
1851
+ InMemoryCache,
1852
+ InMemoryStore,
1775
1853
  InvariantError,
1776
1854
  LogLevels,
1777
1855
  PackageSchema,
@@ -1782,6 +1860,7 @@ function action_builder(state2) {
1782
1860
  ZodEmpty,
1783
1861
  act,
1784
1862
  build_tracer,
1863
+ cache,
1785
1864
  config,
1786
1865
  dispose,
1787
1866
  disposeAndExit,