@rotorsoft/act 0.35.1 → 0.35.2

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