@rotorsoft/act 0.38.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/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,854 +35,7 @@ import {
14
35
  ValidationError,
15
36
  ZodEmpty
16
37
  } from "./chunk-AGWZY6YT.js";
17
-
18
- // src/ports.ts
19
- import { AsyncLocalStorage } from "async_hooks";
20
-
21
- // src/adapters/console-logger.ts
22
- var LEVEL_VALUES = {
23
- fatal: 60,
24
- error: 50,
25
- warn: 40,
26
- info: 30,
27
- debug: 20,
28
- trace: 10
29
- };
30
- var LEVEL_COLORS = {
31
- fatal: "\x1B[41m\x1B[37m",
32
- // white on red bg
33
- error: "\x1B[31m",
34
- // red
35
- warn: "\x1B[33m",
36
- // yellow
37
- info: "\x1B[32m",
38
- // green
39
- debug: "\x1B[36m",
40
- // cyan
41
- trace: "\x1B[90m"
42
- // gray
43
- };
44
- var RESET = "\x1B[0m";
45
- var noop = () => {
46
- };
47
- var ConsoleLogger = class _ConsoleLogger {
48
- level;
49
- _pretty;
50
- fatal;
51
- error;
52
- warn;
53
- info;
54
- debug;
55
- trace;
56
- constructor(options = {}) {
57
- const {
58
- level = "info",
59
- pretty = process.env.NODE_ENV !== "production",
60
- bindings
61
- } = options;
62
- this._pretty = pretty;
63
- this.level = level;
64
- const threshold = LEVEL_VALUES[level] ?? 30;
65
- const write = pretty ? this._prettyWrite.bind(this, bindings) : this._jsonWrite.bind(this, bindings);
66
- this.fatal = write.bind(this, "fatal", 60);
67
- this.error = threshold <= 50 ? write.bind(this, "error", 50) : noop;
68
- this.warn = threshold <= 40 ? write.bind(this, "warn", 40) : noop;
69
- this.info = threshold <= 30 ? write.bind(this, "info", 30) : noop;
70
- this.debug = threshold <= 20 ? write.bind(this, "debug", 20) : noop;
71
- this.trace = threshold <= 10 ? write.bind(this, "trace", 10) : noop;
72
- }
73
- /** No-op — `console.log` has no resources to release. */
74
- async dispose() {
75
- }
76
- /** @inheritDoc */
77
- child(bindings) {
78
- return new _ConsoleLogger({
79
- level: this.level,
80
- pretty: this._pretty,
81
- bindings
82
- });
83
- }
84
- _jsonWrite(bindings, level, _num, objOrMsg, msg) {
85
- let obj;
86
- let message;
87
- if (typeof objOrMsg === "string") {
88
- message = objOrMsg;
89
- obj = {};
90
- } else if (objOrMsg !== null && typeof objOrMsg === "object") {
91
- message = msg;
92
- obj = { ...objOrMsg };
93
- } else {
94
- message = msg;
95
- obj = { value: objOrMsg };
96
- }
97
- const entry = Object.assign({ level, time: Date.now() }, bindings, obj);
98
- if (message) entry.msg = message;
99
- let line;
100
- try {
101
- line = JSON.stringify(entry);
102
- } catch {
103
- line = JSON.stringify({
104
- level,
105
- time: entry.time,
106
- msg: message ?? "[unserializable]",
107
- unserializable: true
108
- });
109
- }
110
- process.stdout.write(line + "\n");
111
- }
112
- _prettyWrite(bindings, level, _num, objOrMsg, msg) {
113
- const color = LEVEL_COLORS[level];
114
- const tag = `${color}${level.toUpperCase().padEnd(5)}${RESET}`;
115
- const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
116
- let message;
117
- let data;
118
- if (typeof objOrMsg === "string") {
119
- message = objOrMsg;
120
- } else {
121
- message = msg ?? "";
122
- if (objOrMsg !== void 0 && objOrMsg !== null) {
123
- try {
124
- data = JSON.stringify(objOrMsg);
125
- } catch {
126
- data = "[unserializable]";
127
- }
128
- }
129
- }
130
- const bindStr = bindings && Object.keys(bindings).length ? ` ${JSON.stringify(bindings)}` : "";
131
- const parts = [ts, tag, message, data, bindStr].filter(Boolean);
132
- process.stdout.write(parts.join(" ") + "\n");
133
- }
134
- };
135
-
136
- // src/lru-map.ts
137
- var LruMap = class {
138
- constructor(_maxSize) {
139
- this._maxSize = _maxSize;
140
- }
141
- _entries = /* @__PURE__ */ new Map();
142
- get(key) {
143
- const v = this._entries.get(key);
144
- if (v === void 0) return void 0;
145
- this._entries.delete(key);
146
- this._entries.set(key, v);
147
- return v;
148
- }
149
- has(key) {
150
- return this._entries.has(key);
151
- }
152
- set(key, value) {
153
- this._entries.delete(key);
154
- if (this._entries.size >= this._maxSize) {
155
- const oldest = this._entries.keys().next().value;
156
- this._entries.delete(oldest);
157
- }
158
- this._entries.set(key, value);
159
- }
160
- delete(key) {
161
- return this._entries.delete(key);
162
- }
163
- clear() {
164
- this._entries.clear();
165
- }
166
- get size() {
167
- return this._entries.size;
168
- }
169
- };
170
- var LruSet = class {
171
- _map;
172
- constructor(maxSize) {
173
- this._map = new LruMap(maxSize);
174
- }
175
- has(value) {
176
- return this._map.has(value);
177
- }
178
- add(value) {
179
- this._map.set(value, true);
180
- }
181
- delete(value) {
182
- return this._map.delete(value);
183
- }
184
- clear() {
185
- this._map.clear();
186
- }
187
- get size() {
188
- return this._map.size;
189
- }
190
- };
191
-
192
- // src/adapters/in-memory-cache.ts
193
- var InMemoryCache = class {
194
- // CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
195
- // any is bidirectionally compatible with the per-call TState binding, while
196
- // the public Cache interface still presents a typed surface to callers.
197
- _entries;
198
- constructor(options) {
199
- this._entries = new LruMap(options?.maxSize ?? 1e3);
200
- }
201
- /** @inheritDoc */
202
- async get(stream) {
203
- return this._entries.get(stream);
204
- }
205
- /** @inheritDoc */
206
- async set(stream, entry) {
207
- this._entries.set(stream, entry);
208
- }
209
- /** @inheritDoc */
210
- async invalidate(stream) {
211
- this._entries.delete(stream);
212
- }
213
- /** @inheritDoc */
214
- async clear() {
215
- this._entries.clear();
216
- }
217
- /** @inheritDoc */
218
- async dispose() {
219
- this._entries.clear();
220
- }
221
- /** Current number of entries held by the LRU. */
222
- get size() {
223
- return this._entries.size;
224
- }
225
- };
226
-
227
- // src/utils.ts
228
- import { prettifyError, ZodError } from "zod";
229
-
230
- // src/config.ts
231
- import * as fs from "fs";
232
- import { z } from "zod";
233
- var PackageSchema = z.object({
234
- name: z.string().min(1),
235
- version: z.string().min(1),
236
- description: z.string().min(1).optional(),
237
- author: z.object({ name: z.string().min(1), email: z.string().optional() }).optional().or(z.string().min(1)).optional(),
238
- license: z.string().min(1).optional(),
239
- dependencies: z.record(z.string(), z.string()).optional()
240
- });
241
- var FALLBACK_PACKAGE = {
242
- name: "act-fallback",
243
- version: "0.0.0-fallback",
244
- description: "Synthetic fallback \u2014 package.json could not be loaded"
245
- };
246
- var getPackage = () => {
247
- try {
248
- const raw = fs.readFileSync("package.json");
249
- return JSON.parse(raw.toString());
250
- } catch (err) {
251
- pkgLoadError = err;
252
- return FALLBACK_PACKAGE;
253
- }
254
- };
255
- var pkgLoadError;
256
- var BaseSchema = PackageSchema.extend({
257
- env: z.enum(Environments),
258
- logLevel: z.enum(LogLevels),
259
- logSingleLine: z.boolean(),
260
- sleepMs: z.number().int().min(0).max(5e3)
261
- });
262
- var { NODE_ENV, LOG_LEVEL, LOG_SINGLE_LINE, SLEEP_MS } = process.env;
263
- var env = NODE_ENV || "development";
264
- var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "fatal" : NODE_ENV === "production" ? "info" : "trace");
265
- var logSingleLine = (LOG_SINGLE_LINE || "true") === "true";
266
- var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100", 10);
267
- var pkg = getPackage();
268
- var _validated;
269
- var config = () => {
270
- if (!_validated) {
271
- _validated = extend(
272
- { ...pkg, env, logLevel, logSingleLine, sleepMs },
273
- BaseSchema
274
- );
275
- if (pkgLoadError) {
276
- const msg = pkgLoadError instanceof Error ? pkgLoadError.message : typeof pkgLoadError === "string" ? pkgLoadError : "unknown error";
277
- log().warn(
278
- `[act] Could not read package.json (${msg}); using synthetic name="${FALLBACK_PACKAGE.name}" version="${FALLBACK_PACKAGE.version}".`
279
- );
280
- pkgLoadError = void 0;
281
- }
282
- }
283
- return _validated;
284
- };
285
-
286
- // src/utils.ts
287
- var validate = (target, payload, schema) => {
288
- try {
289
- return schema ? schema.parse(payload) : payload;
290
- } catch (error) {
291
- if (error instanceof ZodError) {
292
- throw new ValidationError(target, payload, prettifyError(error));
293
- }
294
- throw new ValidationError(target, payload, error);
295
- }
296
- };
297
- var extend = (source, schema, target) => {
298
- const value = validate("config", source, schema);
299
- return { ...target, ...value };
300
- };
301
- async function sleep(ms) {
302
- return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
303
- }
304
-
305
- // src/adapters/in-memory-store.ts
306
- var InMemoryStream = class {
307
- constructor(stream, source, priority = 0) {
308
- this.stream = stream;
309
- this.source = source;
310
- this._priority = priority;
311
- }
312
- _at = -1;
313
- _retry = -1;
314
- _blocked = false;
315
- _error = "";
316
- _leased_by = void 0;
317
- _leased_until = void 0;
318
- _priority = 0;
319
- get priority() {
320
- return this._priority;
321
- }
322
- /**
323
- * Bump the priority via {@link subscribe}: keeps the maximum across
324
- * reactions so the highest-priority registrant wins.
325
- */
326
- bumpPriority(priority) {
327
- if (priority > this._priority) this._priority = priority;
328
- }
329
- /**
330
- * Set the priority outright via {@link prioritize}: operator
331
- * runtime override that ignores the build-time `max()` invariant.
332
- */
333
- setPriority(priority) {
334
- this._priority = priority;
335
- }
336
- get is_available() {
337
- return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date());
338
- }
339
- get at() {
340
- return this._at;
341
- }
342
- get retry() {
343
- return this._retry;
344
- }
345
- get blocked() {
346
- return this._blocked;
347
- }
348
- get error() {
349
- return this._error;
350
- }
351
- get leased_by() {
352
- return this._leased_by;
353
- }
354
- get leased_until() {
355
- return this._leased_until;
356
- }
357
- /**
358
- * Attempt to lease this stream for processing.
359
- * @param lease - The lease request.
360
- * @param millis - Lease duration in milliseconds.
361
- * @returns The granted lease or undefined if blocked.
362
- */
363
- lease(lease, millis) {
364
- if (millis > 0) {
365
- this._leased_by = lease.by;
366
- this._leased_until = new Date(Date.now() + millis);
367
- }
368
- this._retry = this._retry + 1;
369
- return {
370
- stream: this.stream,
371
- source: this.source,
372
- at: lease.at,
373
- by: lease.by,
374
- retry: this._retry,
375
- lagging: lease.lagging
376
- };
377
- }
378
- /**
379
- * Acknowledge completion of processing for this stream.
380
- * @param lease - The lease request.
381
- */
382
- ack(lease) {
383
- if (this._leased_by === lease.by) {
384
- this._leased_by = void 0;
385
- this._leased_until = void 0;
386
- this._at = lease.at;
387
- this._retry = -1;
388
- return {
389
- stream: this.stream,
390
- source: this.source,
391
- at: this._at,
392
- by: lease.by,
393
- retry: this._retry,
394
- lagging: lease.lagging
395
- };
396
- }
397
- }
398
- /**
399
- * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
400
- * @param lease - The lease request.
401
- * @param error Blocked error message.
402
- */
403
- block(lease, error) {
404
- if (this._leased_by === lease.by) {
405
- this._blocked = true;
406
- this._error = error;
407
- return {
408
- stream: this.stream,
409
- source: this.source,
410
- at: this._at,
411
- by: this._leased_by,
412
- retry: this._retry,
413
- error: this._error,
414
- lagging: lease.lagging
415
- };
416
- }
417
- }
418
- /**
419
- * Reset this stream's watermark and state for replay. The retry counter
420
- * resets to -1 to match the constructor + ack() invariant ("released
421
- * stream"); the next claim() bumps it to 0 (first attempt).
422
- */
423
- reset() {
424
- this._at = -1;
425
- this._retry = -1;
426
- this._blocked = false;
427
- this._error = "";
428
- this._leased_by = void 0;
429
- this._leased_until = void 0;
430
- }
431
- };
432
- var InMemoryStore = class {
433
- // stored events
434
- _events = [];
435
- // stored stream positions and other metadata
436
- _streams = /* @__PURE__ */ new Map();
437
- // last committed version per stream — O(1) replacement for filter-on-commit
438
- _streamVersions = /* @__PURE__ */ new Map();
439
- // max non-snapshot event id per stream — drives the source-pattern probe in claim()
440
- // without scanning the full event log.
441
- _maxEventIdByStream = /* @__PURE__ */ new Map();
442
- // global max non-snapshot event id — fast pre-check for source-less streams in claim()
443
- _maxNonSnapEventId = -1;
444
- _resetIndexes() {
445
- this._events.length = 0;
446
- this._streamVersions.clear();
447
- this._maxEventIdByStream.clear();
448
- this._maxNonSnapEventId = -1;
449
- }
450
- /**
451
- * Dispose of the store and clear all events.
452
- * @returns Promise that resolves when disposal is complete.
453
- */
454
- async dispose() {
455
- await sleep();
456
- this._resetIndexes();
457
- }
458
- /**
459
- * Seed the store with initial data (no-op for in-memory).
460
- * @returns Promise that resolves when seeding is complete.
461
- */
462
- async seed() {
463
- await sleep();
464
- }
465
- /**
466
- * Drop all data from the store.
467
- * @returns Promise that resolves when the store is cleared.
468
- */
469
- async drop() {
470
- await sleep();
471
- this._resetIndexes();
472
- this._streams = /* @__PURE__ */ new Map();
473
- }
474
- in_query(query, e) {
475
- if (query.stream) {
476
- if (query.stream_exact) {
477
- if (e.stream !== query.stream) return false;
478
- } else if (!RegExp(`^${query.stream}$`).test(e.stream)) return false;
479
- }
480
- if (query.names && !query.names.includes(e.name)) return false;
481
- if (query.correlation && e.meta?.correlation !== query.correlation)
482
- return false;
483
- if (e.name === SNAP_EVENT && !query.with_snaps) return false;
484
- return true;
485
- }
486
- /**
487
- * Query events in the store, optionally filtered by query options.
488
- * @param callback - Function to call for each event.
489
- * @param query - Optional query options.
490
- * @returns The number of events processed.
491
- */
492
- async query(callback, query) {
493
- await sleep();
494
- let count = 0;
495
- if (query?.backward) {
496
- let i = (query?.before || this._events.length) - 1;
497
- while (i >= 0) {
498
- const e = this._events[i--];
499
- if (query && !this.in_query(query, e)) continue;
500
- if (query?.created_before && e.created >= query.created_before)
501
- continue;
502
- if (query.after && e.id <= query.after) break;
503
- if (query.created_after && e.created <= query.created_after) break;
504
- callback(e);
505
- count++;
506
- if (query?.limit && count >= query.limit) break;
507
- }
508
- } else {
509
- let i = (query?.after ?? -1) + 1;
510
- while (i < this._events.length) {
511
- const e = this._events[i++];
512
- if (query && !this.in_query(query, e)) continue;
513
- if (query?.created_after && e.created <= query.created_after) continue;
514
- if (query?.before && e.id >= query.before) break;
515
- if (query?.created_before && e.created >= query.created_before) break;
516
- callback(e);
517
- count++;
518
- if (query?.limit && count >= query.limit) break;
519
- }
520
- }
521
- return count;
522
- }
523
- /**
524
- * Commit one or more events to a stream.
525
- * @param stream - The stream name.
526
- * @param msgs - The events/messages to commit.
527
- * @param meta - Event metadata.
528
- * @param expectedVersion - Optional optimistic concurrency check.
529
- * @returns The committed events with metadata.
530
- * @throws ConcurrencyError if expectedVersion does not match.
531
- */
532
- async commit(stream, msgs, meta, expectedVersion) {
533
- await sleep();
534
- const currentVersion = this._streamVersions.get(stream) ?? -1;
535
- if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
536
- throw new ConcurrencyError(
537
- stream,
538
- currentVersion,
539
- msgs,
540
- expectedVersion
541
- );
542
- }
543
- let version = currentVersion + 1;
544
- let lastNonSnapId = -1;
545
- const committed = msgs.map(({ name, data }) => {
546
- const c = {
547
- id: this._events.length,
548
- stream,
549
- version,
550
- created: /* @__PURE__ */ new Date(),
551
- name,
552
- data,
553
- meta
554
- };
555
- this._events.push(c);
556
- if (name !== SNAP_EVENT) lastNonSnapId = c.id;
557
- version++;
558
- return c;
559
- });
560
- this._streamVersions.set(stream, version - 1);
561
- if (lastNonSnapId >= 0) {
562
- this._maxEventIdByStream.set(stream, lastNonSnapId);
563
- this._maxNonSnapEventId = lastNonSnapId;
564
- }
565
- return committed;
566
- }
567
- /**
568
- * Atomically discovers and leases streams for processing.
569
- * Fuses poll + lease into a single operation.
570
- * @param lagging - Max streams from lagging frontier.
571
- * @param leading - Max streams from leading frontier.
572
- * @param by - Lease holder identifier.
573
- * @param millis - Lease duration in milliseconds.
574
- * @returns Granted leases.
575
- */
576
- async claim(lagging, leading, by, millis) {
577
- await sleep();
578
- const sourceRegex = /* @__PURE__ */ new Map();
579
- const getRegex = (source) => {
580
- let re = sourceRegex.get(source);
581
- if (!re) {
582
- re = new RegExp(source);
583
- sourceRegex.set(source, re);
584
- }
585
- return re;
586
- };
587
- const hasWork = (s) => {
588
- if (s.at < 0) return true;
589
- if (!s.source) return s.at < this._maxNonSnapEventId;
590
- const re = getRegex(s.source);
591
- for (const [streamName, maxId] of this._maxEventIdByStream) {
592
- if (maxId > s.at && re.test(streamName)) return true;
593
- }
594
- return false;
595
- };
596
- const available = [...this._streams.values()].filter(
597
- (s) => s.is_available && hasWork(s)
598
- );
599
- const lag = available.sort((a, b) => b.priority - a.priority || a.at - b.at).slice(0, lagging).map((s) => ({
600
- stream: s.stream,
601
- source: s.source,
602
- at: s.at,
603
- lagging: true
604
- }));
605
- const lead = available.sort((a, b) => b.at - a.at).slice(0, leading).map((s) => ({
606
- stream: s.stream,
607
- source: s.source,
608
- at: s.at,
609
- lagging: false
610
- }));
611
- const seen = /* @__PURE__ */ new Set();
612
- const combined = [...lag, ...lead].filter((p) => {
613
- if (seen.has(p.stream)) return false;
614
- seen.add(p.stream);
615
- return true;
616
- });
617
- return combined.map(
618
- (p) => this._streams.get(p.stream)?.lease({ ...p, by, retry: 0 }, millis)
619
- ).filter((l) => !!l);
620
- }
621
- /**
622
- * Registers streams for event processing. When the same stream is
623
- * resubscribed with a different priority, the **maximum** wins — so
624
- * the highest-priority registered reaction sets the scheduling lane.
625
- * Use {@link prioritize} for operator runtime overrides.
626
- *
627
- * @param streams - Streams to register with optional source + priority.
628
- * @returns subscribed count and current max watermark.
629
- */
630
- async subscribe(streams) {
631
- await sleep();
632
- let subscribed = 0;
633
- for (const { stream, source, priority = 0 } of streams) {
634
- const existing = this._streams.get(stream);
635
- if (existing) {
636
- existing.bumpPriority(priority);
637
- } else {
638
- this._streams.set(stream, new InMemoryStream(stream, source, priority));
639
- subscribed++;
640
- }
641
- }
642
- let watermark = -1;
643
- for (const s of this._streams.values()) {
644
- if (s.at > watermark) watermark = s.at;
645
- }
646
- return { subscribed, watermark };
647
- }
648
- /**
649
- * Acknowledge completion of processing for leased streams.
650
- * @param leases - Leases to acknowledge, including last processed watermark and lease holder.
651
- */
652
- async ack(leases) {
653
- await sleep();
654
- return leases.map((l) => this._streams.get(l.stream)?.ack(l)).filter((l) => !!l);
655
- }
656
- /**
657
- * Block a stream for processing after failing to process and reaching max retries with blocking enabled.
658
- * @param leases - Leases to block, including lease holder and last error message.
659
- * @returns Blocked leases.
660
- */
661
- async block(leases) {
662
- await sleep();
663
- return leases.map((l) => this._streams.get(l.stream)?.block(l, l.error)).filter((l) => !!l);
664
- }
665
- /**
666
- * Reset watermarks for the given streams to -1, clearing retry, blocked,
667
- * error, and lease state so they can be replayed from the beginning.
668
- * @param streams - Stream names to reset.
669
- * @returns Count of streams that were actually reset.
670
- */
671
- async reset(streams) {
672
- await sleep();
673
- let count = 0;
674
- for (const name of streams) {
675
- const s = this._streams.get(name);
676
- if (s) {
677
- s.reset();
678
- count++;
679
- }
680
- }
681
- return count;
682
- }
683
- /**
684
- * Bulk-update priority of streams matching `filter`. Mirrors
685
- * {@link query_streams}'s filter semantics — see {@link Store.prioritize}.
686
- * Unlike {@link subscribe} (which keeps `max()` of registered
687
- * priorities), this sets the priority outright — operator override
688
- * for the build-time scheduling policy.
689
- *
690
- * @returns Count of streams whose priority changed.
691
- */
692
- async prioritize(filter, priority) {
693
- await sleep();
694
- const streamRe = filter.stream && !filter.stream_exact ? new RegExp(`^${filter.stream}$`) : void 0;
695
- const sourceRe = filter.source && !filter.source_exact ? new RegExp(`^${filter.source}$`) : void 0;
696
- let count = 0;
697
- for (const s of this._streams.values()) {
698
- if (filter.stream !== void 0) {
699
- if (filter.stream_exact ? s.stream !== filter.stream : !streamRe.test(s.stream))
700
- continue;
701
- }
702
- if (filter.source !== void 0) {
703
- if (s.source === void 0) continue;
704
- if (filter.source_exact ? s.source !== filter.source : !sourceRe.test(s.source))
705
- continue;
706
- }
707
- if (filter.blocked !== void 0 && s.blocked !== filter.blocked)
708
- continue;
709
- if (s.priority !== priority) {
710
- s.setPriority(priority);
711
- count++;
712
- }
713
- }
714
- return count;
715
- }
716
- /**
717
- * Streams registered subscription positions to the callback, ordered by
718
- * stream name. Returns the highest event id in the store and the count
719
- * of positions emitted.
720
- */
721
- async query_streams(callback, query) {
722
- await sleep();
723
- const limit = query?.limit ?? 100;
724
- const after = query?.after;
725
- const blocked = query?.blocked;
726
- const streamRe = query?.stream && !query.stream_exact ? new RegExp(`^${query.stream}$`) : void 0;
727
- const sourceRe = query?.source && !query.source_exact ? new RegExp(`^${query.source}$`) : void 0;
728
- const sorted = [...this._streams.values()].sort(
729
- (a, b) => a.stream.localeCompare(b.stream)
730
- );
731
- let count = 0;
732
- for (const s of sorted) {
733
- if (after !== void 0 && s.stream <= after) continue;
734
- if (query?.stream !== void 0) {
735
- if (query.stream_exact ? s.stream !== query.stream : !streamRe.test(s.stream))
736
- continue;
737
- }
738
- if (query?.source !== void 0) {
739
- if (s.source === void 0) continue;
740
- if (query.source_exact ? s.source !== query.source : !sourceRe.test(s.source))
741
- continue;
742
- }
743
- if (blocked !== void 0 && s.blocked !== blocked) continue;
744
- callback({
745
- stream: s.stream,
746
- source: s.source,
747
- at: s.at,
748
- retry: s.retry,
749
- blocked: s.blocked,
750
- error: s.error,
751
- priority: s.priority,
752
- leased_by: s.leased_by,
753
- leased_until: s.leased_until
754
- });
755
- count++;
756
- if (count >= limit) break;
757
- }
758
- return { maxEventId: this._events.length - 1, count };
759
- }
760
- /**
761
- * Atomically truncates streams and seeds each with a snapshot or tombstone.
762
- * @param targets - Streams to truncate with optional snapshot state and meta.
763
- * @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.
764
- */
765
- async truncate(targets) {
766
- await sleep();
767
- const deletedCounts = /* @__PURE__ */ new Map();
768
- const streamSet = new Set(targets.map((t) => t.stream));
769
- for (const e of this._events) {
770
- if (streamSet.has(e.stream)) {
771
- deletedCounts.set(e.stream, (deletedCounts.get(e.stream) ?? 0) + 1);
772
- }
773
- }
774
- this._events = this._events.filter((e) => !streamSet.has(e.stream));
775
- for (const stream of streamSet) {
776
- this._streams.delete(stream);
777
- this._streamVersions.delete(stream);
778
- this._maxEventIdByStream.delete(stream);
779
- }
780
- const result = /* @__PURE__ */ new Map();
781
- for (const { stream, snapshot, meta } of targets) {
782
- const event = {
783
- id: this._events.length,
784
- stream,
785
- version: 0,
786
- created: /* @__PURE__ */ new Date(),
787
- name: snapshot !== void 0 ? SNAP_EVENT : TOMBSTONE_EVENT,
788
- data: snapshot ?? {},
789
- meta: meta ?? { correlation: "", causation: {} }
790
- };
791
- this._events.push(event);
792
- this._streamVersions.set(stream, 0);
793
- if (event.name !== SNAP_EVENT) {
794
- this._maxEventIdByStream.set(stream, event.id);
795
- }
796
- result.set(stream, {
797
- deleted: deletedCounts.get(stream) ?? 0,
798
- committed: event
799
- });
800
- }
801
- let max = -1;
802
- for (const id of this._maxEventIdByStream.values()) if (id > max) max = id;
803
- this._maxNonSnapEventId = max;
804
- return result;
805
- }
806
- };
807
-
808
- // src/ports.ts
809
- var scoped = new AsyncLocalStorage();
810
- var ExitCodes = ["ERROR", "EXIT"];
811
- var adapters = /* @__PURE__ */ new Map();
812
- function port(injector) {
813
- return (adapter) => {
814
- if (!adapters.has(injector.name)) {
815
- const injected = injector(adapter);
816
- adapters.set(injector.name, injected);
817
- log().info(`[act] + ${injector.name}:${injected.constructor.name}`);
818
- }
819
- return adapters.get(injector.name);
820
- };
821
- }
822
- var log = port(function log2(adapter) {
823
- const cfg = config();
824
- return adapter || new ConsoleLogger({
825
- level: cfg.logLevel,
826
- pretty: cfg.env !== "production"
827
- });
828
- });
829
- var _store = port(function store(adapter) {
830
- return adapter ?? new InMemoryStore();
831
- });
832
- var store2 = ((adapter) => {
833
- return scoped.getStore()?.store ?? _store(adapter);
834
- });
835
- var _cache = port(function cache(adapter) {
836
- return adapter ?? new InMemoryCache();
837
- });
838
- var cache2 = ((adapter) => {
839
- return scoped.getStore()?.cache ?? _cache(adapter);
840
- });
841
- var disposers = [];
842
- async function disposeAndExit(code = "EXIT") {
843
- if (code === "ERROR" && config().env === "production") {
844
- log().warn(
845
- "disposeAndExit('ERROR') ignored in production \u2014 process kept alive"
846
- );
847
- return;
848
- }
849
- for (const disposer of [...disposers].reverse()) {
850
- await disposer();
851
- }
852
- for (const adapter of [...adapters.values()].reverse()) {
853
- await adapter.dispose();
854
- log().info(`[act] - ${adapter.constructor.name}`);
855
- }
856
- adapters.clear();
857
- config().env !== "test" && process.exit(code === "ERROR" ? 1 : 0);
858
- }
859
- function dispose(disposer) {
860
- disposer && disposers.push(disposer);
861
- return disposeAndExit;
862
- }
863
- var SNAP_EVENT = "__snapshot__";
864
- var TOMBSTONE_EVENT = "__tombstone__";
38
+ import "./chunk-5WRI5ZAA.js";
865
39
 
866
40
  // src/signals.ts
867
41
  process.once("SIGINT", async (arg) => {
@@ -966,7 +140,7 @@ async function scanStreamHeads(streams) {
966
140
  let maxId = -1;
967
141
  let version = -1;
968
142
  let lastEventName = "";
969
- await store2().query(
143
+ await store().query(
970
144
  (e) => {
971
145
  if (e.name === TOMBSTONE_EVENT || maxId !== -1) return;
972
146
  maxId = e.id;
@@ -983,7 +157,7 @@ async function scanStreamHeads(streams) {
983
157
  async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
984
158
  if (reactiveEventsSize === 0) return [...streamInfo.keys()];
985
159
  const pendingSet = /* @__PURE__ */ new Set();
986
- await store2().query_streams((position) => {
160
+ await store().query_streams((position) => {
987
161
  const sourceRe = position.source ? RegExp(position.source) : void 0;
988
162
  for (const [stream, info] of streamInfo) {
989
163
  if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
@@ -1054,13 +228,13 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
1054
228
  }
1055
229
  };
1056
230
  });
1057
- const truncated = await store2().truncate(truncTargets);
231
+ const truncated = await store().truncate(truncTargets);
1058
232
  await Promise.all(
1059
233
  guarded.map(async (stream) => {
1060
234
  const entry = truncated.get(stream);
1061
235
  const state2 = seedStates.get(stream);
1062
236
  if (state2 && entry) {
1063
- await cache2().set(stream, {
237
+ await cache().set(stream, {
1064
238
  state: state2,
1065
239
  version: entry.committed.version,
1066
240
  event_id: entry.committed.id,
@@ -1068,7 +242,7 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
1068
242
  snaps: 1
1069
243
  });
1070
244
  } else {
1071
- await cache2().invalidate(stream);
245
+ await cache().invalidate(stream);
1072
246
  }
1073
247
  })
1074
248
  );
@@ -1103,7 +277,7 @@ var CorrelateCycle = class {
1103
277
  async init() {
1104
278
  if (this._initialized) return;
1105
279
  this._initialized = true;
1106
- const { watermark } = await store2().subscribe([...this.staticTargets]);
280
+ const { watermark } = await store().subscribe([...this.staticTargets]);
1107
281
  this._checkpoint = watermark;
1108
282
  this.onInit?.();
1109
283
  for (const { stream } of this.staticTargets) {
@@ -1122,7 +296,7 @@ var CorrelateCycle = class {
1122
296
  const after = Math.max(this._checkpoint, query.after || -1);
1123
297
  const correlated = /* @__PURE__ */ new Map();
1124
298
  let last_id = after;
1125
- await store2().query(
299
+ await store().query(
1126
300
  (event) => {
1127
301
  last_id = event.id;
1128
302
  const register = this.registry.events[event.name];
@@ -1642,12 +816,12 @@ var SettleLoop = class {
1642
816
  };
1643
817
 
1644
818
  // src/internal/drain.ts
1645
- var claim = (lagging, leading, by, millis) => store2().claim(lagging, leading, by, millis);
819
+ var claim = (lagging, leading, by, millis) => store().claim(lagging, leading, by, millis);
1646
820
  async function fetch(leased, eventLimit) {
1647
821
  return Promise.all(
1648
822
  leased.map(async ({ stream, source, at, lagging }) => {
1649
823
  const events = [];
1650
- await store2().query((e) => events.push(e), {
824
+ await store().query((e) => events.push(e), {
1651
825
  stream: source,
1652
826
  after: at,
1653
827
  limit: eventLimit
@@ -1656,9 +830,9 @@ async function fetch(leased, eventLimit) {
1656
830
  })
1657
831
  );
1658
832
  }
1659
- var ack = (leases) => store2().ack(leases);
1660
- var block = (leases) => store2().block(leases);
1661
- var subscribe = (streams) => store2().subscribe(streams);
833
+ var ack = (leases) => store().ack(leases);
834
+ var block = (leases) => store().block(leases);
835
+ var subscribe = (streams) => store().subscribe(streams);
1662
836
 
1663
837
  // src/internal/event-sourcing.ts
1664
838
  import { randomUUID as randomUUID3 } from "crypto";
@@ -1666,7 +840,7 @@ import { patch } from "@rotorsoft/act-patch";
1666
840
  async function snap(snapshot) {
1667
841
  try {
1668
842
  const { id, stream, name, meta, version } = snapshot.event;
1669
- await store2().commit(
843
+ await store().commit(
1670
844
  stream,
1671
845
  [{ name: SNAP_EVENT, data: snapshot.state }],
1672
846
  {
@@ -1682,7 +856,7 @@ async function snap(snapshot) {
1682
856
  }
1683
857
  async function tombstone(stream, expectedVersion, correlation) {
1684
858
  try {
1685
- const [committed] = await store2().commit(
859
+ const [committed] = await store().commit(
1686
860
  stream,
1687
861
  [{ name: TOMBSTONE_EVENT, data: {} }],
1688
862
  { correlation, causation: {} },
@@ -1696,7 +870,7 @@ async function tombstone(stream, expectedVersion, correlation) {
1696
870
  }
1697
871
  async function load(me, stream, callback, asOf) {
1698
872
  const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
1699
- const cached = timeTravel ? void 0 : await cache2().get(stream);
873
+ const cached = timeTravel ? void 0 : await cache().get(stream);
1700
874
  const cache_hit = !!cached;
1701
875
  let state2 = cached?.state ?? (me.init ? me.init() : {});
1702
876
  let patches = cached?.patches ?? 0;
@@ -1704,7 +878,7 @@ async function load(me, stream, callback, asOf) {
1704
878
  let version = cached?.version ?? -1;
1705
879
  let replayed = 0;
1706
880
  let event;
1707
- await store2().query(
881
+ await store().query(
1708
882
  (e) => {
1709
883
  event = e;
1710
884
  version = e.version;
@@ -1739,7 +913,7 @@ async function load(me, stream, callback, asOf) {
1739
913
  }
1740
914
  );
1741
915
  if (replayed > 0 && !timeTravel && event) {
1742
- await cache2().set(stream, {
916
+ await cache().set(stream, {
1743
917
  state: state2,
1744
918
  version,
1745
919
  event_id: event.id,
@@ -1812,7 +986,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1812
986
  };
1813
987
  let committed;
1814
988
  try {
1815
- committed = await store2().commit(
989
+ committed = await store().commit(
1816
990
  stream,
1817
991
  emitted,
1818
992
  meta,
@@ -1824,7 +998,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1824
998
  );
1825
999
  } catch (error) {
1826
1000
  if (error instanceof ConcurrencyError) {
1827
- await cache2().invalidate(stream);
1001
+ await cache().invalidate(stream);
1828
1002
  }
1829
1003
  throw error;
1830
1004
  }
@@ -1846,7 +1020,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
1846
1020
  });
1847
1021
  const last = snapshots.at(-1);
1848
1022
  const snapped = me.snap?.(last);
1849
- cache2().set(stream, {
1023
+ cache().set(stream, {
1850
1024
  state: last.state,
1851
1025
  version: last.event.version,
1852
1026
  event_id: last.event.id,
@@ -2087,14 +1261,8 @@ var Act = class {
2087
1261
  },
2088
1262
  options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
2089
1263
  );
2090
- this._notify_disposer = this._wireNotify(options.scoped?.store ?? store2());
2091
- dispose(async () => {
2092
- this._emitter.removeAllListeners();
2093
- this.stop_correlations();
2094
- this.stop_settling();
2095
- const disposer = await this._notify_disposer;
2096
- if (disposer) await disposer();
2097
- });
1264
+ this._notify_disposer = this._wireNotify(options.scoped?.store ?? store());
1265
+ dispose(() => this.shutdown());
2098
1266
  }
2099
1267
  _emitter = new EventEmitter();
2100
1268
  /** Event names with at least one registered reaction (computed at build time) */
@@ -2170,6 +1338,30 @@ var Act = class {
2170
1338
  /** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
2171
1339
  _handle;
2172
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
+ }
2173
1365
  /**
2174
1366
  * Subscribe to {@link Store.notify} when both the store and the
2175
1367
  * registry support it. Returns the disposer (or `undefined` when no
@@ -2365,7 +1557,7 @@ var Act = class {
2365
1557
  return this._scoped(async () => {
2366
1558
  let first;
2367
1559
  let last;
2368
- const count = await store2().query((e) => {
1560
+ const count = await store().query((e) => {
2369
1561
  if (!first) first = e;
2370
1562
  last = e;
2371
1563
  callback?.(e);
@@ -2402,7 +1594,7 @@ var Act = class {
2402
1594
  async query_array(query) {
2403
1595
  return this._scoped(async () => {
2404
1596
  const events = [];
2405
- await store2().query((e) => events.push(e), query);
1597
+ await store().query((e) => events.push(e), query);
2406
1598
  return events;
2407
1599
  });
2408
1600
  }
@@ -2614,7 +1806,7 @@ var Act = class {
2614
1806
  */
2615
1807
  async reset(streams) {
2616
1808
  return this._scoped(async () => {
2617
- const count = await store2().reset(streams);
1809
+ const count = await store().reset(streams);
2618
1810
  if (count > 0 && this._reactive_events.size > 0) this._drain.arm();
2619
1811
  return count;
2620
1812
  });
@@ -2658,7 +1850,7 @@ var Act = class {
2658
1850
  * @see {@link claim} for how priority biases scheduling
2659
1851
  */
2660
1852
  async prioritize(filter, priority) {
2661
- return this._scoped(() => store2().prioritize(filter, priority));
1853
+ return this._scoped(() => store().prioritize(filter, priority));
2662
1854
  }
2663
1855
  /**
2664
1856
  * Close the books — guard, archive, truncate, and optionally restart streams.
@@ -3079,7 +2271,7 @@ export {
3079
2271
  ValidationError,
3080
2272
  ZodEmpty,
3081
2273
  act,
3082
- cache2 as cache,
2274
+ cache,
3083
2275
  config,
3084
2276
  dispose,
3085
2277
  disposeAndExit,
@@ -3091,7 +2283,7 @@ export {
3091
2283
  sleep,
3092
2284
  slice,
3093
2285
  state,
3094
- store2 as store,
2286
+ store,
3095
2287
  validate
3096
2288
  };
3097
2289
  //# sourceMappingURL=index.js.map