@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/dist/index.js CHANGED
@@ -391,41 +391,58 @@ var InMemoryStore = class {
391
391
  });
392
392
  }
393
393
  /**
394
- * Polls the store for unblocked streams needing processing, ordered by lease watermark ascending.
395
- * @param lagging - Max number of streams to poll in ascending order.
396
- * @param leading - Max number of streams to poll in descending order.
397
- * @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.
398
401
  */
399
- async poll(lagging, leading) {
402
+ async claim(lagging, leading, by, millis) {
400
403
  await sleep();
401
- const a = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => a2.at - b2.at).slice(0, lagging).map(({ stream, source, at }) => ({
402
- stream,
403
- source,
404
- 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,
405
409
  lagging: true
406
410
  }));
407
- const b = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => b2.at - a2.at).slice(0, leading).map(({ stream, source, at }) => ({
408
- stream,
409
- source,
410
- 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,
411
415
  lagging: false
412
416
  }));
413
- 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);
414
426
  }
415
427
  /**
416
- * Lease streams for processing (e.g., for distributed consumers).
417
- * @param leases - Lease requests for streams, including end-of-lease watermark, lease holder, and source stream.
418
- * @param leaseMilis - Lease duration in milliseconds.
419
- * @returns Granted leases.
428
+ * Registers streams for event processing.
429
+ * @param streams - Streams to register with optional source.
430
+ * @returns subscribed count and current max watermark.
420
431
  */
421
- async lease(leases, millis) {
432
+ async subscribe(streams) {
422
433
  await sleep();
423
- return leases.map((l) => {
424
- if (!this._streams.has(l.stream)) {
425
- this._streams.set(l.stream, new InMemoryStream(l.stream, l.source));
434
+ let subscribed = 0;
435
+ for (const { stream, source } of streams) {
436
+ if (!this._streams.has(stream)) {
437
+ this._streams.set(stream, new InMemoryStream(stream, source));
438
+ subscribed++;
426
439
  }
427
- return this._streams.get(l.stream)?.lease(l, millis);
428
- }).filter((l) => !!l);
440
+ }
441
+ let watermark = -1;
442
+ for (const s of this._streams.values()) {
443
+ if (s.at > watermark) watermark = s.at;
444
+ }
445
+ return { subscribed, watermark };
429
446
  }
430
447
  /**
431
448
  * Acknowledge completion of processing for leased streams.
@@ -509,8 +526,8 @@ function build_tracer(logLevel2) {
509
526
  );
510
527
  logger.trace(data, "\u26A1\uFE0F fetch");
511
528
  },
512
- correlated: (leases) => {
513
- const data = leases.map(({ stream }) => stream).join(" ");
529
+ correlated: (streams) => {
530
+ const data = streams.map(({ stream }) => stream).join(" ");
514
531
  logger.trace(`\u26A1\uFE0F correlate ${data}`);
515
532
  },
516
533
  leased: (leases) => {
@@ -712,15 +729,21 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
712
729
  // src/act.ts
713
730
  var tracer = build_tracer(config().logLevel);
714
731
  var Act = class {
715
- /**
716
- * Create a new Act orchestrator.
717
- *
718
- * @param registry The registry of state, event, and action schemas
719
- * @param states Map of state names to their (potentially merged) state definitions
720
- */
721
732
  constructor(registry, _states = /* @__PURE__ */ new Map()) {
722
733
  this.registry = registry;
723
734
  this._states = _states;
735
+ const statics = [];
736
+ for (const register of Object.values(this.registry.events)) {
737
+ for (const reaction of register.reactions.values()) {
738
+ if (typeof reaction.resolver === "function") {
739
+ this._has_dynamic_resolvers = true;
740
+ } else if (reaction.resolver) {
741
+ const r = reaction.resolver;
742
+ statics.push({ stream: r.target, source: r.source });
743
+ }
744
+ }
745
+ }
746
+ this._static_targets = statics;
724
747
  dispose(() => {
725
748
  this._emitter.removeAllListeners();
726
749
  this.stop_correlations();
@@ -734,6 +757,10 @@ var Act = class {
734
757
  _correlation_timer = void 0;
735
758
  _settle_timer = void 0;
736
759
  _settling = false;
760
+ _correlation_checkpoint = -1;
761
+ _subscribed_statics = /* @__PURE__ */ new Set();
762
+ _has_dynamic_resolvers = false;
763
+ _correlation_initialized = false;
737
764
  emit(event, args) {
738
765
  return this._emitter.emit(event, args);
739
766
  }
@@ -745,6 +772,14 @@ var Act = class {
745
772
  this._emitter.off(event, listener);
746
773
  return this;
747
774
  }
775
+ /**
776
+ * Create a new Act orchestrator.
777
+ *
778
+ * @param registry The registry of state, event, and action schemas
779
+ * @param states Map of state names to their (potentially merged) state definitions
780
+ */
781
+ /** Static resolver targets collected at build time */
782
+ _static_targets;
748
783
  /**
749
784
  * Executes an action on a state instance, committing resulting events.
750
785
  *
@@ -1029,9 +1064,16 @@ var Act = class {
1029
1064
  this._drain_locked = true;
1030
1065
  const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
1031
1066
  const leading = streamLimit - lagging;
1032
- const polled = await store().poll(lagging, leading);
1067
+ const leased = await store().claim(
1068
+ lagging,
1069
+ leading,
1070
+ randomUUID2(),
1071
+ leaseMillis
1072
+ );
1073
+ if (!leased.length)
1074
+ return { fetched: [], leased: [], acked: [], blocked: [] };
1033
1075
  const fetched = await Promise.all(
1034
- polled.map(async ({ stream, source, at, lagging: lagging2 }) => {
1076
+ leased.map(async ({ stream, source, at, lagging: lagging2 }) => {
1035
1077
  const events = await this.query_array({
1036
1078
  stream: source,
1037
1079
  after: at,
@@ -1040,71 +1082,60 @@ var Act = class {
1040
1082
  return { stream, source, at, lagging: lagging2, events };
1041
1083
  })
1042
1084
  );
1043
- if (fetched.length) {
1044
- tracer.fetched(fetched);
1045
- const leases = /* @__PURE__ */ new Map();
1046
- const fetch_window_at = fetched.reduce(
1047
- (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
1048
- 0
1049
- );
1050
- fetched.forEach(({ stream, lagging: lagging2, events }) => {
1051
- const payloads = events.flatMap((event) => {
1052
- const register = this.registry.events[event.name];
1053
- if (!register) return [];
1054
- return [...register.reactions.values()].filter((reaction) => {
1055
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1056
- return resolved && resolved.target === stream;
1057
- }).map((reaction) => ({ ...reaction, event }));
1058
- });
1059
- leases.set(stream, {
1060
- lease: {
1061
- stream,
1062
- by: randomUUID2(),
1063
- at: events.at(-1)?.id || fetch_window_at,
1064
- // ff when no matching events
1065
- retry: 0,
1066
- lagging: lagging2
1067
- },
1068
- payloads
1069
- });
1085
+ tracer.fetched(fetched);
1086
+ const payloadsMap = /* @__PURE__ */ new Map();
1087
+ const fetch_window_at = fetched.reduce(
1088
+ (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
1089
+ 0
1090
+ );
1091
+ fetched.forEach(({ stream, events }) => {
1092
+ const payloads = events.flatMap((event) => {
1093
+ const register = this.registry.events[event.name];
1094
+ if (!register) return [];
1095
+ return [...register.reactions.values()].filter((reaction) => {
1096
+ const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1097
+ return resolved && resolved.target === stream;
1098
+ }).map((reaction) => ({ ...reaction, event }));
1070
1099
  });
1071
- const leased = await store().lease(
1072
- [...leases.values()].map(({ lease }) => lease),
1073
- leaseMillis
1074
- );
1075
- tracer.leased(leased);
1076
- const handled = await Promise.all(
1077
- leased.map(
1078
- (lease) => this.handle(lease, leases.get(lease.stream).payloads)
1079
- )
1080
- );
1081
- const [lagging_handled, leading_handled] = handled.reduce(
1082
- ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
1083
- lagging_handled2 + (lease.lagging ? handled2 : 0),
1084
- leading_handled2 + (lease.lagging ? 0 : handled2)
1085
- ],
1086
- [0, 0]
1087
- );
1088
- const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
1089
- const leading_avg = leading > 0 ? leading_handled / leading : 0;
1090
- const total = lagging_avg + leading_avg;
1091
- this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
1092
- const acked = await store().ack(
1093
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
1094
- );
1095
- if (acked.length) {
1096
- tracer.acked(acked);
1097
- this.emit("acked", acked);
1098
- }
1099
- const blocked = await store().block(
1100
- handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
1101
- );
1102
- if (blocked.length) {
1103
- tracer.blocked(blocked);
1104
- this.emit("blocked", blocked);
1105
- }
1106
- return { fetched, leased, acked, blocked };
1100
+ payloadsMap.set(stream, payloads);
1101
+ });
1102
+ tracer.leased(leased);
1103
+ const handled = await Promise.all(
1104
+ leased.map((lease) => {
1105
+ const streamFetch = fetched.find((f) => f.stream === lease.stream);
1106
+ const at = streamFetch?.events.at(-1)?.id || fetch_window_at;
1107
+ return this.handle(
1108
+ { ...lease, at },
1109
+ payloadsMap.get(lease.stream) || []
1110
+ );
1111
+ })
1112
+ );
1113
+ const [lagging_handled, leading_handled] = handled.reduce(
1114
+ ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
1115
+ lagging_handled2 + (lease.lagging ? handled2 : 0),
1116
+ leading_handled2 + (lease.lagging ? 0 : handled2)
1117
+ ],
1118
+ [0, 0]
1119
+ );
1120
+ const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
1121
+ const leading_avg = leading > 0 ? leading_handled / leading : 0;
1122
+ const total = lagging_avg + leading_avg;
1123
+ this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
1124
+ const acked = await store().ack(
1125
+ handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
1126
+ );
1127
+ if (acked.length) {
1128
+ tracer.acked(acked);
1129
+ this.emit("acked", acked);
1107
1130
  }
1131
+ const blocked = await store().block(
1132
+ handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
1133
+ );
1134
+ if (blocked.length) {
1135
+ tracer.blocked(blocked);
1136
+ this.emit("blocked", blocked);
1137
+ }
1138
+ return { fetched, leased, acked, blocked };
1108
1139
  } catch (error) {
1109
1140
  logger.error(error);
1110
1141
  } finally {
@@ -1158,35 +1189,70 @@ var Act = class {
1158
1189
  * @see {@link start_correlations} for automatic periodic correlation
1159
1190
  * @see {@link stop_correlations} to stop automatic correlation
1160
1191
  */
1192
+ /**
1193
+ * Initialize correlation state on first call.
1194
+ * - Reads max(at) from store as cold-start checkpoint
1195
+ * - Subscribes static resolver targets (idempotent upsert)
1196
+ * - Populates the subscribed statics set
1197
+ * @internal
1198
+ */
1199
+ async _init_correlation() {
1200
+ if (this._correlation_initialized) return;
1201
+ this._correlation_initialized = true;
1202
+ const { watermark } = await store().subscribe(this._static_targets);
1203
+ this._correlation_checkpoint = watermark;
1204
+ for (const { stream } of this._static_targets) {
1205
+ this._subscribed_statics.add(stream);
1206
+ }
1207
+ }
1161
1208
  async correlate(query = { after: -1, limit: 10 }) {
1209
+ await this._init_correlation();
1210
+ if (!this._has_dynamic_resolvers)
1211
+ return { subscribed: 0, last_id: this._correlation_checkpoint };
1212
+ const after = Math.max(this._correlation_checkpoint, query.after || -1);
1162
1213
  const correlated = /* @__PURE__ */ new Map();
1163
- let last_id = query.after || -1;
1164
- await store().query((event) => {
1165
- last_id = event.id;
1166
- const register = this.registry.events[event.name];
1167
- if (register) {
1168
- for (const reaction of register.reactions.values()) {
1169
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1170
- resolved && (correlated.get(resolved.target) || correlated.set(resolved.target, []).get(resolved.target)).push({ ...reaction, source: resolved.source, event });
1214
+ let last_id = after;
1215
+ await store().query(
1216
+ (event) => {
1217
+ last_id = event.id;
1218
+ const register = this.registry.events[event.name];
1219
+ if (register) {
1220
+ for (const reaction of register.reactions.values()) {
1221
+ if (typeof reaction.resolver !== "function") continue;
1222
+ const resolved = reaction.resolver(event);
1223
+ if (resolved && !this._subscribed_statics.has(resolved.target)) {
1224
+ const entry = correlated.get(resolved.target) || {
1225
+ source: resolved.source,
1226
+ payloads: []
1227
+ };
1228
+ entry.payloads.push({
1229
+ ...reaction,
1230
+ source: resolved.source,
1231
+ event
1232
+ });
1233
+ correlated.set(resolved.target, entry);
1234
+ }
1235
+ }
1171
1236
  }
1172
- }
1173
- }, query);
1237
+ },
1238
+ { ...query, after }
1239
+ );
1240
+ this._correlation_checkpoint = last_id;
1174
1241
  if (correlated.size) {
1175
- const leases = [...correlated.entries()].map(([stream, payloads]) => ({
1242
+ const streams = [...correlated.entries()].map(([stream, { source }]) => ({
1176
1243
  stream,
1177
- // TODO: by convention, the first defined source wins (this can be tricky)
1178
- source: payloads.find((p) => p.source)?.source || void 0,
1179
- by: randomUUID2(),
1180
- at: 0,
1181
- retry: 0,
1182
- lagging: true,
1183
- payloads
1244
+ source
1184
1245
  }));
1185
- const leased = await store().lease(leases, 0);
1186
- leased.length && tracer.correlated(leased);
1187
- return { leased, last_id };
1246
+ const { subscribed } = await store().subscribe(streams);
1247
+ if (subscribed) {
1248
+ tracer.correlated(streams);
1249
+ for (const { stream } of streams) {
1250
+ this._subscribed_statics.add(stream);
1251
+ }
1252
+ }
1253
+ return { subscribed, last_id };
1188
1254
  }
1189
- return { leased: [], last_id };
1255
+ return { subscribed: 0, last_id };
1190
1256
  }
1191
1257
  /**
1192
1258
  * Starts automatic periodic correlation worker for discovering new streams.
@@ -1246,11 +1312,9 @@ var Act = class {
1246
1312
  start_correlations(query = {}, frequency = 1e4, callback) {
1247
1313
  if (this._correlation_timer) return false;
1248
1314
  const limit = query.limit || 100;
1249
- let after = query.after || -1;
1250
1315
  this._correlation_timer = setInterval(
1251
- () => this.correlate({ ...query, after, limit }).then((result) => {
1252
- after = result.last_id;
1253
- if (callback && result.leased.length) callback(result.leased);
1316
+ () => this.correlate({ ...query, after: this._correlation_checkpoint, limit }).then((result) => {
1317
+ if (callback && result.subscribed) callback(result.subscribed);
1254
1318
  }).catch(console.error),
1255
1319
  frequency
1256
1320
  );
@@ -1332,10 +1396,14 @@ var Act = class {
1332
1396
  if (this._settling) return;
1333
1397
  this._settling = true;
1334
1398
  (async () => {
1399
+ await this._init_correlation();
1335
1400
  let lastDrain;
1336
1401
  for (let i = 0; i < maxPasses; i++) {
1337
- const { leased } = await this.correlate(correlateQuery);
1338
- if (leased.length === 0 && i > 0) break;
1402
+ const { subscribed } = await this.correlate({
1403
+ ...correlateQuery,
1404
+ after: this._correlation_checkpoint
1405
+ });
1406
+ if (subscribed === 0 && i > 0) break;
1339
1407
  lastDrain = await this.drain(drainOptions);
1340
1408
  if (!lastDrain.acked.length && !lastDrain.blocked.length) break;
1341
1409
  }