@rotorsoft/act 0.5.6 → 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.js
CHANGED
|
@@ -17,18 +17,24 @@ var ValidationError = class extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
var InvariantError = class extends Error {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
constructor(action2, payload, target, snapshot, description) {
|
|
21
|
+
super(`${action2} failed invariant: ${description}`);
|
|
22
|
+
this.action = action2;
|
|
23
|
+
this.payload = payload;
|
|
24
|
+
this.target = target;
|
|
25
|
+
this.snapshot = snapshot;
|
|
26
|
+
this.description = description;
|
|
23
27
|
this.name = Errors.InvariantError;
|
|
24
|
-
this.details = { name, payload, target, description };
|
|
25
28
|
}
|
|
26
29
|
};
|
|
27
30
|
var ConcurrencyError = class extends Error {
|
|
28
|
-
constructor(lastVersion, events, expectedVersion) {
|
|
31
|
+
constructor(stream, lastVersion, events, expectedVersion) {
|
|
29
32
|
super(
|
|
30
|
-
`Concurrency error committing
|
|
33
|
+
`Concurrency error committing "${events.map((e) => `${stream}.${e.name}.${JSON.stringify(e.data)}`).join(
|
|
34
|
+
", "
|
|
35
|
+
)}". Expected version ${expectedVersion} but found version ${lastVersion}.`
|
|
31
36
|
);
|
|
37
|
+
this.stream = stream;
|
|
32
38
|
this.lastVersion = lastVersion;
|
|
33
39
|
this.events = events;
|
|
34
40
|
this.expectedVersion = expectedVersion;
|
|
@@ -83,8 +89,9 @@ var QuerySchema = z.object({
|
|
|
83
89
|
created_before: z.date().optional(),
|
|
84
90
|
created_after: z.date().optional(),
|
|
85
91
|
backward: z.boolean().optional(),
|
|
86
|
-
correlation: z.string().optional()
|
|
87
|
-
|
|
92
|
+
correlation: z.string().optional(),
|
|
93
|
+
with_snaps: z.boolean().optional()
|
|
94
|
+
}).readonly();
|
|
88
95
|
|
|
89
96
|
// src/types/index.ts
|
|
90
97
|
var Environments = [
|
|
@@ -190,38 +197,71 @@ async function sleep(ms) {
|
|
|
190
197
|
|
|
191
198
|
// src/adapters/InMemoryStore.ts
|
|
192
199
|
var InMemoryStream = class {
|
|
193
|
-
|
|
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
|
+
constructor(stream, source) {
|
|
194
210
|
this.stream = stream;
|
|
211
|
+
this.source = source;
|
|
212
|
+
}
|
|
213
|
+
get is_avaliable() {
|
|
214
|
+
return !this.blocked && (!this.leased_until || this.leased_until <= /* @__PURE__ */ new Date());
|
|
195
215
|
}
|
|
196
|
-
_at = -1;
|
|
197
|
-
_retry = -1;
|
|
198
|
-
_lease;
|
|
199
|
-
_blocked = false;
|
|
200
216
|
/**
|
|
201
217
|
* Attempt to lease this stream for processing.
|
|
202
|
-
* @param
|
|
218
|
+
* @param at - The end-of-lease watermark.
|
|
219
|
+
* @param by - The lease holder.
|
|
220
|
+
* @param millis - Lease duration in milliseconds.
|
|
203
221
|
* @returns The granted lease or undefined if blocked.
|
|
204
222
|
*/
|
|
205
|
-
lease(
|
|
206
|
-
if (
|
|
207
|
-
this.
|
|
208
|
-
|
|
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);
|
|
229
|
+
return {
|
|
230
|
+
stream: this.stream,
|
|
231
|
+
source: this.source,
|
|
232
|
+
at,
|
|
233
|
+
by,
|
|
234
|
+
retry: this.retry
|
|
235
|
+
};
|
|
209
236
|
}
|
|
210
237
|
}
|
|
211
238
|
/**
|
|
212
239
|
* Acknowledge completion of processing for this stream.
|
|
213
|
-
* @param
|
|
240
|
+
* @param at - Last processed watermark.
|
|
241
|
+
* @param by - Lease holder that processed the watermark.
|
|
214
242
|
*/
|
|
215
|
-
ack(
|
|
216
|
-
if (this.
|
|
217
|
-
this.
|
|
218
|
-
this.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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;
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
256
|
+
* @param error Blocked error message.
|
|
257
|
+
*/
|
|
258
|
+
block(by, error) {
|
|
259
|
+
if (this.leased_by === by) {
|
|
260
|
+
this.blocked = true;
|
|
261
|
+
this.error = error;
|
|
262
|
+
return true;
|
|
224
263
|
}
|
|
264
|
+
return false;
|
|
225
265
|
}
|
|
226
266
|
};
|
|
227
267
|
var InMemoryStore = class {
|
|
@@ -251,6 +291,7 @@ var InMemoryStore = class {
|
|
|
251
291
|
async drop() {
|
|
252
292
|
await sleep();
|
|
253
293
|
this._events.length = 0;
|
|
294
|
+
this._streams = /* @__PURE__ */ new Map();
|
|
254
295
|
}
|
|
255
296
|
/**
|
|
256
297
|
* Query events in the store, optionally filtered by query options.
|
|
@@ -268,15 +309,17 @@ var InMemoryStore = class {
|
|
|
268
309
|
limit,
|
|
269
310
|
created_before,
|
|
270
311
|
created_after,
|
|
271
|
-
correlation
|
|
312
|
+
correlation,
|
|
313
|
+
with_snaps = false
|
|
272
314
|
} = query || {};
|
|
273
315
|
let i = after + 1, count = 0;
|
|
274
316
|
while (i < this._events.length) {
|
|
275
317
|
const e = this._events[i++];
|
|
276
|
-
if (stream && e.stream
|
|
318
|
+
if (stream && !RegExp(`^${stream}$`).test(e.stream)) continue;
|
|
277
319
|
if (names && !names.includes(e.name)) continue;
|
|
278
320
|
if (correlation && e.meta?.correlation !== correlation) continue;
|
|
279
321
|
if (created_after && e.created <= created_after) continue;
|
|
322
|
+
if (e.name === SNAP_EVENT && !with_snaps) continue;
|
|
280
323
|
if (before && e.id >= before) break;
|
|
281
324
|
if (created_before && e.created >= created_before) break;
|
|
282
325
|
callback(e);
|
|
@@ -297,12 +340,14 @@ var InMemoryStore = class {
|
|
|
297
340
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
298
341
|
await sleep();
|
|
299
342
|
const instance = this._events.filter((e) => e.stream === stream);
|
|
300
|
-
if (typeof expectedVersion === "number" && instance.length - 1 !== expectedVersion)
|
|
343
|
+
if (typeof expectedVersion === "number" && instance.length - 1 !== expectedVersion) {
|
|
301
344
|
throw new ConcurrencyError(
|
|
345
|
+
stream,
|
|
302
346
|
instance.length - 1,
|
|
303
347
|
msgs,
|
|
304
348
|
expectedVersion
|
|
305
349
|
);
|
|
350
|
+
}
|
|
306
351
|
let version = instance.length;
|
|
307
352
|
return msgs.map(({ name, data }) => {
|
|
308
353
|
const committed = {
|
|
@@ -320,43 +365,48 @@ var InMemoryStore = class {
|
|
|
320
365
|
});
|
|
321
366
|
}
|
|
322
367
|
/**
|
|
323
|
-
*
|
|
324
|
-
* @param limit - Maximum number of streams to
|
|
325
|
-
* @returns
|
|
368
|
+
* Polls the store for unblocked streams needing processing, ordered by lease watermark ascending.
|
|
369
|
+
* @param limit - Maximum number of streams to poll.
|
|
370
|
+
* @returns The polled streams.
|
|
326
371
|
*/
|
|
327
|
-
async
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
(min, s) => Math.min(min, s._at),
|
|
331
|
-
Number.MAX_SAFE_INTEGER
|
|
332
|
-
) : -1;
|
|
333
|
-
const events = [];
|
|
334
|
-
await this.query((e) => e.name !== SNAP_EVENT && events.push(e), {
|
|
335
|
-
after,
|
|
336
|
-
limit
|
|
337
|
-
});
|
|
338
|
-
return { streams: streams.map(({ stream }) => stream), events };
|
|
372
|
+
async poll(limit) {
|
|
373
|
+
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 }));
|
|
339
375
|
}
|
|
340
376
|
/**
|
|
341
377
|
* Lease streams for processing (e.g., for distributed consumers).
|
|
342
|
-
* @param leases - Lease requests.
|
|
378
|
+
* @param leases - Lease requests for streams, including end-of-lease watermark, lease holder, and source stream.
|
|
379
|
+
* @param leaseMilis - Lease duration in milliseconds.
|
|
343
380
|
* @returns Granted leases.
|
|
344
381
|
*/
|
|
345
|
-
async lease(leases) {
|
|
382
|
+
async lease(leases, millis) {
|
|
346
383
|
await sleep();
|
|
347
|
-
return leases.map((
|
|
348
|
-
const
|
|
349
|
-
this._streams.set(
|
|
350
|
-
return
|
|
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);
|
|
351
388
|
}).filter((l) => !!l);
|
|
352
389
|
}
|
|
353
390
|
/**
|
|
354
391
|
* Acknowledge completion of processing for leased streams.
|
|
355
|
-
* @param leases - Leases to acknowledge.
|
|
392
|
+
* @param leases - Leases to acknowledge, including last processed watermark and lease holder.
|
|
356
393
|
*/
|
|
357
394
|
async ack(leases) {
|
|
358
395
|
await sleep();
|
|
359
|
-
leases.
|
|
396
|
+
return leases.filter(
|
|
397
|
+
(lease) => this._streams.get(lease.stream)?.ack(lease.at, lease.by)
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
402
|
+
* @param leases - Leases to block, including lease holder and last error message.
|
|
403
|
+
* @returns Blocked leases.
|
|
404
|
+
*/
|
|
405
|
+
async block(leases) {
|
|
406
|
+
await sleep();
|
|
407
|
+
return leases.filter(
|
|
408
|
+
(lease) => this._streams.get(lease.stream)?.block(lease.by, lease.error)
|
|
409
|
+
);
|
|
360
410
|
}
|
|
361
411
|
};
|
|
362
412
|
|
|
@@ -466,21 +516,21 @@ async function load(me, stream, callback) {
|
|
|
466
516
|
}
|
|
467
517
|
callback && callback({ event, state: state2, patches, snaps });
|
|
468
518
|
},
|
|
469
|
-
{ stream }
|
|
470
|
-
true
|
|
519
|
+
{ stream, with_snaps: true }
|
|
471
520
|
);
|
|
472
|
-
logger.trace(
|
|
521
|
+
logger.trace(state2, `\u{1F7E2} load ${stream}`);
|
|
473
522
|
return { event, state: state2, patches, snaps };
|
|
474
523
|
}
|
|
475
524
|
async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
|
|
476
525
|
const { stream, expectedVersion, actor } = target;
|
|
477
526
|
if (!stream) throw new Error("Missing target stream");
|
|
478
527
|
payload = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
528
|
+
const snapshot = await load(me, stream);
|
|
529
|
+
const expected = expectedVersion || snapshot.event?.version;
|
|
479
530
|
logger.trace(
|
|
480
531
|
payload,
|
|
481
|
-
`\u{1F535} ${
|
|
532
|
+
`\u{1F535} ${stream}.${action2}${typeof expected === "number" ? `.${expected}` : ""}`
|
|
482
533
|
);
|
|
483
|
-
let snapshot = await load(me, stream);
|
|
484
534
|
if (me.given) {
|
|
485
535
|
const invariants = me.given[action2] || [];
|
|
486
536
|
invariants.forEach(({ valid, description }) => {
|
|
@@ -489,15 +539,15 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
489
539
|
action2,
|
|
490
540
|
payload,
|
|
491
541
|
target,
|
|
542
|
+
snapshot,
|
|
492
543
|
description
|
|
493
544
|
);
|
|
494
545
|
});
|
|
495
546
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (!result) return snapshot;
|
|
547
|
+
const result = me.on[action2](payload, snapshot, target);
|
|
548
|
+
if (!result) return [snapshot];
|
|
499
549
|
if (Array.isArray(result) && result.length === 0) {
|
|
500
|
-
return snapshot;
|
|
550
|
+
return [snapshot];
|
|
501
551
|
}
|
|
502
552
|
const tuples = Array.isArray(result[0]) ? result : [result];
|
|
503
553
|
const emitted = tuples.map(([name, data]) => ({
|
|
@@ -520,36 +570,80 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
520
570
|
} : void 0
|
|
521
571
|
}
|
|
522
572
|
};
|
|
573
|
+
logger.trace(
|
|
574
|
+
emitted.map((e) => e.data),
|
|
575
|
+
`\u{1F534} commit ${stream}.${emitted.map((e) => e.name).join(", ")}`
|
|
576
|
+
);
|
|
523
577
|
const committed = await store().commit(
|
|
524
578
|
stream,
|
|
525
579
|
emitted,
|
|
526
580
|
meta,
|
|
527
581
|
// TODO: review reactions not enforcing expected version
|
|
528
|
-
reactingTo ? void 0 :
|
|
582
|
+
reactingTo ? void 0 : expected
|
|
529
583
|
);
|
|
530
|
-
|
|
584
|
+
let { state: state2, patches } = snapshot;
|
|
585
|
+
const snapshots = committed.map((event) => {
|
|
531
586
|
state2 = patch(state2, me.patch[event.name](event, state2));
|
|
532
587
|
patches++;
|
|
533
|
-
logger.trace({ event, state: state2 }, "\u{1F534} commit");
|
|
534
588
|
return { event, state: state2, patches, snaps: snapshot.snaps };
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
|
|
589
|
+
});
|
|
590
|
+
const last = snapshots.at(-1);
|
|
591
|
+
me.snap && me.snap(last) && void snap(last);
|
|
592
|
+
return snapshots;
|
|
538
593
|
}
|
|
539
594
|
|
|
540
595
|
// 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
|
+
}
|
|
541
630
|
var Act = class {
|
|
542
631
|
/**
|
|
543
632
|
* Create a new Act orchestrator.
|
|
544
633
|
*
|
|
545
634
|
* @param registry The registry of state, event, and action schemas
|
|
546
|
-
* @param drainLimit The maximum number of events to drain per cycle
|
|
547
635
|
*/
|
|
548
|
-
constructor(registry
|
|
636
|
+
constructor(registry) {
|
|
549
637
|
this.registry = registry;
|
|
550
|
-
|
|
638
|
+
dispose(() => {
|
|
639
|
+
this._emitter.removeAllListeners();
|
|
640
|
+
this.stop_correlations();
|
|
641
|
+
return Promise.resolve();
|
|
642
|
+
});
|
|
551
643
|
}
|
|
552
644
|
_emitter = new EventEmitter();
|
|
645
|
+
_drain_locked = false;
|
|
646
|
+
_correlation_interval = void 0;
|
|
553
647
|
emit(event, args) {
|
|
554
648
|
return this._emitter.emit(event, args);
|
|
555
649
|
}
|
|
@@ -576,16 +670,17 @@ var Act = class {
|
|
|
576
670
|
* await app.do("increment", { stream: "counter1", actor }, { by: 1 });
|
|
577
671
|
*/
|
|
578
672
|
async do(action2, target, payload, reactingTo, skipValidation = false) {
|
|
579
|
-
const
|
|
673
|
+
const snapshots = await action(
|
|
580
674
|
this.registry.actions[action2],
|
|
581
675
|
action2,
|
|
582
676
|
target,
|
|
583
677
|
payload,
|
|
678
|
+
// @ts-expect-error type lost
|
|
584
679
|
reactingTo,
|
|
585
680
|
skipValidation
|
|
586
681
|
);
|
|
587
|
-
this.emit("committed",
|
|
588
|
-
return
|
|
682
|
+
this.emit("committed", snapshots);
|
|
683
|
+
return snapshots;
|
|
589
684
|
}
|
|
590
685
|
/**
|
|
591
686
|
* Loads the current state snapshot for a given state machine and stream.
|
|
@@ -623,38 +718,76 @@ var Act = class {
|
|
|
623
718
|
}, query);
|
|
624
719
|
return { first, last, count };
|
|
625
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* Query the event store for events matching a filter.
|
|
723
|
+
* Use this version with caution, as it return events in memory.
|
|
724
|
+
*
|
|
725
|
+
* @param query The query filter (e.g., by stream, event name, or time range)
|
|
726
|
+
* @returns The matching events
|
|
727
|
+
*
|
|
728
|
+
* @example
|
|
729
|
+
* const { count } = await app.query({ stream: "counter1" }, (event) => console.log(event));
|
|
730
|
+
*/
|
|
731
|
+
async query_array(query) {
|
|
732
|
+
const events = [];
|
|
733
|
+
await store().query((e) => events.push(e), query);
|
|
734
|
+
return events;
|
|
735
|
+
}
|
|
626
736
|
/**
|
|
627
737
|
* Handles leased reactions.
|
|
628
738
|
*
|
|
739
|
+
* This is called by the main `drain` loop after fetching new events.
|
|
740
|
+
* It handles reactions, supporting retries, blocking, and error handling.
|
|
741
|
+
*
|
|
629
742
|
* @internal
|
|
630
743
|
* @param lease The lease to handle
|
|
631
|
-
* @param
|
|
632
|
-
* @returns The lease
|
|
744
|
+
* @param payloads The reactions to handle
|
|
745
|
+
* @returns The lease with results
|
|
633
746
|
*/
|
|
634
|
-
async handle(lease,
|
|
747
|
+
async handle(lease, payloads) {
|
|
748
|
+
if (payloads.length === 0) return { lease, at: lease.at };
|
|
635
749
|
const stream = lease.stream;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
750
|
+
let at = payloads.at(0).event.id, handled = 0;
|
|
751
|
+
lease.retry > 0 && logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
752
|
+
for (const payload of payloads) {
|
|
753
|
+
const { event, handler, options } = payload;
|
|
639
754
|
try {
|
|
640
755
|
await handler(event, stream);
|
|
641
|
-
|
|
756
|
+
at = event.id;
|
|
757
|
+
handled++;
|
|
642
758
|
} catch (error) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
759
|
+
logger.error(error);
|
|
760
|
+
const block = lease.retry >= options.maxRetries && options.blockOnError;
|
|
761
|
+
block && logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
|
|
762
|
+
return {
|
|
763
|
+
lease,
|
|
764
|
+
at,
|
|
765
|
+
// only report error when nothing was handled
|
|
766
|
+
error: handled === 0 ? error.message : void 0,
|
|
767
|
+
block
|
|
768
|
+
};
|
|
653
769
|
}
|
|
654
770
|
}
|
|
655
|
-
return lease;
|
|
771
|
+
return { lease, at };
|
|
772
|
+
}
|
|
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
|
+
);
|
|
656
790
|
}
|
|
657
|
-
drainLocked = false;
|
|
658
791
|
/**
|
|
659
792
|
* Drains and processes events from the store, triggering reactions and updating state.
|
|
660
793
|
*
|
|
@@ -665,91 +798,162 @@ var Act = class {
|
|
|
665
798
|
* @example
|
|
666
799
|
* await app.drain();
|
|
667
800
|
*/
|
|
668
|
-
async drain(
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
801
|
+
async drain({
|
|
802
|
+
streamLimit = 10,
|
|
803
|
+
eventLimit = 10,
|
|
804
|
+
leaseMillis = 1e4
|
|
805
|
+
} = {}) {
|
|
806
|
+
if (!this._drain_locked) {
|
|
807
|
+
try {
|
|
808
|
+
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]
|
|
817
|
+
);
|
|
818
|
+
if (count > 0) {
|
|
819
|
+
const leases = /* @__PURE__ */ new Map();
|
|
820
|
+
fetch.forEach(({ stream, events }) => {
|
|
821
|
+
const payloads = events.flatMap((event) => {
|
|
822
|
+
const register = this.registry.events[event.name];
|
|
823
|
+
if (!register) return [];
|
|
824
|
+
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;
|
|
829
|
+
return resolved && resolved.target === stream;
|
|
830
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
831
|
+
});
|
|
832
|
+
leases.set(stream, {
|
|
833
|
+
lease: {
|
|
834
|
+
stream,
|
|
835
|
+
by: randomUUID2(),
|
|
836
|
+
at: events.at(-1)?.id || last_at,
|
|
837
|
+
// move the lease watermark forward when no events found in window
|
|
838
|
+
retry: 0
|
|
839
|
+
},
|
|
840
|
+
// @ts-expect-error indexed by key
|
|
841
|
+
payloads
|
|
842
|
+
});
|
|
843
|
+
});
|
|
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
|
+
}
|
|
691
872
|
}
|
|
692
873
|
}
|
|
874
|
+
} catch (error) {
|
|
875
|
+
logger.error(error);
|
|
876
|
+
} finally {
|
|
877
|
+
this._drain_locked = false;
|
|
693
878
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
879
|
+
}
|
|
880
|
+
return { leased: [], acked: [], blocked: [] };
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Correlates streams using reaction resolvers.
|
|
884
|
+
* @param query - The query filter (e.g., by stream, event name, or starting point).
|
|
885
|
+
* @returns The leases of newly correlated streams, and the last seen event ID.
|
|
886
|
+
*/
|
|
887
|
+
async correlate(query = { after: -1, limit: 10 }) {
|
|
888
|
+
const correlated = /* @__PURE__ */ new Map();
|
|
889
|
+
let last_id = query.after || -1;
|
|
890
|
+
await store().query((event) => {
|
|
891
|
+
last_id = event.id;
|
|
892
|
+
const register = this.registry.events[event.name];
|
|
893
|
+
if (register) {
|
|
894
|
+
for (const reaction of register.reactions.values()) {
|
|
895
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
896
|
+
resolved && (correlated.get(resolved.target) || correlated.set(resolved.target, []).get(resolved.target)).push({ ...reaction, source: resolved.source, event });
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}, query);
|
|
900
|
+
if (correlated.size) {
|
|
901
|
+
const leases = [...correlated.entries()].map(([stream, payloads]) => ({
|
|
697
902
|
stream,
|
|
698
|
-
|
|
903
|
+
// TODO: by convention, the first defined source wins (this can be tricky)
|
|
904
|
+
source: payloads.find((p) => p.source)?.source || void 0,
|
|
905
|
+
by: randomUUID2(),
|
|
906
|
+
at: 0,
|
|
699
907
|
retry: 0,
|
|
700
|
-
|
|
908
|
+
payloads
|
|
701
909
|
}));
|
|
702
|
-
const leased = await store().lease(leases);
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
}),
|
|
741
|
-
{}
|
|
742
|
-
),
|
|
743
|
-
"\u26A1\uFE0F ack"
|
|
744
|
-
);
|
|
910
|
+
const leased = await store().lease(leases, 0);
|
|
911
|
+
leased.length && traceCorrelated(leased);
|
|
912
|
+
return { leased, last_id };
|
|
913
|
+
}
|
|
914
|
+
return { leased: [], last_id };
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Starts correlation worker that identifies and registers new streams using reaction resolvers.
|
|
918
|
+
*
|
|
919
|
+
* Enables "dynamic reactions", allowing streams to be auto-discovered based on event content.
|
|
920
|
+
* - Uses a correlation sliding window over the event stream to identify new streams.
|
|
921
|
+
* - Once registered, these streams are picked up by the main `drain` loop.
|
|
922
|
+
* - Users should have full control over their correlation strategy.
|
|
923
|
+
* - The starting point keeps increasing with each new batch of events.
|
|
924
|
+
* - Users are responsible for storing the last seen event ID.
|
|
925
|
+
*
|
|
926
|
+
* @param query - The query filter (e.g., by stream, event name, or starting point).
|
|
927
|
+
* @param frequency - The frequency of correlation checks (in milliseconds).
|
|
928
|
+
* @param callback - Callback to report stats (new strems, last seen event ID, etc.).
|
|
929
|
+
* @returns true if the correlation worker started, false otherwise (already started).
|
|
930
|
+
*/
|
|
931
|
+
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
932
|
+
if (this._correlation_interval) return false;
|
|
933
|
+
const limit = query.limit || 100;
|
|
934
|
+
let after = query.after || -1;
|
|
935
|
+
this._correlation_interval = setInterval(
|
|
936
|
+
() => this.correlate({ ...query, after, limit }).then((result) => {
|
|
937
|
+
after = result.last_id;
|
|
938
|
+
if (callback && result.leased.length) callback(result.leased);
|
|
939
|
+
}).catch(console.error),
|
|
940
|
+
frequency
|
|
941
|
+
);
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
stop_correlations() {
|
|
945
|
+
if (this._correlation_interval) {
|
|
946
|
+
clearInterval(this._correlation_interval);
|
|
947
|
+
this._correlation_interval = void 0;
|
|
745
948
|
}
|
|
746
|
-
this.drainLocked = false;
|
|
747
|
-
return drained.length;
|
|
748
949
|
}
|
|
749
950
|
};
|
|
750
951
|
|
|
751
952
|
// src/act-builder.ts
|
|
752
|
-
var _this_ = ({ stream }) =>
|
|
953
|
+
var _this_ = ({ stream }) => ({
|
|
954
|
+
source: stream,
|
|
955
|
+
target: stream
|
|
956
|
+
});
|
|
753
957
|
var _void_ = () => void 0;
|
|
754
958
|
function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
755
959
|
actions: {},
|
|
@@ -801,8 +1005,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
801
1005
|
resolver: _this_,
|
|
802
1006
|
options: {
|
|
803
1007
|
blockOnError: options?.blockOnError ?? true,
|
|
804
|
-
maxRetries: options?.maxRetries ?? 3
|
|
805
|
-
retryDelayMs: options?.retryDelayMs ?? 1e3
|
|
1008
|
+
maxRetries: options?.maxRetries ?? 3
|
|
806
1009
|
}
|
|
807
1010
|
};
|
|
808
1011
|
registry.events[event].reactions.set(handler.name, reaction);
|
|
@@ -811,7 +1014,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
811
1014
|
to(resolver) {
|
|
812
1015
|
registry.events[event].reactions.set(handler.name, {
|
|
813
1016
|
...reaction,
|
|
814
|
-
resolver
|
|
1017
|
+
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
815
1018
|
});
|
|
816
1019
|
return builder;
|
|
817
1020
|
},
|
|
@@ -825,7 +1028,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
825
1028
|
};
|
|
826
1029
|
}
|
|
827
1030
|
}),
|
|
828
|
-
build: (
|
|
1031
|
+
build: () => new Act(registry),
|
|
829
1032
|
events: registry.events
|
|
830
1033
|
};
|
|
831
1034
|
return builder;
|