@rotorsoft/act 0.35.1 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/@types/act.d.ts +672 -0
- package/dist/@types/act.d.ts.map +1 -0
- package/dist/@types/adapters/console-logger.d.ts +41 -0
- package/dist/@types/adapters/console-logger.d.ts.map +1 -0
- package/dist/@types/adapters/in-memory-cache.d.ts +34 -0
- package/dist/@types/adapters/in-memory-cache.d.ts.map +1 -0
- package/dist/@types/adapters/in-memory-store.d.ts +202 -0
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -0
- package/dist/@types/adapters/index.d.ts +4 -0
- package/dist/@types/adapters/index.d.ts.map +1 -0
- package/dist/@types/builders/act-builder.d.ts +160 -0
- package/dist/@types/builders/act-builder.d.ts.map +1 -0
- package/dist/@types/builders/index.d.ts +13 -0
- package/dist/@types/builders/index.d.ts.map +1 -0
- package/dist/@types/builders/projection-builder.d.ts +101 -0
- package/dist/@types/builders/projection-builder.d.ts.map +1 -0
- package/dist/@types/builders/slice-builder.d.ts +109 -0
- package/dist/@types/builders/slice-builder.d.ts.map +1 -0
- package/dist/@types/builders/state-builder.d.ts +424 -0
- package/dist/@types/builders/state-builder.d.ts.map +1 -0
- package/dist/@types/config.d.ts +119 -0
- package/dist/@types/config.d.ts.map +1 -0
- package/dist/@types/index.d.ts +14 -0
- package/dist/@types/index.d.ts.map +1 -0
- package/dist/@types/internal/build-classify.d.ts +44 -0
- package/dist/@types/internal/build-classify.d.ts.map +1 -0
- package/dist/@types/internal/close-cycle.d.ts +38 -0
- package/dist/@types/internal/close-cycle.d.ts.map +1 -0
- package/dist/@types/internal/correlate-cycle.d.ts +78 -0
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -0
- package/dist/@types/internal/drain-cycle.d.ts +113 -0
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -0
- package/dist/@types/internal/drain-ratio.d.ts +26 -0
- package/dist/@types/internal/drain-ratio.d.ts.map +1 -0
- package/dist/@types/internal/drain.d.ts +41 -0
- package/dist/@types/internal/drain.d.ts.map +1 -0
- package/dist/@types/internal/event-sourcing.d.ts +96 -0
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -0
- package/dist/@types/internal/index.d.ts +29 -0
- package/dist/@types/internal/index.d.ts.map +1 -0
- package/dist/@types/internal/merge.d.ts +31 -0
- package/dist/@types/internal/merge.d.ts.map +1 -0
- package/dist/@types/internal/reactions.d.ts +54 -0
- package/dist/@types/internal/reactions.d.ts.map +1 -0
- package/dist/@types/internal/settle.d.ts +60 -0
- package/dist/@types/internal/settle.d.ts.map +1 -0
- package/dist/@types/internal/tracing.d.ts +45 -0
- package/dist/@types/internal/tracing.d.ts.map +1 -0
- package/dist/@types/lru-map.d.ts +50 -0
- package/dist/@types/lru-map.d.ts.map +1 -0
- package/dist/@types/ports.d.ts +196 -0
- package/dist/@types/ports.d.ts.map +1 -0
- package/dist/@types/signals.d.ts +2 -0
- package/dist/@types/signals.d.ts.map +1 -0
- package/dist/@types/types/action.d.ts +444 -0
- package/dist/@types/types/action.d.ts.map +1 -0
- package/dist/@types/types/errors.d.ts +284 -0
- package/dist/@types/types/errors.d.ts.map +1 -0
- package/dist/@types/types/index.d.ts +39 -0
- package/dist/@types/types/index.d.ts.map +1 -0
- package/dist/@types/types/ports.d.ts +617 -0
- package/dist/@types/types/ports.d.ts.map +1 -0
- package/dist/@types/types/reaction.d.ts +314 -0
- package/dist/@types/types/reaction.d.ts.map +1 -0
- package/dist/@types/types/registry.d.ts +74 -0
- package/dist/@types/types/registry.d.ts.map +1 -0
- package/dist/@types/types/schemas.d.ts +117 -0
- package/dist/@types/types/schemas.d.ts.map +1 -0
- package/dist/@types/utils.d.ts +54 -0
- package/dist/@types/utils.d.ts.map +1 -0
- package/dist/chunk-AGWZY6YT.js +127 -0
- package/dist/chunk-AGWZY6YT.js.map +1 -0
- package/dist/index.cjs +3148 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +2979 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.cjs +166 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.js +33 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +6 -2
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3148 @@
|
|
|
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]) {
|
|
1546
|
+
throw new Error(
|
|
1547
|
+
`Event "${name}" in state "${state2.name}" is declared with different Zod schemas across slices. Cross-slice event schemas must reference the same instance \u2014 extract a shared schema (e.g. \`export const ${name} = z.object({ ... })\` in a shared module) and import it in every slice that declares it.`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
1551
|
+
}
|
|
1552
|
+
const mergedPatch = mergePatches(existing.patch, state2.patch, state2.name);
|
|
1553
|
+
const merged = {
|
|
1554
|
+
...existing,
|
|
1555
|
+
state: mergeSchemas(existing.state, state2.state, state2.name),
|
|
1556
|
+
init: mergeInits(existing.init, state2.init),
|
|
1557
|
+
events: { ...existing.events, ...state2.events },
|
|
1558
|
+
actions: { ...existing.actions, ...state2.actions },
|
|
1559
|
+
patch: mergedPatch,
|
|
1560
|
+
on: { ...existing.on, ...state2.on },
|
|
1561
|
+
given: { ...existing.given, ...state2.given },
|
|
1562
|
+
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
1563
|
+
throw new Error(
|
|
1564
|
+
`Duplicate snap strategy for state "${state2.name}"`
|
|
1565
|
+
);
|
|
1566
|
+
})() : state2.snap || existing.snap
|
|
1567
|
+
};
|
|
1568
|
+
states.set(state2.name, merged);
|
|
1569
|
+
for (const name of Object.keys(merged.actions)) {
|
|
1570
|
+
actions[name] = merged;
|
|
1571
|
+
}
|
|
1572
|
+
for (const name of Object.keys(state2.events)) {
|
|
1573
|
+
if (events[name]) continue;
|
|
1574
|
+
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function mergePatches(existing, incoming, stateName) {
|
|
1578
|
+
const merged = { ...existing };
|
|
1579
|
+
for (const name of Object.keys(incoming)) {
|
|
1580
|
+
const existingP = existing[name];
|
|
1581
|
+
const incomingP = incoming[name];
|
|
1582
|
+
if (!existingP) {
|
|
1583
|
+
merged[name] = incomingP;
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
const existingIsDefault = existingP._passthrough;
|
|
1587
|
+
const incomingIsDefault = incomingP._passthrough;
|
|
1588
|
+
if (!existingIsDefault && !incomingIsDefault && existingP !== incomingP) {
|
|
1589
|
+
throw new Error(
|
|
1590
|
+
`Duplicate custom patch for event "${name}" in state "${stateName}"`
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
if (existingIsDefault && !incomingIsDefault) {
|
|
1594
|
+
merged[name] = incomingP;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return merged;
|
|
1598
|
+
}
|
|
1599
|
+
function mergeEventRegister(target, source) {
|
|
1600
|
+
for (const [eventName, sourceReg] of Object.entries(source)) {
|
|
1601
|
+
const targetReg = target[eventName];
|
|
1602
|
+
if (!targetReg) continue;
|
|
1603
|
+
for (const [name, reaction] of sourceReg.reactions) {
|
|
1604
|
+
targetReg.reactions.set(name, reaction);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function mergeProjection(proj, events) {
|
|
1609
|
+
for (const eventName of Object.keys(proj.events)) {
|
|
1610
|
+
const projRegister = proj.events[eventName];
|
|
1611
|
+
const existing = events[eventName];
|
|
1612
|
+
if (!existing) {
|
|
1613
|
+
events[eventName] = {
|
|
1614
|
+
schema: projRegister.schema,
|
|
1615
|
+
reactions: new Map(projRegister.reactions)
|
|
1616
|
+
};
|
|
1617
|
+
} else {
|
|
1618
|
+
for (const [name, reaction] of projRegister.reactions) {
|
|
1619
|
+
let key = name;
|
|
1620
|
+
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
1621
|
+
existing.reactions.set(key, reaction);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
var _this_ = ({ stream }) => ({
|
|
1627
|
+
source: stream,
|
|
1628
|
+
target: stream
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
// src/internal/reactions.ts
|
|
1632
|
+
function finalize(lease, handled, at, error, options, logger) {
|
|
1633
|
+
if (!error) return { lease, handled, at };
|
|
1634
|
+
logger.error(error);
|
|
1635
|
+
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1636
|
+
if (block2)
|
|
1637
|
+
logger.error(`Blocking ${lease.stream} after ${lease.retry} retries.`);
|
|
1638
|
+
return {
|
|
1639
|
+
lease,
|
|
1640
|
+
handled,
|
|
1641
|
+
at,
|
|
1642
|
+
error: handled === 0 ? error.message : void 0,
|
|
1643
|
+
block: block2
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
function buildHandle(deps) {
|
|
1647
|
+
const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
|
|
1648
|
+
return async (lease, payloads) => {
|
|
1649
|
+
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1650
|
+
const stream = lease.stream;
|
|
1651
|
+
let at = payloads.at(0).event.id;
|
|
1652
|
+
let handled = 0;
|
|
1653
|
+
if (lease.retry > 0)
|
|
1654
|
+
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1655
|
+
const scopedApp = {
|
|
1656
|
+
do: boundDo,
|
|
1657
|
+
load: boundLoad,
|
|
1658
|
+
query: boundQuery,
|
|
1659
|
+
query_array: boundQueryArray
|
|
1660
|
+
};
|
|
1661
|
+
for (const payload of payloads) {
|
|
1662
|
+
const { event, handler } = payload;
|
|
1663
|
+
scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
|
|
1664
|
+
action2,
|
|
1665
|
+
target,
|
|
1666
|
+
actionPayload,
|
|
1667
|
+
reactingTo ?? event,
|
|
1668
|
+
skipValidation
|
|
1669
|
+
);
|
|
1670
|
+
try {
|
|
1671
|
+
await handler(event, stream, scopedApp);
|
|
1672
|
+
at = event.id;
|
|
1673
|
+
handled++;
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
return finalize(
|
|
1676
|
+
lease,
|
|
1677
|
+
handled,
|
|
1678
|
+
at,
|
|
1679
|
+
error,
|
|
1680
|
+
payload.options,
|
|
1681
|
+
logger
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
function buildHandleBatch(logger) {
|
|
1689
|
+
return async (lease, payloads, batchHandler) => {
|
|
1690
|
+
const stream = lease.stream;
|
|
1691
|
+
const events = payloads.map((p) => p.event);
|
|
1692
|
+
const options = payloads[0].options;
|
|
1693
|
+
if (lease.retry > 0)
|
|
1694
|
+
logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
|
|
1695
|
+
try {
|
|
1696
|
+
await batchHandler(events, stream);
|
|
1697
|
+
return finalize(
|
|
1698
|
+
lease,
|
|
1699
|
+
events.length,
|
|
1700
|
+
events.at(-1).id,
|
|
1701
|
+
void 0,
|
|
1702
|
+
options,
|
|
1703
|
+
logger
|
|
1704
|
+
);
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
return finalize(lease, 0, lease.at, error, options, logger);
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// src/internal/settle.ts
|
|
1712
|
+
var SettleLoop = class {
|
|
1713
|
+
constructor(deps, defaultDebounceMs) {
|
|
1714
|
+
this.deps = deps;
|
|
1715
|
+
this.defaultDebounceMs = defaultDebounceMs;
|
|
1716
|
+
}
|
|
1717
|
+
_timer = void 0;
|
|
1718
|
+
_running = false;
|
|
1719
|
+
/**
|
|
1720
|
+
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
1721
|
+
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
1722
|
+
* until no progress is made (no new subscriptions, no acks, no blocks)
|
|
1723
|
+
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
1724
|
+
* via {@link SettleDeps.onSettled}.
|
|
1725
|
+
*/
|
|
1726
|
+
schedule(options = {}) {
|
|
1727
|
+
const {
|
|
1728
|
+
debounceMs = this.defaultDebounceMs,
|
|
1729
|
+
correlate: correlateQuery = { after: -1, limit: 100 },
|
|
1730
|
+
maxPasses = Infinity,
|
|
1731
|
+
...drainOptions
|
|
1732
|
+
} = options;
|
|
1733
|
+
if (this._timer) clearTimeout(this._timer);
|
|
1734
|
+
this._timer = setTimeout(() => {
|
|
1735
|
+
this._timer = void 0;
|
|
1736
|
+
if (this._running) return;
|
|
1737
|
+
this._running = true;
|
|
1738
|
+
(async () => {
|
|
1739
|
+
await this.deps.init();
|
|
1740
|
+
let lastDrain;
|
|
1741
|
+
for (let i = 0; i < maxPasses; i++) {
|
|
1742
|
+
const { subscribed } = await this.deps.correlate({
|
|
1743
|
+
...correlateQuery,
|
|
1744
|
+
after: this.deps.checkpoint()
|
|
1745
|
+
});
|
|
1746
|
+
lastDrain = await this.deps.drain(drainOptions);
|
|
1747
|
+
const made_progress = subscribed > 0 || lastDrain.acked.length > 0 || lastDrain.blocked.length > 0;
|
|
1748
|
+
if (!made_progress) break;
|
|
1749
|
+
}
|
|
1750
|
+
if (lastDrain) this.deps.onSettled(lastDrain);
|
|
1751
|
+
})().catch((err) => this.deps.logger.error(err)).finally(() => {
|
|
1752
|
+
this._running = false;
|
|
1753
|
+
});
|
|
1754
|
+
}, debounceMs);
|
|
1755
|
+
}
|
|
1756
|
+
/** Cancel any pending or active settle cycle. Idempotent. */
|
|
1757
|
+
stop() {
|
|
1758
|
+
if (this._timer) {
|
|
1759
|
+
clearTimeout(this._timer);
|
|
1760
|
+
this._timer = void 0;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
// src/internal/drain.ts
|
|
1766
|
+
var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
|
|
1767
|
+
async function fetch(leased, eventLimit) {
|
|
1768
|
+
return Promise.all(
|
|
1769
|
+
leased.map(async ({ stream, source, at, lagging }) => {
|
|
1770
|
+
const events = [];
|
|
1771
|
+
await store().query((e) => events.push(e), {
|
|
1772
|
+
stream: source,
|
|
1773
|
+
after: at,
|
|
1774
|
+
limit: eventLimit
|
|
1775
|
+
});
|
|
1776
|
+
return { stream, source, at, lagging, events };
|
|
1777
|
+
})
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
var ack = (leases) => store().ack(leases);
|
|
1781
|
+
var block = (leases) => store().block(leases);
|
|
1782
|
+
var subscribe = (streams) => store().subscribe(streams);
|
|
1783
|
+
|
|
1784
|
+
// src/internal/event-sourcing.ts
|
|
1785
|
+
var import_node_crypto3 = require("crypto");
|
|
1786
|
+
var import_act_patch = require("@rotorsoft/act-patch");
|
|
1787
|
+
async function snap(snapshot) {
|
|
1788
|
+
try {
|
|
1789
|
+
const { id, stream, name, meta, version } = snapshot.event;
|
|
1790
|
+
await store().commit(
|
|
1791
|
+
stream,
|
|
1792
|
+
[{ name: SNAP_EVENT, data: snapshot.state }],
|
|
1793
|
+
{
|
|
1794
|
+
correlation: meta.correlation,
|
|
1795
|
+
causation: { event: { id, name, stream } }
|
|
1796
|
+
},
|
|
1797
|
+
version
|
|
1798
|
+
// IMPORTANT! - state events are committed right after the snapshot event
|
|
1799
|
+
);
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
log().error(error);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
async function tombstone(stream, expectedVersion, correlation) {
|
|
1805
|
+
try {
|
|
1806
|
+
const [committed] = await store().commit(
|
|
1807
|
+
stream,
|
|
1808
|
+
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
1809
|
+
{ correlation, causation: {} },
|
|
1810
|
+
expectedVersion
|
|
1811
|
+
);
|
|
1812
|
+
return committed;
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
if (error instanceof ConcurrencyError) return void 0;
|
|
1815
|
+
throw error;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
async function load(me, stream, callback, asOf) {
|
|
1819
|
+
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
1820
|
+
const cached = timeTravel ? void 0 : await cache().get(stream);
|
|
1821
|
+
const cache_hit = !!cached;
|
|
1822
|
+
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
1823
|
+
let patches = cached?.patches ?? 0;
|
|
1824
|
+
let snaps = cached?.snaps ?? 0;
|
|
1825
|
+
let version = cached?.version ?? -1;
|
|
1826
|
+
let replayed = 0;
|
|
1827
|
+
let event;
|
|
1828
|
+
await store().query(
|
|
1829
|
+
(e) => {
|
|
1830
|
+
event = e;
|
|
1831
|
+
version = e.version;
|
|
1832
|
+
if (e.name === SNAP_EVENT) {
|
|
1833
|
+
state2 = e.data;
|
|
1834
|
+
snaps++;
|
|
1835
|
+
patches = 0;
|
|
1836
|
+
replayed++;
|
|
1837
|
+
} else if (me.patch[e.name]) {
|
|
1838
|
+
state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](event, state2));
|
|
1839
|
+
patches++;
|
|
1840
|
+
replayed++;
|
|
1841
|
+
} else if (e.name !== TOMBSTONE_EVENT) {
|
|
1842
|
+
log().warn(
|
|
1843
|
+
`Skipping unknown event "${String(e.name)}" on stream "${stream}" (id=${e.id}) \u2014 no reducer in state "${me.name}"`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
callback?.({
|
|
1847
|
+
event,
|
|
1848
|
+
state: state2,
|
|
1849
|
+
version,
|
|
1850
|
+
patches,
|
|
1851
|
+
snaps,
|
|
1852
|
+
cache_hit,
|
|
1853
|
+
replayed
|
|
1854
|
+
});
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
stream,
|
|
1858
|
+
stream_exact: true,
|
|
1859
|
+
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
1860
|
+
}
|
|
1861
|
+
);
|
|
1862
|
+
if (replayed > 0 && !timeTravel && event) {
|
|
1863
|
+
await cache().set(stream, {
|
|
1864
|
+
state: state2,
|
|
1865
|
+
version,
|
|
1866
|
+
event_id: event.id,
|
|
1867
|
+
patches,
|
|
1868
|
+
snaps
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
1872
|
+
}
|
|
1873
|
+
async function action(me, action2, target, payload, reactingTo, skipValidation = false) {
|
|
1874
|
+
const { stream, expectedVersion, actor } = target;
|
|
1875
|
+
if (!stream) throw new Error("Missing target stream");
|
|
1876
|
+
const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
1877
|
+
const snapshot = await load(me, stream);
|
|
1878
|
+
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
1879
|
+
throw new StreamClosedError(stream);
|
|
1880
|
+
const expected = expectedVersion ?? snapshot.event?.version;
|
|
1881
|
+
if (me.given) {
|
|
1882
|
+
const invariants = me.given[action2] || [];
|
|
1883
|
+
invariants.forEach(({ valid, description }) => {
|
|
1884
|
+
if (!valid(snapshot.state, actor))
|
|
1885
|
+
throw new InvariantError(
|
|
1886
|
+
action2,
|
|
1887
|
+
validated,
|
|
1888
|
+
target,
|
|
1889
|
+
snapshot,
|
|
1890
|
+
description
|
|
1891
|
+
);
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const result = me.on[action2](validated, snapshot, target);
|
|
1895
|
+
if (!result) return [snapshot];
|
|
1896
|
+
if (Array.isArray(result) && result.length === 0) {
|
|
1897
|
+
return [snapshot];
|
|
1898
|
+
}
|
|
1899
|
+
const tuples = Array.isArray(result[0]) ? result : [result];
|
|
1900
|
+
const emitted = tuples.map(([name, data]) => ({
|
|
1901
|
+
name,
|
|
1902
|
+
data: skipValidation ? data : validate(name, data, me.events[name])
|
|
1903
|
+
}));
|
|
1904
|
+
const meta = {
|
|
1905
|
+
correlation: reactingTo?.meta.correlation || (0, import_node_crypto3.randomUUID)(),
|
|
1906
|
+
causation: {
|
|
1907
|
+
action: {
|
|
1908
|
+
name: action2,
|
|
1909
|
+
...target
|
|
1910
|
+
// payload intentionally omitted: it can be large or contain PII,
|
|
1911
|
+
// and callers correlate via the correlation id when they need it.
|
|
1912
|
+
},
|
|
1913
|
+
event: reactingTo ? {
|
|
1914
|
+
id: reactingTo.id,
|
|
1915
|
+
name: reactingTo.name,
|
|
1916
|
+
stream: reactingTo.stream
|
|
1917
|
+
} : void 0
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
let committed;
|
|
1921
|
+
try {
|
|
1922
|
+
committed = await store().commit(
|
|
1923
|
+
stream,
|
|
1924
|
+
emitted,
|
|
1925
|
+
meta,
|
|
1926
|
+
// Reactions skip optimistic concurrency: they always append against the
|
|
1927
|
+
// current head. Stream leasing already serializes concurrent reactions,
|
|
1928
|
+
// and forcing version checks here would turn ordinary catch-up into
|
|
1929
|
+
// spurious retries.
|
|
1930
|
+
reactingTo ? void 0 : expected
|
|
1931
|
+
);
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
if (error instanceof ConcurrencyError) {
|
|
1934
|
+
await cache().invalidate(stream);
|
|
1935
|
+
}
|
|
1936
|
+
throw error;
|
|
1937
|
+
}
|
|
1938
|
+
let { state: state2, patches } = snapshot;
|
|
1939
|
+
const snapshots = committed.map((event) => {
|
|
1940
|
+
const p = me.patch[event.name](event, state2);
|
|
1941
|
+
state2 = (0, import_act_patch.patch)(state2, p);
|
|
1942
|
+
patches++;
|
|
1943
|
+
return {
|
|
1944
|
+
event,
|
|
1945
|
+
state: state2,
|
|
1946
|
+
version: event.version,
|
|
1947
|
+
patches,
|
|
1948
|
+
snaps: snapshot.snaps,
|
|
1949
|
+
patch: p,
|
|
1950
|
+
cache_hit: snapshot.cache_hit,
|
|
1951
|
+
replayed: snapshot.replayed
|
|
1952
|
+
};
|
|
1953
|
+
});
|
|
1954
|
+
const last = snapshots.at(-1);
|
|
1955
|
+
const snapped = me.snap?.(last);
|
|
1956
|
+
cache().set(stream, {
|
|
1957
|
+
state: last.state,
|
|
1958
|
+
version: last.event.version,
|
|
1959
|
+
event_id: last.event.id,
|
|
1960
|
+
patches: snapped ? 0 : last.patches,
|
|
1961
|
+
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
1962
|
+
}).catch((err) => log().error(err));
|
|
1963
|
+
if (snapped) void snap(last);
|
|
1964
|
+
return snapshots;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// src/internal/tracing.ts
|
|
1968
|
+
var PRETTY = config().env !== "production";
|
|
1969
|
+
var C_BLUE = "\x1B[38;5;39m";
|
|
1970
|
+
var C_ORANGE = "\x1B[38;5;208m";
|
|
1971
|
+
var C_GREEN = "\x1B[38;5;42m";
|
|
1972
|
+
var C_MAGENTA = "\x1B[38;5;165m";
|
|
1973
|
+
var C_DRAIN = "\x1B[38;5;244m";
|
|
1974
|
+
var C_HIT = "\x1B[38;5;82m";
|
|
1975
|
+
var C_MISS = "\x1B[38;5;220m";
|
|
1976
|
+
var C_RESET = "\x1B[0m";
|
|
1977
|
+
var es_caption = (caption, color, body) => PRETTY ? `${color}${body}${C_RESET}` : `${caption}: ${body}`;
|
|
1978
|
+
var drain_caption = (caption) => {
|
|
1979
|
+
const tag = `>> ${caption}`;
|
|
1980
|
+
return PRETTY ? `${C_DRAIN}${tag}${C_RESET}` : tag;
|
|
1981
|
+
};
|
|
1982
|
+
var cache_marker = (hit) => {
|
|
1983
|
+
const word = hit ? "hit" : "miss";
|
|
1984
|
+
if (!PRETTY) return word;
|
|
1985
|
+
return `${hit ? C_HIT : C_MISS}${word}${C_RESET}${C_GREEN}`;
|
|
1986
|
+
};
|
|
1987
|
+
var stats_marker = (version, replayed, snaps, patches) => {
|
|
1988
|
+
const text = `v=${version} replayed=${replayed} snaps=${snaps} patches=${patches}`;
|
|
1989
|
+
if (!PRETTY) return text;
|
|
1990
|
+
return `${C_DRAIN}${text}${C_RESET}${C_GREEN}`;
|
|
1991
|
+
};
|
|
1992
|
+
var as_of_marker = (asOf) => {
|
|
1993
|
+
if (!asOf) return "";
|
|
1994
|
+
const parts = [];
|
|
1995
|
+
if (asOf.before !== void 0) parts.push(`before=${asOf.before}`);
|
|
1996
|
+
if (asOf.created_before !== void 0)
|
|
1997
|
+
parts.push(`created_before=${asOf.created_before.toISOString()}`);
|
|
1998
|
+
if (asOf.created_after !== void 0)
|
|
1999
|
+
parts.push(`created_after=${asOf.created_after.toISOString()}`);
|
|
2000
|
+
if (asOf.limit !== void 0) parts.push(`limit=${asOf.limit}`);
|
|
2001
|
+
return parts.length ? ` (as-of ${parts.join(" ")})` : " (as-of)";
|
|
2002
|
+
};
|
|
2003
|
+
var traced = (inner, exit, entry) => (async (...args) => {
|
|
2004
|
+
entry?.(...args);
|
|
2005
|
+
const result = await inner(...args);
|
|
2006
|
+
exit?.(result, ...args);
|
|
2007
|
+
return result;
|
|
2008
|
+
});
|
|
2009
|
+
function buildEs(logger) {
|
|
2010
|
+
if (logger.level !== "trace") {
|
|
2011
|
+
return {
|
|
2012
|
+
snap,
|
|
2013
|
+
load,
|
|
2014
|
+
action,
|
|
2015
|
+
tombstone
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
return {
|
|
2019
|
+
snap: traced(snap, void 0, (snapshot) => {
|
|
2020
|
+
logger.trace(
|
|
2021
|
+
es_caption(
|
|
2022
|
+
"snap",
|
|
2023
|
+
C_MAGENTA,
|
|
2024
|
+
`${snapshot.event.stream}@${snapshot.event.version}`
|
|
2025
|
+
)
|
|
2026
|
+
);
|
|
2027
|
+
}),
|
|
2028
|
+
load: traced(load, (result, _me, stream, _cb, asOf) => {
|
|
2029
|
+
const stats = stats_marker(
|
|
2030
|
+
result.version,
|
|
2031
|
+
result.replayed,
|
|
2032
|
+
result.snaps,
|
|
2033
|
+
result.patches
|
|
2034
|
+
);
|
|
2035
|
+
logger.trace(
|
|
2036
|
+
es_caption(
|
|
2037
|
+
"load",
|
|
2038
|
+
C_GREEN,
|
|
2039
|
+
`${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
2040
|
+
)
|
|
2041
|
+
);
|
|
2042
|
+
}),
|
|
2043
|
+
action: traced(
|
|
2044
|
+
action,
|
|
2045
|
+
(snapshots, _me, _action, target) => {
|
|
2046
|
+
const committed = snapshots.filter((s) => s.event);
|
|
2047
|
+
if (committed.length) {
|
|
2048
|
+
logger.trace(
|
|
2049
|
+
committed.map((s) => s.event.data),
|
|
2050
|
+
es_caption(
|
|
2051
|
+
"committed",
|
|
2052
|
+
C_ORANGE,
|
|
2053
|
+
`${target.stream}.${committed.map((s) => s.event.name).join(", ")}`
|
|
2054
|
+
)
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
},
|
|
2058
|
+
(_me, action2, target, payload) => {
|
|
2059
|
+
logger.trace(
|
|
2060
|
+
payload,
|
|
2061
|
+
es_caption("action", C_BLUE, `${target.stream}.${action2}`)
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
),
|
|
2065
|
+
tombstone: traced(tombstone, (committed, stream) => {
|
|
2066
|
+
if (committed)
|
|
2067
|
+
logger.trace(
|
|
2068
|
+
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
2069
|
+
);
|
|
2070
|
+
})
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
function buildDrain(logger) {
|
|
2074
|
+
if (logger.level !== "trace") {
|
|
2075
|
+
return {
|
|
2076
|
+
claim,
|
|
2077
|
+
fetch,
|
|
2078
|
+
ack,
|
|
2079
|
+
block,
|
|
2080
|
+
subscribe
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
return {
|
|
2084
|
+
claim: traced(claim, (leased) => {
|
|
2085
|
+
if (leased.length) {
|
|
2086
|
+
const data = Object.fromEntries(
|
|
2087
|
+
leased.map(({ stream, at, retry }) => [stream, { at, retry }])
|
|
2088
|
+
);
|
|
2089
|
+
logger.trace(data, drain_caption("claimed"));
|
|
2090
|
+
}
|
|
2091
|
+
}),
|
|
2092
|
+
fetch: traced(fetch, (fetched) => {
|
|
2093
|
+
const data = Object.fromEntries(
|
|
2094
|
+
fetched.map(({ stream, source, events }) => {
|
|
2095
|
+
const key = source ? `${stream}<-${source}` : stream;
|
|
2096
|
+
const value = Object.fromEntries(
|
|
2097
|
+
events.map(({ id, stream: stream2, name }) => [id, { [stream2]: name }])
|
|
2098
|
+
);
|
|
2099
|
+
return [key, value];
|
|
2100
|
+
})
|
|
2101
|
+
);
|
|
2102
|
+
logger.trace(data, drain_caption("fetched"));
|
|
2103
|
+
}),
|
|
2104
|
+
ack: traced(ack, (acked) => {
|
|
2105
|
+
if (acked.length) {
|
|
2106
|
+
const data = Object.fromEntries(
|
|
2107
|
+
acked.map(({ stream, at, retry }) => [stream, { at, retry }])
|
|
2108
|
+
);
|
|
2109
|
+
logger.trace(data, drain_caption("acked"));
|
|
2110
|
+
}
|
|
2111
|
+
}),
|
|
2112
|
+
block: traced(block, (blocked) => {
|
|
2113
|
+
if (blocked.length) {
|
|
2114
|
+
const data = Object.fromEntries(
|
|
2115
|
+
blocked.map(({ stream, at, retry, error }) => [
|
|
2116
|
+
stream,
|
|
2117
|
+
{ at, retry, error }
|
|
2118
|
+
])
|
|
2119
|
+
);
|
|
2120
|
+
logger.trace(data, drain_caption("blocked"));
|
|
2121
|
+
}
|
|
2122
|
+
}),
|
|
2123
|
+
subscribe: traced(subscribe, (result, streams) => {
|
|
2124
|
+
if (result.subscribed) {
|
|
2125
|
+
const data = streams.map(({ stream }) => stream).join(" ");
|
|
2126
|
+
logger.trace(`${drain_caption("correlated")} ${data}`);
|
|
2127
|
+
}
|
|
2128
|
+
})
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// src/act.ts
|
|
2133
|
+
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
2134
|
+
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
2135
|
+
var Act = class {
|
|
2136
|
+
/**
|
|
2137
|
+
* Create a new Act orchestrator. Prefer the {@link act} builder over
|
|
2138
|
+
* direct construction — `act()...build()` wires the registry, merges
|
|
2139
|
+
* partial states, and collects batch handlers from registered slices
|
|
2140
|
+
* and projections in one pass.
|
|
2141
|
+
*
|
|
2142
|
+
* @param registry Schemas for every event and action across registered states
|
|
2143
|
+
* @param _states Merged map of state name → state definition
|
|
2144
|
+
* @param batchHandlers Static-target projection batch handlers (target → handler)
|
|
2145
|
+
* @param options Tuning knobs — see {@link ActOptions}
|
|
2146
|
+
*/
|
|
2147
|
+
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
|
|
2148
|
+
this.registry = registry;
|
|
2149
|
+
this._states = _states;
|
|
2150
|
+
this._batch_handlers = batchHandlers;
|
|
2151
|
+
this._es = buildEs(this._logger);
|
|
2152
|
+
this._cd = buildDrain(this._logger);
|
|
2153
|
+
this._handle = buildHandle({
|
|
2154
|
+
logger: this._logger,
|
|
2155
|
+
boundDo: this._bound_do,
|
|
2156
|
+
boundLoad: this._bound_load,
|
|
2157
|
+
boundQuery: this._bound_query,
|
|
2158
|
+
boundQueryArray: this._bound_query_array
|
|
2159
|
+
});
|
|
2160
|
+
this._handle_batch = buildHandleBatch(this._logger);
|
|
2161
|
+
const { staticTargets, hasDynamicResolvers, reactiveEvents, eventToState } = classifyRegistry(this.registry, this._states);
|
|
2162
|
+
this._reactive_events = reactiveEvents;
|
|
2163
|
+
this._event_to_state = eventToState;
|
|
2164
|
+
this._drain = new DrainController({
|
|
2165
|
+
logger: this._logger,
|
|
2166
|
+
ops: this._cd,
|
|
2167
|
+
registry: this.registry,
|
|
2168
|
+
batchHandlers: this._batch_handlers,
|
|
2169
|
+
handle: this._handle,
|
|
2170
|
+
handleBatch: this._handle_batch,
|
|
2171
|
+
onAcked: (acked) => this.emit("acked", acked),
|
|
2172
|
+
onBlocked: (blocked) => this.emit("blocked", blocked)
|
|
2173
|
+
});
|
|
2174
|
+
this._correlate = new CorrelateCycle(
|
|
2175
|
+
this.registry,
|
|
2176
|
+
staticTargets,
|
|
2177
|
+
hasDynamicResolvers,
|
|
2178
|
+
this._cd,
|
|
2179
|
+
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2180
|
+
// Cold start: assume drain is needed (historical events may need processing)
|
|
2181
|
+
() => {
|
|
2182
|
+
if (this._reactive_events.size > 0) this._drain.arm();
|
|
2183
|
+
}
|
|
2184
|
+
);
|
|
2185
|
+
this._settle = new SettleLoop(
|
|
2186
|
+
{
|
|
2187
|
+
logger: this._logger,
|
|
2188
|
+
init: () => this._correlate.init(),
|
|
2189
|
+
checkpoint: () => this._correlate.checkpoint,
|
|
2190
|
+
correlate: (q) => this.correlate(q),
|
|
2191
|
+
drain: (o) => this.drain(o),
|
|
2192
|
+
onSettled: (drain) => this.emit("settled", drain)
|
|
2193
|
+
},
|
|
2194
|
+
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
2195
|
+
);
|
|
2196
|
+
this._notify_disposer = this._wireNotify();
|
|
2197
|
+
dispose(async () => {
|
|
2198
|
+
this._emitter.removeAllListeners();
|
|
2199
|
+
this.stop_correlations();
|
|
2200
|
+
this.stop_settling();
|
|
2201
|
+
const disposer = await this._notify_disposer;
|
|
2202
|
+
if (disposer) await disposer();
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
_emitter = new import_node_events.default();
|
|
2206
|
+
/** Event names with at least one registered reaction (computed at build time) */
|
|
2207
|
+
_reactive_events;
|
|
2208
|
+
/** Drain pipeline driver: armed flag, concurrency lock, adaptive ratio. */
|
|
2209
|
+
_drain;
|
|
2210
|
+
/** Correlation state machine: lazy init, dynamic-resolver scan, periodic worker. */
|
|
2211
|
+
_correlate;
|
|
2212
|
+
/** Debounced correlate→drain catch-up loop. */
|
|
2213
|
+
_settle;
|
|
2214
|
+
/**
|
|
2215
|
+
* Disposer for the cross-process notify subscription, set up eagerly
|
|
2216
|
+
* during construction. Held as a promise because the subscription
|
|
2217
|
+
* itself may be async (the PG adapter checks out a dedicated client
|
|
2218
|
+
* and runs `LISTEN` before resolving). Resolves to `undefined` when
|
|
2219
|
+
* the store doesn't implement `notify` or there are no registered
|
|
2220
|
+
* reactions.
|
|
2221
|
+
*
|
|
2222
|
+
* **Contract:** the configured store must be injected via
|
|
2223
|
+
* {@link store}`(adapter)` *before* calling `act()...build()`. The
|
|
2224
|
+
* orchestrator wires notify against whatever store is current at
|
|
2225
|
+
* construction time — late injection after build is unsupported.
|
|
2226
|
+
*/
|
|
2227
|
+
_notify_disposer;
|
|
2228
|
+
/**
|
|
2229
|
+
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
2230
|
+
* via {@link ActLifecycleEvents}.
|
|
2231
|
+
*/
|
|
2232
|
+
emit(event, args) {
|
|
2233
|
+
return this._emitter.emit(event, args);
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Register a listener for a lifecycle event. The listener receives the
|
|
2237
|
+
* event-specific payload.
|
|
2238
|
+
*/
|
|
2239
|
+
on(event, listener) {
|
|
2240
|
+
this._emitter.on(event, listener);
|
|
2241
|
+
return this;
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Remove a previously registered lifecycle listener.
|
|
2245
|
+
*/
|
|
2246
|
+
off(event, listener) {
|
|
2247
|
+
this._emitter.off(event, listener);
|
|
2248
|
+
return this;
|
|
2249
|
+
}
|
|
2250
|
+
/** Batch handlers for static-target projections (target → handler) */
|
|
2251
|
+
_batch_handlers;
|
|
2252
|
+
/** Event-sourcing handlers, optionally wrapped with trace decorators */
|
|
2253
|
+
_es;
|
|
2254
|
+
/** Correlate/drain pipeline ops, optionally wrapped with trace decorators */
|
|
2255
|
+
_cd;
|
|
2256
|
+
/**
|
|
2257
|
+
* Event-name → owning state, computed at build time. The duplicate-event
|
|
2258
|
+
* guard in merge.ts ensures one event name maps to at most one state, so
|
|
2259
|
+
* this lookup is unambiguous. Used by `close()` to pick the right reducer
|
|
2260
|
+
* set when seeding a `restart` snapshot in multi-state apps.
|
|
2261
|
+
*/
|
|
2262
|
+
_event_to_state;
|
|
2263
|
+
/** Logger resolved at construction time (after user port configuration) */
|
|
2264
|
+
_logger = log();
|
|
2265
|
+
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
2266
|
+
* payload (it captures the triggering event for reactingTo auto-inject). */
|
|
2267
|
+
_bound_do = this.do.bind(this);
|
|
2268
|
+
_bound_load = this.load.bind(this);
|
|
2269
|
+
_bound_query = this.query.bind(this);
|
|
2270
|
+
_bound_query_array = this.query_array.bind(this);
|
|
2271
|
+
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
2272
|
+
_handle;
|
|
2273
|
+
_handle_batch;
|
|
2274
|
+
/**
|
|
2275
|
+
* Subscribe to {@link Store.notify} when both the store and the
|
|
2276
|
+
* registry support it. Returns the disposer (or `undefined` when no
|
|
2277
|
+
* subscription was made). Errors during subscription are logged but
|
|
2278
|
+
* never thrown — `notify` is a hint, not a contract.
|
|
2279
|
+
*/
|
|
2280
|
+
async _wireNotify() {
|
|
2281
|
+
if (this._reactive_events.size === 0) return void 0;
|
|
2282
|
+
const s = store();
|
|
2283
|
+
if (!s.notify) return void 0;
|
|
2284
|
+
try {
|
|
2285
|
+
return await s.notify((notification) => {
|
|
2286
|
+
try {
|
|
2287
|
+
this.emit("notified", notification);
|
|
2288
|
+
const hasReactive = notification.events.some(
|
|
2289
|
+
(e) => this._reactive_events.has(e.name)
|
|
2290
|
+
);
|
|
2291
|
+
if (hasReactive) {
|
|
2292
|
+
this._drain.arm();
|
|
2293
|
+
this._settle.schedule({ debounceMs: 0 });
|
|
2294
|
+
}
|
|
2295
|
+
} catch (err) {
|
|
2296
|
+
this._logger.error(err, "notified handler threw");
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
} catch (err) {
|
|
2300
|
+
this._logger.error(err, "Store.notify subscription failed");
|
|
2301
|
+
return void 0;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
/**
|
|
2305
|
+
* Executes an action on a state instance, committing resulting events.
|
|
2306
|
+
*
|
|
2307
|
+
* This is the primary method for modifying state. It:
|
|
2308
|
+
* 1. Validates the action payload against the schema
|
|
2309
|
+
* 2. Loads the current state snapshot
|
|
2310
|
+
* 3. Checks invariants (business rules)
|
|
2311
|
+
* 4. Executes the action handler to generate events
|
|
2312
|
+
* 5. Applies events to create new state
|
|
2313
|
+
* 6. Commits events to the store with optimistic concurrency control
|
|
2314
|
+
*
|
|
2315
|
+
* @template TKey - Action name from registered actions
|
|
2316
|
+
* @param action - The name of the action to execute
|
|
2317
|
+
* @param target - Target specification with stream ID and actor context
|
|
2318
|
+
* @param payload - Action payload matching the action's schema
|
|
2319
|
+
* @param reactingTo - Optional event that triggered this action (for correlation)
|
|
2320
|
+
* @param skipValidation - Skip schema validation (use carefully, for performance)
|
|
2321
|
+
* @returns Array of snapshots for all affected states (usually one)
|
|
2322
|
+
*
|
|
2323
|
+
* @throws {ValidationError} If payload doesn't match action schema
|
|
2324
|
+
* @throws {InvariantError} If business rules are violated
|
|
2325
|
+
* @throws {ConcurrencyError} If another process modified the stream
|
|
2326
|
+
*
|
|
2327
|
+
* @example Basic action execution
|
|
2328
|
+
* ```typescript
|
|
2329
|
+
* const snapshots = await app.do(
|
|
2330
|
+
* "increment",
|
|
2331
|
+
* {
|
|
2332
|
+
* stream: "counter-1",
|
|
2333
|
+
* actor: { id: "user1", name: "Alice" }
|
|
2334
|
+
* },
|
|
2335
|
+
* { by: 5 }
|
|
2336
|
+
* );
|
|
2337
|
+
*
|
|
2338
|
+
* console.log(snapshots[0].state.count); // Current count after increment
|
|
2339
|
+
* ```
|
|
2340
|
+
*
|
|
2341
|
+
* @example With error handling
|
|
2342
|
+
* ```typescript
|
|
2343
|
+
* try {
|
|
2344
|
+
* await app.do(
|
|
2345
|
+
* "withdraw",
|
|
2346
|
+
* { stream: "account-123", actor: { id: "user1", name: "Alice" } },
|
|
2347
|
+
* { amount: 1000 }
|
|
2348
|
+
* );
|
|
2349
|
+
* } catch (error) {
|
|
2350
|
+
* if (error instanceof InvariantError) {
|
|
2351
|
+
* console.error("Business rule violated:", error.description);
|
|
2352
|
+
* } else if (error instanceof ConcurrencyError) {
|
|
2353
|
+
* console.error("Concurrent modification detected, retry...");
|
|
2354
|
+
* } else if (error instanceof ValidationError) {
|
|
2355
|
+
* console.error("Invalid payload:", error.details);
|
|
2356
|
+
* }
|
|
2357
|
+
* }
|
|
2358
|
+
* ```
|
|
2359
|
+
*
|
|
2360
|
+
* @example Reaction triggering another action (reactingTo auto-injected)
|
|
2361
|
+
* ```typescript
|
|
2362
|
+
* const app = act()
|
|
2363
|
+
* .withState(Order)
|
|
2364
|
+
* .withState(Inventory)
|
|
2365
|
+
* .on("OrderPlaced")
|
|
2366
|
+
* .do(async function reduceInventory(event, _stream, app) {
|
|
2367
|
+
* // Inside reaction handlers, reactingTo is auto-injected when omitted.
|
|
2368
|
+
* // The triggering event is used by default, maintaining the correlation chain.
|
|
2369
|
+
* await app.do(
|
|
2370
|
+
* "reduceStock",
|
|
2371
|
+
* { stream: "inventory-1", actor: { id: "sys", name: "system" } },
|
|
2372
|
+
* { amount: event.data.items.length }
|
|
2373
|
+
* );
|
|
2374
|
+
* // To use a different correlation, pass reactingTo explicitly:
|
|
2375
|
+
* // await app.do("reduceStock", target, payload, customEvent);
|
|
2376
|
+
* })
|
|
2377
|
+
* .to("inventory-1")
|
|
2378
|
+
* .build();
|
|
2379
|
+
* ```
|
|
2380
|
+
*
|
|
2381
|
+
* @see {@link Target} for target structure
|
|
2382
|
+
* @see {@link Snapshot} for return value structure
|
|
2383
|
+
* @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
|
|
2384
|
+
*/
|
|
2385
|
+
async do(action2, target, payload, reactingTo, skipValidation = false) {
|
|
2386
|
+
const snapshots = await this._es.action(
|
|
2387
|
+
this.registry.actions[action2],
|
|
2388
|
+
action2,
|
|
2389
|
+
target,
|
|
2390
|
+
payload,
|
|
2391
|
+
reactingTo,
|
|
2392
|
+
skipValidation
|
|
2393
|
+
);
|
|
2394
|
+
if (this._reactive_events.size > 0) {
|
|
2395
|
+
for (const snap2 of snapshots) {
|
|
2396
|
+
if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
|
|
2397
|
+
this._drain.arm();
|
|
2398
|
+
break;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
this.emit("committed", snapshots);
|
|
2403
|
+
return snapshots;
|
|
2404
|
+
}
|
|
2405
|
+
async load(stateOrName, stream, callback, asOf) {
|
|
2406
|
+
let merged;
|
|
2407
|
+
if (typeof stateOrName === "string") {
|
|
2408
|
+
const found = this._states.get(stateOrName);
|
|
2409
|
+
if (!found) throw new Error(`State "${stateOrName}" not found`);
|
|
2410
|
+
merged = found;
|
|
2411
|
+
} else {
|
|
2412
|
+
merged = this._states.get(stateOrName.name) || stateOrName;
|
|
2413
|
+
}
|
|
2414
|
+
return await this._es.load(merged, stream, callback, asOf);
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Queries the event store for events matching a filter.
|
|
2418
|
+
*
|
|
2419
|
+
* Use this for analyzing event streams, generating reports, or debugging.
|
|
2420
|
+
* The callback is invoked for each matching event, and the method returns
|
|
2421
|
+
* summary information (first event, last event, total count).
|
|
2422
|
+
*
|
|
2423
|
+
* For small result sets, consider using {@link query_array} instead.
|
|
2424
|
+
*
|
|
2425
|
+
* @param query - Filter criteria — see {@link Query} for available fields
|
|
2426
|
+
* (`stream`, `name`, `after`, `before`, `created_after`, `created_before`,
|
|
2427
|
+
* `limit`, `with_snaps`, `stream_exact`)
|
|
2428
|
+
* @param callback - Optional callback invoked for each matching event
|
|
2429
|
+
* @returns Object with first event, last event, and total count
|
|
2430
|
+
*
|
|
2431
|
+
* @example Query all events for a stream
|
|
2432
|
+
* ```typescript
|
|
2433
|
+
* const { first, last, count } = await app.query(
|
|
2434
|
+
* { stream: "counter-1" },
|
|
2435
|
+
* (event) => console.log(event.name, event.data)
|
|
2436
|
+
* );
|
|
2437
|
+
* console.log(`Found ${count} events from ${first?.id} to ${last?.id}`);
|
|
2438
|
+
* ```
|
|
2439
|
+
*
|
|
2440
|
+
* @example Query specific event types
|
|
2441
|
+
* ```typescript
|
|
2442
|
+
* const { count } = await app.query(
|
|
2443
|
+
* { name: "UserCreated", limit: 100 },
|
|
2444
|
+
* (event) => {
|
|
2445
|
+
* console.log("User created:", event.data.email);
|
|
2446
|
+
* }
|
|
2447
|
+
* );
|
|
2448
|
+
* ```
|
|
2449
|
+
*
|
|
2450
|
+
* @example Query events in time range
|
|
2451
|
+
* ```typescript
|
|
2452
|
+
* const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
2453
|
+
* const { count } = await app.query({
|
|
2454
|
+
* created_after: yesterday,
|
|
2455
|
+
* stream: "user-123"
|
|
2456
|
+
* });
|
|
2457
|
+
* console.log(`User had ${count} events in last 24 hours`);
|
|
2458
|
+
* ```
|
|
2459
|
+
*
|
|
2460
|
+
* @see {@link query_array} for loading events into memory
|
|
2461
|
+
*/
|
|
2462
|
+
async query(query, callback) {
|
|
2463
|
+
let first;
|
|
2464
|
+
let last;
|
|
2465
|
+
const count = await store().query((e) => {
|
|
2466
|
+
if (!first) first = e;
|
|
2467
|
+
last = e;
|
|
2468
|
+
callback?.(e);
|
|
2469
|
+
}, query);
|
|
2470
|
+
return { first, last, count };
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Queries the event store and returns all matching events in memory.
|
|
2474
|
+
*
|
|
2475
|
+
* **Use with caution** - this loads all results into memory. For large result sets,
|
|
2476
|
+
* use {@link query} with a callback instead to process events incrementally.
|
|
2477
|
+
*
|
|
2478
|
+
* @param query - The query filter (same as {@link query})
|
|
2479
|
+
* @returns Array of all matching events
|
|
2480
|
+
*
|
|
2481
|
+
* @example Load all events for a stream
|
|
2482
|
+
* ```typescript
|
|
2483
|
+
* const events = await app.query_array({ stream: "counter-1" });
|
|
2484
|
+
* console.log(`Loaded ${events.length} events`);
|
|
2485
|
+
* events.forEach(event => console.log(event.name, event.data));
|
|
2486
|
+
* ```
|
|
2487
|
+
*
|
|
2488
|
+
* @example Get recent events
|
|
2489
|
+
* ```typescript
|
|
2490
|
+
* const recent = await app.query_array({
|
|
2491
|
+
* stream: "user-123",
|
|
2492
|
+
* limit: 10
|
|
2493
|
+
* });
|
|
2494
|
+
* ```
|
|
2495
|
+
*
|
|
2496
|
+
* @see {@link query} for large result sets
|
|
2497
|
+
*/
|
|
2498
|
+
async query_array(query) {
|
|
2499
|
+
const events = [];
|
|
2500
|
+
await store().query((e) => events.push(e), query);
|
|
2501
|
+
return events;
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Processes pending reactions by draining uncommitted events from the event store.
|
|
2505
|
+
*
|
|
2506
|
+
* Runs a single drain cycle:
|
|
2507
|
+
* 1. Polls the store for streams with uncommitted events
|
|
2508
|
+
* 2. Leases streams to prevent concurrent processing
|
|
2509
|
+
* 3. Fetches events for each leased stream
|
|
2510
|
+
* 4. Executes matching reaction handlers
|
|
2511
|
+
* 5. Acknowledges successful reactions or blocks failing ones
|
|
2512
|
+
*
|
|
2513
|
+
* Drain uses a dual-frontier strategy to balance processing of new streams (lagging)
|
|
2514
|
+
* vs active streams (leading). The ratio adapts based on event pressure.
|
|
2515
|
+
*
|
|
2516
|
+
* Call `correlate()` before `drain()` to discover target streams. For a higher-level
|
|
2517
|
+
* API that handles debouncing, correlation, and signaling automatically, use {@link settle}.
|
|
2518
|
+
*
|
|
2519
|
+
* @param options - Drain configuration — see {@link DrainOptions} for fields
|
|
2520
|
+
* (`streamLimit`, `eventLimit`, `leaseMillis`).
|
|
2521
|
+
* @returns Drain statistics with fetched, leased, acked, and blocked counts
|
|
2522
|
+
*
|
|
2523
|
+
* @example In tests and scripts
|
|
2524
|
+
* ```typescript
|
|
2525
|
+
* await app.do("createUser", target, payload);
|
|
2526
|
+
* await app.correlate();
|
|
2527
|
+
* await app.drain();
|
|
2528
|
+
* ```
|
|
2529
|
+
*
|
|
2530
|
+
* @example In production, prefer settle()
|
|
2531
|
+
* ```typescript
|
|
2532
|
+
* await app.do("CreateItem", target, input);
|
|
2533
|
+
* app.settle(); // debounced correlate→drain, emits "settled"
|
|
2534
|
+
* ```
|
|
2535
|
+
*
|
|
2536
|
+
* @see {@link settle} for debounced correlate→drain with lifecycle events
|
|
2537
|
+
* @see {@link correlate} for dynamic stream discovery
|
|
2538
|
+
* @see {@link start_correlations} for automatic correlation
|
|
2539
|
+
*/
|
|
2540
|
+
async drain(options = {}) {
|
|
2541
|
+
return this._drain.drain(options);
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
2545
|
+
*
|
|
2546
|
+
* Correlation enables "dynamic reactions" where target streams are determined at runtime
|
|
2547
|
+
* based on event content. For example, you might create a stats stream for each user
|
|
2548
|
+
* when they perform certain actions.
|
|
2549
|
+
*
|
|
2550
|
+
* This method scans events matching the query and identifies new target streams based
|
|
2551
|
+
* on reaction resolvers. It then registers these streams so they'll be picked up by
|
|
2552
|
+
* the next drain cycle.
|
|
2553
|
+
*
|
|
2554
|
+
* @param query - Query filter to scan for new correlations
|
|
2555
|
+
* @param query - Scan filter — see {@link Query} for fields (typically
|
|
2556
|
+
* `{ after: <event-id>, limit: <count> }`)
|
|
2557
|
+
* @returns Object with newly leased streams and last scanned event ID
|
|
2558
|
+
*
|
|
2559
|
+
* @example Manual correlation
|
|
2560
|
+
* ```typescript
|
|
2561
|
+
* // Scan for new streams
|
|
2562
|
+
* const { leased, last_id } = await app.correlate({ after: 0, limit: 100 });
|
|
2563
|
+
* console.log(`Found ${leased.length} new streams`);
|
|
2564
|
+
*
|
|
2565
|
+
* // Save last_id for next scan
|
|
2566
|
+
* await saveCheckpoint(last_id);
|
|
2567
|
+
* ```
|
|
2568
|
+
*
|
|
2569
|
+
* @example Dynamic stream creation
|
|
2570
|
+
* ```typescript
|
|
2571
|
+
* const app = act()
|
|
2572
|
+
* .withState(User)
|
|
2573
|
+
* .withState(UserStats)
|
|
2574
|
+
* .on("UserLoggedIn")
|
|
2575
|
+
* .do(async (event) => ["incrementLoginCount", {}])
|
|
2576
|
+
* .to((event) => ({
|
|
2577
|
+
* target: `stats-${event.stream}` // Dynamic target per user
|
|
2578
|
+
* }))
|
|
2579
|
+
* .build();
|
|
2580
|
+
*
|
|
2581
|
+
* // Discover stats streams as users log in
|
|
2582
|
+
* await app.correlate();
|
|
2583
|
+
* ```
|
|
2584
|
+
*
|
|
2585
|
+
* @see {@link start_correlations} for automatic periodic correlation
|
|
2586
|
+
* @see {@link stop_correlations} to stop automatic correlation
|
|
2587
|
+
*/
|
|
2588
|
+
async correlate(query = { after: -1, limit: 10 }) {
|
|
2589
|
+
return this._correlate.correlate(query);
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Starts automatic periodic correlation worker for discovering new streams.
|
|
2593
|
+
*
|
|
2594
|
+
* The correlation worker runs in the background, scanning for new events and identifying
|
|
2595
|
+
* new target streams based on reaction resolvers. It maintains a sliding window that
|
|
2596
|
+
* advances with each scan, ensuring all events are eventually correlated.
|
|
2597
|
+
*
|
|
2598
|
+
* This is useful for dynamic stream creation patterns where you don't know all streams
|
|
2599
|
+
* upfront - they're discovered as events arrive.
|
|
2600
|
+
*
|
|
2601
|
+
* **Note:** Only one correlation worker can run at a time per Act instance.
|
|
2602
|
+
*
|
|
2603
|
+
* @param query - Query filter for correlation scans — see {@link Query}
|
|
2604
|
+
* (typically `{ after: -1, limit: 100 }`)
|
|
2605
|
+
* @param frequency - Correlation frequency in milliseconds (default: 10000)
|
|
2606
|
+
* @param callback - Optional callback invoked with newly discovered streams
|
|
2607
|
+
* @returns `true` if worker started, `false` if already running
|
|
2608
|
+
*
|
|
2609
|
+
* @example Start automatic correlation
|
|
2610
|
+
* ```typescript
|
|
2611
|
+
* // Start correlation worker scanning every 5 seconds
|
|
2612
|
+
* app.start_correlations(
|
|
2613
|
+
* { after: 0, limit: 100 },
|
|
2614
|
+
* 5000,
|
|
2615
|
+
* (leased) => {
|
|
2616
|
+
* console.log(`Discovered ${leased.length} new streams`);
|
|
2617
|
+
* }
|
|
2618
|
+
* );
|
|
2619
|
+
*
|
|
2620
|
+
* // Later, stop it
|
|
2621
|
+
* app.stop_correlations();
|
|
2622
|
+
* ```
|
|
2623
|
+
*
|
|
2624
|
+
* @example With checkpoint persistence
|
|
2625
|
+
* ```typescript
|
|
2626
|
+
* // Load last checkpoint
|
|
2627
|
+
* const lastId = await loadCheckpoint();
|
|
2628
|
+
*
|
|
2629
|
+
* app.start_correlations(
|
|
2630
|
+
* { after: lastId, limit: 100 },
|
|
2631
|
+
* 10000,
|
|
2632
|
+
* async (leased) => {
|
|
2633
|
+
* // Save checkpoint for next restart
|
|
2634
|
+
* if (leased.length) {
|
|
2635
|
+
* const maxId = Math.max(...leased.map(l => l.at));
|
|
2636
|
+
* await saveCheckpoint(maxId);
|
|
2637
|
+
* }
|
|
2638
|
+
* }
|
|
2639
|
+
* );
|
|
2640
|
+
* ```
|
|
2641
|
+
*
|
|
2642
|
+
* @see {@link correlate} for manual one-time correlation
|
|
2643
|
+
* @see {@link stop_correlations} to stop the worker
|
|
2644
|
+
*/
|
|
2645
|
+
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
2646
|
+
return this._correlate.startPolling(query, frequency, callback);
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Stops the automatic correlation worker.
|
|
2650
|
+
*
|
|
2651
|
+
* Call this to stop the background correlation worker started by {@link start_correlations}.
|
|
2652
|
+
* This is automatically called when the Act instance is disposed.
|
|
2653
|
+
*
|
|
2654
|
+
* @example
|
|
2655
|
+
* ```typescript
|
|
2656
|
+
* // Start correlation
|
|
2657
|
+
* app.start_correlations();
|
|
2658
|
+
*
|
|
2659
|
+
* // Later, stop it
|
|
2660
|
+
* app.stop_correlations();
|
|
2661
|
+
* ```
|
|
2662
|
+
*
|
|
2663
|
+
* @see {@link start_correlations}
|
|
2664
|
+
*/
|
|
2665
|
+
stop_correlations() {
|
|
2666
|
+
this._correlate.stopPolling();
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Cancels any pending or active settle cycle.
|
|
2670
|
+
*
|
|
2671
|
+
* @see {@link settle}
|
|
2672
|
+
*/
|
|
2673
|
+
stop_settling() {
|
|
2674
|
+
this._settle.stop();
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Reset reaction stream watermarks and request a drain on the next
|
|
2678
|
+
* `drain()` / `settle()` cycle.
|
|
2679
|
+
*
|
|
2680
|
+
* Use this to replay events through projections (or other reaction targets)
|
|
2681
|
+
* after changing handler logic. Equivalent to calling `store().reset(streams)`
|
|
2682
|
+
* directly, but also raises the orchestrator's internal "needs drain" flag —
|
|
2683
|
+
* `store().reset(...)` alone leaves the flag untouched, so a settled app
|
|
2684
|
+
* would short-circuit and skip the replay.
|
|
2685
|
+
*
|
|
2686
|
+
* Pair with `app.settle()` (or a single `app.drain()` for small streams).
|
|
2687
|
+
* `settle()` loops correlate→drain until no progress is made, so one call
|
|
2688
|
+
* fully catches up paginated streams without forcing callers to roll
|
|
2689
|
+
* their own loop.
|
|
2690
|
+
*
|
|
2691
|
+
* @param streams - Reaction target streams (e.g., projection names) to reset
|
|
2692
|
+
* @returns Count of streams that were actually reset
|
|
2693
|
+
*
|
|
2694
|
+
* @example Rebuild a projection (production)
|
|
2695
|
+
* ```typescript
|
|
2696
|
+
* await app.reset(["my-projection"]);
|
|
2697
|
+
* app.settle({ eventLimit: 1000 }); // emits "settled" when fully replayed
|
|
2698
|
+
* ```
|
|
2699
|
+
*
|
|
2700
|
+
* @example Rebuild a projection (tests / scripts)
|
|
2701
|
+
* ```typescript
|
|
2702
|
+
* await app.reset(["my-projection"]);
|
|
2703
|
+
* await app.drain({ eventLimit: 1000 }); // small streams: one pass is enough
|
|
2704
|
+
* ```
|
|
2705
|
+
*
|
|
2706
|
+
* @see {@link Store.reset} for the underlying store primitive
|
|
2707
|
+
* @see {@link settle} for the debounced full-catch-up loop
|
|
2708
|
+
*/
|
|
2709
|
+
async reset(streams) {
|
|
2710
|
+
const count = await store().reset(streams);
|
|
2711
|
+
if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
|
|
2712
|
+
return count;
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Bulk-update scheduling priority for streams matching `filter`.
|
|
2716
|
+
*
|
|
2717
|
+
* Operator-grade override of the `claim()` lagging-frontier
|
|
2718
|
+
* ordering (ACT-102). Useful when a long-running replay needs to
|
|
2719
|
+
* jump ahead of other lagging streams, or when a no-longer-urgent
|
|
2720
|
+
* job should yield slots back to the rest. Build-time priorities
|
|
2721
|
+
* (set via the resolver's `priority` field) are subject to a
|
|
2722
|
+
* `max()` invariant across reactions; this API ignores that and
|
|
2723
|
+
* sets the priority outright on every matching row.
|
|
2724
|
+
*
|
|
2725
|
+
* Filter shape mirrors {@link query} / {@link Store.query_streams}:
|
|
2726
|
+
* `stream` / `source` are regex by default, exact with the
|
|
2727
|
+
* `*_exact` flags; `blocked` restricts to blocked or unblocked
|
|
2728
|
+
* rows. **An empty filter (`{}`) updates every registered stream.**
|
|
2729
|
+
*
|
|
2730
|
+
* @param filter - Selection criteria (regex by default).
|
|
2731
|
+
* @param priority - New priority value. Set as-is — no clamp.
|
|
2732
|
+
* @returns Count of streams whose priority changed.
|
|
2733
|
+
*
|
|
2734
|
+
* @example Boost a specific projection mid-replay
|
|
2735
|
+
* ```typescript
|
|
2736
|
+
* await app.prioritize({ stream: "^proj-orders$", stream_exact: false }, 10);
|
|
2737
|
+
* ```
|
|
2738
|
+
*
|
|
2739
|
+
* @example Drop all audit projections to background
|
|
2740
|
+
* ```typescript
|
|
2741
|
+
* await app.prioritize({ source: "^audit-" }, -5);
|
|
2742
|
+
* ```
|
|
2743
|
+
*
|
|
2744
|
+
* @example Reset everyone to default
|
|
2745
|
+
* ```typescript
|
|
2746
|
+
* await app.prioritize({}, 0);
|
|
2747
|
+
* ```
|
|
2748
|
+
*
|
|
2749
|
+
* @see {@link Store.prioritize} for the underlying primitive
|
|
2750
|
+
* @see {@link claim} for how priority biases scheduling
|
|
2751
|
+
*/
|
|
2752
|
+
async prioritize(filter, priority) {
|
|
2753
|
+
return store().prioritize(filter, priority);
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Close the books — guard, archive, truncate, and optionally restart streams.
|
|
2757
|
+
*
|
|
2758
|
+
* Safely removes historical events from the operational store:
|
|
2759
|
+
*
|
|
2760
|
+
* 1. **Correlate** — discover pending reaction targets
|
|
2761
|
+
* 2. **Safety check** — skip streams with pending reactions (skipped when no reactive events)
|
|
2762
|
+
* 3. **Guard** — commit `__tombstone__` with `expectedVersion` to block concurrent writes
|
|
2763
|
+
* 4. **Load state** — for streams in `snapshots`, load final state while guarded (no races)
|
|
2764
|
+
* 5. **Archive** — user callback per stream (abort-all on failure, streams are guarded)
|
|
2765
|
+
* 6. **Truncate + seed** — atomic: delete all events, insert `__snapshot__` or `__tombstone__`
|
|
2766
|
+
* 7. **Cache** — invalidate (tombstoned) or warm (restarted)
|
|
2767
|
+
* 8. **Emit "closed"** — lifecycle event with results
|
|
2768
|
+
*
|
|
2769
|
+
* @param targets - Per-stream close options (stream, restart?, archive?)
|
|
2770
|
+
* @returns `{ truncated: TruncateResult, skipped: string[] }`
|
|
2771
|
+
*
|
|
2772
|
+
* @example Archive and close
|
|
2773
|
+
* ```typescript
|
|
2774
|
+
* await app.close([
|
|
2775
|
+
* { stream: "order-123", archive: async () => { await archiveToS3("order-123"); } },
|
|
2776
|
+
* { stream: "order-456" },
|
|
2777
|
+
* ]);
|
|
2778
|
+
* ```
|
|
2779
|
+
*
|
|
2780
|
+
* @example Close with restart (state loaded automatically after guard)
|
|
2781
|
+
* ```typescript
|
|
2782
|
+
* await app.close([
|
|
2783
|
+
* { stream: "counter-1", restart: true },
|
|
2784
|
+
* { stream: "counter-2" }, // tombstoned
|
|
2785
|
+
* ]);
|
|
2786
|
+
* ```
|
|
2787
|
+
*/
|
|
2788
|
+
async close(targets) {
|
|
2789
|
+
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
2790
|
+
await this.correlate({ limit: 1e3 });
|
|
2791
|
+
const result = await runCloseCycle(targets, {
|
|
2792
|
+
reactiveEventsSize: this._reactive_events.size,
|
|
2793
|
+
eventToState: this._event_to_state,
|
|
2794
|
+
load: this._es.load,
|
|
2795
|
+
tombstone: this._es.tombstone,
|
|
2796
|
+
logger: this._logger
|
|
2797
|
+
});
|
|
2798
|
+
this.emit("closed", result);
|
|
2799
|
+
return result;
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Debounced, non-blocking correlate→drain cycle.
|
|
2803
|
+
*
|
|
2804
|
+
* Call this after `app.do()` (or `app.reset()`) to schedule a background
|
|
2805
|
+
* drain. Multiple rapid calls within the debounce window are coalesced
|
|
2806
|
+
* into a single cycle. Runs correlate→drain in a loop until a pass makes
|
|
2807
|
+
* no progress — no new subscriptions, no acks, no blocks — then emits
|
|
2808
|
+
* the `"settled"` lifecycle event. This means a single `settle()` call
|
|
2809
|
+
* fully catches up paginated streams (e.g. after `reset()` on a long
|
|
2810
|
+
* projection) without forcing callers to loop.
|
|
2811
|
+
*
|
|
2812
|
+
* @param options - Settle configuration — see {@link SettleOptions} for fields:
|
|
2813
|
+
* `debounceMs` (default 10), `correlate` (default `{ after: -1, limit: 100 }`),
|
|
2814
|
+
* `maxPasses` (default `Infinity` — kill-switch for runaway loops),
|
|
2815
|
+
* `streamLimit` (default 10), `eventLimit` (default 10),
|
|
2816
|
+
* `leaseMillis` (default 10000).
|
|
2817
|
+
*
|
|
2818
|
+
* @example API mutations
|
|
2819
|
+
* ```typescript
|
|
2820
|
+
* await app.do("CreateItem", target, input);
|
|
2821
|
+
* app.settle(); // non-blocking, returns immediately
|
|
2822
|
+
*
|
|
2823
|
+
* app.on("settled", (drain) => {
|
|
2824
|
+
* // notify SSE clients, invalidate caches, etc.
|
|
2825
|
+
* });
|
|
2826
|
+
* ```
|
|
2827
|
+
*
|
|
2828
|
+
* @see {@link drain} for single synchronous drain cycles
|
|
2829
|
+
* @see {@link correlate} for manual correlation
|
|
2830
|
+
*/
|
|
2831
|
+
settle(options = {}) {
|
|
2832
|
+
this._settle.schedule(options);
|
|
2833
|
+
}
|
|
2834
|
+
};
|
|
2835
|
+
|
|
2836
|
+
// src/builders/act-builder.ts
|
|
2837
|
+
function registerBatchHandler(proj, batchHandlers) {
|
|
2838
|
+
if (!proj.batchHandler || !proj.target) return;
|
|
2839
|
+
const existing = batchHandlers.get(proj.target);
|
|
2840
|
+
if (existing && existing !== proj.batchHandler) {
|
|
2841
|
+
throw new Error(`Duplicate batch handler for target "${proj.target}"`);
|
|
2842
|
+
}
|
|
2843
|
+
batchHandlers.set(proj.target, proj.batchHandler);
|
|
2844
|
+
}
|
|
2845
|
+
function act() {
|
|
2846
|
+
const states = /* @__PURE__ */ new Map();
|
|
2847
|
+
const registry = {
|
|
2848
|
+
actions: {},
|
|
2849
|
+
events: {}
|
|
2850
|
+
};
|
|
2851
|
+
const pendingProjections = [];
|
|
2852
|
+
const batchHandlers = /* @__PURE__ */ new Map();
|
|
2853
|
+
const builder = {
|
|
2854
|
+
withState: (state2) => {
|
|
2855
|
+
registerState(state2, states, registry.actions, registry.events);
|
|
2856
|
+
return builder;
|
|
2857
|
+
},
|
|
2858
|
+
withSlice: (input) => {
|
|
2859
|
+
for (const s of input.states.values()) {
|
|
2860
|
+
registerState(s, states, registry.actions, registry.events);
|
|
2861
|
+
}
|
|
2862
|
+
mergeEventRegister(registry.events, input.events);
|
|
2863
|
+
pendingProjections.push(...input.projections);
|
|
2864
|
+
return builder;
|
|
2865
|
+
},
|
|
2866
|
+
withProjection: (proj) => {
|
|
2867
|
+
mergeProjection(proj, registry.events);
|
|
2868
|
+
registerBatchHandler(proj, batchHandlers);
|
|
2869
|
+
return builder;
|
|
2870
|
+
},
|
|
2871
|
+
withActor: () => builder,
|
|
2872
|
+
on: (event) => ({
|
|
2873
|
+
do: (handler, options) => {
|
|
2874
|
+
const reaction = {
|
|
2875
|
+
handler,
|
|
2876
|
+
resolver: _this_,
|
|
2877
|
+
options: {
|
|
2878
|
+
blockOnError: options?.blockOnError ?? true,
|
|
2879
|
+
maxRetries: options?.maxRetries ?? 3
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
if (!handler.name)
|
|
2883
|
+
throw new Error(
|
|
2884
|
+
`Reaction handler for "${String(event)}" must be a named function`
|
|
2885
|
+
);
|
|
2886
|
+
registry.events[event].reactions.set(handler.name, reaction);
|
|
2887
|
+
return Object.assign(builder, {
|
|
2888
|
+
to(resolver) {
|
|
2889
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2890
|
+
return builder;
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
}),
|
|
2895
|
+
build: (options) => {
|
|
2896
|
+
for (const proj of pendingProjections) {
|
|
2897
|
+
mergeProjection(proj, registry.events);
|
|
2898
|
+
registerBatchHandler(proj, batchHandlers);
|
|
2899
|
+
}
|
|
2900
|
+
return new Act(
|
|
2901
|
+
registry,
|
|
2902
|
+
states,
|
|
2903
|
+
batchHandlers,
|
|
2904
|
+
options
|
|
2905
|
+
);
|
|
2906
|
+
},
|
|
2907
|
+
events: registry.events
|
|
2908
|
+
};
|
|
2909
|
+
return builder;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
// src/builders/projection-builder.ts
|
|
2913
|
+
function _projection(target) {
|
|
2914
|
+
const events = {};
|
|
2915
|
+
const defaultResolver = typeof target === "string" ? { target } : void 0;
|
|
2916
|
+
const base = {
|
|
2917
|
+
on: (entry) => {
|
|
2918
|
+
const keys = Object.keys(entry);
|
|
2919
|
+
if (keys.length !== 1) throw new Error(".on() requires exactly one key");
|
|
2920
|
+
const event = keys[0];
|
|
2921
|
+
const schema = entry[event];
|
|
2922
|
+
if (!(event in events)) {
|
|
2923
|
+
events[event] = {
|
|
2924
|
+
schema,
|
|
2925
|
+
reactions: /* @__PURE__ */ new Map()
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
return {
|
|
2929
|
+
do: (handler) => {
|
|
2930
|
+
const reaction = {
|
|
2931
|
+
handler,
|
|
2932
|
+
resolver: defaultResolver ?? _this_,
|
|
2933
|
+
options: {
|
|
2934
|
+
blockOnError: true,
|
|
2935
|
+
maxRetries: 3
|
|
2936
|
+
}
|
|
2937
|
+
};
|
|
2938
|
+
const register = events[event];
|
|
2939
|
+
if (!handler.name)
|
|
2940
|
+
throw new Error(
|
|
2941
|
+
`Projection handler for "${event}" must be a named function`
|
|
2942
|
+
);
|
|
2943
|
+
register.reactions.set(handler.name, reaction);
|
|
2944
|
+
const widened = base;
|
|
2945
|
+
return Object.assign(widened, {
|
|
2946
|
+
to(resolver) {
|
|
2947
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2948
|
+
return widened;
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
},
|
|
2954
|
+
build: () => ({
|
|
2955
|
+
_tag: "Projection",
|
|
2956
|
+
events,
|
|
2957
|
+
...target !== void 0 && { target }
|
|
2958
|
+
}),
|
|
2959
|
+
events
|
|
2960
|
+
};
|
|
2961
|
+
if (typeof target === "string") {
|
|
2962
|
+
return Object.assign(base, {
|
|
2963
|
+
batch: (handler) => ({
|
|
2964
|
+
build: () => ({
|
|
2965
|
+
_tag: "Projection",
|
|
2966
|
+
events,
|
|
2967
|
+
target,
|
|
2968
|
+
batchHandler: handler
|
|
2969
|
+
})
|
|
2970
|
+
})
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
return base;
|
|
2974
|
+
}
|
|
2975
|
+
function projection(target) {
|
|
2976
|
+
return _projection(target);
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
// src/builders/slice-builder.ts
|
|
2980
|
+
function slice() {
|
|
2981
|
+
const states = /* @__PURE__ */ new Map();
|
|
2982
|
+
const actions = {};
|
|
2983
|
+
const events = {};
|
|
2984
|
+
const projections = [];
|
|
2985
|
+
const builder = {
|
|
2986
|
+
withState: (state2) => {
|
|
2987
|
+
registerState(state2, states, actions, events);
|
|
2988
|
+
return builder;
|
|
2989
|
+
},
|
|
2990
|
+
withProjection: (proj) => {
|
|
2991
|
+
projections.push(proj);
|
|
2992
|
+
return builder;
|
|
2993
|
+
},
|
|
2994
|
+
on: (event) => ({
|
|
2995
|
+
do: (handler, options) => {
|
|
2996
|
+
const reaction = {
|
|
2997
|
+
handler,
|
|
2998
|
+
resolver: _this_,
|
|
2999
|
+
options: {
|
|
3000
|
+
blockOnError: options?.blockOnError ?? true,
|
|
3001
|
+
maxRetries: options?.maxRetries ?? 3
|
|
3002
|
+
}
|
|
3003
|
+
};
|
|
3004
|
+
if (!handler.name)
|
|
3005
|
+
throw new Error(
|
|
3006
|
+
`Reaction handler for "${String(event)}" must be a named function`
|
|
3007
|
+
);
|
|
3008
|
+
events[event].reactions.set(handler.name, reaction);
|
|
3009
|
+
return Object.assign(builder, {
|
|
3010
|
+
to(resolver) {
|
|
3011
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
3012
|
+
return builder;
|
|
3013
|
+
}
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
}),
|
|
3017
|
+
build: () => ({
|
|
3018
|
+
_tag: "Slice",
|
|
3019
|
+
states,
|
|
3020
|
+
events,
|
|
3021
|
+
projections
|
|
3022
|
+
}),
|
|
3023
|
+
events
|
|
3024
|
+
};
|
|
3025
|
+
return builder;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// src/builders/state-builder.ts
|
|
3029
|
+
function state(entry) {
|
|
3030
|
+
const keys = Object.keys(entry);
|
|
3031
|
+
if (keys.length !== 1) throw new Error("state() requires exactly one key");
|
|
3032
|
+
const name = keys[0];
|
|
3033
|
+
const stateSchema = entry[name];
|
|
3034
|
+
return {
|
|
3035
|
+
init(init) {
|
|
3036
|
+
return {
|
|
3037
|
+
emits(events) {
|
|
3038
|
+
const defaultPatch = Object.fromEntries(
|
|
3039
|
+
Object.keys(events).map((k) => {
|
|
3040
|
+
const fn = Object.assign(({ data }) => data, {
|
|
3041
|
+
_passthrough: true
|
|
3042
|
+
});
|
|
3043
|
+
return [k, fn];
|
|
3044
|
+
})
|
|
3045
|
+
);
|
|
3046
|
+
const internal = {
|
|
3047
|
+
events,
|
|
3048
|
+
actions: {},
|
|
3049
|
+
state: stateSchema,
|
|
3050
|
+
name,
|
|
3051
|
+
init,
|
|
3052
|
+
patch: defaultPatch,
|
|
3053
|
+
on: {}
|
|
3054
|
+
};
|
|
3055
|
+
const builder = action_builder(internal);
|
|
3056
|
+
return Object.assign(builder, {
|
|
3057
|
+
patch(customPatch) {
|
|
3058
|
+
Object.assign(internal.patch, customPatch);
|
|
3059
|
+
return builder;
|
|
3060
|
+
}
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
function action_builder(state2) {
|
|
3068
|
+
const internal = state2;
|
|
3069
|
+
const builder = {
|
|
3070
|
+
on(entry) {
|
|
3071
|
+
const keys = Object.keys(entry);
|
|
3072
|
+
if (keys.length !== 1) throw new Error(".on() requires exactly one key");
|
|
3073
|
+
const action2 = keys[0];
|
|
3074
|
+
const schema = entry[action2];
|
|
3075
|
+
if (action2 in internal.actions)
|
|
3076
|
+
throw new Error(`Duplicate action "${action2}"`);
|
|
3077
|
+
internal.actions[action2] = schema;
|
|
3078
|
+
function given(rules) {
|
|
3079
|
+
internal.given ??= {};
|
|
3080
|
+
internal.given[action2] = rules;
|
|
3081
|
+
return { emit };
|
|
3082
|
+
}
|
|
3083
|
+
function emit(handler) {
|
|
3084
|
+
if (typeof handler === "string") {
|
|
3085
|
+
const eventName = handler;
|
|
3086
|
+
internal.on[action2] = (payload) => [
|
|
3087
|
+
eventName,
|
|
3088
|
+
payload
|
|
3089
|
+
];
|
|
3090
|
+
} else {
|
|
3091
|
+
internal.on[action2] = handler;
|
|
3092
|
+
}
|
|
3093
|
+
return builder;
|
|
3094
|
+
}
|
|
3095
|
+
return { given, emit };
|
|
3096
|
+
},
|
|
3097
|
+
snap(snap2) {
|
|
3098
|
+
internal.snap = snap2;
|
|
3099
|
+
return builder;
|
|
3100
|
+
},
|
|
3101
|
+
build() {
|
|
3102
|
+
return internal;
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
return builder;
|
|
3106
|
+
}
|
|
3107
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3108
|
+
0 && (module.exports = {
|
|
3109
|
+
Act,
|
|
3110
|
+
ActorSchema,
|
|
3111
|
+
CausationEventSchema,
|
|
3112
|
+
CommittedMetaSchema,
|
|
3113
|
+
ConcurrencyError,
|
|
3114
|
+
ConsoleLogger,
|
|
3115
|
+
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
3116
|
+
DEFAULT_SETTLE_DEBOUNCE_MS,
|
|
3117
|
+
Environments,
|
|
3118
|
+
Errors,
|
|
3119
|
+
EventMetaSchema,
|
|
3120
|
+
ExitCodes,
|
|
3121
|
+
InMemoryCache,
|
|
3122
|
+
InMemoryStore,
|
|
3123
|
+
InvariantError,
|
|
3124
|
+
LogLevels,
|
|
3125
|
+
PackageSchema,
|
|
3126
|
+
QuerySchema,
|
|
3127
|
+
SNAP_EVENT,
|
|
3128
|
+
StreamClosedError,
|
|
3129
|
+
TOMBSTONE_EVENT,
|
|
3130
|
+
TargetSchema,
|
|
3131
|
+
ValidationError,
|
|
3132
|
+
ZodEmpty,
|
|
3133
|
+
act,
|
|
3134
|
+
cache,
|
|
3135
|
+
config,
|
|
3136
|
+
dispose,
|
|
3137
|
+
disposeAndExit,
|
|
3138
|
+
extend,
|
|
3139
|
+
log,
|
|
3140
|
+
port,
|
|
3141
|
+
projection,
|
|
3142
|
+
sleep,
|
|
3143
|
+
slice,
|
|
3144
|
+
state,
|
|
3145
|
+
store,
|
|
3146
|
+
validate
|
|
3147
|
+
});
|
|
3148
|
+
//# sourceMappingURL=index.cjs.map
|