@rotorsoft/act 0.32.5 → 0.32.6
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/.tsbuildinfo +1 -1
- package/dist/@types/adapters/{ConsoleLogger.d.ts → console-logger.d.ts} +2 -2
- package/dist/@types/adapters/console-logger.d.ts.map +1 -0
- package/dist/@types/adapters/{InMemoryCache.d.ts → in-memory-cache.d.ts} +1 -1
- package/dist/@types/adapters/in-memory-cache.d.ts.map +1 -0
- package/dist/@types/adapters/{InMemoryStore.d.ts → in-memory-store.d.ts} +5 -1
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -0
- package/dist/@types/adapters/index.d.ts +3 -3
- package/dist/@types/adapters/index.d.ts.map +1 -1
- package/dist/@types/config.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/lru-map.d.ts.map +1 -1
- package/dist/@types/ports.d.ts +1 -1
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/@types/utils.d.ts +27 -296
- package/dist/@types/utils.d.ts.map +1 -1
- package/dist/{chunk-JBKZJXQZ.js → chunk-IDEYGKT4.js} +2 -2
- package/dist/{chunk-JBKZJXQZ.js.map → chunk-IDEYGKT4.js.map} +1 -1
- package/dist/index.cjs +127 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +127 -48
- package/dist/index.js.map +1 -1
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +1 -1
- package/dist/@types/adapters/ConsoleLogger.d.ts.map +0 -1
- package/dist/@types/adapters/InMemoryCache.d.ts.map +0 -1
- package/dist/@types/adapters/InMemoryStore.d.ts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -70,7 +70,7 @@ __export(index_exports, {
|
|
|
70
70
|
});
|
|
71
71
|
module.exports = __toCommonJS(index_exports);
|
|
72
72
|
|
|
73
|
-
// src/adapters/
|
|
73
|
+
// src/adapters/console-logger.ts
|
|
74
74
|
var LEVEL_VALUES = {
|
|
75
75
|
fatal: 60,
|
|
76
76
|
error: 50,
|
|
@@ -139,14 +139,25 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
139
139
|
obj = {};
|
|
140
140
|
} else if (objOrMsg !== null && typeof objOrMsg === "object") {
|
|
141
141
|
message = msg;
|
|
142
|
-
obj =
|
|
142
|
+
obj = { ...objOrMsg };
|
|
143
143
|
} else {
|
|
144
144
|
message = msg;
|
|
145
145
|
obj = { value: objOrMsg };
|
|
146
146
|
}
|
|
147
147
|
const entry = Object.assign({ level, time: Date.now() }, bindings, obj);
|
|
148
148
|
if (message) entry.msg = message;
|
|
149
|
-
|
|
149
|
+
let line;
|
|
150
|
+
try {
|
|
151
|
+
line = JSON.stringify(entry);
|
|
152
|
+
} catch {
|
|
153
|
+
line = JSON.stringify({
|
|
154
|
+
level,
|
|
155
|
+
time: entry.time,
|
|
156
|
+
msg: message ?? "[unserializable]",
|
|
157
|
+
unserializable: true
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
process.stdout.write(line + "\n");
|
|
150
161
|
}
|
|
151
162
|
_prettyWrite(bindings, level, _num, objOrMsg, msg) {
|
|
152
163
|
const color = LEVEL_COLORS[level];
|
|
@@ -192,7 +203,7 @@ var LruMap = class {
|
|
|
192
203
|
this._entries.delete(key);
|
|
193
204
|
if (this._entries.size >= this._maxSize) {
|
|
194
205
|
const oldest = this._entries.keys().next().value;
|
|
195
|
-
|
|
206
|
+
this._entries.delete(oldest);
|
|
196
207
|
}
|
|
197
208
|
this._entries.set(key, value);
|
|
198
209
|
}
|
|
@@ -228,7 +239,7 @@ var LruSet = class {
|
|
|
228
239
|
}
|
|
229
240
|
};
|
|
230
241
|
|
|
231
|
-
// src/adapters/
|
|
242
|
+
// src/adapters/in-memory-cache.ts
|
|
232
243
|
var InMemoryCache = class {
|
|
233
244
|
// CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
|
|
234
245
|
// any is bidirectionally compatible with the per-call TState binding, while
|
|
@@ -294,7 +305,7 @@ var StreamClosedError = class extends Error {
|
|
|
294
305
|
var ConcurrencyError = class extends Error {
|
|
295
306
|
constructor(stream, lastVersion, events, expectedVersion) {
|
|
296
307
|
super(
|
|
297
|
-
`Concurrency error committing "${events.map((e) => `${stream}.${e.name}
|
|
308
|
+
`Concurrency error committing "${events.map((e) => `${stream}.${e.name}`).join(
|
|
298
309
|
", "
|
|
299
310
|
)}". Expected version ${expectedVersion} but found version ${lastVersion}.`
|
|
300
311
|
);
|
|
@@ -383,10 +394,21 @@ var PackageSchema = import_zod2.z.object({
|
|
|
383
394
|
license: import_zod2.z.string().min(1).optional(),
|
|
384
395
|
dependencies: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).optional()
|
|
385
396
|
});
|
|
397
|
+
var FALLBACK_PACKAGE = {
|
|
398
|
+
name: "act-fallback",
|
|
399
|
+
version: "0.0.0-fallback",
|
|
400
|
+
description: "Synthetic fallback \u2014 package.json could not be loaded"
|
|
401
|
+
};
|
|
386
402
|
var getPackage = () => {
|
|
387
|
-
|
|
388
|
-
|
|
403
|
+
try {
|
|
404
|
+
const raw = fs.readFileSync("package.json");
|
|
405
|
+
return JSON.parse(raw.toString());
|
|
406
|
+
} catch (err) {
|
|
407
|
+
pkgLoadError = err;
|
|
408
|
+
return FALLBACK_PACKAGE;
|
|
409
|
+
}
|
|
389
410
|
};
|
|
411
|
+
var pkgLoadError;
|
|
390
412
|
var BaseSchema = PackageSchema.extend({
|
|
391
413
|
env: import_zod2.z.enum(Environments),
|
|
392
414
|
logLevel: import_zod2.z.enum(LogLevels),
|
|
@@ -406,6 +428,13 @@ var config = () => {
|
|
|
406
428
|
{ ...pkg, env, logLevel, logSingleLine, sleepMs },
|
|
407
429
|
BaseSchema
|
|
408
430
|
);
|
|
431
|
+
if (pkgLoadError) {
|
|
432
|
+
const msg = pkgLoadError instanceof Error ? pkgLoadError.message : typeof pkgLoadError === "string" ? pkgLoadError : "unknown error";
|
|
433
|
+
log().warn(
|
|
434
|
+
`[act] Could not read package.json (${msg}); using synthetic name="${FALLBACK_PACKAGE.name}" version="${FALLBACK_PACKAGE.version}".`
|
|
435
|
+
);
|
|
436
|
+
pkgLoadError = void 0;
|
|
437
|
+
}
|
|
409
438
|
}
|
|
410
439
|
return _validated;
|
|
411
440
|
};
|
|
@@ -429,7 +458,7 @@ async function sleep(ms) {
|
|
|
429
458
|
return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
|
|
430
459
|
}
|
|
431
460
|
|
|
432
|
-
// src/adapters/
|
|
461
|
+
// src/adapters/in-memory-store.ts
|
|
433
462
|
var InMemoryStream = class {
|
|
434
463
|
constructor(stream, source) {
|
|
435
464
|
this.stream = stream;
|
|
@@ -524,11 +553,13 @@ var InMemoryStream = class {
|
|
|
524
553
|
}
|
|
525
554
|
}
|
|
526
555
|
/**
|
|
527
|
-
* Reset this stream's watermark and state for replay.
|
|
556
|
+
* Reset this stream's watermark and state for replay. The retry counter
|
|
557
|
+
* resets to -1 to match the constructor + ack() invariant ("released
|
|
558
|
+
* stream"); the next claim() bumps it to 0 (first attempt).
|
|
528
559
|
*/
|
|
529
560
|
reset() {
|
|
530
561
|
this._at = -1;
|
|
531
|
-
this._retry =
|
|
562
|
+
this._retry = -1;
|
|
532
563
|
this._blocked = false;
|
|
533
564
|
this._error = "";
|
|
534
565
|
this._leased_by = void 0;
|
|
@@ -540,13 +571,26 @@ var InMemoryStore = class {
|
|
|
540
571
|
_events = [];
|
|
541
572
|
// stored stream positions and other metadata
|
|
542
573
|
_streams = /* @__PURE__ */ new Map();
|
|
574
|
+
// last committed version per stream — O(1) replacement for filter-on-commit
|
|
575
|
+
_streamVersions = /* @__PURE__ */ new Map();
|
|
576
|
+
// max non-snapshot event id per stream — drives the source-pattern probe in claim()
|
|
577
|
+
// without scanning the full event log.
|
|
578
|
+
_maxEventIdByStream = /* @__PURE__ */ new Map();
|
|
579
|
+
// global max non-snapshot event id — fast pre-check for source-less streams in claim()
|
|
580
|
+
_maxNonSnapEventId = -1;
|
|
581
|
+
_resetIndexes() {
|
|
582
|
+
this._events.length = 0;
|
|
583
|
+
this._streamVersions.clear();
|
|
584
|
+
this._maxEventIdByStream.clear();
|
|
585
|
+
this._maxNonSnapEventId = -1;
|
|
586
|
+
}
|
|
543
587
|
/**
|
|
544
588
|
* Dispose of the store and clear all events.
|
|
545
589
|
* @returns Promise that resolves when disposal is complete.
|
|
546
590
|
*/
|
|
547
591
|
async dispose() {
|
|
548
592
|
await sleep();
|
|
549
|
-
this.
|
|
593
|
+
this._resetIndexes();
|
|
550
594
|
}
|
|
551
595
|
/**
|
|
552
596
|
* Seed the store with initial data (no-op for in-memory).
|
|
@@ -561,7 +605,7 @@ var InMemoryStore = class {
|
|
|
561
605
|
*/
|
|
562
606
|
async drop() {
|
|
563
607
|
await sleep();
|
|
564
|
-
this.
|
|
608
|
+
this._resetIndexes();
|
|
565
609
|
this._streams = /* @__PURE__ */ new Map();
|
|
566
610
|
}
|
|
567
611
|
in_query(query, e) {
|
|
@@ -624,18 +668,19 @@ var InMemoryStore = class {
|
|
|
624
668
|
*/
|
|
625
669
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
626
670
|
await sleep();
|
|
627
|
-
const
|
|
628
|
-
if (typeof expectedVersion === "number" &&
|
|
671
|
+
const currentVersion = this._streamVersions.get(stream) ?? -1;
|
|
672
|
+
if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
|
|
629
673
|
throw new ConcurrencyError(
|
|
630
674
|
stream,
|
|
631
|
-
|
|
675
|
+
currentVersion,
|
|
632
676
|
msgs,
|
|
633
677
|
expectedVersion
|
|
634
678
|
);
|
|
635
679
|
}
|
|
636
|
-
let version =
|
|
637
|
-
|
|
638
|
-
|
|
680
|
+
let version = currentVersion + 1;
|
|
681
|
+
let lastNonSnapId = -1;
|
|
682
|
+
const committed = msgs.map(({ name, data }) => {
|
|
683
|
+
const c = {
|
|
639
684
|
id: this._events.length,
|
|
640
685
|
stream,
|
|
641
686
|
version,
|
|
@@ -644,10 +689,17 @@ var InMemoryStore = class {
|
|
|
644
689
|
data,
|
|
645
690
|
meta
|
|
646
691
|
};
|
|
647
|
-
this._events.push(
|
|
692
|
+
this._events.push(c);
|
|
693
|
+
if (name !== SNAP_EVENT) lastNonSnapId = c.id;
|
|
648
694
|
version++;
|
|
649
|
-
return
|
|
695
|
+
return c;
|
|
650
696
|
});
|
|
697
|
+
this._streamVersions.set(stream, version - 1);
|
|
698
|
+
if (lastNonSnapId >= 0) {
|
|
699
|
+
this._maxEventIdByStream.set(stream, lastNonSnapId);
|
|
700
|
+
this._maxNonSnapEventId = lastNonSnapId;
|
|
701
|
+
}
|
|
702
|
+
return committed;
|
|
651
703
|
}
|
|
652
704
|
/**
|
|
653
705
|
* Atomically discovers and leases streams for processing.
|
|
@@ -660,10 +712,26 @@ var InMemoryStore = class {
|
|
|
660
712
|
*/
|
|
661
713
|
async claim(lagging, leading, by, millis) {
|
|
662
714
|
await sleep();
|
|
715
|
+
const sourceRegex = /* @__PURE__ */ new Map();
|
|
716
|
+
const getRegex = (source) => {
|
|
717
|
+
let re = sourceRegex.get(source);
|
|
718
|
+
if (!re) {
|
|
719
|
+
re = new RegExp(source);
|
|
720
|
+
sourceRegex.set(source, re);
|
|
721
|
+
}
|
|
722
|
+
return re;
|
|
723
|
+
};
|
|
724
|
+
const hasWork = (s) => {
|
|
725
|
+
if (s.at < 0) return true;
|
|
726
|
+
if (!s.source) return s.at < this._maxNonSnapEventId;
|
|
727
|
+
const re = getRegex(s.source);
|
|
728
|
+
for (const [streamName, maxId] of this._maxEventIdByStream) {
|
|
729
|
+
if (maxId > s.at && re.test(streamName)) return true;
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
};
|
|
663
733
|
const available = [...this._streams.values()].filter(
|
|
664
|
-
(s) => s.is_available && (s
|
|
665
|
-
(e) => e.id > s.at && e.name !== SNAP_EVENT && (!s.source || RegExp(s.source).test(e.stream))
|
|
666
|
-
))
|
|
734
|
+
(s) => s.is_available && hasWork(s)
|
|
667
735
|
);
|
|
668
736
|
const lag = available.sort((a, b) => a.at - b.at).slice(0, lagging).map((s) => ({
|
|
669
737
|
stream: s.stream,
|
|
@@ -800,9 +868,13 @@ var InMemoryStore = class {
|
|
|
800
868
|
}
|
|
801
869
|
}
|
|
802
870
|
this._events = this._events.filter((e) => !streamSet.has(e.stream));
|
|
871
|
+
for (const stream of streamSet) {
|
|
872
|
+
this._streams.delete(stream);
|
|
873
|
+
this._streamVersions.delete(stream);
|
|
874
|
+
this._maxEventIdByStream.delete(stream);
|
|
875
|
+
}
|
|
803
876
|
const result = /* @__PURE__ */ new Map();
|
|
804
877
|
for (const { stream, snapshot, meta } of targets) {
|
|
805
|
-
this._streams.delete(stream);
|
|
806
878
|
const event = {
|
|
807
879
|
id: this._events.length,
|
|
808
880
|
stream,
|
|
@@ -813,11 +885,18 @@ var InMemoryStore = class {
|
|
|
813
885
|
meta: meta ?? { correlation: "", causation: {} }
|
|
814
886
|
};
|
|
815
887
|
this._events.push(event);
|
|
888
|
+
this._streamVersions.set(stream, 0);
|
|
889
|
+
if (event.name !== SNAP_EVENT) {
|
|
890
|
+
this._maxEventIdByStream.set(stream, event.id);
|
|
891
|
+
}
|
|
816
892
|
result.set(stream, {
|
|
817
893
|
deleted: deletedCounts.get(stream) ?? 0,
|
|
818
894
|
committed: event
|
|
819
895
|
});
|
|
820
896
|
}
|
|
897
|
+
let max = -1;
|
|
898
|
+
for (const id of this._maxEventIdByStream.values()) if (id > max) max = id;
|
|
899
|
+
this._maxNonSnapEventId = max;
|
|
821
900
|
return result;
|
|
822
901
|
}
|
|
823
902
|
};
|
|
@@ -850,7 +929,12 @@ var cache = port(function cache2(adapter) {
|
|
|
850
929
|
});
|
|
851
930
|
var disposers = [];
|
|
852
931
|
async function disposeAndExit(code = "EXIT") {
|
|
853
|
-
if (code === "ERROR" && config().env === "production")
|
|
932
|
+
if (code === "ERROR" && config().env === "production") {
|
|
933
|
+
log().warn(
|
|
934
|
+
"disposeAndExit('ERROR') ignored in production \u2014 process kept alive"
|
|
935
|
+
);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
854
938
|
for (const disposer of [...disposers].reverse()) {
|
|
855
939
|
await disposer();
|
|
856
940
|
}
|
|
@@ -892,7 +976,6 @@ var import_events = __toESM(require("events"), 1);
|
|
|
892
976
|
// src/internal/close-cycle.ts
|
|
893
977
|
var import_crypto = require("crypto");
|
|
894
978
|
async function runCloseCycle(targets, deps) {
|
|
895
|
-
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
896
979
|
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
897
980
|
const streams = [...targetMap.keys()];
|
|
898
981
|
const skipped = [];
|
|
@@ -935,22 +1018,15 @@ async function scanStreamHeads(streams) {
|
|
|
935
1018
|
streams.map(async (s) => {
|
|
936
1019
|
let maxId = -1;
|
|
937
1020
|
let version = -1;
|
|
938
|
-
let lastEventName;
|
|
1021
|
+
let lastEventName = "";
|
|
939
1022
|
await store().query(
|
|
940
1023
|
(e) => {
|
|
941
|
-
if (e.name === TOMBSTONE_EVENT) return;
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
if (e.name !== SNAP_EVENT && lastEventName === void 0) {
|
|
947
|
-
lastEventName = e.name;
|
|
948
|
-
}
|
|
1024
|
+
if (e.name === TOMBSTONE_EVENT || maxId !== -1) return;
|
|
1025
|
+
maxId = e.id;
|
|
1026
|
+
version = e.version;
|
|
1027
|
+
lastEventName = e.name;
|
|
949
1028
|
},
|
|
950
|
-
|
|
951
|
-
// always preceded by the domain event it captured). Streams with
|
|
952
|
-
// unusual layouts fall back to no-seed via the lookup miss path.
|
|
953
|
-
{ stream: s, stream_exact: true, backward: true, limit: 2 }
|
|
1029
|
+
{ stream: s, stream_exact: true, backward: true, limit: 1 }
|
|
954
1030
|
);
|
|
955
1031
|
if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
|
|
956
1032
|
})
|
|
@@ -996,11 +1072,11 @@ async function loadRestartSeeds(guarded, targetMap, streamInfo, eventToState, lo
|
|
|
996
1072
|
const seedStates = /* @__PURE__ */ new Map();
|
|
997
1073
|
await Promise.all(
|
|
998
1074
|
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
999
|
-
const lastEventName = streamInfo.get(stream)
|
|
1000
|
-
const ownerState =
|
|
1075
|
+
const lastEventName = streamInfo.get(stream).lastEventName;
|
|
1076
|
+
const ownerState = eventToState.get(lastEventName);
|
|
1001
1077
|
if (!ownerState) {
|
|
1002
1078
|
logger.error(
|
|
1003
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName
|
|
1079
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName}". Stream will be tombstoned instead.`
|
|
1004
1080
|
);
|
|
1005
1081
|
return;
|
|
1006
1082
|
}
|
|
@@ -1078,8 +1154,8 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1078
1154
|
const handled = await Promise.all(
|
|
1079
1155
|
leased.map((lease) => {
|
|
1080
1156
|
const entry = fetchMap.get(lease.stream);
|
|
1081
|
-
const at = entry
|
|
1082
|
-
const payloads = entry
|
|
1157
|
+
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1158
|
+
const { payloads } = entry;
|
|
1083
1159
|
const batchHandler = batchHandlers.get(lease.stream);
|
|
1084
1160
|
if (batchHandler && payloads.length > 0) {
|
|
1085
1161
|
return handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
@@ -1375,8 +1451,8 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1375
1451
|
action: {
|
|
1376
1452
|
name: action2,
|
|
1377
1453
|
...target
|
|
1378
|
-
// payload
|
|
1379
|
-
//
|
|
1454
|
+
// payload intentionally omitted: it can be large or contain PII,
|
|
1455
|
+
// and callers correlate via the correlation id when they need it.
|
|
1380
1456
|
},
|
|
1381
1457
|
event: reactingTo ? {
|
|
1382
1458
|
id: reactingTo.id,
|
|
@@ -1391,7 +1467,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1391
1467
|
stream,
|
|
1392
1468
|
emitted,
|
|
1393
1469
|
meta,
|
|
1394
|
-
//
|
|
1470
|
+
// Reactions skip optimistic concurrency: they always append against the
|
|
1471
|
+
// current head. Stream leasing already serializes concurrent reactions,
|
|
1472
|
+
// and forcing version checks here would turn ordinary catch-up into
|
|
1473
|
+
// spurious retries.
|
|
1395
1474
|
reactingTo ? void 0 : expected
|
|
1396
1475
|
);
|
|
1397
1476
|
} catch (error) {
|