@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.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,49 @@ var InMemoryStore = class {
|
|
|
320
365
|
});
|
|
321
366
|
}
|
|
322
367
|
/**
|
|
323
|
-
*
|
|
324
|
-
* @param limit - Maximum number of streams to
|
|
325
|
-
* @
|
|
368
|
+
* 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).
|
|
371
|
+
* @returns The polled streams.
|
|
326
372
|
*/
|
|
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 };
|
|
373
|
+
async poll(limit, descending = false) {
|
|
374
|
+
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 }));
|
|
339
376
|
}
|
|
340
377
|
/**
|
|
341
378
|
* Lease streams for processing (e.g., for distributed consumers).
|
|
342
|
-
* @param leases - Lease requests.
|
|
379
|
+
* @param leases - Lease requests for streams, including end-of-lease watermark, lease holder, and source stream.
|
|
380
|
+
* @param leaseMilis - Lease duration in milliseconds.
|
|
343
381
|
* @returns Granted leases.
|
|
344
382
|
*/
|
|
345
|
-
async lease(leases) {
|
|
383
|
+
async lease(leases, millis) {
|
|
346
384
|
await sleep();
|
|
347
|
-
return leases.map((
|
|
348
|
-
const
|
|
349
|
-
this._streams.set(
|
|
350
|
-
return
|
|
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);
|
|
351
389
|
}).filter((l) => !!l);
|
|
352
390
|
}
|
|
353
391
|
/**
|
|
354
392
|
* Acknowledge completion of processing for leased streams.
|
|
355
|
-
* @param leases - Leases to acknowledge.
|
|
393
|
+
* @param leases - Leases to acknowledge, including last processed watermark and lease holder.
|
|
356
394
|
*/
|
|
357
395
|
async ack(leases) {
|
|
358
396
|
await sleep();
|
|
359
|
-
leases.
|
|
397
|
+
return leases.filter(
|
|
398
|
+
(lease) => this._streams.get(lease.stream)?.ack(lease.at, lease.by)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
403
|
+
* @param leases - Leases to block, including lease holder and last error message.
|
|
404
|
+
* @returns Blocked leases.
|
|
405
|
+
*/
|
|
406
|
+
async block(leases) {
|
|
407
|
+
await sleep();
|
|
408
|
+
return leases.filter(
|
|
409
|
+
(lease) => this._streams.get(lease.stream)?.block(lease.by, lease.error)
|
|
410
|
+
);
|
|
360
411
|
}
|
|
361
412
|
};
|
|
362
413
|
|
|
@@ -405,6 +456,62 @@ var SNAP_EVENT = "__snapshot__";
|
|
|
405
456
|
var store = port(function store2(adapter) {
|
|
406
457
|
return adapter || new InMemoryStore();
|
|
407
458
|
});
|
|
459
|
+
function build_tracer(logLevel2) {
|
|
460
|
+
if (logLevel2 === "trace") {
|
|
461
|
+
return {
|
|
462
|
+
fetched: (fetched) => {
|
|
463
|
+
const data = Object.fromEntries(
|
|
464
|
+
fetched.map(({ stream, source, events }) => {
|
|
465
|
+
const key = source ? `${stream}<-${source}` : stream;
|
|
466
|
+
const value = Object.fromEntries(
|
|
467
|
+
events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
|
|
468
|
+
);
|
|
469
|
+
return [key, value];
|
|
470
|
+
})
|
|
471
|
+
);
|
|
472
|
+
logger.trace(data, "\u26A1\uFE0F fetch");
|
|
473
|
+
},
|
|
474
|
+
correlated: (leases) => {
|
|
475
|
+
const data = leases.map(({ stream }) => stream).join(" ");
|
|
476
|
+
logger.trace(`\u26A1\uFE0F correlate ${data}`);
|
|
477
|
+
},
|
|
478
|
+
leased: (leases) => {
|
|
479
|
+
const data = Object.fromEntries(
|
|
480
|
+
leases.map(({ stream, at, retry }) => [stream, { at, retry }])
|
|
481
|
+
);
|
|
482
|
+
logger.trace(data, "\u26A1\uFE0F lease");
|
|
483
|
+
},
|
|
484
|
+
acked: (leases) => {
|
|
485
|
+
const data = Object.fromEntries(
|
|
486
|
+
leases.map(({ stream, at, retry }) => [stream, { at, retry }])
|
|
487
|
+
);
|
|
488
|
+
logger.trace(data, "\u26A1\uFE0F ack");
|
|
489
|
+
},
|
|
490
|
+
blocked: (leases) => {
|
|
491
|
+
const data = Object.fromEntries(
|
|
492
|
+
leases.map(({ stream, at, retry, error }) => [
|
|
493
|
+
stream,
|
|
494
|
+
{ at, retry, error }
|
|
495
|
+
])
|
|
496
|
+
);
|
|
497
|
+
logger.trace(data, "\u26A1\uFE0F block");
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
} else {
|
|
501
|
+
return {
|
|
502
|
+
fetched: () => {
|
|
503
|
+
},
|
|
504
|
+
correlated: () => {
|
|
505
|
+
},
|
|
506
|
+
leased: () => {
|
|
507
|
+
},
|
|
508
|
+
acked: () => {
|
|
509
|
+
},
|
|
510
|
+
blocked: () => {
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
408
515
|
|
|
409
516
|
// src/signals.ts
|
|
410
517
|
process.once("SIGINT", async (arg) => {
|
|
@@ -466,21 +573,21 @@ async function load(me, stream, callback) {
|
|
|
466
573
|
}
|
|
467
574
|
callback && callback({ event, state: state2, patches, snaps });
|
|
468
575
|
},
|
|
469
|
-
{ stream }
|
|
470
|
-
true
|
|
576
|
+
{ stream, with_snaps: true }
|
|
471
577
|
);
|
|
472
|
-
logger.trace(
|
|
578
|
+
logger.trace(state2, `\u{1F7E2} load ${stream}`);
|
|
473
579
|
return { event, state: state2, patches, snaps };
|
|
474
580
|
}
|
|
475
581
|
async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
|
|
476
582
|
const { stream, expectedVersion, actor } = target;
|
|
477
583
|
if (!stream) throw new Error("Missing target stream");
|
|
478
584
|
payload = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
585
|
+
const snapshot = await load(me, stream);
|
|
586
|
+
const expected = expectedVersion || snapshot.event?.version;
|
|
479
587
|
logger.trace(
|
|
480
588
|
payload,
|
|
481
|
-
`\u{1F535} ${
|
|
589
|
+
`\u{1F535} ${stream}.${action2}${typeof expected === "number" ? `.${expected}` : ""}`
|
|
482
590
|
);
|
|
483
|
-
let snapshot = await load(me, stream);
|
|
484
591
|
if (me.given) {
|
|
485
592
|
const invariants = me.given[action2] || [];
|
|
486
593
|
invariants.forEach(({ valid, description }) => {
|
|
@@ -489,15 +596,15 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
489
596
|
action2,
|
|
490
597
|
payload,
|
|
491
598
|
target,
|
|
599
|
+
snapshot,
|
|
492
600
|
description
|
|
493
601
|
);
|
|
494
602
|
});
|
|
495
603
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (!result) return snapshot;
|
|
604
|
+
const result = me.on[action2](payload, snapshot, target);
|
|
605
|
+
if (!result) return [snapshot];
|
|
499
606
|
if (Array.isArray(result) && result.length === 0) {
|
|
500
|
-
return snapshot;
|
|
607
|
+
return [snapshot];
|
|
501
608
|
}
|
|
502
609
|
const tuples = Array.isArray(result[0]) ? result : [result];
|
|
503
610
|
const emitted = tuples.map(([name, data]) => ({
|
|
@@ -520,36 +627,47 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
520
627
|
} : void 0
|
|
521
628
|
}
|
|
522
629
|
};
|
|
630
|
+
logger.trace(
|
|
631
|
+
emitted.map((e) => e.data),
|
|
632
|
+
`\u{1F534} commit ${stream}.${emitted.map((e) => e.name).join(", ")}`
|
|
633
|
+
);
|
|
523
634
|
const committed = await store().commit(
|
|
524
635
|
stream,
|
|
525
636
|
emitted,
|
|
526
637
|
meta,
|
|
527
638
|
// TODO: review reactions not enforcing expected version
|
|
528
|
-
reactingTo ? void 0 :
|
|
639
|
+
reactingTo ? void 0 : expected
|
|
529
640
|
);
|
|
530
|
-
|
|
641
|
+
let { state: state2, patches } = snapshot;
|
|
642
|
+
const snapshots = committed.map((event) => {
|
|
531
643
|
state2 = patch(state2, me.patch[event.name](event, state2));
|
|
532
644
|
patches++;
|
|
533
|
-
logger.trace({ event, state: state2 }, "\u{1F534} commit");
|
|
534
645
|
return { event, state: state2, patches, snaps: snapshot.snaps };
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
|
|
646
|
+
});
|
|
647
|
+
const last = snapshots.at(-1);
|
|
648
|
+
me.snap && me.snap(last) && void snap(last);
|
|
649
|
+
return snapshots;
|
|
538
650
|
}
|
|
539
651
|
|
|
540
652
|
// src/act.ts
|
|
653
|
+
var tracer = build_tracer(config().logLevel);
|
|
541
654
|
var Act = class {
|
|
542
655
|
/**
|
|
543
656
|
* Create a new Act orchestrator.
|
|
544
657
|
*
|
|
545
658
|
* @param registry The registry of state, event, and action schemas
|
|
546
|
-
* @param drainLimit The maximum number of events to drain per cycle
|
|
547
659
|
*/
|
|
548
|
-
constructor(registry
|
|
660
|
+
constructor(registry) {
|
|
549
661
|
this.registry = registry;
|
|
550
|
-
|
|
662
|
+
dispose(() => {
|
|
663
|
+
this._emitter.removeAllListeners();
|
|
664
|
+
this.stop_correlations();
|
|
665
|
+
return Promise.resolve();
|
|
666
|
+
});
|
|
551
667
|
}
|
|
552
668
|
_emitter = new EventEmitter();
|
|
669
|
+
_drain_locked = false;
|
|
670
|
+
_correlation_interval = void 0;
|
|
553
671
|
emit(event, args) {
|
|
554
672
|
return this._emitter.emit(event, args);
|
|
555
673
|
}
|
|
@@ -576,16 +694,17 @@ var Act = class {
|
|
|
576
694
|
* await app.do("increment", { stream: "counter1", actor }, { by: 1 });
|
|
577
695
|
*/
|
|
578
696
|
async do(action2, target, payload, reactingTo, skipValidation = false) {
|
|
579
|
-
const
|
|
697
|
+
const snapshots = await action(
|
|
580
698
|
this.registry.actions[action2],
|
|
581
699
|
action2,
|
|
582
700
|
target,
|
|
583
701
|
payload,
|
|
702
|
+
// @ts-expect-error type lost
|
|
584
703
|
reactingTo,
|
|
585
704
|
skipValidation
|
|
586
705
|
);
|
|
587
|
-
this.emit("committed",
|
|
588
|
-
return
|
|
706
|
+
this.emit("committed", snapshots);
|
|
707
|
+
return snapshots;
|
|
589
708
|
}
|
|
590
709
|
/**
|
|
591
710
|
* Loads the current state snapshot for a given state machine and stream.
|
|
@@ -623,38 +742,58 @@ var Act = class {
|
|
|
623
742
|
}, query);
|
|
624
743
|
return { first, last, count };
|
|
625
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* Query the event store for events matching a filter.
|
|
747
|
+
* Use this version with caution, as it return events in memory.
|
|
748
|
+
*
|
|
749
|
+
* @param query The query filter (e.g., by stream, event name, or time range)
|
|
750
|
+
* @returns The matching events
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* const { count } = await app.query({ stream: "counter1" }, (event) => console.log(event));
|
|
754
|
+
*/
|
|
755
|
+
async query_array(query) {
|
|
756
|
+
const events = [];
|
|
757
|
+
await store().query((e) => events.push(e), query);
|
|
758
|
+
return events;
|
|
759
|
+
}
|
|
626
760
|
/**
|
|
627
761
|
* Handles leased reactions.
|
|
628
762
|
*
|
|
763
|
+
* This is called by the main `drain` loop after fetching new events.
|
|
764
|
+
* It handles reactions, supporting retries, blocking, and error handling.
|
|
765
|
+
*
|
|
629
766
|
* @internal
|
|
630
767
|
* @param lease The lease to handle
|
|
631
|
-
* @param
|
|
632
|
-
* @returns The lease
|
|
768
|
+
* @param payloads The reactions to handle
|
|
769
|
+
* @returns The lease with results
|
|
633
770
|
*/
|
|
634
|
-
async handle(lease,
|
|
771
|
+
async handle(lease, payloads) {
|
|
772
|
+
if (payloads.length === 0) return { lease, at: lease.at };
|
|
635
773
|
const stream = lease.stream;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
774
|
+
let at = payloads.at(0).event.id, handled = 0;
|
|
775
|
+
lease.retry > 0 && logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
776
|
+
for (const payload of payloads) {
|
|
777
|
+
const { event, handler, options } = payload;
|
|
639
778
|
try {
|
|
640
779
|
await handler(event, stream);
|
|
641
|
-
|
|
780
|
+
at = event.id;
|
|
781
|
+
handled++;
|
|
642
782
|
} catch (error) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
783
|
+
logger.error(error);
|
|
784
|
+
const block = lease.retry >= options.maxRetries && options.blockOnError;
|
|
785
|
+
block && logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
|
|
786
|
+
return {
|
|
787
|
+
lease,
|
|
788
|
+
at,
|
|
789
|
+
// only report error when nothing was handled
|
|
790
|
+
error: handled === 0 ? error.message : void 0,
|
|
791
|
+
block
|
|
792
|
+
};
|
|
653
793
|
}
|
|
654
794
|
}
|
|
655
|
-
return lease;
|
|
795
|
+
return { lease, at };
|
|
656
796
|
}
|
|
657
|
-
drainLocked = false;
|
|
658
797
|
/**
|
|
659
798
|
* Drains and processes events from the store, triggering reactions and updating state.
|
|
660
799
|
*
|
|
@@ -665,91 +804,173 @@ var Act = class {
|
|
|
665
804
|
* @example
|
|
666
805
|
* await app.drain();
|
|
667
806
|
*/
|
|
668
|
-
async drain(
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
807
|
+
async drain({
|
|
808
|
+
streamLimit = 10,
|
|
809
|
+
eventLimit = 10,
|
|
810
|
+
leaseMillis = 1e4,
|
|
811
|
+
descending = false
|
|
812
|
+
} = {}) {
|
|
813
|
+
if (!this._drain_locked) {
|
|
814
|
+
try {
|
|
815
|
+
this._drain_locked = true;
|
|
816
|
+
const polled = await store().poll(streamLimit, descending);
|
|
817
|
+
const fetched = await Promise.all(
|
|
818
|
+
polled.map(async ({ stream, source, at }) => {
|
|
819
|
+
const events = await this.query_array({
|
|
820
|
+
stream: source,
|
|
821
|
+
after: at,
|
|
822
|
+
limit: eventLimit
|
|
823
|
+
});
|
|
824
|
+
return { stream, source, events };
|
|
825
|
+
})
|
|
826
|
+
);
|
|
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) {
|
|
836
|
+
const leases = /* @__PURE__ */ new Map();
|
|
837
|
+
fetched.forEach(({ stream, events }) => {
|
|
838
|
+
const payloads = events.flatMap((event) => {
|
|
839
|
+
const register = this.registry.events[event.name];
|
|
840
|
+
if (!register) return [];
|
|
841
|
+
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;
|
|
846
|
+
return resolved && resolved.target === stream;
|
|
847
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
848
|
+
});
|
|
849
|
+
leases.set(stream, {
|
|
850
|
+
lease: {
|
|
851
|
+
stream,
|
|
852
|
+
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
|
|
856
|
+
},
|
|
857
|
+
// @ts-expect-error indexed by key
|
|
858
|
+
payloads
|
|
859
|
+
});
|
|
860
|
+
});
|
|
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
|
+
}
|
|
691
889
|
}
|
|
692
890
|
}
|
|
891
|
+
} catch (error) {
|
|
892
|
+
logger.error(error);
|
|
893
|
+
} finally {
|
|
894
|
+
this._drain_locked = false;
|
|
693
895
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
896
|
+
}
|
|
897
|
+
return { leased: [], acked: [], blocked: [] };
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Correlates streams using reaction resolvers.
|
|
901
|
+
* @param query - The query filter (e.g., by stream, event name, or starting point).
|
|
902
|
+
* @returns The leases of newly correlated streams, and the last seen event ID.
|
|
903
|
+
*/
|
|
904
|
+
async correlate(query = { after: -1, limit: 10 }) {
|
|
905
|
+
const correlated = /* @__PURE__ */ new Map();
|
|
906
|
+
let last_id = query.after || -1;
|
|
907
|
+
await store().query((event) => {
|
|
908
|
+
last_id = event.id;
|
|
909
|
+
const register = this.registry.events[event.name];
|
|
910
|
+
if (register) {
|
|
911
|
+
for (const reaction of register.reactions.values()) {
|
|
912
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
913
|
+
resolved && (correlated.get(resolved.target) || correlated.set(resolved.target, []).get(resolved.target)).push({ ...reaction, source: resolved.source, event });
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}, query);
|
|
917
|
+
if (correlated.size) {
|
|
918
|
+
const leases = [...correlated.entries()].map(([stream, payloads]) => ({
|
|
697
919
|
stream,
|
|
698
|
-
|
|
920
|
+
// TODO: by convention, the first defined source wins (this can be tricky)
|
|
921
|
+
source: payloads.find((p) => p.source)?.source || void 0,
|
|
922
|
+
by: randomUUID2(),
|
|
923
|
+
at: 0,
|
|
699
924
|
retry: 0,
|
|
700
|
-
|
|
925
|
+
payloads
|
|
701
926
|
}));
|
|
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
|
-
);
|
|
927
|
+
const leased = await store().lease(leases, 0);
|
|
928
|
+
leased.length && tracer.correlated(leased);
|
|
929
|
+
return { leased, last_id };
|
|
930
|
+
}
|
|
931
|
+
return { leased: [], last_id };
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Starts correlation worker that identifies and registers new streams using reaction resolvers.
|
|
935
|
+
*
|
|
936
|
+
* Enables "dynamic reactions", allowing streams to be auto-discovered based on event content.
|
|
937
|
+
* - Uses a correlation sliding window over the event stream to identify new streams.
|
|
938
|
+
* - Once registered, these streams are picked up by the main `drain` loop.
|
|
939
|
+
* - Users should have full control over their correlation strategy.
|
|
940
|
+
* - The starting point keeps increasing with each new batch of events.
|
|
941
|
+
* - Users are responsible for storing the last seen event ID.
|
|
942
|
+
*
|
|
943
|
+
* @param query - The query filter (e.g., by stream, event name, or starting point).
|
|
944
|
+
* @param frequency - The frequency of correlation checks (in milliseconds).
|
|
945
|
+
* @param callback - Callback to report stats (new strems, last seen event ID, etc.).
|
|
946
|
+
* @returns true if the correlation worker started, false otherwise (already started).
|
|
947
|
+
*/
|
|
948
|
+
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
949
|
+
if (this._correlation_interval) return false;
|
|
950
|
+
const limit = query.limit || 100;
|
|
951
|
+
let after = query.after || -1;
|
|
952
|
+
this._correlation_interval = setInterval(
|
|
953
|
+
() => this.correlate({ ...query, after, limit }).then((result) => {
|
|
954
|
+
after = result.last_id;
|
|
955
|
+
if (callback && result.leased.length) callback(result.leased);
|
|
956
|
+
}).catch(console.error),
|
|
957
|
+
frequency
|
|
958
|
+
);
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
stop_correlations() {
|
|
962
|
+
if (this._correlation_interval) {
|
|
963
|
+
clearInterval(this._correlation_interval);
|
|
964
|
+
this._correlation_interval = void 0;
|
|
745
965
|
}
|
|
746
|
-
this.drainLocked = false;
|
|
747
|
-
return drained.length;
|
|
748
966
|
}
|
|
749
967
|
};
|
|
750
968
|
|
|
751
969
|
// src/act-builder.ts
|
|
752
|
-
var _this_ = ({ stream }) =>
|
|
970
|
+
var _this_ = ({ stream }) => ({
|
|
971
|
+
source: stream,
|
|
972
|
+
target: stream
|
|
973
|
+
});
|
|
753
974
|
var _void_ = () => void 0;
|
|
754
975
|
function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
755
976
|
actions: {},
|
|
@@ -801,8 +1022,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
801
1022
|
resolver: _this_,
|
|
802
1023
|
options: {
|
|
803
1024
|
blockOnError: options?.blockOnError ?? true,
|
|
804
|
-
maxRetries: options?.maxRetries ?? 3
|
|
805
|
-
retryDelayMs: options?.retryDelayMs ?? 1e3
|
|
1025
|
+
maxRetries: options?.maxRetries ?? 3
|
|
806
1026
|
}
|
|
807
1027
|
};
|
|
808
1028
|
registry.events[event].reactions.set(handler.name, reaction);
|
|
@@ -811,7 +1031,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
811
1031
|
to(resolver) {
|
|
812
1032
|
registry.events[event].reactions.set(handler.name, {
|
|
813
1033
|
...reaction,
|
|
814
|
-
resolver
|
|
1034
|
+
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
815
1035
|
});
|
|
816
1036
|
return builder;
|
|
817
1037
|
},
|
|
@@ -825,7 +1045,7 @@ function act(states = /* @__PURE__ */ new Set(), registry = {
|
|
|
825
1045
|
};
|
|
826
1046
|
}
|
|
827
1047
|
}),
|
|
828
|
-
build: (
|
|
1048
|
+
build: () => new Act(registry),
|
|
829
1049
|
events: registry.events
|
|
830
1050
|
};
|
|
831
1051
|
return builder;
|
|
@@ -905,6 +1125,7 @@ export {
|
|
|
905
1125
|
ValidationError,
|
|
906
1126
|
ZodEmpty,
|
|
907
1127
|
act,
|
|
1128
|
+
build_tracer,
|
|
908
1129
|
config,
|
|
909
1130
|
dispose,
|
|
910
1131
|
disposeAndExit,
|