@rotorsoft/act 0.32.4 → 0.32.5
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/act.d.ts +65 -47
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/InMemoryCache.d.ts +1 -2
- package/dist/@types/adapters/InMemoryCache.d.ts.map +1 -1
- package/dist/@types/{act-builder.d.ts → builders/act-builder.d.ts} +5 -5
- package/dist/@types/builders/act-builder.d.ts.map +1 -0
- package/dist/@types/builders/index.d.ts +13 -0
- package/dist/@types/builders/index.d.ts.map +1 -0
- package/dist/@types/{projection-builder.d.ts → builders/projection-builder.d.ts} +3 -3
- package/dist/@types/builders/projection-builder.d.ts.map +1 -0
- package/dist/@types/{slice-builder.d.ts → builders/slice-builder.d.ts} +2 -2
- package/dist/@types/builders/slice-builder.d.ts.map +1 -0
- package/dist/@types/{state-builder.d.ts → builders/state-builder.d.ts} +1 -1
- package/dist/@types/builders/state-builder.d.ts.map +1 -0
- package/dist/@types/index.d.ts +1 -4
- package/dist/@types/index.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +38 -0
- package/dist/@types/internal/close-cycle.d.ts.map +1 -0
- package/dist/@types/internal/drain-cycle.d.ts +61 -0
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -0
- package/dist/@types/internal/drain-ratio.d.ts +26 -0
- package/dist/@types/internal/drain-ratio.d.ts.map +1 -0
- package/dist/@types/internal/event-sourcing.d.ts +14 -0
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +5 -1
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/lru-map.d.ts +50 -0
- package/dist/@types/internal/lru-map.d.ts.map +1 -0
- package/dist/@types/internal/merge.d.ts +13 -1
- package/dist/@types/internal/merge.d.ts.map +1 -1
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +7 -1
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/index.cjs +521 -394
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +520 -394
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/@types/act-builder.d.ts.map +0 -1
- package/dist/@types/projection-builder.d.ts.map +0 -1
- package/dist/@types/slice-builder.d.ts.map +0 -1
- package/dist/@types/state-builder.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -117,26 +117,75 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
117
117
|
}
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
+
// src/internal/lru-map.ts
|
|
121
|
+
var LruMap = class {
|
|
122
|
+
constructor(_maxSize) {
|
|
123
|
+
this._maxSize = _maxSize;
|
|
124
|
+
}
|
|
125
|
+
_entries = /* @__PURE__ */ new Map();
|
|
126
|
+
get(key) {
|
|
127
|
+
const v = this._entries.get(key);
|
|
128
|
+
if (v === void 0) return void 0;
|
|
129
|
+
this._entries.delete(key);
|
|
130
|
+
this._entries.set(key, v);
|
|
131
|
+
return v;
|
|
132
|
+
}
|
|
133
|
+
has(key) {
|
|
134
|
+
return this._entries.has(key);
|
|
135
|
+
}
|
|
136
|
+
set(key, value) {
|
|
137
|
+
this._entries.delete(key);
|
|
138
|
+
if (this._entries.size >= this._maxSize) {
|
|
139
|
+
const oldest = this._entries.keys().next().value;
|
|
140
|
+
if (oldest !== void 0) this._entries.delete(oldest);
|
|
141
|
+
}
|
|
142
|
+
this._entries.set(key, value);
|
|
143
|
+
}
|
|
144
|
+
delete(key) {
|
|
145
|
+
return this._entries.delete(key);
|
|
146
|
+
}
|
|
147
|
+
clear() {
|
|
148
|
+
this._entries.clear();
|
|
149
|
+
}
|
|
150
|
+
get size() {
|
|
151
|
+
return this._entries.size;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var LruSet = class {
|
|
155
|
+
_map;
|
|
156
|
+
constructor(maxSize) {
|
|
157
|
+
this._map = new LruMap(maxSize);
|
|
158
|
+
}
|
|
159
|
+
has(value) {
|
|
160
|
+
return this._map.has(value);
|
|
161
|
+
}
|
|
162
|
+
add(value) {
|
|
163
|
+
this._map.set(value, true);
|
|
164
|
+
}
|
|
165
|
+
delete(value) {
|
|
166
|
+
return this._map.delete(value);
|
|
167
|
+
}
|
|
168
|
+
clear() {
|
|
169
|
+
this._map.clear();
|
|
170
|
+
}
|
|
171
|
+
get size() {
|
|
172
|
+
return this._map.size;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
120
176
|
// src/adapters/InMemoryCache.ts
|
|
121
177
|
var InMemoryCache = class {
|
|
122
|
-
|
|
123
|
-
|
|
178
|
+
// CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
|
|
179
|
+
// any is bidirectionally compatible with the per-call TState binding, while
|
|
180
|
+
// the public Cache interface still presents a typed surface to callers.
|
|
181
|
+
_entries;
|
|
124
182
|
constructor(options) {
|
|
125
|
-
this.
|
|
183
|
+
this._entries = new LruMap(options?.maxSize ?? 1e3);
|
|
126
184
|
}
|
|
127
185
|
async get(stream) {
|
|
128
|
-
|
|
129
|
-
if (!entry) return void 0;
|
|
130
|
-
this._entries.delete(stream);
|
|
131
|
-
this._entries.set(stream, entry);
|
|
132
|
-
return entry;
|
|
186
|
+
return this._entries.get(stream);
|
|
133
187
|
}
|
|
134
188
|
async set(stream, entry) {
|
|
135
|
-
this._entries.delete(stream);
|
|
136
|
-
if (this._entries.size >= this._maxSize) {
|
|
137
|
-
const first = this._entries.keys().next().value;
|
|
138
|
-
this._entries.delete(first);
|
|
139
|
-
}
|
|
140
189
|
this._entries.set(stream, entry);
|
|
141
190
|
}
|
|
142
191
|
async invalidate(stream) {
|
|
@@ -671,9 +720,233 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
671
720
|
});
|
|
672
721
|
|
|
673
722
|
// src/act.ts
|
|
674
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
675
723
|
import EventEmitter from "events";
|
|
676
724
|
|
|
725
|
+
// src/internal/close-cycle.ts
|
|
726
|
+
import { randomUUID } from "crypto";
|
|
727
|
+
async function runCloseCycle(targets, deps) {
|
|
728
|
+
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
729
|
+
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
730
|
+
const streams = [...targetMap.keys()];
|
|
731
|
+
const skipped = [];
|
|
732
|
+
const streamInfo = await scanStreamHeads(streams);
|
|
733
|
+
const safe = await partitionBySafety(
|
|
734
|
+
streamInfo,
|
|
735
|
+
deps.reactiveEventsSize,
|
|
736
|
+
skipped
|
|
737
|
+
);
|
|
738
|
+
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
739
|
+
const correlation = randomUUID();
|
|
740
|
+
const { guarded, guardEvents } = await guardWithTombstones(
|
|
741
|
+
safe,
|
|
742
|
+
streamInfo,
|
|
743
|
+
correlation,
|
|
744
|
+
deps.tombstone,
|
|
745
|
+
skipped
|
|
746
|
+
);
|
|
747
|
+
if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
748
|
+
const seedStates = await loadRestartSeeds(
|
|
749
|
+
guarded,
|
|
750
|
+
targetMap,
|
|
751
|
+
streamInfo,
|
|
752
|
+
deps.eventToState,
|
|
753
|
+
deps.load,
|
|
754
|
+
deps.logger
|
|
755
|
+
);
|
|
756
|
+
await runArchiveCallbacks(guarded, targetMap);
|
|
757
|
+
const truncated = await truncateAndWarmCache(
|
|
758
|
+
guarded,
|
|
759
|
+
seedStates,
|
|
760
|
+
guardEvents,
|
|
761
|
+
correlation
|
|
762
|
+
);
|
|
763
|
+
return { truncated, skipped };
|
|
764
|
+
}
|
|
765
|
+
async function scanStreamHeads(streams) {
|
|
766
|
+
const out = /* @__PURE__ */ new Map();
|
|
767
|
+
await Promise.all(
|
|
768
|
+
streams.map(async (s) => {
|
|
769
|
+
let maxId = -1;
|
|
770
|
+
let version = -1;
|
|
771
|
+
let lastEventName;
|
|
772
|
+
await store().query(
|
|
773
|
+
(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
|
+
}
|
|
782
|
+
},
|
|
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 }
|
|
787
|
+
);
|
|
788
|
+
if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
|
|
789
|
+
})
|
|
790
|
+
);
|
|
791
|
+
return out;
|
|
792
|
+
}
|
|
793
|
+
async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
|
|
794
|
+
if (reactiveEventsSize === 0) return [...streamInfo.keys()];
|
|
795
|
+
const pendingSet = /* @__PURE__ */ new Set();
|
|
796
|
+
await store().query_streams((position) => {
|
|
797
|
+
const sourceRe = position.source ? RegExp(position.source) : void 0;
|
|
798
|
+
for (const [stream, info] of streamInfo) {
|
|
799
|
+
if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
|
|
800
|
+
pendingSet.add(stream);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
const safe = [];
|
|
805
|
+
for (const [stream] of streamInfo) {
|
|
806
|
+
if (pendingSet.has(stream)) skipped.push(stream);
|
|
807
|
+
else safe.push(stream);
|
|
808
|
+
}
|
|
809
|
+
return safe;
|
|
810
|
+
}
|
|
811
|
+
async function guardWithTombstones(safe, streamInfo, correlation, tombstone2, skipped) {
|
|
812
|
+
const guarded = [];
|
|
813
|
+
const guardEvents = /* @__PURE__ */ new Map();
|
|
814
|
+
await Promise.all(
|
|
815
|
+
safe.map(async (stream) => {
|
|
816
|
+
const info = streamInfo.get(stream);
|
|
817
|
+
const committed = await tombstone2(stream, info.version, correlation);
|
|
818
|
+
if (committed) {
|
|
819
|
+
guarded.push(stream);
|
|
820
|
+
guardEvents.set(stream, { id: committed.id, stream });
|
|
821
|
+
} else {
|
|
822
|
+
skipped.push(stream);
|
|
823
|
+
}
|
|
824
|
+
})
|
|
825
|
+
);
|
|
826
|
+
return { guarded, guardEvents };
|
|
827
|
+
}
|
|
828
|
+
async function loadRestartSeeds(guarded, targetMap, streamInfo, eventToState, load2, logger) {
|
|
829
|
+
const seedStates = /* @__PURE__ */ new Map();
|
|
830
|
+
await Promise.all(
|
|
831
|
+
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;
|
|
834
|
+
if (!ownerState) {
|
|
835
|
+
logger.error(
|
|
836
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName ?? "<none>"}". Stream will be tombstoned instead.`
|
|
837
|
+
);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const snap2 = await load2(ownerState, stream);
|
|
841
|
+
seedStates.set(stream, snap2.state);
|
|
842
|
+
})
|
|
843
|
+
);
|
|
844
|
+
return seedStates;
|
|
845
|
+
}
|
|
846
|
+
async function runArchiveCallbacks(guarded, targetMap) {
|
|
847
|
+
for (const stream of guarded) {
|
|
848
|
+
const archiveFn = targetMap.get(stream)?.archive;
|
|
849
|
+
if (archiveFn) await archiveFn();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlation) {
|
|
853
|
+
const truncTargets = guarded.map((stream) => {
|
|
854
|
+
const snapshot = seedStates.get(stream);
|
|
855
|
+
const guard = guardEvents.get(stream);
|
|
856
|
+
return {
|
|
857
|
+
stream,
|
|
858
|
+
snapshot,
|
|
859
|
+
meta: {
|
|
860
|
+
correlation,
|
|
861
|
+
causation: {
|
|
862
|
+
event: { id: guard.id, name: TOMBSTONE_EVENT, stream: guard.stream }
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
});
|
|
867
|
+
const truncated = await store().truncate(truncTargets);
|
|
868
|
+
await Promise.all(
|
|
869
|
+
guarded.map(async (stream) => {
|
|
870
|
+
const entry = truncated.get(stream);
|
|
871
|
+
const state2 = seedStates.get(stream);
|
|
872
|
+
if (state2 && entry) {
|
|
873
|
+
await cache().set(stream, {
|
|
874
|
+
state: state2,
|
|
875
|
+
version: entry.committed.version,
|
|
876
|
+
event_id: entry.committed.id,
|
|
877
|
+
patches: 0,
|
|
878
|
+
snaps: 1
|
|
879
|
+
});
|
|
880
|
+
} else {
|
|
881
|
+
await cache().invalidate(stream);
|
|
882
|
+
}
|
|
883
|
+
})
|
|
884
|
+
);
|
|
885
|
+
return truncated;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/internal/drain-cycle.ts
|
|
889
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
890
|
+
async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis) {
|
|
891
|
+
const leased = await ops.claim(lagging, leading, randomUUID2(), leaseMillis);
|
|
892
|
+
if (!leased.length) return void 0;
|
|
893
|
+
const fetched = await ops.fetch(leased, eventLimit);
|
|
894
|
+
const fetchMap = /* @__PURE__ */ new Map();
|
|
895
|
+
const fetch_window_at = fetched.reduce(
|
|
896
|
+
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
897
|
+
0
|
|
898
|
+
);
|
|
899
|
+
for (const f of fetched) {
|
|
900
|
+
const { stream, events } = f;
|
|
901
|
+
const payloads = events.flatMap((event) => {
|
|
902
|
+
const register = registry.events[event.name];
|
|
903
|
+
if (!register) return [];
|
|
904
|
+
return [...register.reactions.values()].filter((reaction) => {
|
|
905
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
906
|
+
return resolved && resolved.target === stream;
|
|
907
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
908
|
+
});
|
|
909
|
+
fetchMap.set(stream, { fetch: f, payloads });
|
|
910
|
+
}
|
|
911
|
+
const handled = await Promise.all(
|
|
912
|
+
leased.map((lease) => {
|
|
913
|
+
const entry = fetchMap.get(lease.stream);
|
|
914
|
+
const at = entry?.fetch.events.at(-1)?.id || fetch_window_at;
|
|
915
|
+
const payloads = entry?.payloads ?? [];
|
|
916
|
+
const batchHandler = batchHandlers.get(lease.stream);
|
|
917
|
+
if (batchHandler && payloads.length > 0) {
|
|
918
|
+
return handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
919
|
+
}
|
|
920
|
+
return handle({ ...lease, at }, payloads);
|
|
921
|
+
})
|
|
922
|
+
);
|
|
923
|
+
const acked = await ops.ack(
|
|
924
|
+
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
925
|
+
);
|
|
926
|
+
const blocked = await ops.block(
|
|
927
|
+
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
928
|
+
);
|
|
929
|
+
return { leased, fetched, handled, acked, blocked };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/internal/drain-ratio.ts
|
|
933
|
+
var RATIO_MIN = 0.2;
|
|
934
|
+
var RATIO_MAX = 0.8;
|
|
935
|
+
var RATIO_DEFAULT = 0.5;
|
|
936
|
+
function computeLagLeadRatio(handled, lagging, leading) {
|
|
937
|
+
let lagging_handled = 0;
|
|
938
|
+
let leading_handled = 0;
|
|
939
|
+
for (const { lease, handled: count } of handled) {
|
|
940
|
+
if (lease.lagging) lagging_handled += count;
|
|
941
|
+
else leading_handled += count;
|
|
942
|
+
}
|
|
943
|
+
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
944
|
+
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
945
|
+
const total = lagging_avg + leading_avg;
|
|
946
|
+
if (total === 0) return RATIO_DEFAULT;
|
|
947
|
+
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
948
|
+
}
|
|
949
|
+
|
|
677
950
|
// src/internal/merge.ts
|
|
678
951
|
import { ZodObject } from "zod";
|
|
679
952
|
function baseTypeName(zodType) {
|
|
@@ -781,6 +1054,15 @@ function mergePatches(existing, incoming, stateName) {
|
|
|
781
1054
|
}
|
|
782
1055
|
return merged;
|
|
783
1056
|
}
|
|
1057
|
+
function mergeEventRegister(target, source) {
|
|
1058
|
+
for (const [eventName, sourceReg] of Object.entries(source)) {
|
|
1059
|
+
const targetReg = target[eventName];
|
|
1060
|
+
if (!targetReg) continue;
|
|
1061
|
+
for (const [name, reaction] of sourceReg.reactions) {
|
|
1062
|
+
targetReg.reactions.set(name, reaction);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
784
1066
|
function mergeProjection(proj, events) {
|
|
785
1067
|
for (const eventName of Object.keys(proj.events)) {
|
|
786
1068
|
const projRegister = proj.events[eventName];
|
|
@@ -825,7 +1107,7 @@ var subscribe = (streams) => store().subscribe(streams);
|
|
|
825
1107
|
|
|
826
1108
|
// src/internal/event-sourcing.ts
|
|
827
1109
|
import { patch } from "@rotorsoft/act-patch";
|
|
828
|
-
import { randomUUID } from "crypto";
|
|
1110
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
829
1111
|
async function snap(snapshot) {
|
|
830
1112
|
try {
|
|
831
1113
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
@@ -843,6 +1125,20 @@ async function snap(snapshot) {
|
|
|
843
1125
|
log().error(error);
|
|
844
1126
|
}
|
|
845
1127
|
}
|
|
1128
|
+
async function tombstone(stream, expectedVersion, correlation) {
|
|
1129
|
+
try {
|
|
1130
|
+
const [committed] = await store().commit(
|
|
1131
|
+
stream,
|
|
1132
|
+
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
1133
|
+
{ correlation, causation: {} },
|
|
1134
|
+
expectedVersion
|
|
1135
|
+
);
|
|
1136
|
+
return committed;
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
if (error instanceof ConcurrencyError) return void 0;
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
846
1142
|
async function load(me, stream, callback, asOf) {
|
|
847
1143
|
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
848
1144
|
const cached = timeTravel ? void 0 : await cache().get(stream);
|
|
@@ -907,7 +1203,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
907
1203
|
data: skipValidation ? data : validate(name, data, me.events[name])
|
|
908
1204
|
}));
|
|
909
1205
|
const meta = {
|
|
910
|
-
correlation: reactingTo?.meta.correlation ||
|
|
1206
|
+
correlation: reactingTo?.meta.correlation || randomUUID3(),
|
|
911
1207
|
causation: {
|
|
912
1208
|
action: {
|
|
913
1209
|
name: action2,
|
|
@@ -978,7 +1274,12 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
978
1274
|
});
|
|
979
1275
|
function buildEs(logger) {
|
|
980
1276
|
if (logger.level !== "trace") {
|
|
981
|
-
return {
|
|
1277
|
+
return {
|
|
1278
|
+
snap,
|
|
1279
|
+
load,
|
|
1280
|
+
action,
|
|
1281
|
+
tombstone
|
|
1282
|
+
};
|
|
982
1283
|
}
|
|
983
1284
|
return {
|
|
984
1285
|
snap: traced(snap, void 0, (snapshot) => {
|
|
@@ -1016,7 +1317,13 @@ function buildEs(logger) {
|
|
|
1016
1317
|
es_caption("action", C_BLUE, `${target.stream}.${action2}`)
|
|
1017
1318
|
);
|
|
1018
1319
|
}
|
|
1019
|
-
)
|
|
1320
|
+
),
|
|
1321
|
+
tombstone: traced(tombstone, (committed, stream) => {
|
|
1322
|
+
if (committed)
|
|
1323
|
+
logger.trace(
|
|
1324
|
+
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
1325
|
+
);
|
|
1326
|
+
})
|
|
1020
1327
|
};
|
|
1021
1328
|
}
|
|
1022
1329
|
function buildDrain(logger) {
|
|
@@ -1079,11 +1386,15 @@ function buildDrain(logger) {
|
|
|
1079
1386
|
}
|
|
1080
1387
|
|
|
1081
1388
|
// src/act.ts
|
|
1389
|
+
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
1082
1390
|
var Act = class {
|
|
1083
|
-
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map()) {
|
|
1391
|
+
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
|
|
1084
1392
|
this.registry = registry;
|
|
1085
1393
|
this._states = _states;
|
|
1086
1394
|
this._batch_handlers = batchHandlers;
|
|
1395
|
+
this._subscribed_streams = new LruSet(
|
|
1396
|
+
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS
|
|
1397
|
+
);
|
|
1087
1398
|
this._es = buildEs(this._logger);
|
|
1088
1399
|
this._cd = buildDrain(this._logger);
|
|
1089
1400
|
const statics = /* @__PURE__ */ new Map();
|
|
@@ -1121,20 +1432,42 @@ var Act = class {
|
|
|
1121
1432
|
_settle_timer = void 0;
|
|
1122
1433
|
_settling = false;
|
|
1123
1434
|
_correlation_checkpoint = -1;
|
|
1124
|
-
|
|
1435
|
+
/**
|
|
1436
|
+
* Streams already subscribed via store.subscribe() — both the static
|
|
1437
|
+
* targets registered at init and dynamic targets discovered by
|
|
1438
|
+
* correlate(). correlate() consults this set to avoid re-subscribing
|
|
1439
|
+
* known streams.
|
|
1440
|
+
*
|
|
1441
|
+
* Bounded LRU so apps that mint millions of dynamic targets (one per
|
|
1442
|
+
* aggregate) don't grow this unbounded. Eviction costs at most one
|
|
1443
|
+
* redundant store.subscribe() call per evicted-but-still-active stream
|
|
1444
|
+
* (subscribe is idempotent). Cap configurable via {@link ActOptions}.
|
|
1445
|
+
*/
|
|
1446
|
+
_subscribed_streams;
|
|
1125
1447
|
_has_dynamic_resolvers = false;
|
|
1126
1448
|
_correlation_initialized = false;
|
|
1127
1449
|
/** Event names with at least one registered reaction (computed at build time) */
|
|
1128
1450
|
_reactive_events = /* @__PURE__ */ new Set();
|
|
1129
1451
|
/** Set in do() when a committed event has reactions — cleared by drain() */
|
|
1130
1452
|
_needs_drain = false;
|
|
1453
|
+
/**
|
|
1454
|
+
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
1455
|
+
* via {@link ActLifecycleEvents}.
|
|
1456
|
+
*/
|
|
1131
1457
|
emit(event, args) {
|
|
1132
1458
|
return this._emitter.emit(event, args);
|
|
1133
1459
|
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Register a listener for a lifecycle event. The listener receives the
|
|
1462
|
+
* event-specific payload.
|
|
1463
|
+
*/
|
|
1134
1464
|
on(event, listener) {
|
|
1135
1465
|
this._emitter.on(event, listener);
|
|
1136
1466
|
return this;
|
|
1137
1467
|
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Remove a previously registered lifecycle listener.
|
|
1470
|
+
*/
|
|
1138
1471
|
off(event, listener) {
|
|
1139
1472
|
this._emitter.off(event, listener);
|
|
1140
1473
|
return this;
|
|
@@ -1168,6 +1501,9 @@ var Act = class {
|
|
|
1168
1501
|
_bound_load = this.load.bind(this);
|
|
1169
1502
|
_bound_query = this.query.bind(this);
|
|
1170
1503
|
_bound_query_array = this.query_array.bind(this);
|
|
1504
|
+
/** Pre-bound dispatchers handed to runDrainCycle each cycle. */
|
|
1505
|
+
_bound_handle = this.handle.bind(this);
|
|
1506
|
+
_bound_handle_batch = this.handleBatch.bind(this);
|
|
1171
1507
|
/**
|
|
1172
1508
|
* Executes an action on a state instance, committing resulting events.
|
|
1173
1509
|
*
|
|
@@ -1370,26 +1706,46 @@ var Act = class {
|
|
|
1370
1706
|
return events;
|
|
1371
1707
|
}
|
|
1372
1708
|
/**
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1709
|
+
* Shared finalization for the two reaction-runner shapes (per-event
|
|
1710
|
+
* `handle` and bulk `handleBatch`). Centralizes the error log, retry-vs-
|
|
1711
|
+
* block decision, and the "error reported only when nothing was handled"
|
|
1712
|
+
* rule that's true in both shapes (in batch mode, `handled` is always 0
|
|
1713
|
+
* on failure, so the rule degenerates to "always reported").
|
|
1714
|
+
*/
|
|
1715
|
+
_finalize(lease, handled, at, error, options) {
|
|
1716
|
+
if (!error) return { lease, handled, at };
|
|
1717
|
+
this._logger.error(error);
|
|
1718
|
+
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1719
|
+
if (block2)
|
|
1720
|
+
this._logger.error(
|
|
1721
|
+
`Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1722
|
+
);
|
|
1723
|
+
return {
|
|
1724
|
+
lease,
|
|
1725
|
+
handled,
|
|
1726
|
+
at,
|
|
1727
|
+
error: handled === 0 ? error.message : void 0,
|
|
1728
|
+
block: block2
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Handles leased reactions one event at a time.
|
|
1377
1733
|
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1734
|
+
* Called by the main `drain` loop after fetching new events. Each handler
|
|
1735
|
+
* receives a scoped `IAct` proxy that auto-injects the triggering event
|
|
1736
|
+
* as `reactingTo` when `do()` is called without it, maintaining
|
|
1737
|
+
* correlation chains by default (#587). Handlers can still pass an
|
|
1738
|
+
* explicit `reactingTo` to override.
|
|
1382
1739
|
*
|
|
1383
1740
|
* @internal
|
|
1384
|
-
* @param lease The lease to handle
|
|
1385
|
-
* @param payloads The reactions to handle
|
|
1386
|
-
* @returns The lease with results
|
|
1387
1741
|
*/
|
|
1388
1742
|
async handle(lease, payloads) {
|
|
1389
1743
|
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1390
1744
|
const stream = lease.stream;
|
|
1391
|
-
let at = payloads.at(0).event.id
|
|
1392
|
-
|
|
1745
|
+
let at = payloads.at(0).event.id;
|
|
1746
|
+
let handled = 0;
|
|
1747
|
+
if (lease.retry > 0)
|
|
1748
|
+
this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1393
1749
|
const doAction = this._bound_do;
|
|
1394
1750
|
const scopedApp = {
|
|
1395
1751
|
do: doAction,
|
|
@@ -1398,7 +1754,7 @@ var Act = class {
|
|
|
1398
1754
|
query_array: this._bound_query_array
|
|
1399
1755
|
};
|
|
1400
1756
|
for (const payload of payloads) {
|
|
1401
|
-
const { event, handler
|
|
1757
|
+
const { event, handler } = payload;
|
|
1402
1758
|
scopedApp.do = (action2, target, payload2, reactingTo, skipValidation) => doAction(
|
|
1403
1759
|
action2,
|
|
1404
1760
|
target,
|
|
@@ -1411,22 +1767,16 @@ var Act = class {
|
|
|
1411
1767
|
at = event.id;
|
|
1412
1768
|
handled++;
|
|
1413
1769
|
} catch (error) {
|
|
1414
|
-
this.
|
|
1415
|
-
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1416
|
-
block2 && this._logger.error(
|
|
1417
|
-
`Blocking ${stream} after ${lease.retry} retries.`
|
|
1418
|
-
);
|
|
1419
|
-
return {
|
|
1770
|
+
return this._finalize(
|
|
1420
1771
|
lease,
|
|
1421
1772
|
handled,
|
|
1422
1773
|
at,
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
};
|
|
1774
|
+
error,
|
|
1775
|
+
payload.options
|
|
1776
|
+
);
|
|
1427
1777
|
}
|
|
1428
1778
|
}
|
|
1429
|
-
return
|
|
1779
|
+
return this._finalize(lease, handled, at, void 0, payloads[0].options);
|
|
1430
1780
|
}
|
|
1431
1781
|
/**
|
|
1432
1782
|
* Handles a batch of events for a projection with a batch handler.
|
|
@@ -1436,33 +1786,26 @@ var Act = class {
|
|
|
1436
1786
|
* in a single call, enabling bulk DB operations.
|
|
1437
1787
|
*
|
|
1438
1788
|
* @internal
|
|
1439
|
-
* @param lease The lease to handle
|
|
1440
|
-
* @param payloads The reactions to handle
|
|
1441
|
-
* @param batchHandler The batch handler for this projection
|
|
1442
|
-
* @returns The lease with results
|
|
1443
1789
|
*/
|
|
1444
1790
|
async handleBatch(lease, payloads, batchHandler) {
|
|
1445
1791
|
const stream = lease.stream;
|
|
1446
1792
|
const events = payloads.map((p) => p.event);
|
|
1447
|
-
const
|
|
1448
|
-
lease.retry > 0
|
|
1449
|
-
|
|
1450
|
-
|
|
1793
|
+
const options = payloads[0].options;
|
|
1794
|
+
if (lease.retry > 0)
|
|
1795
|
+
this._logger.warn(
|
|
1796
|
+
`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
|
|
1797
|
+
);
|
|
1451
1798
|
try {
|
|
1452
1799
|
await batchHandler(events, stream);
|
|
1453
|
-
return
|
|
1454
|
-
} catch (error) {
|
|
1455
|
-
this._logger.error(error);
|
|
1456
|
-
const { options } = payloads[0];
|
|
1457
|
-
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1458
|
-
block2 && this._logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
|
|
1459
|
-
return {
|
|
1800
|
+
return this._finalize(
|
|
1460
1801
|
lease,
|
|
1461
|
-
|
|
1462
|
-
at
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1802
|
+
events.length,
|
|
1803
|
+
events.at(-1).id,
|
|
1804
|
+
void 0,
|
|
1805
|
+
options
|
|
1806
|
+
);
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
return this._finalize(lease, 0, lease.at, error, options);
|
|
1466
1809
|
}
|
|
1467
1810
|
}
|
|
1468
1811
|
/**
|
|
@@ -1512,82 +1855,46 @@ var Act = class {
|
|
|
1512
1855
|
if (!this._needs_drain) {
|
|
1513
1856
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1514
1857
|
}
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
for (const f of fetched) {
|
|
1537
|
-
const { stream, events } = f;
|
|
1538
|
-
const payloads = events.flatMap((event) => {
|
|
1539
|
-
const register = this.registry.events[event.name];
|
|
1540
|
-
if (!register) return [];
|
|
1541
|
-
return [...register.reactions.values()].filter((reaction) => {
|
|
1542
|
-
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1543
|
-
return resolved && resolved.target === stream;
|
|
1544
|
-
}).map((reaction) => ({ ...reaction, event }));
|
|
1545
|
-
});
|
|
1546
|
-
fetchMap.set(stream, { fetch: f, payloads });
|
|
1547
|
-
}
|
|
1548
|
-
const handled = await Promise.all(
|
|
1549
|
-
leased.map((lease) => {
|
|
1550
|
-
const entry = fetchMap.get(lease.stream);
|
|
1551
|
-
const at = entry?.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1552
|
-
const payloads = entry?.payloads ?? [];
|
|
1553
|
-
const batchHandler = this._batch_handlers.get(lease.stream);
|
|
1554
|
-
if (batchHandler && payloads.length > 0) {
|
|
1555
|
-
return this.handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
1556
|
-
}
|
|
1557
|
-
return this.handle({ ...lease, at }, payloads);
|
|
1558
|
-
})
|
|
1559
|
-
);
|
|
1560
|
-
const [lagging_handled, leading_handled] = handled.reduce(
|
|
1561
|
-
([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
|
|
1562
|
-
lagging_handled2 + (lease.lagging ? handled2 : 0),
|
|
1563
|
-
leading_handled2 + (lease.lagging ? 0 : handled2)
|
|
1564
|
-
],
|
|
1565
|
-
[0, 0]
|
|
1566
|
-
);
|
|
1567
|
-
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1568
|
-
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1569
|
-
const total = lagging_avg + leading_avg;
|
|
1570
|
-
this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
|
|
1571
|
-
const acked = await this._cd.ack(
|
|
1572
|
-
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1573
|
-
);
|
|
1574
|
-
if (acked.length) this.emit("acked", acked);
|
|
1575
|
-
const blocked = await this._cd.block(
|
|
1576
|
-
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
1577
|
-
);
|
|
1578
|
-
if (blocked.length) this.emit("blocked", blocked);
|
|
1579
|
-
const result = { fetched, leased, acked, blocked };
|
|
1580
|
-
const hasErrors = handled.some(({ error }) => error);
|
|
1581
|
-
if (!acked.length && !blocked.length && !hasErrors)
|
|
1582
|
-
this._needs_drain = false;
|
|
1583
|
-
return result;
|
|
1584
|
-
} catch (error) {
|
|
1585
|
-
this._logger.error(error);
|
|
1586
|
-
} finally {
|
|
1587
|
-
this._drain_locked = false;
|
|
1858
|
+
if (this._drain_locked) {
|
|
1859
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1860
|
+
}
|
|
1861
|
+
try {
|
|
1862
|
+
this._drain_locked = true;
|
|
1863
|
+
const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
|
|
1864
|
+
const leading = streamLimit - lagging;
|
|
1865
|
+
const cycle = await runDrainCycle(
|
|
1866
|
+
this._cd,
|
|
1867
|
+
this.registry,
|
|
1868
|
+
this._batch_handlers,
|
|
1869
|
+
this._bound_handle,
|
|
1870
|
+
this._bound_handle_batch,
|
|
1871
|
+
lagging,
|
|
1872
|
+
leading,
|
|
1873
|
+
eventLimit,
|
|
1874
|
+
leaseMillis
|
|
1875
|
+
);
|
|
1876
|
+
if (!cycle) {
|
|
1877
|
+
this._needs_drain = false;
|
|
1878
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1588
1879
|
}
|
|
1880
|
+
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1881
|
+
this._drain_lag2lead_ratio = computeLagLeadRatio(
|
|
1882
|
+
handled,
|
|
1883
|
+
lagging,
|
|
1884
|
+
leading
|
|
1885
|
+
);
|
|
1886
|
+
if (acked.length) this.emit("acked", acked);
|
|
1887
|
+
if (blocked.length) this.emit("blocked", blocked);
|
|
1888
|
+
const hasErrors = handled.some(({ error }) => error);
|
|
1889
|
+
if (!acked.length && !blocked.length && !hasErrors)
|
|
1890
|
+
this._needs_drain = false;
|
|
1891
|
+
return { fetched, leased, acked, blocked };
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
this._logger.error(error);
|
|
1894
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1895
|
+
} finally {
|
|
1896
|
+
this._drain_locked = false;
|
|
1589
1897
|
}
|
|
1590
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1591
1898
|
}
|
|
1592
1899
|
/**
|
|
1593
1900
|
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
@@ -1648,7 +1955,7 @@ var Act = class {
|
|
|
1648
1955
|
this._correlation_checkpoint = watermark;
|
|
1649
1956
|
if (this._reactive_events.size > 0) this._needs_drain = true;
|
|
1650
1957
|
for (const { stream } of this._static_targets) {
|
|
1651
|
-
this.
|
|
1958
|
+
this._subscribed_streams.add(stream);
|
|
1652
1959
|
}
|
|
1653
1960
|
}
|
|
1654
1961
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
@@ -1666,7 +1973,7 @@ var Act = class {
|
|
|
1666
1973
|
for (const reaction of register.reactions.values()) {
|
|
1667
1974
|
if (typeof reaction.resolver !== "function") continue;
|
|
1668
1975
|
const resolved = reaction.resolver(event);
|
|
1669
|
-
if (resolved && !this.
|
|
1976
|
+
if (resolved && !this._subscribed_streams.has(resolved.target)) {
|
|
1670
1977
|
const entry = correlated.get(resolved.target) || {
|
|
1671
1978
|
source: resolved.source,
|
|
1672
1979
|
payloads: []
|
|
@@ -1692,7 +1999,7 @@ var Act = class {
|
|
|
1692
1999
|
this._correlation_checkpoint = last_id;
|
|
1693
2000
|
if (subscribed) {
|
|
1694
2001
|
for (const { stream } of streams) {
|
|
1695
|
-
this.
|
|
2002
|
+
this._subscribed_streams.add(stream);
|
|
1696
2003
|
}
|
|
1697
2004
|
}
|
|
1698
2005
|
return { subscribed, last_id };
|
|
@@ -1875,143 +2182,14 @@ var Act = class {
|
|
|
1875
2182
|
*/
|
|
1876
2183
|
async close(targets) {
|
|
1877
2184
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
1878
|
-
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
1879
|
-
const streams = [...targetMap.keys()];
|
|
1880
2185
|
await this.correlate({ limit: 1e3 });
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
await store().query(
|
|
1888
|
-
(e) => {
|
|
1889
|
-
if (e.name === TOMBSTONE_EVENT) return;
|
|
1890
|
-
if (maxId === -1) {
|
|
1891
|
-
maxId = e.id;
|
|
1892
|
-
version = e.version;
|
|
1893
|
-
}
|
|
1894
|
-
if (e.name !== SNAP_EVENT && lastEventName === void 0) {
|
|
1895
|
-
lastEventName = e.name;
|
|
1896
|
-
}
|
|
1897
|
-
},
|
|
1898
|
-
// limit: 2 covers the typical snapshot-at-head case (snapshot is
|
|
1899
|
-
// always preceded by the domain event it captured). Streams with
|
|
1900
|
-
// unusual layouts fall back to no-seed via the lookup miss path.
|
|
1901
|
-
{ stream: s, stream_exact: true, backward: true, limit: 2 }
|
|
1902
|
-
);
|
|
1903
|
-
if (maxId >= 0) streamInfo.set(s, { maxId, version, lastEventName });
|
|
1904
|
-
})
|
|
1905
|
-
);
|
|
1906
|
-
const skipped = [];
|
|
1907
|
-
let safe;
|
|
1908
|
-
if (this._reactive_events.size === 0) {
|
|
1909
|
-
safe = [...streamInfo.keys()];
|
|
1910
|
-
} else {
|
|
1911
|
-
const pendingSet = /* @__PURE__ */ new Set();
|
|
1912
|
-
await store().query_streams((position) => {
|
|
1913
|
-
const sourceRe = position.source ? RegExp(position.source) : void 0;
|
|
1914
|
-
for (const [stream, info] of streamInfo) {
|
|
1915
|
-
if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
|
|
1916
|
-
pendingSet.add(stream);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
});
|
|
1920
|
-
safe = [];
|
|
1921
|
-
for (const [stream] of streamInfo) {
|
|
1922
|
-
if (pendingSet.has(stream)) {
|
|
1923
|
-
skipped.push(stream);
|
|
1924
|
-
} else {
|
|
1925
|
-
safe.push(stream);
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
if (!safe.length) {
|
|
1930
|
-
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1931
|
-
this.emit("closed", result2);
|
|
1932
|
-
return result2;
|
|
1933
|
-
}
|
|
1934
|
-
const correlation = randomUUID2();
|
|
1935
|
-
const guarded = [];
|
|
1936
|
-
const guardEvents = /* @__PURE__ */ new Map();
|
|
1937
|
-
await Promise.all(
|
|
1938
|
-
safe.map(async (stream) => {
|
|
1939
|
-
try {
|
|
1940
|
-
const info = streamInfo.get(stream);
|
|
1941
|
-
const [committed] = await store().commit(
|
|
1942
|
-
stream,
|
|
1943
|
-
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
1944
|
-
{ correlation, causation: {} },
|
|
1945
|
-
info.version
|
|
1946
|
-
);
|
|
1947
|
-
guarded.push(stream);
|
|
1948
|
-
guardEvents.set(stream, { id: committed.id, stream });
|
|
1949
|
-
} catch {
|
|
1950
|
-
skipped.push(stream);
|
|
1951
|
-
}
|
|
1952
|
-
})
|
|
1953
|
-
);
|
|
1954
|
-
if (!guarded.length) {
|
|
1955
|
-
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1956
|
-
this.emit("closed", result2);
|
|
1957
|
-
return result2;
|
|
1958
|
-
}
|
|
1959
|
-
const seedStates = /* @__PURE__ */ new Map();
|
|
1960
|
-
await Promise.all(
|
|
1961
|
-
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
1962
|
-
const lastEventName = streamInfo.get(stream)?.lastEventName;
|
|
1963
|
-
const ownerState = lastEventName ? this._event_to_state.get(lastEventName) : void 0;
|
|
1964
|
-
if (!ownerState) {
|
|
1965
|
-
this._logger.error(
|
|
1966
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName ?? "<none>"}". Stream will be tombstoned instead.`
|
|
1967
|
-
);
|
|
1968
|
-
return;
|
|
1969
|
-
}
|
|
1970
|
-
const snap2 = await this._es.load(ownerState, stream);
|
|
1971
|
-
seedStates.set(stream, snap2.state);
|
|
1972
|
-
})
|
|
1973
|
-
);
|
|
1974
|
-
for (const stream of guarded) {
|
|
1975
|
-
const archiveFn = targetMap.get(stream)?.archive;
|
|
1976
|
-
if (archiveFn) await archiveFn();
|
|
1977
|
-
}
|
|
1978
|
-
const truncTargets = guarded.map((stream) => {
|
|
1979
|
-
const snapshot = seedStates.get(stream);
|
|
1980
|
-
const guard = guardEvents.get(stream);
|
|
1981
|
-
return {
|
|
1982
|
-
stream,
|
|
1983
|
-
snapshot,
|
|
1984
|
-
meta: {
|
|
1985
|
-
correlation,
|
|
1986
|
-
causation: {
|
|
1987
|
-
event: {
|
|
1988
|
-
id: guard.id,
|
|
1989
|
-
name: TOMBSTONE_EVENT,
|
|
1990
|
-
stream: guard.stream
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
};
|
|
2186
|
+
const result = await runCloseCycle(targets, {
|
|
2187
|
+
reactiveEventsSize: this._reactive_events.size,
|
|
2188
|
+
eventToState: this._event_to_state,
|
|
2189
|
+
load: this._es.load,
|
|
2190
|
+
tombstone: this._es.tombstone,
|
|
2191
|
+
logger: this._logger
|
|
1995
2192
|
});
|
|
1996
|
-
const truncated = await store().truncate(truncTargets);
|
|
1997
|
-
await Promise.all(
|
|
1998
|
-
guarded.map(async (stream) => {
|
|
1999
|
-
const entry = truncated.get(stream);
|
|
2000
|
-
const state2 = seedStates.get(stream);
|
|
2001
|
-
if (state2 && entry) {
|
|
2002
|
-
await cache().set(stream, {
|
|
2003
|
-
state: state2,
|
|
2004
|
-
version: entry.committed.version,
|
|
2005
|
-
event_id: entry.committed.id,
|
|
2006
|
-
patches: 0,
|
|
2007
|
-
snaps: 1
|
|
2008
|
-
});
|
|
2009
|
-
} else {
|
|
2010
|
-
await cache().invalidate(stream);
|
|
2011
|
-
}
|
|
2012
|
-
})
|
|
2013
|
-
);
|
|
2014
|
-
const result = { truncated, skipped };
|
|
2015
2193
|
this.emit("closed", result);
|
|
2016
2194
|
return result;
|
|
2017
2195
|
}
|
|
@@ -2080,7 +2258,7 @@ var Act = class {
|
|
|
2080
2258
|
}
|
|
2081
2259
|
};
|
|
2082
2260
|
|
|
2083
|
-
// src/act-builder.ts
|
|
2261
|
+
// src/builders/act-builder.ts
|
|
2084
2262
|
function registerBatchHandler(proj, batchHandlers) {
|
|
2085
2263
|
if (!proj.batchHandler || !proj.target) return;
|
|
2086
2264
|
const existing = batchHandlers.get(proj.target);
|
|
@@ -2089,56 +2267,33 @@ function registerBatchHandler(proj, batchHandlers) {
|
|
|
2089
2267
|
}
|
|
2090
2268
|
batchHandlers.set(proj.target, proj.batchHandler);
|
|
2091
2269
|
}
|
|
2092
|
-
function act(
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
},
|
|
2270
|
+
function act() {
|
|
2271
|
+
const states = /* @__PURE__ */ new Map();
|
|
2272
|
+
const registry = {
|
|
2273
|
+
actions: {},
|
|
2274
|
+
events: {}
|
|
2275
|
+
};
|
|
2276
|
+
const pendingProjections = [];
|
|
2277
|
+
const batchHandlers = /* @__PURE__ */ new Map();
|
|
2096
2278
|
const builder = {
|
|
2097
2279
|
withState: (state2) => {
|
|
2098
2280
|
registerState(state2, states, registry.actions, registry.events);
|
|
2099
|
-
return
|
|
2100
|
-
states,
|
|
2101
|
-
registry,
|
|
2102
|
-
pendingProjections,
|
|
2103
|
-
batchHandlers
|
|
2104
|
-
);
|
|
2281
|
+
return builder;
|
|
2105
2282
|
},
|
|
2106
2283
|
withSlice: (input) => {
|
|
2107
2284
|
for (const s of input.states.values()) {
|
|
2108
2285
|
registerState(s, states, registry.actions, registry.events);
|
|
2109
2286
|
}
|
|
2110
|
-
|
|
2111
|
-
const sliceRegister = input.events[eventName];
|
|
2112
|
-
for (const [name, reaction] of sliceRegister.reactions) {
|
|
2113
|
-
registry.events[eventName].reactions.set(name, reaction);
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2287
|
+
mergeEventRegister(registry.events, input.events);
|
|
2116
2288
|
pendingProjections.push(...input.projections);
|
|
2117
|
-
return
|
|
2118
|
-
states,
|
|
2119
|
-
registry,
|
|
2120
|
-
pendingProjections,
|
|
2121
|
-
batchHandlers
|
|
2122
|
-
);
|
|
2289
|
+
return builder;
|
|
2123
2290
|
},
|
|
2124
2291
|
withProjection: (proj) => {
|
|
2125
2292
|
mergeProjection(proj, registry.events);
|
|
2126
2293
|
registerBatchHandler(proj, batchHandlers);
|
|
2127
|
-
return
|
|
2128
|
-
states,
|
|
2129
|
-
registry,
|
|
2130
|
-
pendingProjections,
|
|
2131
|
-
batchHandlers
|
|
2132
|
-
);
|
|
2133
|
-
},
|
|
2134
|
-
withActor: () => {
|
|
2135
|
-
return act(
|
|
2136
|
-
states,
|
|
2137
|
-
registry,
|
|
2138
|
-
pendingProjections,
|
|
2139
|
-
batchHandlers
|
|
2140
|
-
);
|
|
2294
|
+
return builder;
|
|
2141
2295
|
},
|
|
2296
|
+
withActor: () => builder,
|
|
2142
2297
|
on: (event) => ({
|
|
2143
2298
|
do: (handler, options) => {
|
|
2144
2299
|
const reaction = {
|
|
@@ -2154,19 +2309,15 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2154
2309
|
`Reaction handler for "${String(event)}" must be a named function`
|
|
2155
2310
|
);
|
|
2156
2311
|
registry.events[event].reactions.set(handler.name, reaction);
|
|
2157
|
-
return {
|
|
2158
|
-
...builder,
|
|
2312
|
+
return Object.assign(builder, {
|
|
2159
2313
|
to(resolver) {
|
|
2160
|
-
|
|
2161
|
-
...reaction,
|
|
2162
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2163
|
-
});
|
|
2314
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2164
2315
|
return builder;
|
|
2165
2316
|
}
|
|
2166
|
-
};
|
|
2317
|
+
});
|
|
2167
2318
|
}
|
|
2168
2319
|
}),
|
|
2169
|
-
build: () => {
|
|
2320
|
+
build: (options) => {
|
|
2170
2321
|
for (const proj of pendingProjections) {
|
|
2171
2322
|
mergeProjection(proj, registry.events);
|
|
2172
2323
|
registerBatchHandler(proj, batchHandlers);
|
|
@@ -2174,7 +2325,8 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2174
2325
|
return new Act(
|
|
2175
2326
|
registry,
|
|
2176
2327
|
states,
|
|
2177
|
-
batchHandlers
|
|
2328
|
+
batchHandlers,
|
|
2329
|
+
options
|
|
2178
2330
|
);
|
|
2179
2331
|
},
|
|
2180
2332
|
events: registry.events
|
|
@@ -2182,8 +2334,9 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2182
2334
|
return builder;
|
|
2183
2335
|
}
|
|
2184
2336
|
|
|
2185
|
-
// src/projection-builder.ts
|
|
2186
|
-
function _projection(target
|
|
2337
|
+
// src/builders/projection-builder.ts
|
|
2338
|
+
function _projection(target) {
|
|
2339
|
+
const events = {};
|
|
2187
2340
|
const defaultResolver = typeof target === "string" ? { target } : void 0;
|
|
2188
2341
|
const base = {
|
|
2189
2342
|
on: (entry) => {
|
|
@@ -2213,17 +2366,13 @@ function _projection(target, events) {
|
|
|
2213
2366
|
`Projection handler for "${event}" must be a named function`
|
|
2214
2367
|
);
|
|
2215
2368
|
register.reactions.set(handler.name, reaction);
|
|
2216
|
-
const
|
|
2217
|
-
return {
|
|
2218
|
-
...nextBuilder,
|
|
2369
|
+
const widened = base;
|
|
2370
|
+
return Object.assign(widened, {
|
|
2219
2371
|
to(resolver) {
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2223
|
-
});
|
|
2224
|
-
return nextBuilder;
|
|
2372
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2373
|
+
return widened;
|
|
2225
2374
|
}
|
|
2226
|
-
};
|
|
2375
|
+
});
|
|
2227
2376
|
}
|
|
2228
2377
|
};
|
|
2229
2378
|
},
|
|
@@ -2235,8 +2384,7 @@ function _projection(target, events) {
|
|
|
2235
2384
|
events
|
|
2236
2385
|
};
|
|
2237
2386
|
if (typeof target === "string") {
|
|
2238
|
-
return {
|
|
2239
|
-
...base,
|
|
2387
|
+
return Object.assign(base, {
|
|
2240
2388
|
batch: (handler) => ({
|
|
2241
2389
|
build: () => ({
|
|
2242
2390
|
_tag: "Projection",
|
|
@@ -2245,34 +2393,28 @@ function _projection(target, events) {
|
|
|
2245
2393
|
batchHandler: handler
|
|
2246
2394
|
})
|
|
2247
2395
|
})
|
|
2248
|
-
};
|
|
2396
|
+
});
|
|
2249
2397
|
}
|
|
2250
2398
|
return base;
|
|
2251
2399
|
}
|
|
2252
|
-
function projection(target
|
|
2253
|
-
return _projection(target
|
|
2400
|
+
function projection(target) {
|
|
2401
|
+
return _projection(target);
|
|
2254
2402
|
}
|
|
2255
2403
|
|
|
2256
|
-
// src/slice-builder.ts
|
|
2257
|
-
function slice(
|
|
2404
|
+
// src/builders/slice-builder.ts
|
|
2405
|
+
function slice() {
|
|
2406
|
+
const states = /* @__PURE__ */ new Map();
|
|
2407
|
+
const actions = {};
|
|
2408
|
+
const events = {};
|
|
2409
|
+
const projections = [];
|
|
2258
2410
|
const builder = {
|
|
2259
2411
|
withState: (state2) => {
|
|
2260
2412
|
registerState(state2, states, actions, events);
|
|
2261
|
-
return
|
|
2262
|
-
states,
|
|
2263
|
-
actions,
|
|
2264
|
-
events,
|
|
2265
|
-
projections
|
|
2266
|
-
);
|
|
2413
|
+
return builder;
|
|
2267
2414
|
},
|
|
2268
2415
|
withProjection: (proj) => {
|
|
2269
2416
|
projections.push(proj);
|
|
2270
|
-
return
|
|
2271
|
-
states,
|
|
2272
|
-
actions,
|
|
2273
|
-
events,
|
|
2274
|
-
projections
|
|
2275
|
-
);
|
|
2417
|
+
return builder;
|
|
2276
2418
|
},
|
|
2277
2419
|
on: (event) => ({
|
|
2278
2420
|
do: (handler, options) => {
|
|
@@ -2289,16 +2431,12 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
|
|
|
2289
2431
|
`Reaction handler for "${String(event)}" must be a named function`
|
|
2290
2432
|
);
|
|
2291
2433
|
events[event].reactions.set(handler.name, reaction);
|
|
2292
|
-
return {
|
|
2293
|
-
...builder,
|
|
2434
|
+
return Object.assign(builder, {
|
|
2294
2435
|
to(resolver) {
|
|
2295
|
-
|
|
2296
|
-
...reaction,
|
|
2297
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2298
|
-
});
|
|
2436
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2299
2437
|
return builder;
|
|
2300
2438
|
}
|
|
2301
|
-
};
|
|
2439
|
+
});
|
|
2302
2440
|
}
|
|
2303
2441
|
}),
|
|
2304
2442
|
build: () => ({
|
|
@@ -2312,7 +2450,7 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
|
|
|
2312
2450
|
return builder;
|
|
2313
2451
|
}
|
|
2314
2452
|
|
|
2315
|
-
// src/state-builder.ts
|
|
2453
|
+
// src/builders/state-builder.ts
|
|
2316
2454
|
function state(entry) {
|
|
2317
2455
|
const keys = Object.keys(entry);
|
|
2318
2456
|
if (keys.length !== 1) throw new Error("state() requires exactly one key");
|
|
@@ -2330,7 +2468,7 @@ function state(entry) {
|
|
|
2330
2468
|
return [k, fn];
|
|
2331
2469
|
})
|
|
2332
2470
|
);
|
|
2333
|
-
const
|
|
2471
|
+
const internal = {
|
|
2334
2472
|
events,
|
|
2335
2473
|
actions: {},
|
|
2336
2474
|
state: stateSchema,
|
|
@@ -2338,18 +2476,12 @@ function state(entry) {
|
|
|
2338
2476
|
init,
|
|
2339
2477
|
patch: defaultPatch,
|
|
2340
2478
|
on: {}
|
|
2341
|
-
}
|
|
2479
|
+
};
|
|
2480
|
+
const builder = action_builder(internal);
|
|
2342
2481
|
return Object.assign(builder, {
|
|
2343
2482
|
patch(customPatch) {
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
actions: {},
|
|
2347
|
-
state: stateSchema,
|
|
2348
|
-
name,
|
|
2349
|
-
init,
|
|
2350
|
-
patch: { ...defaultPatch, ...customPatch },
|
|
2351
|
-
on: {}
|
|
2352
|
-
});
|
|
2483
|
+
Object.assign(internal.patch, customPatch);
|
|
2484
|
+
return builder;
|
|
2353
2485
|
}
|
|
2354
2486
|
});
|
|
2355
2487
|
}
|
|
@@ -2358,50 +2490,43 @@ function state(entry) {
|
|
|
2358
2490
|
};
|
|
2359
2491
|
}
|
|
2360
2492
|
function action_builder(state2) {
|
|
2361
|
-
|
|
2493
|
+
const internal = state2;
|
|
2494
|
+
const builder = {
|
|
2362
2495
|
on(entry) {
|
|
2363
2496
|
const keys = Object.keys(entry);
|
|
2364
2497
|
if (keys.length !== 1) throw new Error(".on() requires exactly one key");
|
|
2365
2498
|
const action2 = keys[0];
|
|
2366
2499
|
const schema = entry[action2];
|
|
2367
|
-
if (action2 in
|
|
2500
|
+
if (action2 in internal.actions)
|
|
2368
2501
|
throw new Error(`Duplicate action "${action2}"`);
|
|
2369
|
-
|
|
2370
|
-
...state2.actions,
|
|
2371
|
-
[action2]: schema
|
|
2372
|
-
};
|
|
2373
|
-
const on = { ...state2.on };
|
|
2374
|
-
const _given = { ...state2.given };
|
|
2502
|
+
internal.actions[action2] = schema;
|
|
2375
2503
|
function given(rules) {
|
|
2376
|
-
|
|
2504
|
+
(internal.given ??= {})[action2] = rules;
|
|
2377
2505
|
return { emit };
|
|
2378
2506
|
}
|
|
2379
2507
|
function emit(handler) {
|
|
2380
2508
|
if (typeof handler === "string") {
|
|
2381
2509
|
const eventName = handler;
|
|
2382
|
-
on[action2] = (
|
|
2510
|
+
internal.on[action2] = (payload) => [
|
|
2511
|
+
eventName,
|
|
2512
|
+
payload
|
|
2513
|
+
];
|
|
2383
2514
|
} else {
|
|
2384
|
-
on[action2] = handler;
|
|
2515
|
+
internal.on[action2] = handler;
|
|
2385
2516
|
}
|
|
2386
|
-
return
|
|
2387
|
-
...state2,
|
|
2388
|
-
actions,
|
|
2389
|
-
on,
|
|
2390
|
-
given: _given
|
|
2391
|
-
});
|
|
2517
|
+
return builder;
|
|
2392
2518
|
}
|
|
2393
2519
|
return { given, emit };
|
|
2394
2520
|
},
|
|
2395
2521
|
snap(snap2) {
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
snap: snap2
|
|
2399
|
-
});
|
|
2522
|
+
internal.snap = snap2;
|
|
2523
|
+
return builder;
|
|
2400
2524
|
},
|
|
2401
2525
|
build() {
|
|
2402
|
-
return
|
|
2526
|
+
return internal;
|
|
2403
2527
|
}
|
|
2404
2528
|
};
|
|
2529
|
+
return builder;
|
|
2405
2530
|
}
|
|
2406
2531
|
export {
|
|
2407
2532
|
Act,
|
|
@@ -2410,6 +2535,7 @@ export {
|
|
|
2410
2535
|
CommittedMetaSchema,
|
|
2411
2536
|
ConcurrencyError,
|
|
2412
2537
|
ConsoleLogger,
|
|
2538
|
+
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2413
2539
|
Environments,
|
|
2414
2540
|
Errors,
|
|
2415
2541
|
EventMetaSchema,
|