@rotorsoft/act 0.20.0 → 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
@@ -391,41 +391,54 @@ 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 Number of newly registered streams.
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 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++;
426
439
  }
427
- return this._streams.get(l.stream)?.lease(l, millis);
428
- }).filter((l) => !!l);
440
+ }
441
+ return count;
429
442
  }
430
443
  /**
431
444
  * Acknowledge completion of processing for leased streams.
@@ -509,8 +522,8 @@ function build_tracer(logLevel2) {
509
522
  );
510
523
  logger.trace(data, "\u26A1\uFE0F fetch");
511
524
  },
512
- correlated: (leases) => {
513
- const data = leases.map(({ stream }) => stream).join(" ");
525
+ correlated: (streams) => {
526
+ const data = streams.map(({ stream }) => stream).join(" ");
514
527
  logger.trace(`\u26A1\uFE0F correlate ${data}`);
515
528
  },
516
529
  leased: (leases) => {
@@ -1029,9 +1042,16 @@ var Act = class {
1029
1042
  this._drain_locked = true;
1030
1043
  const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
1031
1044
  const leading = streamLimit - lagging;
1032
- 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: [] };
1033
1053
  const fetched = await Promise.all(
1034
- polled.map(async ({ stream, source, at, lagging: lagging2 }) => {
1054
+ leased.map(async ({ stream, source, at, lagging: lagging2 }) => {
1035
1055
  const events = await this.query_array({
1036
1056
  stream: source,
1037
1057
  after: at,
@@ -1040,71 +1060,60 @@ var Act = class {
1040
1060
  return { stream, source, at, lagging: lagging2, events };
1041
1061
  })
1042
1062
  );
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
- });
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 }));
1070
1077
  });
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 };
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);
1107
1115
  }
1116
+ return { fetched, leased, acked, blocked };
1108
1117
  } catch (error) {
1109
1118
  logger.error(error);
1110
1119
  } finally {
@@ -1167,26 +1176,31 @@ var Act = class {
1167
1176
  if (register) {
1168
1177
  for (const reaction of register.reactions.values()) {
1169
1178
  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 });
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
+ }
1171
1191
  }
1172
1192
  }
1173
1193
  }, query);
1174
1194
  if (correlated.size) {
1175
- const leases = [...correlated.entries()].map(([stream, payloads]) => ({
1195
+ const streams = [...correlated.entries()].map(([stream, { source }]) => ({
1176
1196
  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
1197
+ source
1184
1198
  }));
1185
- const leased = await store().lease(leases, 0);
1186
- leased.length && tracer.correlated(leased);
1187
- return { leased, last_id };
1199
+ const subscribed = await store().subscribe(streams);
1200
+ subscribed && tracer.correlated(streams);
1201
+ return { subscribed, last_id };
1188
1202
  }
1189
- return { leased: [], last_id };
1203
+ return { subscribed: 0, last_id };
1190
1204
  }
1191
1205
  /**
1192
1206
  * Starts automatic periodic correlation worker for discovering new streams.
@@ -1250,7 +1264,7 @@ var Act = class {
1250
1264
  this._correlation_timer = setInterval(
1251
1265
  () => this.correlate({ ...query, after, limit }).then((result) => {
1252
1266
  after = result.last_id;
1253
- if (callback && result.leased.length) callback(result.leased);
1267
+ if (callback && result.subscribed) callback(result.subscribed);
1254
1268
  }).catch(console.error),
1255
1269
  frequency
1256
1270
  );
@@ -1334,8 +1348,8 @@ var Act = class {
1334
1348
  (async () => {
1335
1349
  let lastDrain;
1336
1350
  for (let i = 0; i < maxPasses; i++) {
1337
- const { leased } = await this.correlate(correlateQuery);
1338
- if (leased.length === 0 && i > 0) break;
1351
+ const { subscribed } = await this.correlate(correlateQuery);
1352
+ if (subscribed === 0 && i > 0) break;
1339
1353
  lastDrain = await this.drain(drainOptions);
1340
1354
  if (!lastDrain.acked.length && !lastDrain.blocked.length) break;
1341
1355
  }