@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act-builder.d.ts +32 -0
- package/dist/@types/act-builder.d.ts.map +1 -0
- package/dist/@types/act.d.ts +75 -0
- package/dist/@types/act.d.ts.map +1 -0
- package/dist/{adapters → @types/adapters}/InMemoryStore.d.ts +1 -1
- package/dist/@types/adapters/InMemoryStore.d.ts.map +1 -0
- package/dist/{config.d.ts → @types/config.d.ts} +4 -0
- package/dist/@types/config.d.ts.map +1 -0
- package/dist/@types/event-sourcing.d.ts +41 -0
- package/dist/@types/event-sourcing.d.ts.map +1 -0
- package/dist/@types/index.d.ts +13 -0
- package/dist/@types/index.d.ts.map +1 -0
- package/dist/{ports.d.ts → @types/ports.d.ts} +5 -1
- package/dist/@types/ports.d.ts.map +1 -0
- package/dist/@types/state-builder.d.ts +57 -0
- package/dist/@types/state-builder.d.ts.map +1 -0
- package/dist/{types → @types/types}/action.d.ts +26 -24
- package/dist/@types/types/action.d.ts.map +1 -0
- package/dist/{types → @types/types}/errors.d.ts +1 -1
- package/dist/@types/types/errors.d.ts.map +1 -0
- package/dist/{types → @types/types}/index.d.ts +6 -6
- package/dist/@types/types/index.d.ts.map +1 -0
- package/dist/{types → @types/types}/ports.d.ts +2 -2
- package/dist/@types/types/ports.d.ts.map +1 -0
- package/dist/{types → @types/types}/reaction.d.ts +1 -1
- package/dist/@types/types/reaction.d.ts.map +1 -0
- package/dist/{types → @types/types}/registry.d.ts +4 -4
- package/dist/@types/types/registry.d.ts.map +1 -0
- package/dist/{types → @types/types}/schemas.d.ts +4 -4
- package/dist/@types/types/schemas.d.ts.map +1 -0
- package/dist/{utils.d.ts → @types/utils.d.ts} +4 -1
- package/dist/@types/utils.d.ts.map +1 -0
- package/dist/index.cjs +928 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +854 -18
- package/dist/index.js.map +1 -1
- package/package.json +17 -5
- package/dist/act.d.ts +0 -24
- package/dist/act.d.ts.map +0 -1
- package/dist/act.js +0 -136
- package/dist/act.js.map +0 -1
- package/dist/adapters/InMemoryStore.d.ts.map +0 -1
- package/dist/adapters/InMemoryStore.js +0 -125
- package/dist/adapters/InMemoryStore.js.map +0 -1
- package/dist/builder.d.ts +0 -17
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js +0 -70
- package/dist/builder.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -41
- package/dist/config.js.map +0 -1
- package/dist/event-sourcing.d.ts +0 -5
- package/dist/event-sourcing.d.ts.map +0 -1
- package/dist/event-sourcing.js +0 -101
- package/dist/event-sourcing.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/ports.d.ts.map +0 -1
- package/dist/ports.js +0 -56
- package/dist/ports.js.map +0 -1
- package/dist/types/action.d.ts.map +0 -1
- package/dist/types/action.js +0 -2
- package/dist/types/action.js.map +0 -1
- package/dist/types/errors.d.ts.map +0 -1
- package/dist/types/errors.js +0 -44
- package/dist/types/errors.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -17
- package/dist/types/index.js.map +0 -1
- package/dist/types/ports.d.ts.map +0 -1
- package/dist/types/ports.js +0 -2
- package/dist/types/ports.js.map +0 -1
- package/dist/types/reaction.d.ts.map +0 -1
- package/dist/types/reaction.js +0 -2
- package/dist/types/reaction.js.map +0 -1
- package/dist/types/registry.d.ts.map +0 -1
- package/dist/types/registry.js +0 -2
- package/dist/types/registry.js.map +0 -1
- package/dist/types/schemas.d.ts.map +0 -1
- package/dist/types/schemas.js +0 -81
- package/dist/types/schemas.js.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -73
- package/dist/utils.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,27 +1,863 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
817
|
+
logger.info(arg, "SIGINT");
|
|
818
|
+
await disposeAndExit("EXIT");
|
|
14
819
|
});
|
|
15
820
|
process.once("SIGTERM", async (arg) => {
|
|
16
|
-
|
|
17
|
-
|
|
821
|
+
logger.info(arg, "SIGTERM");
|
|
822
|
+
await disposeAndExit("EXIT");
|
|
18
823
|
});
|
|
19
824
|
process.once("uncaughtException", async (arg) => {
|
|
20
|
-
|
|
21
|
-
|
|
825
|
+
logger.error(arg, "Uncaught Exception");
|
|
826
|
+
await disposeAndExit("ERROR");
|
|
22
827
|
});
|
|
23
828
|
process.once("unhandledRejection", async (arg) => {
|
|
24
|
-
|
|
25
|
-
|
|
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
|