@rotorsoft/act 0.5.7 → 0.6.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/.tsbuildinfo +1 -1
- package/dist/@types/act-builder.d.ts +3 -3
- package/dist/@types/act-builder.d.ts.map +1 -1
- package/dist/@types/act.d.ts +82 -20
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/InMemoryStore.d.ts +23 -11
- package/dist/@types/adapters/InMemoryStore.d.ts.map +1 -1
- package/dist/@types/event-sourcing.d.ts +1 -1
- package/dist/@types/event-sourcing.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +12 -1
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/errors.d.ts +10 -17
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +21 -11
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +57 -23
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/@types/types/registry.d.ts +8 -4
- package/dist/@types/types/registry.d.ts.map +1 -1
- package/dist/@types/types/schemas.d.ts +3 -14
- package/dist/@types/types/schemas.d.ts.map +1 -1
- package/dist/index.cjs +375 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +375 -172
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -81,18 +81,24 @@ var ValidationError = class extends Error {
|
|
|
81
81
|
}
|
|
82
82
|
};
|
|
83
83
|
var InvariantError = class extends Error {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
constructor(action2, payload, target, snapshot, description) {
|
|
85
|
+
super(`${action2} failed invariant: ${description}`);
|
|
86
|
+
this.action = action2;
|
|
87
|
+
this.payload = payload;
|
|
88
|
+
this.target = target;
|
|
89
|
+
this.snapshot = snapshot;
|
|
90
|
+
this.description = description;
|
|
87
91
|
this.name = Errors.InvariantError;
|
|
88
|
-
this.details = { name, payload, target, description };
|
|
89
92
|
}
|
|
90
93
|
};
|
|
91
94
|
var ConcurrencyError = class extends Error {
|
|
92
|
-
constructor(lastVersion, events, expectedVersion) {
|
|
95
|
+
constructor(stream, lastVersion, events, expectedVersion) {
|
|
93
96
|
super(
|
|
94
|
-
`Concurrency error committing
|
|
97
|
+
`Concurrency error committing "${events.map((e) => `${stream}.${e.name}.${JSON.stringify(e.data)}`).join(
|
|
98
|
+
", "
|
|
99
|
+
)}". Expected version ${expectedVersion} but found version ${lastVersion}.`
|
|
95
100
|
);
|
|
101
|
+
this.stream = stream;
|
|
96
102
|
this.lastVersion = lastVersion;
|
|
97
103
|
this.events = events;
|
|
98
104
|
this.expectedVersion = expectedVersion;
|
|
@@ -147,8 +153,9 @@ var QuerySchema = import_zod.z.object({
|
|
|
147
153
|
created_before: import_zod.z.date().optional(),
|
|
148
154
|
created_after: import_zod.z.date().optional(),
|
|
149
155
|
backward: import_zod.z.boolean().optional(),
|
|
150
|
-
correlation: import_zod.z.string().optional()
|
|
151
|
-
|
|
156
|
+
correlation: import_zod.z.string().optional(),
|
|
157
|
+
with_snaps: import_zod.z.boolean().optional()
|
|
158
|
+
}).readonly();
|
|
152
159
|
|
|
153
160
|
// src/types/index.ts
|
|
154
161
|
var Environments = [
|
|
@@ -254,38 +261,71 @@ async function sleep(ms) {
|
|
|
254
261
|
|
|
255
262
|
// src/adapters/InMemoryStore.ts
|
|
256
263
|
var InMemoryStream = class {
|
|
257
|
-
|
|
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
|
+
constructor(stream, source) {
|
|
258
274
|
this.stream = stream;
|
|
275
|
+
this.source = source;
|
|
276
|
+
}
|
|
277
|
+
get is_avaliable() {
|
|
278
|
+
return !this.blocked && (!this.leased_until || this.leased_until <= /* @__PURE__ */ new Date());
|
|
259
279
|
}
|
|
260
|
-
_at = -1;
|
|
261
|
-
_retry = -1;
|
|
262
|
-
_lease;
|
|
263
|
-
_blocked = false;
|
|
264
280
|
/**
|
|
265
281
|
* Attempt to lease this stream for processing.
|
|
266
|
-
* @param
|
|
282
|
+
* @param at - The end-of-lease watermark.
|
|
283
|
+
* @param by - The lease holder.
|
|
284
|
+
* @param millis - Lease duration in milliseconds.
|
|
267
285
|
* @returns The granted lease or undefined if blocked.
|
|
268
286
|
*/
|
|
269
|
-
lease(
|
|
270
|
-
if (
|
|
271
|
-
this.
|
|
272
|
-
|
|
287
|
+
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);
|
|
293
|
+
return {
|
|
294
|
+
stream: this.stream,
|
|
295
|
+
source: this.source,
|
|
296
|
+
at,
|
|
297
|
+
by,
|
|
298
|
+
retry: this.retry
|
|
299
|
+
};
|
|
273
300
|
}
|
|
274
301
|
}
|
|
275
302
|
/**
|
|
276
303
|
* Acknowledge completion of processing for this stream.
|
|
277
|
-
* @param
|
|
304
|
+
* @param at - Last processed watermark.
|
|
305
|
+
* @param by - Lease holder that processed the watermark.
|
|
278
306
|
*/
|
|
279
|
-
ack(
|
|
280
|
-
if (this.
|
|
281
|
-
this.
|
|
282
|
-
this.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
307
|
+
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;
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
320
|
+
* @param error Blocked error message.
|
|
321
|
+
*/
|
|
322
|
+
block(by, error) {
|
|
323
|
+
if (this.leased_by === by) {
|
|
324
|
+
this.blocked = true;
|
|
325
|
+
this.error = error;
|
|
326
|
+
return true;
|
|
288
327
|
}
|
|
328
|
+
return false;
|
|
289
329
|
}
|
|
290
330
|
};
|
|
291
331
|
var InMemoryStore = class {
|
|
@@ -315,6 +355,7 @@ var InMemoryStore = class {
|
|
|
315
355
|
async drop() {
|
|
316
356
|
await sleep();
|
|
317
357
|
this._events.length = 0;
|
|
358
|
+
this._streams = /* @__PURE__ */ new Map();
|
|
318
359
|
}
|
|
319
360
|
/**
|
|
320
361
|
* Query events in the store, optionally filtered by query options.
|
|
@@ -332,15 +373,17 @@ var InMemoryStore = class {
|
|
|
332
373
|
limit,
|
|
333
374
|
created_before,
|
|
334
375
|
created_after,
|
|
335
|
-
correlation
|
|
376
|
+
correlation,
|
|
377
|
+
with_snaps = false
|
|
336
378
|
} = query || {};
|
|
337
379
|
let i = after + 1, count = 0;
|
|
338
380
|
while (i < this._events.length) {
|
|
339
381
|
const e = this._events[i++];
|
|
340
|
-
if (stream && e.stream
|
|
382
|
+
if (stream && !RegExp(`^${stream}$`).test(e.stream)) continue;
|
|
341
383
|
if (names && !names.includes(e.name)) continue;
|
|
342
384
|
if (correlation && e.meta?.correlation !== correlation) continue;
|
|
343
385
|
if (created_after && e.created <= created_after) continue;
|
|
386
|
+
if (e.name === SNAP_EVENT && !with_snaps) continue;
|
|
344
387
|
if (before && e.id >= before) break;
|
|
345
388
|
if (created_before && e.created >= created_before) break;
|
|
346
389
|
callback(e);
|
|
@@ -361,12 +404,14 @@ var InMemoryStore = class {
|
|
|
361
404
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
362
405
|
await sleep();
|
|
363
406
|
const instance = this._events.filter((e) => e.stream === stream);
|
|
364
|
-
if (typeof expectedVersion === "number" && instance.length - 1 !== expectedVersion)
|
|
407
|
+
if (typeof expectedVersion === "number" && instance.length - 1 !== expectedVersion) {
|
|
365
408
|
throw new ConcurrencyError(
|
|
409
|
+
stream,
|
|
366
410
|
instance.length - 1,
|
|
367
411
|
msgs,
|
|
368
412
|
expectedVersion
|
|
369
413
|
);
|
|
414
|
+
}
|
|
370
415
|
let version = instance.length;
|
|
371
416
|
return msgs.map(({ name, data }) => {
|
|
372
417
|
const committed = {
|
|
@@ -384,43 +429,48 @@ var InMemoryStore = class {
|
|
|
384
429
|
});
|
|
385
430
|
}
|
|
386
431
|
/**
|
|
387
|
-
*
|
|
388
|
-
* @param limit - Maximum number of streams to
|
|
389
|
-
* @returns
|
|
432
|
+
* Polls the store for unblocked streams needing processing, ordered by lease watermark ascending.
|
|
433
|
+
* @param limit - Maximum number of streams to poll.
|
|
434
|
+
* @returns The polled streams.
|
|
390
435
|
*/
|
|
391
|
-
async
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
(min, s) => Math.min(min, s._at),
|
|
395
|
-
Number.MAX_SAFE_INTEGER
|
|
396
|
-
) : -1;
|
|
397
|
-
const events = [];
|
|
398
|
-
await this.query((e) => e.name !== SNAP_EVENT && events.push(e), {
|
|
399
|
-
after,
|
|
400
|
-
limit
|
|
401
|
-
});
|
|
402
|
-
return { streams: streams.map(({ stream }) => stream), events };
|
|
436
|
+
async poll(limit) {
|
|
437
|
+
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 }));
|
|
403
439
|
}
|
|
404
440
|
/**
|
|
405
441
|
* Lease streams for processing (e.g., for distributed consumers).
|
|
406
|
-
* @param leases - Lease requests.
|
|
442
|
+
* @param leases - Lease requests for streams, including end-of-lease watermark, lease holder, and source stream.
|
|
443
|
+
* @param leaseMilis - Lease duration in milliseconds.
|
|
407
444
|
* @returns Granted leases.
|
|
408
445
|
*/
|
|
409
|
-
async lease(leases) {
|
|
446
|
+
async lease(leases, millis) {
|
|
410
447
|
await sleep();
|
|
411
|
-
return leases.map((
|
|
412
|
-
const
|
|
413
|
-
this._streams.set(
|
|
414
|
-
return
|
|
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);
|
|
415
452
|
}).filter((l) => !!l);
|
|
416
453
|
}
|
|
417
454
|
/**
|
|
418
455
|
* Acknowledge completion of processing for leased streams.
|
|
419
|
-
* @param leases - Leases to acknowledge.
|
|
456
|
+
* @param leases - Leases to acknowledge, including last processed watermark and lease holder.
|
|
420
457
|
*/
|
|
421
458
|
async ack(leases) {
|
|
422
459
|
await sleep();
|
|
423
|
-
leases.
|
|
460
|
+
return leases.filter(
|
|
461
|
+
(lease) => this._streams.get(lease.stream)?.ack(lease.at, lease.by)
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
466
|
+
* @param leases - Leases to block, including lease holder and last error message.
|
|
467
|
+
* @returns Blocked leases.
|
|
468
|
+
*/
|
|
469
|
+
async block(leases) {
|
|
470
|
+
await sleep();
|
|
471
|
+
return leases.filter(
|
|
472
|
+
(lease) => this._streams.get(lease.stream)?.block(lease.by, lease.error)
|
|
473
|
+
);
|
|
424
474
|
}
|
|
425
475
|
};
|
|
426
476
|
|
|
@@ -530,21 +580,21 @@ async function load(me, stream, callback) {
|
|
|
530
580
|
}
|
|
531
581
|
callback && callback({ event, state: state2, patches, snaps });
|
|
532
582
|
},
|
|
533
|
-
{ stream }
|
|
534
|
-
true
|
|
583
|
+
{ stream, with_snaps: true }
|
|
535
584
|
);
|
|
536
|
-
logger.trace(
|
|
585
|
+
logger.trace(state2, `\u{1F7E2} load ${stream}`);
|
|
537
586
|
return { event, state: state2, patches, snaps };
|
|
538
587
|
}
|
|
539
588
|
async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
|
|
540
589
|
const { stream, expectedVersion, actor } = target;
|
|
541
590
|
if (!stream) throw new Error("Missing target stream");
|
|
542
591
|
payload = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
592
|
+
const snapshot = await load(me, stream);
|
|
593
|
+
const expected = expectedVersion || snapshot.event?.version;
|
|
543
594
|
logger.trace(
|
|
544
595
|
payload,
|
|
545
|
-
`\u{1F535} ${
|
|
596
|
+
`\u{1F535} ${stream}.${action2}${typeof expected === "number" ? `.${expected}` : ""}`
|
|
546
597
|
);
|
|
547
|
-
let snapshot = await load(me, stream);
|
|
548
598
|
if (me.given) {
|
|
549
599
|
const invariants = me.given[action2] || [];
|
|
550
600
|
invariants.forEach(({ valid, description }) => {
|
|
@@ -553,15 +603,15 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
553
603
|
action2,
|
|
554
604
|
payload,
|
|
555
605
|
target,
|
|
606
|
+
snapshot,
|
|
556
607
|
description
|
|
557
608
|
);
|
|
558
609
|
});
|
|
559
610
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (!result) return snapshot;
|
|
611
|
+
const result = me.on[action2](payload, snapshot, target);
|
|
612
|
+
if (!result) return [snapshot];
|
|
563
613
|
if (Array.isArray(result) && result.length === 0) {
|
|
564
|
-
return snapshot;
|
|
614
|
+
return [snapshot];
|
|
565
615
|
}
|
|
566
616
|
const tuples = Array.isArray(result[0]) ? result : [result];
|
|
567
617
|
const emitted = tuples.map(([name, data]) => ({
|
|
@@ -584,36 +634,80 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
584
634
|
} : void 0
|
|
585
635
|
}
|
|
586
636
|
};
|
|
637
|
+
logger.trace(
|
|
638
|
+
emitted.map((e) => e.data),
|
|
639
|
+
`\u{1F534} commit ${stream}.${emitted.map((e) => e.name).join(", ")}`
|
|
640
|
+
);
|
|
587
641
|
const committed = await store().commit(
|
|
588
642
|
stream,
|
|
589
643
|
emitted,
|
|
590
644
|
meta,
|
|
591
645
|
// TODO: review reactions not enforcing expected version
|
|
592
|
-
reactingTo ? void 0 :
|
|
646
|
+
reactingTo ? void 0 : expected
|
|
593
647
|
);
|
|
594
|
-
|
|
648
|
+
let { state: state2, patches } = snapshot;
|
|
649
|
+
const snapshots = committed.map((event) => {
|
|
595
650
|
state2 = patch(state2, me.patch[event.name](event, state2));
|
|
596
651
|
patches++;
|
|
597
|
-
logger.trace({ event, state: state2 }, "\u{1F534} commit");
|
|
598
652
|
return { event, state: state2, patches, snaps: snapshot.snaps };
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
|
|
653
|
+
});
|
|
654
|
+
const last = snapshots.at(-1);
|
|
655
|
+
me.snap && me.snap(last) && void snap(last);
|
|
656
|
+
return snapshots;
|
|
602
657
|
}
|
|
603
658
|
|
|
604
659
|
// 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
|
+
}
|
|
605
694
|
var Act = class {
|
|
606
695
|
/**
|
|
607
696
|
* Create a new Act orchestrator.
|
|
608
697
|
*
|
|
609
698
|
* @param registry The registry of state, event, and action schemas
|
|
610
|
-
* @param drainLimit The maximum number of events to drain per cycle
|
|
611
699
|
*/
|
|
612
|
-
constructor(registry
|
|
700
|
+
constructor(registry) {
|
|
613
701
|
this.registry = registry;
|
|
614
|
-
|
|
702
|
+
dispose(() => {
|
|
703
|
+
this._emitter.removeAllListeners();
|
|
704
|
+
this.stop_correlations();
|
|
705
|
+
return Promise.resolve();
|
|
706
|
+
});
|
|
615
707
|
}
|
|
616
708
|
_emitter = new import_events.default();
|
|
709
|
+
_drain_locked = false;
|
|
710
|
+
_correlation_interval = void 0;
|
|
617
711
|
emit(event, args) {
|
|
618
712
|
return this._emitter.emit(event, args);
|
|
619
713
|
}
|
|
@@ -640,16 +734,17 @@ var Act = class {
|
|
|
640
734
|
* await app.do("increment", { stream: "counter1", actor }, { by: 1 });
|
|
641
735
|
*/
|
|
642
736
|
async do(action2, target, payload, reactingTo, skipValidation = false) {
|
|
643
|
-
const
|
|
737
|
+
const snapshots = await action(
|
|
644
738
|
this.registry.actions[action2],
|
|
645
739
|
action2,
|
|
646
740
|
target,
|
|
647
741
|
payload,
|
|
742
|
+
// @ts-expect-error type lost
|
|
648
743
|
reactingTo,
|
|
649
744
|
skipValidation
|
|
650
745
|
);
|
|
651
|
-
this.emit("committed",
|
|
652
|
-
return
|
|
746
|
+
this.emit("committed", snapshots);
|
|
747
|
+
return snapshots;
|
|
653
748
|
}
|
|
654
749
|
/**
|
|
655
750
|
* Loads the current state snapshot for a given state machine and stream.
|
|
@@ -687,38 +782,76 @@ var Act = class {
|
|
|
687
782
|
}, query);
|
|
688
783
|
return { first, last, count };
|
|
689
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* Query the event store for events matching a filter.
|
|
787
|
+
* Use this version with caution, as it return events in memory.
|
|
788
|
+
*
|
|
789
|
+
* @param query The query filter (e.g., by stream, event name, or time range)
|
|
790
|
+
* @returns The matching events
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* const { count } = await app.query({ stream: "counter1" }, (event) => console.log(event));
|
|
794
|
+
*/
|
|
795
|
+
async query_array(query) {
|
|
796
|
+
const events = [];
|
|
797
|
+
await store().query((e) => events.push(e), query);
|
|
798
|
+
return events;
|
|
799
|
+
}
|
|
690
800
|
/**
|
|
691
801
|
* Handles leased reactions.
|
|
692
802
|
*
|
|
803
|
+
* This is called by the main `drain` loop after fetching new events.
|
|
804
|
+
* It handles reactions, supporting retries, blocking, and error handling.
|
|
805
|
+
*
|
|
693
806
|
* @internal
|
|
694
807
|
* @param lease The lease to handle
|
|
695
|
-
* @param
|
|
696
|
-
* @returns The lease
|
|
808
|
+
* @param payloads The reactions to handle
|
|
809
|
+
* @returns The lease with results
|
|
697
810
|
*/
|
|
698
|
-
async handle(lease,
|
|
811
|
+
async handle(lease, payloads) {
|
|
812
|
+
if (payloads.length === 0) return { lease, at: lease.at };
|
|
699
813
|
const stream = lease.stream;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
814
|
+
let at = payloads.at(0).event.id, handled = 0;
|
|
815
|
+
lease.retry > 0 && logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
816
|
+
for (const payload of payloads) {
|
|
817
|
+
const { event, handler, options } = payload;
|
|
703
818
|
try {
|
|
704
819
|
await handler(event, stream);
|
|
705
|
-
|
|
820
|
+
at = event.id;
|
|
821
|
+
handled++;
|
|
706
822
|
} catch (error) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
823
|
+
logger.error(error);
|
|
824
|
+
const block = lease.retry >= options.maxRetries && options.blockOnError;
|
|
825
|
+
block && logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
|
|
826
|
+
return {
|
|
827
|
+
lease,
|
|
828
|
+
at,
|
|
829
|
+
// only report error when nothing was handled
|
|
830
|
+
error: handled === 0 ? error.message : void 0,
|
|
831
|
+
block
|
|
832
|
+
};
|
|
717
833
|
}
|
|
718
834
|
}
|
|
719
|
-
return lease;
|
|
835
|
+
return { lease, at };
|
|
836
|
+
}
|
|
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
|
+
);
|
|
720
854
|
}
|
|
721
|
-
drainLocked = false;
|
|
722
855
|
/**
|
|
723
856
|
* Drains and processes events from the store, triggering reactions and updating state.
|
|
724
857
|
*
|
|
@@ -729,91 +862,162 @@ var Act = class {
|
|
|
729
862
|
* @example
|
|
730
863
|
* await app.drain();
|
|
731
864
|
*/
|
|
732
|
-
async drain(
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
865
|
+
async drain({
|
|
866
|
+
streamLimit = 10,
|
|
867
|
+
eventLimit = 10,
|
|
868
|
+
leaseMillis = 1e4
|
|
869
|
+
} = {}) {
|
|
870
|
+
if (!this._drain_locked) {
|
|
871
|
+
try {
|
|
872
|
+
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]
|
|
881
|
+
);
|
|
882
|
+
if (count > 0) {
|
|
883
|
+
const leases = /* @__PURE__ */ new Map();
|
|
884
|
+
fetch.forEach(({ stream, events }) => {
|
|
885
|
+
const payloads = events.flatMap((event) => {
|
|
886
|
+
const register = this.registry.events[event.name];
|
|
887
|
+
if (!register) return [];
|
|
888
|
+
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;
|
|
893
|
+
return resolved && resolved.target === stream;
|
|
894
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
895
|
+
});
|
|
896
|
+
leases.set(stream, {
|
|
897
|
+
lease: {
|
|
898
|
+
stream,
|
|
899
|
+
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
|
|
902
|
+
retry: 0
|
|
903
|
+
},
|
|
904
|
+
// @ts-expect-error indexed by key
|
|
905
|
+
payloads
|
|
906
|
+
});
|
|
907
|
+
});
|
|
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
|
+
}
|
|
755
936
|
}
|
|
756
937
|
}
|
|
938
|
+
} catch (error) {
|
|
939
|
+
logger.error(error);
|
|
940
|
+
} finally {
|
|
941
|
+
this._drain_locked = false;
|
|
757
942
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
943
|
+
}
|
|
944
|
+
return { leased: [], acked: [], blocked: [] };
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Correlates streams using reaction resolvers.
|
|
948
|
+
* @param query - The query filter (e.g., by stream, event name, or starting point).
|
|
949
|
+
* @returns The leases of newly correlated streams, and the last seen event ID.
|
|
950
|
+
*/
|
|
951
|
+
async correlate(query = { after: -1, limit: 10 }) {
|
|
952
|
+
const correlated = /* @__PURE__ */ new Map();
|
|
953
|
+
let last_id = query.after || -1;
|
|
954
|
+
await store().query((event) => {
|
|
955
|
+
last_id = event.id;
|
|
956
|
+
const register = this.registry.events[event.name];
|
|
957
|
+
if (register) {
|
|
958
|
+
for (const reaction of register.reactions.values()) {
|
|
959
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
960
|
+
resolved && (correlated.get(resolved.target) || correlated.set(resolved.target, []).get(resolved.target)).push({ ...reaction, source: resolved.source, event });
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}, query);
|
|
964
|
+
if (correlated.size) {
|
|
965
|
+
const leases = [...correlated.entries()].map(([stream, payloads]) => ({
|
|
761
966
|
stream,
|
|
762
|
-
|
|
967
|
+
// TODO: by convention, the first defined source wins (this can be tricky)
|
|
968
|
+
source: payloads.find((p) => p.source)?.source || void 0,
|
|
969
|
+
by: (0, import_crypto2.randomUUID)(),
|
|
970
|
+
at: 0,
|
|
763
971
|
retry: 0,
|
|
764
|
-
|
|
972
|
+
payloads
|
|
765
973
|
}));
|
|
766
|
-
const leased = await store().lease(leases);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}),
|
|
805
|
-
{}
|
|
806
|
-
),
|
|
807
|
-
"\u26A1\uFE0F ack"
|
|
808
|
-
);
|
|
974
|
+
const leased = await store().lease(leases, 0);
|
|
975
|
+
leased.length && traceCorrelated(leased);
|
|
976
|
+
return { leased, last_id };
|
|
977
|
+
}
|
|
978
|
+
return { leased: [], last_id };
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Starts correlation worker that identifies and registers new streams using reaction resolvers.
|
|
982
|
+
*
|
|
983
|
+
* Enables "dynamic reactions", allowing streams to be auto-discovered based on event content.
|
|
984
|
+
* - Uses a correlation sliding window over the event stream to identify new streams.
|
|
985
|
+
* - Once registered, these streams are picked up by the main `drain` loop.
|
|
986
|
+
* - Users should have full control over their correlation strategy.
|
|
987
|
+
* - The starting point keeps increasing with each new batch of events.
|
|
988
|
+
* - Users are responsible for storing the last seen event ID.
|
|
989
|
+
*
|
|
990
|
+
* @param query - The query filter (e.g., by stream, event name, or starting point).
|
|
991
|
+
* @param frequency - The frequency of correlation checks (in milliseconds).
|
|
992
|
+
* @param callback - Callback to report stats (new strems, last seen event ID, etc.).
|
|
993
|
+
* @returns true if the correlation worker started, false otherwise (already started).
|
|
994
|
+
*/
|
|
995
|
+
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
996
|
+
if (this._correlation_interval) return false;
|
|
997
|
+
const limit = query.limit || 100;
|
|
998
|
+
let after = query.after || -1;
|
|
999
|
+
this._correlation_interval = setInterval(
|
|
1000
|
+
() => this.correlate({ ...query, after, limit }).then((result) => {
|
|
1001
|
+
after = result.last_id;
|
|
1002
|
+
if (callback && result.leased.length) callback(result.leased);
|
|
1003
|
+
}).catch(console.error),
|
|
1004
|
+
frequency
|
|
1005
|
+
);
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
stop_correlations() {
|
|
1009
|
+
if (this._correlation_interval) {
|
|
1010
|
+
clearInterval(this._correlation_interval);
|
|
1011
|
+
this._correlation_interval = void 0;
|
|
809
1012
|
}
|
|
810
|
-
this.drainLocked = false;
|
|
811
|
-
return drained.length;
|
|
812
1013
|
}
|
|
813
1014
|
};
|
|
814
1015
|
|
|
815
1016
|
// src/act-builder.ts
|
|
816
|
-
var _this_ = ({ stream }) =>
|
|
1017
|
+
var _this_ = ({ stream }) => ({
|
|
1018
|
+
source: stream,
|
|
1019
|
+
target: stream
|
|
1020
|
+
});
|
|
817
1021
|
var _void_ = () => void 0;
|
|
818
1022
|
function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
819
1023
|
actions: {},
|
|
@@ -865,8 +1069,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
865
1069
|
resolver: _this_,
|
|
866
1070
|
options: {
|
|
867
1071
|
blockOnError: options?.blockOnError ?? true,
|
|
868
|
-
maxRetries: options?.maxRetries ?? 3
|
|
869
|
-
retryDelayMs: options?.retryDelayMs ?? 1e3
|
|
1072
|
+
maxRetries: options?.maxRetries ?? 3
|
|
870
1073
|
}
|
|
871
1074
|
};
|
|
872
1075
|
registry.events[event].reactions.set(handler.name, reaction);
|
|
@@ -875,7 +1078,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
875
1078
|
to(resolver) {
|
|
876
1079
|
registry.events[event].reactions.set(handler.name, {
|
|
877
1080
|
...reaction,
|
|
878
|
-
resolver
|
|
1081
|
+
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
879
1082
|
});
|
|
880
1083
|
return builder;
|
|
881
1084
|
},
|
|
@@ -889,7 +1092,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
889
1092
|
};
|
|
890
1093
|
}
|
|
891
1094
|
}),
|
|
892
|
-
build: (
|
|
1095
|
+
build: () => new Act(registry),
|
|
893
1096
|
events: registry.events
|
|
894
1097
|
};
|
|
895
1098
|
return builder;
|