@rotorsoft/act 0.33.2 → 0.34.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 (83) hide show
  1. package/README.md +26 -2
  2. package/package.json +4 -2
  3. package/dist/.tsbuildinfo +0 -1
  4. package/dist/@types/act.d.ts +0 -598
  5. package/dist/@types/act.d.ts.map +0 -1
  6. package/dist/@types/adapters/console-logger.d.ts +0 -41
  7. package/dist/@types/adapters/console-logger.d.ts.map +0 -1
  8. package/dist/@types/adapters/in-memory-cache.d.ts +0 -34
  9. package/dist/@types/adapters/in-memory-cache.d.ts.map +0 -1
  10. package/dist/@types/adapters/in-memory-store.d.ts +0 -181
  11. package/dist/@types/adapters/in-memory-store.d.ts.map +0 -1
  12. package/dist/@types/adapters/index.d.ts +0 -4
  13. package/dist/@types/adapters/index.d.ts.map +0 -1
  14. package/dist/@types/builders/act-builder.d.ts +0 -160
  15. package/dist/@types/builders/act-builder.d.ts.map +0 -1
  16. package/dist/@types/builders/index.d.ts +0 -13
  17. package/dist/@types/builders/index.d.ts.map +0 -1
  18. package/dist/@types/builders/projection-builder.d.ts +0 -101
  19. package/dist/@types/builders/projection-builder.d.ts.map +0 -1
  20. package/dist/@types/builders/slice-builder.d.ts +0 -109
  21. package/dist/@types/builders/slice-builder.d.ts.map +0 -1
  22. package/dist/@types/builders/state-builder.d.ts +0 -408
  23. package/dist/@types/builders/state-builder.d.ts.map +0 -1
  24. package/dist/@types/config.d.ts +0 -119
  25. package/dist/@types/config.d.ts.map +0 -1
  26. package/dist/@types/index.d.ts +0 -14
  27. package/dist/@types/index.d.ts.map +0 -1
  28. package/dist/@types/internal/build-classify.d.ts +0 -44
  29. package/dist/@types/internal/build-classify.d.ts.map +0 -1
  30. package/dist/@types/internal/close-cycle.d.ts +0 -38
  31. package/dist/@types/internal/close-cycle.d.ts.map +0 -1
  32. package/dist/@types/internal/correlate-cycle.d.ts +0 -73
  33. package/dist/@types/internal/correlate-cycle.d.ts.map +0 -1
  34. package/dist/@types/internal/drain-cycle.d.ts +0 -113
  35. package/dist/@types/internal/drain-cycle.d.ts.map +0 -1
  36. package/dist/@types/internal/drain-ratio.d.ts +0 -26
  37. package/dist/@types/internal/drain-ratio.d.ts.map +0 -1
  38. package/dist/@types/internal/drain.d.ts +0 -41
  39. package/dist/@types/internal/drain.d.ts.map +0 -1
  40. package/dist/@types/internal/event-sourcing.d.ts +0 -96
  41. package/dist/@types/internal/event-sourcing.d.ts.map +0 -1
  42. package/dist/@types/internal/index.d.ts +0 -29
  43. package/dist/@types/internal/index.d.ts.map +0 -1
  44. package/dist/@types/internal/merge.d.ts +0 -31
  45. package/dist/@types/internal/merge.d.ts.map +0 -1
  46. package/dist/@types/internal/reactions.d.ts +0 -54
  47. package/dist/@types/internal/reactions.d.ts.map +0 -1
  48. package/dist/@types/internal/settle.d.ts +0 -60
  49. package/dist/@types/internal/settle.d.ts.map +0 -1
  50. package/dist/@types/internal/tracing.d.ts +0 -45
  51. package/dist/@types/internal/tracing.d.ts.map +0 -1
  52. package/dist/@types/lru-map.d.ts +0 -50
  53. package/dist/@types/lru-map.d.ts.map +0 -1
  54. package/dist/@types/ports.d.ts +0 -196
  55. package/dist/@types/ports.d.ts.map +0 -1
  56. package/dist/@types/signals.d.ts +0 -2
  57. package/dist/@types/signals.d.ts.map +0 -1
  58. package/dist/@types/types/action.d.ts +0 -444
  59. package/dist/@types/types/action.d.ts.map +0 -1
  60. package/dist/@types/types/errors.d.ts +0 -284
  61. package/dist/@types/types/errors.d.ts.map +0 -1
  62. package/dist/@types/types/index.d.ts +0 -39
  63. package/dist/@types/types/index.d.ts.map +0 -1
  64. package/dist/@types/types/ports.d.ts +0 -485
  65. package/dist/@types/types/ports.d.ts.map +0 -1
  66. package/dist/@types/types/reaction.d.ts +0 -291
  67. package/dist/@types/types/reaction.d.ts.map +0 -1
  68. package/dist/@types/types/registry.d.ts +0 -74
  69. package/dist/@types/types/registry.d.ts.map +0 -1
  70. package/dist/@types/types/schemas.d.ts +0 -117
  71. package/dist/@types/types/schemas.d.ts.map +0 -1
  72. package/dist/@types/utils.d.ts +0 -54
  73. package/dist/@types/utils.d.ts.map +0 -1
  74. package/dist/chunk-AWT7TU22.js +0 -127
  75. package/dist/chunk-AWT7TU22.js.map +0 -1
  76. package/dist/index.cjs +0 -2985
  77. package/dist/index.cjs.map +0 -1
  78. package/dist/index.js +0 -2816
  79. package/dist/index.js.map +0 -1
  80. package/dist/types/index.cjs +0 -166
  81. package/dist/types/index.cjs.map +0 -1
  82. package/dist/types/index.js +0 -33
  83. package/dist/types/index.js.map +0 -1
package/dist/index.cjs DELETED
@@ -1,2985 +0,0 @@
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
- ConsoleLogger: () => ConsoleLogger,
39
- DEFAULT_MAX_SUBSCRIBED_STREAMS: () => DEFAULT_MAX_SUBSCRIBED_STREAMS,
40
- DEFAULT_SETTLE_DEBOUNCE_MS: () => DEFAULT_SETTLE_DEBOUNCE_MS,
41
- Environments: () => Environments,
42
- Errors: () => Errors,
43
- EventMetaSchema: () => EventMetaSchema,
44
- ExitCodes: () => ExitCodes,
45
- InMemoryCache: () => InMemoryCache,
46
- InMemoryStore: () => InMemoryStore,
47
- InvariantError: () => InvariantError,
48
- LogLevels: () => LogLevels,
49
- PackageSchema: () => PackageSchema,
50
- QuerySchema: () => QuerySchema,
51
- SNAP_EVENT: () => SNAP_EVENT,
52
- StreamClosedError: () => StreamClosedError,
53
- TOMBSTONE_EVENT: () => TOMBSTONE_EVENT,
54
- TargetSchema: () => TargetSchema,
55
- ValidationError: () => ValidationError,
56
- ZodEmpty: () => ZodEmpty,
57
- act: () => act,
58
- cache: () => cache,
59
- config: () => config,
60
- dispose: () => dispose,
61
- disposeAndExit: () => disposeAndExit,
62
- extend: () => extend,
63
- log: () => log,
64
- port: () => port,
65
- projection: () => projection,
66
- sleep: () => sleep,
67
- slice: () => slice,
68
- state: () => state,
69
- store: () => store,
70
- validate: () => validate
71
- });
72
- module.exports = __toCommonJS(index_exports);
73
-
74
- // src/adapters/console-logger.ts
75
- var LEVEL_VALUES = {
76
- fatal: 60,
77
- error: 50,
78
- warn: 40,
79
- info: 30,
80
- debug: 20,
81
- trace: 10
82
- };
83
- var LEVEL_COLORS = {
84
- fatal: "\x1B[41m\x1B[37m",
85
- // white on red bg
86
- error: "\x1B[31m",
87
- // red
88
- warn: "\x1B[33m",
89
- // yellow
90
- info: "\x1B[32m",
91
- // green
92
- debug: "\x1B[36m",
93
- // cyan
94
- trace: "\x1B[90m"
95
- // gray
96
- };
97
- var RESET = "\x1B[0m";
98
- var noop = () => {
99
- };
100
- var ConsoleLogger = class _ConsoleLogger {
101
- level;
102
- _pretty;
103
- fatal;
104
- error;
105
- warn;
106
- info;
107
- debug;
108
- trace;
109
- constructor(options = {}) {
110
- const {
111
- level = "info",
112
- pretty = process.env.NODE_ENV !== "production",
113
- bindings
114
- } = options;
115
- this._pretty = pretty;
116
- this.level = level;
117
- const threshold = LEVEL_VALUES[level] ?? 30;
118
- const write = pretty ? this._prettyWrite.bind(this, bindings) : this._jsonWrite.bind(this, bindings);
119
- this.fatal = write.bind(this, "fatal", 60);
120
- this.error = threshold <= 50 ? write.bind(this, "error", 50) : noop;
121
- this.warn = threshold <= 40 ? write.bind(this, "warn", 40) : noop;
122
- this.info = threshold <= 30 ? write.bind(this, "info", 30) : noop;
123
- this.debug = threshold <= 20 ? write.bind(this, "debug", 20) : noop;
124
- this.trace = threshold <= 10 ? write.bind(this, "trace", 10) : noop;
125
- }
126
- /** No-op — `console.log` has no resources to release. */
127
- async dispose() {
128
- }
129
- /** @inheritDoc */
130
- child(bindings) {
131
- return new _ConsoleLogger({
132
- level: this.level,
133
- pretty: this._pretty,
134
- bindings
135
- });
136
- }
137
- _jsonWrite(bindings, level, _num, objOrMsg, msg) {
138
- let obj;
139
- let message;
140
- if (typeof objOrMsg === "string") {
141
- message = objOrMsg;
142
- obj = {};
143
- } else if (objOrMsg !== null && typeof objOrMsg === "object") {
144
- message = msg;
145
- obj = { ...objOrMsg };
146
- } else {
147
- message = msg;
148
- obj = { value: objOrMsg };
149
- }
150
- const entry = Object.assign({ level, time: Date.now() }, bindings, obj);
151
- if (message) entry.msg = message;
152
- let line;
153
- try {
154
- line = JSON.stringify(entry);
155
- } catch {
156
- line = JSON.stringify({
157
- level,
158
- time: entry.time,
159
- msg: message ?? "[unserializable]",
160
- unserializable: true
161
- });
162
- }
163
- process.stdout.write(line + "\n");
164
- }
165
- _prettyWrite(bindings, level, _num, objOrMsg, msg) {
166
- const color = LEVEL_COLORS[level];
167
- const tag = `${color}${level.toUpperCase().padEnd(5)}${RESET}`;
168
- const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
169
- let message;
170
- let data;
171
- if (typeof objOrMsg === "string") {
172
- message = objOrMsg;
173
- } else {
174
- message = msg ?? "";
175
- if (objOrMsg !== void 0 && objOrMsg !== null) {
176
- try {
177
- data = JSON.stringify(objOrMsg);
178
- } catch {
179
- data = "[unserializable]";
180
- }
181
- }
182
- }
183
- const bindStr = bindings && Object.keys(bindings).length ? ` ${JSON.stringify(bindings)}` : "";
184
- const parts = [ts, tag, message, data, bindStr].filter(Boolean);
185
- process.stdout.write(parts.join(" ") + "\n");
186
- }
187
- };
188
-
189
- // src/lru-map.ts
190
- var LruMap = class {
191
- constructor(_maxSize) {
192
- this._maxSize = _maxSize;
193
- }
194
- _entries = /* @__PURE__ */ new Map();
195
- get(key) {
196
- const v = this._entries.get(key);
197
- if (v === void 0) return void 0;
198
- this._entries.delete(key);
199
- this._entries.set(key, v);
200
- return v;
201
- }
202
- has(key) {
203
- return this._entries.has(key);
204
- }
205
- set(key, value) {
206
- this._entries.delete(key);
207
- if (this._entries.size >= this._maxSize) {
208
- const oldest = this._entries.keys().next().value;
209
- this._entries.delete(oldest);
210
- }
211
- this._entries.set(key, value);
212
- }
213
- delete(key) {
214
- return this._entries.delete(key);
215
- }
216
- clear() {
217
- this._entries.clear();
218
- }
219
- get size() {
220
- return this._entries.size;
221
- }
222
- };
223
- var LruSet = class {
224
- _map;
225
- constructor(maxSize) {
226
- this._map = new LruMap(maxSize);
227
- }
228
- has(value) {
229
- return this._map.has(value);
230
- }
231
- add(value) {
232
- this._map.set(value, true);
233
- }
234
- delete(value) {
235
- return this._map.delete(value);
236
- }
237
- clear() {
238
- this._map.clear();
239
- }
240
- get size() {
241
- return this._map.size;
242
- }
243
- };
244
-
245
- // src/adapters/in-memory-cache.ts
246
- var InMemoryCache = class {
247
- // CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
248
- // any is bidirectionally compatible with the per-call TState binding, while
249
- // the public Cache interface still presents a typed surface to callers.
250
- _entries;
251
- constructor(options) {
252
- this._entries = new LruMap(options?.maxSize ?? 1e3);
253
- }
254
- /** @inheritDoc */
255
- async get(stream) {
256
- return this._entries.get(stream);
257
- }
258
- /** @inheritDoc */
259
- async set(stream, entry) {
260
- this._entries.set(stream, entry);
261
- }
262
- /** @inheritDoc */
263
- async invalidate(stream) {
264
- this._entries.delete(stream);
265
- }
266
- /** @inheritDoc */
267
- async clear() {
268
- this._entries.clear();
269
- }
270
- /** @inheritDoc */
271
- async dispose() {
272
- this._entries.clear();
273
- }
274
- /** Current number of entries held by the LRU. */
275
- get size() {
276
- return this._entries.size;
277
- }
278
- };
279
-
280
- // src/types/errors.ts
281
- var Errors = {
282
- ValidationError: "ERR_VALIDATION",
283
- InvariantError: "ERR_INVARIANT",
284
- ConcurrencyError: "ERR_CONCURRENCY",
285
- StreamClosedError: "ERR_STREAM_CLOSED"
286
- };
287
- var ValidationError = class extends Error {
288
- constructor(target, payload, details) {
289
- super(`Invalid ${target} payload`);
290
- this.target = target;
291
- this.payload = payload;
292
- this.details = details;
293
- this.name = Errors.ValidationError;
294
- }
295
- };
296
- var InvariantError = class extends Error {
297
- constructor(action2, payload, target, snapshot, description) {
298
- super(`${action2} failed invariant: ${description}`);
299
- this.action = action2;
300
- this.payload = payload;
301
- this.target = target;
302
- this.snapshot = snapshot;
303
- this.description = description;
304
- this.name = Errors.InvariantError;
305
- }
306
- };
307
- var ConcurrencyError = class extends Error {
308
- constructor(stream, lastVersion, events, expectedVersion) {
309
- super(
310
- `Concurrency error committing "${events.map((e) => `${stream}.${e.name}`).join(
311
- ", "
312
- )}". Expected version ${expectedVersion} but found version ${lastVersion}.`
313
- );
314
- this.stream = stream;
315
- this.lastVersion = lastVersion;
316
- this.events = events;
317
- this.expectedVersion = expectedVersion;
318
- this.name = Errors.ConcurrencyError;
319
- }
320
- };
321
- var StreamClosedError = class extends Error {
322
- constructor(stream) {
323
- super(`Stream "${stream}" is closed (tombstoned)`);
324
- this.stream = stream;
325
- this.name = Errors.StreamClosedError;
326
- }
327
- };
328
-
329
- // src/utils.ts
330
- var import_zod3 = require("zod");
331
-
332
- // src/config.ts
333
- var fs = __toESM(require("fs"), 1);
334
- var import_zod2 = require("zod");
335
-
336
- // src/types/schemas.ts
337
- var import_zod = require("zod");
338
- var ZodEmpty = import_zod.z.record(import_zod.z.string(), import_zod.z.never());
339
- var ActorSchema = import_zod.z.object({
340
- id: import_zod.z.string(),
341
- name: import_zod.z.string()
342
- }).loose().readonly();
343
- var TargetSchema = import_zod.z.object({
344
- stream: import_zod.z.string(),
345
- actor: ActorSchema,
346
- expectedVersion: import_zod.z.number().optional()
347
- }).loose().readonly();
348
- var CausationEventSchema = import_zod.z.object({
349
- id: import_zod.z.number(),
350
- name: import_zod.z.string(),
351
- stream: import_zod.z.string()
352
- });
353
- var EventMetaSchema = import_zod.z.object({
354
- correlation: import_zod.z.string(),
355
- causation: import_zod.z.object({
356
- action: TargetSchema.and(import_zod.z.object({ name: import_zod.z.string() })).optional(),
357
- event: CausationEventSchema.optional()
358
- })
359
- }).readonly();
360
- var CommittedMetaSchema = import_zod.z.object({
361
- id: import_zod.z.number(),
362
- stream: import_zod.z.string(),
363
- version: import_zod.z.number(),
364
- created: import_zod.z.date(),
365
- meta: EventMetaSchema
366
- }).readonly();
367
- var QuerySchema = import_zod.z.object({
368
- stream: import_zod.z.string().optional(),
369
- names: import_zod.z.string().array().optional(),
370
- before: import_zod.z.number().optional(),
371
- after: import_zod.z.number().optional(),
372
- limit: import_zod.z.number().optional(),
373
- created_before: import_zod.z.date().optional(),
374
- created_after: import_zod.z.date().optional(),
375
- backward: import_zod.z.boolean().optional(),
376
- correlation: import_zod.z.string().optional(),
377
- with_snaps: import_zod.z.boolean().optional(),
378
- stream_exact: import_zod.z.boolean().optional()
379
- }).readonly();
380
-
381
- // src/types/index.ts
382
- var Environments = [
383
- "development",
384
- "test",
385
- "staging",
386
- "production"
387
- ];
388
- var LogLevels = [
389
- "fatal",
390
- "error",
391
- "warn",
392
- "info",
393
- "debug",
394
- "trace"
395
- ];
396
-
397
- // src/config.ts
398
- var PackageSchema = import_zod2.z.object({
399
- name: import_zod2.z.string().min(1),
400
- version: import_zod2.z.string().min(1),
401
- description: import_zod2.z.string().min(1).optional(),
402
- author: import_zod2.z.object({ name: import_zod2.z.string().min(1), email: import_zod2.z.string().optional() }).optional().or(import_zod2.z.string().min(1)).optional(),
403
- license: import_zod2.z.string().min(1).optional(),
404
- dependencies: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).optional()
405
- });
406
- var FALLBACK_PACKAGE = {
407
- name: "act-fallback",
408
- version: "0.0.0-fallback",
409
- description: "Synthetic fallback \u2014 package.json could not be loaded"
410
- };
411
- var getPackage = () => {
412
- try {
413
- const raw = fs.readFileSync("package.json");
414
- return JSON.parse(raw.toString());
415
- } catch (err) {
416
- pkgLoadError = err;
417
- return FALLBACK_PACKAGE;
418
- }
419
- };
420
- var pkgLoadError;
421
- var BaseSchema = PackageSchema.extend({
422
- env: import_zod2.z.enum(Environments),
423
- logLevel: import_zod2.z.enum(LogLevels),
424
- logSingleLine: import_zod2.z.boolean(),
425
- sleepMs: import_zod2.z.number().int().min(0).max(5e3)
426
- });
427
- var { NODE_ENV, LOG_LEVEL, LOG_SINGLE_LINE, SLEEP_MS } = process.env;
428
- var env = NODE_ENV || "development";
429
- var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "fatal" : NODE_ENV === "production" ? "info" : "trace");
430
- var logSingleLine = (LOG_SINGLE_LINE || "true") === "true";
431
- var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100", 10);
432
- var pkg = getPackage();
433
- var _validated;
434
- var config = () => {
435
- if (!_validated) {
436
- _validated = extend(
437
- { ...pkg, env, logLevel, logSingleLine, sleepMs },
438
- BaseSchema
439
- );
440
- if (pkgLoadError) {
441
- const msg = pkgLoadError instanceof Error ? pkgLoadError.message : typeof pkgLoadError === "string" ? pkgLoadError : "unknown error";
442
- log().warn(
443
- `[act] Could not read package.json (${msg}); using synthetic name="${FALLBACK_PACKAGE.name}" version="${FALLBACK_PACKAGE.version}".`
444
- );
445
- pkgLoadError = void 0;
446
- }
447
- }
448
- return _validated;
449
- };
450
-
451
- // src/utils.ts
452
- var validate = (target, payload, schema) => {
453
- try {
454
- return schema ? schema.parse(payload) : payload;
455
- } catch (error) {
456
- if (error instanceof import_zod3.ZodError) {
457
- throw new ValidationError(target, payload, (0, import_zod3.prettifyError)(error));
458
- }
459
- throw new ValidationError(target, payload, error);
460
- }
461
- };
462
- var extend = (source, schema, target) => {
463
- const value = validate("config", source, schema);
464
- return { ...target, ...value };
465
- };
466
- async function sleep(ms) {
467
- return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
468
- }
469
-
470
- // src/adapters/in-memory-store.ts
471
- var InMemoryStream = class {
472
- constructor(stream, source) {
473
- this.stream = stream;
474
- this.source = source;
475
- }
476
- _at = -1;
477
- _retry = -1;
478
- _blocked = false;
479
- _error = "";
480
- _leased_by = void 0;
481
- _leased_until = void 0;
482
- get is_available() {
483
- return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date());
484
- }
485
- get at() {
486
- return this._at;
487
- }
488
- get retry() {
489
- return this._retry;
490
- }
491
- get blocked() {
492
- return this._blocked;
493
- }
494
- get error() {
495
- return this._error;
496
- }
497
- get leased_by() {
498
- return this._leased_by;
499
- }
500
- get leased_until() {
501
- return this._leased_until;
502
- }
503
- /**
504
- * Attempt to lease this stream for processing.
505
- * @param lease - The lease request.
506
- * @param millis - Lease duration in milliseconds.
507
- * @returns The granted lease or undefined if blocked.
508
- */
509
- lease(lease, millis) {
510
- if (millis > 0) {
511
- this._leased_by = lease.by;
512
- this._leased_until = new Date(Date.now() + millis);
513
- }
514
- this._retry = this._retry + 1;
515
- return {
516
- stream: this.stream,
517
- source: this.source,
518
- at: lease.at,
519
- by: lease.by,
520
- retry: this._retry,
521
- lagging: lease.lagging
522
- };
523
- }
524
- /**
525
- * Acknowledge completion of processing for this stream.
526
- * @param lease - The lease request.
527
- */
528
- ack(lease) {
529
- if (this._leased_by === lease.by) {
530
- this._leased_by = void 0;
531
- this._leased_until = void 0;
532
- this._at = lease.at;
533
- this._retry = -1;
534
- return {
535
- stream: this.stream,
536
- source: this.source,
537
- at: this._at,
538
- by: lease.by,
539
- retry: this._retry,
540
- lagging: lease.lagging
541
- };
542
- }
543
- }
544
- /**
545
- * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
546
- * @param lease - The lease request.
547
- * @param error Blocked error message.
548
- */
549
- block(lease, error) {
550
- if (this._leased_by === lease.by) {
551
- this._blocked = true;
552
- this._error = error;
553
- return {
554
- stream: this.stream,
555
- source: this.source,
556
- at: this._at,
557
- by: this._leased_by,
558
- retry: this._retry,
559
- error: this._error,
560
- lagging: lease.lagging
561
- };
562
- }
563
- }
564
- /**
565
- * Reset this stream's watermark and state for replay. The retry counter
566
- * resets to -1 to match the constructor + ack() invariant ("released
567
- * stream"); the next claim() bumps it to 0 (first attempt).
568
- */
569
- reset() {
570
- this._at = -1;
571
- this._retry = -1;
572
- this._blocked = false;
573
- this._error = "";
574
- this._leased_by = void 0;
575
- this._leased_until = void 0;
576
- }
577
- };
578
- var InMemoryStore = class {
579
- // stored events
580
- _events = [];
581
- // stored stream positions and other metadata
582
- _streams = /* @__PURE__ */ new Map();
583
- // last committed version per stream — O(1) replacement for filter-on-commit
584
- _streamVersions = /* @__PURE__ */ new Map();
585
- // max non-snapshot event id per stream — drives the source-pattern probe in claim()
586
- // without scanning the full event log.
587
- _maxEventIdByStream = /* @__PURE__ */ new Map();
588
- // global max non-snapshot event id — fast pre-check for source-less streams in claim()
589
- _maxNonSnapEventId = -1;
590
- _resetIndexes() {
591
- this._events.length = 0;
592
- this._streamVersions.clear();
593
- this._maxEventIdByStream.clear();
594
- this._maxNonSnapEventId = -1;
595
- }
596
- /**
597
- * Dispose of the store and clear all events.
598
- * @returns Promise that resolves when disposal is complete.
599
- */
600
- async dispose() {
601
- await sleep();
602
- this._resetIndexes();
603
- }
604
- /**
605
- * Seed the store with initial data (no-op for in-memory).
606
- * @returns Promise that resolves when seeding is complete.
607
- */
608
- async seed() {
609
- await sleep();
610
- }
611
- /**
612
- * Drop all data from the store.
613
- * @returns Promise that resolves when the store is cleared.
614
- */
615
- async drop() {
616
- await sleep();
617
- this._resetIndexes();
618
- this._streams = /* @__PURE__ */ new Map();
619
- }
620
- in_query(query, e) {
621
- if (query.stream) {
622
- if (query.stream_exact) {
623
- if (e.stream !== query.stream) return false;
624
- } else if (!RegExp(`^${query.stream}$`).test(e.stream)) return false;
625
- }
626
- if (query.names && !query.names.includes(e.name)) return false;
627
- if (query.correlation && e.meta?.correlation !== query.correlation)
628
- return false;
629
- if (e.name === SNAP_EVENT && !query.with_snaps) return false;
630
- return true;
631
- }
632
- /**
633
- * Query events in the store, optionally filtered by query options.
634
- * @param callback - Function to call for each event.
635
- * @param query - Optional query options.
636
- * @returns The number of events processed.
637
- */
638
- async query(callback, query) {
639
- await sleep();
640
- let count = 0;
641
- if (query?.backward) {
642
- let i = (query?.before || this._events.length) - 1;
643
- while (i >= 0) {
644
- const e = this._events[i--];
645
- if (query && !this.in_query(query, e)) continue;
646
- if (query?.created_before && e.created >= query.created_before)
647
- continue;
648
- if (query.after && e.id <= query.after) break;
649
- if (query.created_after && e.created <= query.created_after) break;
650
- callback(e);
651
- count++;
652
- if (query?.limit && count >= query.limit) break;
653
- }
654
- } else {
655
- let i = (query?.after ?? -1) + 1;
656
- while (i < this._events.length) {
657
- const e = this._events[i++];
658
- if (query && !this.in_query(query, e)) continue;
659
- if (query?.created_after && e.created <= query.created_after) continue;
660
- if (query?.before && e.id >= query.before) break;
661
- if (query?.created_before && e.created >= query.created_before) break;
662
- callback(e);
663
- count++;
664
- if (query?.limit && count >= query.limit) break;
665
- }
666
- }
667
- return count;
668
- }
669
- /**
670
- * Commit one or more events to a stream.
671
- * @param stream - The stream name.
672
- * @param msgs - The events/messages to commit.
673
- * @param meta - Event metadata.
674
- * @param expectedVersion - Optional optimistic concurrency check.
675
- * @returns The committed events with metadata.
676
- * @throws ConcurrencyError if expectedVersion does not match.
677
- */
678
- async commit(stream, msgs, meta, expectedVersion) {
679
- await sleep();
680
- const currentVersion = this._streamVersions.get(stream) ?? -1;
681
- if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
682
- throw new ConcurrencyError(
683
- stream,
684
- currentVersion,
685
- msgs,
686
- expectedVersion
687
- );
688
- }
689
- let version = currentVersion + 1;
690
- let lastNonSnapId = -1;
691
- const committed = msgs.map(({ name, data }) => {
692
- const c = {
693
- id: this._events.length,
694
- stream,
695
- version,
696
- created: /* @__PURE__ */ new Date(),
697
- name,
698
- data,
699
- meta
700
- };
701
- this._events.push(c);
702
- if (name !== SNAP_EVENT) lastNonSnapId = c.id;
703
- version++;
704
- return c;
705
- });
706
- this._streamVersions.set(stream, version - 1);
707
- if (lastNonSnapId >= 0) {
708
- this._maxEventIdByStream.set(stream, lastNonSnapId);
709
- this._maxNonSnapEventId = lastNonSnapId;
710
- }
711
- return committed;
712
- }
713
- /**
714
- * Atomically discovers and leases streams for processing.
715
- * Fuses poll + lease into a single operation.
716
- * @param lagging - Max streams from lagging frontier.
717
- * @param leading - Max streams from leading frontier.
718
- * @param by - Lease holder identifier.
719
- * @param millis - Lease duration in milliseconds.
720
- * @returns Granted leases.
721
- */
722
- async claim(lagging, leading, by, millis) {
723
- await sleep();
724
- const sourceRegex = /* @__PURE__ */ new Map();
725
- const getRegex = (source) => {
726
- let re = sourceRegex.get(source);
727
- if (!re) {
728
- re = new RegExp(source);
729
- sourceRegex.set(source, re);
730
- }
731
- return re;
732
- };
733
- const hasWork = (s) => {
734
- if (s.at < 0) return true;
735
- if (!s.source) return s.at < this._maxNonSnapEventId;
736
- const re = getRegex(s.source);
737
- for (const [streamName, maxId] of this._maxEventIdByStream) {
738
- if (maxId > s.at && re.test(streamName)) return true;
739
- }
740
- return false;
741
- };
742
- const available = [...this._streams.values()].filter(
743
- (s) => s.is_available && hasWork(s)
744
- );
745
- const lag = available.sort((a, b) => a.at - b.at).slice(0, lagging).map((s) => ({
746
- stream: s.stream,
747
- source: s.source,
748
- at: s.at,
749
- lagging: true
750
- }));
751
- const lead = available.sort((a, b) => b.at - a.at).slice(0, leading).map((s) => ({
752
- stream: s.stream,
753
- source: s.source,
754
- at: s.at,
755
- lagging: false
756
- }));
757
- const seen = /* @__PURE__ */ new Set();
758
- const combined = [...lag, ...lead].filter((p) => {
759
- if (seen.has(p.stream)) return false;
760
- seen.add(p.stream);
761
- return true;
762
- });
763
- return combined.map(
764
- (p) => this._streams.get(p.stream)?.lease({ ...p, by, retry: 0 }, millis)
765
- ).filter((l) => !!l);
766
- }
767
- /**
768
- * Registers streams for event processing.
769
- * @param streams - Streams to register with optional source.
770
- * @returns subscribed count and current max watermark.
771
- */
772
- async subscribe(streams) {
773
- await sleep();
774
- let subscribed = 0;
775
- for (const { stream, source } of streams) {
776
- if (!this._streams.has(stream)) {
777
- this._streams.set(stream, new InMemoryStream(stream, source));
778
- subscribed++;
779
- }
780
- }
781
- let watermark = -1;
782
- for (const s of this._streams.values()) {
783
- if (s.at > watermark) watermark = s.at;
784
- }
785
- return { subscribed, watermark };
786
- }
787
- /**
788
- * Acknowledge completion of processing for leased streams.
789
- * @param leases - Leases to acknowledge, including last processed watermark and lease holder.
790
- */
791
- async ack(leases) {
792
- await sleep();
793
- return leases.map((l) => this._streams.get(l.stream)?.ack(l)).filter((l) => !!l);
794
- }
795
- /**
796
- * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
797
- * @param leases - Leases to block, including lease holder and last error message.
798
- * @returns Blocked leases.
799
- */
800
- async block(leases) {
801
- await sleep();
802
- return leases.map((l) => this._streams.get(l.stream)?.block(l, l.error)).filter((l) => !!l);
803
- }
804
- /**
805
- * Reset watermarks for the given streams to -1, clearing retry, blocked,
806
- * error, and lease state so they can be replayed from the beginning.
807
- * @param streams - Stream names to reset.
808
- * @returns Count of streams that were actually reset.
809
- */
810
- async reset(streams) {
811
- await sleep();
812
- let count = 0;
813
- for (const name of streams) {
814
- const s = this._streams.get(name);
815
- if (s) {
816
- s.reset();
817
- count++;
818
- }
819
- }
820
- return count;
821
- }
822
- /**
823
- * Streams registered subscription positions to the callback, ordered by
824
- * stream name. Returns the highest event id in the store and the count
825
- * of positions emitted.
826
- */
827
- async query_streams(callback, query) {
828
- await sleep();
829
- const limit = query?.limit ?? 100;
830
- const after = query?.after;
831
- const blocked = query?.blocked;
832
- const streamRe = query?.stream && !query.stream_exact ? new RegExp(`^${query.stream}$`) : void 0;
833
- const sourceRe = query?.source && !query.source_exact ? new RegExp(`^${query.source}$`) : void 0;
834
- const sorted = [...this._streams.values()].sort(
835
- (a, b) => a.stream.localeCompare(b.stream)
836
- );
837
- let count = 0;
838
- for (const s of sorted) {
839
- if (after !== void 0 && s.stream <= after) continue;
840
- if (query?.stream !== void 0) {
841
- if (query.stream_exact ? s.stream !== query.stream : !streamRe.test(s.stream))
842
- continue;
843
- }
844
- if (query?.source !== void 0) {
845
- if (s.source === void 0) continue;
846
- if (query.source_exact ? s.source !== query.source : !sourceRe.test(s.source))
847
- continue;
848
- }
849
- if (blocked !== void 0 && s.blocked !== blocked) continue;
850
- callback({
851
- stream: s.stream,
852
- source: s.source,
853
- at: s.at,
854
- retry: s.retry,
855
- blocked: s.blocked,
856
- error: s.error,
857
- leased_by: s.leased_by,
858
- leased_until: s.leased_until
859
- });
860
- count++;
861
- if (count >= limit) break;
862
- }
863
- return { maxEventId: this._events.length - 1, count };
864
- }
865
- /**
866
- * Atomically truncates streams and seeds each with a snapshot or tombstone.
867
- * @param targets - Streams to truncate with optional snapshot state and meta.
868
- * @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.
869
- */
870
- async truncate(targets) {
871
- await sleep();
872
- const deletedCounts = /* @__PURE__ */ new Map();
873
- const streamSet = new Set(targets.map((t) => t.stream));
874
- for (const e of this._events) {
875
- if (streamSet.has(e.stream)) {
876
- deletedCounts.set(e.stream, (deletedCounts.get(e.stream) ?? 0) + 1);
877
- }
878
- }
879
- this._events = this._events.filter((e) => !streamSet.has(e.stream));
880
- for (const stream of streamSet) {
881
- this._streams.delete(stream);
882
- this._streamVersions.delete(stream);
883
- this._maxEventIdByStream.delete(stream);
884
- }
885
- const result = /* @__PURE__ */ new Map();
886
- for (const { stream, snapshot, meta } of targets) {
887
- const event = {
888
- id: this._events.length,
889
- stream,
890
- version: 0,
891
- created: /* @__PURE__ */ new Date(),
892
- name: snapshot !== void 0 ? SNAP_EVENT : TOMBSTONE_EVENT,
893
- data: snapshot ?? {},
894
- meta: meta ?? { correlation: "", causation: {} }
895
- };
896
- this._events.push(event);
897
- this._streamVersions.set(stream, 0);
898
- if (event.name !== SNAP_EVENT) {
899
- this._maxEventIdByStream.set(stream, event.id);
900
- }
901
- result.set(stream, {
902
- deleted: deletedCounts.get(stream) ?? 0,
903
- committed: event
904
- });
905
- }
906
- let max = -1;
907
- for (const id of this._maxEventIdByStream.values()) if (id > max) max = id;
908
- this._maxNonSnapEventId = max;
909
- return result;
910
- }
911
- };
912
-
913
- // src/ports.ts
914
- var ExitCodes = ["ERROR", "EXIT"];
915
- var adapters = /* @__PURE__ */ new Map();
916
- function port(injector) {
917
- return (adapter) => {
918
- if (!adapters.has(injector.name)) {
919
- const injected = injector(adapter);
920
- adapters.set(injector.name, injected);
921
- log().info(`[act] + ${injector.name}:${injected.constructor.name}`);
922
- }
923
- return adapters.get(injector.name);
924
- };
925
- }
926
- var log = port(function log2(adapter) {
927
- const cfg = config();
928
- return adapter || new ConsoleLogger({
929
- level: cfg.logLevel,
930
- pretty: cfg.env !== "production"
931
- });
932
- });
933
- var store = port(function store2(adapter) {
934
- return adapter || new InMemoryStore();
935
- });
936
- var cache = port(function cache2(adapter) {
937
- return adapter || new InMemoryCache();
938
- });
939
- var disposers = [];
940
- async function disposeAndExit(code = "EXIT") {
941
- if (code === "ERROR" && config().env === "production") {
942
- log().warn(
943
- "disposeAndExit('ERROR') ignored in production \u2014 process kept alive"
944
- );
945
- return;
946
- }
947
- for (const disposer of [...disposers].reverse()) {
948
- await disposer();
949
- }
950
- for (const adapter of [...adapters.values()].reverse()) {
951
- await adapter.dispose();
952
- log().info(`[act] - ${adapter.constructor.name}`);
953
- }
954
- adapters.clear();
955
- config().env !== "test" && process.exit(code === "ERROR" ? 1 : 0);
956
- }
957
- function dispose(disposer) {
958
- disposer && disposers.push(disposer);
959
- return disposeAndExit;
960
- }
961
- var SNAP_EVENT = "__snapshot__";
962
- var TOMBSTONE_EVENT = "__tombstone__";
963
-
964
- // src/signals.ts
965
- process.once("SIGINT", async (arg) => {
966
- log().info(arg, "SIGINT");
967
- await disposeAndExit("EXIT");
968
- });
969
- process.once("SIGTERM", async (arg) => {
970
- log().info(arg, "SIGTERM");
971
- await disposeAndExit("EXIT");
972
- });
973
- process.once("uncaughtException", async (arg) => {
974
- log().error(arg, "Uncaught Exception");
975
- await disposeAndExit("ERROR");
976
- });
977
- process.once("unhandledRejection", async (arg) => {
978
- log().error(arg, "Unhandled Rejection");
979
- await disposeAndExit("ERROR");
980
- });
981
-
982
- // src/act.ts
983
- var import_node_events = __toESM(require("events"), 1);
984
-
985
- // src/internal/build-classify.ts
986
- function classifyRegistry(registry, states) {
987
- const statics = /* @__PURE__ */ new Map();
988
- const reactiveEvents = /* @__PURE__ */ new Set();
989
- let hasDynamicResolvers = false;
990
- for (const [name, register] of Object.entries(registry.events)) {
991
- if (register.reactions.size > 0) reactiveEvents.add(name);
992
- for (const reaction of register.reactions.values()) {
993
- if (typeof reaction.resolver === "function") {
994
- hasDynamicResolvers = true;
995
- } else {
996
- const { target, source } = reaction.resolver;
997
- const key = `${target}|${source ?? ""}`;
998
- if (!statics.has(key)) statics.set(key, { stream: target, source });
999
- }
1000
- }
1001
- }
1002
- const eventToState = /* @__PURE__ */ new Map();
1003
- for (const merged of states.values()) {
1004
- for (const eventName of Object.keys(merged.events)) {
1005
- eventToState.set(eventName, merged);
1006
- }
1007
- }
1008
- return {
1009
- staticTargets: [...statics.values()],
1010
- hasDynamicResolvers,
1011
- reactiveEvents,
1012
- eventToState
1013
- };
1014
- }
1015
-
1016
- // src/internal/close-cycle.ts
1017
- var import_node_crypto = require("crypto");
1018
- async function runCloseCycle(targets, deps) {
1019
- const targetMap = new Map(targets.map((t) => [t.stream, t]));
1020
- const streams = [...targetMap.keys()];
1021
- const skipped = [];
1022
- const streamInfo = await scanStreamHeads(streams);
1023
- const safe = await partitionBySafety(
1024
- streamInfo,
1025
- deps.reactiveEventsSize,
1026
- skipped
1027
- );
1028
- if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
1029
- const correlation = (0, import_node_crypto.randomUUID)();
1030
- const { guarded, guardEvents } = await guardWithTombstones(
1031
- safe,
1032
- streamInfo,
1033
- correlation,
1034
- deps.tombstone,
1035
- skipped
1036
- );
1037
- if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
1038
- const seedStates = await loadRestartSeeds(
1039
- guarded,
1040
- targetMap,
1041
- streamInfo,
1042
- deps.eventToState,
1043
- deps.load,
1044
- deps.logger
1045
- );
1046
- await runArchiveCallbacks(guarded, targetMap);
1047
- const truncated = await truncateAndWarmCache(
1048
- guarded,
1049
- seedStates,
1050
- guardEvents,
1051
- correlation
1052
- );
1053
- return { truncated, skipped };
1054
- }
1055
- async function scanStreamHeads(streams) {
1056
- const out = /* @__PURE__ */ new Map();
1057
- await Promise.all(
1058
- streams.map(async (s) => {
1059
- let maxId = -1;
1060
- let version = -1;
1061
- let lastEventName = "";
1062
- await store().query(
1063
- (e) => {
1064
- if (e.name === TOMBSTONE_EVENT || maxId !== -1) return;
1065
- maxId = e.id;
1066
- version = e.version;
1067
- lastEventName = e.name;
1068
- },
1069
- { stream: s, stream_exact: true, backward: true, limit: 1 }
1070
- );
1071
- if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
1072
- })
1073
- );
1074
- return out;
1075
- }
1076
- async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
1077
- if (reactiveEventsSize === 0) return [...streamInfo.keys()];
1078
- const pendingSet = /* @__PURE__ */ new Set();
1079
- await store().query_streams((position) => {
1080
- const sourceRe = position.source ? RegExp(position.source) : void 0;
1081
- for (const [stream, info] of streamInfo) {
1082
- if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
1083
- pendingSet.add(stream);
1084
- }
1085
- }
1086
- });
1087
- const safe = [];
1088
- for (const [stream] of streamInfo) {
1089
- if (pendingSet.has(stream)) skipped.push(stream);
1090
- else safe.push(stream);
1091
- }
1092
- return safe;
1093
- }
1094
- async function guardWithTombstones(safe, streamInfo, correlation, tombstone2, skipped) {
1095
- const guarded = [];
1096
- const guardEvents = /* @__PURE__ */ new Map();
1097
- await Promise.all(
1098
- safe.map(async (stream) => {
1099
- const info = streamInfo.get(stream);
1100
- const committed = await tombstone2(stream, info.version, correlation);
1101
- if (committed) {
1102
- guarded.push(stream);
1103
- guardEvents.set(stream, { id: committed.id, stream });
1104
- } else {
1105
- skipped.push(stream);
1106
- }
1107
- })
1108
- );
1109
- return { guarded, guardEvents };
1110
- }
1111
- async function loadRestartSeeds(guarded, targetMap, streamInfo, eventToState, load2, logger) {
1112
- const seedStates = /* @__PURE__ */ new Map();
1113
- await Promise.all(
1114
- guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
1115
- const lastEventName = streamInfo.get(stream).lastEventName;
1116
- const ownerState = eventToState.get(lastEventName);
1117
- if (!ownerState) {
1118
- logger.error(
1119
- `Cannot seed restart for "${stream}": no registered state owns event "${lastEventName}". Stream will be tombstoned instead.`
1120
- );
1121
- return;
1122
- }
1123
- const snap2 = await load2(ownerState, stream);
1124
- seedStates.set(stream, snap2.state);
1125
- })
1126
- );
1127
- return seedStates;
1128
- }
1129
- async function runArchiveCallbacks(guarded, targetMap) {
1130
- for (const stream of guarded) {
1131
- const archiveFn = targetMap.get(stream)?.archive;
1132
- if (archiveFn) await archiveFn();
1133
- }
1134
- }
1135
- async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlation) {
1136
- const truncTargets = guarded.map((stream) => {
1137
- const snapshot = seedStates.get(stream);
1138
- const guard = guardEvents.get(stream);
1139
- return {
1140
- stream,
1141
- snapshot,
1142
- meta: {
1143
- correlation,
1144
- causation: {
1145
- event: { id: guard.id, name: TOMBSTONE_EVENT, stream: guard.stream }
1146
- }
1147
- }
1148
- };
1149
- });
1150
- const truncated = await store().truncate(truncTargets);
1151
- await Promise.all(
1152
- guarded.map(async (stream) => {
1153
- const entry = truncated.get(stream);
1154
- const state2 = seedStates.get(stream);
1155
- if (state2 && entry) {
1156
- await cache().set(stream, {
1157
- state: state2,
1158
- version: entry.committed.version,
1159
- event_id: entry.committed.id,
1160
- patches: 0,
1161
- snaps: 1
1162
- });
1163
- } else {
1164
- await cache().invalidate(stream);
1165
- }
1166
- })
1167
- );
1168
- return truncated;
1169
- }
1170
-
1171
- // src/internal/correlate-cycle.ts
1172
- var CorrelateCycle = class {
1173
- constructor(registry, staticTargets, hasDynamicResolvers, cd, maxSubscribedStreams, onInit) {
1174
- this.registry = registry;
1175
- this.staticTargets = staticTargets;
1176
- this.hasDynamicResolvers = hasDynamicResolvers;
1177
- this.cd = cd;
1178
- this.onInit = onInit;
1179
- this._subscribed = new LruSet(maxSubscribedStreams);
1180
- }
1181
- _checkpoint = -1;
1182
- _initialized = false;
1183
- _timer = void 0;
1184
- _subscribed;
1185
- /** Last correlated event id. */
1186
- get checkpoint() {
1187
- return this._checkpoint;
1188
- }
1189
- /**
1190
- * Initialize correlation state on first call.
1191
- * - Reads max(at) from store as cold-start checkpoint
1192
- * - Subscribes static resolver targets (idempotent upsert)
1193
- * - Populates the subscribed-streams LRU
1194
- * - Fires `onInit` once (Act uses this to flag a cold-start drain)
1195
- */
1196
- async init() {
1197
- if (this._initialized) return;
1198
- this._initialized = true;
1199
- const { watermark } = await store().subscribe([...this.staticTargets]);
1200
- this._checkpoint = watermark;
1201
- this.onInit?.();
1202
- for (const { stream } of this.staticTargets) {
1203
- this._subscribed.add(stream);
1204
- }
1205
- }
1206
- /**
1207
- * Discover dynamic-resolver targets in the events past the checkpoint
1208
- * and register any new streams via `cd.subscribe`. Static targets are
1209
- * subscribed at init time, so this only walks dynamic resolvers.
1210
- */
1211
- async correlate(query = { after: -1, limit: 10 }) {
1212
- await this.init();
1213
- if (!this.hasDynamicResolvers)
1214
- return { subscribed: 0, last_id: this._checkpoint };
1215
- const after = Math.max(this._checkpoint, query.after || -1);
1216
- const correlated = /* @__PURE__ */ new Map();
1217
- let last_id = after;
1218
- await store().query(
1219
- (event) => {
1220
- last_id = event.id;
1221
- const register = this.registry.events[event.name];
1222
- if (register) {
1223
- for (const reaction of register.reactions.values()) {
1224
- if (typeof reaction.resolver !== "function") continue;
1225
- const resolved = reaction.resolver(event);
1226
- if (resolved && !this._subscribed.has(resolved.target)) {
1227
- const entry = correlated.get(resolved.target) || {
1228
- source: resolved.source,
1229
- payloads: []
1230
- };
1231
- entry.payloads.push({
1232
- ...reaction,
1233
- source: resolved.source,
1234
- event
1235
- });
1236
- correlated.set(resolved.target, entry);
1237
- }
1238
- }
1239
- }
1240
- },
1241
- { ...query, after }
1242
- );
1243
- if (correlated.size) {
1244
- const streams = [...correlated.entries()].map(([stream, { source }]) => ({
1245
- stream,
1246
- source
1247
- }));
1248
- const { subscribed } = await this.cd.subscribe(streams);
1249
- this._checkpoint = last_id;
1250
- if (subscribed) {
1251
- for (const { stream } of streams) {
1252
- this._subscribed.add(stream);
1253
- }
1254
- }
1255
- return { subscribed, last_id };
1256
- }
1257
- this._checkpoint = last_id;
1258
- return { subscribed: 0, last_id };
1259
- }
1260
- /**
1261
- * Start a periodic correlation worker. Returns false if one is already
1262
- * running. Errors from `correlate()` are sent to `console.error` (matches
1263
- * pre-extraction behavior; the timer keeps running on failure).
1264
- */
1265
- startPolling(query = {}, frequency = 1e4, callback) {
1266
- if (this._timer) return false;
1267
- const limit = query.limit || 100;
1268
- this._timer = setInterval(
1269
- () => this.correlate({ ...query, after: this._checkpoint, limit }).then((result) => {
1270
- if (callback && result.subscribed) callback(result.subscribed);
1271
- }).catch(console.error),
1272
- frequency
1273
- );
1274
- return true;
1275
- }
1276
- /** Stop the periodic correlation worker. Idempotent. */
1277
- stopPolling() {
1278
- if (this._timer) {
1279
- clearInterval(this._timer);
1280
- this._timer = void 0;
1281
- }
1282
- }
1283
- };
1284
-
1285
- // src/internal/drain-cycle.ts
1286
- var import_node_crypto2 = require("crypto");
1287
-
1288
- // src/internal/drain-ratio.ts
1289
- var RATIO_MIN = 0.2;
1290
- var RATIO_MAX = 0.8;
1291
- var RATIO_DEFAULT = 0.5;
1292
- function computeLagLeadRatio(handled, lagging, leading) {
1293
- let lagging_handled = 0;
1294
- let leading_handled = 0;
1295
- for (const { lease, handled: count } of handled) {
1296
- if (lease.lagging) lagging_handled += count;
1297
- else leading_handled += count;
1298
- }
1299
- const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
1300
- const leading_avg = leading > 0 ? leading_handled / leading : 0;
1301
- const total = lagging_avg + leading_avg;
1302
- if (total === 0) return RATIO_DEFAULT;
1303
- return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
1304
- }
1305
-
1306
- // src/internal/drain-cycle.ts
1307
- async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis) {
1308
- const leased = await ops.claim(lagging, leading, (0, import_node_crypto2.randomUUID)(), leaseMillis);
1309
- if (!leased.length) return void 0;
1310
- const fetched = await ops.fetch(leased, eventLimit);
1311
- const fetchMap = /* @__PURE__ */ new Map();
1312
- const fetch_window_at = fetched.reduce(
1313
- (max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
1314
- 0
1315
- );
1316
- for (const f of fetched) {
1317
- const { stream, events } = f;
1318
- const payloads = events.flatMap((event) => {
1319
- const register = registry.events[event.name];
1320
- if (!register) return [];
1321
- return [...register.reactions.values()].filter((reaction) => {
1322
- const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
1323
- return resolved && resolved.target === stream;
1324
- }).map((reaction) => ({ ...reaction, event }));
1325
- });
1326
- fetchMap.set(stream, { fetch: f, payloads });
1327
- }
1328
- const handled = await Promise.all(
1329
- leased.map((lease) => {
1330
- const entry = fetchMap.get(lease.stream);
1331
- const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
1332
- const { payloads } = entry;
1333
- const batchHandler = batchHandlers.get(lease.stream);
1334
- if (batchHandler && payloads.length > 0) {
1335
- return handleBatch({ ...lease, at }, payloads, batchHandler);
1336
- }
1337
- return handle({ ...lease, at }, payloads);
1338
- })
1339
- );
1340
- const acked = await ops.ack(
1341
- handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
1342
- );
1343
- const blocked = await ops.block(
1344
- handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
1345
- );
1346
- return { leased, fetched, handled, acked, blocked };
1347
- }
1348
- var EMPTY_DRAIN = {
1349
- fetched: [],
1350
- leased: [],
1351
- acked: [],
1352
- blocked: []
1353
- };
1354
- var DrainController = class {
1355
- constructor(deps) {
1356
- this.deps = deps;
1357
- }
1358
- _armed = false;
1359
- _locked = false;
1360
- _ratio = 0.5;
1361
- /**
1362
- * Signal that a commit (or reset / cold-start) may have produced work.
1363
- * Subsequent `drain()` calls will run the pipeline; once the pipeline
1364
- * settles to no-progress, the controller disarms itself.
1365
- */
1366
- arm() {
1367
- this._armed = true;
1368
- }
1369
- /** Read-only flag — true while a commit / reset is unprocessed. */
1370
- get armed() {
1371
- return this._armed;
1372
- }
1373
- /** Run one drain pass. Short-circuits when not armed or already running. */
1374
- async drain({
1375
- streamLimit = 10,
1376
- eventLimit = 10,
1377
- leaseMillis = 1e4
1378
- } = {}) {
1379
- if (!this._armed) return EMPTY_DRAIN;
1380
- if (this._locked) return EMPTY_DRAIN;
1381
- try {
1382
- this._locked = true;
1383
- const lagging = Math.ceil(streamLimit * this._ratio);
1384
- const leading = streamLimit - lagging;
1385
- const cycle = await runDrainCycle(
1386
- this.deps.ops,
1387
- this.deps.registry,
1388
- this.deps.batchHandlers,
1389
- this.deps.handle,
1390
- this.deps.handleBatch,
1391
- lagging,
1392
- leading,
1393
- eventLimit,
1394
- leaseMillis
1395
- );
1396
- if (!cycle) {
1397
- this._armed = false;
1398
- return EMPTY_DRAIN;
1399
- }
1400
- const { leased, fetched, handled, acked, blocked } = cycle;
1401
- this._ratio = computeLagLeadRatio(handled, lagging, leading);
1402
- if (acked.length) this.deps.onAcked(acked);
1403
- if (blocked.length) this.deps.onBlocked(blocked);
1404
- const hasErrors = handled.some(({ error }) => error);
1405
- if (!acked.length && !blocked.length && !hasErrors) this._armed = false;
1406
- return { fetched, leased, acked, blocked };
1407
- } catch (error) {
1408
- this.deps.logger.error(error);
1409
- return EMPTY_DRAIN;
1410
- } finally {
1411
- this._locked = false;
1412
- }
1413
- }
1414
- };
1415
-
1416
- // src/internal/merge.ts
1417
- var import_zod4 = require("zod");
1418
- function baseTypeName(zodType) {
1419
- let t = zodType;
1420
- while (typeof t.unwrap === "function") {
1421
- t = t.unwrap();
1422
- }
1423
- return t.constructor.name;
1424
- }
1425
- function mergeSchemas(existing, incoming, stateName) {
1426
- if (existing instanceof import_zod4.ZodObject && incoming instanceof import_zod4.ZodObject) {
1427
- const existingShape = existing.shape;
1428
- const incomingShape = incoming.shape;
1429
- for (const key of Object.keys(incomingShape)) {
1430
- if (key in existingShape) {
1431
- const existingBase = baseTypeName(existingShape[key]);
1432
- const incomingBase = baseTypeName(incomingShape[key]);
1433
- if (existingBase !== incomingBase) {
1434
- throw new Error(
1435
- `Schema conflict in "${stateName}": key "${key}" has type "${existingBase}" but incoming partial declares "${incomingBase}"`
1436
- );
1437
- }
1438
- }
1439
- }
1440
- return existing.extend(incomingShape);
1441
- }
1442
- return existing;
1443
- }
1444
- function mergeInits(existing, incoming) {
1445
- return () => ({ ...existing(), ...incoming() });
1446
- }
1447
- function registerState(state2, states, actions, events) {
1448
- const existing = states.get(state2.name);
1449
- if (existing) {
1450
- mergeIntoExisting(state2, existing, states, actions, events);
1451
- } else {
1452
- registerNewState(state2, states, actions, events);
1453
- }
1454
- }
1455
- function registerNewState(state2, states, actions, events) {
1456
- states.set(state2.name, state2);
1457
- for (const name of Object.keys(state2.actions)) {
1458
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
1459
- actions[name] = state2;
1460
- }
1461
- for (const name of Object.keys(state2.events)) {
1462
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
1463
- events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
1464
- }
1465
- }
1466
- function mergeIntoExisting(state2, existing, states, actions, events) {
1467
- for (const name of Object.keys(state2.actions)) {
1468
- if (existing.actions[name] === state2.actions[name]) continue;
1469
- if (actions[name]) throw new Error(`Duplicate action "${name}"`);
1470
- }
1471
- for (const name of Object.keys(state2.events)) {
1472
- if (existing.events[name] === state2.events[name]) continue;
1473
- if (existing.events[name]) continue;
1474
- if (events[name]) throw new Error(`Duplicate event "${name}"`);
1475
- }
1476
- const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
1477
- const merged = {
1478
- ...existing,
1479
- state: mergeSchemas(existing.state, state2.state, state2.name),
1480
- init: mergeInits(existing.init, state2.init),
1481
- events: { ...existing.events, ...state2.events },
1482
- actions: { ...existing.actions, ...state2.actions },
1483
- patch: mergedPatch,
1484
- on: { ...existing.on, ...state2.on },
1485
- given: { ...existing.given, ...state2.given },
1486
- snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
1487
- throw new Error(
1488
- `Duplicate snap strategy for state "${state2.name}"`
1489
- );
1490
- })() : state2.snap || existing.snap
1491
- };
1492
- states.set(state2.name, merged);
1493
- for (const name of Object.keys(merged.actions)) {
1494
- actions[name] = merged;
1495
- }
1496
- for (const name of Object.keys(state2.events)) {
1497
- if (events[name]) continue;
1498
- events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
1499
- }
1500
- }
1501
- function mergePatches(existing, incoming, stateName) {
1502
- const merged = { ...existing };
1503
- for (const name of Object.keys(incoming)) {
1504
- const existingP = existing[name];
1505
- const incomingP = incoming[name];
1506
- if (!existingP) {
1507
- merged[name] = incomingP;
1508
- continue;
1509
- }
1510
- const existingIsDefault = existingP._passthrough;
1511
- const incomingIsDefault = incomingP._passthrough;
1512
- if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
1513
- throw new Error(
1514
- `Duplicate custom patch for event "${name}" in state "${stateName}"`
1515
- );
1516
- }
1517
- if (existingIsDefault && !incomingIsDefault) {
1518
- merged[name] = incomingP;
1519
- }
1520
- }
1521
- return merged;
1522
- }
1523
- function mergeEventRegister(target, source) {
1524
- for (const [eventName, sourceReg] of Object.entries(source)) {
1525
- const targetReg = target[eventName];
1526
- if (!targetReg) continue;
1527
- for (const [name, reaction] of sourceReg.reactions) {
1528
- targetReg.reactions.set(name, reaction);
1529
- }
1530
- }
1531
- }
1532
- function mergeProjection(proj, events) {
1533
- for (const eventName of Object.keys(proj.events)) {
1534
- const projRegister = proj.events[eventName];
1535
- const existing = events[eventName];
1536
- if (!existing) {
1537
- events[eventName] = {
1538
- schema: projRegister.schema,
1539
- reactions: new Map(projRegister.reactions)
1540
- };
1541
- } else {
1542
- for (const [name, reaction] of projRegister.reactions) {
1543
- let key = name;
1544
- while (existing.reactions.has(key)) key = `${key}_p`;
1545
- existing.reactions.set(key, reaction);
1546
- }
1547
- }
1548
- }
1549
- }
1550
- var _this_ = ({ stream }) => ({
1551
- source: stream,
1552
- target: stream
1553
- });
1554
-
1555
- // src/internal/reactions.ts
1556
- function finalize(lease, handled, at, error, options, logger) {
1557
- if (!error) return { lease, handled, at };
1558
- logger.error(error);
1559
- const block2 = lease.retry >= options.maxRetries && options.blockOnError;
1560
- if (block2)
1561
- logger.error(`Blocking ${lease.stream} after ${lease.retry} retries.`);
1562
- return {
1563
- lease,
1564
- handled,
1565
- at,
1566
- error: handled === 0 ? error.message : void 0,
1567
- block: block2
1568
- };
1569
- }
1570
- function buildHandle(deps) {
1571
- const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
1572
- return async (lease, payloads) => {
1573
- if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
1574
- const stream = lease.stream;
1575
- let at = payloads.at(0).event.id;
1576
- let handled = 0;
1577
- if (lease.retry > 0)
1578
- logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
1579
- const scopedApp = {
1580
- do: boundDo,
1581
- load: boundLoad,
1582
- query: boundQuery,
1583
- query_array: boundQueryArray
1584
- };
1585
- for (const payload of payloads) {
1586
- const { event, handler } = payload;
1587
- scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
1588
- action2,
1589
- target,
1590
- actionPayload,
1591
- reactingTo ?? event,
1592
- skipValidation
1593
- );
1594
- try {
1595
- await handler(event, stream, scopedApp);
1596
- at = event.id;
1597
- handled++;
1598
- } catch (error) {
1599
- return finalize(
1600
- lease,
1601
- handled,
1602
- at,
1603
- error,
1604
- payload.options,
1605
- logger
1606
- );
1607
- }
1608
- }
1609
- return finalize(lease, handled, at, void 0, payloads[0].options, logger);
1610
- };
1611
- }
1612
- function buildHandleBatch(logger) {
1613
- return async (lease, payloads, batchHandler) => {
1614
- const stream = lease.stream;
1615
- const events = payloads.map((p) => p.event);
1616
- const options = payloads[0].options;
1617
- if (lease.retry > 0)
1618
- logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
1619
- try {
1620
- await batchHandler(events, stream);
1621
- return finalize(
1622
- lease,
1623
- events.length,
1624
- events.at(-1).id,
1625
- void 0,
1626
- options,
1627
- logger
1628
- );
1629
- } catch (error) {
1630
- return finalize(lease, 0, lease.at, error, options, logger);
1631
- }
1632
- };
1633
- }
1634
-
1635
- // src/internal/settle.ts
1636
- var SettleLoop = class {
1637
- constructor(deps, defaultDebounceMs) {
1638
- this.deps = deps;
1639
- this.defaultDebounceMs = defaultDebounceMs;
1640
- }
1641
- _timer = void 0;
1642
- _running = false;
1643
- /**
1644
- * Schedule a settle pass. Multiple calls inside the debounce window
1645
- * coalesce into one cycle. The cycle runs correlate→drain in a loop
1646
- * until no progress is made (no new subscriptions, no acks, no blocks)
1647
- * or `maxPasses` is reached, then emits the `"settled"` lifecycle event
1648
- * via {@link SettleDeps.onSettled}.
1649
- */
1650
- schedule(options = {}) {
1651
- const {
1652
- debounceMs = this.defaultDebounceMs,
1653
- correlate: correlateQuery = { after: -1, limit: 100 },
1654
- maxPasses = Infinity,
1655
- ...drainOptions
1656
- } = options;
1657
- if (this._timer) clearTimeout(this._timer);
1658
- this._timer = setTimeout(() => {
1659
- this._timer = void 0;
1660
- if (this._running) return;
1661
- this._running = true;
1662
- (async () => {
1663
- await this.deps.init();
1664
- let lastDrain;
1665
- for (let i = 0; i < maxPasses; i++) {
1666
- const { subscribed } = await this.deps.correlate({
1667
- ...correlateQuery,
1668
- after: this.deps.checkpoint()
1669
- });
1670
- lastDrain = await this.deps.drain(drainOptions);
1671
- const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
1672
- if (!made_progress) break;
1673
- }
1674
- if (lastDrain) this.deps.onSettled(lastDrain);
1675
- })().catch((err) => this.deps.logger.error(err)).finally(() => {
1676
- this._running = false;
1677
- });
1678
- }, debounceMs);
1679
- }
1680
- /** Cancel any pending or active settle cycle. Idempotent. */
1681
- stop() {
1682
- if (this._timer) {
1683
- clearTimeout(this._timer);
1684
- this._timer = void 0;
1685
- }
1686
- }
1687
- };
1688
-
1689
- // src/internal/drain.ts
1690
- var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
1691
- async function fetch(leased, eventLimit) {
1692
- return Promise.all(
1693
- leased.map(async ({ stream, source, at, lagging }) => {
1694
- const events = [];
1695
- await store().query((e) => events.push(e), {
1696
- stream: source,
1697
- after: at,
1698
- limit: eventLimit
1699
- });
1700
- return { stream, source, at, lagging, events };
1701
- })
1702
- );
1703
- }
1704
- var ack = (leases) => store().ack(leases);
1705
- var block = (leases) => store().block(leases);
1706
- var subscribe = (streams) => store().subscribe(streams);
1707
-
1708
- // src/internal/event-sourcing.ts
1709
- var import_node_crypto3 = require("crypto");
1710
- var import_act_patch = require("@rotorsoft/act-patch");
1711
- async function snap(snapshot) {
1712
- try {
1713
- const { id, stream, name, meta, version } = snapshot.event;
1714
- await store().commit(
1715
- stream,
1716
- [{ name: SNAP_EVENT, data: snapshot.state }],
1717
- {
1718
- correlation: meta.correlation,
1719
- causation: { event: { id, name, stream } }
1720
- },
1721
- version
1722
- // IMPORTANT! - state events are committed right after the snapshot event
1723
- );
1724
- } catch (error) {
1725
- log().error(error);
1726
- }
1727
- }
1728
- async function tombstone(stream, expectedVersion, correlation) {
1729
- try {
1730
- const [committed] = await store().commit(
1731
- stream,
1732
- [{ name: TOMBSTONE_EVENT, data: {} }],
1733
- { correlation, causation: {} },
1734
- expectedVersion
1735
- );
1736
- return committed;
1737
- } catch (error) {
1738
- if (error instanceof ConcurrencyError) return void 0;
1739
- throw error;
1740
- }
1741
- }
1742
- async function load(me, stream, callback, asOf) {
1743
- const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
1744
- const cached = timeTravel ? void 0 : await cache().get(stream);
1745
- const cache_hit = !!cached;
1746
- let state2 = cached?.state ?? (me.init ? me.init() : {});
1747
- let patches = cached?.patches ?? 0;
1748
- let snaps = cached?.snaps ?? 0;
1749
- let version = cached?.version ?? -1;
1750
- let replayed = 0;
1751
- let event;
1752
- await store().query(
1753
- (e) => {
1754
- event = e;
1755
- version = e.version;
1756
- if (e.name === SNAP_EVENT) {
1757
- state2 = e.data;
1758
- snaps++;
1759
- patches = 0;
1760
- replayed++;
1761
- } else if (me.patch[e.name]) {
1762
- state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](event, state2));
1763
- patches++;
1764
- replayed++;
1765
- } else if (e.name !== TOMBSTONE_EVENT) {
1766
- log().warn(
1767
- `Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
1768
- );
1769
- }
1770
- callback?.({
1771
- event,
1772
- state: state2,
1773
- version,
1774
- patches,
1775
- snaps,
1776
- cache_hit,
1777
- replayed
1778
- });
1779
- },
1780
- {
1781
- stream,
1782
- stream_exact: true,
1783
- ...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
1784
- }
1785
- );
1786
- if (replayed > 0 && !timeTravel && event) {
1787
- await cache().set(stream, {
1788
- state: state2,
1789
- version,
1790
- event_id: event.id,
1791
- patches,
1792
- snaps
1793
- });
1794
- }
1795
- return { event, state: state2, version, patches, snaps, cache_hit, replayed };
1796
- }
1797
- async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
1798
- const { stream, expectedVersion, actor } = target;
1799
- if (!stream) throw new Error("Missing target stream");
1800
- payload = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
1801
- const snapshot = await load(me, stream);
1802
- if (snapshot.event?.name === TOMBSTONE_EVENT)
1803
- throw new StreamClosedError(stream);
1804
- const expected = expectedVersion ?? snapshot.event?.version;
1805
- if (me.given) {
1806
- const invariants = me.given[action2] || [];
1807
- invariants.forEach(({ valid, description }) => {
1808
- if (!valid(snapshot.state, actor))
1809
- throw new InvariantError(
1810
- action2,
1811
- payload,
1812
- target,
1813
- snapshot,
1814
- description
1815
- );
1816
- });
1817
- }
1818
- const result = me.on[action2](payload, snapshot, target);
1819
- if (!result) return [snapshot];
1820
- if (Array.isArray(result) && result.length === 0) {
1821
- return [snapshot];
1822
- }
1823
- const tuples = Array.isArray(result[0]) ? result : [result];
1824
- const emitted = tuples.map(([name, data]) => ({
1825
- name,
1826
- data: skipValidation ? data : validate(name, data, me.events[name])
1827
- }));
1828
- const meta = {
1829
- correlation: reactingTo?.meta.correlation || (0, import_node_crypto3.randomUUID)(),
1830
- causation: {
1831
- action: {
1832
- name: action2,
1833
- ...target
1834
- // payload intentionally omitted: it can be large or contain PII,
1835
- // and callers correlate via the correlation id when they need it.
1836
- },
1837
- event: reactingTo ? {
1838
- id: reactingTo.id,
1839
- name: reactingTo.name,
1840
- stream: reactingTo.stream
1841
- } : void 0
1842
- }
1843
- };
1844
- let committed;
1845
- try {
1846
- committed = await store().commit(
1847
- stream,
1848
- emitted,
1849
- meta,
1850
- // Reactions skip optimistic concurrency: they always append against the
1851
- // current head. Stream leasing already serializes concurrent reactions,
1852
- // and forcing version checks here would turn ordinary catch-up into
1853
- // spurious retries.
1854
- reactingTo ? void 0 : expected
1855
- );
1856
- } catch (error) {
1857
- if (error instanceof ConcurrencyError) {
1858
- await cache().invalidate(stream);
1859
- }
1860
- throw error;
1861
- }
1862
- let { state: state2, patches } = snapshot;
1863
- const snapshots = committed.map((event) => {
1864
- const p = me.patch[event.name](event, state2);
1865
- state2 = (0, import_act_patch.patch)(state2, p);
1866
- patches++;
1867
- return {
1868
- event,
1869
- state: state2,
1870
- version: event.version,
1871
- patches,
1872
- snaps: snapshot.snaps,
1873
- patch: p,
1874
- cache_hit: snapshot.cache_hit,
1875
- replayed: snapshot.replayed
1876
- };
1877
- });
1878
- const last = snapshots.at(-1);
1879
- const snapped = me.snap?.(last);
1880
- cache().set(stream, {
1881
- state: last.state,
1882
- version: last.event.version,
1883
- event_id: last.event.id,
1884
- patches: snapped ? 0 : last.patches,
1885
- snaps: snapped ? last.snaps + 1 : last.snaps
1886
- }).catch((err) => log().error(err));
1887
- if (snapped) void snap(last);
1888
- return snapshots;
1889
- }
1890
-
1891
- // src/internal/tracing.ts
1892
- var PRETTY = config().env !== "production";
1893
- var C_BLUE = "\x1B[38;5;39m";
1894
- var C_ORANGE = "\x1B[38;5;208m";
1895
- var C_GREEN = "\x1B[38;5;42m";
1896
- var C_MAGENTA = "\x1B[38;5;165m";
1897
- var C_DRAIN = "\x1B[38;5;244m";
1898
- var C_HIT = "\x1B[38;5;82m";
1899
- var C_MISS = "\x1B[38;5;220m";
1900
- var C_RESET = "\x1B[0m";
1901
- var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
1902
- var drain_caption = (caption) => {
1903
- const tag = `>> ${caption}`;
1904
- return PRETTY ? `${C_DRAIN}${tag}${C_RESET}` : tag;
1905
- };
1906
- var cache_marker = (hit) => {
1907
- const word = hit ? "hit" : "miss";
1908
- if (!PRETTY) return word;
1909
- return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
1910
- };
1911
- var stats_marker = (version, replayed, snaps, patches) => {
1912
- const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
1913
- if (!PRETTY) return text;
1914
- return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
1915
- };
1916
- var as_of_marker = (asOf) => {
1917
- if (!asOf) return "";
1918
- const parts = [];
1919
- if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
1920
- if (asOf.created_before !== void 0)
1921
- parts.push(`created_before=${asOf.created_before.toISOString()}`);
1922
- if (asOf.created_after !== void 0)
1923
- parts.push(`created_after=${asOf.created_after.toISOString()}`);
1924
- if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
1925
- return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
1926
- };
1927
- var traced = (inner, exit, entry) => (async (...args) => {
1928
- entry?.(...args);
1929
- const result = await inner(...args);
1930
- exit?.(result, ...args);
1931
- return result;
1932
- });
1933
- function buildEs(logger) {
1934
- if (logger.level !== "trace") {
1935
- return {
1936
- snap,
1937
- load,
1938
- action,
1939
- tombstone
1940
- };
1941
- }
1942
- return {
1943
- snap: traced(snap, void 0, (snapshot) => {
1944
- logger.trace(
1945
- es_caption(
1946
- "snap",
1947
- C_MAGENTA,
1948
- `${snapshot.event.stream}@${snapshot.event.version}`
1949
- )
1950
- );
1951
- }),
1952
- load: traced(load, (result, _me, stream, _cb, asOf) => {
1953
- const stats = stats_marker(
1954
- result.version,
1955
- result.replayed,
1956
- result.snaps,
1957
- result.patches
1958
- );
1959
- logger.trace(
1960
- es_caption(
1961
- "load",
1962
- C_GREEN,
1963
- `${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
1964
- )
1965
- );
1966
- }),
1967
- action: traced(
1968
- action,
1969
- (snapshots, _me, _action, target) => {
1970
- const committed = snapshots.filter((s) => s.event);
1971
- if (committed.length) {
1972
- logger.trace(
1973
- committed.map((s) => s.event.data),
1974
- es_caption(
1975
- "committed",
1976
- C_ORANGE,
1977
- `${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
1978
- )
1979
- );
1980
- }
1981
- },
1982
- (_me, action2, target, payload) => {
1983
- logger.trace(
1984
- payload,
1985
- es_caption("action", C_BLUE, `${target.stream}.${action2}`)
1986
- );
1987
- }
1988
- ),
1989
- tombstone: traced(tombstone, (committed, stream) => {
1990
- if (committed)
1991
- logger.trace(
1992
- es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
1993
- );
1994
- })
1995
- };
1996
- }
1997
- function buildDrain(logger) {
1998
- if (logger.level !== "trace") {
1999
- return {
2000
- claim,
2001
- fetch,
2002
- ack,
2003
- block,
2004
- subscribe
2005
- };
2006
- }
2007
- return {
2008
- claim: traced(claim, (leased) => {
2009
- if (leased.length) {
2010
- const data = Object.fromEntries(
2011
- leased.map(({ stream, at, retry }) => [stream, { at, retry }])
2012
- );
2013
- logger.trace(data, drain_caption("claimed"));
2014
- }
2015
- }),
2016
- fetch: traced(fetch, (fetched) => {
2017
- const data = Object.fromEntries(
2018
- fetched.map(({ stream, source, events }) => {
2019
- const key = source ? `${stream}<-${source}` : stream;
2020
- const value = Object.fromEntries(
2021
- events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
2022
- );
2023
- return [key, value];
2024
- })
2025
- );
2026
- logger.trace(data, drain_caption("fetched"));
2027
- }),
2028
- ack: traced(ack, (acked) => {
2029
- if (acked.length) {
2030
- const data = Object.fromEntries(
2031
- acked.map(({ stream, at, retry }) => [stream, { at, retry }])
2032
- );
2033
- logger.trace(data, drain_caption("acked"));
2034
- }
2035
- }),
2036
- block: traced(block, (blocked) => {
2037
- if (blocked.length) {
2038
- const data = Object.fromEntries(
2039
- blocked.map(({ stream, at, retry, error }) => [
2040
- stream,
2041
- { at, retry, error }
2042
- ])
2043
- );
2044
- logger.trace(data, drain_caption("blocked"));
2045
- }
2046
- }),
2047
- subscribe: traced(subscribe, (result, streams) => {
2048
- if (result.subscribed) {
2049
- const data = streams.map(({ stream }) => stream).join(" ");
2050
- logger.trace(`${drain_caption("correlated")} ${data}`);
2051
- }
2052
- })
2053
- };
2054
- }
2055
-
2056
- // src/act.ts
2057
- var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
2058
- var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
2059
- var Act = class {
2060
- /**
2061
- * Create a new Act orchestrator. Prefer the {@link act} builder over
2062
- * direct construction — `act()...build()` wires the registry, merges
2063
- * partial states, and collects batch handlers from registered slices
2064
- * and projections in one pass.
2065
- *
2066
- * @param registry Schemas for every event and action across registered states
2067
- * @param _states Merged map of state name → state definition
2068
- * @param batchHandlers Static-target projection batch handlers (target → handler)
2069
- * @param options Tuning knobs — see {@link ActOptions}
2070
- */
2071
- constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
2072
- this.registry = registry;
2073
- this._states = _states;
2074
- this._batch_handlers = batchHandlers;
2075
- this._es = buildEs(this._logger);
2076
- this._cd = buildDrain(this._logger);
2077
- this._handle = buildHandle({
2078
- logger: this._logger,
2079
- boundDo: this._bound_do,
2080
- boundLoad: this._bound_load,
2081
- boundQuery: this._bound_query,
2082
- boundQueryArray: this._bound_query_array
2083
- });
2084
- this._handle_batch = buildHandleBatch(this._logger);
2085
- const { staticTargets, hasDynamicResolvers, reactiveEvents, eventToState } = classifyRegistry(this.registry, this._states);
2086
- this._reactive_events = reactiveEvents;
2087
- this._event_to_state = eventToState;
2088
- this._drain = new DrainController({
2089
- logger: this._logger,
2090
- ops: this._cd,
2091
- registry: this.registry,
2092
- batchHandlers: this._batch_handlers,
2093
- handle: this._handle,
2094
- handleBatch: this._handle_batch,
2095
- onAcked: (acked) => this.emit("acked", acked),
2096
- onBlocked: (blocked) => this.emit("blocked", blocked)
2097
- });
2098
- this._correlate = new CorrelateCycle(
2099
- this.registry,
2100
- staticTargets,
2101
- hasDynamicResolvers,
2102
- this._cd,
2103
- options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
2104
- // Cold start: assume drain is needed (historical events may need processing)
2105
- () => {
2106
- if (this._reactive_events.size > 0) this._drain.arm();
2107
- }
2108
- );
2109
- this._settle = new SettleLoop(
2110
- {
2111
- logger: this._logger,
2112
- init: () => this._correlate.init(),
2113
- checkpoint: () => this._correlate.checkpoint,
2114
- correlate: (q) => this.correlate(q),
2115
- drain: (o) => this.drain(o),
2116
- onSettled: (drain) => this.emit("settled", drain)
2117
- },
2118
- options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
2119
- );
2120
- dispose(() => {
2121
- this._emitter.removeAllListeners();
2122
- this.stop_correlations();
2123
- this.stop_settling();
2124
- return Promise.resolve();
2125
- });
2126
- }
2127
- _emitter = new import_node_events.default();
2128
- /** Event names with at least one registered reaction (computed at build time) */
2129
- _reactive_events;
2130
- /** Drain pipeline driver: armed flag, concurrency lock, adaptive ratio. */
2131
- _drain;
2132
- /** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
2133
- _correlate;
2134
- /** Debounced correlate→drain catch-up loop. */
2135
- _settle;
2136
- /**
2137
- * Emit a lifecycle event. The payload type is inferred from the event name
2138
- * via {@link ActLifecycleEvents}.
2139
- */
2140
- emit(event, args) {
2141
- return this._emitter.emit(event, args);
2142
- }
2143
- /**
2144
- * Register a listener for a lifecycle event. The listener receives the
2145
- * event-specific payload.
2146
- */
2147
- on(event, listener) {
2148
- this._emitter.on(event, listener);
2149
- return this;
2150
- }
2151
- /**
2152
- * Remove a previously registered lifecycle listener.
2153
- */
2154
- off(event, listener) {
2155
- this._emitter.off(event, listener);
2156
- return this;
2157
- }
2158
- /** Batch handlers for static-target projections (target → handler) */
2159
- _batch_handlers;
2160
- /** Event-sourcing handlers, optionally wrapped with trace decorators */
2161
- _es;
2162
- /** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
2163
- _cd;
2164
- /**
2165
- * Event-name → owning state, computed at build time. The duplicate-event
2166
- * guard in merge.ts ensures one event name maps to at most one state, so
2167
- * this lookup is unambiguous. Used by `close()` to pick the right reducer
2168
- * set when seeding a `restart` snapshot in multi-state apps.
2169
- */
2170
- _event_to_state;
2171
- /** Logger resolved at construction time (after user port configuration) */
2172
- _logger = log();
2173
- /** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
2174
- * payload (it captures the triggering event for reactingTo auto-inject). */
2175
- _bound_do = this.do.bind(this);
2176
- _bound_load = this.load.bind(this);
2177
- _bound_query = this.query.bind(this);
2178
- _bound_query_array = this.query_array.bind(this);
2179
- /** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
2180
- _handle;
2181
- _handle_batch;
2182
- /**
2183
- * Executes an action on a state instance, committing resulting events.
2184
- *
2185
- * This is the primary method for modifying state. It:
2186
- * 1. Validates the action payload against the schema
2187
- * 2. Loads the current state snapshot
2188
- * 3. Checks invariants (business rules)
2189
- * 4. Executes the action handler to generate events
2190
- * 5. Applies events to create new state
2191
- * 6. Commits events to the store with optimistic concurrency control
2192
- *
2193
- * @template TKey - Action name from registered actions
2194
- * @param action - The name of the action to execute
2195
- * @param target - Target specification with stream ID and actor context
2196
- * @param payload - Action payload matching the action's schema
2197
- * @param reactingTo - Optional event that triggered this action (for correlation)
2198
- * @param skipValidation - Skip schema validation (use carefully, for performance)
2199
- * @returns Array of snapshots for all affected states (usually one)
2200
- *
2201
- * @throws {ValidationError} If payload doesn't match action schema
2202
- * @throws {InvariantError} If business rules are violated
2203
- * @throws {ConcurrencyError} If another process modified the stream
2204
- *
2205
- * @example Basic action execution
2206
- * ```typescript
2207
- * const snapshots = await app.do(
2208
- * "increment",
2209
- * {
2210
- * stream: "counter-1",
2211
- * actor: { id: "user1", name: "Alice" }
2212
- * },
2213
- * { by: 5 }
2214
- * );
2215
- *
2216
- * console.log(snapshots[0].state.count); // Current count after increment
2217
- * ```
2218
- *
2219
- * @example With error handling
2220
- * ```typescript
2221
- * try {
2222
- * await app.do(
2223
- * "withdraw",
2224
- * { stream: "account-123", actor: { id: "user1", name: "Alice" } },
2225
- * { amount: 1000 }
2226
- * );
2227
- * } catch (error) {
2228
- * if (error instanceof InvariantError) {
2229
- * console.error("Business rule violated:", error.description);
2230
- * } else if (error instanceof ConcurrencyError) {
2231
- * console.error("Concurrent modification detected, retry...");
2232
- * } else if (error instanceof ValidationError) {
2233
- * console.error("Invalid payload:", error.details);
2234
- * }
2235
- * }
2236
- * ```
2237
- *
2238
- * @example Reaction triggering another action (reactingTo auto-injected)
2239
- * ```typescript
2240
- * const app = act()
2241
- * .withState(Order)
2242
- * .withState(Inventory)
2243
- * .on("OrderPlaced")
2244
- * .do(async function reduceInventory(event, _stream, app) {
2245
- * // Inside reaction handlers, reactingTo is auto-injected when omitted.
2246
- * // The triggering event is used by default, maintaining the correlation chain.
2247
- * await app.do(
2248
- * "reduceStock",
2249
- * { stream: "inventory-1", actor: { id: "sys", name: "system" } },
2250
- * { amount: event.data.items.length }
2251
- * );
2252
- * // To use a different correlation, pass reactingTo explicitly:
2253
- * // await app.do("reduceStock", target, payload, customEvent);
2254
- * })
2255
- * .to("inventory-1")
2256
- * .build();
2257
- * ```
2258
- *
2259
- * @see {@link Target} for target structure
2260
- * @see {@link Snapshot} for return value structure
2261
- * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
2262
- */
2263
- async do(action2, target, payload, reactingTo, skipValidation = false) {
2264
- const snapshots = await this._es.action(
2265
- this.registry.actions[action2],
2266
- action2,
2267
- target,
2268
- payload,
2269
- reactingTo,
2270
- skipValidation
2271
- );
2272
- if (this._reactive_events.size > 0) {
2273
- for (const snap2 of snapshots) {
2274
- if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
2275
- this._drain.arm();
2276
- break;
2277
- }
2278
- }
2279
- }
2280
- this.emit("committed", snapshots);
2281
- return snapshots;
2282
- }
2283
- async load(stateOrName, stream, callback, asOf) {
2284
- let merged;
2285
- if (typeof stateOrName === "string") {
2286
- const found = this._states.get(stateOrName);
2287
- if (!found) throw new Error(`State "${stateOrName}" not found`);
2288
- merged = found;
2289
- } else {
2290
- merged = this._states.get(stateOrName.name) || stateOrName;
2291
- }
2292
- return await this._es.load(merged, stream, callback, asOf);
2293
- }
2294
- /**
2295
- * Queries the event store for events matching a filter.
2296
- *
2297
- * Use this for analyzing event streams, generating reports, or debugging.
2298
- * The callback is invoked for each matching event, and the method returns
2299
- * summary information (first event, last event, total count).
2300
- *
2301
- * For small result sets, consider using {@link query_array} instead.
2302
- *
2303
- * @param query - Filter criteria — see {@link Query} for available fields
2304
- * (`stream`, `name`, `after`, `before`, `created_after`, `created_before`,
2305
- * `limit`, `with_snaps`, `stream_exact`)
2306
- * @param callback - Optional callback invoked for each matching event
2307
- * @returns Object with first event, last event, and total count
2308
- *
2309
- * @example Query all events for a stream
2310
- * ```typescript
2311
- * const { first, last, count } = await app.query(
2312
- * { stream: "counter-1" },
2313
- * (event) => console.log(event.name, event.data)
2314
- * );
2315
- * console.log(`Found ${count} events from ${first?.id} to ${last?.id}`);
2316
- * ```
2317
- *
2318
- * @example Query specific event types
2319
- * ```typescript
2320
- * const { count } = await app.query(
2321
- * { name: "UserCreated", limit: 100 },
2322
- * (event) => {
2323
- * console.log("User created:", event.data.email);
2324
- * }
2325
- * );
2326
- * ```
2327
- *
2328
- * @example Query events in time range
2329
- * ```typescript
2330
- * const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
2331
- * const { count } = await app.query({
2332
- * created_after: yesterday,
2333
- * stream: "user-123"
2334
- * });
2335
- * console.log(`User had ${count} events in last 24 hours`);
2336
- * ```
2337
- *
2338
- * @see {@link query_array} for loading events into memory
2339
- */
2340
- async query(query, callback) {
2341
- let first;
2342
- let last;
2343
- const count = await store().query((e) => {
2344
- if (!first) first = e;
2345
- last = e;
2346
- callback?.(e);
2347
- }, query);
2348
- return { first, last, count };
2349
- }
2350
- /**
2351
- * Queries the event store and returns all matching events in memory.
2352
- *
2353
- * **Use with caution** - this loads all results into memory. For large result sets,
2354
- * use {@link query} with a callback instead to process events incrementally.
2355
- *
2356
- * @param query - The query filter (same as {@link query})
2357
- * @returns Array of all matching events
2358
- *
2359
- * @example Load all events for a stream
2360
- * ```typescript
2361
- * const events = await app.query_array({ stream: "counter-1" });
2362
- * console.log(`Loaded ${events.length} events`);
2363
- * events.forEach(event => console.log(event.name, event.data));
2364
- * ```
2365
- *
2366
- * @example Get recent events
2367
- * ```typescript
2368
- * const recent = await app.query_array({
2369
- * stream: "user-123",
2370
- * limit: 10
2371
- * });
2372
- * ```
2373
- *
2374
- * @see {@link query} for large result sets
2375
- */
2376
- async query_array(query) {
2377
- const events = [];
2378
- await store().query((e) => events.push(e), query);
2379
- return events;
2380
- }
2381
- /**
2382
- * Processes pending reactions by draining uncommitted events from the event store.
2383
- *
2384
- * Runs a single drain cycle:
2385
- * 1. Polls the store for streams with uncommitted events
2386
- * 2. Leases streams to prevent concurrent processing
2387
- * 3. Fetches events for each leased stream
2388
- * 4. Executes matching reaction handlers
2389
- * 5. Acknowledges successful reactions or blocks failing ones
2390
- *
2391
- * Drain uses a dual-frontier strategy to balance processing of new streams (lagging)
2392
- * vs active streams (leading). The ratio adapts based on event pressure.
2393
- *
2394
- * Call `correlate()` before `drain()` to discover target streams. For a higher-level
2395
- * API that handles debouncing, correlation, and signaling automatically, use {@link settle}.
2396
- *
2397
- * @param options - Drain configuration — see {@link DrainOptions} for fields
2398
- * (`streamLimit`, `eventLimit`, `leaseMillis`).
2399
- * @returns Drain statistics with fetched, leased, acked, and blocked counts
2400
- *
2401
- * @example In tests and scripts
2402
- * ```typescript
2403
- * await app.do("createUser", target, payload);
2404
- * await app.correlate();
2405
- * await app.drain();
2406
- * ```
2407
- *
2408
- * @example In production, prefer settle()
2409
- * ```typescript
2410
- * await app.do("CreateItem", target, input);
2411
- * app.settle(); // debounced correlate→drain, emits "settled"
2412
- * ```
2413
- *
2414
- * @see {@link settle} for debounced correlate→drain with lifecycle events
2415
- * @see {@link correlate} for dynamic stream discovery
2416
- * @see {@link start_correlations} for automatic correlation
2417
- */
2418
- async drain(options = {}) {
2419
- return this._drain.drain(options);
2420
- }
2421
- /**
2422
- * Discovers and registers new streams dynamically based on reaction resolvers.
2423
- *
2424
- * Correlation enables "dynamic reactions" where target streams are determined at runtime
2425
- * based on event content. For example, you might create a stats stream for each user
2426
- * when they perform certain actions.
2427
- *
2428
- * This method scans events matching the query and identifies new target streams based
2429
- * on reaction resolvers. It then registers these streams so they'll be picked up by
2430
- * the next drain cycle.
2431
- *
2432
- * @param query - Query filter to scan for new correlations
2433
- * @param query - Scan filter — see {@link Query} for fields (typically
2434
- * `{ after: <event-id>, limit: <count> }`)
2435
- * @returns Object with newly leased streams and last scanned event ID
2436
- *
2437
- * @example Manual correlation
2438
- * ```typescript
2439
- * // Scan for new streams
2440
- * const { leased, last_id } = await app.correlate({ after: 0, limit: 100 });
2441
- * console.log(`Found ${leased.length} new streams`);
2442
- *
2443
- * // Save last_id for next scan
2444
- * await saveCheckpoint(last_id);
2445
- * ```
2446
- *
2447
- * @example Dynamic stream creation
2448
- * ```typescript
2449
- * const app = act()
2450
- * .withState(User)
2451
- * .withState(UserStats)
2452
- * .on("UserLoggedIn")
2453
- * .do(async (event) => ["incrementLoginCount", {}])
2454
- * .to((event) => ({
2455
- * target: `stats-${event.stream}` // Dynamic target per user
2456
- * }))
2457
- * .build();
2458
- *
2459
- * // Discover stats streams as users log in
2460
- * await app.correlate();
2461
- * ```
2462
- *
2463
- * @see {@link start_correlations} for automatic periodic correlation
2464
- * @see {@link stop_correlations} to stop automatic correlation
2465
- */
2466
- async correlate(query = { after: -1, limit: 10 }) {
2467
- return this._correlate.correlate(query);
2468
- }
2469
- /**
2470
- * Starts automatic periodic correlation worker for discovering new streams.
2471
- *
2472
- * The correlation worker runs in the background, scanning for new events and identifying
2473
- * new target streams based on reaction resolvers. It maintains a sliding window that
2474
- * advances with each scan, ensuring all events are eventually correlated.
2475
- *
2476
- * This is useful for dynamic stream creation patterns where you don't know all streams
2477
- * upfront - they're discovered as events arrive.
2478
- *
2479
- * **Note:** Only one correlation worker can run at a time per Act instance.
2480
- *
2481
- * @param query - Query filter for correlation scans — see {@link Query}
2482
- * (typically `{ after: -1, limit: 100 }`)
2483
- * @param frequency - Correlation frequency in milliseconds (default: 10000)
2484
- * @param callback - Optional callback invoked with newly discovered streams
2485
- * @returns `true` if worker started, `false` if already running
2486
- *
2487
- * @example Start automatic correlation
2488
- * ```typescript
2489
- * // Start correlation worker scanning every 5 seconds
2490
- * app.start_correlations(
2491
- * { after: 0, limit: 100 },
2492
- * 5000,
2493
- * (leased) => {
2494
- * console.log(`Discovered ${leased.length} new streams`);
2495
- * }
2496
- * );
2497
- *
2498
- * // Later, stop it
2499
- * app.stop_correlations();
2500
- * ```
2501
- *
2502
- * @example With checkpoint persistence
2503
- * ```typescript
2504
- * // Load last checkpoint
2505
- * const lastId = await loadCheckpoint();
2506
- *
2507
- * app.start_correlations(
2508
- * { after: lastId, limit: 100 },
2509
- * 10000,
2510
- * async (leased) => {
2511
- * // Save checkpoint for next restart
2512
- * if (leased.length) {
2513
- * const maxId = Math.max(...leased.map(l => l.at));
2514
- * await saveCheckpoint(maxId);
2515
- * }
2516
- * }
2517
- * );
2518
- * ```
2519
- *
2520
- * @see {@link correlate} for manual one-time correlation
2521
- * @see {@link stop_correlations} to stop the worker
2522
- */
2523
- start_correlations(query = {}, frequency = 1e4, callback) {
2524
- return this._correlate.startPolling(query, frequency, callback);
2525
- }
2526
- /**
2527
- * Stops the automatic correlation worker.
2528
- *
2529
- * Call this to stop the background correlation worker started by {@link start_correlations}.
2530
- * This is automatically called when the Act instance is disposed.
2531
- *
2532
- * @example
2533
- * ```typescript
2534
- * // Start correlation
2535
- * app.start_correlations();
2536
- *
2537
- * // Later, stop it
2538
- * app.stop_correlations();
2539
- * ```
2540
- *
2541
- * @see {@link start_correlations}
2542
- */
2543
- stop_correlations() {
2544
- this._correlate.stopPolling();
2545
- }
2546
- /**
2547
- * Cancels any pending or active settle cycle.
2548
- *
2549
- * @see {@link settle}
2550
- */
2551
- stop_settling() {
2552
- this._settle.stop();
2553
- }
2554
- /**
2555
- * Reset reaction stream watermarks and request a drain on the next
2556
- * `drain()` / `settle()` cycle.
2557
- *
2558
- * Use this to replay events through projections (or other reaction targets)
2559
- * after changing handler logic. Equivalent to calling `store().reset(streams)`
2560
- * directly, but also raises the orchestrator's internal "needs drain" flag —
2561
- * `store().reset(...)` alone leaves the flag untouched, so a settled app
2562
- * would short-circuit and skip the replay.
2563
- *
2564
- * Pair with `app.settle()` (or a single `app.drain()` for small streams).
2565
- * `settle()` loops correlate→drain until no progress is made, so one call
2566
- * fully catches up paginated streams without forcing callers to roll
2567
- * their own loop.
2568
- *
2569
- * @param streams - Reaction target streams (e.g., projection names) to reset
2570
- * @returns Count of streams that were actually reset
2571
- *
2572
- * @example Rebuild a projection (production)
2573
- * ```typescript
2574
- * await app.reset(["my-projection"]);
2575
- * app.settle({ eventLimit: 1000 }); // emits "settled" when fully replayed
2576
- * ```
2577
- *
2578
- * @example Rebuild a projection (tests / scripts)
2579
- * ```typescript
2580
- * await app.reset(["my-projection"]);
2581
- * await app.drain({ eventLimit: 1000 }); // small streams: one pass is enough
2582
- * ```
2583
- *
2584
- * @see {@link Store.reset} for the underlying store primitive
2585
- * @see {@link settle} for the debounced full-catch-up loop
2586
- */
2587
- async reset(streams) {
2588
- const count = await store().reset(streams);
2589
- if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
2590
- return count;
2591
- }
2592
- /**
2593
- * Close the books — guard, archive, truncate, and optionally restart streams.
2594
- *
2595
- * Safely removes historical events from the operational store:
2596
- *
2597
- * 1. **Correlate** — discover pending reaction targets
2598
- * 2. **Safety check** — skip streams with pending reactions (skipped when no reactive events)
2599
- * 3. **Guard** — commit `__tombstone__` with `expectedVersion` to block concurrent writes
2600
- * 4. **Load state** — for streams in `snapshots`, load final state while guarded (no races)
2601
- * 5. **Archive** — user callback per stream (abort-all on failure, streams are guarded)
2602
- * 6. **Truncate + seed** — atomic: delete all events, insert `__snapshot__` or `__tombstone__`
2603
- * 7. **Cache** — invalidate (tombstoned) or warm (restarted)
2604
- * 8. **Emit "closed"** — lifecycle event with results
2605
- *
2606
- * @param targets - Per-stream close options (stream, restart?, archive?)
2607
- * @returns `{ truncated: TruncateResult, skipped: string[] }`
2608
- *
2609
- * @example Archive and close
2610
- * ```typescript
2611
- * await app.close([
2612
- * { stream: "order-123", archive: async () => { await archiveToS3("order-123"); } },
2613
- * { stream: "order-456" },
2614
- * ]);
2615
- * ```
2616
- *
2617
- * @example Close with restart (state loaded automatically after guard)
2618
- * ```typescript
2619
- * await app.close([
2620
- * { stream: "counter-1", restart: true },
2621
- * { stream: "counter-2" }, // tombstoned
2622
- * ]);
2623
- * ```
2624
- */
2625
- async close(targets) {
2626
- if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
2627
- await this.correlate({ limit: 1e3 });
2628
- const result = await runCloseCycle(targets, {
2629
- reactiveEventsSize: this._reactive_events.size,
2630
- eventToState: this._event_to_state,
2631
- load: this._es.load,
2632
- tombstone: this._es.tombstone,
2633
- logger: this._logger
2634
- });
2635
- this.emit("closed", result);
2636
- return result;
2637
- }
2638
- /**
2639
- * Debounced, non-blocking correlate→drain cycle.
2640
- *
2641
- * Call this after `app.do()` (or `app.reset()`) to schedule a background
2642
- * drain. Multiple rapid calls within the debounce window are coalesced
2643
- * into a single cycle. Runs correlate→drain in a loop until a pass makes
2644
- * no progress — no new subscriptions, no acks, no blocks — then emits
2645
- * the `"settled"` lifecycle event. This means a single `settle()` call
2646
- * fully catches up paginated streams (e.g. after `reset()` on a long
2647
- * projection) without forcing callers to loop.
2648
- *
2649
- * @param options - Settle configuration — see {@link SettleOptions} for fields:
2650
- * `debounceMs` (default 10), `correlate` (default `{ after: -1, limit: 100 }`),
2651
- * `maxPasses` (default `Infinity` — kill-switch for runaway loops),
2652
- * `streamLimit` (default 10), `eventLimit` (default 10),
2653
- * `leaseMillis` (default 10000).
2654
- *
2655
- * @example API mutations
2656
- * ```typescript
2657
- * await app.do("CreateItem", target, input);
2658
- * app.settle(); // non-blocking, returns immediately
2659
- *
2660
- * app.on("settled", (drain) => {
2661
- * // notify SSE clients, invalidate caches, etc.
2662
- * });
2663
- * ```
2664
- *
2665
- * @see {@link drain} for single synchronous drain cycles
2666
- * @see {@link correlate} for manual correlation
2667
- */
2668
- settle(options = {}) {
2669
- this._settle.schedule(options);
2670
- }
2671
- };
2672
-
2673
- // src/builders/act-builder.ts
2674
- function registerBatchHandler(proj, batchHandlers) {
2675
- if (!proj.batchHandler || !proj.target) return;
2676
- const existing = batchHandlers.get(proj.target);
2677
- if (existing && existing !== proj.batchHandler) {
2678
- throw new Error(`Duplicate batch handler for target "${proj.target}"`);
2679
- }
2680
- batchHandlers.set(proj.target, proj.batchHandler);
2681
- }
2682
- function act() {
2683
- const states = /* @__PURE__ */ new Map();
2684
- const registry = {
2685
- actions: {},
2686
- events: {}
2687
- };
2688
- const pendingProjections = [];
2689
- const batchHandlers = /* @__PURE__ */ new Map();
2690
- const builder = {
2691
- withState: (state2) => {
2692
- registerState(state2, states, registry.actions, registry.events);
2693
- return builder;
2694
- },
2695
- withSlice: (input) => {
2696
- for (const s of input.states.values()) {
2697
- registerState(s, states, registry.actions, registry.events);
2698
- }
2699
- mergeEventRegister(registry.events, input.events);
2700
- pendingProjections.push(...input.projections);
2701
- return builder;
2702
- },
2703
- withProjection: (proj) => {
2704
- mergeProjection(proj, registry.events);
2705
- registerBatchHandler(proj, batchHandlers);
2706
- return builder;
2707
- },
2708
- withActor: () => builder,
2709
- on: (event) => ({
2710
- do: (handler, options) => {
2711
- const reaction = {
2712
- handler,
2713
- resolver: _this_,
2714
- options: {
2715
- blockOnError: options?.blockOnError ?? true,
2716
- maxRetries: options?.maxRetries ?? 3
2717
- }
2718
- };
2719
- if (!handler.name)
2720
- throw new Error(
2721
- `Reaction handler for "${String(event)}" must be a named function`
2722
- );
2723
- registry.events[event].reactions.set(handler.name, reaction);
2724
- return Object.assign(builder, {
2725
- to(resolver) {
2726
- reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
2727
- return builder;
2728
- }
2729
- });
2730
- }
2731
- }),
2732
- build: (options) => {
2733
- for (const proj of pendingProjections) {
2734
- mergeProjection(proj, registry.events);
2735
- registerBatchHandler(proj, batchHandlers);
2736
- }
2737
- return new Act(
2738
- registry,
2739
- states,
2740
- batchHandlers,
2741
- options
2742
- );
2743
- },
2744
- events: registry.events
2745
- };
2746
- return builder;
2747
- }
2748
-
2749
- // src/builders/projection-builder.ts
2750
- function _projection(target) {
2751
- const events = {};
2752
- const defaultResolver = typeof target === "string" ? { target } : void 0;
2753
- const base = {
2754
- on: (entry) => {
2755
- const keys = Object.keys(entry);
2756
- if (keys.length !== 1) throw new Error(".on() requires exactly one key");
2757
- const event = keys[0];
2758
- const schema = entry[event];
2759
- if (!(event in events)) {
2760
- events[event] = {
2761
- schema,
2762
- reactions: /* @__PURE__ */ new Map()
2763
- };
2764
- }
2765
- return {
2766
- do: (handler) => {
2767
- const reaction = {
2768
- handler,
2769
- resolver: defaultResolver ?? _this_,
2770
- options: {
2771
- blockOnError: true,
2772
- maxRetries: 3
2773
- }
2774
- };
2775
- const register = events[event];
2776
- if (!handler.name)
2777
- throw new Error(
2778
- `Projection handler for "${event}" must be a named function`
2779
- );
2780
- register.reactions.set(handler.name, reaction);
2781
- const widened = base;
2782
- return Object.assign(widened, {
2783
- to(resolver) {
2784
- reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
2785
- return widened;
2786
- }
2787
- });
2788
- }
2789
- };
2790
- },
2791
- build: () => ({
2792
- _tag: "Projection",
2793
- events,
2794
- ...target !== void 0 && { target }
2795
- }),
2796
- events
2797
- };
2798
- if (typeof target === "string") {
2799
- return Object.assign(base, {
2800
- batch: (handler) => ({
2801
- build: () => ({
2802
- _tag: "Projection",
2803
- events,
2804
- target,
2805
- batchHandler: handler
2806
- })
2807
- })
2808
- });
2809
- }
2810
- return base;
2811
- }
2812
- function projection(target) {
2813
- return _projection(target);
2814
- }
2815
-
2816
- // src/builders/slice-builder.ts
2817
- function slice() {
2818
- const states = /* @__PURE__ */ new Map();
2819
- const actions = {};
2820
- const events = {};
2821
- const projections = [];
2822
- const builder = {
2823
- withState: (state2) => {
2824
- registerState(state2, states, actions, events);
2825
- return builder;
2826
- },
2827
- withProjection: (proj) => {
2828
- projections.push(proj);
2829
- return builder;
2830
- },
2831
- on: (event) => ({
2832
- do: (handler, options) => {
2833
- const reaction = {
2834
- handler,
2835
- resolver: _this_,
2836
- options: {
2837
- blockOnError: options?.blockOnError ?? true,
2838
- maxRetries: options?.maxRetries ?? 3
2839
- }
2840
- };
2841
- if (!handler.name)
2842
- throw new Error(
2843
- `Reaction handler for "${String(event)}" must be a named function`
2844
- );
2845
- events[event].reactions.set(handler.name, reaction);
2846
- return Object.assign(builder, {
2847
- to(resolver) {
2848
- reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
2849
- return builder;
2850
- }
2851
- });
2852
- }
2853
- }),
2854
- build: () => ({
2855
- _tag: "Slice",
2856
- states,
2857
- events,
2858
- projections
2859
- }),
2860
- events
2861
- };
2862
- return builder;
2863
- }
2864
-
2865
- // src/builders/state-builder.ts
2866
- function state(entry) {
2867
- const keys = Object.keys(entry);
2868
- if (keys.length !== 1) throw new Error("state() requires exactly one key");
2869
- const name = keys[0];
2870
- const stateSchema = entry[name];
2871
- return {
2872
- init(init) {
2873
- return {
2874
- emits(events) {
2875
- const defaultPatch = Object.fromEntries(
2876
- Object.keys(events).map((k) => {
2877
- const fn = Object.assign(({ data }) => data, {
2878
- _passthrough: true
2879
- });
2880
- return [k, fn];
2881
- })
2882
- );
2883
- const internal = {
2884
- events,
2885
- actions: {},
2886
- state: stateSchema,
2887
- name,
2888
- init,
2889
- patch: defaultPatch,
2890
- on: {}
2891
- };
2892
- const builder = action_builder(internal);
2893
- return Object.assign(builder, {
2894
- patch(customPatch) {
2895
- Object.assign(internal.patch, customPatch);
2896
- return builder;
2897
- }
2898
- });
2899
- }
2900
- };
2901
- }
2902
- };
2903
- }
2904
- function action_builder(state2) {
2905
- const internal = state2;
2906
- const builder = {
2907
- on(entry) {
2908
- const keys = Object.keys(entry);
2909
- if (keys.length !== 1) throw new Error(".on() requires exactly one key");
2910
- const action2 = keys[0];
2911
- const schema = entry[action2];
2912
- if (action2 in internal.actions)
2913
- throw new Error(`Duplicate action "${action2}"`);
2914
- internal.actions[action2] = schema;
2915
- function given(rules) {
2916
- internal.given ??= {};
2917
- internal.given[action2] = rules;
2918
- return { emit };
2919
- }
2920
- function emit(handler) {
2921
- if (typeof handler === "string") {
2922
- const eventName = handler;
2923
- internal.on[action2] = (payload) => [
2924
- eventName,
2925
- payload
2926
- ];
2927
- } else {
2928
- internal.on[action2] = handler;
2929
- }
2930
- return builder;
2931
- }
2932
- return { given, emit };
2933
- },
2934
- snap(snap2) {
2935
- internal.snap = snap2;
2936
- return builder;
2937
- },
2938
- build() {
2939
- return internal;
2940
- }
2941
- };
2942
- return builder;
2943
- }
2944
- // Annotate the CommonJS export names for ESM import in node:
2945
- 0 && (module.exports = {
2946
- Act,
2947
- ActorSchema,
2948
- CausationEventSchema,
2949
- CommittedMetaSchema,
2950
- ConcurrencyError,
2951
- ConsoleLogger,
2952
- DEFAULT_MAX_SUBSCRIBED_STREAMS,
2953
- DEFAULT_SETTLE_DEBOUNCE_MS,
2954
- Environments,
2955
- Errors,
2956
- EventMetaSchema,
2957
- ExitCodes,
2958
- InMemoryCache,
2959
- InMemoryStore,
2960
- InvariantError,
2961
- LogLevels,
2962
- PackageSchema,
2963
- QuerySchema,
2964
- SNAP_EVENT,
2965
- StreamClosedError,
2966
- TOMBSTONE_EVENT,
2967
- TargetSchema,
2968
- ValidationError,
2969
- ZodEmpty,
2970
- act,
2971
- cache,
2972
- config,
2973
- dispose,
2974
- disposeAndExit,
2975
- extend,
2976
- log,
2977
- port,
2978
- projection,
2979
- sleep,
2980
- slice,
2981
- state,
2982
- store,
2983
- validate
2984
- });
2985
- //# sourceMappingURL=index.cjs.map