@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.cjs CHANGED
@@ -460,41 +460,58 @@ var InMemoryStore = class {
460
460
  });
461
461
  }
462
462
  /**
463
- * Polls the store for unblocked streams needing processing, ordered by lease watermark ascending.
464
- * @param lagging - Max number of streams to poll in ascending order.
465
- * @param leading - Max number of streams to poll in descending order.
466
- * @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.
467
470
  */
468
- async poll(lagging, leading) {
471
+ async claim(lagging, leading, by, millis) {
469
472
  await sleep();
470
- const a = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => a2.at - b2.at).slice(0, lagging).map(({ stream, source, at }) => ({
471
- stream,
472
- source,
473
- 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,
474
478
  lagging: true
475
479
  }));
476
- const b = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => b2.at - a2.at).slice(0, leading).map(({ stream, source, at }) => ({
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
- 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);
483
495
  }
484
496
  /**
485
- * Lease streams for processing (e.g., for distributed consumers).
486
- * @param leases - Lease requests for streams, including end-of-lease watermark, lease holder, and source stream.
487
- * @param leaseMilis - Lease duration in milliseconds.
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 lease(leases, millis) {
501
+ async subscribe(streams) {
491
502
  await sleep();
492
- return leases.map((l) => {
493
- if (!this._streams.has(l.stream)) {
494
- this._streams.set(l.stream, new InMemoryStream(l.stream, l.source));
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
- return this._streams.get(l.stream)?.lease(l, millis);
497
- }).filter((l) => !!l);
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: (leases) => {
582
- const data = leases.map(({ stream }) => stream).join(" ");
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 polled = await store().poll(lagging, leading);
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
- polled.map(async ({ stream, source, at, lagging: lagging2 }) => {
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
- if (fetched.length) {
1113
- tracer.fetched(fetched);
1114
- const leases = /* @__PURE__ */ new Map();
1115
- const fetch_window_at = fetched.reduce(
1116
- (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
1117
- 0
1118
- );
1119
- fetched.forEach(({ stream, lagging: lagging2, events }) => {
1120
- const payloads = events.flatMap((event) => {
1121
- const register = this.registry.events[event.name];
1122
- if (!register) return [];
1123
- return [...register.reactions.values()].filter((reaction) => {
1124
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1125
- return resolved && resolved.target === stream;
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
- const leased = await store().lease(
1141
- [...leases.values()].map(({ lease }) => lease),
1142
- leaseMillis
1143
- );
1144
- tracer.leased(leased);
1145
- const handled = await Promise.all(
1146
- leased.map(
1147
- (lease) => this.handle(lease, leases.get(lease.stream).payloads)
1148
- )
1149
- );
1150
- const [lagging_handled, leading_handled] = handled.reduce(
1151
- ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
1152
- lagging_handled2 + (lease.lagging ? handled2 : 0),
1153
- leading_handled2 + (lease.lagging ? 0 : handled2)
1154
- ],
1155
- [0, 0]
1156
- );
1157
- const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
1158
- const leading_avg = leading > 0 ? leading_handled / leading : 0;
1159
- const total = lagging_avg + leading_avg;
1160
- this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
1161
- const acked = await store().ack(
1162
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
1163
- );
1164
- if (acked.length) {
1165
- tracer.acked(acked);
1166
- this.emit("acked", acked);
1167
- }
1168
- const blocked = await store().block(
1169
- handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
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 = query.after || -1;
1233
- await store().query((event) => {
1234
- last_id = event.id;
1235
- const register = this.registry.events[event.name];
1236
- if (register) {
1237
- for (const reaction of register.reactions.values()) {
1238
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1239
- resolved && (correlated.get(resolved.target) || correlated.set(resolved.target, []).get(resolved.target)).push({ ...reaction, source: resolved.source, event });
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
- }, query);
1306
+ },
1307
+ { ...query, after }
1308
+ );
1309
+ this._correlation_checkpoint = last_id;
1243
1310
  if (correlated.size) {
1244
- const leases = [...correlated.entries()].map(([stream, payloads]) => ({
1311
+ const streams = [...correlated.entries()].map(([stream, { source }]) => ({
1245
1312
  stream,
1246
- // TODO: by convention, the first defined source wins (this can be tricky)
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 leased = await store().lease(leases, 0);
1255
- leased.length && tracer.correlated(leased);
1256
- return { leased, last_id };
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 { leased: [], last_id };
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
- after = result.last_id;
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 { leased } = await this.correlate(correlateQuery);
1407
- if (leased.length === 0 && i > 0) break;
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
  }