@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.js CHANGED
@@ -197,21 +197,21 @@ async function sleep(ms) {
197
197
 
198
198
  // src/adapters/InMemoryStore.ts
199
199
  var InMemoryStream = class {
200
- stream;
201
- source;
202
- at = -1;
203
- retry = -1;
204
- blocked = false;
205
- error = "";
206
- leased_at = void 0;
207
- leased_by = void 0;
208
- leased_until = void 0;
209
200
  constructor(stream, source) {
210
201
  this.stream = stream;
211
202
  this.source = source;
212
203
  }
204
+ _at = -1;
205
+ _retry = -1;
206
+ _blocked = false;
207
+ _error = "";
208
+ _leased_by = void 0;
209
+ _leased_until = void 0;
213
210
  get is_avaliable() {
214
- return !this.blocked && (!this.leased_until || this.leased_until <= /* @__PURE__ */ new Date());
211
+ return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date());
212
+ }
213
+ get at() {
214
+ return this._at;
215
215
  }
216
216
  /**
217
217
  * Attempt to lease this stream for processing.
@@ -221,17 +221,18 @@ var InMemoryStream = class {
221
221
  * @returns The granted lease or undefined if blocked.
222
222
  */
223
223
  lease(at, by, millis) {
224
- if (this.is_avaliable && at > this.at) {
225
- this.leased_at = at;
226
- this.leased_by = by;
227
- this.leased_until = new Date(Date.now() + millis);
228
- millis > 0 && (this.retry = this.retry + 1);
224
+ if (this.is_avaliable) {
225
+ if (millis > 0) {
226
+ this._leased_by = by;
227
+ this._leased_until = new Date(Date.now() + millis);
228
+ this._retry = this._retry + 1;
229
+ }
229
230
  return {
230
231
  stream: this.stream,
231
232
  source: this.source,
232
233
  at,
233
234
  by,
234
- retry: this.retry
235
+ retry: this._retry
235
236
  };
236
237
  }
237
238
  }
@@ -241,27 +242,37 @@ var InMemoryStream = class {
241
242
  * @param by - Lease holder that processed the watermark.
242
243
  */
243
244
  ack(at, by) {
244
- if (this.leased_by === by && at >= this.at) {
245
- this.leased_at = void 0;
246
- this.leased_by = void 0;
247
- this.leased_until = void 0;
248
- this.at = at;
249
- this.retry = -1;
250
- return true;
245
+ if (this._leased_by === by) {
246
+ this._leased_by = void 0;
247
+ this._leased_until = void 0;
248
+ this._at = at;
249
+ this._retry = -1;
250
+ return {
251
+ stream: this.stream,
252
+ source: this.source,
253
+ at: this._at,
254
+ by,
255
+ retry: this._retry
256
+ };
251
257
  }
252
- return false;
253
258
  }
254
259
  /**
255
260
  * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
256
261
  * @param error Blocked error message.
257
262
  */
258
263
  block(by, error) {
259
- if (this.leased_by === by) {
260
- this.blocked = true;
261
- this.error = error;
262
- return true;
264
+ if (this._leased_by === by) {
265
+ this._blocked = true;
266
+ this._error = error;
267
+ return {
268
+ stream: this.stream,
269
+ source: this.source,
270
+ at: this._at,
271
+ by: this._leased_by,
272
+ retry: this._retry,
273
+ error: this._error
274
+ };
263
275
  }
264
- return false;
265
276
  }
266
277
  };
267
278
  var InMemoryStore = class {
@@ -293,6 +304,15 @@ var InMemoryStore = class {
293
304
  this._events.length = 0;
294
305
  this._streams = /* @__PURE__ */ new Map();
295
306
  }
307
+ in_query(query, e) {
308
+ if (query.stream && !RegExp(`^${query.stream}$`).test(e.stream))
309
+ return false;
310
+ if (query.names && !query.names.includes(e.name)) return false;
311
+ if (query.correlation && e.meta?.correlation !== query.correlation)
312
+ return false;
313
+ if (e.name === SNAP_EVENT && !query.with_snaps) return false;
314
+ return true;
315
+ }
296
316
  /**
297
317
  * Query events in the store, optionally filtered by query options.
298
318
  * @param callback - Function to call for each event.
@@ -301,30 +321,32 @@ var InMemoryStore = class {
301
321
  */
302
322
  async query(callback, query) {
303
323
  await sleep();
304
- const {
305
- stream,
306
- names,
307
- before,
308
- after = -1,
309
- limit,
310
- created_before,
311
- created_after,
312
- correlation,
313
- with_snaps = false
314
- } = query || {};
315
- let i = after + 1, count = 0;
316
- while (i < this._events.length) {
317
- const e = this._events[i++];
318
- if (stream && !RegExp(`^${stream}$`).test(e.stream)) continue;
319
- if (names && !names.includes(e.name)) continue;
320
- if (correlation && e.meta?.correlation !== correlation) continue;
321
- if (created_after && e.created <= created_after) continue;
322
- if (e.name === SNAP_EVENT && !with_snaps) continue;
323
- if (before && e.id >= before) break;
324
- if (created_before && e.created >= created_before) break;
325
- callback(e);
326
- count++;
327
- if (limit && count >= limit) break;
324
+ let count = 0;
325
+ if (query?.backward) {
326
+ let i = (query?.before || this._events.length) - 1;
327
+ while (i >= 0) {
328
+ const e = this._events[i--];
329
+ if (query && !this.in_query(query, e)) continue;
330
+ if (query?.created_before && e.created >= query.created_before)
331
+ continue;
332
+ if (query.after && e.id <= query.after) break;
333
+ if (query.created_after && e.created <= query.created_after) break;
334
+ callback(e);
335
+ count++;
336
+ if (query?.limit && count >= query.limit) break;
337
+ }
338
+ } else {
339
+ let i = (query?.after ?? -1) + 1;
340
+ while (i < this._events.length) {
341
+ const e = this._events[i++];
342
+ if (query && !this.in_query(query, e)) continue;
343
+ if (query?.created_after && e.created <= query.created_after) continue;
344
+ if (query?.before && e.id >= query.before) break;
345
+ if (query?.created_before && e.created >= query.created_before) break;
346
+ callback(e);
347
+ count++;
348
+ if (query?.limit && count >= query.limit) break;
349
+ }
328
350
  }
329
351
  return count;
330
352
  }
@@ -366,12 +388,15 @@ var InMemoryStore = class {
366
388
  }
367
389
  /**
368
390
  * Polls the store for unblocked streams needing processing, ordered by lease watermark ascending.
369
- * @param limit - Maximum number of streams to poll.
391
+ * @param lagging - Max number of streams to poll in ascending order.
392
+ * @param leading - Max number of streams to poll in descending order.
370
393
  * @returns The polled streams.
371
394
  */
372
- async poll(limit) {
395
+ async poll(lagging, leading) {
373
396
  await sleep();
374
- 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 }));
397
+ 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 }));
398
+ 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 }));
399
+ return [...a, ...b];
375
400
  }
376
401
  /**
377
402
  * Lease streams for processing (e.g., for distributed consumers).
@@ -381,10 +406,11 @@ var InMemoryStore = class {
381
406
  */
382
407
  async lease(leases, millis) {
383
408
  await sleep();
384
- return leases.map(({ stream, at, by, source }) => {
385
- const found = this._streams.get(stream) || // store new correlations
386
- this._streams.set(stream, new InMemoryStream(stream, source)).get(stream);
387
- return found.lease(at, by, millis);
409
+ return leases.map((l) => {
410
+ if (!this._streams.has(l.stream)) {
411
+ this._streams.set(l.stream, new InMemoryStream(l.stream, l.source));
412
+ }
413
+ return this._streams.get(l.stream)?.lease(l.at, l.by, millis);
388
414
  }).filter((l) => !!l);
389
415
  }
390
416
  /**
@@ -393,9 +419,7 @@ var InMemoryStore = class {
393
419
  */
394
420
  async ack(leases) {
395
421
  await sleep();
396
- return leases.filter(
397
- (lease) => this._streams.get(lease.stream)?.ack(lease.at, lease.by)
398
- );
422
+ return leases.map((l) => this._streams.get(l.stream)?.ack(l.at, l.by)).filter((l) => !!l);
399
423
  }
400
424
  /**
401
425
  * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
@@ -404,9 +428,7 @@ var InMemoryStore = class {
404
428
  */
405
429
  async block(leases) {
406
430
  await sleep();
407
- return leases.filter(
408
- (lease) => this._streams.get(lease.stream)?.block(lease.by, lease.error)
409
- );
431
+ return leases.map((l) => this._streams.get(l.stream)?.block(l.by, l.error)).filter((l) => !!l);
410
432
  }
411
433
  };
412
434
 
@@ -455,6 +477,62 @@ var SNAP_EVENT = "__snapshot__";
455
477
  var store = port(function store2(adapter) {
456
478
  return adapter || new InMemoryStore();
457
479
  });
480
+ function build_tracer(logLevel2) {
481
+ if (logLevel2 === "trace") {
482
+ return {
483
+ fetched: (fetched) => {
484
+ const data = Object.fromEntries(
485
+ fetched.map(({ stream, source, events }) => {
486
+ const key = source ? `${stream}<-${source}` : stream;
487
+ const value = Object.fromEntries(
488
+ events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
489
+ );
490
+ return [key, value];
491
+ })
492
+ );
493
+ logger.trace(data, "\u26A1\uFE0F fetch");
494
+ },
495
+ correlated: (leases) => {
496
+ const data = leases.map(({ stream }) => stream).join(" ");
497
+ logger.trace(`\u26A1\uFE0F correlate ${data}`);
498
+ },
499
+ leased: (leases) => {
500
+ const data = Object.fromEntries(
501
+ leases.map(({ stream, at, retry }) => [stream, { at, retry }])
502
+ );
503
+ logger.trace(data, "\u26A1\uFE0F lease");
504
+ },
505
+ acked: (leases) => {
506
+ const data = Object.fromEntries(
507
+ leases.map(({ stream, at, retry }) => [stream, { at, retry }])
508
+ );
509
+ logger.trace(data, "\u26A1\uFE0F ack");
510
+ },
511
+ blocked: (leases) => {
512
+ const data = Object.fromEntries(
513
+ leases.map(({ stream, at, retry, error }) => [
514
+ stream,
515
+ { at, retry, error }
516
+ ])
517
+ );
518
+ logger.trace(data, "\u26A1\uFE0F block");
519
+ }
520
+ };
521
+ } else {
522
+ return {
523
+ fetched: () => {
524
+ },
525
+ correlated: () => {
526
+ },
527
+ leased: () => {
528
+ },
529
+ acked: () => {
530
+ },
531
+ blocked: () => {
532
+ }
533
+ };
534
+ }
535
+ }
458
536
 
459
537
  // src/signals.ts
460
538
  process.once("SIGINT", async (arg) => {
@@ -593,40 +671,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
593
671
  }
594
672
 
595
673
  // src/act.ts
596
- function traceFetch(fetch) {
597
- const data = Object.fromEntries(
598
- fetch.map(({ stream, source, events }) => {
599
- const key = source ? `${stream}<-${source}` : stream;
600
- const value = Object.fromEntries(
601
- events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
602
- );
603
- return [key, value];
604
- })
605
- );
606
- logger.trace(data, "\u26A1\uFE0F fetch");
607
- }
608
- function traceCorrelated(leases) {
609
- const data = leases.map(({ stream }) => stream).join(" ");
610
- logger.trace(`\u26A1\uFE0F correlate ${data}`);
611
- }
612
- function traceLeased(leases) {
613
- const data = Object.fromEntries(
614
- leases.map(({ stream, at, retry }) => [stream, { at, retry }])
615
- );
616
- logger.trace(data, "\u26A1\uFE0F lease");
617
- }
618
- function traceAcked(leases) {
619
- const data = Object.fromEntries(
620
- leases.map(({ stream, at, retry }) => [stream, { at, retry }])
621
- );
622
- logger.trace(data, "\u26A1\uFE0F ack");
623
- }
624
- function traceBlocked(leases) {
625
- const data = Object.fromEntries(
626
- leases.map(({ stream, at, retry, error }) => [stream, { at, retry, error }])
627
- );
628
- logger.trace(data, "\u26A1\uFE0F block");
629
- }
674
+ var tracer = build_tracer(config().logLevel);
630
675
  var Act = class {
631
676
  /**
632
677
  * Create a new Act orchestrator.
@@ -770,24 +815,6 @@ var Act = class {
770
815
  }
771
816
  return { lease, at };
772
817
  }
773
- /**
774
- * Fetches new events from store according to the fetch options.
775
- * @param options - Fetch options.
776
- * @returns Fetched streams with next events to process.
777
- */
778
- async fetch({ streamLimit = 10, eventLimit = 10 }) {
779
- const polled = await store().poll(streamLimit);
780
- return Promise.all(
781
- polled.map(async ({ stream, source, at }) => {
782
- const events = await this.query_array({
783
- stream: source,
784
- after: at,
785
- limit: eventLimit
786
- });
787
- return { stream, source, events };
788
- })
789
- );
790
- }
791
818
  /**
792
819
  * Drains and processes events from the store, triggering reactions and updating state.
793
820
  *
@@ -806,26 +833,31 @@ var Act = class {
806
833
  if (!this._drain_locked) {
807
834
  try {
808
835
  this._drain_locked = true;
809
- const fetch = await this.fetch({ streamLimit, eventLimit });
810
- fetch.length && traceFetch(fetch);
811
- const [last_at, count] = fetch.reduce(
812
- ([last_at2, count2], { events }) => [
813
- Math.max(last_at2, events.at(-1)?.id || 0),
814
- count2 + events.length
815
- ],
816
- [0, 0]
836
+ const lagging = Math.ceil(streamLimit * 2 / 3);
837
+ const leading = streamLimit - lagging;
838
+ const polled = await store().poll(lagging, leading);
839
+ const fetched = await Promise.all(
840
+ polled.map(async ({ stream, source, at }) => {
841
+ const events = await this.query_array({
842
+ stream: source,
843
+ after: at,
844
+ limit: eventLimit
845
+ });
846
+ return { stream, source, at, events };
847
+ })
817
848
  );
818
- if (count > 0) {
849
+ if (fetched.length) {
850
+ tracer.fetched(fetched);
819
851
  const leases = /* @__PURE__ */ new Map();
820
- fetch.forEach(({ stream, events }) => {
852
+ const last_window_at = fetched.reduce(
853
+ (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
854
+ 0
855
+ );
856
+ fetched.forEach(({ stream, events }) => {
821
857
  const payloads = events.flatMap((event) => {
822
- const register = this.registry.events[event.name];
823
- if (!register) return [];
858
+ const register = this.registry.events[event.name] || [];
824
859
  return [...register.reactions.values()].filter((reaction) => {
825
- const resolved = typeof reaction.resolver === "function" ? (
826
- // @ts-expect-error index by key
827
- reaction.resolver(event)
828
- ) : reaction.resolver;
860
+ const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
829
861
  return resolved && resolved.target === stream;
830
862
  }).map((reaction) => ({ ...reaction, event }));
831
863
  });
@@ -833,43 +865,39 @@ var Act = class {
833
865
  lease: {
834
866
  stream,
835
867
  by: randomUUID2(),
836
- at: events.at(-1)?.id || last_at,
837
- // move the lease watermark forward when no events found in window
868
+ at: events.at(-1)?.id || last_window_at,
869
+ // ff when no matching events
838
870
  retry: 0
839
871
  },
840
872
  // @ts-expect-error indexed by key
841
873
  payloads
842
874
  });
843
875
  });
844
- if (leases.size) {
845
- const leased = await store().lease(
846
- [...leases.values()].map((l) => l.lease),
847
- leaseMillis
848
- );
849
- if (leased.length) {
850
- traceLeased(leased);
851
- const handled = await Promise.all(
852
- leased.map(
853
- (lease) => this.handle(lease, leases.get(lease.stream).payloads)
854
- )
855
- );
856
- const acked = await store().ack(
857
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
858
- );
859
- if (acked.length) {
860
- traceAcked(acked);
861
- this.emit("acked", acked);
862
- }
863
- const blocked = await store().block(
864
- handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
865
- );
866
- if (blocked.length) {
867
- traceBlocked(blocked);
868
- this.emit("blocked", blocked);
869
- }
870
- return { leased, acked, blocked };
871
- }
876
+ const leased = await store().lease(
877
+ [...leases.values()].map((l) => l.lease),
878
+ leaseMillis
879
+ );
880
+ tracer.leased(leased);
881
+ const handled = await Promise.all(
882
+ leased.map(
883
+ (lease) => this.handle(lease, leases.get(lease.stream).payloads)
884
+ )
885
+ );
886
+ const acked = await store().ack(
887
+ handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
888
+ );
889
+ if (acked.length) {
890
+ tracer.acked(acked);
891
+ this.emit("acked", acked);
892
+ }
893
+ const blocked = await store().block(
894
+ handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
895
+ );
896
+ if (blocked.length) {
897
+ tracer.blocked(blocked);
898
+ this.emit("blocked", blocked);
872
899
  }
900
+ return { fetched, leased, acked, blocked };
873
901
  }
874
902
  } catch (error) {
875
903
  logger.error(error);
@@ -877,7 +905,7 @@ var Act = class {
877
905
  this._drain_locked = false;
878
906
  }
879
907
  }
880
- return { leased: [], acked: [], blocked: [] };
908
+ return { fetched: [], leased: [], acked: [], blocked: [] };
881
909
  }
882
910
  /**
883
911
  * Correlates streams using reaction resolvers.
@@ -908,7 +936,7 @@ var Act = class {
908
936
  payloads
909
937
  }));
910
938
  const leased = await store().lease(leases, 0);
911
- leased.length && traceCorrelated(leased);
939
+ leased.length && tracer.correlated(leased);
912
940
  return { leased, last_id };
913
941
  }
914
942
  return { leased: [], last_id };
@@ -1108,6 +1136,7 @@ export {
1108
1136
  ValidationError,
1109
1137
  ZodEmpty,
1110
1138
  act,
1139
+ build_tracer,
1111
1140
  config,
1112
1141
  dispose,
1113
1142
  disposeAndExit,