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.
- package/dist/CausalInspector.css +124 -0
- package/dist/CausalInspector.d.ts +13 -0
- package/dist/CausalInspector.js +257 -0
- package/dist/components/CopyablePayload.d.ts +4 -0
- package/dist/components/CopyablePayload.js +44 -0
- package/dist/components/FilterBar.d.ts +1 -0
- package/dist/components/FilterBar.js +23 -0
- package/dist/components/GlobalScrubber.d.ts +1 -0
- package/dist/components/GlobalScrubber.js +148 -0
- package/dist/components/JsonSyntax.d.ts +3 -0
- package/dist/components/JsonSyntax.js +40 -0
- package/dist/context.d.ts +26 -0
- package/dist/context.js +28 -0
- package/dist/engines/index.d.ts +22 -0
- package/dist/engines/index.js +29 -0
- package/dist/engines/query.d.ts +14 -0
- package/dist/engines/query.js +241 -0
- package/dist/engines/scrubber.d.ts +12 -0
- package/dist/engines/scrubber.js +69 -0
- package/dist/engines/storage.d.ts +15 -0
- package/dist/engines/storage.js +16 -0
- package/dist/engines/subscription.d.ts +17 -0
- package/dist/engines/subscription.js +44 -0
- package/dist/engines/url.d.ts +13 -0
- package/dist/engines/url.js +64 -0
- package/dist/events.d.ts +77 -0
- package/dist/events.js +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +29 -0
- package/dist/machine/core.d.ts +22 -0
- package/dist/machine/core.js +34 -0
- package/dist/machine/engine.d.ts +17 -0
- package/dist/machine/engine.js +21 -0
- package/dist/machine/events.d.ts +18 -0
- package/dist/machine/events.js +1 -0
- package/dist/machine/hooks.d.ts +23 -0
- package/dist/machine/hooks.js +52 -0
- package/dist/machine/index.d.ts +5 -0
- package/dist/machine/index.js +4 -0
- package/dist/machine/store.d.ts +27 -0
- package/dist/machine/store.js +42 -0
- package/dist/panes/AggregateTimelinePane.d.ts +2 -0
- package/dist/panes/AggregateTimelinePane.js +224 -0
- package/dist/panes/CausalFlowPane.d.ts +7 -0
- package/dist/panes/CausalFlowPane.js +596 -0
- package/dist/panes/CausalTreePane.d.ts +5 -0
- package/dist/panes/CausalTreePane.js +158 -0
- package/dist/panes/CorrelationExplorerPane.d.ts +2 -0
- package/dist/panes/CorrelationExplorerPane.js +46 -0
- package/dist/panes/LogsPane.d.ts +6 -0
- package/dist/panes/LogsPane.js +65 -0
- package/dist/panes/TimelinePane.d.ts +6 -0
- package/dist/panes/TimelinePane.js +121 -0
- package/dist/panes/WaterfallPane.d.ts +2 -0
- package/dist/panes/WaterfallPane.js +202 -0
- package/dist/queries.d.ts +15 -0
- package/dist/queries.js +175 -0
- package/dist/reducer.d.ts +4 -0
- package/dist/reducer.js +177 -0
- package/dist/state.d.ts +34 -0
- package/dist/state.js +39 -0
- package/dist/theme.d.ts +7 -0
- package/dist/theme.js +34 -0
- package/dist/types.d.ts +140 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +91 -0
- 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 {};
|
package/dist/context.js
ADDED
|
@@ -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
|
+
};
|