@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.js
CHANGED
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
TargetSchema,
|
|
14
14
|
ValidationError,
|
|
15
15
|
ZodEmpty
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-IDEYGKT4.js";
|
|
17
17
|
|
|
18
|
-
// src/adapters/
|
|
18
|
+
// src/adapters/console-logger.ts
|
|
19
19
|
var LEVEL_VALUES = {
|
|
20
20
|
fatal: 60,
|
|
21
21
|
error: 50,
|
|
@@ -84,14 +84,25 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
84
84
|
obj = {};
|
|
85
85
|
} else if (objOrMsg !== null && typeof objOrMsg === "object") {
|
|
86
86
|
message = msg;
|
|
87
|
-
obj =
|
|
87
|
+
obj = { ...objOrMsg };
|
|
88
88
|
} else {
|
|
89
89
|
message = msg;
|
|
90
90
|
obj = { value: objOrMsg };
|
|
91
91
|
}
|
|
92
92
|
const entry = Object.assign({ level, time: Date.now() }, bindings, obj);
|
|
93
93
|
if (message) entry.msg = message;
|
|
94
|
-
|
|
94
|
+
let line;
|
|
95
|
+
try {
|
|
96
|
+
line = JSON.stringify(entry);
|
|
97
|
+
} catch {
|
|
98
|
+
line = JSON.stringify({
|
|
99
|
+
level,
|
|
100
|
+
time: entry.time,
|
|
101
|
+
msg: message ?? "[unserializable]",
|
|
102
|
+
unserializable: true
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(line + "\n");
|
|
95
106
|
}
|
|
96
107
|
_prettyWrite(bindings, level, _num, objOrMsg, msg) {
|
|
97
108
|
const color = LEVEL_COLORS[level];
|
|
@@ -137,7 +148,7 @@ var LruMap = class {
|
|
|
137
148
|
this._entries.delete(key);
|
|
138
149
|
if (this._entries.size >= this._maxSize) {
|
|
139
150
|
const oldest = this._entries.keys().next().value;
|
|
140
|
-
|
|
151
|
+
this._entries.delete(oldest);
|
|
141
152
|
}
|
|
142
153
|
this._entries.set(key, value);
|
|
143
154
|
}
|
|
@@ -173,7 +184,7 @@ var LruSet = class {
|
|
|
173
184
|
}
|
|
174
185
|
};
|
|
175
186
|
|
|
176
|
-
// src/adapters/
|
|
187
|
+
// src/adapters/in-memory-cache.ts
|
|
177
188
|
var InMemoryCache = class {
|
|
178
189
|
// CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
|
|
179
190
|
// any is bidirectionally compatible with the per-call TState binding, while
|
|
@@ -216,10 +227,21 @@ var PackageSchema = z.object({
|
|
|
216
227
|
license: z.string().min(1).optional(),
|
|
217
228
|
dependencies: z.record(z.string(), z.string()).optional()
|
|
218
229
|
});
|
|
230
|
+
var FALLBACK_PACKAGE = {
|
|
231
|
+
name: "act-fallback",
|
|
232
|
+
version: "0.0.0-fallback",
|
|
233
|
+
description: "Synthetic fallback \u2014 package.json could not be loaded"
|
|
234
|
+
};
|
|
219
235
|
var getPackage = () => {
|
|
220
|
-
|
|
221
|
-
|
|
236
|
+
try {
|
|
237
|
+
const raw = fs.readFileSync("package.json");
|
|
238
|
+
return JSON.parse(raw.toString());
|
|
239
|
+
} catch (err) {
|
|
240
|
+
pkgLoadError = err;
|
|
241
|
+
return FALLBACK_PACKAGE;
|
|
242
|
+
}
|
|
222
243
|
};
|
|
244
|
+
var pkgLoadError;
|
|
223
245
|
var BaseSchema = PackageSchema.extend({
|
|
224
246
|
env: z.enum(Environments),
|
|
225
247
|
logLevel: z.enum(LogLevels),
|
|
@@ -239,6 +261,13 @@ var config = () => {
|
|
|
239
261
|
{ ...pkg, env, logLevel, logSingleLine, sleepMs },
|
|
240
262
|
BaseSchema
|
|
241
263
|
);
|
|
264
|
+
if (pkgLoadError) {
|
|
265
|
+
const msg = pkgLoadError instanceof Error ? pkgLoadError.message : typeof pkgLoadError === "string" ? pkgLoadError : "unknown error";
|
|
266
|
+
log().warn(
|
|
267
|
+
`[act] Could not read package.json (${msg}); using synthetic name="${FALLBACK_PACKAGE.name}" version="${FALLBACK_PACKAGE.version}".`
|
|
268
|
+
);
|
|
269
|
+
pkgLoadError = void 0;
|
|
270
|
+
}
|
|
242
271
|
}
|
|
243
272
|
return _validated;
|
|
244
273
|
};
|
|
@@ -262,7 +291,7 @@ async function sleep(ms) {
|
|
|
262
291
|
return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
|
|
263
292
|
}
|
|
264
293
|
|
|
265
|
-
// src/adapters/
|
|
294
|
+
// src/adapters/in-memory-store.ts
|
|
266
295
|
var InMemoryStream = class {
|
|
267
296
|
constructor(stream, source) {
|
|
268
297
|
this.stream = stream;
|
|
@@ -357,11 +386,13 @@ var InMemoryStream = class {
|
|
|
357
386
|
}
|
|
358
387
|
}
|
|
359
388
|
/**
|
|
360
|
-
* Reset this stream's watermark and state for replay.
|
|
389
|
+
* Reset this stream's watermark and state for replay. The retry counter
|
|
390
|
+
* resets to -1 to match the constructor + ack() invariant ("released
|
|
391
|
+
* stream"); the next claim() bumps it to 0 (first attempt).
|
|
361
392
|
*/
|
|
362
393
|
reset() {
|
|
363
394
|
this._at = -1;
|
|
364
|
-
this._retry =
|
|
395
|
+
this._retry = -1;
|
|
365
396
|
this._blocked = false;
|
|
366
397
|
this._error = "";
|
|
367
398
|
this._leased_by = void 0;
|
|
@@ -373,13 +404,26 @@ var InMemoryStore = class {
|
|
|
373
404
|
_events = [];
|
|
374
405
|
// stored stream positions and other metadata
|
|
375
406
|
_streams = /* @__PURE__ */ new Map();
|
|
407
|
+
// last committed version per stream — O(1) replacement for filter-on-commit
|
|
408
|
+
_streamVersions = /* @__PURE__ */ new Map();
|
|
409
|
+
// max non-snapshot event id per stream — drives the source-pattern probe in claim()
|
|
410
|
+
// without scanning the full event log.
|
|
411
|
+
_maxEventIdByStream = /* @__PURE__ */ new Map();
|
|
412
|
+
// global max non-snapshot event id — fast pre-check for source-less streams in claim()
|
|
413
|
+
_maxNonSnapEventId = -1;
|
|
414
|
+
_resetIndexes() {
|
|
415
|
+
this._events.length = 0;
|
|
416
|
+
this._streamVersions.clear();
|
|
417
|
+
this._maxEventIdByStream.clear();
|
|
418
|
+
this._maxNonSnapEventId = -1;
|
|
419
|
+
}
|
|
376
420
|
/**
|
|
377
421
|
* Dispose of the store and clear all events.
|
|
378
422
|
* @returns Promise that resolves when disposal is complete.
|
|
379
423
|
*/
|
|
380
424
|
async dispose() {
|
|
381
425
|
await sleep();
|
|
382
|
-
this.
|
|
426
|
+
this._resetIndexes();
|
|
383
427
|
}
|
|
384
428
|
/**
|
|
385
429
|
* Seed the store with initial data (no-op for in-memory).
|
|
@@ -394,7 +438,7 @@ var InMemoryStore = class {
|
|
|
394
438
|
*/
|
|
395
439
|
async drop() {
|
|
396
440
|
await sleep();
|
|
397
|
-
this.
|
|
441
|
+
this._resetIndexes();
|
|
398
442
|
this._streams = /* @__PURE__ */ new Map();
|
|
399
443
|
}
|
|
400
444
|
in_query(query, e) {
|
|
@@ -457,18 +501,19 @@ var InMemoryStore = class {
|
|
|
457
501
|
*/
|
|
458
502
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
459
503
|
await sleep();
|
|
460
|
-
const
|
|
461
|
-
if (typeof expectedVersion === "number" &&
|
|
504
|
+
const currentVersion = this._streamVersions.get(stream) ?? -1;
|
|
505
|
+
if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
|
|
462
506
|
throw new ConcurrencyError(
|
|
463
507
|
stream,
|
|
464
|
-
|
|
508
|
+
currentVersion,
|
|
465
509
|
msgs,
|
|
466
510
|
expectedVersion
|
|
467
511
|
);
|
|
468
512
|
}
|
|
469
|
-
let version =
|
|
470
|
-
|
|
471
|
-
|
|
513
|
+
let version = currentVersion + 1;
|
|
514
|
+
let lastNonSnapId = -1;
|
|
515
|
+
const committed = msgs.map(({ name, data }) => {
|
|
516
|
+
const c = {
|
|
472
517
|
id: this._events.length,
|
|
473
518
|
stream,
|
|
474
519
|
version,
|
|
@@ -477,10 +522,17 @@ var InMemoryStore = class {
|
|
|
477
522
|
data,
|
|
478
523
|
meta
|
|
479
524
|
};
|
|
480
|
-
this._events.push(
|
|
525
|
+
this._events.push(c);
|
|
526
|
+
if (name !== SNAP_EVENT) lastNonSnapId = c.id;
|
|
481
527
|
version++;
|
|
482
|
-
return
|
|
528
|
+
return c;
|
|
483
529
|
});
|
|
530
|
+
this._streamVersions.set(stream, version - 1);
|
|
531
|
+
if (lastNonSnapId >= 0) {
|
|
532
|
+
this._maxEventIdByStream.set(stream, lastNonSnapId);
|
|
533
|
+
this._maxNonSnapEventId = lastNonSnapId;
|
|
534
|
+
}
|
|
535
|
+
return committed;
|
|
484
536
|
}
|
|
485
537
|
/**
|
|
486
538
|
* Atomically discovers and leases streams for processing.
|
|
@@ -493,10 +545,26 @@ var InMemoryStore = class {
|
|
|
493
545
|
*/
|
|
494
546
|
async claim(lagging, leading, by, millis) {
|
|
495
547
|
await sleep();
|
|
548
|
+
const sourceRegex = /* @__PURE__ */ new Map();
|
|
549
|
+
const getRegex = (source) => {
|
|
550
|
+
let re = sourceRegex.get(source);
|
|
551
|
+
if (!re) {
|
|
552
|
+
re = new RegExp(source);
|
|
553
|
+
sourceRegex.set(source, re);
|
|
554
|
+
}
|
|
555
|
+
return re;
|
|
556
|
+
};
|
|
557
|
+
const hasWork = (s) => {
|
|
558
|
+
if (s.at < 0) return true;
|
|
559
|
+
if (!s.source) return s.at < this._maxNonSnapEventId;
|
|
560
|
+
const re = getRegex(s.source);
|
|
561
|
+
for (const [streamName, maxId] of this._maxEventIdByStream) {
|
|
562
|
+
if (maxId > s.at && re.test(streamName)) return true;
|
|
563
|
+
}
|
|
564
|
+
return false;
|
|
565
|
+
};
|
|
496
566
|
const available = [...this._streams.values()].filter(
|
|
497
|
-
(s) => s.is_available && (s
|
|
498
|
-
(e) => e.id > s.at && e.name !== SNAP_EVENT && (!s.source || RegExp(s.source).test(e.stream))
|
|
499
|
-
))
|
|
567
|
+
(s) => s.is_available && hasWork(s)
|
|
500
568
|
);
|
|
501
569
|
const lag = available.sort((a, b) => a.at - b.at).slice(0, lagging).map((s) => ({
|
|
502
570
|
stream: s.stream,
|
|
@@ -633,9 +701,13 @@ var InMemoryStore = class {
|
|
|
633
701
|
}
|
|
634
702
|
}
|
|
635
703
|
this._events = this._events.filter((e) => !streamSet.has(e.stream));
|
|
704
|
+
for (const stream of streamSet) {
|
|
705
|
+
this._streams.delete(stream);
|
|
706
|
+
this._streamVersions.delete(stream);
|
|
707
|
+
this._maxEventIdByStream.delete(stream);
|
|
708
|
+
}
|
|
636
709
|
const result = /* @__PURE__ */ new Map();
|
|
637
710
|
for (const { stream, snapshot, meta } of targets) {
|
|
638
|
-
this._streams.delete(stream);
|
|
639
711
|
const event = {
|
|
640
712
|
id: this._events.length,
|
|
641
713
|
stream,
|
|
@@ -646,11 +718,18 @@ var InMemoryStore = class {
|
|
|
646
718
|
meta: meta ?? { correlation: "", causation: {} }
|
|
647
719
|
};
|
|
648
720
|
this._events.push(event);
|
|
721
|
+
this._streamVersions.set(stream, 0);
|
|
722
|
+
if (event.name !== SNAP_EVENT) {
|
|
723
|
+
this._maxEventIdByStream.set(stream, event.id);
|
|
724
|
+
}
|
|
649
725
|
result.set(stream, {
|
|
650
726
|
deleted: deletedCounts.get(stream) ?? 0,
|
|
651
727
|
committed: event
|
|
652
728
|
});
|
|
653
729
|
}
|
|
730
|
+
let max = -1;
|
|
731
|
+
for (const id of this._maxEventIdByStream.values()) if (id > max) max = id;
|
|
732
|
+
this._maxNonSnapEventId = max;
|
|
654
733
|
return result;
|
|
655
734
|
}
|
|
656
735
|
};
|
|
@@ -683,7 +762,12 @@ var cache = port(function cache2(adapter) {
|
|
|
683
762
|
});
|
|
684
763
|
var disposers = [];
|
|
685
764
|
async function disposeAndExit(code = "EXIT") {
|
|
686
|
-
if (code === "ERROR" && config().env === "production")
|
|
765
|
+
if (code === "ERROR" && config().env === "production") {
|
|
766
|
+
log().warn(
|
|
767
|
+
"disposeAndExit('ERROR') ignored in production \u2014 process kept alive"
|
|
768
|
+
);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
687
771
|
for (const disposer of [...disposers].reverse()) {
|
|
688
772
|
await disposer();
|
|
689
773
|
}
|
|
@@ -725,7 +809,6 @@ import EventEmitter from "events";
|
|
|
725
809
|
// src/internal/close-cycle.ts
|
|
726
810
|
import { randomUUID } from "crypto";
|
|
727
811
|
async function runCloseCycle(targets, deps) {
|
|
728
|
-
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
729
812
|
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
730
813
|
const streams = [...targetMap.keys()];
|
|
731
814
|
const skipped = [];
|
|
@@ -768,22 +851,15 @@ async function scanStreamHeads(streams) {
|
|
|
768
851
|
streams.map(async (s) => {
|
|
769
852
|
let maxId = -1;
|
|
770
853
|
let version = -1;
|
|
771
|
-
let lastEventName;
|
|
854
|
+
let lastEventName = "";
|
|
772
855
|
await store().query(
|
|
773
856
|
(e) => {
|
|
774
|
-
if (e.name === TOMBSTONE_EVENT) return;
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
if (e.name !== SNAP_EVENT && lastEventName === void 0) {
|
|
780
|
-
lastEventName = e.name;
|
|
781
|
-
}
|
|
857
|
+
if (e.name === TOMBSTONE_EVENT || maxId !== -1) return;
|
|
858
|
+
maxId = e.id;
|
|
859
|
+
version = e.version;
|
|
860
|
+
lastEventName = e.name;
|
|
782
861
|
},
|
|
783
|
-
|
|
784
|
-
// always preceded by the domain event it captured). Streams with
|
|
785
|
-
// unusual layouts fall back to no-seed via the lookup miss path.
|
|
786
|
-
{ stream: s, stream_exact: true, backward: true, limit: 2 }
|
|
862
|
+
{ stream: s, stream_exact: true, backward: true, limit: 1 }
|
|
787
863
|
);
|
|
788
864
|
if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
|
|
789
865
|
})
|
|
@@ -829,11 +905,11 @@ async function loadRestartSeeds(guarded, targetMap, streamInfo, eventToState, lo
|
|
|
829
905
|
const seedStates = /* @__PURE__ */ new Map();
|
|
830
906
|
await Promise.all(
|
|
831
907
|
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
832
|
-
const lastEventName = streamInfo.get(stream)
|
|
833
|
-
const ownerState =
|
|
908
|
+
const lastEventName = streamInfo.get(stream).lastEventName;
|
|
909
|
+
const ownerState = eventToState.get(lastEventName);
|
|
834
910
|
if (!ownerState) {
|
|
835
911
|
logger.error(
|
|
836
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName
|
|
912
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName}". Stream will be tombstoned instead.`
|
|
837
913
|
);
|
|
838
914
|
return;
|
|
839
915
|
}
|
|
@@ -911,8 +987,8 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
911
987
|
const handled = await Promise.all(
|
|
912
988
|
leased.map((lease) => {
|
|
913
989
|
const entry = fetchMap.get(lease.stream);
|
|
914
|
-
const at = entry
|
|
915
|
-
const payloads = entry
|
|
990
|
+
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
991
|
+
const { payloads } = entry;
|
|
916
992
|
const batchHandler = batchHandlers.get(lease.stream);
|
|
917
993
|
if (batchHandler && payloads.length > 0) {
|
|
918
994
|
return handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
@@ -1208,8 +1284,8 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1208
1284
|
action: {
|
|
1209
1285
|
name: action2,
|
|
1210
1286
|
...target
|
|
1211
|
-
// payload
|
|
1212
|
-
//
|
|
1287
|
+
// payload intentionally omitted: it can be large or contain PII,
|
|
1288
|
+
// and callers correlate via the correlation id when they need it.
|
|
1213
1289
|
},
|
|
1214
1290
|
event: reactingTo ? {
|
|
1215
1291
|
id: reactingTo.id,
|
|
@@ -1224,7 +1300,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1224
1300
|
stream,
|
|
1225
1301
|
emitted,
|
|
1226
1302
|
meta,
|
|
1227
|
-
//
|
|
1303
|
+
// Reactions skip optimistic concurrency: they always append against the
|
|
1304
|
+
// current head. Stream leasing already serializes concurrent reactions,
|
|
1305
|
+
// and forcing version checks here would turn ordinary catch-up into
|
|
1306
|
+
// spurious retries.
|
|
1228
1307
|
reactingTo ? void 0 : expected
|
|
1229
1308
|
);
|
|
1230
1309
|
} catch (error) {
|