@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.js CHANGED
@@ -13,9 +13,9 @@ import {
13
13
  TargetSchema,
14
14
  ValidationError,
15
15
  ZodEmpty
16
- } from "./chunk-JBKZJXQZ.js";
16
+ } from "./chunk-IDEYGKT4.js";
17
17
 
18
- // src/adapters/ConsoleLogger.ts
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 = Object.fromEntries(Object.entries(objOrMsg));
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
- process.stdout.write(JSON.stringify(entry) + "\n");
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
- if (oldest !== void 0) this._entries.delete(oldest);
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/InMemoryCache.ts
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
- const pkg2 = fs.readFileSync("package.json");
221
- return JSON.parse(pkg2.toString());
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/InMemoryStore.ts
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 = 0;
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._events.length = 0;
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._events.length = 0;
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 instance = this._events.filter((e) => e.stream === stream);
461
- if (typeof expectedVersion === "number" && instance.length - 1 !== expectedVersion) {
504
+ const currentVersion = this._streamVersions.get(stream) ?? -1;
505
+ if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
462
506
  throw new ConcurrencyError(
463
507
  stream,
464
- instance.length - 1,
508
+ currentVersion,
465
509
  msgs,
466
510
  expectedVersion
467
511
  );
468
512
  }
469
- let version = instance.length;
470
- return msgs.map(({ name, data }) => {
471
- const committed = {
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(committed);
525
+ this._events.push(c);
526
+ if (name !== SNAP_EVENT) lastNonSnapId = c.id;
481
527
  version++;
482
- return committed;
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.at < 0 || this._events.some(
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") return;
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
- if (maxId === -1) {
776
- maxId = e.id;
777
- version = e.version;
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
- // limit: 2 covers the typical snapshot-at-head case (snapshot is
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)?.lastEventName;
833
- const ownerState = lastEventName ? eventToState.get(lastEventName) : void 0;
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 ?? "<none>"}". Stream will be tombstoned instead.`
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?.fetch.events.at(-1)?.id || fetch_window_at;
915
- const payloads = entry?.payloads ?? [];
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: TODO: flag to include action payload in metadata
1212
- // not included by default to avoid large payloads
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
- // TODO: review reactions not enforcing expected version
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) {