@rotorsoft/act 0.5.7 → 0.6.1
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 +72 -20
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/InMemoryStore.d.ts +24 -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/ports.d.ts +13 -1
- package/dist/@types/ports.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 +22 -11
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +60 -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 +394 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +393 -172
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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,
|
|
@@ -81,18 +82,24 @@ var ValidationError = class extends Error {
|
|
|
81
82
|
}
|
|
82
83
|
};
|
|
83
84
|
var InvariantError = class extends Error {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
constructor(action2, payload, target, snapshot, description) {
|
|
86
|
+
super(`${action2} failed invariant: ${description}`);
|
|
87
|
+
this.action = action2;
|
|
88
|
+
this.payload = payload;
|
|
89
|
+
this.target = target;
|
|
90
|
+
this.snapshot = snapshot;
|
|
91
|
+
this.description = description;
|
|
87
92
|
this.name = Errors.InvariantError;
|
|
88
|
-
this.details = { name, payload, target, description };
|
|
89
93
|
}
|
|
90
94
|
};
|
|
91
95
|
var ConcurrencyError = class extends Error {
|
|
92
|
-
constructor(lastVersion, events, expectedVersion) {
|
|
96
|
+
constructor(stream, lastVersion, events, expectedVersion) {
|
|
93
97
|
super(
|
|
94
|
-
`Concurrency error committing
|
|
98
|
+
`Concurrency error committing "${events.map((e) => `${stream}.${e.name}.${JSON.stringify(e.data)}`).join(
|
|
99
|
+
", "
|
|
100
|
+
)}". Expected version ${expectedVersion} but found version ${lastVersion}.`
|
|
95
101
|
);
|
|
102
|
+
this.stream = stream;
|
|
96
103
|
this.lastVersion = lastVersion;
|
|
97
104
|
this.events = events;
|
|
98
105
|
this.expectedVersion = expectedVersion;
|
|
@@ -147,8 +154,9 @@ var QuerySchema = import_zod.z.object({
|
|
|
147
154
|
created_before: import_zod.z.date().optional(),
|
|
148
155
|
created_after: import_zod.z.date().optional(),
|
|
149
156
|
backward: import_zod.z.boolean().optional(),
|
|
150
|
-
correlation: import_zod.z.string().optional()
|
|
151
|
-
|
|
157
|
+
correlation: import_zod.z.string().optional(),
|
|
158
|
+
with_snaps: import_zod.z.boolean().optional()
|
|
159
|
+
}).readonly();
|
|
152
160
|
|
|
153
161
|
// src/types/index.ts
|
|
154
162
|
var Environments = [
|
|
@@ -254,38 +262,71 @@ async function sleep(ms) {
|
|
|
254
262
|
|
|
255
263
|
// src/adapters/InMemoryStore.ts
|
|
256
264
|
var InMemoryStream = class {
|
|
257
|
-
|
|
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
|
+
constructor(stream, source) {
|
|
258
275
|
this.stream = stream;
|
|
276
|
+
this.source = source;
|
|
277
|
+
}
|
|
278
|
+
get is_avaliable() {
|
|
279
|
+
return !this.blocked && (!this.leased_until || this.leased_until <= /* @__PURE__ */ new Date());
|
|
259
280
|
}
|
|
260
|
-
_at = -1;
|
|
261
|
-
_retry = -1;
|
|
262
|
-
_lease;
|
|
263
|
-
_blocked = false;
|
|
264
281
|
/**
|
|
265
282
|
* Attempt to lease this stream for processing.
|
|
266
|
-
* @param
|
|
283
|
+
* @param at - The end-of-lease watermark.
|
|
284
|
+
* @param by - The lease holder.
|
|
285
|
+
* @param millis - Lease duration in milliseconds.
|
|
267
286
|
* @returns The granted lease or undefined if blocked.
|
|
268
287
|
*/
|
|
269
|
-
lease(
|
|
270
|
-
if (
|
|
271
|
-
this.
|
|
272
|
-
|
|
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);
|
|
294
|
+
return {
|
|
295
|
+
stream: this.stream,
|
|
296
|
+
source: this.source,
|
|
297
|
+
at,
|
|
298
|
+
by,
|
|
299
|
+
retry: this.retry
|
|
300
|
+
};
|
|
273
301
|
}
|
|
274
302
|
}
|
|
275
303
|
/**
|
|
276
304
|
* Acknowledge completion of processing for this stream.
|
|
277
|
-
* @param
|
|
305
|
+
* @param at - Last processed watermark.
|
|
306
|
+
* @param by - Lease holder that processed the watermark.
|
|
278
307
|
*/
|
|
279
|
-
ack(
|
|
280
|
-
if (this.
|
|
281
|
-
this.
|
|
282
|
-
this.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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;
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
321
|
+
* @param error Blocked error message.
|
|
322
|
+
*/
|
|
323
|
+
block(by, error) {
|
|
324
|
+
if (this.leased_by === by) {
|
|
325
|
+
this.blocked = true;
|
|
326
|
+
this.error = error;
|
|
327
|
+
return true;
|
|
288
328
|
}
|
|
329
|
+
return false;
|
|
289
330
|
}
|
|
290
331
|
};
|
|
291
332
|
var InMemoryStore = class {
|
|
@@ -315,6 +356,7 @@ var InMemoryStore = class {
|
|
|
315
356
|
async drop() {
|
|
316
357
|
await sleep();
|
|
317
358
|
this._events.length = 0;
|
|
359
|
+
this._streams = /* @__PURE__ */ new Map();
|
|
318
360
|
}
|
|
319
361
|
/**
|
|
320
362
|
* Query events in the store, optionally filtered by query options.
|
|
@@ -332,15 +374,17 @@ var InMemoryStore = class {
|
|
|
332
374
|
limit,
|
|
333
375
|
created_before,
|
|
334
376
|
created_after,
|
|
335
|
-
correlation
|
|
377
|
+
correlation,
|
|
378
|
+
with_snaps = false
|
|
336
379
|
} = query || {};
|
|
337
380
|
let i = after + 1, count = 0;
|
|
338
381
|
while (i < this._events.length) {
|
|
339
382
|
const e = this._events[i++];
|
|
340
|
-
if (stream && e.stream
|
|
383
|
+
if (stream && !RegExp(`^${stream}$`).test(e.stream)) continue;
|
|
341
384
|
if (names && !names.includes(e.name)) continue;
|
|
342
385
|
if (correlation && e.meta?.correlation !== correlation) continue;
|
|
343
386
|
if (created_after && e.created <= created_after) continue;
|
|
387
|
+
if (e.name === SNAP_EVENT && !with_snaps) continue;
|
|
344
388
|
if (before && e.id >= before) break;
|
|
345
389
|
if (created_before && e.created >= created_before) break;
|
|
346
390
|
callback(e);
|
|
@@ -361,12 +405,14 @@ var InMemoryStore = class {
|
|
|
361
405
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
362
406
|
await sleep();
|
|
363
407
|
const instance = this._events.filter((e) => e.stream === stream);
|
|
364
|
-
if (typeof expectedVersion === "number" && instance.length - 1 !== expectedVersion)
|
|
408
|
+
if (typeof expectedVersion === "number" && instance.length - 1 !== expectedVersion) {
|
|
365
409
|
throw new ConcurrencyError(
|
|
410
|
+
stream,
|
|
366
411
|
instance.length - 1,
|
|
367
412
|
msgs,
|
|
368
413
|
expectedVersion
|
|
369
414
|
);
|
|
415
|
+
}
|
|
370
416
|
let version = instance.length;
|
|
371
417
|
return msgs.map(({ name, data }) => {
|
|
372
418
|
const committed = {
|
|
@@ -384,43 +430,49 @@ var InMemoryStore = class {
|
|
|
384
430
|
});
|
|
385
431
|
}
|
|
386
432
|
/**
|
|
387
|
-
*
|
|
388
|
-
* @param limit - Maximum number of streams to
|
|
389
|
-
* @
|
|
433
|
+
* 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).
|
|
436
|
+
* @returns The polled streams.
|
|
390
437
|
*/
|
|
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 };
|
|
438
|
+
async poll(limit, descending = false) {
|
|
439
|
+
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 }));
|
|
403
441
|
}
|
|
404
442
|
/**
|
|
405
443
|
* Lease streams for processing (e.g., for distributed consumers).
|
|
406
|
-
* @param leases - Lease requests.
|
|
444
|
+
* @param leases - Lease requests for streams, including end-of-lease watermark, lease holder, and source stream.
|
|
445
|
+
* @param leaseMilis - Lease duration in milliseconds.
|
|
407
446
|
* @returns Granted leases.
|
|
408
447
|
*/
|
|
409
|
-
async lease(leases) {
|
|
448
|
+
async lease(leases, millis) {
|
|
410
449
|
await sleep();
|
|
411
|
-
return leases.map((
|
|
412
|
-
const
|
|
413
|
-
this._streams.set(
|
|
414
|
-
return
|
|
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);
|
|
415
454
|
}).filter((l) => !!l);
|
|
416
455
|
}
|
|
417
456
|
/**
|
|
418
457
|
* Acknowledge completion of processing for leased streams.
|
|
419
|
-
* @param leases - Leases to acknowledge.
|
|
458
|
+
* @param leases - Leases to acknowledge, including last processed watermark and lease holder.
|
|
420
459
|
*/
|
|
421
460
|
async ack(leases) {
|
|
422
461
|
await sleep();
|
|
423
|
-
leases.
|
|
462
|
+
return leases.filter(
|
|
463
|
+
(lease) => this._streams.get(lease.stream)?.ack(lease.at, lease.by)
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
468
|
+
* @param leases - Leases to block, including lease holder and last error message.
|
|
469
|
+
* @returns Blocked leases.
|
|
470
|
+
*/
|
|
471
|
+
async block(leases) {
|
|
472
|
+
await sleep();
|
|
473
|
+
return leases.filter(
|
|
474
|
+
(lease) => this._streams.get(lease.stream)?.block(lease.by, lease.error)
|
|
475
|
+
);
|
|
424
476
|
}
|
|
425
477
|
};
|
|
426
478
|
|
|
@@ -469,6 +521,62 @@ var SNAP_EVENT = "__snapshot__";
|
|
|
469
521
|
var store = port(function store2(adapter) {
|
|
470
522
|
return adapter || new InMemoryStore();
|
|
471
523
|
});
|
|
524
|
+
function build_tracer(logLevel2) {
|
|
525
|
+
if (logLevel2 === "trace") {
|
|
526
|
+
return {
|
|
527
|
+
fetched: (fetched) => {
|
|
528
|
+
const data = Object.fromEntries(
|
|
529
|
+
fetched.map(({ stream, source, events }) => {
|
|
530
|
+
const key = source ? `${stream}<-${source}` : stream;
|
|
531
|
+
const value = Object.fromEntries(
|
|
532
|
+
events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
|
|
533
|
+
);
|
|
534
|
+
return [key, value];
|
|
535
|
+
})
|
|
536
|
+
);
|
|
537
|
+
logger.trace(data, "\u26A1\uFE0F fetch");
|
|
538
|
+
},
|
|
539
|
+
correlated: (leases) => {
|
|
540
|
+
const data = leases.map(({ stream }) => stream).join(" ");
|
|
541
|
+
logger.trace(`\u26A1\uFE0F correlate ${data}`);
|
|
542
|
+
},
|
|
543
|
+
leased: (leases) => {
|
|
544
|
+
const data = Object.fromEntries(
|
|
545
|
+
leases.map(({ stream, at, retry }) => [stream, { at, retry }])
|
|
546
|
+
);
|
|
547
|
+
logger.trace(data, "\u26A1\uFE0F lease");
|
|
548
|
+
},
|
|
549
|
+
acked: (leases) => {
|
|
550
|
+
const data = Object.fromEntries(
|
|
551
|
+
leases.map(({ stream, at, retry }) => [stream, { at, retry }])
|
|
552
|
+
);
|
|
553
|
+
logger.trace(data, "\u26A1\uFE0F ack");
|
|
554
|
+
},
|
|
555
|
+
blocked: (leases) => {
|
|
556
|
+
const data = Object.fromEntries(
|
|
557
|
+
leases.map(({ stream, at, retry, error }) => [
|
|
558
|
+
stream,
|
|
559
|
+
{ at, retry, error }
|
|
560
|
+
])
|
|
561
|
+
);
|
|
562
|
+
logger.trace(data, "\u26A1\uFE0F block");
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
} else {
|
|
566
|
+
return {
|
|
567
|
+
fetched: () => {
|
|
568
|
+
},
|
|
569
|
+
correlated: () => {
|
|
570
|
+
},
|
|
571
|
+
leased: () => {
|
|
572
|
+
},
|
|
573
|
+
acked: () => {
|
|
574
|
+
},
|
|
575
|
+
blocked: () => {
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
472
580
|
|
|
473
581
|
// src/signals.ts
|
|
474
582
|
process.once("SIGINT", async (arg) => {
|
|
@@ -530,21 +638,21 @@ async function load(me, stream, callback) {
|
|
|
530
638
|
}
|
|
531
639
|
callback && callback({ event, state: state2, patches, snaps });
|
|
532
640
|
},
|
|
533
|
-
{ stream }
|
|
534
|
-
true
|
|
641
|
+
{ stream, with_snaps: true }
|
|
535
642
|
);
|
|
536
|
-
logger.trace(
|
|
643
|
+
logger.trace(state2, `\u{1F7E2} load ${stream}`);
|
|
537
644
|
return { event, state: state2, patches, snaps };
|
|
538
645
|
}
|
|
539
646
|
async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
|
|
540
647
|
const { stream, expectedVersion, actor } = target;
|
|
541
648
|
if (!stream) throw new Error("Missing target stream");
|
|
542
649
|
payload = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
650
|
+
const snapshot = await load(me, stream);
|
|
651
|
+
const expected = expectedVersion || snapshot.event?.version;
|
|
543
652
|
logger.trace(
|
|
544
653
|
payload,
|
|
545
|
-
`\u{1F535} ${
|
|
654
|
+
`\u{1F535} ${stream}.${action2}${typeof expected === "number" ? `.${expected}` : ""}`
|
|
546
655
|
);
|
|
547
|
-
let snapshot = await load(me, stream);
|
|
548
656
|
if (me.given) {
|
|
549
657
|
const invariants = me.given[action2] || [];
|
|
550
658
|
invariants.forEach(({ valid, description }) => {
|
|
@@ -553,15 +661,15 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
553
661
|
action2,
|
|
554
662
|
payload,
|
|
555
663
|
target,
|
|
664
|
+
snapshot,
|
|
556
665
|
description
|
|
557
666
|
);
|
|
558
667
|
});
|
|
559
668
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (!result) return snapshot;
|
|
669
|
+
const result = me.on[action2](payload, snapshot, target);
|
|
670
|
+
if (!result) return [snapshot];
|
|
563
671
|
if (Array.isArray(result) && result.length === 0) {
|
|
564
|
-
return snapshot;
|
|
672
|
+
return [snapshot];
|
|
565
673
|
}
|
|
566
674
|
const tuples = Array.isArray(result[0]) ? result : [result];
|
|
567
675
|
const emitted = tuples.map(([name, data]) => ({
|
|
@@ -584,36 +692,47 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
584
692
|
} : void 0
|
|
585
693
|
}
|
|
586
694
|
};
|
|
695
|
+
logger.trace(
|
|
696
|
+
emitted.map((e) => e.data),
|
|
697
|
+
`\u{1F534} commit ${stream}.${emitted.map((e) => e.name).join(", ")}`
|
|
698
|
+
);
|
|
587
699
|
const committed = await store().commit(
|
|
588
700
|
stream,
|
|
589
701
|
emitted,
|
|
590
702
|
meta,
|
|
591
703
|
// TODO: review reactions not enforcing expected version
|
|
592
|
-
reactingTo ? void 0 :
|
|
704
|
+
reactingTo ? void 0 : expected
|
|
593
705
|
);
|
|
594
|
-
|
|
706
|
+
let { state: state2, patches } = snapshot;
|
|
707
|
+
const snapshots = committed.map((event) => {
|
|
595
708
|
state2 = patch(state2, me.patch[event.name](event, state2));
|
|
596
709
|
patches++;
|
|
597
|
-
logger.trace({ event, state: state2 }, "\u{1F534} commit");
|
|
598
710
|
return { event, state: state2, patches, snaps: snapshot.snaps };
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
|
|
711
|
+
});
|
|
712
|
+
const last = snapshots.at(-1);
|
|
713
|
+
me.snap && me.snap(last) && void snap(last);
|
|
714
|
+
return snapshots;
|
|
602
715
|
}
|
|
603
716
|
|
|
604
717
|
// src/act.ts
|
|
718
|
+
var tracer = build_tracer(config().logLevel);
|
|
605
719
|
var Act = class {
|
|
606
720
|
/**
|
|
607
721
|
* Create a new Act orchestrator.
|
|
608
722
|
*
|
|
609
723
|
* @param registry The registry of state, event, and action schemas
|
|
610
|
-
* @param drainLimit The maximum number of events to drain per cycle
|
|
611
724
|
*/
|
|
612
|
-
constructor(registry
|
|
725
|
+
constructor(registry) {
|
|
613
726
|
this.registry = registry;
|
|
614
|
-
|
|
727
|
+
dispose(() => {
|
|
728
|
+
this._emitter.removeAllListeners();
|
|
729
|
+
this.stop_correlations();
|
|
730
|
+
return Promise.resolve();
|
|
731
|
+
});
|
|
615
732
|
}
|
|
616
733
|
_emitter = new import_events.default();
|
|
734
|
+
_drain_locked = false;
|
|
735
|
+
_correlation_interval = void 0;
|
|
617
736
|
emit(event, args) {
|
|
618
737
|
return this._emitter.emit(event, args);
|
|
619
738
|
}
|
|
@@ -640,16 +759,17 @@ var Act = class {
|
|
|
640
759
|
* await app.do("increment", { stream: "counter1", actor }, { by: 1 });
|
|
641
760
|
*/
|
|
642
761
|
async do(action2, target, payload, reactingTo, skipValidation = false) {
|
|
643
|
-
const
|
|
762
|
+
const snapshots = await action(
|
|
644
763
|
this.registry.actions[action2],
|
|
645
764
|
action2,
|
|
646
765
|
target,
|
|
647
766
|
payload,
|
|
767
|
+
// @ts-expect-error type lost
|
|
648
768
|
reactingTo,
|
|
649
769
|
skipValidation
|
|
650
770
|
);
|
|
651
|
-
this.emit("committed",
|
|
652
|
-
return
|
|
771
|
+
this.emit("committed", snapshots);
|
|
772
|
+
return snapshots;
|
|
653
773
|
}
|
|
654
774
|
/**
|
|
655
775
|
* Loads the current state snapshot for a given state machine and stream.
|
|
@@ -687,38 +807,58 @@ var Act = class {
|
|
|
687
807
|
}, query);
|
|
688
808
|
return { first, last, count };
|
|
689
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Query the event store for events matching a filter.
|
|
812
|
+
* Use this version with caution, as it return events in memory.
|
|
813
|
+
*
|
|
814
|
+
* @param query The query filter (e.g., by stream, event name, or time range)
|
|
815
|
+
* @returns The matching events
|
|
816
|
+
*
|
|
817
|
+
* @example
|
|
818
|
+
* const { count } = await app.query({ stream: "counter1" }, (event) => console.log(event));
|
|
819
|
+
*/
|
|
820
|
+
async query_array(query) {
|
|
821
|
+
const events = [];
|
|
822
|
+
await store().query((e) => events.push(e), query);
|
|
823
|
+
return events;
|
|
824
|
+
}
|
|
690
825
|
/**
|
|
691
826
|
* Handles leased reactions.
|
|
692
827
|
*
|
|
828
|
+
* This is called by the main `drain` loop after fetching new events.
|
|
829
|
+
* It handles reactions, supporting retries, blocking, and error handling.
|
|
830
|
+
*
|
|
693
831
|
* @internal
|
|
694
832
|
* @param lease The lease to handle
|
|
695
|
-
* @param
|
|
696
|
-
* @returns The lease
|
|
833
|
+
* @param payloads The reactions to handle
|
|
834
|
+
* @returns The lease with results
|
|
697
835
|
*/
|
|
698
|
-
async handle(lease,
|
|
836
|
+
async handle(lease, payloads) {
|
|
837
|
+
if (payloads.length === 0) return { lease, at: lease.at };
|
|
699
838
|
const stream = lease.stream;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
839
|
+
let at = payloads.at(0).event.id, handled = 0;
|
|
840
|
+
lease.retry > 0 && logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
841
|
+
for (const payload of payloads) {
|
|
842
|
+
const { event, handler, options } = payload;
|
|
703
843
|
try {
|
|
704
844
|
await handler(event, stream);
|
|
705
|
-
|
|
845
|
+
at = event.id;
|
|
846
|
+
handled++;
|
|
706
847
|
} catch (error) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
848
|
+
logger.error(error);
|
|
849
|
+
const block = lease.retry >= options.maxRetries && options.blockOnError;
|
|
850
|
+
block && logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
|
|
851
|
+
return {
|
|
852
|
+
lease,
|
|
853
|
+
at,
|
|
854
|
+
// only report error when nothing was handled
|
|
855
|
+
error: handled === 0 ? error.message : void 0,
|
|
856
|
+
block
|
|
857
|
+
};
|
|
717
858
|
}
|
|
718
859
|
}
|
|
719
|
-
return lease;
|
|
860
|
+
return { lease, at };
|
|
720
861
|
}
|
|
721
|
-
drainLocked = false;
|
|
722
862
|
/**
|
|
723
863
|
* Drains and processes events from the store, triggering reactions and updating state.
|
|
724
864
|
*
|
|
@@ -729,91 +869,173 @@ var Act = class {
|
|
|
729
869
|
* @example
|
|
730
870
|
* await app.drain();
|
|
731
871
|
*/
|
|
732
|
-
async drain(
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
872
|
+
async drain({
|
|
873
|
+
streamLimit = 10,
|
|
874
|
+
eventLimit = 10,
|
|
875
|
+
leaseMillis = 1e4,
|
|
876
|
+
descending = false
|
|
877
|
+
} = {}) {
|
|
878
|
+
if (!this._drain_locked) {
|
|
879
|
+
try {
|
|
880
|
+
this._drain_locked = true;
|
|
881
|
+
const polled = await store().poll(streamLimit, descending);
|
|
882
|
+
const fetched = await Promise.all(
|
|
883
|
+
polled.map(async ({ stream, source, at }) => {
|
|
884
|
+
const events = await this.query_array({
|
|
885
|
+
stream: source,
|
|
886
|
+
after: at,
|
|
887
|
+
limit: eventLimit
|
|
888
|
+
});
|
|
889
|
+
return { stream, source, events };
|
|
890
|
+
})
|
|
891
|
+
);
|
|
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) {
|
|
901
|
+
const leases = /* @__PURE__ */ new Map();
|
|
902
|
+
fetched.forEach(({ stream, events }) => {
|
|
903
|
+
const payloads = events.flatMap((event) => {
|
|
904
|
+
const register = this.registry.events[event.name];
|
|
905
|
+
if (!register) return [];
|
|
906
|
+
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;
|
|
911
|
+
return resolved && resolved.target === stream;
|
|
912
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
913
|
+
});
|
|
914
|
+
leases.set(stream, {
|
|
915
|
+
lease: {
|
|
916
|
+
stream,
|
|
917
|
+
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
|
|
921
|
+
},
|
|
922
|
+
// @ts-expect-error indexed by key
|
|
923
|
+
payloads
|
|
924
|
+
});
|
|
925
|
+
});
|
|
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
|
+
}
|
|
755
954
|
}
|
|
756
955
|
}
|
|
956
|
+
} catch (error) {
|
|
957
|
+
logger.error(error);
|
|
958
|
+
} finally {
|
|
959
|
+
this._drain_locked = false;
|
|
757
960
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
961
|
+
}
|
|
962
|
+
return { leased: [], acked: [], blocked: [] };
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Correlates streams using reaction resolvers.
|
|
966
|
+
* @param query - The query filter (e.g., by stream, event name, or starting point).
|
|
967
|
+
* @returns The leases of newly correlated streams, and the last seen event ID.
|
|
968
|
+
*/
|
|
969
|
+
async correlate(query = { after: -1, limit: 10 }) {
|
|
970
|
+
const correlated = /* @__PURE__ */ new Map();
|
|
971
|
+
let last_id = query.after || -1;
|
|
972
|
+
await store().query((event) => {
|
|
973
|
+
last_id = event.id;
|
|
974
|
+
const register = this.registry.events[event.name];
|
|
975
|
+
if (register) {
|
|
976
|
+
for (const reaction of register.reactions.values()) {
|
|
977
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
978
|
+
resolved && (correlated.get(resolved.target) || correlated.set(resolved.target, []).get(resolved.target)).push({ ...reaction, source: resolved.source, event });
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}, query);
|
|
982
|
+
if (correlated.size) {
|
|
983
|
+
const leases = [...correlated.entries()].map(([stream, payloads]) => ({
|
|
761
984
|
stream,
|
|
762
|
-
|
|
985
|
+
// TODO: by convention, the first defined source wins (this can be tricky)
|
|
986
|
+
source: payloads.find((p) => p.source)?.source || void 0,
|
|
987
|
+
by: (0, import_crypto2.randomUUID)(),
|
|
988
|
+
at: 0,
|
|
763
989
|
retry: 0,
|
|
764
|
-
|
|
990
|
+
payloads
|
|
765
991
|
}));
|
|
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
|
-
);
|
|
992
|
+
const leased = await store().lease(leases, 0);
|
|
993
|
+
leased.length && tracer.correlated(leased);
|
|
994
|
+
return { leased, last_id };
|
|
995
|
+
}
|
|
996
|
+
return { leased: [], last_id };
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Starts correlation worker that identifies and registers new streams using reaction resolvers.
|
|
1000
|
+
*
|
|
1001
|
+
* Enables "dynamic reactions", allowing streams to be auto-discovered based on event content.
|
|
1002
|
+
* - Uses a correlation sliding window over the event stream to identify new streams.
|
|
1003
|
+
* - Once registered, these streams are picked up by the main `drain` loop.
|
|
1004
|
+
* - Users should have full control over their correlation strategy.
|
|
1005
|
+
* - The starting point keeps increasing with each new batch of events.
|
|
1006
|
+
* - Users are responsible for storing the last seen event ID.
|
|
1007
|
+
*
|
|
1008
|
+
* @param query - The query filter (e.g., by stream, event name, or starting point).
|
|
1009
|
+
* @param frequency - The frequency of correlation checks (in milliseconds).
|
|
1010
|
+
* @param callback - Callback to report stats (new strems, last seen event ID, etc.).
|
|
1011
|
+
* @returns true if the correlation worker started, false otherwise (already started).
|
|
1012
|
+
*/
|
|
1013
|
+
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
1014
|
+
if (this._correlation_interval) return false;
|
|
1015
|
+
const limit = query.limit || 100;
|
|
1016
|
+
let after = query.after || -1;
|
|
1017
|
+
this._correlation_interval = setInterval(
|
|
1018
|
+
() => this.correlate({ ...query, after, limit }).then((result) => {
|
|
1019
|
+
after = result.last_id;
|
|
1020
|
+
if (callback && result.leased.length) callback(result.leased);
|
|
1021
|
+
}).catch(console.error),
|
|
1022
|
+
frequency
|
|
1023
|
+
);
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
stop_correlations() {
|
|
1027
|
+
if (this._correlation_interval) {
|
|
1028
|
+
clearInterval(this._correlation_interval);
|
|
1029
|
+
this._correlation_interval = void 0;
|
|
809
1030
|
}
|
|
810
|
-
this.drainLocked = false;
|
|
811
|
-
return drained.length;
|
|
812
1031
|
}
|
|
813
1032
|
};
|
|
814
1033
|
|
|
815
1034
|
// src/act-builder.ts
|
|
816
|
-
var _this_ = ({ stream }) =>
|
|
1035
|
+
var _this_ = ({ stream }) => ({
|
|
1036
|
+
source: stream,
|
|
1037
|
+
target: stream
|
|
1038
|
+
});
|
|
817
1039
|
var _void_ = () => void 0;
|
|
818
1040
|
function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
819
1041
|
actions: {},
|
|
@@ -865,8 +1087,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
865
1087
|
resolver: _this_,
|
|
866
1088
|
options: {
|
|
867
1089
|
blockOnError: options?.blockOnError ?? true,
|
|
868
|
-
maxRetries: options?.maxRetries ?? 3
|
|
869
|
-
retryDelayMs: options?.retryDelayMs ?? 1e3
|
|
1090
|
+
maxRetries: options?.maxRetries ?? 3
|
|
870
1091
|
}
|
|
871
1092
|
};
|
|
872
1093
|
registry.events[event].reactions.set(handler.name, reaction);
|
|
@@ -875,7 +1096,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
875
1096
|
to(resolver) {
|
|
876
1097
|
registry.events[event].reactions.set(handler.name, {
|
|
877
1098
|
...reaction,
|
|
878
|
-
resolver
|
|
1099
|
+
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
879
1100
|
});
|
|
880
1101
|
return builder;
|
|
881
1102
|
},
|
|
@@ -889,7 +1110,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
889
1110
|
};
|
|
890
1111
|
}
|
|
891
1112
|
}),
|
|
892
|
-
build: (
|
|
1113
|
+
build: () => new Act(registry),
|
|
893
1114
|
events: registry.events
|
|
894
1115
|
};
|
|
895
1116
|
return builder;
|
|
@@ -970,6 +1191,7 @@ function action_builder(state2) {
|
|
|
970
1191
|
ValidationError,
|
|
971
1192
|
ZodEmpty,
|
|
972
1193
|
act,
|
|
1194
|
+
build_tracer,
|
|
973
1195
|
config,
|
|
974
1196
|
dispose,
|
|
975
1197
|
disposeAndExit,
|