@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.js CHANGED
@@ -197,71 +197,84 @@ 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.
218
- * @param at - The end-of-lease watermark.
219
- * @param by - The lease holder.
218
+ * @param lease - The lease request.
220
219
  * @param millis - Lease duration in milliseconds.
221
220
  * @returns The granted lease or undefined if blocked.
222
221
  */
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);
222
+ lease(lease, millis) {
223
+ if (this.is_avaliable) {
224
+ if (millis > 0) {
225
+ this._leased_by = lease.by;
226
+ this._leased_until = new Date(Date.now() + millis);
227
+ this._retry = this._retry + 1;
228
+ }
229
229
  return {
230
230
  stream: this.stream,
231
231
  source: this.source,
232
- at,
233
- by,
234
- retry: this.retry
232
+ at: lease.at,
233
+ by: lease.by,
234
+ retry: this._retry,
235
+ lagging: lease.lagging
235
236
  };
236
237
  }
237
238
  }
238
239
  /**
239
240
  * Acknowledge completion of processing for this stream.
240
- * @param at - Last processed watermark.
241
- * @param by - Lease holder that processed the watermark.
241
+ * @param lease - The lease request.
242
242
  */
243
- 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;
243
+ ack(lease) {
244
+ if (this._leased_by === lease.by) {
245
+ this._leased_by = void 0;
246
+ this._leased_until = void 0;
247
+ this._at = lease.at;
248
+ this._retry = -1;
249
+ return {
250
+ stream: this.stream,
251
+ source: this.source,
252
+ at: this._at,
253
+ by: lease.by,
254
+ retry: this._retry,
255
+ lagging: lease.lagging
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.
261
+ * @param lease - The lease request.
256
262
  * @param error Blocked error message.
257
263
  */
258
- block(by, error) {
259
- if (this.leased_by === by) {
260
- this.blocked = true;
261
- this.error = error;
262
- return true;
264
+ block(lease, error) {
265
+ if (this._leased_by === lease.by) {
266
+ this._blocked = true;
267
+ this._error = error;
268
+ return {
269
+ stream: this.stream,
270
+ source: this.source,
271
+ at: this._at,
272
+ by: this._leased_by,
273
+ retry: this._retry,
274
+ error: this._error,
275
+ lagging: lease.lagging
276
+ };
263
277
  }
264
- return false;
265
278
  }
266
279
  };
267
280
  var InMemoryStore = class {
@@ -293,6 +306,15 @@ var InMemoryStore = class {
293
306
  this._events.length = 0;
294
307
  this._streams = /* @__PURE__ */ new Map();
295
308
  }
309
+ in_query(query, e) {
310
+ if (query.stream && !RegExp(`^${query.stream}$`).test(e.stream))
311
+ return false;
312
+ if (query.names && !query.names.includes(e.name)) return false;
313
+ if (query.correlation && e.meta?.correlation !== query.correlation)
314
+ return false;
315
+ if (e.name === SNAP_EVENT && !query.with_snaps) return false;
316
+ return true;
317
+ }
296
318
  /**
297
319
  * Query events in the store, optionally filtered by query options.
298
320
  * @param callback - Function to call for each event.
@@ -301,30 +323,32 @@ var InMemoryStore = class {
301
323
  */
302
324
  async query(callback, query) {
303
325
  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;
326
+ let count = 0;
327
+ if (query?.backward) {
328
+ let i = (query?.before || this._events.length) - 1;
329
+ while (i >= 0) {
330
+ const e = this._events[i--];
331
+ if (query && !this.in_query(query, e)) continue;
332
+ if (query?.created_before && e.created >= query.created_before)
333
+ continue;
334
+ if (query.after && e.id <= query.after) break;
335
+ if (query.created_after && e.created <= query.created_after) break;
336
+ callback(e);
337
+ count++;
338
+ if (query?.limit && count >= query.limit) break;
339
+ }
340
+ } else {
341
+ let i = (query?.after ?? -1) + 1;
342
+ while (i < this._events.length) {
343
+ const e = this._events[i++];
344
+ if (query && !this.in_query(query, e)) continue;
345
+ if (query?.created_after && e.created <= query.created_after) continue;
346
+ if (query?.before && e.id >= query.before) break;
347
+ if (query?.created_before && e.created >= query.created_before) break;
348
+ callback(e);
349
+ count++;
350
+ if (query?.limit && count >= query.limit) break;
351
+ }
328
352
  }
329
353
  return count;
330
354
  }
@@ -366,13 +390,25 @@ var InMemoryStore = class {
366
390
  }
367
391
  /**
368
392
  * Polls the store for unblocked streams needing processing, ordered by lease watermark ascending.
369
- * @param limit - Maximum number of streams to poll.
370
- * @param descending - Whether to poll streams in descending order (aka poll the most advanced first).
393
+ * @param lagging - Max number of streams to poll in ascending order.
394
+ * @param leading - Max number of streams to poll in descending order.
371
395
  * @returns The polled streams.
372
396
  */
373
- async poll(limit, descending = false) {
397
+ async poll(lagging, leading) {
374
398
  await sleep();
375
- 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 }));
399
+ const a = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => a2.at - b2.at).slice(0, lagging).map(({ stream, source, at }) => ({
400
+ stream,
401
+ source,
402
+ at,
403
+ lagging: true
404
+ }));
405
+ const b = [...this._streams.values()].filter((s) => s.is_avaliable).sort((a2, b2) => b2.at - a2.at).slice(0, leading).map(({ stream, source, at }) => ({
406
+ stream,
407
+ source,
408
+ at,
409
+ lagging: false
410
+ }));
411
+ return [...a, ...b];
376
412
  }
377
413
  /**
378
414
  * Lease streams for processing (e.g., for distributed consumers).
@@ -382,10 +418,11 @@ var InMemoryStore = class {
382
418
  */
383
419
  async lease(leases, millis) {
384
420
  await sleep();
385
- return leases.map(({ stream, at, by, source }) => {
386
- const found = this._streams.get(stream) || // store new correlations
387
- this._streams.set(stream, new InMemoryStream(stream, source)).get(stream);
388
- return found.lease(at, by, millis);
421
+ return leases.map((l) => {
422
+ if (!this._streams.has(l.stream)) {
423
+ this._streams.set(l.stream, new InMemoryStream(l.stream, l.source));
424
+ }
425
+ return this._streams.get(l.stream)?.lease(l, millis);
389
426
  }).filter((l) => !!l);
390
427
  }
391
428
  /**
@@ -394,9 +431,7 @@ var InMemoryStore = class {
394
431
  */
395
432
  async ack(leases) {
396
433
  await sleep();
397
- return leases.filter(
398
- (lease) => this._streams.get(lease.stream)?.ack(lease.at, lease.by)
399
- );
434
+ return leases.map((l) => this._streams.get(l.stream)?.ack(l)).filter((l) => !!l);
400
435
  }
401
436
  /**
402
437
  * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
@@ -405,9 +440,7 @@ var InMemoryStore = class {
405
440
  */
406
441
  async block(leases) {
407
442
  await sleep();
408
- return leases.filter(
409
- (lease) => this._streams.get(lease.stream)?.block(lease.by, lease.error)
410
- );
443
+ return leases.map((l) => this._streams.get(l.stream)?.block(l, l.error)).filter((l) => !!l);
411
444
  }
412
445
  };
413
446
 
@@ -667,6 +700,7 @@ var Act = class {
667
700
  }
668
701
  _emitter = new EventEmitter();
669
702
  _drain_locked = false;
703
+ _drain_lag2lead_ratio = 0.5;
670
704
  _correlation_interval = void 0;
671
705
  emit(event, args) {
672
706
  return this._emitter.emit(event, args);
@@ -769,7 +803,7 @@ var Act = class {
769
803
  * @returns The lease with results
770
804
  */
771
805
  async handle(lease, payloads) {
772
- if (payloads.length === 0) return { lease, at: lease.at };
806
+ if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
773
807
  const stream = lease.stream;
774
808
  let at = payloads.at(0).event.id, handled = 0;
775
809
  lease.retry > 0 && logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
@@ -785,6 +819,7 @@ var Act = class {
785
819
  block && logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
786
820
  return {
787
821
  lease,
822
+ handled,
788
823
  at,
789
824
  // only report error when nothing was handled
790
825
  error: handled === 0 ? error.message : void 0,
@@ -792,7 +827,7 @@ var Act = class {
792
827
  };
793
828
  }
794
829
  }
795
- return { lease, at };
830
+ return { lease, handled, at };
796
831
  }
797
832
  /**
798
833
  * Drains and processes events from the store, triggering reactions and updating state.
@@ -807,42 +842,36 @@ var Act = class {
807
842
  async drain({
808
843
  streamLimit = 10,
809
844
  eventLimit = 10,
810
- leaseMillis = 1e4,
811
- descending = false
845
+ leaseMillis = 1e4
812
846
  } = {}) {
813
847
  if (!this._drain_locked) {
814
848
  try {
815
849
  this._drain_locked = true;
816
- const polled = await store().poll(streamLimit, descending);
850
+ const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
851
+ const leading = streamLimit - lagging;
852
+ const polled = await store().poll(lagging, leading);
817
853
  const fetched = await Promise.all(
818
- polled.map(async ({ stream, source, at }) => {
854
+ polled.map(async ({ stream, source, at, lagging: lagging2 }) => {
819
855
  const events = await this.query_array({
820
856
  stream: source,
821
857
  after: at,
822
858
  limit: eventLimit
823
859
  });
824
- return { stream, source, events };
860
+ return { stream, source, at, lagging: lagging2, events };
825
861
  })
826
862
  );
827
- fetched.length && tracer.fetched(fetched);
828
- const [last_at, count] = fetched.reduce(
829
- ([last_at2, count2], { events }) => [
830
- Math.max(last_at2, events.at(-1)?.id || 0),
831
- count2 + events.length
832
- ],
833
- [0, 0]
834
- );
835
- if (count > 0) {
863
+ if (fetched.length) {
864
+ tracer.fetched(fetched);
836
865
  const leases = /* @__PURE__ */ new Map();
837
- fetched.forEach(({ stream, events }) => {
866
+ const fetch_window_at = fetched.reduce(
867
+ (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
868
+ 0
869
+ );
870
+ fetched.forEach(({ stream, lagging: lagging2, events }) => {
838
871
  const payloads = events.flatMap((event) => {
839
- const register = this.registry.events[event.name];
840
- if (!register) return [];
872
+ const register = this.registry.events[event.name] || [];
841
873
  return [...register.reactions.values()].filter((reaction) => {
842
- const resolved = typeof reaction.resolver === "function" ? (
843
- // @ts-expect-error index by key
844
- reaction.resolver(event)
845
- ) : reaction.resolver;
874
+ const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
846
875
  return resolved && resolved.target === stream;
847
876
  }).map((reaction) => ({ ...reaction, event }));
848
877
  });
@@ -850,43 +879,51 @@ var Act = class {
850
879
  lease: {
851
880
  stream,
852
881
  by: randomUUID2(),
853
- at: events.at(-1)?.id || last_at,
854
- // move the lease watermark forward when no events found in window
855
- retry: 0
882
+ at: events.at(-1)?.id || fetch_window_at,
883
+ // ff when no matching events
884
+ retry: 0,
885
+ lagging: lagging2
856
886
  },
857
887
  // @ts-expect-error indexed by key
858
888
  payloads
859
889
  });
860
890
  });
861
- if (leases.size) {
862
- const leased = await store().lease(
863
- [...leases.values()].map((l) => l.lease),
864
- leaseMillis
865
- );
866
- if (leased.length) {
867
- tracer.leased(leased);
868
- const handled = await Promise.all(
869
- leased.map(
870
- (lease) => this.handle(lease, leases.get(lease.stream).payloads)
871
- )
872
- );
873
- const acked = await store().ack(
874
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
875
- );
876
- if (acked.length) {
877
- tracer.acked(acked);
878
- this.emit("acked", acked);
879
- }
880
- const blocked = await store().block(
881
- handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
882
- );
883
- if (blocked.length) {
884
- tracer.blocked(blocked);
885
- this.emit("blocked", blocked);
886
- }
887
- return { leased, acked, blocked };
888
- }
891
+ const leased = await store().lease(
892
+ [...leases.values()].map(({ lease }) => lease),
893
+ leaseMillis
894
+ );
895
+ tracer.leased(leased);
896
+ const handled = await Promise.all(
897
+ leased.map(
898
+ (lease) => this.handle(lease, leases.get(lease.stream).payloads)
899
+ )
900
+ );
901
+ const [lagging_handled, leading_handled] = handled.reduce(
902
+ ([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
903
+ lagging_handled2 + (lease.lagging ? handled2 : 0),
904
+ leading_handled2 + (lease.lagging ? 0 : handled2)
905
+ ],
906
+ [0, 0]
907
+ );
908
+ const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
909
+ const leading_avg = leading > 0 ? leading_handled / leading : 0;
910
+ const total = lagging_avg + leading_avg;
911
+ this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
912
+ const acked = await store().ack(
913
+ handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
914
+ );
915
+ if (acked.length) {
916
+ tracer.acked(acked);
917
+ this.emit("acked", acked);
918
+ }
919
+ const blocked = await store().block(
920
+ handled.filter(({ block }) => block).map(({ lease, error }) => ({ ...lease, error }))
921
+ );
922
+ if (blocked.length) {
923
+ tracer.blocked(blocked);
924
+ this.emit("blocked", blocked);
889
925
  }
926
+ return { fetched, leased, acked, blocked };
890
927
  }
891
928
  } catch (error) {
892
929
  logger.error(error);
@@ -894,7 +931,7 @@ var Act = class {
894
931
  this._drain_locked = false;
895
932
  }
896
933
  }
897
- return { leased: [], acked: [], blocked: [] };
934
+ return { fetched: [], leased: [], acked: [], blocked: [] };
898
935
  }
899
936
  /**
900
937
  * Correlates streams using reaction resolvers.
@@ -922,6 +959,7 @@ var Act = class {
922
959
  by: randomUUID2(),
923
960
  at: 0,
924
961
  retry: 0,
962
+ lagging: true,
925
963
  payloads
926
964
  }));
927
965
  const leased = await store().lease(leases, 0);