@rotorsoft/act 0.6.1 → 0.6.3

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
@@ -262,71 +262,84 @@ async function sleep(ms) {
262
262
 
263
263
  // src/adapters/InMemoryStore.ts
264
264
  var InMemoryStream = class {
265
- stream;
266
- source;
267
- at = -1;
268
- retry = -1;
269
- blocked = false;
270
- error = "";
271
- leased_at = void 0;
272
- leased_by = void 0;
273
- leased_until = void 0;
274
265
  constructor(stream, source) {
275
266
  this.stream = stream;
276
267
  this.source = source;
277
268
  }
269
+ _at = -1;
270
+ _retry = -1;
271
+ _blocked = false;
272
+ _error = "";
273
+ _leased_by = void 0;
274
+ _leased_until = void 0;
278
275
  get is_avaliable() {
279
- return !this.blocked && (!this.leased_until || this.leased_until <= /* @__PURE__ */ new Date());
276
+ return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date());
277
+ }
278
+ get at() {
279
+ return this._at;
280
280
  }
281
281
  /**
282
282
  * Attempt to lease this stream for processing.
283
- * @param at - The end-of-lease watermark.
284
- * @param by - The lease holder.
283
+ * @param lease - The lease request.
285
284
  * @param millis - Lease duration in milliseconds.
286
285
  * @returns The granted lease or undefined if blocked.
287
286
  */
288
- lease(at, by, millis) {
289
- if (this.is_avaliable && at > this.at) {
290
- this.leased_at = at;
291
- this.leased_by = by;
292
- this.leased_until = new Date(Date.now() + millis);
293
- millis > 0 && (this.retry = this.retry + 1);
287
+ lease(lease, millis) {
288
+ if (this.is_avaliable) {
289
+ if (millis > 0) {
290
+ this._leased_by = lease.by;
291
+ this._leased_until = new Date(Date.now() + millis);
292
+ this._retry = this._retry + 1;
293
+ }
294
294
  return {
295
295
  stream: this.stream,
296
296
  source: this.source,
297
- at,
298
- by,
299
- retry: this.retry
297
+ at: lease.at,
298
+ by: lease.by,
299
+ retry: this._retry,
300
+ lagging: lease.lagging
300
301
  };
301
302
  }
302
303
  }
303
304
  /**
304
305
  * Acknowledge completion of processing for this stream.
305
- * @param at - Last processed watermark.
306
- * @param by - Lease holder that processed the watermark.
306
+ * @param lease - The lease request.
307
307
  */
308
- ack(at, by) {
309
- if (this.leased_by === by && at >= this.at) {
310
- this.leased_at = void 0;
311
- this.leased_by = void 0;
312
- this.leased_until = void 0;
313
- this.at = at;
314
- this.retry = -1;
315
- return true;
308
+ ack(lease) {
309
+ if (this._leased_by === lease.by) {
310
+ this._leased_by = void 0;
311
+ this._leased_until = void 0;
312
+ this._at = lease.at;
313
+ this._retry = -1;
314
+ return {
315
+ stream: this.stream,
316
+ source: this.source,
317
+ at: this._at,
318
+ by: lease.by,
319
+ retry: this._retry,
320
+ lagging: lease.lagging
321
+ };
316
322
  }
317
- return false;
318
323
  }
319
324
  /**
320
325
  * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
326
+ * @param lease - The lease request.
321
327
  * @param error Blocked error message.
322
328
  */
323
- block(by, error) {
324
- if (this.leased_by === by) {
325
- this.blocked = true;
326
- this.error = error;
327
- return true;
329
+ block(lease, error) {
330
+ if (this._leased_by === lease.by) {
331
+ this._blocked = true;
332
+ this._error = error;
333
+ return {
334
+ stream: this.stream,
335
+ source: this.source,
336
+ at: this._at,
337
+ by: this._leased_by,
338
+ retry: this._retry,
339
+ error: this._error,
340
+ lagging: lease.lagging
341
+ };
328
342
  }
329
- return false;
330
343
  }
331
344
  };
332
345
  var InMemoryStore = class {
@@ -358,6 +371,15 @@ var InMemoryStore = class {
358
371
  this._events.length = 0;
359
372
  this._streams = /* @__PURE__ */ new Map();
360
373
  }
374
+ in_query(query, e) {
375
+ if (query.stream && !RegExp(`^${query.stream}$`).test(e.stream))
376
+ return false;
377
+ if (query.names && !query.names.includes(e.name)) return false;
378
+ if (query.correlation && e.meta?.correlation !== query.correlation)
379
+ return false;
380
+ if (e.name === SNAP_EVENT && !query.with_snaps) return false;
381
+ return true;
382
+ }
361
383
  /**
362
384
  * Query events in the store, optionally filtered by query options.
363
385
  * @param callback - Function to call for each event.
@@ -366,30 +388,32 @@ var InMemoryStore = class {
366
388
  */
367
389
  async query(callback, query) {
368
390
  await sleep();
369
- const {
370
- stream,
371
- names,
372
- before,
373
- after = -1,
374
- limit,
375
- created_before,
376
- created_after,
377
- correlation,
378
- with_snaps = false
379
- } = query || {};
380
- let i = after + 1, count = 0;
381
- while (i < this._events.length) {
382
- const e = this._events[i++];
383
- if (stream && !RegExp(`^${stream}$`).test(e.stream)) continue;
384
- if (names && !names.includes(e.name)) continue;
385
- if (correlation && e.meta?.correlation !== correlation) continue;
386
- if (created_after && e.created <= created_after) continue;
387
- if (e.name === SNAP_EVENT && !with_snaps) continue;
388
- if (before && e.id >= before) break;
389
- if (created_before && e.created >= created_before) break;
390
- callback(e);
391
- count++;
392
- if (limit && count >= limit) break;
391
+ let count = 0;
392
+ if (query?.backward) {
393
+ let i = (query?.before || this._events.length) - 1;
394
+ while (i >= 0) {
395
+ const e = this._events[i--];
396
+ if (query && !this.in_query(query, e)) continue;
397
+ if (query?.created_before && e.created >= query.created_before)
398
+ continue;
399
+ if (query.after && e.id <= query.after) break;
400
+ if (query.created_after && e.created <= query.created_after) break;
401
+ callback(e);
402
+ count++;
403
+ if (query?.limit && count >= query.limit) break;
404
+ }
405
+ } else {
406
+ let i = (query?.after ?? -1) + 1;
407
+ while (i < this._events.length) {
408
+ const e = this._events[i++];
409
+ if (query && !this.in_query(query, e)) continue;
410
+ if (query?.created_after && e.created <= query.created_after) continue;
411
+ if (query?.before && e.id >= query.before) break;
412
+ if (query?.created_before && e.created >= query.created_before) break;
413
+ callback(e);
414
+ count++;
415
+ if (query?.limit && count >= query.limit) break;
416
+ }
393
417
  }
394
418
  return count;
395
419
  }
@@ -431,13 +455,25 @@ var InMemoryStore = class {
431
455
  }
432
456
  /**
433
457
  * Polls the store for unblocked streams needing processing, ordered by lease watermark ascending.
434
- * @param limit - Maximum number of streams to poll.
435
- * @param descending - Whether to poll streams in descending order (aka poll the most advanced first).
458
+ * @param lagging - Max number of streams to poll in ascending order.
459
+ * @param leading - Max number of streams to poll in descending order.
436
460
  * @returns The polled streams.
437
461
  */
438
- async poll(limit, descending = false) {
462
+ async poll(lagging, leading) {
439
463
  await sleep();
440
- return [...this._streams.values()].filter((s) => s.is_avaliable).sort((a, b) => descending ? b.at - a.at : a.at - b.at).slice(0, limit).map(({ stream, source, at }) => ({ stream, source, at }));
464
+ const a = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => a2.at - b2.at).slice(0, lagging).map(({ stream, source, at }) => ({
465
+ stream,
466
+ source,
467
+ at,
468
+ lagging: true
469
+ }));
470
+ const b = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => b2.at - a2.at).slice(0, leading).map(({ stream, source, at }) => ({
471
+ stream,
472
+ source,
473
+ at,
474
+ lagging: false
475
+ }));
476
+ return [...a, ...b];
441
477
  }
442
478
  /**
443
479
  * Lease streams for processing (e.g., for distributed consumers).
@@ -447,10 +483,11 @@ var InMemoryStore = class {
447
483
  */
448
484
  async lease(leases, millis) {
449
485
  await sleep();
450
- return leases.map(({ stream, at, by, source }) => {
451
- const found = this._streams.get(stream) || // store new correlations
452
- this._streams.set(stream, new InMemoryStream(stream, source)).get(stream);
453
- return found.lease(at, by, millis);
486
+ return leases.map((l) => {
487
+ if (!this._streams.has(l.stream)) {
488
+ this._streams.set(l.stream, new InMemoryStream(l.stream, l.source));
489
+ }
490
+ return this._streams.get(l.stream)?.lease(l, millis);
454
491
  }).filter((l) => !!l);
455
492
  }
456
493
  /**
@@ -459,9 +496,7 @@ var InMemoryStore = class {
459
496
  */
460
497
  async ack(leases) {
461
498
  await sleep();
462
- return leases.filter(
463
- (lease) => this._streams.get(lease.stream)?.ack(lease.at, lease.by)
464
- );
499
+ return leases.map((l) => this._streams.get(l.stream)?.ack(l)).filter((l) => !!l);
465
500
  }
466
501
  /**
467
502
  * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
@@ -470,9 +505,7 @@ var InMemoryStore = class {
470
505
  */
471
506
  async block(leases) {
472
507
  await sleep();
473
- return leases.filter(
474
- (lease) => this._streams.get(lease.stream)?.block(lease.by, lease.error)
475
- );
508
+ return leases.map((l) => this._streams.get(l.stream)?.block(l, l.error)).filter((l) => !!l);
476
509
  }
477
510
  };
478
511
 
@@ -732,6 +765,7 @@ var Act = class {
732
765
  }
733
766
  _emitter = new import_events.default();
734
767
  _drain_locked = false;
768
+ _drain_lag2lead_ratio = 0.5;
735
769
  _correlation_interval = void 0;
736
770
  emit(event, args) {
737
771
  return this._emitter.emit(event, args);
@@ -834,7 +868,7 @@ var Act = class {
834
868
  * @returns The lease with results
835
869
  */
836
870
  async handle(lease, payloads) {
837
- if (payloads.length === 0) return { lease, at: lease.at };
871
+ if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
838
872
  const stream = lease.stream;
839
873
  let at = payloads.at(0).event.id, handled = 0;
840
874
  lease.retry > 0 && logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
@@ -850,6 +884,7 @@ var Act = class {
850
884
  block && logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
851
885
  return {
852
886
  lease,
887
+ handled,
853
888
  at,
854
889
  // only report error when nothing was handled
855
890
  error: handled === 0 ? error.message : void 0,
@@ -857,7 +892,7 @@ var Act = class {
857
892
  };
858
893
  }
859
894
  }
860
- return { lease, at };
895
+ return { lease, handled, at };
861
896
  }
862
897
  /**
863
898
  * Drains and processes events from the store, triggering reactions and updating state.
@@ -872,42 +907,36 @@ var Act = class {
872
907
  async drain({
873
908
  streamLimit = 10,
874
909
  eventLimit = 10,
875
- leaseMillis = 1e4,
876
- descending = false
910
+ leaseMillis = 1e4
877
911
  } = {}) {
878
912
  if (!this._drain_locked) {
879
913
  try {
880
914
  this._drain_locked = true;
881
- const polled = await store().poll(streamLimit, descending);
915
+ const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
916
+ const leading = streamLimit - lagging;
917
+ const polled = await store().poll(lagging, leading);
882
918
  const fetched = await Promise.all(
883
- polled.map(async ({ stream, source, at }) => {
919
+ polled.map(async ({ stream, source, at, lagging: lagging2 }) => {
884
920
  const events = await this.query_array({
885
921
  stream: source,
886
922
  after: at,
887
923
  limit: eventLimit
888
924
  });
889
- return { stream, source, events };
925
+ return { stream, source, at, lagging: lagging2, events };
890
926
  })
891
927
  );
892
- fetched.length && tracer.fetched(fetched);
893
- const [last_at, count] = fetched.reduce(
894
- ([last_at2, count2], { events }) => [
895
- Math.max(last_at2, events.at(-1)?.id || 0),
896
- count2 + events.length
897
- ],
898
- [0, 0]
899
- );
900
- if (count > 0) {
928
+ if (fetched.length) {
929
+ tracer.fetched(fetched);
901
930
  const leases = /* @__PURE__ */ new Map();
902
- fetched.forEach(({ stream, events }) => {
931
+ const fetch_window_at = fetched.reduce(
932
+ (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
933
+ 0
934
+ );
935
+ fetched.forEach(({ stream, lagging: lagging2, events }) => {
903
936
  const payloads = events.flatMap((event) => {
904
- const register = this.registry.events[event.name];
905
- if (!register) return [];
937
+ const register = this.registry.events[event.name] || [];
906
938
  return [...register.reactions.values()].filter((reaction) => {
907
- const resolved = typeof reaction.resolver === "function" ? (
908
- // @ts-expect-error index by key
909
- reaction.resolver(event)
910
- ) : reaction.resolver;
939
+ const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
911
940
  return resolved && resolved.target === stream;
912
941
  }).map((reaction) => ({ ...reaction, event }));
913
942
  });
@@ -915,43 +944,51 @@ var Act = class {
915
944
  lease: {
916
945
  stream,
917
946
  by: (0, import_crypto2.randomUUID)(),
918
- at: events.at(-1)?.id || last_at,
919
- // move the lease watermark forward when no events found in window
920
- retry: 0
947
+ at: events.at(-1)?.id || fetch_window_at,
948
+ // ff when no matching events
949
+ retry: 0,
950
+ lagging: lagging2
921
951
  },
922
952
  // @ts-expect-error indexed by key
923
953
  payloads
924
954
  });
925
955
  });
926
- if (leases.size) {
927
- const leased = await store().lease(
928
- [...leases.values()].map((l) => l.lease),
929
- leaseMillis
930
- );
931
- if (leased.length) {
932
- tracer.leased(leased);
933
- const handled = await Promise.all(
934
- leased.map(
935
- (lease) => this.handle(lease, leases.get(lease.stream).payloads)
936
- )
937
- );
938
- const acked = await store().ack(
939
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
940
- );
941
- if (acked.length) {
942
- tracer.acked(acked);
943
- this.emit("acked", acked);
944
- }
945
- const blocked = await store().block(
946
- handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
947
- );
948
- if (blocked.length) {
949
- tracer.blocked(blocked);
950
- this.emit("blocked", blocked);
951
- }
952
- return { leased, acked, blocked };
953
- }
956
+ const leased = await store().lease(
957
+ [...leases.values()].map(({ lease }) => lease),
958
+ leaseMillis
959
+ );
960
+ tracer.leased(leased);
961
+ const handled = await Promise.all(
962
+ leased.map(
963
+ (lease) => this.handle(lease, leases.get(lease.stream).payloads)
964
+ )
965
+ );
966
+ const [lagging_handled, leading_handled] = handled.reduce(
967
+ ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
968
+ lagging_handled2 + (lease.lagging ? handled2 : 0),
969
+ leading_handled2 + (lease.lagging ? 0 : handled2)
970
+ ],
971
+ [0, 0]
972
+ );
973
+ const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
974
+ const leading_avg = leading > 0 ? leading_handled / leading : 0;
975
+ const total = lagging_avg + leading_avg;
976
+ this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
977
+ const acked = await store().ack(
978
+ handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
979
+ );
980
+ if (acked.length) {
981
+ tracer.acked(acked);
982
+ this.emit("acked", acked);
983
+ }
984
+ const blocked = await store().block(
985
+ handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
986
+ );
987
+ if (blocked.length) {
988
+ tracer.blocked(blocked);
989
+ this.emit("blocked", blocked);
954
990
  }
991
+ return { fetched, leased, acked, blocked };
955
992
  }
956
993
  } catch (error) {
957
994
  logger.error(error);
@@ -959,7 +996,7 @@ var Act = class {
959
996
  this._drain_locked = false;
960
997
  }
961
998
  }
962
- return { leased: [], acked: [], blocked: [] };
999
+ return { fetched: [], leased: [], acked: [], blocked: [] };
963
1000
  }
964
1001
  /**
965
1002
  * Correlates streams using reaction resolvers.
@@ -987,6 +1024,7 @@ var Act = class {
987
1024
  by: (0, import_crypto2.randomUUID)(),
988
1025
  at: 0,
989
1026
  retry: 0,
1027
+ lagging: true,
990
1028
  payloads
991
1029
  }));
992
1030
  const leased = await store().lease(leases, 0);