@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.
Files changed (32) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/adapters/{ConsoleLogger.d.ts → console-logger.d.ts} +2 -2
  3. package/dist/@types/adapters/console-logger.d.ts.map +1 -0
  4. package/dist/@types/adapters/{InMemoryCache.d.ts → in-memory-cache.d.ts} +1 -1
  5. package/dist/@types/adapters/in-memory-cache.d.ts.map +1 -0
  6. package/dist/@types/adapters/{InMemoryStore.d.ts → in-memory-store.d.ts} +5 -1
  7. package/dist/@types/adapters/in-memory-store.d.ts.map +1 -0
  8. package/dist/@types/adapters/index.d.ts +3 -3
  9. package/dist/@types/adapters/index.d.ts.map +1 -1
  10. package/dist/@types/config.d.ts.map +1 -1
  11. package/dist/@types/internal/close-cycle.d.ts.map +1 -1
  12. package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
  13. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  14. package/dist/@types/internal/lru-map.d.ts.map +1 -1
  15. package/dist/@types/ports.d.ts +1 -1
  16. package/dist/@types/ports.d.ts.map +1 -1
  17. package/dist/@types/types/errors.d.ts.map +1 -1
  18. package/dist/@types/utils.d.ts +27 -296
  19. package/dist/@types/utils.d.ts.map +1 -1
  20. package/dist/{chunk-JBKZJXQZ.js → chunk-IDEYGKT4.js} +2 -2
  21. package/dist/{chunk-JBKZJXQZ.js.map → chunk-IDEYGKT4.js.map} +1 -1
  22. package/dist/index.cjs +127 -48
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.js +127 -48
  25. package/dist/index.js.map +1 -1
  26. package/dist/types/index.cjs +1 -1
  27. package/dist/types/index.cjs.map +1 -1
  28. package/dist/types/index.js +1 -1
  29. package/package.json +1 -1
  30. package/dist/@types/adapters/ConsoleLogger.d.ts.map +0 -1
  31. package/dist/@types/adapters/InMemoryCache.d.ts.map +0 -1
  32. 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/ConsoleLogger.ts
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 = Object.fromEntries(Object.entries(objOrMsg));
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
- process.stdout.write(JSON.stringify(entry) + "\n");
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
- if (oldest !== void 0) this._entries.delete(oldest);
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/InMemoryCache.ts
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}.${JSON.stringify(e.data)}`).join(
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
- const pkg2 = fs.readFileSync("package.json");
388
- return JSON.parse(pkg2.toString());
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/InMemoryStore.ts
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 = 0;
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._events.length = 0;
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._events.length = 0;
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 instance = this._events.filter((e) => e.stream === stream);
628
- if (typeof expectedVersion === "number" && instance.length - 1 !== expectedVersion) {
671
+ const currentVersion = this._streamVersions.get(stream) ?? -1;
672
+ if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
629
673
  throw new ConcurrencyError(
630
674
  stream,
631
- instance.length - 1,
675
+ currentVersion,
632
676
  msgs,
633
677
  expectedVersion
634
678
  );
635
679
  }
636
- let version = instance.length;
637
- return msgs.map(({ name, data }) => {
638
- const committed = {
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(committed);
692
+ this._events.push(c);
693
+ if (name !== SNAP_EVENT) lastNonSnapId = c.id;
648
694
  version++;
649
- return committed;
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.at < 0 || this._events.some(
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") return;
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
- if (maxId === -1) {
943
- maxId = e.id;
944
- version = e.version;
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
- // limit: 2 covers the typical snapshot-at-head case (snapshot is
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)?.lastEventName;
1000
- const ownerState = lastEventName ? eventToState.get(lastEventName) : void 0;
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 ?? "<none>"}". Stream will be tombstoned instead.`
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?.fetch.events.at(-1)?.id || fetch_window_at;
1082
- const payloads = entry?.payloads ?? [];
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: TODO: flag to include action payload in metadata
1379
- // not included by default to avoid large payloads
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
- // TODO: review reactions not enforcing expected version
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) {