@rotorsoft/act 0.37.0 → 0.39.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 +54 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +27 -0
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/builders/act-builder.d.ts.map +1 -1
- package/dist/@types/ports.d.ts +10 -51
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/test/index.d.ts +2 -0
- package/dist/@types/test/index.d.ts.map +1 -0
- package/dist/@types/test/sandbox.d.ts +113 -0
- package/dist/@types/test/sandbox.d.ts.map +1 -0
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/chunk-5WRI5ZAA.js.map +1 -0
- package/dist/chunk-RHS57BUR.js +877 -0
- package/dist/chunk-RHS57BUR.js.map +1 -0
- package/dist/dist-INXH4GUE.js +546 -0
- package/dist/dist-INXH4GUE.js.map +1 -0
- package/dist/index.cjs +175 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +160 -933
- package/dist/index.js.map +1 -1
- package/dist/magic-string.es-ZGO77SPS.js +1309 -0
- package/dist/magic-string.es-ZGO77SPS.js.map +1 -0
- package/dist/test/index.cjs +4833 -0
- package/dist/test/index.cjs.map +1 -0
- package/dist/test/index.js +3912 -0
- package/dist/test/index.js.map +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +6 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConsoleLogger,
|
|
3
|
+
ExitCodes,
|
|
4
|
+
InMemoryCache,
|
|
5
|
+
InMemoryStore,
|
|
6
|
+
LruSet,
|
|
7
|
+
PackageSchema,
|
|
8
|
+
SNAP_EVENT,
|
|
9
|
+
TOMBSTONE_EVENT,
|
|
10
|
+
cache,
|
|
11
|
+
config,
|
|
12
|
+
dispose,
|
|
13
|
+
disposeAndExit,
|
|
14
|
+
extend,
|
|
15
|
+
log,
|
|
16
|
+
port,
|
|
17
|
+
scoped,
|
|
18
|
+
sleep,
|
|
19
|
+
store,
|
|
20
|
+
validate
|
|
21
|
+
} from "./chunk-RHS57BUR.js";
|
|
1
22
|
import {
|
|
2
23
|
ActorSchema,
|
|
3
24
|
CausationEventSchema,
|
|
@@ -14,844 +35,7 @@ import {
|
|
|
14
35
|
ValidationError,
|
|
15
36
|
ZodEmpty
|
|
16
37
|
} from "./chunk-AGWZY6YT.js";
|
|
17
|
-
|
|
18
|
-
// src/adapters/console-logger.ts
|
|
19
|
-
var LEVEL_VALUES = {
|
|
20
|
-
fatal: 60,
|
|
21
|
-
error: 50,
|
|
22
|
-
warn: 40,
|
|
23
|
-
info: 30,
|
|
24
|
-
debug: 20,
|
|
25
|
-
trace: 10
|
|
26
|
-
};
|
|
27
|
-
var LEVEL_COLORS = {
|
|
28
|
-
fatal: "\x1B[41m\x1B[37m",
|
|
29
|
-
// white on red bg
|
|
30
|
-
error: "\x1B[31m",
|
|
31
|
-
// red
|
|
32
|
-
warn: "\x1B[33m",
|
|
33
|
-
// yellow
|
|
34
|
-
info: "\x1B[32m",
|
|
35
|
-
// green
|
|
36
|
-
debug: "\x1B[36m",
|
|
37
|
-
// cyan
|
|
38
|
-
trace: "\x1B[90m"
|
|
39
|
-
// gray
|
|
40
|
-
};
|
|
41
|
-
var RESET = "\x1B[0m";
|
|
42
|
-
var noop = () => {
|
|
43
|
-
};
|
|
44
|
-
var ConsoleLogger = class _ConsoleLogger {
|
|
45
|
-
level;
|
|
46
|
-
_pretty;
|
|
47
|
-
fatal;
|
|
48
|
-
error;
|
|
49
|
-
warn;
|
|
50
|
-
info;
|
|
51
|
-
debug;
|
|
52
|
-
trace;
|
|
53
|
-
constructor(options = {}) {
|
|
54
|
-
const {
|
|
55
|
-
level = "info",
|
|
56
|
-
pretty = process.env.NODE_ENV !== "production",
|
|
57
|
-
bindings
|
|
58
|
-
} = options;
|
|
59
|
-
this._pretty = pretty;
|
|
60
|
-
this.level = level;
|
|
61
|
-
const threshold = LEVEL_VALUES[level] ?? 30;
|
|
62
|
-
const write = pretty ? this._prettyWrite.bind(this, bindings) : this._jsonWrite.bind(this, bindings);
|
|
63
|
-
this.fatal = write.bind(this, "fatal", 60);
|
|
64
|
-
this.error = threshold <= 50 ? write.bind(this, "error", 50) : noop;
|
|
65
|
-
this.warn = threshold <= 40 ? write.bind(this, "warn", 40) : noop;
|
|
66
|
-
this.info = threshold <= 30 ? write.bind(this, "info", 30) : noop;
|
|
67
|
-
this.debug = threshold <= 20 ? write.bind(this, "debug", 20) : noop;
|
|
68
|
-
this.trace = threshold <= 10 ? write.bind(this, "trace", 10) : noop;
|
|
69
|
-
}
|
|
70
|
-
/** No-op — `console.log` has no resources to release. */
|
|
71
|
-
async dispose() {
|
|
72
|
-
}
|
|
73
|
-
/** @inheritDoc */
|
|
74
|
-
child(bindings) {
|
|
75
|
-
return new _ConsoleLogger({
|
|
76
|
-
level: this.level,
|
|
77
|
-
pretty: this._pretty,
|
|
78
|
-
bindings
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
_jsonWrite(bindings, level, _num, objOrMsg, msg) {
|
|
82
|
-
let obj;
|
|
83
|
-
let message;
|
|
84
|
-
if (typeof objOrMsg === "string") {
|
|
85
|
-
message = objOrMsg;
|
|
86
|
-
obj = {};
|
|
87
|
-
} else if (objOrMsg !== null && typeof objOrMsg === "object") {
|
|
88
|
-
message = msg;
|
|
89
|
-
obj = { ...objOrMsg };
|
|
90
|
-
} else {
|
|
91
|
-
message = msg;
|
|
92
|
-
obj = { value: objOrMsg };
|
|
93
|
-
}
|
|
94
|
-
const entry = Object.assign({ level, time: Date.now() }, bindings, obj);
|
|
95
|
-
if (message) entry.msg = message;
|
|
96
|
-
let line;
|
|
97
|
-
try {
|
|
98
|
-
line = JSON.stringify(entry);
|
|
99
|
-
} catch {
|
|
100
|
-
line = JSON.stringify({
|
|
101
|
-
level,
|
|
102
|
-
time: entry.time,
|
|
103
|
-
msg: message ?? "[unserializable]",
|
|
104
|
-
unserializable: true
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
process.stdout.write(line + "\n");
|
|
108
|
-
}
|
|
109
|
-
_prettyWrite(bindings, level, _num, objOrMsg, msg) {
|
|
110
|
-
const color = LEVEL_COLORS[level];
|
|
111
|
-
const tag = `${color}${level.toUpperCase().padEnd(5)}${RESET}`;
|
|
112
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
113
|
-
let message;
|
|
114
|
-
let data;
|
|
115
|
-
if (typeof objOrMsg === "string") {
|
|
116
|
-
message = objOrMsg;
|
|
117
|
-
} else {
|
|
118
|
-
message = msg ?? "";
|
|
119
|
-
if (objOrMsg !== void 0 && objOrMsg !== null) {
|
|
120
|
-
try {
|
|
121
|
-
data = JSON.stringify(objOrMsg);
|
|
122
|
-
} catch {
|
|
123
|
-
data = "[unserializable]";
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const bindStr = bindings && Object.keys(bindings).length ? ` ${JSON.stringify(bindings)}` : "";
|
|
128
|
-
const parts = [ts, tag, message, data, bindStr].filter(Boolean);
|
|
129
|
-
process.stdout.write(parts.join(" ") + "\n");
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// src/lru-map.ts
|
|
134
|
-
var LruMap = class {
|
|
135
|
-
constructor(_maxSize) {
|
|
136
|
-
this._maxSize = _maxSize;
|
|
137
|
-
}
|
|
138
|
-
_entries = /* @__PURE__ */ new Map();
|
|
139
|
-
get(key) {
|
|
140
|
-
const v = this._entries.get(key);
|
|
141
|
-
if (v === void 0) return void 0;
|
|
142
|
-
this._entries.delete(key);
|
|
143
|
-
this._entries.set(key, v);
|
|
144
|
-
return v;
|
|
145
|
-
}
|
|
146
|
-
has(key) {
|
|
147
|
-
return this._entries.has(key);
|
|
148
|
-
}
|
|
149
|
-
set(key, value) {
|
|
150
|
-
this._entries.delete(key);
|
|
151
|
-
if (this._entries.size >= this._maxSize) {
|
|
152
|
-
const oldest = this._entries.keys().next().value;
|
|
153
|
-
this._entries.delete(oldest);
|
|
154
|
-
}
|
|
155
|
-
this._entries.set(key, value);
|
|
156
|
-
}
|
|
157
|
-
delete(key) {
|
|
158
|
-
return this._entries.delete(key);
|
|
159
|
-
}
|
|
160
|
-
clear() {
|
|
161
|
-
this._entries.clear();
|
|
162
|
-
}
|
|
163
|
-
get size() {
|
|
164
|
-
return this._entries.size;
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
var LruSet = class {
|
|
168
|
-
_map;
|
|
169
|
-
constructor(maxSize) {
|
|
170
|
-
this._map = new LruMap(maxSize);
|
|
171
|
-
}
|
|
172
|
-
has(value) {
|
|
173
|
-
return this._map.has(value);
|
|
174
|
-
}
|
|
175
|
-
add(value) {
|
|
176
|
-
this._map.set(value, true);
|
|
177
|
-
}
|
|
178
|
-
delete(value) {
|
|
179
|
-
return this._map.delete(value);
|
|
180
|
-
}
|
|
181
|
-
clear() {
|
|
182
|
-
this._map.clear();
|
|
183
|
-
}
|
|
184
|
-
get size() {
|
|
185
|
-
return this._map.size;
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// src/adapters/in-memory-cache.ts
|
|
190
|
-
var InMemoryCache = class {
|
|
191
|
-
// CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
|
|
192
|
-
// any is bidirectionally compatible with the per-call TState binding, while
|
|
193
|
-
// the public Cache interface still presents a typed surface to callers.
|
|
194
|
-
_entries;
|
|
195
|
-
constructor(options) {
|
|
196
|
-
this._entries = new LruMap(options?.maxSize ?? 1e3);
|
|
197
|
-
}
|
|
198
|
-
/** @inheritDoc */
|
|
199
|
-
async get(stream) {
|
|
200
|
-
return this._entries.get(stream);
|
|
201
|
-
}
|
|
202
|
-
/** @inheritDoc */
|
|
203
|
-
async set(stream, entry) {
|
|
204
|
-
this._entries.set(stream, entry);
|
|
205
|
-
}
|
|
206
|
-
/** @inheritDoc */
|
|
207
|
-
async invalidate(stream) {
|
|
208
|
-
this._entries.delete(stream);
|
|
209
|
-
}
|
|
210
|
-
/** @inheritDoc */
|
|
211
|
-
async clear() {
|
|
212
|
-
this._entries.clear();
|
|
213
|
-
}
|
|
214
|
-
/** @inheritDoc */
|
|
215
|
-
async dispose() {
|
|
216
|
-
this._entries.clear();
|
|
217
|
-
}
|
|
218
|
-
/** Current number of entries held by the LRU. */
|
|
219
|
-
get size() {
|
|
220
|
-
return this._entries.size;
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
// src/utils.ts
|
|
225
|
-
import { prettifyError, ZodError } from "zod";
|
|
226
|
-
|
|
227
|
-
// src/config.ts
|
|
228
|
-
import * as fs from "fs";
|
|
229
|
-
import { z } from "zod";
|
|
230
|
-
var PackageSchema = z.object({
|
|
231
|
-
name: z.string().min(1),
|
|
232
|
-
version: z.string().min(1),
|
|
233
|
-
description: z.string().min(1).optional(),
|
|
234
|
-
author: z.object({ name: z.string().min(1), email: z.string().optional() }).optional().or(z.string().min(1)).optional(),
|
|
235
|
-
license: z.string().min(1).optional(),
|
|
236
|
-
dependencies: z.record(z.string(), z.string()).optional()
|
|
237
|
-
});
|
|
238
|
-
var FALLBACK_PACKAGE = {
|
|
239
|
-
name: "act-fallback",
|
|
240
|
-
version: "0.0.0-fallback",
|
|
241
|
-
description: "Synthetic fallback \u2014 package.json could not be loaded"
|
|
242
|
-
};
|
|
243
|
-
var getPackage = () => {
|
|
244
|
-
try {
|
|
245
|
-
const raw = fs.readFileSync("package.json");
|
|
246
|
-
return JSON.parse(raw.toString());
|
|
247
|
-
} catch (err) {
|
|
248
|
-
pkgLoadError = err;
|
|
249
|
-
return FALLBACK_PACKAGE;
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
var pkgLoadError;
|
|
253
|
-
var BaseSchema = PackageSchema.extend({
|
|
254
|
-
env: z.enum(Environments),
|
|
255
|
-
logLevel: z.enum(LogLevels),
|
|
256
|
-
logSingleLine: z.boolean(),
|
|
257
|
-
sleepMs: z.number().int().min(0).max(5e3)
|
|
258
|
-
});
|
|
259
|
-
var { NODE_ENV, LOG_LEVEL, LOG_SINGLE_LINE, SLEEP_MS } = process.env;
|
|
260
|
-
var env = NODE_ENV || "development";
|
|
261
|
-
var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "fatal" : NODE_ENV === "production" ? "info" : "trace");
|
|
262
|
-
var logSingleLine = (LOG_SINGLE_LINE || "true") === "true";
|
|
263
|
-
var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100", 10);
|
|
264
|
-
var pkg = getPackage();
|
|
265
|
-
var _validated;
|
|
266
|
-
var config = () => {
|
|
267
|
-
if (!_validated) {
|
|
268
|
-
_validated = extend(
|
|
269
|
-
{ ...pkg, env, logLevel, logSingleLine, sleepMs },
|
|
270
|
-
BaseSchema
|
|
271
|
-
);
|
|
272
|
-
if (pkgLoadError) {
|
|
273
|
-
const msg = pkgLoadError instanceof Error ? pkgLoadError.message : typeof pkgLoadError === "string" ? pkgLoadError : "unknown error";
|
|
274
|
-
log().warn(
|
|
275
|
-
`[act] Could not read package.json (${msg}); using synthetic name="${FALLBACK_PACKAGE.name}" version="${FALLBACK_PACKAGE.version}".`
|
|
276
|
-
);
|
|
277
|
-
pkgLoadError = void 0;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return _validated;
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
// src/utils.ts
|
|
284
|
-
var validate = (target, payload, schema) => {
|
|
285
|
-
try {
|
|
286
|
-
return schema ? schema.parse(payload) : payload;
|
|
287
|
-
} catch (error) {
|
|
288
|
-
if (error instanceof ZodError) {
|
|
289
|
-
throw new ValidationError(target, payload, prettifyError(error));
|
|
290
|
-
}
|
|
291
|
-
throw new ValidationError(target, payload, error);
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
var extend = (source, schema, target) => {
|
|
295
|
-
const value = validate("config", source, schema);
|
|
296
|
-
return { ...target, ...value };
|
|
297
|
-
};
|
|
298
|
-
async function sleep(ms) {
|
|
299
|
-
return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// src/adapters/in-memory-store.ts
|
|
303
|
-
var InMemoryStream = class {
|
|
304
|
-
constructor(stream, source, priority = 0) {
|
|
305
|
-
this.stream = stream;
|
|
306
|
-
this.source = source;
|
|
307
|
-
this._priority = priority;
|
|
308
|
-
}
|
|
309
|
-
_at = -1;
|
|
310
|
-
_retry = -1;
|
|
311
|
-
_blocked = false;
|
|
312
|
-
_error = "";
|
|
313
|
-
_leased_by = void 0;
|
|
314
|
-
_leased_until = void 0;
|
|
315
|
-
_priority = 0;
|
|
316
|
-
get priority() {
|
|
317
|
-
return this._priority;
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Bump the priority via {@link subscribe}: keeps the maximum across
|
|
321
|
-
* reactions so the highest-priority registrant wins.
|
|
322
|
-
*/
|
|
323
|
-
bumpPriority(priority) {
|
|
324
|
-
if (priority > this._priority) this._priority = priority;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Set the priority outright via {@link prioritize}: operator
|
|
328
|
-
* runtime override that ignores the build-time `max()` invariant.
|
|
329
|
-
*/
|
|
330
|
-
setPriority(priority) {
|
|
331
|
-
this._priority = priority;
|
|
332
|
-
}
|
|
333
|
-
get is_available() {
|
|
334
|
-
return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date());
|
|
335
|
-
}
|
|
336
|
-
get at() {
|
|
337
|
-
return this._at;
|
|
338
|
-
}
|
|
339
|
-
get retry() {
|
|
340
|
-
return this._retry;
|
|
341
|
-
}
|
|
342
|
-
get blocked() {
|
|
343
|
-
return this._blocked;
|
|
344
|
-
}
|
|
345
|
-
get error() {
|
|
346
|
-
return this._error;
|
|
347
|
-
}
|
|
348
|
-
get leased_by() {
|
|
349
|
-
return this._leased_by;
|
|
350
|
-
}
|
|
351
|
-
get leased_until() {
|
|
352
|
-
return this._leased_until;
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Attempt to lease this stream for processing.
|
|
356
|
-
* @param lease - The lease request.
|
|
357
|
-
* @param millis - Lease duration in milliseconds.
|
|
358
|
-
* @returns The granted lease or undefined if blocked.
|
|
359
|
-
*/
|
|
360
|
-
lease(lease, millis) {
|
|
361
|
-
if (millis > 0) {
|
|
362
|
-
this._leased_by = lease.by;
|
|
363
|
-
this._leased_until = new Date(Date.now() + millis);
|
|
364
|
-
}
|
|
365
|
-
this._retry = this._retry + 1;
|
|
366
|
-
return {
|
|
367
|
-
stream: this.stream,
|
|
368
|
-
source: this.source,
|
|
369
|
-
at: lease.at,
|
|
370
|
-
by: lease.by,
|
|
371
|
-
retry: this._retry,
|
|
372
|
-
lagging: lease.lagging
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Acknowledge completion of processing for this stream.
|
|
377
|
-
* @param lease - The lease request.
|
|
378
|
-
*/
|
|
379
|
-
ack(lease) {
|
|
380
|
-
if (this._leased_by === lease.by) {
|
|
381
|
-
this._leased_by = void 0;
|
|
382
|
-
this._leased_until = void 0;
|
|
383
|
-
this._at = lease.at;
|
|
384
|
-
this._retry = -1;
|
|
385
|
-
return {
|
|
386
|
-
stream: this.stream,
|
|
387
|
-
source: this.source,
|
|
388
|
-
at: this._at,
|
|
389
|
-
by: lease.by,
|
|
390
|
-
retry: this._retry,
|
|
391
|
-
lagging: lease.lagging
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
397
|
-
* @param lease - The lease request.
|
|
398
|
-
* @param error Blocked error message.
|
|
399
|
-
*/
|
|
400
|
-
block(lease, error) {
|
|
401
|
-
if (this._leased_by === lease.by) {
|
|
402
|
-
this._blocked = true;
|
|
403
|
-
this._error = error;
|
|
404
|
-
return {
|
|
405
|
-
stream: this.stream,
|
|
406
|
-
source: this.source,
|
|
407
|
-
at: this._at,
|
|
408
|
-
by: this._leased_by,
|
|
409
|
-
retry: this._retry,
|
|
410
|
-
error: this._error,
|
|
411
|
-
lagging: lease.lagging
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Reset this stream's watermark and state for replay. The retry counter
|
|
417
|
-
* resets to -1 to match the constructor + ack() invariant ("released
|
|
418
|
-
* stream"); the next claim() bumps it to 0 (first attempt).
|
|
419
|
-
*/
|
|
420
|
-
reset() {
|
|
421
|
-
this._at = -1;
|
|
422
|
-
this._retry = -1;
|
|
423
|
-
this._blocked = false;
|
|
424
|
-
this._error = "";
|
|
425
|
-
this._leased_by = void 0;
|
|
426
|
-
this._leased_until = void 0;
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
var InMemoryStore = class {
|
|
430
|
-
// stored events
|
|
431
|
-
_events = [];
|
|
432
|
-
// stored stream positions and other metadata
|
|
433
|
-
_streams = /* @__PURE__ */ new Map();
|
|
434
|
-
// last committed version per stream — O(1) replacement for filter-on-commit
|
|
435
|
-
_streamVersions = /* @__PURE__ */ new Map();
|
|
436
|
-
// max non-snapshot event id per stream — drives the source-pattern probe in claim()
|
|
437
|
-
// without scanning the full event log.
|
|
438
|
-
_maxEventIdByStream = /* @__PURE__ */ new Map();
|
|
439
|
-
// global max non-snapshot event id — fast pre-check for source-less streams in claim()
|
|
440
|
-
_maxNonSnapEventId = -1;
|
|
441
|
-
_resetIndexes() {
|
|
442
|
-
this._events.length = 0;
|
|
443
|
-
this._streamVersions.clear();
|
|
444
|
-
this._maxEventIdByStream.clear();
|
|
445
|
-
this._maxNonSnapEventId = -1;
|
|
446
|
-
}
|
|
447
|
-
/**
|
|
448
|
-
* Dispose of the store and clear all events.
|
|
449
|
-
* @returns Promise that resolves when disposal is complete.
|
|
450
|
-
*/
|
|
451
|
-
async dispose() {
|
|
452
|
-
await sleep();
|
|
453
|
-
this._resetIndexes();
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Seed the store with initial data (no-op for in-memory).
|
|
457
|
-
* @returns Promise that resolves when seeding is complete.
|
|
458
|
-
*/
|
|
459
|
-
async seed() {
|
|
460
|
-
await sleep();
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Drop all data from the store.
|
|
464
|
-
* @returns Promise that resolves when the store is cleared.
|
|
465
|
-
*/
|
|
466
|
-
async drop() {
|
|
467
|
-
await sleep();
|
|
468
|
-
this._resetIndexes();
|
|
469
|
-
this._streams = /* @__PURE__ */ new Map();
|
|
470
|
-
}
|
|
471
|
-
in_query(query, e) {
|
|
472
|
-
if (query.stream) {
|
|
473
|
-
if (query.stream_exact) {
|
|
474
|
-
if (e.stream !== query.stream) return false;
|
|
475
|
-
} else if (!RegExp(`^${query.stream}$`).test(e.stream)) return false;
|
|
476
|
-
}
|
|
477
|
-
if (query.names && !query.names.includes(e.name)) return false;
|
|
478
|
-
if (query.correlation && e.meta?.correlation !== query.correlation)
|
|
479
|
-
return false;
|
|
480
|
-
if (e.name === SNAP_EVENT && !query.with_snaps) return false;
|
|
481
|
-
return true;
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Query events in the store, optionally filtered by query options.
|
|
485
|
-
* @param callback - Function to call for each event.
|
|
486
|
-
* @param query - Optional query options.
|
|
487
|
-
* @returns The number of events processed.
|
|
488
|
-
*/
|
|
489
|
-
async query(callback, query) {
|
|
490
|
-
await sleep();
|
|
491
|
-
let count = 0;
|
|
492
|
-
if (query?.backward) {
|
|
493
|
-
let i = (query?.before || this._events.length) - 1;
|
|
494
|
-
while (i >= 0) {
|
|
495
|
-
const e = this._events[i--];
|
|
496
|
-
if (query && !this.in_query(query, e)) continue;
|
|
497
|
-
if (query?.created_before && e.created >= query.created_before)
|
|
498
|
-
continue;
|
|
499
|
-
if (query.after && e.id <= query.after) break;
|
|
500
|
-
if (query.created_after && e.created <= query.created_after) break;
|
|
501
|
-
callback(e);
|
|
502
|
-
count++;
|
|
503
|
-
if (query?.limit && count >= query.limit) break;
|
|
504
|
-
}
|
|
505
|
-
} else {
|
|
506
|
-
let i = (query?.after ?? -1) + 1;
|
|
507
|
-
while (i < this._events.length) {
|
|
508
|
-
const e = this._events[i++];
|
|
509
|
-
if (query && !this.in_query(query, e)) continue;
|
|
510
|
-
if (query?.created_after && e.created <= query.created_after) continue;
|
|
511
|
-
if (query?.before && e.id >= query.before) break;
|
|
512
|
-
if (query?.created_before && e.created >= query.created_before) break;
|
|
513
|
-
callback(e);
|
|
514
|
-
count++;
|
|
515
|
-
if (query?.limit && count >= query.limit) break;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return count;
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Commit one or more events to a stream.
|
|
522
|
-
* @param stream - The stream name.
|
|
523
|
-
* @param msgs - The events/messages to commit.
|
|
524
|
-
* @param meta - Event metadata.
|
|
525
|
-
* @param expectedVersion - Optional optimistic concurrency check.
|
|
526
|
-
* @returns The committed events with metadata.
|
|
527
|
-
* @throws ConcurrencyError if expectedVersion does not match.
|
|
528
|
-
*/
|
|
529
|
-
async commit(stream, msgs, meta, expectedVersion) {
|
|
530
|
-
await sleep();
|
|
531
|
-
const currentVersion = this._streamVersions.get(stream) ?? -1;
|
|
532
|
-
if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
|
|
533
|
-
throw new ConcurrencyError(
|
|
534
|
-
stream,
|
|
535
|
-
currentVersion,
|
|
536
|
-
msgs,
|
|
537
|
-
expectedVersion
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
let version = currentVersion + 1;
|
|
541
|
-
let lastNonSnapId = -1;
|
|
542
|
-
const committed = msgs.map(({ name, data }) => {
|
|
543
|
-
const c = {
|
|
544
|
-
id: this._events.length,
|
|
545
|
-
stream,
|
|
546
|
-
version,
|
|
547
|
-
created: /* @__PURE__ */ new Date(),
|
|
548
|
-
name,
|
|
549
|
-
data,
|
|
550
|
-
meta
|
|
551
|
-
};
|
|
552
|
-
this._events.push(c);
|
|
553
|
-
if (name !== SNAP_EVENT) lastNonSnapId = c.id;
|
|
554
|
-
version++;
|
|
555
|
-
return c;
|
|
556
|
-
});
|
|
557
|
-
this._streamVersions.set(stream, version - 1);
|
|
558
|
-
if (lastNonSnapId >= 0) {
|
|
559
|
-
this._maxEventIdByStream.set(stream, lastNonSnapId);
|
|
560
|
-
this._maxNonSnapEventId = lastNonSnapId;
|
|
561
|
-
}
|
|
562
|
-
return committed;
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Atomically discovers and leases streams for processing.
|
|
566
|
-
* Fuses poll + lease into a single operation.
|
|
567
|
-
* @param lagging - Max streams from lagging frontier.
|
|
568
|
-
* @param leading - Max streams from leading frontier.
|
|
569
|
-
* @param by - Lease holder identifier.
|
|
570
|
-
* @param millis - Lease duration in milliseconds.
|
|
571
|
-
* @returns Granted leases.
|
|
572
|
-
*/
|
|
573
|
-
async claim(lagging, leading, by, millis) {
|
|
574
|
-
await sleep();
|
|
575
|
-
const sourceRegex = /* @__PURE__ */ new Map();
|
|
576
|
-
const getRegex = (source) => {
|
|
577
|
-
let re = sourceRegex.get(source);
|
|
578
|
-
if (!re) {
|
|
579
|
-
re = new RegExp(source);
|
|
580
|
-
sourceRegex.set(source, re);
|
|
581
|
-
}
|
|
582
|
-
return re;
|
|
583
|
-
};
|
|
584
|
-
const hasWork = (s) => {
|
|
585
|
-
if (s.at < 0) return true;
|
|
586
|
-
if (!s.source) return s.at < this._maxNonSnapEventId;
|
|
587
|
-
const re = getRegex(s.source);
|
|
588
|
-
for (const [streamName, maxId] of this._maxEventIdByStream) {
|
|
589
|
-
if (maxId > s.at && re.test(streamName)) return true;
|
|
590
|
-
}
|
|
591
|
-
return false;
|
|
592
|
-
};
|
|
593
|
-
const available = [...this._streams.values()].filter(
|
|
594
|
-
(s) => s.is_available && hasWork(s)
|
|
595
|
-
);
|
|
596
|
-
const lag = available.sort((a, b) => b.priority - a.priority || a.at - b.at).slice(0, lagging).map((s) => ({
|
|
597
|
-
stream: s.stream,
|
|
598
|
-
source: s.source,
|
|
599
|
-
at: s.at,
|
|
600
|
-
lagging: true
|
|
601
|
-
}));
|
|
602
|
-
const lead = available.sort((a, b) => b.at - a.at).slice(0, leading).map((s) => ({
|
|
603
|
-
stream: s.stream,
|
|
604
|
-
source: s.source,
|
|
605
|
-
at: s.at,
|
|
606
|
-
lagging: false
|
|
607
|
-
}));
|
|
608
|
-
const seen = /* @__PURE__ */ new Set();
|
|
609
|
-
const combined = [...lag, ...lead].filter((p) => {
|
|
610
|
-
if (seen.has(p.stream)) return false;
|
|
611
|
-
seen.add(p.stream);
|
|
612
|
-
return true;
|
|
613
|
-
});
|
|
614
|
-
return combined.map(
|
|
615
|
-
(p) => this._streams.get(p.stream)?.lease({ ...p, by, retry: 0 }, millis)
|
|
616
|
-
).filter((l) => !!l);
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Registers streams for event processing. When the same stream is
|
|
620
|
-
* resubscribed with a different priority, the **maximum** wins — so
|
|
621
|
-
* the highest-priority registered reaction sets the scheduling lane.
|
|
622
|
-
* Use {@link prioritize} for operator runtime overrides.
|
|
623
|
-
*
|
|
624
|
-
* @param streams - Streams to register with optional source + priority.
|
|
625
|
-
* @returns subscribed count and current max watermark.
|
|
626
|
-
*/
|
|
627
|
-
async subscribe(streams) {
|
|
628
|
-
await sleep();
|
|
629
|
-
let subscribed = 0;
|
|
630
|
-
for (const { stream, source, priority = 0 } of streams) {
|
|
631
|
-
const existing = this._streams.get(stream);
|
|
632
|
-
if (existing) {
|
|
633
|
-
existing.bumpPriority(priority);
|
|
634
|
-
} else {
|
|
635
|
-
this._streams.set(stream, new InMemoryStream(stream, source, priority));
|
|
636
|
-
subscribed++;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
let watermark = -1;
|
|
640
|
-
for (const s of this._streams.values()) {
|
|
641
|
-
if (s.at > watermark) watermark = s.at;
|
|
642
|
-
}
|
|
643
|
-
return { subscribed, watermark };
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Acknowledge completion of processing for leased streams.
|
|
647
|
-
* @param leases - Leases to acknowledge, including last processed watermark and lease holder.
|
|
648
|
-
*/
|
|
649
|
-
async ack(leases) {
|
|
650
|
-
await sleep();
|
|
651
|
-
return leases.map((l) => this._streams.get(l.stream)?.ack(l)).filter((l) => !!l);
|
|
652
|
-
}
|
|
653
|
-
/**
|
|
654
|
-
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
655
|
-
* @param leases - Leases to block, including lease holder and last error message.
|
|
656
|
-
* @returns Blocked leases.
|
|
657
|
-
*/
|
|
658
|
-
async block(leases) {
|
|
659
|
-
await sleep();
|
|
660
|
-
return leases.map((l) => this._streams.get(l.stream)?.block(l, l.error)).filter((l) => !!l);
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Reset watermarks for the given streams to -1, clearing retry, blocked,
|
|
664
|
-
* error, and lease state so they can be replayed from the beginning.
|
|
665
|
-
* @param streams - Stream names to reset.
|
|
666
|
-
* @returns Count of streams that were actually reset.
|
|
667
|
-
*/
|
|
668
|
-
async reset(streams) {
|
|
669
|
-
await sleep();
|
|
670
|
-
let count = 0;
|
|
671
|
-
for (const name of streams) {
|
|
672
|
-
const s = this._streams.get(name);
|
|
673
|
-
if (s) {
|
|
674
|
-
s.reset();
|
|
675
|
-
count++;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
return count;
|
|
679
|
-
}
|
|
680
|
-
/**
|
|
681
|
-
* Bulk-update priority of streams matching `filter`. Mirrors
|
|
682
|
-
* {@link query_streams}'s filter semantics — see {@link Store.prioritize}.
|
|
683
|
-
* Unlike {@link subscribe} (which keeps `max()` of registered
|
|
684
|
-
* priorities), this sets the priority outright — operator override
|
|
685
|
-
* for the build-time scheduling policy.
|
|
686
|
-
*
|
|
687
|
-
* @returns Count of streams whose priority changed.
|
|
688
|
-
*/
|
|
689
|
-
async prioritize(filter, priority) {
|
|
690
|
-
await sleep();
|
|
691
|
-
const streamRe = filter.stream && !filter.stream_exact ? new RegExp(`^${filter.stream}$`) : void 0;
|
|
692
|
-
const sourceRe = filter.source && !filter.source_exact ? new RegExp(`^${filter.source}$`) : void 0;
|
|
693
|
-
let count = 0;
|
|
694
|
-
for (const s of this._streams.values()) {
|
|
695
|
-
if (filter.stream !== void 0) {
|
|
696
|
-
if (filter.stream_exact ? s.stream !== filter.stream : !streamRe.test(s.stream))
|
|
697
|
-
continue;
|
|
698
|
-
}
|
|
699
|
-
if (filter.source !== void 0) {
|
|
700
|
-
if (s.source === void 0) continue;
|
|
701
|
-
if (filter.source_exact ? s.source !== filter.source : !sourceRe.test(s.source))
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
if (filter.blocked !== void 0 && s.blocked !== filter.blocked)
|
|
705
|
-
continue;
|
|
706
|
-
if (s.priority !== priority) {
|
|
707
|
-
s.setPriority(priority);
|
|
708
|
-
count++;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
return count;
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Streams registered subscription positions to the callback, ordered by
|
|
715
|
-
* stream name. Returns the highest event id in the store and the count
|
|
716
|
-
* of positions emitted.
|
|
717
|
-
*/
|
|
718
|
-
async query_streams(callback, query) {
|
|
719
|
-
await sleep();
|
|
720
|
-
const limit = query?.limit ?? 100;
|
|
721
|
-
const after = query?.after;
|
|
722
|
-
const blocked = query?.blocked;
|
|
723
|
-
const streamRe = query?.stream && !query.stream_exact ? new RegExp(`^${query.stream}$`) : void 0;
|
|
724
|
-
const sourceRe = query?.source && !query.source_exact ? new RegExp(`^${query.source}$`) : void 0;
|
|
725
|
-
const sorted = [...this._streams.values()].sort(
|
|
726
|
-
(a, b) => a.stream.localeCompare(b.stream)
|
|
727
|
-
);
|
|
728
|
-
let count = 0;
|
|
729
|
-
for (const s of sorted) {
|
|
730
|
-
if (after !== void 0 && s.stream <= after) continue;
|
|
731
|
-
if (query?.stream !== void 0) {
|
|
732
|
-
if (query.stream_exact ? s.stream !== query.stream : !streamRe.test(s.stream))
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
|
-
if (query?.source !== void 0) {
|
|
736
|
-
if (s.source === void 0) continue;
|
|
737
|
-
if (query.source_exact ? s.source !== query.source : !sourceRe.test(s.source))
|
|
738
|
-
continue;
|
|
739
|
-
}
|
|
740
|
-
if (blocked !== void 0 && s.blocked !== blocked) continue;
|
|
741
|
-
callback({
|
|
742
|
-
stream: s.stream,
|
|
743
|
-
source: s.source,
|
|
744
|
-
at: s.at,
|
|
745
|
-
retry: s.retry,
|
|
746
|
-
blocked: s.blocked,
|
|
747
|
-
error: s.error,
|
|
748
|
-
priority: s.priority,
|
|
749
|
-
leased_by: s.leased_by,
|
|
750
|
-
leased_until: s.leased_until
|
|
751
|
-
});
|
|
752
|
-
count++;
|
|
753
|
-
if (count >= limit) break;
|
|
754
|
-
}
|
|
755
|
-
return { maxEventId: this._events.length - 1, count };
|
|
756
|
-
}
|
|
757
|
-
/**
|
|
758
|
-
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|
|
759
|
-
* @param targets - Streams to truncate with optional snapshot state and meta.
|
|
760
|
-
* @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.
|
|
761
|
-
*/
|
|
762
|
-
async truncate(targets) {
|
|
763
|
-
await sleep();
|
|
764
|
-
const deletedCounts = /* @__PURE__ */ new Map();
|
|
765
|
-
const streamSet = new Set(targets.map((t) => t.stream));
|
|
766
|
-
for (const e of this._events) {
|
|
767
|
-
if (streamSet.has(e.stream)) {
|
|
768
|
-
deletedCounts.set(e.stream, (deletedCounts.get(e.stream) ?? 0) + 1);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
this._events = this._events.filter((e) => !streamSet.has(e.stream));
|
|
772
|
-
for (const stream of streamSet) {
|
|
773
|
-
this._streams.delete(stream);
|
|
774
|
-
this._streamVersions.delete(stream);
|
|
775
|
-
this._maxEventIdByStream.delete(stream);
|
|
776
|
-
}
|
|
777
|
-
const result = /* @__PURE__ */ new Map();
|
|
778
|
-
for (const { stream, snapshot, meta } of targets) {
|
|
779
|
-
const event = {
|
|
780
|
-
id: this._events.length,
|
|
781
|
-
stream,
|
|
782
|
-
version: 0,
|
|
783
|
-
created: /* @__PURE__ */ new Date(),
|
|
784
|
-
name: snapshot !== void 0 ? SNAP_EVENT : TOMBSTONE_EVENT,
|
|
785
|
-
data: snapshot ?? {},
|
|
786
|
-
meta: meta ?? { correlation: "", causation: {} }
|
|
787
|
-
};
|
|
788
|
-
this._events.push(event);
|
|
789
|
-
this._streamVersions.set(stream, 0);
|
|
790
|
-
if (event.name !== SNAP_EVENT) {
|
|
791
|
-
this._maxEventIdByStream.set(stream, event.id);
|
|
792
|
-
}
|
|
793
|
-
result.set(stream, {
|
|
794
|
-
deleted: deletedCounts.get(stream) ?? 0,
|
|
795
|
-
committed: event
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
let max = -1;
|
|
799
|
-
for (const id of this._maxEventIdByStream.values()) if (id > max) max = id;
|
|
800
|
-
this._maxNonSnapEventId = max;
|
|
801
|
-
return result;
|
|
802
|
-
}
|
|
803
|
-
};
|
|
804
|
-
|
|
805
|
-
// src/ports.ts
|
|
806
|
-
var ExitCodes = ["ERROR", "EXIT"];
|
|
807
|
-
var adapters = /* @__PURE__ */ new Map();
|
|
808
|
-
function port(injector) {
|
|
809
|
-
return (adapter) => {
|
|
810
|
-
if (!adapters.has(injector.name)) {
|
|
811
|
-
const injected = injector(adapter);
|
|
812
|
-
adapters.set(injector.name, injected);
|
|
813
|
-
log().info(`[act] + ${injector.name}:${injected.constructor.name}`);
|
|
814
|
-
}
|
|
815
|
-
return adapters.get(injector.name);
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
var log = port(function log2(adapter) {
|
|
819
|
-
const cfg = config();
|
|
820
|
-
return adapter || new ConsoleLogger({
|
|
821
|
-
level: cfg.logLevel,
|
|
822
|
-
pretty: cfg.env !== "production"
|
|
823
|
-
});
|
|
824
|
-
});
|
|
825
|
-
var store = port(function store2(adapter) {
|
|
826
|
-
return adapter || new InMemoryStore();
|
|
827
|
-
});
|
|
828
|
-
var cache = port(function cache2(adapter) {
|
|
829
|
-
return adapter || new InMemoryCache();
|
|
830
|
-
});
|
|
831
|
-
var disposers = [];
|
|
832
|
-
async function disposeAndExit(code = "EXIT") {
|
|
833
|
-
if (code === "ERROR" && config().env === "production") {
|
|
834
|
-
log().warn(
|
|
835
|
-
"disposeAndExit('ERROR') ignored in production \u2014 process kept alive"
|
|
836
|
-
);
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
for (const disposer of [...disposers].reverse()) {
|
|
840
|
-
await disposer();
|
|
841
|
-
}
|
|
842
|
-
for (const adapter of [...adapters.values()].reverse()) {
|
|
843
|
-
await adapter.dispose();
|
|
844
|
-
log().info(`[act] - ${adapter.constructor.name}`);
|
|
845
|
-
}
|
|
846
|
-
adapters.clear();
|
|
847
|
-
config().env !== "test" && process.exit(code === "ERROR" ? 1 : 0);
|
|
848
|
-
}
|
|
849
|
-
function dispose(disposer) {
|
|
850
|
-
disposer && disposers.push(disposer);
|
|
851
|
-
return disposeAndExit;
|
|
852
|
-
}
|
|
853
|
-
var SNAP_EVENT = "__snapshot__";
|
|
854
|
-
var TOMBSTONE_EVENT = "__tombstone__";
|
|
38
|
+
import "./chunk-5WRI5ZAA.js";
|
|
855
39
|
|
|
856
40
|
// src/signals.ts
|
|
857
41
|
process.once("SIGINT", async (arg) => {
|
|
@@ -2031,6 +1215,7 @@ var Act = class {
|
|
|
2031
1215
|
this.registry = registry;
|
|
2032
1216
|
this._states = _states;
|
|
2033
1217
|
this._batch_handlers = batchHandlers;
|
|
1218
|
+
this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
|
|
2034
1219
|
this._es = buildEs(this._logger);
|
|
2035
1220
|
this._cd = buildDrain(this._logger);
|
|
2036
1221
|
this._handle = buildHandle({
|
|
@@ -2076,14 +1261,8 @@ var Act = class {
|
|
|
2076
1261
|
},
|
|
2077
1262
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
2078
1263
|
);
|
|
2079
|
-
this._notify_disposer = this._wireNotify();
|
|
2080
|
-
dispose(
|
|
2081
|
-
this._emitter.removeAllListeners();
|
|
2082
|
-
this.stop_correlations();
|
|
2083
|
-
this.stop_settling();
|
|
2084
|
-
const disposer = await this._notify_disposer;
|
|
2085
|
-
if (disposer) await disposer();
|
|
2086
|
-
});
|
|
1264
|
+
this._notify_disposer = this._wireNotify(options.scoped?.store ?? store());
|
|
1265
|
+
dispose(() => this.shutdown());
|
|
2087
1266
|
}
|
|
2088
1267
|
_emitter = new EventEmitter();
|
|
2089
1268
|
/** Event names with at least one registered reaction (computed at build time) */
|
|
@@ -2145,6 +1324,11 @@ var Act = class {
|
|
|
2145
1324
|
_event_to_state;
|
|
2146
1325
|
/** Logger resolved at construction time (after user port configuration) */
|
|
2147
1326
|
_logger = log();
|
|
1327
|
+
/** Wraps a public-method body so internal `store()`/`cache()` resolve to the
|
|
1328
|
+
* per-Act ports (ACT-501). No-op when the Act is unscoped — so the singleton
|
|
1329
|
+
* path keeps reading fresh `store()`/`cache()` per call, which matters for
|
|
1330
|
+
* tests that dispose and re-seed mid-suite. */
|
|
1331
|
+
_scoped;
|
|
2148
1332
|
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
2149
1333
|
* payload (it captures the triggering event for reactingTo auto-inject). */
|
|
2150
1334
|
_bound_do = this.do.bind(this);
|
|
@@ -2154,15 +1338,38 @@ var Act = class {
|
|
|
2154
1338
|
/** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
|
|
2155
1339
|
_handle;
|
|
2156
1340
|
_handle_batch;
|
|
1341
|
+
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
1342
|
+
_shutdown_promise;
|
|
1343
|
+
/**
|
|
1344
|
+
* Per-instance teardown: remove lifecycle listeners, stop the
|
|
1345
|
+
* correlation worker, cancel any pending settle cycle, and tear
|
|
1346
|
+
* down the cross-process notify subscription.
|
|
1347
|
+
*
|
|
1348
|
+
* Idempotent — repeated calls return the same promise. Registered
|
|
1349
|
+
* automatically with the global `dispose()` registry at construction,
|
|
1350
|
+
* so process-wide `dispose()()` covers it; test helpers (or operators
|
|
1351
|
+
* that mint short-lived Acts) call it explicitly for prompt cleanup.
|
|
1352
|
+
*/
|
|
1353
|
+
shutdown() {
|
|
1354
|
+
if (!this._shutdown_promise) {
|
|
1355
|
+
this._shutdown_promise = (async () => {
|
|
1356
|
+
this._emitter.removeAllListeners();
|
|
1357
|
+
this.stop_correlations();
|
|
1358
|
+
this.stop_settling();
|
|
1359
|
+
const disposer = await this._notify_disposer;
|
|
1360
|
+
if (disposer) await disposer();
|
|
1361
|
+
})();
|
|
1362
|
+
}
|
|
1363
|
+
return this._shutdown_promise;
|
|
1364
|
+
}
|
|
2157
1365
|
/**
|
|
2158
1366
|
* Subscribe to {@link Store.notify} when both the store and the
|
|
2159
1367
|
* registry support it. Returns the disposer (or `undefined` when no
|
|
2160
1368
|
* subscription was made). Errors during subscription are logged but
|
|
2161
1369
|
* never thrown — `notify` is a hint, not a contract.
|
|
2162
1370
|
*/
|
|
2163
|
-
async _wireNotify() {
|
|
1371
|
+
async _wireNotify(s) {
|
|
2164
1372
|
if (this._reactive_events.size === 0) return void 0;
|
|
2165
|
-
const s = store();
|
|
2166
1373
|
if (!s.notify) return void 0;
|
|
2167
1374
|
try {
|
|
2168
1375
|
return await s.notify((notification) => {
|
|
@@ -2266,35 +1473,39 @@ var Act = class {
|
|
|
2266
1473
|
* @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
|
|
2267
1474
|
*/
|
|
2268
1475
|
async do(action2, target, payload, reactingTo, skipValidation = false) {
|
|
2269
|
-
|
|
2270
|
-
this.
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
this.
|
|
2281
|
-
|
|
1476
|
+
return this._scoped(async () => {
|
|
1477
|
+
const snapshots = await this._es.action(
|
|
1478
|
+
this.registry.actions[action2],
|
|
1479
|
+
action2,
|
|
1480
|
+
target,
|
|
1481
|
+
payload,
|
|
1482
|
+
reactingTo,
|
|
1483
|
+
skipValidation
|
|
1484
|
+
);
|
|
1485
|
+
if (this._reactive_events.size > 0) {
|
|
1486
|
+
for (const snap2 of snapshots) {
|
|
1487
|
+
if (snap2.event?.name && this._reactive_events.has(snap2.event.name)) {
|
|
1488
|
+
this._drain.arm();
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
2282
1491
|
}
|
|
2283
1492
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
1493
|
+
this.emit("committed", snapshots);
|
|
1494
|
+
return snapshots;
|
|
1495
|
+
});
|
|
2287
1496
|
}
|
|
2288
1497
|
async load(stateOrName, stream, callback, asOf) {
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
1498
|
+
return this._scoped(async () => {
|
|
1499
|
+
let merged;
|
|
1500
|
+
if (typeof stateOrName === "string") {
|
|
1501
|
+
const found = this._states.get(stateOrName);
|
|
1502
|
+
if (!found) throw new Error(`State "${stateOrName}" not found`);
|
|
1503
|
+
merged = found;
|
|
1504
|
+
} else {
|
|
1505
|
+
merged = this._states.get(stateOrName.name) || stateOrName;
|
|
1506
|
+
}
|
|
1507
|
+
return await this._es.load(merged, stream, callback, asOf);
|
|
1508
|
+
});
|
|
2298
1509
|
}
|
|
2299
1510
|
/**
|
|
2300
1511
|
* Queries the event store for events matching a filter.
|
|
@@ -2343,14 +1554,16 @@ var Act = class {
|
|
|
2343
1554
|
* @see {@link query_array} for loading events into memory
|
|
2344
1555
|
*/
|
|
2345
1556
|
async query(query, callback) {
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
1557
|
+
return this._scoped(async () => {
|
|
1558
|
+
let first;
|
|
1559
|
+
let last;
|
|
1560
|
+
const count = await store().query((e) => {
|
|
1561
|
+
if (!first) first = e;
|
|
1562
|
+
last = e;
|
|
1563
|
+
callback?.(e);
|
|
1564
|
+
}, query);
|
|
1565
|
+
return { first, last, count };
|
|
1566
|
+
});
|
|
2354
1567
|
}
|
|
2355
1568
|
/**
|
|
2356
1569
|
* Queries the event store and returns all matching events in memory.
|
|
@@ -2379,9 +1592,11 @@ var Act = class {
|
|
|
2379
1592
|
* @see {@link query} for large result sets
|
|
2380
1593
|
*/
|
|
2381
1594
|
async query_array(query) {
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
1595
|
+
return this._scoped(async () => {
|
|
1596
|
+
const events = [];
|
|
1597
|
+
await store().query((e) => events.push(e), query);
|
|
1598
|
+
return events;
|
|
1599
|
+
});
|
|
2385
1600
|
}
|
|
2386
1601
|
/**
|
|
2387
1602
|
* Processes pending reactions by draining uncommitted events from the event store.
|
|
@@ -2421,7 +1636,7 @@ var Act = class {
|
|
|
2421
1636
|
* @see {@link start_correlations} for automatic correlation
|
|
2422
1637
|
*/
|
|
2423
1638
|
async drain(options = {}) {
|
|
2424
|
-
return this._drain.drain(options);
|
|
1639
|
+
return this._scoped(() => this._drain.drain(options));
|
|
2425
1640
|
}
|
|
2426
1641
|
/**
|
|
2427
1642
|
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
@@ -2469,7 +1684,7 @@ var Act = class {
|
|
|
2469
1684
|
* @see {@link stop_correlations} to stop automatic correlation
|
|
2470
1685
|
*/
|
|
2471
1686
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
2472
|
-
return this._correlate.correlate(query);
|
|
1687
|
+
return this._scoped(() => this._correlate.correlate(query));
|
|
2473
1688
|
}
|
|
2474
1689
|
/**
|
|
2475
1690
|
* Starts automatic periodic correlation worker for discovering new streams.
|
|
@@ -2590,9 +1805,11 @@ var Act = class {
|
|
|
2590
1805
|
* @see {@link settle} for the debounced full-catch-up loop
|
|
2591
1806
|
*/
|
|
2592
1807
|
async reset(streams) {
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
1808
|
+
return this._scoped(async () => {
|
|
1809
|
+
const count = await store().reset(streams);
|
|
1810
|
+
if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
|
|
1811
|
+
return count;
|
|
1812
|
+
});
|
|
2596
1813
|
}
|
|
2597
1814
|
/**
|
|
2598
1815
|
* Bulk-update scheduling priority for streams matching `filter`.
|
|
@@ -2633,7 +1850,7 @@ var Act = class {
|
|
|
2633
1850
|
* @see {@link claim} for how priority biases scheduling
|
|
2634
1851
|
*/
|
|
2635
1852
|
async prioritize(filter, priority) {
|
|
2636
|
-
return store().prioritize(filter, priority);
|
|
1853
|
+
return this._scoped(() => store().prioritize(filter, priority));
|
|
2637
1854
|
}
|
|
2638
1855
|
/**
|
|
2639
1856
|
* Close the books — guard, archive, truncate, and optionally restart streams.
|
|
@@ -2670,16 +1887,18 @@ var Act = class {
|
|
|
2670
1887
|
*/
|
|
2671
1888
|
async close(targets) {
|
|
2672
1889
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
1890
|
+
return this._scoped(async () => {
|
|
1891
|
+
await this.correlate({ limit: 1e3 });
|
|
1892
|
+
const result = await runCloseCycle(targets, {
|
|
1893
|
+
reactiveEventsSize: this._reactive_events.size,
|
|
1894
|
+
eventToState: this._event_to_state,
|
|
1895
|
+
load: this._es.load,
|
|
1896
|
+
tombstone: this._es.tombstone,
|
|
1897
|
+
logger: this._logger
|
|
1898
|
+
});
|
|
1899
|
+
this.emit("closed", result);
|
|
1900
|
+
return result;
|
|
2680
1901
|
});
|
|
2681
|
-
this.emit("closed", result);
|
|
2682
|
-
return result;
|
|
2683
1902
|
}
|
|
2684
1903
|
/**
|
|
2685
1904
|
* Debounced, non-blocking correlate→drain cycle.
|
|
@@ -2733,6 +1952,41 @@ function act() {
|
|
|
2733
1952
|
};
|
|
2734
1953
|
const pendingProjections = [];
|
|
2735
1954
|
const batchHandlers = /* @__PURE__ */ new Map();
|
|
1955
|
+
let _built = false;
|
|
1956
|
+
const finalizeDeprecations = () => {
|
|
1957
|
+
const deprecationSummary = [];
|
|
1958
|
+
for (const state2 of states.values()) {
|
|
1959
|
+
const eventNames = Object.keys(state2.events);
|
|
1960
|
+
const deprecated = deprecatedEventNames(eventNames);
|
|
1961
|
+
if (deprecated.size === 0) continue;
|
|
1962
|
+
state2._deprecated = deprecated;
|
|
1963
|
+
for (const name of deprecated) {
|
|
1964
|
+
const current = currentVersionOf(name, eventNames);
|
|
1965
|
+
deprecationSummary.push({
|
|
1966
|
+
stateName: state2.name,
|
|
1967
|
+
deprecated: name,
|
|
1968
|
+
current
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
for (const [actionName, handler] of Object.entries(state2.on)) {
|
|
1972
|
+
const staticTarget = handler?._staticEmit;
|
|
1973
|
+
if (staticTarget && deprecated.has(staticTarget)) {
|
|
1974
|
+
const current = currentVersionOf(staticTarget, eventNames);
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
`Action "${actionName}" in state "${state2.name}" emits deprecated event "${staticTarget}". A newer version exists: "${current}". Update the .emit() call to target the current version. The reducer (.patch) for "${staticTarget}" stays as-is \u2014 historical events still need it.`
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
if (deprecationSummary.length > 0) {
|
|
1982
|
+
const list = deprecationSummary.map(
|
|
1983
|
+
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.stateName}")`
|
|
1984
|
+
).join(", ");
|
|
1985
|
+
log().info(
|
|
1986
|
+
`Act registered ${deprecationSummary.length} deprecated event(s): ${list}. These are legacy versions kept for the read path. Consider truncating closed streams via app.close() when feasible to reduce historical event load. See docs/docs/architecture/event-schema-evolution.md.`
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
2736
1990
|
const builder = {
|
|
2737
1991
|
withState: (state2) => {
|
|
2738
1992
|
registerState(state2, states, registry.actions, registry.events);
|
|
@@ -2776,41 +2030,13 @@ function act() {
|
|
|
2776
2030
|
}
|
|
2777
2031
|
}),
|
|
2778
2032
|
build: (options) => {
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
const deprecationSummary = [];
|
|
2784
|
-
for (const state2 of states.values()) {
|
|
2785
|
-
const eventNames = Object.keys(state2.events);
|
|
2786
|
-
const deprecated = deprecatedEventNames(eventNames);
|
|
2787
|
-
if (deprecated.size === 0) continue;
|
|
2788
|
-
state2._deprecated = deprecated;
|
|
2789
|
-
for (const name of deprecated) {
|
|
2790
|
-
const current = currentVersionOf(name, eventNames);
|
|
2791
|
-
deprecationSummary.push({
|
|
2792
|
-
stateName: state2.name,
|
|
2793
|
-
deprecated: name,
|
|
2794
|
-
current
|
|
2795
|
-
});
|
|
2796
|
-
}
|
|
2797
|
-
for (const [actionName, handler] of Object.entries(state2.on)) {
|
|
2798
|
-
const staticTarget = handler?._staticEmit;
|
|
2799
|
-
if (staticTarget && deprecated.has(staticTarget)) {
|
|
2800
|
-
const current = currentVersionOf(staticTarget, eventNames);
|
|
2801
|
-
throw new Error(
|
|
2802
|
-
`Action "${actionName}" in state "${state2.name}" emits deprecated event "${staticTarget}". A newer version exists: "${current}". Update the .emit() call to target the current version. The reducer (.patch) for "${staticTarget}" stays as-is \u2014 historical events still need it.`
|
|
2803
|
-
);
|
|
2804
|
-
}
|
|
2033
|
+
if (!_built) {
|
|
2034
|
+
for (const proj of pendingProjections) {
|
|
2035
|
+
mergeProjection(proj, registry.events);
|
|
2036
|
+
registerBatchHandler(proj, batchHandlers);
|
|
2805
2037
|
}
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
const list = deprecationSummary.map(
|
|
2809
|
-
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.stateName}")`
|
|
2810
|
-
).join(", ");
|
|
2811
|
-
log().info(
|
|
2812
|
-
`Act registered ${deprecationSummary.length} deprecated event(s): ${list}. These are legacy versions kept for the read path. Consider truncating closed streams via app.close() when feasible to reduce historical event load. See docs/docs/architecture/event-schema-evolution.md.`
|
|
2813
|
-
);
|
|
2038
|
+
finalizeDeprecations();
|
|
2039
|
+
_built = true;
|
|
2814
2040
|
}
|
|
2815
2041
|
return new Act(
|
|
2816
2042
|
registry,
|
|
@@ -3053,6 +2279,7 @@ export {
|
|
|
3053
2279
|
log,
|
|
3054
2280
|
port,
|
|
3055
2281
|
projection,
|
|
2282
|
+
scoped,
|
|
3056
2283
|
sleep,
|
|
3057
2284
|
slice,
|
|
3058
2285
|
state,
|