@rotorsoft/act 0.6.0 → 0.6.2

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
@@ -48,6 +48,7 @@ __export(index_exports, {
48
48
  ValidationError: () => ValidationError,
49
49
  ZodEmpty: () => ZodEmpty,
50
50
  act: () => act,
51
+ build_tracer: () => build_tracer,
51
52
  config: () => config,
52
53
  dispose: () => dispose,
53
54
  disposeAndExit: () => disposeAndExit,
@@ -261,21 +262,21 @@ async function sleep(ms) {
261
262
 
262
263
  // src/adapters/InMemoryStore.ts
263
264
  var InMemoryStream = class {
264
- stream;
265
- source;
266
- at = -1;
267
- retry = -1;
268
- blocked = false;
269
- error = "";
270
- leased_at = void 0;
271
- leased_by = void 0;
272
- leased_until = void 0;
273
265
  constructor(stream, source) {
274
266
  this.stream = stream;
275
267
  this.source = source;
276
268
  }
269
+ _at = -1;
270
+ _retry = -1;
271
+ _blocked = false;
272
+ _error = "";
273
+ _leased_by = void 0;
274
+ _leased_until = void 0;
277
275
  get is_avaliable() {
278
- 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;
279
280
  }
280
281
  /**
281
282
  * Attempt to lease this stream for processing.
@@ -285,17 +286,18 @@ var InMemoryStream = class {
285
286
  * @returns The granted lease or undefined if blocked.
286
287
  */
287
288
  lease(at, by, millis) {
288
- if (this.is_avaliable && at > this.at) {
289
- this.leased_at = at;
290
- this.leased_by = by;
291
- this.leased_until = new Date(Date.now() + millis);
292
- millis > 0 && (this.retry = this.retry + 1);
289
+ if (this.is_avaliable) {
290
+ if (millis > 0) {
291
+ this._leased_by = by;
292
+ this._leased_until = new Date(Date.now() + millis);
293
+ this._retry = this._retry + 1;
294
+ }
293
295
  return {
294
296
  stream: this.stream,
295
297
  source: this.source,
296
298
  at,
297
299
  by,
298
- retry: this.retry
300
+ retry: this._retry
299
301
  };
300
302
  }
301
303
  }
@@ -305,27 +307,37 @@ var InMemoryStream = class {
305
307
  * @param by - Lease holder that processed the watermark.
306
308
  */
307
309
  ack(at, by) {
308
- if (this.leased_by === by && at >= this.at) {
309
- this.leased_at = void 0;
310
- this.leased_by = void 0;
311
- this.leased_until = void 0;
312
- this.at = at;
313
- this.retry = -1;
314
- return true;
310
+ if (this._leased_by === by) {
311
+ this._leased_by = void 0;
312
+ this._leased_until = void 0;
313
+ this._at = at;
314
+ this._retry = -1;
315
+ return {
316
+ stream: this.stream,
317
+ source: this.source,
318
+ at: this._at,
319
+ by,
320
+ retry: this._retry
321
+ };
315
322
  }
316
- return false;
317
323
  }
318
324
  /**
319
325
  * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
320
326
  * @param error Blocked error message.
321
327
  */
322
328
  block(by, error) {
323
- if (this.leased_by === by) {
324
- this.blocked = true;
325
- this.error = error;
326
- return true;
329
+ if (this._leased_by === by) {
330
+ this._blocked = true;
331
+ this._error = error;
332
+ return {
333
+ stream: this.stream,
334
+ source: this.source,
335
+ at: this._at,
336
+ by: this._leased_by,
337
+ retry: this._retry,
338
+ error: this._error
339
+ };
327
340
  }
328
- return false;
329
341
  }
330
342
  };
331
343
  var InMemoryStore = class {
@@ -357,6 +369,15 @@ var InMemoryStore = class {
357
369
  this._events.length = 0;
358
370
  this._streams = /* @__PURE__ */ new Map();
359
371
  }
372
+ in_query(query, e) {
373
+ if (query.stream && !RegExp(`^${query.stream}$`).test(e.stream))
374
+ return false;
375
+ if (query.names && !query.names.includes(e.name)) return false;
376
+ if (query.correlation && e.meta?.correlation !== query.correlation)
377
+ return false;
378
+ if (e.name === SNAP_EVENT && !query.with_snaps) return false;
379
+ return true;
380
+ }
360
381
  /**
361
382
  * Query events in the store, optionally filtered by query options.
362
383
  * @param callback - Function to call for each event.
@@ -365,30 +386,32 @@ var InMemoryStore = class {
365
386
  */
366
387
  async query(callback, query) {
367
388
  await sleep();
368
- const {
369
- stream,
370
- names,
371
- before,
372
- after = -1,
373
- limit,
374
- created_before,
375
- created_after,
376
- correlation,
377
- with_snaps = false
378
- } = query || {};
379
- let i = after + 1, count = 0;
380
- while (i < this._events.length) {
381
- const e = this._events[i++];
382
- if (stream && !RegExp(`^${stream}$`).test(e.stream)) continue;
383
- if (names && !names.includes(e.name)) continue;
384
- if (correlation && e.meta?.correlation !== correlation) continue;
385
- if (created_after && e.created <= created_after) continue;
386
- if (e.name === SNAP_EVENT && !with_snaps) continue;
387
- if (before && e.id >= before) break;
388
- if (created_before && e.created >= created_before) break;
389
- callback(e);
390
- count++;
391
- if (limit && count >= limit) break;
389
+ let count = 0;
390
+ if (query?.backward) {
391
+ let i = (query?.before || this._events.length) - 1;
392
+ while (i >= 0) {
393
+ const e = this._events[i--];
394
+ if (query && !this.in_query(query, e)) continue;
395
+ if (query?.created_before && e.created >= query.created_before)
396
+ continue;
397
+ if (query.after && e.id <= query.after) break;
398
+ if (query.created_after && e.created <= query.created_after) break;
399
+ callback(e);
400
+ count++;
401
+ if (query?.limit && count >= query.limit) break;
402
+ }
403
+ } else {
404
+ let i = (query?.after ?? -1) + 1;
405
+ while (i < this._events.length) {
406
+ const e = this._events[i++];
407
+ if (query && !this.in_query(query, e)) continue;
408
+ if (query?.created_after && e.created <= query.created_after) continue;
409
+ if (query?.before && e.id >= query.before) break;
410
+ if (query?.created_before && e.created >= query.created_before) break;
411
+ callback(e);
412
+ count++;
413
+ if (query?.limit && count >= query.limit) break;
414
+ }
392
415
  }
393
416
  return count;
394
417
  }
@@ -430,12 +453,15 @@ var InMemoryStore = class {
430
453
  }
431
454
  /**
432
455
  * Polls the store for unblocked streams needing processing, ordered by lease watermark ascending.
433
- * @param limit - Maximum number of streams to poll.
456
+ * @param lagging - Max number of streams to poll in ascending order.
457
+ * @param leading - Max number of streams to poll in descending order.
434
458
  * @returns The polled streams.
435
459
  */
436
- async poll(limit) {
460
+ async poll(lagging, leading) {
437
461
  await sleep();
438
- return [...this._streams.values()].filter((s) => s.is_avaliable).sort((a, b) => a.at - b.at).slice(0, limit).map(({ stream, source, at }) => ({ stream, source, at }));
462
+ const a = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => a2.at - b2.at).slice(0, lagging).map(({ stream, source, at }) => ({ stream, source, at }));
463
+ const b = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => b2.at - a2.at).slice(0, leading).map(({ stream, source, at }) => ({ stream, source, at }));
464
+ return [...a, ...b];
439
465
  }
440
466
  /**
441
467
  * Lease streams for processing (e.g., for distributed consumers).
@@ -445,10 +471,11 @@ var InMemoryStore = class {
445
471
  */
446
472
  async lease(leases, millis) {
447
473
  await sleep();
448
- return leases.map(({ stream, at, by, source }) => {
449
- const found = this._streams.get(stream) || // store new correlations
450
- this._streams.set(stream, new InMemoryStream(stream, source)).get(stream);
451
- return found.lease(at, by, millis);
474
+ return leases.map((l) => {
475
+ if (!this._streams.has(l.stream)) {
476
+ this._streams.set(l.stream, new InMemoryStream(l.stream, l.source));
477
+ }
478
+ return this._streams.get(l.stream)?.lease(l.at, l.by, millis);
452
479
  }).filter((l) => !!l);
453
480
  }
454
481
  /**
@@ -457,9 +484,7 @@ var InMemoryStore = class {
457
484
  */
458
485
  async ack(leases) {
459
486
  await sleep();
460
- return leases.filter(
461
- (lease) => this._streams.get(lease.stream)?.ack(lease.at, lease.by)
462
- );
487
+ return leases.map((l) => this._streams.get(l.stream)?.ack(l.at, l.by)).filter((l) => !!l);
463
488
  }
464
489
  /**
465
490
  * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
@@ -468,9 +493,7 @@ var InMemoryStore = class {
468
493
  */
469
494
  async block(leases) {
470
495
  await sleep();
471
- return leases.filter(
472
- (lease) => this._streams.get(lease.stream)?.block(lease.by, lease.error)
473
- );
496
+ return leases.map((l) => this._streams.get(l.stream)?.block(l.by, l.error)).filter((l) => !!l);
474
497
  }
475
498
  };
476
499
 
@@ -519,6 +542,62 @@ var SNAP_EVENT = "__snapshot__";
519
542
  var store = port(function store2(adapter) {
520
543
  return adapter || new InMemoryStore();
521
544
  });
545
+ function build_tracer(logLevel2) {
546
+ if (logLevel2 === "trace") {
547
+ return {
548
+ fetched: (fetched) => {
549
+ const data = Object.fromEntries(
550
+ fetched.map(({ stream, source, events }) => {
551
+ const key = source ? `${stream}<-${source}` : stream;
552
+ const value = Object.fromEntries(
553
+ events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
554
+ );
555
+ return [key, value];
556
+ })
557
+ );
558
+ logger.trace(data, "\u26A1\uFE0F fetch");
559
+ },
560
+ correlated: (leases) => {
561
+ const data = leases.map(({ stream }) => stream).join(" ");
562
+ logger.trace(`\u26A1\uFE0F correlate ${data}`);
563
+ },
564
+ leased: (leases) => {
565
+ const data = Object.fromEntries(
566
+ leases.map(({ stream, at, retry }) => [stream, { at, retry }])
567
+ );
568
+ logger.trace(data, "\u26A1\uFE0F lease");
569
+ },
570
+ acked: (leases) => {
571
+ const data = Object.fromEntries(
572
+ leases.map(({ stream, at, retry }) => [stream, { at, retry }])
573
+ );
574
+ logger.trace(data, "\u26A1\uFE0F ack");
575
+ },
576
+ blocked: (leases) => {
577
+ const data = Object.fromEntries(
578
+ leases.map(({ stream, at, retry, error }) => [
579
+ stream,
580
+ { at, retry, error }
581
+ ])
582
+ );
583
+ logger.trace(data, "\u26A1\uFE0F block");
584
+ }
585
+ };
586
+ } else {
587
+ return {
588
+ fetched: () => {
589
+ },
590
+ correlated: () => {
591
+ },
592
+ leased: () => {
593
+ },
594
+ acked: () => {
595
+ },
596
+ blocked: () => {
597
+ }
598
+ };
599
+ }
600
+ }
522
601
 
523
602
  // src/signals.ts
524
603
  process.once("SIGINT", async (arg) => {
@@ -657,40 +736,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
657
736
  }
658
737
 
659
738
  // src/act.ts
660
- function traceFetch(fetch) {
661
- const data = Object.fromEntries(
662
- fetch.map(({ stream, source, events }) => {
663
- const key = source ? `${stream}<-${source}` : stream;
664
- const value = Object.fromEntries(
665
- events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
666
- );
667
- return [key, value];
668
- })
669
- );
670
- logger.trace(data, "\u26A1\uFE0F fetch");
671
- }
672
- function traceCorrelated(leases) {
673
- const data = leases.map(({ stream }) => stream).join(" ");
674
- logger.trace(`\u26A1\uFE0F correlate ${data}`);
675
- }
676
- function traceLeased(leases) {
677
- const data = Object.fromEntries(
678
- leases.map(({ stream, at, retry }) => [stream, { at, retry }])
679
- );
680
- logger.trace(data, "\u26A1\uFE0F lease");
681
- }
682
- function traceAcked(leases) {
683
- const data = Object.fromEntries(
684
- leases.map(({ stream, at, retry }) => [stream, { at, retry }])
685
- );
686
- logger.trace(data, "\u26A1\uFE0F ack");
687
- }
688
- function traceBlocked(leases) {
689
- const data = Object.fromEntries(
690
- leases.map(({ stream, at, retry, error }) => [stream, { at, retry, error }])
691
- );
692
- logger.trace(data, "\u26A1\uFE0F block");
693
- }
739
+ var tracer = build_tracer(config().logLevel);
694
740
  var Act = class {
695
741
  /**
696
742
  * Create a new Act orchestrator.
@@ -834,24 +880,6 @@ var Act = class {
834
880
  }
835
881
  return { lease, at };
836
882
  }
837
- /**
838
- * Fetches new events from store according to the fetch options.
839
- * @param options - Fetch options.
840
- * @returns Fetched streams with next events to process.
841
- */
842
- async fetch({ streamLimit = 10, eventLimit = 10 }) {
843
- const polled = await store().poll(streamLimit);
844
- return Promise.all(
845
- polled.map(async ({ stream, source, at }) => {
846
- const events = await this.query_array({
847
- stream: source,
848
- after: at,
849
- limit: eventLimit
850
- });
851
- return { stream, source, events };
852
- })
853
- );
854
- }
855
883
  /**
856
884
  * Drains and processes events from the store, triggering reactions and updating state.
857
885
  *
@@ -870,26 +898,31 @@ var Act = class {
870
898
  if (!this._drain_locked) {
871
899
  try {
872
900
  this._drain_locked = true;
873
- const fetch = await this.fetch({ streamLimit, eventLimit });
874
- fetch.length && traceFetch(fetch);
875
- const [last_at, count] = fetch.reduce(
876
- ([last_at2, count2], { events }) => [
877
- Math.max(last_at2, events.at(-1)?.id || 0),
878
- count2 + events.length
879
- ],
880
- [0, 0]
901
+ const lagging = Math.ceil(streamLimit * 2 / 3);
902
+ const leading = streamLimit - lagging;
903
+ const polled = await store().poll(lagging, leading);
904
+ const fetched = await Promise.all(
905
+ polled.map(async ({ stream, source, at }) => {
906
+ const events = await this.query_array({
907
+ stream: source,
908
+ after: at,
909
+ limit: eventLimit
910
+ });
911
+ return { stream, source, at, events };
912
+ })
881
913
  );
882
- if (count > 0) {
914
+ if (fetched.length) {
915
+ tracer.fetched(fetched);
883
916
  const leases = /* @__PURE__ */ new Map();
884
- fetch.forEach(({ stream, events }) => {
917
+ const last_window_at = fetched.reduce(
918
+ (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
919
+ 0
920
+ );
921
+ fetched.forEach(({ stream, events }) => {
885
922
  const payloads = events.flatMap((event) => {
886
- const register = this.registry.events[event.name];
887
- if (!register) return [];
923
+ const register = this.registry.events[event.name] || [];
888
924
  return [...register.reactions.values()].filter((reaction) => {
889
- const resolved = typeof reaction.resolver === "function" ? (
890
- // @ts-expect-error index by key
891
- reaction.resolver(event)
892
- ) : reaction.resolver;
925
+ const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
893
926
  return resolved && resolved.target === stream;
894
927
  }).map((reaction) => ({ ...reaction, event }));
895
928
  });
@@ -897,43 +930,39 @@ var Act = class {
897
930
  lease: {
898
931
  stream,
899
932
  by: (0, import_crypto2.randomUUID)(),
900
- at: events.at(-1)?.id || last_at,
901
- // move the lease watermark forward when no events found in window
933
+ at: events.at(-1)?.id || last_window_at,
934
+ // ff when no matching events
902
935
  retry: 0
903
936
  },
904
937
  // @ts-expect-error indexed by key
905
938
  payloads
906
939
  });
907
940
  });
908
- if (leases.size) {
909
- const leased = await store().lease(
910
- [...leases.values()].map((l) => l.lease),
911
- leaseMillis
912
- );
913
- if (leased.length) {
914
- traceLeased(leased);
915
- const handled = await Promise.all(
916
- leased.map(
917
- (lease) => this.handle(lease, leases.get(lease.stream).payloads)
918
- )
919
- );
920
- const acked = await store().ack(
921
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
922
- );
923
- if (acked.length) {
924
- traceAcked(acked);
925
- this.emit("acked", acked);
926
- }
927
- const blocked = await store().block(
928
- handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
929
- );
930
- if (blocked.length) {
931
- traceBlocked(blocked);
932
- this.emit("blocked", blocked);
933
- }
934
- return { leased, acked, blocked };
935
- }
941
+ const leased = await store().lease(
942
+ [...leases.values()].map((l) => l.lease),
943
+ leaseMillis
944
+ );
945
+ tracer.leased(leased);
946
+ const handled = await Promise.all(
947
+ leased.map(
948
+ (lease) => this.handle(lease, leases.get(lease.stream).payloads)
949
+ )
950
+ );
951
+ const acked = await store().ack(
952
+ handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
953
+ );
954
+ if (acked.length) {
955
+ tracer.acked(acked);
956
+ this.emit("acked", acked);
957
+ }
958
+ const blocked = await store().block(
959
+ handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
960
+ );
961
+ if (blocked.length) {
962
+ tracer.blocked(blocked);
963
+ this.emit("blocked", blocked);
936
964
  }
965
+ return { fetched, leased, acked, blocked };
937
966
  }
938
967
  } catch (error) {
939
968
  logger.error(error);
@@ -941,7 +970,7 @@ var Act = class {
941
970
  this._drain_locked = false;
942
971
  }
943
972
  }
944
- return { leased: [], acked: [], blocked: [] };
973
+ return { fetched: [], leased: [], acked: [], blocked: [] };
945
974
  }
946
975
  /**
947
976
  * Correlates streams using reaction resolvers.
@@ -972,7 +1001,7 @@ var Act = class {
972
1001
  payloads
973
1002
  }));
974
1003
  const leased = await store().lease(leases, 0);
975
- leased.length && traceCorrelated(leased);
1004
+ leased.length && tracer.correlated(leased);
976
1005
  return { leased, last_id };
977
1006
  }
978
1007
  return { leased: [], last_id };
@@ -1173,6 +1202,7 @@ function action_builder(state2) {
1173
1202
  ValidationError,
1174
1203
  ZodEmpty,
1175
1204
  act,
1205
+ build_tracer,
1176
1206
  config,
1177
1207
  dispose,
1178
1208
  disposeAndExit,