@rotorsoft/act 0.2.0 → 0.4.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.
Files changed (85) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act-builder.d.ts +32 -0
  3. package/dist/@types/act-builder.d.ts.map +1 -0
  4. package/dist/@types/act.d.ts +75 -0
  5. package/dist/@types/act.d.ts.map +1 -0
  6. package/dist/{adapters → @types/adapters}/InMemoryStore.d.ts +1 -1
  7. package/dist/@types/adapters/InMemoryStore.d.ts.map +1 -0
  8. package/dist/{config.d.ts → @types/config.d.ts} +4 -0
  9. package/dist/@types/config.d.ts.map +1 -0
  10. package/dist/@types/event-sourcing.d.ts +41 -0
  11. package/dist/@types/event-sourcing.d.ts.map +1 -0
  12. package/dist/@types/index.d.ts +13 -0
  13. package/dist/@types/index.d.ts.map +1 -0
  14. package/dist/{ports.d.ts → @types/ports.d.ts} +5 -1
  15. package/dist/@types/ports.d.ts.map +1 -0
  16. package/dist/@types/state-builder.d.ts +57 -0
  17. package/dist/@types/state-builder.d.ts.map +1 -0
  18. package/dist/{types → @types/types}/action.d.ts +26 -24
  19. package/dist/@types/types/action.d.ts.map +1 -0
  20. package/dist/{types → @types/types}/errors.d.ts +1 -1
  21. package/dist/@types/types/errors.d.ts.map +1 -0
  22. package/dist/{types → @types/types}/index.d.ts +6 -6
  23. package/dist/@types/types/index.d.ts.map +1 -0
  24. package/dist/{types → @types/types}/ports.d.ts +2 -2
  25. package/dist/@types/types/ports.d.ts.map +1 -0
  26. package/dist/{types → @types/types}/reaction.d.ts +1 -1
  27. package/dist/@types/types/reaction.d.ts.map +1 -0
  28. package/dist/{types → @types/types}/registry.d.ts +4 -4
  29. package/dist/@types/types/registry.d.ts.map +1 -0
  30. package/dist/{types → @types/types}/schemas.d.ts +4 -4
  31. package/dist/@types/types/schemas.d.ts.map +1 -0
  32. package/dist/{utils.d.ts → @types/utils.d.ts} +4 -1
  33. package/dist/@types/utils.d.ts.map +1 -0
  34. package/dist/index.cjs +928 -0
  35. package/dist/index.cjs.map +1 -0
  36. package/dist/index.js +854 -18
  37. package/dist/index.js.map +1 -1
  38. package/package.json +17 -5
  39. package/dist/act.d.ts +0 -24
  40. package/dist/act.d.ts.map +0 -1
  41. package/dist/act.js +0 -136
  42. package/dist/act.js.map +0 -1
  43. package/dist/adapters/InMemoryStore.d.ts.map +0 -1
  44. package/dist/adapters/InMemoryStore.js +0 -125
  45. package/dist/adapters/InMemoryStore.js.map +0 -1
  46. package/dist/builder.d.ts +0 -17
  47. package/dist/builder.d.ts.map +0 -1
  48. package/dist/builder.js +0 -70
  49. package/dist/builder.js.map +0 -1
  50. package/dist/config.d.ts.map +0 -1
  51. package/dist/config.js +0 -41
  52. package/dist/config.js.map +0 -1
  53. package/dist/event-sourcing.d.ts +0 -5
  54. package/dist/event-sourcing.d.ts.map +0 -1
  55. package/dist/event-sourcing.js +0 -101
  56. package/dist/event-sourcing.js.map +0 -1
  57. package/dist/index.d.ts +0 -9
  58. package/dist/index.d.ts.map +0 -1
  59. package/dist/ports.d.ts.map +0 -1
  60. package/dist/ports.js +0 -56
  61. package/dist/ports.js.map +0 -1
  62. package/dist/types/action.d.ts.map +0 -1
  63. package/dist/types/action.js +0 -2
  64. package/dist/types/action.js.map +0 -1
  65. package/dist/types/errors.d.ts.map +0 -1
  66. package/dist/types/errors.js +0 -44
  67. package/dist/types/errors.js.map +0 -1
  68. package/dist/types/index.d.ts.map +0 -1
  69. package/dist/types/index.js +0 -17
  70. package/dist/types/index.js.map +0 -1
  71. package/dist/types/ports.d.ts.map +0 -1
  72. package/dist/types/ports.js +0 -2
  73. package/dist/types/ports.js.map +0 -1
  74. package/dist/types/reaction.d.ts.map +0 -1
  75. package/dist/types/reaction.js +0 -2
  76. package/dist/types/reaction.js.map +0 -1
  77. package/dist/types/registry.d.ts.map +0 -1
  78. package/dist/types/registry.js +0 -2
  79. package/dist/types/registry.js.map +0 -1
  80. package/dist/types/schemas.d.ts.map +0 -1
  81. package/dist/types/schemas.js +0 -81
  82. package/dist/types/schemas.js.map +0 -1
  83. package/dist/utils.d.ts.map +0 -1
  84. package/dist/utils.js +0 -73
  85. package/dist/utils.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,27 +1,863 @@
1
- import { config } from "./config";
2
- import { disposeAndExit, logger } from "./ports";
3
- /** @module act */
4
- export * from "./act";
5
- export * from "./builder";
6
- export * from "./ports";
7
- export * from "./types";
8
- export * from "./utils";
9
- export { config };
10
- // exit on signals
1
+ // src/config.ts
2
+ import * as dotenv from "dotenv";
3
+ import * as fs from "fs";
4
+ import { z as z2 } from "zod/v4";
5
+
6
+ // src/types/errors.ts
7
+ var Errors = {
8
+ ValidationError: "ERR_VALIDATION",
9
+ InvariantError: "ERR_INVARIANT",
10
+ ConcurrencyError: "ERR_CONCURRENCY"
11
+ };
12
+ var ValidationError = class extends Error {
13
+ constructor(target, payload, details) {
14
+ super(`Invalid ${target} payload`);
15
+ this.target = target;
16
+ this.payload = payload;
17
+ this.details = details;
18
+ this.name = Errors.ValidationError;
19
+ }
20
+ };
21
+ var InvariantError = class extends Error {
22
+ details;
23
+ constructor(name, payload, target, description) {
24
+ super(`${name} failed invariant: ${description}`);
25
+ this.name = Errors.InvariantError;
26
+ this.details = { name, payload, target, description };
27
+ }
28
+ };
29
+ var ConcurrencyError = class extends Error {
30
+ constructor(lastVersion, events, expectedVersion) {
31
+ super(
32
+ `Concurrency error committing event "${events.at(0)?.name}". Expected version ${expectedVersion} but found version ${lastVersion}.`
33
+ );
34
+ this.lastVersion = lastVersion;
35
+ this.events = events;
36
+ this.expectedVersion = expectedVersion;
37
+ this.name = Errors.ConcurrencyError;
38
+ }
39
+ };
40
+
41
+ // src/types/schemas.ts
42
+ import { z } from "zod/v4";
43
+ var ZodEmpty = z.record(z.string(), z.never());
44
+ var ActorSchema = z.object({
45
+ id: z.string(),
46
+ name: z.string()
47
+ }).readonly();
48
+ var TargetSchema = z.object({
49
+ stream: z.string(),
50
+ actor: ActorSchema,
51
+ expectedVersion: z.number().optional()
52
+ }).readonly();
53
+ var CausationEventSchema = z.object({
54
+ id: z.number(),
55
+ name: z.string(),
56
+ stream: z.string()
57
+ });
58
+ var EventMetaSchema = z.object({
59
+ correlation: z.string(),
60
+ causation: z.object({
61
+ action: TargetSchema.and(z.object({ name: z.string() })).optional(),
62
+ event: CausationEventSchema.optional()
63
+ })
64
+ }).readonly();
65
+ var CommittedMetaSchema = z.object({
66
+ id: z.number(),
67
+ stream: z.string(),
68
+ version: z.number(),
69
+ created: z.date(),
70
+ meta: EventMetaSchema
71
+ }).readonly();
72
+ function buildSnapshotSchema(s) {
73
+ const events = Object.entries(s.events).map(
74
+ ([name, zod]) => z.object({
75
+ name: z.literal(name),
76
+ data: zod,
77
+ id: z.number(),
78
+ stream: z.string(),
79
+ version: z.number(),
80
+ created: z.date(),
81
+ meta: EventMetaSchema
82
+ })
83
+ );
84
+ return z.object({
85
+ state: s.state.readonly(),
86
+ event: z.union([events[0], events[1], ...events.slice(2)]).optional(),
87
+ patches: z.number(),
88
+ snaps: z.number()
89
+ });
90
+ }
91
+ var QuerySchema = z.object({
92
+ stream: z.string().optional(),
93
+ names: z.string().array().optional(),
94
+ before: z.number().optional(),
95
+ after: z.number().optional(),
96
+ limit: z.number().optional(),
97
+ created_before: z.date().optional(),
98
+ created_after: z.date().optional(),
99
+ backward: z.boolean().optional(),
100
+ correlation: z.string().optional()
101
+ });
102
+
103
+ // src/types/index.ts
104
+ var Environments = [
105
+ "development",
106
+ "test",
107
+ "staging",
108
+ "production"
109
+ ];
110
+ var LogLevels = [
111
+ "fatal",
112
+ "error",
113
+ "warn",
114
+ "info",
115
+ "debug",
116
+ "trace"
117
+ ];
118
+
119
+ // src/utils.ts
120
+ import { prettifyError } from "zod/v4";
121
+ var UNMERGEABLES = [
122
+ RegExp,
123
+ Date,
124
+ Array,
125
+ Map,
126
+ Set,
127
+ WeakMap,
128
+ WeakSet,
129
+ ArrayBuffer,
130
+ SharedArrayBuffer,
131
+ DataView,
132
+ Int8Array,
133
+ Uint8Array,
134
+ Uint8ClampedArray,
135
+ Int16Array,
136
+ Uint16Array,
137
+ Int32Array,
138
+ Uint32Array,
139
+ Float32Array,
140
+ Float64Array
141
+ ];
142
+ var is_mergeable = (value) => !!value && typeof value === "object" && !UNMERGEABLES.some((t) => value instanceof t);
143
+ var patch = (original, patches) => {
144
+ const copy = {};
145
+ Object.keys({ ...original, ...patches }).forEach((key) => {
146
+ const patched_value = patches[key];
147
+ const original_value = original[key];
148
+ const patched = patches && key in patches;
149
+ const deleted = patched && (typeof patched_value === "undefined" || patched_value === null);
150
+ const value = patched && !deleted ? patched_value : original_value;
151
+ !deleted && (copy[key] = is_mergeable(value) ? patch(original_value || {}, patched_value || {}) : value);
152
+ });
153
+ return copy;
154
+ };
155
+ var validate = (target, payload, schema) => {
156
+ try {
157
+ return schema ? schema.parse(payload) : payload;
158
+ } catch (error) {
159
+ if (error instanceof Error && error.name === "ZodError") {
160
+ throw new ValidationError(
161
+ target,
162
+ payload,
163
+ prettifyError(error)
164
+ );
165
+ }
166
+ throw new ValidationError(target, payload, error);
167
+ }
168
+ };
169
+ var extend = (source, schema, target) => {
170
+ const value = validate("config", source, schema);
171
+ return Object.assign(target || {}, value);
172
+ };
173
+ async function sleep(ms) {
174
+ return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
175
+ }
176
+
177
+ // src/config.ts
178
+ dotenv.config();
179
+ var PackageSchema = z2.object({
180
+ name: z2.string().min(1),
181
+ version: z2.string().min(1),
182
+ description: z2.string().min(1),
183
+ author: z2.object({ name: z2.string().min(1), email: z2.string().optional() }).or(z2.string().min(1)),
184
+ license: z2.string().min(1),
185
+ dependencies: z2.record(z2.string(), z2.string())
186
+ });
187
+ var getPackage = () => {
188
+ const pkg2 = fs.readFileSync("package.json");
189
+ return JSON.parse(pkg2.toString());
190
+ };
191
+ var BaseSchema = PackageSchema.extend({
192
+ env: z2.enum(Environments),
193
+ logLevel: z2.enum(LogLevels),
194
+ logSingleLine: z2.boolean(),
195
+ sleepMs: z2.number().int().min(0).max(5e3)
196
+ });
197
+ var { NODE_ENV, LOG_LEVEL, LOG_SINGLE_LINE, SLEEP_MS } = process.env;
198
+ var env = NODE_ENV || "development";
199
+ var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "error" : LOG_LEVEL === "production" ? "info" : "trace");
200
+ var logSingleLine = (LOG_SINGLE_LINE || "true") === "true";
201
+ var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100");
202
+ var pkg = getPackage();
203
+ var config = () => {
204
+ return extend({ ...pkg, env, logLevel, logSingleLine, sleepMs }, BaseSchema);
205
+ };
206
+
207
+ // src/ports.ts
208
+ import { pino } from "pino";
209
+
210
+ // src/adapters/InMemoryStore.ts
211
+ var InMemoryStream = class {
212
+ constructor(stream) {
213
+ this.stream = stream;
214
+ }
215
+ _at = -1;
216
+ _retry = -1;
217
+ _lease;
218
+ _blocked = false;
219
+ lease(lease) {
220
+ if (!this._blocked && lease.at > this._at) {
221
+ this._lease = { ...lease, retry: this._retry + 1 };
222
+ return this._lease;
223
+ }
224
+ }
225
+ ack(lease) {
226
+ if (this._lease && lease.at >= this._at) {
227
+ this._retry = lease.retry;
228
+ this._blocked = lease.block;
229
+ if (!this._retry && !this._blocked) {
230
+ this._at = lease.at;
231
+ }
232
+ this._lease = void 0;
233
+ }
234
+ }
235
+ };
236
+ var InMemoryStore = class {
237
+ // stored events
238
+ _events = [];
239
+ // stored stream positions and other metadata
240
+ _streams = /* @__PURE__ */ new Map();
241
+ async dispose() {
242
+ await sleep();
243
+ this._events.length = 0;
244
+ }
245
+ async seed() {
246
+ await sleep();
247
+ }
248
+ async drop() {
249
+ await sleep();
250
+ this._events.length = 0;
251
+ }
252
+ async query(callback, query) {
253
+ await sleep();
254
+ const {
255
+ stream,
256
+ names,
257
+ before,
258
+ after = -1,
259
+ limit,
260
+ created_before,
261
+ created_after,
262
+ correlation
263
+ } = query || {};
264
+ let i = after + 1, count = 0;
265
+ while (i < this._events.length) {
266
+ const e = this._events[i++];
267
+ if (stream && e.stream !== stream) continue;
268
+ if (names && !names.includes(e.name)) continue;
269
+ if (correlation && e.meta?.correlation !== correlation) continue;
270
+ if (created_after && e.created <= created_after) continue;
271
+ if (before && e.id >= before) break;
272
+ if (created_before && e.created >= created_before) break;
273
+ callback(e);
274
+ count++;
275
+ if (limit && count >= limit) break;
276
+ }
277
+ return count;
278
+ }
279
+ async commit(stream, msgs, meta, expectedVersion) {
280
+ await sleep();
281
+ const instance = this._events.filter((e) => e.stream === stream);
282
+ if (expectedVersion && instance.length - 1 !== expectedVersion)
283
+ throw new ConcurrencyError(
284
+ instance.length - 1,
285
+ msgs,
286
+ expectedVersion
287
+ );
288
+ let version = instance.length;
289
+ return msgs.map(({ name, data }) => {
290
+ const committed = {
291
+ id: this._events.length,
292
+ stream,
293
+ version,
294
+ created: /* @__PURE__ */ new Date(),
295
+ name,
296
+ data,
297
+ meta
298
+ };
299
+ this._events.push(committed);
300
+ version++;
301
+ return committed;
302
+ });
303
+ }
304
+ /**
305
+ * Fetches new events from stream watermarks
306
+ */
307
+ async fetch(limit) {
308
+ const streams = [...this._streams.values()].filter((s) => !s._blocked).sort((a, b) => a._at - b._at).slice(0, limit);
309
+ const after = streams.length ? streams.reduce(
310
+ (min, s) => Math.min(min, s._at),
311
+ Number.MAX_SAFE_INTEGER
312
+ ) : -1;
313
+ const events = [];
314
+ await this.query((e) => events.push(e), { after, limit });
315
+ return { streams: streams.map(({ stream }) => stream), events };
316
+ }
317
+ async lease(leases) {
318
+ await sleep();
319
+ return leases.map((lease) => {
320
+ const stream = this._streams.get(lease.stream) || // store new correlations
321
+ this._streams.set(lease.stream, new InMemoryStream(lease.stream)).get(lease.stream);
322
+ return stream.lease(lease);
323
+ }).filter((l) => !!l);
324
+ }
325
+ async ack(leases) {
326
+ await sleep();
327
+ leases.forEach((lease) => this._streams.get(lease.stream)?.ack(lease));
328
+ }
329
+ };
330
+
331
+ // src/ports.ts
332
+ var ExitCodes = ["ERROR", "EXIT"];
333
+ var logger = pino({
334
+ transport: config().env !== "production" ? {
335
+ target: "pino-pretty",
336
+ options: {
337
+ ignore: "pid,hostname",
338
+ singleLine: config().logSingleLine,
339
+ colorize: true
340
+ }
341
+ } : void 0,
342
+ level: config().logLevel
343
+ });
344
+ var adapters = /* @__PURE__ */ new Map();
345
+ function port(injector) {
346
+ return function(adapter) {
347
+ if (!adapters.has(injector.name)) {
348
+ const injected = injector(adapter);
349
+ adapters.set(injector.name, injected);
350
+ logger.info(`\u{1F50C} injected ${injector.name}:${injected.constructor.name}`);
351
+ }
352
+ return adapters.get(injector.name);
353
+ };
354
+ }
355
+ var disposers = [];
356
+ async function disposeAndExit(code = "EXIT") {
357
+ if (code === "ERROR" && config().env === "production") return;
358
+ await Promise.all(disposers.map((disposer) => disposer()));
359
+ await Promise.all(
360
+ [...adapters.values()].reverse().map(async (adapter) => {
361
+ await adapter.dispose();
362
+ logger.info(`\u{1F50C} disposed ${adapter.constructor.name}`);
363
+ })
364
+ );
365
+ adapters.clear();
366
+ config().env !== "test" && process.exit(code === "ERROR" ? 1 : 0);
367
+ }
368
+ function dispose(disposer) {
369
+ disposer && disposers.push(disposer);
370
+ return disposeAndExit;
371
+ }
372
+ var SNAP_EVENT = "__snapshot__";
373
+ var store = port(function store2(adapter) {
374
+ return adapter || new InMemoryStore();
375
+ });
376
+
377
+ // src/act.ts
378
+ import { randomUUID as randomUUID2 } from "crypto";
379
+ import EventEmitter from "events";
380
+
381
+ // src/event-sourcing.ts
382
+ import { randomUUID } from "crypto";
383
+ async function snap(snapshot) {
384
+ try {
385
+ const { id, stream, name, meta, version } = snapshot.event;
386
+ const snapped = await store().commit(
387
+ stream,
388
+ [{ name: SNAP_EVENT, data: snapshot.state }],
389
+ {
390
+ correlation: meta.correlation,
391
+ causation: { event: { id, name, stream } }
392
+ },
393
+ version
394
+ // IMPORTANT! - state events are committed right after the snapshot event
395
+ );
396
+ logger.trace(snapped, "\u{1F7E0} snap");
397
+ } catch (error) {
398
+ logger.error(error);
399
+ }
400
+ }
401
+ async function load(me, stream, callback) {
402
+ let state2 = me.init ? me.init() : {};
403
+ let patches = 0;
404
+ let snaps = 0;
405
+ let event;
406
+ await store().query(
407
+ (e) => {
408
+ event = e;
409
+ if (e.name === SNAP_EVENT) {
410
+ state2 = e.data;
411
+ snaps++;
412
+ patches = 0;
413
+ } else if (me.patch[e.name]) {
414
+ state2 = patch(state2, me.patch[e.name](event, state2));
415
+ patches++;
416
+ }
417
+ callback && callback({ event, state: state2, patches, snaps });
418
+ },
419
+ { stream },
420
+ true
421
+ );
422
+ logger.trace({ stream, patches, snaps, state: state2 }, "\u{1F7E2} load");
423
+ return { event, state: state2, patches, snaps };
424
+ }
425
+ async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
426
+ const { stream, expectedVersion, actor } = target;
427
+ if (!stream) throw new Error("Missing target stream");
428
+ payload = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
429
+ logger.trace(
430
+ payload,
431
+ `\u{1F535} ${action2} "${stream}${expectedVersion ? `@${expectedVersion}` : ""}"`
432
+ );
433
+ let snapshot = await load(me, stream);
434
+ if (me.given) {
435
+ const invariants = me.given[action2] || [];
436
+ invariants.forEach(({ valid, description }) => {
437
+ if (!valid(snapshot.state, actor))
438
+ throw new InvariantError(
439
+ action2,
440
+ payload,
441
+ target,
442
+ description
443
+ );
444
+ });
445
+ }
446
+ let { state: state2, patches } = snapshot;
447
+ const result = me.on[action2](payload, state2, target);
448
+ if (!result) return snapshot;
449
+ if (Array.isArray(result) && result.length === 0) {
450
+ return snapshot;
451
+ }
452
+ const tuples = Array.isArray(result[0]) ? result : [result];
453
+ const emitted = tuples.map(([name, data]) => ({
454
+ name,
455
+ data: skipValidation ? data : validate(name, data, me.events[name])
456
+ }));
457
+ const meta = {
458
+ correlation: reactingTo?.meta.correlation || randomUUID(),
459
+ causation: {
460
+ action: {
461
+ name: action2,
462
+ ...target
463
+ // payload: TODO: flag to include action payload in metadata
464
+ // not included by default to avoid large payloads
465
+ },
466
+ event: reactingTo ? {
467
+ id: reactingTo.id,
468
+ name: reactingTo.name,
469
+ stream: reactingTo.stream
470
+ } : void 0
471
+ }
472
+ };
473
+ const committed = await store().commit(
474
+ stream,
475
+ emitted,
476
+ meta,
477
+ // TODO: review reactions not enforcing expected version
478
+ reactingTo ? void 0 : expectedVersion || snapshot.event?.version
479
+ );
480
+ snapshot = committed.map((event) => {
481
+ state2 = patch(state2, me.patch[event.name](event, state2));
482
+ patches++;
483
+ logger.trace({ event, state: state2 }, "\u{1F534} commit");
484
+ return { event, state: state2, patches, snaps: snapshot.snaps };
485
+ }).at(-1);
486
+ me.snap && me.snap(snapshot) && void snap(snapshot);
487
+ return snapshot;
488
+ }
489
+
490
+ // src/act.ts
491
+ var Act = class {
492
+ constructor(registry, drainLimit) {
493
+ this.registry = registry;
494
+ this.drainLimit = drainLimit;
495
+ }
496
+ _emitter = new EventEmitter();
497
+ emit(event, args) {
498
+ return this._emitter.emit(event, args);
499
+ }
500
+ on(event, listener) {
501
+ this._emitter.on(event, listener);
502
+ return this;
503
+ }
504
+ /**
505
+ * Executes an action and emits an event to be committed by the store.
506
+ *
507
+ * @template K The type of action to execute
508
+ * @template T The type of target
509
+ * @template P The type of payloads
510
+ * @param action The action to execute
511
+ * @param target The target of the action
512
+ * @param payload The payload of the action
513
+ * @param reactingTo The event that the action is reacting to
514
+ * @param skipValidation Whether to skip validation
515
+ * @returns The snapshot of the committed Event
516
+ */
517
+ async do(action2, target, payload, reactingTo, skipValidation = false) {
518
+ const snapshot = await action(
519
+ this.registry.actions[action2],
520
+ action2,
521
+ target,
522
+ payload,
523
+ reactingTo,
524
+ skipValidation
525
+ );
526
+ this.emit("committed", snapshot);
527
+ return snapshot;
528
+ }
529
+ /**
530
+ * Loads a snapshot of the state from the store.
531
+ *
532
+ * @template SX The type of state
533
+ * @template EX The type of events
534
+ * @template AX The type of actions
535
+ * @param state The state to load
536
+ * @param stream The stream to load
537
+ * @param callback The callback to call with the snapshot
538
+ * @returns The snapshot of the loaded state
539
+ */
540
+ async load(state2, stream, callback) {
541
+ return await load(state2, stream, callback);
542
+ }
543
+ /**
544
+ * Queries the store for events.
545
+ *
546
+ * @param query The query to execute
547
+ * @param callback The callback to call with the events
548
+ * @returns The query result
549
+ */
550
+ async query(query, callback) {
551
+ let first = void 0, last = void 0;
552
+ const count = await store().query((e) => {
553
+ !first && (first = e);
554
+ last = e;
555
+ callback && callback(e);
556
+ }, query);
557
+ return { first, last, count };
558
+ }
559
+ /**
560
+ * Handles leased reactions.
561
+ *
562
+ * @param lease The lease to handle
563
+ * @param reactions The reactions to handle
564
+ * @returns The lease
565
+ */
566
+ async handle(lease, reactions) {
567
+ const stream = lease.stream;
568
+ lease.retry > 0 && logger.error(`Retrying ${stream}@${lease.at} (${lease.retry}).`);
569
+ for (const reaction of reactions) {
570
+ const { event, handler, options } = reaction;
571
+ try {
572
+ await handler(event, stream);
573
+ lease.at = event.id;
574
+ lease.count = (lease.count || 0) + 1;
575
+ } catch (error) {
576
+ lease.error = error;
577
+ if (error instanceof ValidationError)
578
+ logger.error({ stream, error }, error.message);
579
+ else logger.error(error);
580
+ if (lease.retry < options.maxRetries) lease.retry++;
581
+ else if (options.blockOnError) {
582
+ lease.block = true;
583
+ logger.error(`Blocked ${stream} after ${lease.retry} retries.`);
584
+ }
585
+ break;
586
+ }
587
+ }
588
+ return lease;
589
+ }
590
+ drainLocked = false;
591
+ /**
592
+ * Drains events from the store.
593
+ *
594
+ * @returns The number of drained events
595
+ */
596
+ async drain() {
597
+ if (this.drainLocked) return 0;
598
+ this.drainLocked = true;
599
+ const drained = [];
600
+ const { streams, events } = await store().fetch(this.drainLimit);
601
+ if (events.length) {
602
+ logger.trace(
603
+ events.map(({ id, stream, name }) => ({ id, stream, name })).reduce(
604
+ (a, { id, stream, name }) => ({ ...a, [id]: { [stream]: name } }),
605
+ {}
606
+ ),
607
+ "\u26A1\uFE0F fetch"
608
+ );
609
+ const resolved = new Set(streams);
610
+ const correlated = /* @__PURE__ */ new Map();
611
+ for (const event of events)
612
+ for (const reaction of this.registry.events[event.name].reactions.values()) {
613
+ const stream = typeof reaction.resolver === "string" ? reaction.resolver : reaction.resolver(event);
614
+ if (stream) {
615
+ resolved.add(stream);
616
+ (correlated.get(stream) || correlated.set(stream, []).get(stream)).push({ ...reaction, event });
617
+ }
618
+ }
619
+ const last = events.at(-1).id;
620
+ const leases = [...resolved.values()].map((stream) => ({
621
+ by: randomUUID2(),
622
+ stream,
623
+ at: last,
624
+ retry: 0,
625
+ block: false
626
+ }));
627
+ const leased = await store().lease(leases);
628
+ logger.trace(
629
+ leased.map(({ stream, at, retry }) => ({ stream, at, retry })).reduce(
630
+ (a, { stream, at, retry }) => ({ ...a, [stream]: { at, retry } }),
631
+ {}
632
+ ),
633
+ "\u26A1\uFE0F lease"
634
+ );
635
+ const handling = leased.map((lease) => ({
636
+ lease,
637
+ reactions: correlated.get(lease.stream) || []
638
+ })).filter(({ reactions }) => reactions.length);
639
+ if (handling.length) {
640
+ await Promise.allSettled(
641
+ handling.map(({ lease, reactions }) => this.handle(lease, reactions))
642
+ ).then(
643
+ (promise) => {
644
+ promise.forEach((result) => {
645
+ if (result.status === "rejected") logger.error(result.reason);
646
+ else if (result.value.count) drained.push(result.value);
647
+ });
648
+ },
649
+ (error) => logger.error(error)
650
+ );
651
+ drained.length && this.emit("drained", drained);
652
+ }
653
+ await store().ack(leased);
654
+ logger.trace(
655
+ leased.map(({ stream, at, retry, block, count: handled }) => ({
656
+ stream,
657
+ at,
658
+ retry,
659
+ block,
660
+ handled
661
+ })).reduce(
662
+ (a, { stream, at, retry, block, handled }) => ({
663
+ ...a,
664
+ [stream]: { at, retry, block, handled }
665
+ }),
666
+ {}
667
+ ),
668
+ "\u26A1\uFE0F ack"
669
+ );
670
+ }
671
+ this.drainLocked = false;
672
+ return drained.length;
673
+ }
674
+ };
675
+
676
+ // src/act-builder.ts
677
+ var _this_ = ({ stream }) => stream;
678
+ var _void_ = () => void 0;
679
+ function act(states = /* @__PURE__ */ new Set(), registry = {
680
+ actions: {},
681
+ events: {}
682
+ }) {
683
+ const builder = {
684
+ /**
685
+ * Adds a state to the builder.
686
+ *
687
+ * @template SX The type of state
688
+ * @template EX The type of events
689
+ * @template AX The type of actions
690
+ * @param state The state to add
691
+ * @returns The builder
692
+ */
693
+ with: (state2) => {
694
+ if (!states.has(state2.name)) {
695
+ states.add(state2.name);
696
+ for (const name of Object.keys(state2.actions)) {
697
+ if (registry.actions[name])
698
+ throw new Error(`Duplicate action "${name}"`);
699
+ registry.actions[name] = state2;
700
+ }
701
+ for (const name of Object.keys(state2.events)) {
702
+ if (registry.events[name])
703
+ throw new Error(`Duplicate event "${name}"`);
704
+ registry.events[name] = {
705
+ schema: state2.events[name],
706
+ reactions: /* @__PURE__ */ new Map()
707
+ };
708
+ }
709
+ }
710
+ return act(
711
+ states,
712
+ registry
713
+ );
714
+ },
715
+ /**
716
+ * Adds a reaction to an event.
717
+ *
718
+ * @template K The type of event
719
+ * @param event The event to add a reaction to
720
+ * @returns The builder
721
+ */
722
+ on: (event) => ({
723
+ do: (handler, options) => {
724
+ const reaction = {
725
+ handler,
726
+ resolver: _this_,
727
+ options: {
728
+ blockOnError: options?.blockOnError ?? true,
729
+ maxRetries: options?.maxRetries ?? 3,
730
+ retryDelayMs: options?.retryDelayMs ?? 1e3
731
+ }
732
+ };
733
+ registry.events[event].reactions.set(handler.name, reaction);
734
+ return {
735
+ ...builder,
736
+ to(resolver) {
737
+ registry.events[event].reactions.set(handler.name, {
738
+ ...reaction,
739
+ resolver
740
+ });
741
+ return builder;
742
+ },
743
+ void() {
744
+ registry.events[event].reactions.set(handler.name, {
745
+ ...reaction,
746
+ resolver: _void_
747
+ });
748
+ return builder;
749
+ }
750
+ };
751
+ }
752
+ }),
753
+ build: (drainLimit = 10) => new Act(registry, drainLimit),
754
+ events: registry.events
755
+ };
756
+ return builder;
757
+ }
758
+
759
+ // src/state-builder.ts
760
+ function state(name, state2) {
761
+ return {
762
+ init(init) {
763
+ return {
764
+ emits(events) {
765
+ return {
766
+ patch(patch2) {
767
+ return action_builder({
768
+ events,
769
+ actions: {},
770
+ state: state2,
771
+ name,
772
+ init,
773
+ patch: patch2,
774
+ on: {}
775
+ });
776
+ }
777
+ };
778
+ }
779
+ };
780
+ }
781
+ };
782
+ }
783
+ function action_builder(state2) {
784
+ return {
785
+ on(action2, schema) {
786
+ if (action2 in state2.actions)
787
+ throw new Error(`Duplicate action "${action2}"`);
788
+ const actions = { ...state2.actions, [action2]: schema };
789
+ const on = { ...state2.on };
790
+ const _given = { ...state2.given };
791
+ function given(rules) {
792
+ _given[action2] = rules;
793
+ return { emit };
794
+ }
795
+ function emit(handler) {
796
+ on[action2] = handler;
797
+ return action_builder({
798
+ ...state2,
799
+ actions,
800
+ on,
801
+ given: _given
802
+ });
803
+ }
804
+ return { given, emit };
805
+ },
806
+ snap(snap2) {
807
+ return action_builder({ ...state2, snap: snap2 });
808
+ },
809
+ build() {
810
+ return state2;
811
+ }
812
+ };
813
+ }
814
+
815
+ // src/index.ts
11
816
  process.once("SIGINT", async (arg) => {
12
- logger.info(arg, "SIGINT");
13
- await disposeAndExit("EXIT");
817
+ logger.info(arg, "SIGINT");
818
+ await disposeAndExit("EXIT");
14
819
  });
15
820
  process.once("SIGTERM", async (arg) => {
16
- logger.info(arg, "SIGTERM");
17
- await disposeAndExit("EXIT");
821
+ logger.info(arg, "SIGTERM");
822
+ await disposeAndExit("EXIT");
18
823
  });
19
824
  process.once("uncaughtException", async (arg) => {
20
- logger.error(arg, "Uncaught Exception");
21
- await disposeAndExit("ERROR");
825
+ logger.error(arg, "Uncaught Exception");
826
+ await disposeAndExit("ERROR");
22
827
  });
23
828
  process.once("unhandledRejection", async (arg) => {
24
- logger.error(arg, "Unhandled Rejection");
25
- await disposeAndExit("ERROR");
829
+ logger.error(arg, "Unhandled Rejection");
830
+ await disposeAndExit("ERROR");
26
831
  });
832
+ export {
833
+ Act,
834
+ ActorSchema,
835
+ CausationEventSchema,
836
+ CommittedMetaSchema,
837
+ ConcurrencyError,
838
+ Environments,
839
+ Errors,
840
+ EventMetaSchema,
841
+ ExitCodes,
842
+ InvariantError,
843
+ LogLevels,
844
+ QuerySchema,
845
+ SNAP_EVENT,
846
+ TargetSchema,
847
+ ValidationError,
848
+ ZodEmpty,
849
+ act,
850
+ buildSnapshotSchema,
851
+ config,
852
+ dispose,
853
+ disposeAndExit,
854
+ extend,
855
+ logger,
856
+ patch,
857
+ port,
858
+ sleep,
859
+ state,
860
+ store,
861
+ validate
862
+ };
27
863
  //# sourceMappingURL=index.js.map