causal-inspector 0.1.0

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 (68) hide show
  1. package/dist/CausalInspector.css +124 -0
  2. package/dist/CausalInspector.d.ts +13 -0
  3. package/dist/CausalInspector.js +257 -0
  4. package/dist/components/CopyablePayload.d.ts +4 -0
  5. package/dist/components/CopyablePayload.js +44 -0
  6. package/dist/components/FilterBar.d.ts +1 -0
  7. package/dist/components/FilterBar.js +23 -0
  8. package/dist/components/GlobalScrubber.d.ts +1 -0
  9. package/dist/components/GlobalScrubber.js +148 -0
  10. package/dist/components/JsonSyntax.d.ts +3 -0
  11. package/dist/components/JsonSyntax.js +40 -0
  12. package/dist/context.d.ts +26 -0
  13. package/dist/context.js +28 -0
  14. package/dist/engines/index.d.ts +22 -0
  15. package/dist/engines/index.js +29 -0
  16. package/dist/engines/query.d.ts +14 -0
  17. package/dist/engines/query.js +241 -0
  18. package/dist/engines/scrubber.d.ts +12 -0
  19. package/dist/engines/scrubber.js +69 -0
  20. package/dist/engines/storage.d.ts +15 -0
  21. package/dist/engines/storage.js +16 -0
  22. package/dist/engines/subscription.d.ts +17 -0
  23. package/dist/engines/subscription.js +44 -0
  24. package/dist/engines/url.d.ts +13 -0
  25. package/dist/engines/url.js +64 -0
  26. package/dist/events.d.ts +77 -0
  27. package/dist/events.js +1 -0
  28. package/dist/index.d.ts +30 -0
  29. package/dist/index.js +29 -0
  30. package/dist/machine/core.d.ts +22 -0
  31. package/dist/machine/core.js +34 -0
  32. package/dist/machine/engine.d.ts +17 -0
  33. package/dist/machine/engine.js +21 -0
  34. package/dist/machine/events.d.ts +18 -0
  35. package/dist/machine/events.js +1 -0
  36. package/dist/machine/hooks.d.ts +23 -0
  37. package/dist/machine/hooks.js +52 -0
  38. package/dist/machine/index.d.ts +5 -0
  39. package/dist/machine/index.js +4 -0
  40. package/dist/machine/store.d.ts +27 -0
  41. package/dist/machine/store.js +42 -0
  42. package/dist/panes/AggregateTimelinePane.d.ts +2 -0
  43. package/dist/panes/AggregateTimelinePane.js +224 -0
  44. package/dist/panes/CausalFlowPane.d.ts +7 -0
  45. package/dist/panes/CausalFlowPane.js +596 -0
  46. package/dist/panes/CausalTreePane.d.ts +5 -0
  47. package/dist/panes/CausalTreePane.js +158 -0
  48. package/dist/panes/CorrelationExplorerPane.d.ts +2 -0
  49. package/dist/panes/CorrelationExplorerPane.js +46 -0
  50. package/dist/panes/LogsPane.d.ts +6 -0
  51. package/dist/panes/LogsPane.js +65 -0
  52. package/dist/panes/TimelinePane.d.ts +6 -0
  53. package/dist/panes/TimelinePane.js +121 -0
  54. package/dist/panes/WaterfallPane.d.ts +2 -0
  55. package/dist/panes/WaterfallPane.js +202 -0
  56. package/dist/queries.d.ts +15 -0
  57. package/dist/queries.js +175 -0
  58. package/dist/reducer.d.ts +4 -0
  59. package/dist/reducer.js +177 -0
  60. package/dist/state.d.ts +34 -0
  61. package/dist/state.js +39 -0
  62. package/dist/theme.d.ts +7 -0
  63. package/dist/theme.js +34 -0
  64. package/dist/types.d.ts +140 -0
  65. package/dist/types.js +1 -0
  66. package/dist/utils.d.ts +14 -0
  67. package/dist/utils.js +91 -0
  68. package/package.json +43 -0
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ function tokenizeJson(json) {
3
+ const tokens = [];
4
+ const re = /("(?:[^"\\]|\\.)*")\s*:|("(?:[^"\\]|\\.)*")|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b)|(\bnull\b)|([{}[\]:,])/g;
5
+ let lastIndex = 0;
6
+ let match;
7
+ while ((match = re.exec(json)) !== null) {
8
+ if (match.index > lastIndex) {
9
+ tokens.push({ text: json.slice(lastIndex, match.index), color: "" });
10
+ }
11
+ if (match[1] !== undefined) {
12
+ tokens.push({ text: match[1], color: "text-blue-400" });
13
+ tokens.push({ text: ":", color: "text-zinc-500" });
14
+ }
15
+ else if (match[2] !== undefined) {
16
+ tokens.push({ text: match[2], color: "text-green-400" });
17
+ }
18
+ else if (match[3] !== undefined) {
19
+ tokens.push({ text: match[3], color: "text-amber-400" });
20
+ }
21
+ else if (match[4] !== undefined) {
22
+ tokens.push({ text: match[4], color: "text-purple-400" });
23
+ }
24
+ else if (match[5] !== undefined) {
25
+ tokens.push({ text: match[5], color: "text-zinc-500" });
26
+ }
27
+ else if (match[6] !== undefined) {
28
+ tokens.push({ text: match[6], color: "text-zinc-500" });
29
+ }
30
+ lastIndex = re.lastIndex;
31
+ }
32
+ if (lastIndex < json.length) {
33
+ tokens.push({ text: json.slice(lastIndex), color: "" });
34
+ }
35
+ return tokens;
36
+ }
37
+ export function JsonSyntax({ json }) {
38
+ const tokens = tokenizeJson(json);
39
+ return (_jsx(_Fragment, { children: tokens.map((t, i) => (_jsx("span", { className: t.color, children: t.text }, i))) }));
40
+ }
@@ -0,0 +1,26 @@
1
+ import { type ReactNode } from "react";
2
+ import { type InspectorState } from "./state";
3
+ import type { InspectorMachineEvent } from "./events";
4
+ import type { EngineCreator } from "./machine";
5
+ type CausalInspectorProviderProps = {
6
+ createEngine: EngineCreator<InspectorState, InspectorMachineEvent>;
7
+ /** Override slices of initial state (e.g. hydrated paneLayout). */
8
+ initialState?: Partial<InspectorState>;
9
+ children: ReactNode;
10
+ };
11
+ /**
12
+ * Provides the causal inspector machine to all child components.
13
+ *
14
+ * Usage:
15
+ * ```tsx
16
+ * <CausalInspectorProvider
17
+ * createEngine={createInspectorEngine(transport, storage)}
18
+ * initialState={{ paneLayout: savedLayout }}
19
+ * >
20
+ * <TimelinePane />
21
+ * <CausalFlowPane />
22
+ * </CausalInspectorProvider>
23
+ * ```
24
+ */
25
+ export declare function CausalInspectorProvider({ createEngine, initialState: overrides, children, }: CausalInspectorProviderProps): import("react/jsx-runtime").JSX.Element;
26
+ export {};
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo } from "react";
3
+ import { Machine, MachineContext } from "./machine";
4
+ import { reducer } from "./reducer";
5
+ import { initialState } from "./state";
6
+ /**
7
+ * Provides the causal inspector machine to all child components.
8
+ *
9
+ * Usage:
10
+ * ```tsx
11
+ * <CausalInspectorProvider
12
+ * createEngine={createInspectorEngine(transport, storage)}
13
+ * initialState={{ paneLayout: savedLayout }}
14
+ * >
15
+ * <TimelinePane />
16
+ * <CausalFlowPane />
17
+ * </CausalInspectorProvider>
18
+ * ```
19
+ */
20
+ export function CausalInspectorProvider({ createEngine, initialState: overrides, children, }) {
21
+ const machine = useMemo(() => new Machine(reducer, createEngine, overrides ? { ...initialState, ...overrides } : initialState),
22
+ // eslint-disable-next-line react-hooks/exhaustive-deps
23
+ []);
24
+ useEffect(() => {
25
+ return () => machine.dispose();
26
+ }, [machine]);
27
+ return (_jsx(MachineContext.Provider, { value: machine, children: children }));
28
+ }
@@ -0,0 +1,22 @@
1
+ import { type EngineCreator } from "../machine";
2
+ import type { InspectorMachineEvent } from "../events";
3
+ import type { InspectorState } from "../state";
4
+ import { type SubscriptionTransport } from "./subscription";
5
+ import { type QueryTransport } from "./query";
6
+ import { type StorageTransport } from "./storage";
7
+ export type { SubscriptionTransport } from "./subscription";
8
+ export type { QueryTransport } from "./query";
9
+ export type { StorageTransport } from "./storage";
10
+ export { createSubscriptionEngine } from "./subscription";
11
+ export { createQueryEngine } from "./query";
12
+ export { createStorageEngine } from "./storage";
13
+ export { createScrubberEngine } from "./scrubber";
14
+ export { createUrlEngine } from "./url";
15
+ export type InspectorTransport = SubscriptionTransport & QueryTransport;
16
+ /**
17
+ * Create the default inspector engine combining subscription + query engines.
18
+ *
19
+ * Pass `storage` to persist pane layout changes. Load initial layout
20
+ * via `CausalInspectorProvider`'s `initialState` prop instead.
21
+ */
22
+ export declare const createInspectorEngine: (transport: InspectorTransport, storage?: StorageTransport) => EngineCreator<InspectorState, InspectorMachineEvent>;
@@ -0,0 +1,29 @@
1
+ import { combineEngineCreators } from "../machine";
2
+ import { createSubscriptionEngine } from "./subscription";
3
+ import { createQueryEngine } from "./query";
4
+ import { createStorageEngine } from "./storage";
5
+ import { createScrubberEngine } from "./scrubber";
6
+ import { createUrlEngine } from "./url";
7
+ export { createSubscriptionEngine } from "./subscription";
8
+ export { createQueryEngine } from "./query";
9
+ export { createStorageEngine } from "./storage";
10
+ export { createScrubberEngine } from "./scrubber";
11
+ export { createUrlEngine } from "./url";
12
+ /**
13
+ * Create the default inspector engine combining subscription + query engines.
14
+ *
15
+ * Pass `storage` to persist pane layout changes. Load initial layout
16
+ * via `CausalInspectorProvider`'s `initialState` prop instead.
17
+ */
18
+ export const createInspectorEngine = (transport, storage) => {
19
+ const engines = [
20
+ createUrlEngine,
21
+ createSubscriptionEngine(transport),
22
+ createQueryEngine(transport),
23
+ createScrubberEngine,
24
+ ];
25
+ if (storage) {
26
+ engines.push(createStorageEngine(storage));
27
+ }
28
+ return combineEngineCreators(...engines);
29
+ };
@@ -0,0 +1,14 @@
1
+ import type { EngineCreator } from "../machine";
2
+ import type { InspectorMachineEvent } from "../events";
3
+ import type { InspectorState } from "../state";
4
+ export type QueryTransport = {
5
+ /** Execute a GraphQL query. Returns the `data` object. */
6
+ query: <T = unknown>(query: string, variables?: Record<string, unknown>) => Promise<T>;
7
+ };
8
+ /**
9
+ * Query engine — fetches data in response to state transitions.
10
+ *
11
+ * State-reactive: watches (curr, prev) diffs for navigation state.
12
+ * Event-reactive: handles explicit requests (load_more, filter_changed, etc.).
13
+ */
14
+ export declare const createQueryEngine: (transport: QueryTransport) => EngineCreator<InspectorState, InspectorMachineEvent>;
@@ -0,0 +1,241 @@
1
+ import { INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECTOR_CORRELATIONS, INSPECTOR_REACTOR_DEPENDENCIES, INSPECTOR_AGGREGATE_KEYS, INSPECTOR_AGGREGATE_LIFECYCLE, INSPECTOR_REACTOR_LOGS_BY_CORRELATION, INSPECTOR_REACTOR_DESCRIPTIONS, INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, INSPECTOR_AGGREGATE_TIMELINE, INSPECTOR_REACTOR_OUTCOMES, } from "../queries";
2
+ /**
3
+ * Query engine — fetches data in response to state transitions.
4
+ *
5
+ * State-reactive: watches (curr, prev) diffs for navigation state.
6
+ * Event-reactive: handles explicit requests (load_more, filter_changed, etc.).
7
+ */
8
+ export const createQueryEngine = (transport) => {
9
+ return (dispatch, getState) => {
10
+ let flowPollTimer = null;
11
+ let correlationPollTimer = null;
12
+ // Stale-response guards
13
+ let activeCausalSeq = null;
14
+ let activeFlowCorrelationId = null;
15
+ const fetchEvents = async () => {
16
+ const state = getState();
17
+ const cursor = state.events.length > 0
18
+ ? state.events[state.events.length - 1].seq
19
+ : undefined;
20
+ try {
21
+ const data = await transport.query(INSPECTOR_EVENTS, {
22
+ limit: 50,
23
+ cursor,
24
+ search: state.filters.search || undefined,
25
+ from: state.filters.from || undefined,
26
+ to: state.filters.to || undefined,
27
+ correlationId: state.filters.correlationId || undefined,
28
+ aggregateKey: state.filters.aggregateKey || undefined,
29
+ });
30
+ dispatch({
31
+ type: "events/page_loaded",
32
+ payload: {
33
+ events: data.inspectorEvents.events,
34
+ hasMore: data.inspectorEvents.nextCursor != null,
35
+ },
36
+ });
37
+ }
38
+ catch (e) {
39
+ console.error("[causal-inspector] fetch events failed:", e);
40
+ }
41
+ };
42
+ const fetchCausalTree = async (seq) => {
43
+ activeCausalSeq = seq;
44
+ try {
45
+ const data = await transport.query(INSPECTOR_CAUSAL_TREE, { seq });
46
+ if (activeCausalSeq !== seq)
47
+ return; // stale
48
+ dispatch({
49
+ type: "events/causal_tree_loaded",
50
+ payload: data.inspectorCausalTree,
51
+ });
52
+ }
53
+ catch (e) {
54
+ console.error("[causal-inspector] fetch causal tree failed:", e);
55
+ }
56
+ };
57
+ const fetchFlow = async (correlationId) => {
58
+ activeFlowCorrelationId = correlationId;
59
+ try {
60
+ const data = await transport.query(INSPECTOR_CAUSAL_FLOW, { correlationId });
61
+ if (activeFlowCorrelationId !== correlationId)
62
+ return; // stale
63
+ dispatch({
64
+ type: "events/flow_loaded",
65
+ payload: data.inspectorCausalFlow.events,
66
+ });
67
+ }
68
+ catch (e) {
69
+ console.error("[causal-inspector] fetch flow failed:", e);
70
+ }
71
+ };
72
+ const fetchFlowMetadata = async (correlationId) => {
73
+ try {
74
+ const [descData, snapshotData, aggTimelineData, outcomeData] = await Promise.all([
75
+ transport.query(INSPECTOR_REACTOR_DESCRIPTIONS, { correlationId }),
76
+ transport.query(INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, { correlationId }),
77
+ transport.query(INSPECTOR_AGGREGATE_TIMELINE, { correlationId }),
78
+ transport.query(INSPECTOR_REACTOR_OUTCOMES, { correlationId }),
79
+ ]);
80
+ if (activeFlowCorrelationId !== correlationId)
81
+ return; // stale
82
+ dispatch({
83
+ type: "events/descriptions_loaded",
84
+ payload: {
85
+ correlationId,
86
+ descriptions: descData.inspectorReactorDescriptions,
87
+ },
88
+ });
89
+ dispatch({
90
+ type: "events/description_snapshots_loaded",
91
+ payload: {
92
+ correlationId,
93
+ snapshots: snapshotData.inspectorReactorDescriptionSnapshots,
94
+ },
95
+ });
96
+ dispatch({
97
+ type: "events/aggregate_timeline_loaded",
98
+ payload: {
99
+ correlationId,
100
+ entries: aggTimelineData.inspectorAggregateTimeline,
101
+ },
102
+ });
103
+ dispatch({
104
+ type: "events/outcomes_loaded",
105
+ payload: {
106
+ correlationId,
107
+ outcomes: outcomeData.inspectorReactorOutcomes,
108
+ },
109
+ });
110
+ }
111
+ catch (e) {
112
+ console.error("[causal-inspector] fetch flow metadata failed:", e);
113
+ }
114
+ };
115
+ const fetchLogs = async (correlationId) => {
116
+ try {
117
+ const data = await transport.query(INSPECTOR_REACTOR_LOGS_BY_CORRELATION, { correlationId });
118
+ dispatch({ type: "events/logs_loaded", payload: data.inspectorReactorLogsByCorrelation });
119
+ }
120
+ catch (e) {
121
+ console.error("[causal-inspector] fetch logs failed:", e);
122
+ }
123
+ };
124
+ const fetchCorrelations = async (search) => {
125
+ try {
126
+ const data = await transport.query(INSPECTOR_CORRELATIONS, { search: search || undefined, limit: 100 });
127
+ dispatch({
128
+ type: "events/correlations_loaded",
129
+ payload: data.inspectorCorrelations,
130
+ });
131
+ }
132
+ catch (e) {
133
+ console.error("[causal-inspector] fetch correlations failed:", e);
134
+ }
135
+ };
136
+ const fetchReactorDependencies = async () => {
137
+ try {
138
+ const data = await transport.query(INSPECTOR_REACTOR_DEPENDENCIES);
139
+ dispatch({
140
+ type: "events/reactor_dependencies_loaded",
141
+ payload: data.inspectorReactorDependencies,
142
+ });
143
+ }
144
+ catch (e) {
145
+ console.error("[causal-inspector] fetch reactor dependencies failed:", e);
146
+ }
147
+ };
148
+ const fetchAggregateKeys = async () => {
149
+ try {
150
+ const data = await transport.query(INSPECTOR_AGGREGATE_KEYS);
151
+ dispatch({
152
+ type: "events/aggregate_keys_loaded",
153
+ payload: data.inspectorAggregateKeys,
154
+ });
155
+ }
156
+ catch (e) {
157
+ console.error("[causal-inspector] fetch aggregate keys failed:", e);
158
+ }
159
+ };
160
+ const fetchAggregateLifecycle = async (aggregateKey) => {
161
+ try {
162
+ const data = await transport.query(INSPECTOR_AGGREGATE_LIFECYCLE, { aggregateKey, limit: 200 });
163
+ dispatch({
164
+ type: "events/aggregate_lifecycle_loaded",
165
+ payload: { key: aggregateKey, entries: data.inspectorAggregateLifecycle },
166
+ });
167
+ }
168
+ catch (e) {
169
+ console.error("[causal-inspector] fetch aggregate lifecycle failed:", e);
170
+ }
171
+ };
172
+ const startFlowPolling = (correlationId) => {
173
+ stopFlowPolling();
174
+ fetchFlowMetadata(correlationId);
175
+ flowPollTimer = setInterval(() => fetchFlowMetadata(correlationId), 5000);
176
+ };
177
+ const stopFlowPolling = () => {
178
+ if (flowPollTimer) {
179
+ clearInterval(flowPollTimer);
180
+ flowPollTimer = null;
181
+ }
182
+ };
183
+ const stopCorrelationPolling = () => {
184
+ if (correlationPollTimer) {
185
+ clearInterval(correlationPollTimer);
186
+ correlationPollTimer = null;
187
+ }
188
+ };
189
+ // Initial load
190
+ fetchEvents();
191
+ fetchCorrelations();
192
+ fetchReactorDependencies();
193
+ fetchAggregateKeys();
194
+ return {
195
+ handleEvent: (event, curr, prev) => {
196
+ // ── State-reactive: navigation transitions ──
197
+ if (curr.flowCorrelationId !== prev.flowCorrelationId) {
198
+ if (curr.flowCorrelationId) {
199
+ // Flow opened
200
+ fetchFlow(curr.flowCorrelationId);
201
+ startFlowPolling(curr.flowCorrelationId);
202
+ fetchLogs(curr.flowCorrelationId);
203
+ stopCorrelationPolling();
204
+ }
205
+ else {
206
+ // Flow closed
207
+ stopFlowPolling();
208
+ }
209
+ }
210
+ // ── Event-reactive: explicit user requests ──
211
+ switch (event.type) {
212
+ case "ui/load_more_requested":
213
+ fetchEvents();
214
+ break;
215
+ case "ui/event_selected":
216
+ fetchCausalTree(event.payload.seq);
217
+ break;
218
+ case "ui/filter_changed":
219
+ dispatch({
220
+ type: "events/page_loaded",
221
+ payload: { events: [], hasMore: true },
222
+ });
223
+ fetchEvents();
224
+ break;
225
+ case "ui/correlations_requested":
226
+ fetchCorrelations(event.payload.search);
227
+ stopCorrelationPolling();
228
+ correlationPollTimer = setInterval(() => fetchCorrelations(event.payload.search), 5000);
229
+ break;
230
+ case "ui/aggregate_lifecycle_requested":
231
+ fetchAggregateLifecycle(event.payload.aggregateKey);
232
+ break;
233
+ }
234
+ },
235
+ dispose: () => {
236
+ stopFlowPolling();
237
+ stopCorrelationPolling();
238
+ },
239
+ };
240
+ };
241
+ };
@@ -0,0 +1,12 @@
1
+ import type { EngineCreator } from "../machine";
2
+ import type { InspectorMachineEvent } from "../events";
3
+ import type { InspectorState } from "../state";
4
+ /**
5
+ * Scrubber playback engine — advances scrubberEnd on a timer
6
+ * when scrubberPlaying is true.
7
+ *
8
+ * State-reactive: watches (curr, prev) diffs instead of specific events.
9
+ * Context-sensitive: derives the walkable sequence from current state
10
+ * (global events, flow events, or filtered subset).
11
+ */
12
+ export declare const createScrubberEngine: EngineCreator<InspectorState, InspectorMachineEvent>;
@@ -0,0 +1,69 @@
1
+ import { getScrubberSequence } from "../utils";
2
+ /**
3
+ * Scrubber playback engine — advances scrubberEnd on a timer
4
+ * when scrubberPlaying is true.
5
+ *
6
+ * State-reactive: watches (curr, prev) diffs instead of specific events.
7
+ * Context-sensitive: derives the walkable sequence from current state
8
+ * (global events, flow events, or filtered subset).
9
+ */
10
+ export const createScrubberEngine = (dispatch, getState) => {
11
+ let timer = null;
12
+ function stop() {
13
+ if (timer != null) {
14
+ clearInterval(timer);
15
+ timer = null;
16
+ }
17
+ }
18
+ function start() {
19
+ stop();
20
+ const { scrubberSpeed } = getState();
21
+ timer = setInterval(() => {
22
+ const state = getState();
23
+ if (!state.scrubberPlaying) {
24
+ stop();
25
+ return;
26
+ }
27
+ const seqs = getScrubberSequence(state);
28
+ if (seqs.length === 0) {
29
+ stop();
30
+ return;
31
+ }
32
+ const currentEnd = state.scrubberEnd;
33
+ if (currentEnd == null) {
34
+ // At "show all" — set to first seq to begin replay
35
+ dispatch({ type: "ui/scrubber_end_changed", payload: { end: seqs[0] } });
36
+ return;
37
+ }
38
+ // Find next seq after current end position
39
+ const nextSeq = seqs.find((s) => s > currentEnd);
40
+ if (nextSeq != null) {
41
+ dispatch({ type: "ui/scrubber_end_changed", payload: { end: nextSeq } });
42
+ }
43
+ else {
44
+ // Reached the end — stop playing
45
+ dispatch({ type: "ui/scrubber_play_toggled" });
46
+ }
47
+ }, scrubberSpeed);
48
+ }
49
+ return {
50
+ handleEvent: (_event, curr, prev) => {
51
+ // Playing state changed
52
+ if (curr.scrubberPlaying !== prev.scrubberPlaying) {
53
+ if (curr.scrubberPlaying)
54
+ start();
55
+ else
56
+ stop();
57
+ }
58
+ // Speed changed while playing → restart with new interval
59
+ if (curr.scrubberSpeed !== prev.scrubberSpeed && curr.scrubberPlaying) {
60
+ start();
61
+ }
62
+ // Flow changed → stop playback
63
+ if (curr.flowCorrelationId !== prev.flowCorrelationId) {
64
+ stop();
65
+ }
66
+ },
67
+ dispose: () => stop(),
68
+ };
69
+ };
@@ -0,0 +1,15 @@
1
+ import type { EngineCreator } from "../machine";
2
+ import type { InspectorMachineEvent } from "../events";
3
+ import type { InspectorState } from "../state";
4
+ import type { PaneLayout } from "../types";
5
+ export type StorageTransport = {
6
+ /** Persist layout to storage. */
7
+ saveLayout: (layout: PaneLayout) => void;
8
+ };
9
+ /**
10
+ * Storage engine — persists pane layout changes as a side effect.
11
+ *
12
+ * Initial layout should be loaded by the consumer and passed via
13
+ * the provider's `initialState` — not dispatched from here.
14
+ */
15
+ export declare const createStorageEngine: (transport: StorageTransport) => EngineCreator<InspectorState, InspectorMachineEvent>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Storage engine — persists pane layout changes as a side effect.
3
+ *
4
+ * Initial layout should be loaded by the consumer and passed via
5
+ * the provider's `initialState` — not dispatched from here.
6
+ */
7
+ export const createStorageEngine = (transport) => {
8
+ return () => ({
9
+ handleEvent: (event) => {
10
+ if (event.type === "ui/layout_changed") {
11
+ transport.saveLayout(event.payload);
12
+ }
13
+ },
14
+ dispose: () => { },
15
+ });
16
+ };
@@ -0,0 +1,17 @@
1
+ import type { EngineCreator } from "../machine";
2
+ import type { InspectorMachineEvent } from "../events";
3
+ import type { InspectorState } from "../state";
4
+ export type SubscriptionTransport = {
5
+ /** Subscribe to a GraphQL subscription. Returns an unsubscribe function. */
6
+ subscribe: (query: string, variables: Record<string, unknown>, onData: (data: unknown) => void, onError?: (error: unknown) => void) => () => void;
7
+ };
8
+ /**
9
+ * Subscription engine — connects to the live event stream via WebSocket.
10
+ *
11
+ * On creation, subscribes to `inspectorEventAdded`. Each event dispatches
12
+ * directly into the reducer as `events/received`.
13
+ *
14
+ * Transport is injected so consumers can use any GraphQL WS client
15
+ * (graphql-ws, Apollo, urql, etc).
16
+ */
17
+ export declare const createSubscriptionEngine: (transport: SubscriptionTransport) => EngineCreator<InspectorState, InspectorMachineEvent>;
@@ -0,0 +1,44 @@
1
+ import { EVENTS_SUBSCRIPTION } from "../queries";
2
+ /**
3
+ * Subscription engine — connects to the live event stream via WebSocket.
4
+ *
5
+ * On creation, subscribes to `inspectorEventAdded`. Each event dispatches
6
+ * directly into the reducer as `events/received`.
7
+ *
8
+ * Transport is injected so consumers can use any GraphQL WS client
9
+ * (graphql-ws, Apollo, urql, etc).
10
+ */
11
+ export const createSubscriptionEngine = (transport) => {
12
+ return (dispatch, getState) => {
13
+ let unsub = null;
14
+ const connect = () => {
15
+ const state = getState();
16
+ const lastSeq = state.events.length > 0 ? state.events[0].seq : undefined;
17
+ let connected = false;
18
+ unsub = transport.subscribe(EVENTS_SUBSCRIPTION, lastSeq != null ? { lastSeq } : {}, (data) => {
19
+ if (!connected) {
20
+ connected = true;
21
+ dispatch({ type: "events/subscription_connected" });
22
+ }
23
+ const event = data
24
+ .inspectorEventAdded;
25
+ if (event) {
26
+ dispatch({ type: "events/received", payload: [event] });
27
+ }
28
+ }, (error) => {
29
+ console.error("[causal-inspector] subscription error:", error);
30
+ dispatch({
31
+ type: "events/subscription_error",
32
+ payload: { message: String(error) },
33
+ });
34
+ });
35
+ };
36
+ // Connect immediately
37
+ connect();
38
+ return {
39
+ dispose: () => {
40
+ unsub?.();
41
+ },
42
+ };
43
+ };
44
+ };
@@ -0,0 +1,13 @@
1
+ import type { EngineCreator } from "../machine";
2
+ import type { InspectorMachineEvent } from "../events";
3
+ import type { InspectorState } from "../state";
4
+ /**
5
+ * URL engine — keeps the browser URL in sync with navigation state.
6
+ *
7
+ * For user-initiated actions (ui/flow_opened, etc.), the reducer updates
8
+ * state directly. This engine just writes the URL as a side effect.
9
+ *
10
+ * For browser-initiated navigation (back/forward), this engine dispatches
11
+ * location/changed so the reducer can update state from the URL.
12
+ */
13
+ export declare const createUrlEngine: EngineCreator<InspectorState, InspectorMachineEvent>;
@@ -0,0 +1,64 @@
1
+ function parseUrl() {
2
+ const params = new URLSearchParams(window.location.search);
3
+ return {
4
+ correlationId: params.get("correlation"),
5
+ handler: params.get("handler"),
6
+ };
7
+ }
8
+ function buildSearch(correlationId, handler) {
9
+ const params = new URLSearchParams(window.location.search);
10
+ if (correlationId)
11
+ params.set("correlation", correlationId);
12
+ else {
13
+ params.delete("correlation");
14
+ params.delete("handler");
15
+ }
16
+ if (handler && correlationId)
17
+ params.set("handler", handler);
18
+ else
19
+ params.delete("handler");
20
+ const search = params.toString();
21
+ return search ? `?${search}` : window.location.pathname;
22
+ }
23
+ /**
24
+ * URL engine — keeps the browser URL in sync with navigation state.
25
+ *
26
+ * For user-initiated actions (ui/flow_opened, etc.), the reducer updates
27
+ * state directly. This engine just writes the URL as a side effect.
28
+ *
29
+ * For browser-initiated navigation (back/forward), this engine dispatches
30
+ * location/changed so the reducer can update state from the URL.
31
+ */
32
+ export const createUrlEngine = (dispatch, _getState) => {
33
+ // Popstate — browser back/forward
34
+ const onPopState = () => {
35
+ dispatch({ type: "location/changed", payload: parseUrl() });
36
+ };
37
+ window.addEventListener("popstate", onPopState);
38
+ // Seed from current URL on init — deferred so the Machine constructor finishes first.
39
+ queueMicrotask(() => {
40
+ const initial = parseUrl();
41
+ if (initial.correlationId || initial.handler) {
42
+ dispatch({ type: "location/changed", payload: initial });
43
+ }
44
+ });
45
+ return {
46
+ handleEvent: (event) => {
47
+ switch (event.type) {
48
+ case "ui/flow_opened":
49
+ window.history.pushState(null, "", buildSearch(event.payload.correlationId, null));
50
+ break;
51
+ case "ui/flow_closed":
52
+ window.history.pushState(null, "", buildSearch(null, null));
53
+ break;
54
+ case "ui/handler_selected":
55
+ // Replace rather than push — handler changes within a flow are fine as one history entry
56
+ window.history.replaceState(null, "", buildSearch(new URLSearchParams(window.location.search).get("correlation"), event.payload.reactorId));
57
+ break;
58
+ }
59
+ },
60
+ dispose: () => {
61
+ window.removeEventListener("popstate", onPopState);
62
+ },
63
+ };
64
+ };