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