@bimetal/devtools 0.15.10 → 0.16.7
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/causation-forest.d.ts +60 -0
- package/dist/causation-forest.d.ts.map +1 -0
- package/dist/causation-forest.js +191 -0
- package/dist/causation-forest.js.map +1 -0
- package/dist/index.d.ts +50 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -7
- package/dist/index.js.map +1 -1
- package/dist/inspectable-json.d.ts +71 -0
- package/dist/inspectable-json.d.ts.map +1 -0
- package/dist/inspectable-json.js +225 -0
- package/dist/inspectable-json.js.map +1 -0
- package/dist/observed-bridge.d.ts +71 -0
- package/dist/observed-bridge.d.ts.map +1 -0
- package/dist/observed-bridge.js +165 -0
- package/dist/observed-bridge.js.map +1 -0
- package/dist/observed-helpers.d.ts +88 -0
- package/dist/observed-helpers.d.ts.map +1 -0
- package/dist/observed-helpers.js +76 -0
- package/dist/observed-helpers.js.map +1 -0
- package/dist/registry.d.ts +42 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +84 -0
- package/dist/registry.js.map +1 -1
- package/dist/replay-engine.d.ts +128 -0
- package/dist/replay-engine.d.ts.map +1 -0
- package/dist/replay-engine.js +199 -0
- package/dist/replay-engine.js.map +1 -0
- package/dist/snapshot-export.d.ts +73 -0
- package/dist/snapshot-export.d.ts.map +1 -0
- package/dist/snapshot-export.js +119 -0
- package/dist/snapshot-export.js.map +1 -0
- package/dist/time-travel.d.ts +62 -0
- package/dist/time-travel.d.ts.map +1 -0
- package/dist/time-travel.js +140 -0
- package/dist/time-travel.js.map +1 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createReplayEngine` — pure synchrone Fold-Function über einen Stream.
|
|
3
|
+
*
|
|
4
|
+
* Architektur-Verträge aus `design/calendar/16-replay-target-architecture.md`:
|
|
5
|
+
* - **Passive Beobachtung**: Engine ruft NIEMALS `eventStore.append`,
|
|
6
|
+
* `store.dispatch` oder `broker.publish`. Architecture-Test verriegelt.
|
|
7
|
+
* - **subscribe-first + buffer + read + drain**: kein Race-Window zwischen
|
|
8
|
+
* Catch-up und Live-Tail (v0.15.7-Lehre).
|
|
9
|
+
* - **Sticky Error**: bei `apply`-Wurf wird `status = 'error'` und KEIN
|
|
10
|
+
* weiteres Event mehr gefolded — sonst entstände Fold auf staleren
|
|
11
|
+
* Zustand und der User sähe vorgegaukelten Aggregat-Stand.
|
|
12
|
+
* - **Recovery nur via `dispose()` + neuer `createReplayEngine`-Call.**
|
|
13
|
+
* Engine kennt keine `clearError`-Operation.
|
|
14
|
+
*
|
|
15
|
+
* Implementations-Notes:
|
|
16
|
+
* - `handleEvent` ist **synchron** (Architektur-Vertrag).
|
|
17
|
+
* - Initial-Read ist async (das ist OK — `read()` ist der einzige
|
|
18
|
+
* await; danach läuft alles synchron).
|
|
19
|
+
* - `snapshot`-Getter ist reference-stable bis zur nächsten Mutation
|
|
20
|
+
* (analog Devtools-Registry, useSyncExternalStore-Vertrag).
|
|
21
|
+
*/
|
|
22
|
+
import type { DomainEvent, Unsubscribe } from '@bimetal/event-sourcing';
|
|
23
|
+
import type { EventStoreLike } from './types.js';
|
|
24
|
+
/**
|
|
25
|
+
* Domain-agnostisches Aggregate-Interface. Bewusst KEIN `handle(state, command)`
|
|
26
|
+
* — Command-Semantik gehört nicht in den Devtools-Vertrag.
|
|
27
|
+
*/
|
|
28
|
+
export interface AggregateLike<S> {
|
|
29
|
+
readonly initialState: S;
|
|
30
|
+
apply(state: S, event: DomainEvent): S;
|
|
31
|
+
}
|
|
32
|
+
export interface ProjectionLike<R> {
|
|
33
|
+
readonly initialState: R;
|
|
34
|
+
apply(readModel: R, event: DomainEvent): R;
|
|
35
|
+
readonly version: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Strukturelles SnapshotStore-Interface. Optional — Engine liest nur,
|
|
39
|
+
* schreibt NIE rein. Aktuell für Konsistenz-Checks reserviert (in v0.16.0
|
|
40
|
+
* noch nicht genutzt, kommt mit späterer Slice).
|
|
41
|
+
*/
|
|
42
|
+
export interface SnapshotStoreLike {
|
|
43
|
+
load(streamId: string): Promise<SnapshotLike | null>;
|
|
44
|
+
}
|
|
45
|
+
export interface SnapshotLike {
|
|
46
|
+
readonly state: unknown;
|
|
47
|
+
readonly projectionVersion: number;
|
|
48
|
+
readonly version: number;
|
|
49
|
+
readonly timestamp: number;
|
|
50
|
+
}
|
|
51
|
+
export interface ReplayTargetHandle<S = unknown, R = unknown> {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly eventStore: EventStoreLike;
|
|
55
|
+
readonly streamId: string;
|
|
56
|
+
readonly aggregate: AggregateLike<S>;
|
|
57
|
+
readonly projection: ProjectionLike<R>;
|
|
58
|
+
readonly snapshotStore?: SnapshotStoreLike;
|
|
59
|
+
}
|
|
60
|
+
export interface ReplayEngineSnapshot<S, R> {
|
|
61
|
+
readonly state: S;
|
|
62
|
+
readonly readModel: R;
|
|
63
|
+
/** `envelope.version` des zuletzt erfolgreich applied Events; `0` bei initialState. */
|
|
64
|
+
readonly lastAppliedVersion: number;
|
|
65
|
+
readonly status: 'replaying' | 'live' | 'error';
|
|
66
|
+
/**
|
|
67
|
+
* Bei `status='error'`: gesetzt, mit `event` als der Event, der den
|
|
68
|
+
* Fehler auslöste — oder als der zuletzt blockierte Event (nach
|
|
69
|
+
* status='error' aktualisiert sich `event` mit jedem eingehenden,
|
|
70
|
+
* nicht-applied Event, damit der User „letzter blockierter Event war v9"
|
|
71
|
+
* sehen kann). `event=null` zeigt: Fehler kam aus `read()`, nicht aus `apply()`.
|
|
72
|
+
*/
|
|
73
|
+
readonly lastError?: {
|
|
74
|
+
readonly event: DomainEvent | null;
|
|
75
|
+
readonly error: Error;
|
|
76
|
+
};
|
|
77
|
+
readonly lastAppliedEventId?: string;
|
|
78
|
+
/**
|
|
79
|
+
* `metadata.schemaVersion` des zuletzt erfolgreich applied Events.
|
|
80
|
+
* Konsumenten-UI vergleicht das mit der `projection.version` aus dem
|
|
81
|
+
* Handle, um Schema-Drift sichtbar zu machen (v0.16-Architektur-Doc
|
|
82
|
+
* Leitplanke 6).
|
|
83
|
+
*/
|
|
84
|
+
readonly lastAppliedSchemaVersion?: number;
|
|
85
|
+
/** Events nach `status='error'` eingegangen, aber NICHT applied. */
|
|
86
|
+
readonly blockedEventCount: number;
|
|
87
|
+
}
|
|
88
|
+
export interface ReplayEngine<S, R> {
|
|
89
|
+
/** Reference-stable bis zur nächsten Mutation. */
|
|
90
|
+
readonly snapshot: ReplayEngineSnapshot<S, R>;
|
|
91
|
+
subscribe(listener: () => void): Unsubscribe;
|
|
92
|
+
/** Cleanup: stoppt Live-Subscriber, befreit Listener. */
|
|
93
|
+
dispose(): void;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Initialer Snapshot für eine fresh angesetzte Replay-Sequenz. `status`
|
|
97
|
+
* beginnt bei `'replaying'`; der Konsument (Engine ODER TimeTravelController)
|
|
98
|
+
* entscheidet selbst, wann er auf `'live'`/`'ready'` umschaltet.
|
|
99
|
+
*/
|
|
100
|
+
export declare function createInitialSnapshot<S, R>(aggregate: AggregateLike<S>, projection: ProjectionLike<R>): ReplayEngineSnapshot<S, R>;
|
|
101
|
+
/**
|
|
102
|
+
* Pure synchroner Fold von genau einem Event auf einen Snapshot.
|
|
103
|
+
*
|
|
104
|
+
* **Atomarer Commit:** beide `apply`-Aufrufe (aggregate + projection)
|
|
105
|
+
* laufen ZUERST in lokale `next*`-Variablen. Erst wenn BEIDE
|
|
106
|
+
* erfolgreich sind, wird das neue Snapshot-Objekt mit allen Tracking-
|
|
107
|
+
* Feldern (`lastAppliedVersion`, `lastAppliedEventId`,
|
|
108
|
+
* `lastAppliedSchemaVersion`) zurückgegeben. Wirft `projection.apply`,
|
|
109
|
+
* ist `nextState` lokal verloren, der Input-`snap` bleibt unverändert —
|
|
110
|
+
* Half-Update unmöglich.
|
|
111
|
+
*
|
|
112
|
+
* **Sticky-Error:** Bei Input-`snap.status === 'error'` wird der Event
|
|
113
|
+
* NICHT applied. Output trägt `blockedEventCount + 1` und ein
|
|
114
|
+
* aktualisiertes `lastError.event` (auf den jeweils zuletzt blockierten
|
|
115
|
+
* Event), damit der User „letzter blockierter Event war v9" sehen kann.
|
|
116
|
+
*
|
|
117
|
+
* **Nichts read/subscribe-bezogenes.** Reine Funktion: Snapshot rein,
|
|
118
|
+
* Event rein, Snapshot raus. Live-Engine UND TimeTravelController
|
|
119
|
+
* konsumieren dieselbe Funktion.
|
|
120
|
+
*/
|
|
121
|
+
export declare function applyEventToSnapshot<S, R>(snap: ReplayEngineSnapshot<S, R>, event: DomainEvent, aggregate: AggregateLike<S>, projection: ProjectionLike<R>): ReplayEngineSnapshot<S, R>;
|
|
122
|
+
/**
|
|
123
|
+
* Convenience: Reduce über ein Array von Events — equivalent zu
|
|
124
|
+
* sequenziellem `applyEventToSnapshot`. Pure Funktion, keine Side-Effects.
|
|
125
|
+
*/
|
|
126
|
+
export declare function foldEvents<S, R>(events: readonly DomainEvent[], aggregate: AggregateLike<S>, projection: ProjectionLike<R>, initial: ReplayEngineSnapshot<S, R>): ReplayEngineSnapshot<S, R>;
|
|
127
|
+
export declare function createReplayEngine<S, R>(handle: ReplayTargetHandle<S, R>): ReplayEngine<S, R>;
|
|
128
|
+
//# sourceMappingURL=replay-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-engine.d.ts","sourceRoot":"","sources":["../src/replay-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createReplayEngine` — pure synchrone Fold-Function über einen Stream.
|
|
3
|
+
*
|
|
4
|
+
* Architektur-Verträge aus `design/calendar/16-replay-target-architecture.md`:
|
|
5
|
+
* - **Passive Beobachtung**: Engine ruft NIEMALS `eventStore.append`,
|
|
6
|
+
* `store.dispatch` oder `broker.publish`. Architecture-Test verriegelt.
|
|
7
|
+
* - **subscribe-first + buffer + read + drain**: kein Race-Window zwischen
|
|
8
|
+
* Catch-up und Live-Tail (v0.15.7-Lehre).
|
|
9
|
+
* - **Sticky Error**: bei `apply`-Wurf wird `status = 'error'` und KEIN
|
|
10
|
+
* weiteres Event mehr gefolded — sonst entstände Fold auf staleren
|
|
11
|
+
* Zustand und der User sähe vorgegaukelten Aggregat-Stand.
|
|
12
|
+
* - **Recovery nur via `dispose()` + neuer `createReplayEngine`-Call.**
|
|
13
|
+
* Engine kennt keine `clearError`-Operation.
|
|
14
|
+
*
|
|
15
|
+
* Implementations-Notes:
|
|
16
|
+
* - `handleEvent` ist **synchron** (Architektur-Vertrag).
|
|
17
|
+
* - Initial-Read ist async (das ist OK — `read()` ist der einzige
|
|
18
|
+
* await; danach läuft alles synchron).
|
|
19
|
+
* - `snapshot`-Getter ist reference-stable bis zur nächsten Mutation
|
|
20
|
+
* (analog Devtools-Registry, useSyncExternalStore-Vertrag).
|
|
21
|
+
*/
|
|
22
|
+
// ── Pure Fold-Core (v0.16.2 Sub 1) ────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* Initialer Snapshot für eine fresh angesetzte Replay-Sequenz. `status`
|
|
25
|
+
* beginnt bei `'replaying'`; der Konsument (Engine ODER TimeTravelController)
|
|
26
|
+
* entscheidet selbst, wann er auf `'live'`/`'ready'` umschaltet.
|
|
27
|
+
*/
|
|
28
|
+
export function createInitialSnapshot(aggregate, projection) {
|
|
29
|
+
return {
|
|
30
|
+
state: aggregate.initialState,
|
|
31
|
+
readModel: projection.initialState,
|
|
32
|
+
lastAppliedVersion: 0,
|
|
33
|
+
status: 'replaying',
|
|
34
|
+
blockedEventCount: 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Pure synchroner Fold von genau einem Event auf einen Snapshot.
|
|
39
|
+
*
|
|
40
|
+
* **Atomarer Commit:** beide `apply`-Aufrufe (aggregate + projection)
|
|
41
|
+
* laufen ZUERST in lokale `next*`-Variablen. Erst wenn BEIDE
|
|
42
|
+
* erfolgreich sind, wird das neue Snapshot-Objekt mit allen Tracking-
|
|
43
|
+
* Feldern (`lastAppliedVersion`, `lastAppliedEventId`,
|
|
44
|
+
* `lastAppliedSchemaVersion`) zurückgegeben. Wirft `projection.apply`,
|
|
45
|
+
* ist `nextState` lokal verloren, der Input-`snap` bleibt unverändert —
|
|
46
|
+
* Half-Update unmöglich.
|
|
47
|
+
*
|
|
48
|
+
* **Sticky-Error:** Bei Input-`snap.status === 'error'` wird der Event
|
|
49
|
+
* NICHT applied. Output trägt `blockedEventCount + 1` und ein
|
|
50
|
+
* aktualisiertes `lastError.event` (auf den jeweils zuletzt blockierten
|
|
51
|
+
* Event), damit der User „letzter blockierter Event war v9" sehen kann.
|
|
52
|
+
*
|
|
53
|
+
* **Nichts read/subscribe-bezogenes.** Reine Funktion: Snapshot rein,
|
|
54
|
+
* Event rein, Snapshot raus. Live-Engine UND TimeTravelController
|
|
55
|
+
* konsumieren dieselbe Funktion.
|
|
56
|
+
*/
|
|
57
|
+
export function applyEventToSnapshot(snap, event, aggregate, projection) {
|
|
58
|
+
if (snap.status === 'error') {
|
|
59
|
+
return {
|
|
60
|
+
...snap,
|
|
61
|
+
blockedEventCount: snap.blockedEventCount + 1,
|
|
62
|
+
lastError: snap.lastError
|
|
63
|
+
? { event, error: snap.lastError.error }
|
|
64
|
+
: snap.lastError,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const nextState = aggregate.apply(snap.state, event);
|
|
69
|
+
const nextReadModel = projection.apply(snap.readModel, event);
|
|
70
|
+
return {
|
|
71
|
+
...snap,
|
|
72
|
+
state: nextState,
|
|
73
|
+
readModel: nextReadModel,
|
|
74
|
+
lastAppliedVersion: event.envelope.version,
|
|
75
|
+
lastAppliedEventId: event.envelope.id,
|
|
76
|
+
lastAppliedSchemaVersion: event.metadata.schemaVersion,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
...snap,
|
|
82
|
+
status: 'error',
|
|
83
|
+
lastError: { event, error: error instanceof Error ? error : new Error(String(error)) },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Convenience: Reduce über ein Array von Events — equivalent zu
|
|
89
|
+
* sequenziellem `applyEventToSnapshot`. Pure Funktion, keine Side-Effects.
|
|
90
|
+
*/
|
|
91
|
+
export function foldEvents(events, aggregate, projection, initial) {
|
|
92
|
+
let current = initial;
|
|
93
|
+
for (const event of events) {
|
|
94
|
+
current = applyEventToSnapshot(current, event, aggregate, projection);
|
|
95
|
+
}
|
|
96
|
+
return current;
|
|
97
|
+
}
|
|
98
|
+
// ── Factory ───────────────────────────────────────────────────────
|
|
99
|
+
export function createReplayEngine(handle) {
|
|
100
|
+
let current = createInitialSnapshot(handle.aggregate, handle.projection);
|
|
101
|
+
const listeners = new Set();
|
|
102
|
+
function invalidate() {
|
|
103
|
+
for (const l of listeners) {
|
|
104
|
+
try {
|
|
105
|
+
l();
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// eslint-disable-next-line no-console
|
|
109
|
+
console.error('[@bimetal/devtools] ReplayEngine listener threw:', err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function applyEvent(event) {
|
|
114
|
+
current = applyEventToSnapshot(current, event, handle.aggregate, handle.projection);
|
|
115
|
+
invalidate();
|
|
116
|
+
}
|
|
117
|
+
// ── Lifecycle: subscribe-first + buffer + read + drain ──────────
|
|
118
|
+
const pendingBuffer = [];
|
|
119
|
+
let drained = false;
|
|
120
|
+
let disposed = false;
|
|
121
|
+
// 1. ZUERST subscribe — Events ab jetzt landen im Buffer (bis drained=true).
|
|
122
|
+
const off = handle.eventStore.subscribe((event) => {
|
|
123
|
+
if (disposed)
|
|
124
|
+
return;
|
|
125
|
+
if (!drained) {
|
|
126
|
+
pendingBuffer.push(event);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
applyEvent(event);
|
|
130
|
+
}, handle.streamId);
|
|
131
|
+
// 2. Dann read() für historische Events. Async — danach drain mit
|
|
132
|
+
// envelope.version-basierter Dedup gegen den pendingBuffer.
|
|
133
|
+
void (async () => {
|
|
134
|
+
try {
|
|
135
|
+
const initial = await handle.eventStore.read(handle.streamId);
|
|
136
|
+
if (disposed)
|
|
137
|
+
return;
|
|
138
|
+
// 3. Fold initial
|
|
139
|
+
for (const event of initial) {
|
|
140
|
+
applyEvent(event);
|
|
141
|
+
}
|
|
142
|
+
// 4. Drain pendingBuffer mit Versions-Dedup gegen initial. Garantie:
|
|
143
|
+
// jedes Event aus dem Stream wird genau einmal applied.
|
|
144
|
+
const lastInitialVersion = initial.length > 0
|
|
145
|
+
? initial[initial.length - 1].envelope.version
|
|
146
|
+
: 0;
|
|
147
|
+
for (const event of pendingBuffer) {
|
|
148
|
+
if (event.envelope.version > lastInitialVersion) {
|
|
149
|
+
applyEvent(event);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
pendingBuffer.length = 0;
|
|
153
|
+
drained = true;
|
|
154
|
+
// 5. Status-Übergang: 'replaying' → 'live' nur wenn kein apply-Fehler
|
|
155
|
+
// während des initial-Folds ausgelöst wurde.
|
|
156
|
+
if (current.status === 'replaying') {
|
|
157
|
+
current = { ...current, status: 'live' };
|
|
158
|
+
invalidate();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
// read() wirft → Error-State, off() bleibt aktiv damit Engine
|
|
163
|
+
// sichtbar bleibt; dispose() vom Konsumenten räumt auf.
|
|
164
|
+
current = {
|
|
165
|
+
...current,
|
|
166
|
+
status: 'error',
|
|
167
|
+
lastError: { event: null, error: error instanceof Error ? error : new Error(String(error)) },
|
|
168
|
+
};
|
|
169
|
+
drained = true; // verhindere Buffer-Akkumulation bei Folge-Events
|
|
170
|
+
pendingBuffer.length = 0;
|
|
171
|
+
invalidate();
|
|
172
|
+
}
|
|
173
|
+
})();
|
|
174
|
+
return {
|
|
175
|
+
/**
|
|
176
|
+
* Reference-stable: `current` ist das Snapshot-Objekt; jede Mutation
|
|
177
|
+
* ersetzt es vollständig durch ein neues. Konsument vergleicht per
|
|
178
|
+
* Identität (=`===`), keine Deep-Eq nötig.
|
|
179
|
+
*/
|
|
180
|
+
get snapshot() {
|
|
181
|
+
return current;
|
|
182
|
+
},
|
|
183
|
+
subscribe(listener) {
|
|
184
|
+
listeners.add(listener);
|
|
185
|
+
return () => {
|
|
186
|
+
listeners.delete(listener);
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
dispose() {
|
|
190
|
+
if (disposed)
|
|
191
|
+
return;
|
|
192
|
+
disposed = true;
|
|
193
|
+
off();
|
|
194
|
+
listeners.clear();
|
|
195
|
+
pendingBuffer.length = 0;
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=replay-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-engine.js","sourceRoot":"","sources":["../src/replay-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.16.6 — DevtoolsSnapshot Export/Import.
|
|
3
|
+
*
|
|
4
|
+
* Captured Stream + State exportierbar als versioniertes JSON-Dokument;
|
|
5
|
+
* Loader-API für späteren Sandbox-Replay. Sandbox-UI ist NICHT v0.16.6
|
|
6
|
+
* (eigene Slice).
|
|
7
|
+
*
|
|
8
|
+
* **Verträge (Pilot-Verriegelungen 2026-06-09):**
|
|
9
|
+
* - `exportSnapshot(state, { exportedAt })` — Konsument liefert die
|
|
10
|
+
* Zeit. Helper ist pure: keine Zeit-/Zufalls-/Konsolen-APIs. Die
|
|
11
|
+
* konkreten verbotenen Identifier sind im Architecture-Test
|
|
12
|
+
* verriegelt; diese Quelle vermeidet sie auch in Kommentaren, damit
|
|
13
|
+
* der Source-Scan einfach bleibt. Der State wird beim Erzeugen via
|
|
14
|
+
* `toInspectableJson` normalisiert + detached — Mutationen am Live-
|
|
15
|
+
* State treffen den Snapshot nicht (Sub-1-Pilot-Schärfung).
|
|
16
|
+
* - `serializeSnapshot(snap)` — reines `JSON.stringify`. Snapshot ist
|
|
17
|
+
* bereits JSON-normalisiert beim Erzeugen, daher kein Pipeline-Schritt
|
|
18
|
+
* mehr. Lossy-Charakter (Map/Set/BigInt → JSON-Form, zyklische
|
|
19
|
+
* Strukturen → marker) gehört zum Snapshot-Format selbst, nicht zu
|
|
20
|
+
* Serialize: Sandbox-Replay ist Diagnose, kein Round-Trip-Beweis.
|
|
21
|
+
* - `parseSnapshot(json)` — strukturelle Validierung. Wirft bei
|
|
22
|
+
* JSON-Parse-Fehler, Nicht-Objekt-Top-Level, falscher `version`,
|
|
23
|
+
* nicht-finite `exportedAt`, fehlendem `state` oder fehlenden/
|
|
24
|
+
* falsch-typisierten Pflicht-State-Feldern. Tiefe Domain-Schema-
|
|
25
|
+
* Validierung NICHT in v0.16.6.
|
|
26
|
+
*/
|
|
27
|
+
import type { DevtoolsState } from './types.js';
|
|
28
|
+
/**
|
|
29
|
+
* Format-Version als zentrale Konstante. Pilot-Verriegelung 2026-06-09
|
|
30
|
+
* (Sub-1-Schärfung): Architecture-Test prüft den Wert dieser Konstante
|
|
31
|
+
* direkt, statt freie `version:`-Literale im Source zu scannen. Eine
|
|
32
|
+
* spätere Slice, die das Format ändert, MUSS die Konstante hier ändern
|
|
33
|
+
* — und nichts anderes funktioniert ohne diese Stelle anzufassen.
|
|
34
|
+
*/
|
|
35
|
+
export declare const SNAPSHOT_VERSION: "1.0.0";
|
|
36
|
+
export interface DevtoolsSnapshot {
|
|
37
|
+
readonly version: typeof SNAPSHOT_VERSION;
|
|
38
|
+
readonly exportedAt: number;
|
|
39
|
+
/**
|
|
40
|
+
* JSON-normalisierte Form von `DevtoolsState`. Lossy: Map→Object,
|
|
41
|
+
* Date→ISO, BigInt→string, Function→null, Cycle→marker. Strukturell
|
|
42
|
+
* äquivalent zum übergebenen State, aber NICHT typ-identisch und
|
|
43
|
+
* **detached** — Mutationen am ursprünglichen State treffen den
|
|
44
|
+
* Snapshot nicht.
|
|
45
|
+
*/
|
|
46
|
+
readonly state: DevtoolsState;
|
|
47
|
+
}
|
|
48
|
+
export interface ExportSnapshotOptions {
|
|
49
|
+
readonly exportedAt: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Versioniertes Snapshot-Dokument bauen. Pilot-Schärfung Sub 1
|
|
53
|
+
* (HIGH-Finding): der State wird beim Erzeugen via `toInspectableJson`
|
|
54
|
+
* normalisiert + detached. Damit ist `serializeSnapshot()` nur noch
|
|
55
|
+
* `JSON.stringify`, und ein Konsument, der nach `exportSnapshot()` die
|
|
56
|
+
* Registry weiter-mutiert, beeinflusst den Snapshot nicht.
|
|
57
|
+
*
|
|
58
|
+
* Pure: deterministisch über das `state`-Argument, `exportedAt` vom
|
|
59
|
+
* Konsumenten (kein Date-/Random-/Konsolen-API).
|
|
60
|
+
*/
|
|
61
|
+
export declare function exportSnapshot(state: DevtoolsState, options: ExportSnapshotOptions): DevtoolsSnapshot;
|
|
62
|
+
/**
|
|
63
|
+
* JSON-String erzeugen. Snapshot ist bereits JSON-normalisiert (siehe
|
|
64
|
+
* `exportSnapshot`), daher reicht `JSON.stringify`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function serializeSnapshot(snap: DevtoolsSnapshot): string;
|
|
67
|
+
/**
|
|
68
|
+
* Strukturelle Validierung: Top-Level-Form, `version`, `exportedAt`,
|
|
69
|
+
* `state` und die sieben benannten Array-Felder. Wirft mit konkreter
|
|
70
|
+
* Message pro Verletzung.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseSnapshot(json: string): DevtoolsSnapshot;
|
|
73
|
+
//# sourceMappingURL=snapshot-export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-export.d.ts","sourceRoot":"","sources":["../src/snapshot-export.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAG,OAAgB,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,gBAAgB,CAAC;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,qBAAqB,GAC7B,gBAAgB,CAMlB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAEhE;AAiBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAqC5D"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.16.6 — DevtoolsSnapshot Export/Import.
|
|
3
|
+
*
|
|
4
|
+
* Captured Stream + State exportierbar als versioniertes JSON-Dokument;
|
|
5
|
+
* Loader-API für späteren Sandbox-Replay. Sandbox-UI ist NICHT v0.16.6
|
|
6
|
+
* (eigene Slice).
|
|
7
|
+
*
|
|
8
|
+
* **Verträge (Pilot-Verriegelungen 2026-06-09):**
|
|
9
|
+
* - `exportSnapshot(state, { exportedAt })` — Konsument liefert die
|
|
10
|
+
* Zeit. Helper ist pure: keine Zeit-/Zufalls-/Konsolen-APIs. Die
|
|
11
|
+
* konkreten verbotenen Identifier sind im Architecture-Test
|
|
12
|
+
* verriegelt; diese Quelle vermeidet sie auch in Kommentaren, damit
|
|
13
|
+
* der Source-Scan einfach bleibt. Der State wird beim Erzeugen via
|
|
14
|
+
* `toInspectableJson` normalisiert + detached — Mutationen am Live-
|
|
15
|
+
* State treffen den Snapshot nicht (Sub-1-Pilot-Schärfung).
|
|
16
|
+
* - `serializeSnapshot(snap)` — reines `JSON.stringify`. Snapshot ist
|
|
17
|
+
* bereits JSON-normalisiert beim Erzeugen, daher kein Pipeline-Schritt
|
|
18
|
+
* mehr. Lossy-Charakter (Map/Set/BigInt → JSON-Form, zyklische
|
|
19
|
+
* Strukturen → marker) gehört zum Snapshot-Format selbst, nicht zu
|
|
20
|
+
* Serialize: Sandbox-Replay ist Diagnose, kein Round-Trip-Beweis.
|
|
21
|
+
* - `parseSnapshot(json)` — strukturelle Validierung. Wirft bei
|
|
22
|
+
* JSON-Parse-Fehler, Nicht-Objekt-Top-Level, falscher `version`,
|
|
23
|
+
* nicht-finite `exportedAt`, fehlendem `state` oder fehlenden/
|
|
24
|
+
* falsch-typisierten Pflicht-State-Feldern. Tiefe Domain-Schema-
|
|
25
|
+
* Validierung NICHT in v0.16.6.
|
|
26
|
+
*/
|
|
27
|
+
import { toInspectableJson } from './inspectable-json.js';
|
|
28
|
+
/**
|
|
29
|
+
* Format-Version als zentrale Konstante. Pilot-Verriegelung 2026-06-09
|
|
30
|
+
* (Sub-1-Schärfung): Architecture-Test prüft den Wert dieser Konstante
|
|
31
|
+
* direkt, statt freie `version:`-Literale im Source zu scannen. Eine
|
|
32
|
+
* spätere Slice, die das Format ändert, MUSS die Konstante hier ändern
|
|
33
|
+
* — und nichts anderes funktioniert ohne diese Stelle anzufassen.
|
|
34
|
+
*/
|
|
35
|
+
export const SNAPSHOT_VERSION = '1.0.0';
|
|
36
|
+
/**
|
|
37
|
+
* Versioniertes Snapshot-Dokument bauen. Pilot-Schärfung Sub 1
|
|
38
|
+
* (HIGH-Finding): der State wird beim Erzeugen via `toInspectableJson`
|
|
39
|
+
* normalisiert + detached. Damit ist `serializeSnapshot()` nur noch
|
|
40
|
+
* `JSON.stringify`, und ein Konsument, der nach `exportSnapshot()` die
|
|
41
|
+
* Registry weiter-mutiert, beeinflusst den Snapshot nicht.
|
|
42
|
+
*
|
|
43
|
+
* Pure: deterministisch über das `state`-Argument, `exportedAt` vom
|
|
44
|
+
* Konsumenten (kein Date-/Random-/Konsolen-API).
|
|
45
|
+
*/
|
|
46
|
+
export function exportSnapshot(state, options) {
|
|
47
|
+
return {
|
|
48
|
+
version: SNAPSHOT_VERSION,
|
|
49
|
+
exportedAt: options.exportedAt,
|
|
50
|
+
state: toInspectableJson(state),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* JSON-String erzeugen. Snapshot ist bereits JSON-normalisiert (siehe
|
|
55
|
+
* `exportSnapshot`), daher reicht `JSON.stringify`.
|
|
56
|
+
*/
|
|
57
|
+
export function serializeSnapshot(snap) {
|
|
58
|
+
return JSON.stringify(snap, null, 2);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Liste der State-Felder, die `parseSnapshot` namentlich prüft. Pilot-
|
|
62
|
+
* Verriegelung: NICHT „irgendeine sieben Arrays" — exakt diese Namen.
|
|
63
|
+
* Reihenfolge ist die Reihenfolge der Throw-Tests.
|
|
64
|
+
*/
|
|
65
|
+
const REQUIRED_STATE_ARRAY_FIELDS = [
|
|
66
|
+
'events',
|
|
67
|
+
'syncRecords',
|
|
68
|
+
'bridgeRecords',
|
|
69
|
+
'eventStores',
|
|
70
|
+
'syncTransports',
|
|
71
|
+
'replayTargets',
|
|
72
|
+
'bridges',
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* Strukturelle Validierung: Top-Level-Form, `version`, `exportedAt`,
|
|
76
|
+
* `state` und die sieben benannten Array-Felder. Wirft mit konkreter
|
|
77
|
+
* Message pro Verletzung.
|
|
78
|
+
*/
|
|
79
|
+
export function parseSnapshot(json) {
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(json);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw new Error(`parseSnapshot: invalid JSON — ${err instanceof Error ? err.message : String(err)}`);
|
|
86
|
+
}
|
|
87
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
88
|
+
throw new Error('parseSnapshot: top-level must be a plain object');
|
|
89
|
+
}
|
|
90
|
+
const obj = parsed;
|
|
91
|
+
if (obj.version !== SNAPSHOT_VERSION) {
|
|
92
|
+
throw new Error(`parseSnapshot: unsupported version "${String(obj.version)}" — expected '${SNAPSHOT_VERSION}'`);
|
|
93
|
+
}
|
|
94
|
+
if (typeof obj.exportedAt !== 'number' || !Number.isFinite(obj.exportedAt)) {
|
|
95
|
+
throw new Error('parseSnapshot: exportedAt must be a finite number');
|
|
96
|
+
}
|
|
97
|
+
const state = obj.state;
|
|
98
|
+
if (state === null || typeof state !== 'object' || Array.isArray(state)) {
|
|
99
|
+
throw new Error('parseSnapshot: state must be a plain object');
|
|
100
|
+
}
|
|
101
|
+
const stateObj = state;
|
|
102
|
+
for (const fieldName of REQUIRED_STATE_ARRAY_FIELDS) {
|
|
103
|
+
if (!(fieldName in stateObj)) {
|
|
104
|
+
throw new Error(`parseSnapshot: state.${fieldName} is missing`);
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(stateObj[fieldName])) {
|
|
107
|
+
throw new Error(`parseSnapshot: state.${fieldName} must be an array (got ${typeOfDetailed(stateObj[fieldName])})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
function typeOfDetailed(value) {
|
|
113
|
+
if (value === null)
|
|
114
|
+
return 'null';
|
|
115
|
+
if (Array.isArray(value))
|
|
116
|
+
return 'array';
|
|
117
|
+
return typeof value;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=snapshot-export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-export.js","sourceRoot":"","sources":["../src/snapshot-export.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createTimeTravelController` — ephemerer Controller, baut View-State zu
|
|
3
|
+
* einer beliebigen Stream-Version (v0.16.2 Sub 3).
|
|
4
|
+
*
|
|
5
|
+
* Architektur-Verträge aus `design/calendar/16-replay-target-architecture.md`:
|
|
6
|
+
* - **Eigene Subscribe-Seite**, NICHT im `DevtoolsState`-Snapshot — Slider-
|
|
7
|
+
* Bewegungen churchen nicht die Always-On-Listener im Inspector.
|
|
8
|
+
* - **Pure Fold-Kern wiederverwendet**: nutzt `foldEvents` aus
|
|
9
|
+
* `replay-engine.ts`, KEINE eigene Engine. Live-Engine + Controller
|
|
10
|
+
* produzieren bit-identische Snapshots (Sub 1 verriegelt).
|
|
11
|
+
* - **Stale-Cancel via running-counter**: schnelle Slider-Drags droppen
|
|
12
|
+
* ältere `goTo`-Resolves silently.
|
|
13
|
+
* - **Kein Range-Read**: `eventStore.read(streamId)` komplett, filter
|
|
14
|
+
* via `event.envelope.version <= targetVersion`. Optimierung erst mit
|
|
15
|
+
* Snapshot-Reuse-Slice.
|
|
16
|
+
* - **dispose-safe**: alle Methoden silent no-op nach dispose.
|
|
17
|
+
* - **`view.status` ist eigenständig** ('idle' | 'loading' | 'ready' | 'error')
|
|
18
|
+
* — getrennt von `view.snapshot.status` (das ist der Fold-Status:
|
|
19
|
+
* 'replaying' / 'live' / 'error'). UI nimmt `view.status` für den
|
|
20
|
+
* Controller-Zustand, nicht den geschachtelten Snapshot-Status.
|
|
21
|
+
*/
|
|
22
|
+
import type { Unsubscribe } from '@bimetal/event-sourcing';
|
|
23
|
+
import { type ReplayEngineSnapshot, type ReplayTargetHandle } from './replay-engine.js';
|
|
24
|
+
import { type JsonDiffEntry } from './inspectable-json.js';
|
|
25
|
+
export type TimeTravelStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
26
|
+
export interface TimeTravelView<S = unknown, R = unknown> {
|
|
27
|
+
readonly status: TimeTravelStatus;
|
|
28
|
+
/** Aktuelle Ziel-Version. Beim ersten `goTo(v)` wird das `v`. */
|
|
29
|
+
readonly cursor: number;
|
|
30
|
+
/** Snapshot zur `cursor`-Version. `state`/`readModel` ableitbar. */
|
|
31
|
+
readonly snapshot: ReplayEngineSnapshot<S, R>;
|
|
32
|
+
/** Höchste im Stream gesehene Version beim letzten erfolgreichen `goTo`. */
|
|
33
|
+
readonly maxVersion: number;
|
|
34
|
+
readonly lastError?: Error;
|
|
35
|
+
}
|
|
36
|
+
export interface TimeTravelController<S = unknown, R = unknown> {
|
|
37
|
+
/** Reference-stable bis zur nächsten Mutation. */
|
|
38
|
+
readonly view: TimeTravelView<S, R>;
|
|
39
|
+
subscribe(listener: () => void): Unsubscribe;
|
|
40
|
+
/**
|
|
41
|
+
* Setzt den Cursor und re-faltet den Stream bis zu dieser Version.
|
|
42
|
+
* Stale-Cancel: ein neuer `goTo`-Aufruf invalidiert ältere laufende
|
|
43
|
+
* Reads — wenn die ältere `read`-Promise später resolved, wird ihr
|
|
44
|
+
* Resultat verworfen.
|
|
45
|
+
*/
|
|
46
|
+
goTo(version: number): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Diff zwischen `view.snapshot.state` und einem Vergleichs-State.
|
|
49
|
+
* - Ohne Argument: gegen Live (= alle Events gefoldet).
|
|
50
|
+
* - Mit `compareToVersion`: gegen genau diese Version.
|
|
51
|
+
*
|
|
52
|
+
* Diff operiert auf `state` (nicht `readModel`) — der Aggregate-Stand
|
|
53
|
+
* ist der semantische Anker. UI kann zusätzlich `readModel`-Diff
|
|
54
|
+
* anzeigen, indem sie `jsonDiff(currentView.snapshot.readModel, ...)`
|
|
55
|
+
* direkt ruft; das ist Konsumenten-Konzern.
|
|
56
|
+
*/
|
|
57
|
+
diff(compareToVersion?: number): Promise<readonly JsonDiffEntry[]>;
|
|
58
|
+
/** Cleanup. Idempotent. Nach `dispose()` no-op'en alle Methoden. */
|
|
59
|
+
dispose(): void;
|
|
60
|
+
}
|
|
61
|
+
export declare function createTimeTravelController<S, R>(handle: ReplayTargetHandle<S, R>): TimeTravelController<S, R>;
|
|
62
|
+
//# sourceMappingURL=time-travel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-travel.d.ts","sourceRoot":"","sources":["../src/time-travel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAe,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAErE,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAEtE,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO;IACtD,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9C,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO;IAC5D,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpC,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW,CAAC;IAC7C;;;;;OAKG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC;;;;;;;;;OASG;IACH,IAAI,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACnE,oEAAoE;IACpE,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,CAAC,EAC7C,MAAM,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,GAC/B,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CA8H5B"}
|