causal-inspector 0.1.5 → 0.2.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.d.ts +8 -1
- package/dist/CausalInspector.js +32 -9
- package/dist/components/EffectList.d.ts +4 -0
- package/dist/components/EffectList.js +15 -0
- package/dist/components/FilterBar.js +2 -2
- package/dist/components/GlobalScrubber.js +6 -6
- package/dist/engines/query.js +134 -53
- package/dist/engines/scrubber.js +1 -1
- package/dist/engines/url.d.ts +5 -2
- package/dist/engines/url.js +50 -22
- package/dist/events.d.ts +41 -12
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/panes/AggregateTimelinePane.js +4 -4
- package/dist/panes/CausalFlowPane.js +16 -16
- package/dist/panes/CausalTreePane.js +51 -17
- package/dist/panes/CorrelationExplorerPane.d.ts +2 -2
- package/dist/panes/CorrelationExplorerPane.js +10 -10
- package/dist/panes/LogsPane.js +6 -6
- package/dist/panes/SubjectChainPane.d.ts +1 -0
- package/dist/panes/SubjectChainPane.js +50 -0
- package/dist/panes/TimelinePane.js +32 -16
- package/dist/panes/WaterfallPane.js +185 -72
- package/dist/panes/WorkflowExplorerPane.d.ts +2 -0
- package/dist/panes/WorkflowExplorerPane.js +46 -0
- package/dist/queries.d.ts +16 -11
- package/dist/queries.js +114 -24
- package/dist/reducer.js +102 -36
- package/dist/state.d.ts +16 -5
- package/dist/state.js +17 -6
- package/dist/theme.js +1 -0
- package/dist/types.d.ts +59 -11
- package/dist/utils.js +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SubjectChainMode } from "./types";
|
|
1
2
|
import "./CausalInspector.css";
|
|
2
3
|
export type CausalInspectorProps = {
|
|
3
4
|
/** GraphQL endpoint URL (relative or absolute). Queries POST here, WS connects to {endpoint}/ws */
|
|
@@ -9,5 +10,11 @@ export type CausalInspectorProps = {
|
|
|
9
10
|
};
|
|
10
11
|
/** CSS class for the container */
|
|
11
12
|
className?: string;
|
|
13
|
+
/** Pre-navigate to a specific entity's subject chain on mount */
|
|
14
|
+
initialSubject?: {
|
|
15
|
+
aggregateType: string;
|
|
16
|
+
aggregateId: string;
|
|
17
|
+
mode?: SubjectChainMode;
|
|
18
|
+
};
|
|
12
19
|
};
|
|
13
|
-
export declare function CausalInspector({ endpoint, fetchOptions, className, }: CausalInspectorProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export declare function CausalInspector({ endpoint, fetchOptions, className, initialSubject, }: CausalInspectorProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/CausalInspector.js
CHANGED
|
@@ -8,11 +8,12 @@ import { createInspectorEngine } from "./engines";
|
|
|
8
8
|
import { useSelector, useDispatch } from "./machine";
|
|
9
9
|
import { TimelinePane } from "./panes/TimelinePane";
|
|
10
10
|
import { CausalTreePane } from "./panes/CausalTreePane";
|
|
11
|
+
import { SubjectChainPane } from "./panes/SubjectChainPane";
|
|
11
12
|
import { CausalFlowPane } from "./panes/CausalFlowPane";
|
|
12
13
|
import { LogsPane } from "./panes/LogsPane";
|
|
13
14
|
import { AggregateTimelinePane } from "./panes/AggregateTimelinePane";
|
|
14
15
|
import { WaterfallPane } from "./panes/WaterfallPane";
|
|
15
|
-
import {
|
|
16
|
+
import { WorkflowExplorerPane } from "./panes/WorkflowExplorerPane";
|
|
16
17
|
import { GlobalScrubber } from "./components/GlobalScrubber";
|
|
17
18
|
import "./CausalInspector.css";
|
|
18
19
|
// ── Transport ─────────────────────────────────────────────────
|
|
@@ -139,9 +140,14 @@ const PANE_REGISTRY = [
|
|
|
139
140
|
render: () => _jsx(WaterfallPane, {}),
|
|
140
141
|
},
|
|
141
142
|
{
|
|
142
|
-
name: "
|
|
143
|
-
component: "
|
|
144
|
-
render: () => _jsx(
|
|
143
|
+
name: "Workflows",
|
|
144
|
+
component: "workflows",
|
|
145
|
+
render: () => _jsx(WorkflowExplorerPane, {}),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "Subject Chain",
|
|
149
|
+
component: "subject-chain",
|
|
150
|
+
render: () => _jsx(SubjectChainPane, {}),
|
|
145
151
|
},
|
|
146
152
|
];
|
|
147
153
|
// ── Helpers ───────────────────────────────────────────────────
|
|
@@ -158,8 +164,9 @@ function findTab(model, component) {
|
|
|
158
164
|
// ── InspectorLayout (inner component) ─────────────────────────
|
|
159
165
|
function InspectorLayout() {
|
|
160
166
|
const paneLayout = useSelector((s) => s.paneLayout);
|
|
161
|
-
const
|
|
167
|
+
const flowWorkflowId = useSelector((s) => s.flowWorkflowId);
|
|
162
168
|
const selectedSeq = useSelector((s) => s.selectedSeq);
|
|
169
|
+
const subjectType = useSelector((s) => s.subjectType);
|
|
163
170
|
const dispatch = useDispatch();
|
|
164
171
|
const modelRef = useRef(null);
|
|
165
172
|
if (!modelRef.current) {
|
|
@@ -189,9 +196,13 @@ function InspectorLayout() {
|
|
|
189
196
|
addTab("causal-tree", "Causal Tree");
|
|
190
197
|
}, [selectedSeq, addTab]);
|
|
191
198
|
useEffect(() => {
|
|
192
|
-
if (
|
|
199
|
+
if (flowWorkflowId)
|
|
193
200
|
addTab("causal-flow", "Flow");
|
|
194
|
-
}, [
|
|
201
|
+
}, [flowWorkflowId, addTab]);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (subjectType)
|
|
204
|
+
addTab("subject-chain", "Subject Chain");
|
|
205
|
+
}, [subjectType, addTab]);
|
|
195
206
|
const factory = useCallback((node) => {
|
|
196
207
|
const component = node.getComponent();
|
|
197
208
|
const pane = PANE_REGISTRY.find((p) => p.component === component);
|
|
@@ -249,10 +260,22 @@ function InspectorLayout() {
|
|
|
249
260
|
}
|
|
250
261
|
// ── CausalInspector (public API) ──────────────────────────────
|
|
251
262
|
const savedLayout = loadSavedLayout();
|
|
252
|
-
export function CausalInspector({ endpoint, fetchOptions, className, }) {
|
|
263
|
+
export function CausalInspector({ endpoint, fetchOptions, className, initialSubject, }) {
|
|
253
264
|
const transport = useMemo(() => createTransport(endpoint, fetchOptions),
|
|
254
265
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
255
266
|
[endpoint]);
|
|
256
267
|
const createEngine = useMemo(() => createInspectorEngine(transport, storage), [transport]);
|
|
257
|
-
|
|
268
|
+
const initialState = useMemo(() => {
|
|
269
|
+
const base = savedLayout ? { paneLayout: savedLayout } : undefined;
|
|
270
|
+
if (!initialSubject)
|
|
271
|
+
return base;
|
|
272
|
+
return {
|
|
273
|
+
...base,
|
|
274
|
+
subjectType: initialSubject.aggregateType,
|
|
275
|
+
subjectId: initialSubject.aggregateId,
|
|
276
|
+
subjectMode: initialSubject.mode ?? "both",
|
|
277
|
+
subjectChainLoading: true,
|
|
278
|
+
};
|
|
279
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
280
|
+
return (_jsx("div", { className: `causal-inspector${className ? ` ${className}` : ""}`, children: _jsx(CausalInspectorProvider, { createEngine: createEngine, initialState: initialState, children: _jsx(InspectorLayout, {}) }) }));
|
|
258
281
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
4
|
+
function EffectRow({ effect }) {
|
|
5
|
+
const [expanded, setExpanded] = useState(false);
|
|
6
|
+
const valueStr = JSON.stringify(effect.value, null, 2);
|
|
7
|
+
const preview = valueStr.length > 120 ? valueStr.slice(0, 120) + "…" : valueStr;
|
|
8
|
+
return (_jsxs("div", { className: "py-1", children: [_jsxs("button", { onClick: () => setExpanded((v) => !v), className: "flex items-start gap-1.5 w-full text-left group/effect", children: [_jsx("span", { className: "mt-0.5 text-muted-foreground/40 shrink-0", children: expanded ? _jsx(ChevronDown, { size: 10 }) : _jsx(ChevronRight, { size: 10 }) }), _jsx("span", { className: "text-[9px] font-mono text-indigo-400/70 shrink-0", children: effect.consumer }), _jsx("span", { className: "text-[9px] text-muted-foreground/40 shrink-0", children: "\u00B7" }), _jsx("span", { className: "text-[9px] font-mono text-foreground/70 shrink-0", children: effect.label }), !expanded && (_jsx("span", { className: "text-[9px] font-mono text-muted-foreground/50 truncate min-w-0", children: preview }))] }), expanded && (_jsx("pre", { className: "mt-1 ml-4 text-[9px] font-mono text-foreground/60 whitespace-pre-wrap break-all bg-white/[0.02] rounded p-1.5 border border-border/50", children: valueStr }))] }));
|
|
9
|
+
}
|
|
10
|
+
export function EffectList({ effects }) {
|
|
11
|
+
if (effects.length === 0) {
|
|
12
|
+
return (_jsx("div", { className: "py-1 text-[9px] text-muted-foreground/40 italic ml-3", children: "No effects" }));
|
|
13
|
+
}
|
|
14
|
+
return (_jsx("div", { className: "ml-3 border-l border-indigo-500/15 pl-2", children: effects.map((e, i) => (_jsx(EffectRow, { effect: e }, `${e.consumer}:${e.label}:${i}`))) }));
|
|
15
|
+
}
|
|
@@ -7,9 +7,9 @@ export function FilterBar() {
|
|
|
7
7
|
return (_jsxs("div", { className: "flex flex-wrap items-center gap-2 px-3 py-2 border-b border-border", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsxs("div", { className: "relative flex items-center", children: [_jsx(Search, { size: 12, className: "absolute left-2.5 text-muted-foreground pointer-events-none" }), _jsx("input", { type: "text", placeholder: "search events...", value: filters.search, onChange: (e) => dispatch({
|
|
8
8
|
type: "ui/filter_changed",
|
|
9
9
|
payload: { search: e.target.value },
|
|
10
|
-
}), className: "pl-7 pr-2 py-1.5 text-xs rounded-md bg-background/50 border border-border text-foreground placeholder:text-muted-foreground w-64 focus:outline-none focus:ring-1 focus:ring-indigo-500/40 focus:border-indigo-500/30 transition-all" })] }), filters.
|
|
10
|
+
}), className: "pl-7 pr-2 py-1.5 text-xs rounded-md bg-background/50 border border-border text-foreground placeholder:text-muted-foreground w-64 focus:outline-none focus:ring-1 focus:ring-indigo-500/40 focus:border-indigo-500/30 transition-all" })] }), filters.workflowId && (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-px h-4 bg-border" }), _jsxs("span", { className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-[10px] font-mono", children: [filters.workflowId.slice(0, 8), _jsx("button", { onClick: () => dispatch({
|
|
11
11
|
type: "ui/filter_changed",
|
|
12
|
-
payload: {
|
|
12
|
+
payload: { workflowId: null },
|
|
13
13
|
}), className: "hover:text-foreground transition-colors", children: _jsx(X, { size: 10 }) })] })] })), filters.aggregateKey && (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-px h-4 bg-border" }), _jsxs("span", { className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-teal-500/10 border border-teal-500/20 text-teal-400 text-[10px] font-mono", children: [filters.aggregateKey.split(":")[0], ":", filters.aggregateKey.split(":").slice(1).join(":").slice(0, 8), _jsx("button", { onClick: () => dispatch({
|
|
14
14
|
type: "ui/filter_changed",
|
|
15
15
|
payload: { aggregateKey: null },
|
|
@@ -58,15 +58,15 @@ function RangeSlider({ seqs, start, end, onStartChange, onEndChange, }) {
|
|
|
58
58
|
// ---------------------------------------------------------------------------
|
|
59
59
|
// Context label
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
|
-
function getContextLabel(
|
|
62
|
-
if (!
|
|
61
|
+
function getContextLabel(flowWorkflowId, flowSelection) {
|
|
62
|
+
if (!flowWorkflowId)
|
|
63
63
|
return "All events";
|
|
64
64
|
if (flowSelection) {
|
|
65
65
|
if (flowSelection.kind === "event-type")
|
|
66
66
|
return `${flowSelection.name} events`;
|
|
67
67
|
return `Reactor ${flowSelection.reactorId}`;
|
|
68
68
|
}
|
|
69
|
-
return `Flow ${
|
|
69
|
+
return `Flow ${flowWorkflowId.slice(0, 8)}`;
|
|
70
70
|
}
|
|
71
71
|
// ---------------------------------------------------------------------------
|
|
72
72
|
// GlobalScrubber
|
|
@@ -76,12 +76,12 @@ export function GlobalScrubber() {
|
|
|
76
76
|
const scrubberEnd = useSelector((s) => s.scrubberEnd);
|
|
77
77
|
const playing = useSelector((s) => s.scrubberPlaying);
|
|
78
78
|
const speed = useSelector((s) => s.scrubberSpeed);
|
|
79
|
-
const
|
|
79
|
+
const flowWorkflowId = useSelector((s) => s.flowWorkflowId);
|
|
80
80
|
const flowSelection = useSelector((s) => s.flowSelection);
|
|
81
81
|
const flowData = useSelector((s) => s.flowData);
|
|
82
82
|
const events = useSelector((s) => s.events);
|
|
83
83
|
const dispatch = useDispatch();
|
|
84
|
-
const seqs = useMemo(() => getScrubberSequence({
|
|
84
|
+
const seqs = useMemo(() => getScrubberSequence({ flowWorkflowId, flowData, flowSelection, events }), [flowWorkflowId, flowData, flowSelection, events]);
|
|
85
85
|
const endVal = scrubberEnd ?? (seqs[seqs.length - 1] ?? 0);
|
|
86
86
|
const isAtEnd = scrubberEnd == null || (seqs.length > 0 && scrubberEnd >= seqs[seqs.length - 1]);
|
|
87
87
|
// Count events in range
|
|
@@ -139,7 +139,7 @@ export function GlobalScrubber() {
|
|
|
139
139
|
const disabled = seqs.length < 2;
|
|
140
140
|
const speedLabel = speed <= 50 ? "4x" : speed <= 150 ? "2x" : speed <= 300 ? "1x" : "0.5x";
|
|
141
141
|
const btnClass = `p-1.5 rounded-md transition-all duration-150 ${disabled ? "text-muted-foreground/20 cursor-default" : "text-muted-foreground/60 hover:text-foreground hover:bg-white/[0.05]"}`;
|
|
142
|
-
const contextLabel = getContextLabel(
|
|
142
|
+
const contextLabel = getContextLabel(flowWorkflowId, flowSelection);
|
|
143
143
|
return (_jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-t border-border shrink-0", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsx("span", { className: "text-[9px] font-medium text-muted-foreground/40 uppercase tracking-wider shrink-0 max-w-[120px] truncate", title: contextLabel, children: contextLabel }), _jsx("button", { onClick: disabled ? undefined : reset, className: btnClass, title: "Reset to start", children: _jsx(RotateCcw, { size: 12 }) }), _jsx("button", { onClick: disabled ? undefined : stepBack, className: btnClass, title: "Step back", children: _jsx(SkipBack, { size: 12 }) }), _jsx("button", { onClick: disabled ? undefined : togglePlay, className: `p-1.5 rounded-md transition-all duration-150 ${disabled
|
|
144
144
|
? "text-muted-foreground/20 cursor-default"
|
|
145
145
|
: playing
|
package/dist/engines/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW,
|
|
1
|
+
import { INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECTOR_WORKFLOWS, INSPECTOR_REACTOR_DEPENDENCIES, INSPECTOR_AGGREGATE_KEYS, INSPECTOR_AGGREGATE_LIFECYCLE, INSPECTOR_REACTOR_LOGS_BY_WORKFLOW, INSPECTOR_REACTOR_DESCRIPTIONS, INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, INSPECTOR_AGGREGATE_TIMELINE, INSPECTOR_REACTOR_OUTCOMES, INSPECTOR_REACTOR_ATTEMPTS, INSPECTOR_SUBJECT_CHAIN, INSPECTOR_EFFECTS_FOR_EVENT, } from "../queries";
|
|
2
2
|
/**
|
|
3
3
|
* Query engine — fetches data in response to state transitions.
|
|
4
4
|
*
|
|
@@ -8,10 +8,11 @@ import { INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECT
|
|
|
8
8
|
export const createQueryEngine = (transport) => {
|
|
9
9
|
return (dispatch, getState) => {
|
|
10
10
|
let flowPollTimer = null;
|
|
11
|
-
let
|
|
11
|
+
let workflowPollTimer = null;
|
|
12
12
|
// Stale-response guards
|
|
13
13
|
let activeCausalSeq = null;
|
|
14
|
-
let
|
|
14
|
+
let activeFlowWorkflowId = null;
|
|
15
|
+
let activeSubjectKey = null;
|
|
15
16
|
const fetchEvents = async () => {
|
|
16
17
|
const state = getState();
|
|
17
18
|
const cursor = state.events.length > 0
|
|
@@ -22,7 +23,7 @@ export const createQueryEngine = (transport) => {
|
|
|
22
23
|
limit: 50,
|
|
23
24
|
cursor,
|
|
24
25
|
search: state.filters.search || undefined,
|
|
25
|
-
|
|
26
|
+
workflowId: state.filters.workflowId || undefined,
|
|
26
27
|
aggregateKey: state.filters.aggregateKey || undefined,
|
|
27
28
|
});
|
|
28
29
|
dispatch({
|
|
@@ -52,11 +53,11 @@ export const createQueryEngine = (transport) => {
|
|
|
52
53
|
console.error("[causal-inspector] fetch causal tree failed:", e);
|
|
53
54
|
}
|
|
54
55
|
};
|
|
55
|
-
const fetchFlow = async (
|
|
56
|
-
|
|
56
|
+
const fetchFlow = async (workflowId) => {
|
|
57
|
+
activeFlowWorkflowId = workflowId;
|
|
57
58
|
try {
|
|
58
|
-
const data = await transport.query(INSPECTOR_CAUSAL_FLOW, {
|
|
59
|
-
if (
|
|
59
|
+
const data = await transport.query(INSPECTOR_CAUSAL_FLOW, { workflowId });
|
|
60
|
+
if (activeFlowWorkflowId !== workflowId)
|
|
60
61
|
return; // stale
|
|
61
62
|
dispatch({
|
|
62
63
|
type: "events/flow_loaded",
|
|
@@ -67,80 +68,88 @@ export const createQueryEngine = (transport) => {
|
|
|
67
68
|
console.error("[causal-inspector] fetch flow failed:", e);
|
|
68
69
|
}
|
|
69
70
|
};
|
|
70
|
-
const fetchFlowMetadata = async (
|
|
71
|
+
const fetchFlowMetadata = async (workflowId) => {
|
|
71
72
|
try {
|
|
72
|
-
const [descData, snapshotData, aggTimelineData, outcomeData] = await Promise.all([
|
|
73
|
-
transport.query(INSPECTOR_REACTOR_DESCRIPTIONS, {
|
|
74
|
-
transport.query(INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, {
|
|
75
|
-
transport.query(INSPECTOR_AGGREGATE_TIMELINE, {
|
|
76
|
-
transport.query(INSPECTOR_REACTOR_OUTCOMES, {
|
|
73
|
+
const [descData, snapshotData, aggTimelineData, outcomeData, attemptData] = await Promise.all([
|
|
74
|
+
transport.query(INSPECTOR_REACTOR_DESCRIPTIONS, { workflowId }),
|
|
75
|
+
transport.query(INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, { workflowId }),
|
|
76
|
+
transport.query(INSPECTOR_AGGREGATE_TIMELINE, { workflowId }),
|
|
77
|
+
transport.query(INSPECTOR_REACTOR_OUTCOMES, { workflowId }),
|
|
78
|
+
transport.query(INSPECTOR_REACTOR_ATTEMPTS, { workflowId }),
|
|
77
79
|
]);
|
|
78
|
-
if (
|
|
80
|
+
if (activeFlowWorkflowId !== workflowId)
|
|
79
81
|
return; // stale
|
|
80
82
|
dispatch({
|
|
81
83
|
type: "events/descriptions_loaded",
|
|
82
84
|
payload: {
|
|
83
|
-
|
|
85
|
+
workflowId,
|
|
84
86
|
descriptions: descData.inspectorReactorDescriptions,
|
|
85
87
|
},
|
|
86
88
|
});
|
|
87
89
|
dispatch({
|
|
88
90
|
type: "events/description_snapshots_loaded",
|
|
89
91
|
payload: {
|
|
90
|
-
|
|
92
|
+
workflowId,
|
|
91
93
|
snapshots: snapshotData.inspectorReactorDescriptionSnapshots,
|
|
92
94
|
},
|
|
93
95
|
});
|
|
94
96
|
dispatch({
|
|
95
97
|
type: "events/aggregate_timeline_loaded",
|
|
96
98
|
payload: {
|
|
97
|
-
|
|
99
|
+
workflowId,
|
|
98
100
|
entries: aggTimelineData.inspectorAggregateTimeline,
|
|
99
101
|
},
|
|
100
102
|
});
|
|
101
103
|
dispatch({
|
|
102
104
|
type: "events/outcomes_loaded",
|
|
103
105
|
payload: {
|
|
104
|
-
|
|
106
|
+
workflowId,
|
|
105
107
|
outcomes: outcomeData.inspectorReactorOutcomes,
|
|
106
108
|
},
|
|
107
109
|
});
|
|
110
|
+
dispatch({
|
|
111
|
+
type: "events/attempts_loaded",
|
|
112
|
+
payload: {
|
|
113
|
+
workflowId,
|
|
114
|
+
attempts: attemptData.inspectorReactorAttempts,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
108
117
|
}
|
|
109
118
|
catch (e) {
|
|
110
119
|
console.error("[causal-inspector] fetch flow metadata failed:", e);
|
|
111
120
|
}
|
|
112
121
|
};
|
|
113
|
-
const fetchLogs = async (
|
|
122
|
+
const fetchLogs = async (workflowId) => {
|
|
114
123
|
try {
|
|
115
|
-
const data = await transport.query(
|
|
116
|
-
dispatch({ type: "events/logs_loaded", payload: data.
|
|
124
|
+
const data = await transport.query(INSPECTOR_REACTOR_LOGS_BY_WORKFLOW, { workflowId });
|
|
125
|
+
dispatch({ type: "events/logs_loaded", payload: data.inspectorReactorLogsByWorkflow });
|
|
117
126
|
}
|
|
118
127
|
catch (e) {
|
|
119
128
|
console.error("[causal-inspector] fetch logs failed:", e);
|
|
120
129
|
}
|
|
121
130
|
};
|
|
122
|
-
let
|
|
123
|
-
const
|
|
131
|
+
let workflowCursor = null;
|
|
132
|
+
const fetchWorkflows = async (opts) => {
|
|
124
133
|
const append = opts?.append ?? false;
|
|
125
|
-
const cursor = append ?
|
|
134
|
+
const cursor = append ? workflowCursor : undefined;
|
|
126
135
|
try {
|
|
127
|
-
const data = await transport.query(
|
|
136
|
+
const data = await transport.query(INSPECTOR_WORKFLOWS, {
|
|
128
137
|
search: opts?.search || undefined,
|
|
129
138
|
limit: 50,
|
|
130
139
|
cursor: cursor || undefined,
|
|
131
140
|
});
|
|
132
|
-
|
|
141
|
+
workflowCursor = data.inspectorWorkflows.nextCursor;
|
|
133
142
|
dispatch({
|
|
134
|
-
type: "events/
|
|
143
|
+
type: "events/workflows_loaded",
|
|
135
144
|
payload: {
|
|
136
|
-
|
|
137
|
-
hasMore: data.
|
|
145
|
+
workflows: data.inspectorWorkflows.workflows,
|
|
146
|
+
hasMore: data.inspectorWorkflows.nextCursor != null,
|
|
138
147
|
append,
|
|
139
148
|
},
|
|
140
149
|
});
|
|
141
150
|
}
|
|
142
151
|
catch (e) {
|
|
143
|
-
console.error("[causal-inspector] fetch
|
|
152
|
+
console.error("[causal-inspector] fetch workflows failed:", e);
|
|
144
153
|
}
|
|
145
154
|
};
|
|
146
155
|
const fetchReactorDependencies = async () => {
|
|
@@ -179,10 +188,54 @@ export const createQueryEngine = (transport) => {
|
|
|
179
188
|
console.error("[causal-inspector] fetch aggregate lifecycle failed:", e);
|
|
180
189
|
}
|
|
181
190
|
};
|
|
182
|
-
const
|
|
191
|
+
const fetchSubjectChain = async (aggregateType, aggregateId, mode, cursor) => {
|
|
192
|
+
const key = `${aggregateType}:${aggregateId}:${mode}`;
|
|
193
|
+
activeSubjectKey = key;
|
|
194
|
+
try {
|
|
195
|
+
const data = await transport.query(INSPECTOR_SUBJECT_CHAIN, {
|
|
196
|
+
aggregateType,
|
|
197
|
+
aggregateId,
|
|
198
|
+
mode: mode.toUpperCase(),
|
|
199
|
+
limit: 50,
|
|
200
|
+
cursor: cursor ?? undefined,
|
|
201
|
+
});
|
|
202
|
+
if (activeSubjectKey !== key)
|
|
203
|
+
return; // navigated away
|
|
204
|
+
const page = data.inspectorSubjectChain;
|
|
205
|
+
dispatch({
|
|
206
|
+
type: "events/subject_chain_loaded",
|
|
207
|
+
payload: {
|
|
208
|
+
events: page.events,
|
|
209
|
+
hasMore: page.nextCursor != null,
|
|
210
|
+
cursor: page.nextCursor,
|
|
211
|
+
depthCapped: page.depthCapReached,
|
|
212
|
+
append: cursor != null,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
console.error("[causal-inspector] fetch subject chain failed:", e);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const fetchEventEffects = async (eventId) => {
|
|
221
|
+
const state = getState();
|
|
222
|
+
if (eventId in state.expandedEffects)
|
|
223
|
+
return; // already loaded
|
|
224
|
+
try {
|
|
225
|
+
const data = await transport.query(INSPECTOR_EFFECTS_FOR_EVENT, { eventId });
|
|
226
|
+
dispatch({
|
|
227
|
+
type: "events/event_effects_loaded",
|
|
228
|
+
payload: { eventId, effects: data.inspectorEffectsForEvent },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
console.error("[causal-inspector] fetch event effects failed:", e);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const startFlowPolling = (workflowId) => {
|
|
183
236
|
stopFlowPolling();
|
|
184
|
-
fetchFlowMetadata(
|
|
185
|
-
flowPollTimer = setInterval(() => fetchFlowMetadata(
|
|
237
|
+
fetchFlowMetadata(workflowId);
|
|
238
|
+
flowPollTimer = setInterval(() => fetchFlowMetadata(workflowId), 5000);
|
|
186
239
|
};
|
|
187
240
|
const stopFlowPolling = () => {
|
|
188
241
|
if (flowPollTimer) {
|
|
@@ -190,27 +243,33 @@ export const createQueryEngine = (transport) => {
|
|
|
190
243
|
flowPollTimer = null;
|
|
191
244
|
}
|
|
192
245
|
};
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
195
|
-
clearInterval(
|
|
196
|
-
|
|
246
|
+
const stopWorkflowPolling = () => {
|
|
247
|
+
if (workflowPollTimer) {
|
|
248
|
+
clearInterval(workflowPollTimer);
|
|
249
|
+
workflowPollTimer = null;
|
|
197
250
|
}
|
|
198
251
|
};
|
|
199
252
|
// Initial load
|
|
200
253
|
fetchEvents();
|
|
201
|
-
|
|
254
|
+
fetchWorkflows();
|
|
202
255
|
fetchReactorDependencies();
|
|
203
256
|
fetchAggregateKeys();
|
|
204
257
|
return {
|
|
205
258
|
handleEvent: (event, curr, prev) => {
|
|
206
259
|
// ── State-reactive: navigation transitions ──
|
|
207
|
-
|
|
208
|
-
|
|
260
|
+
// Subject changed → fetch first page
|
|
261
|
+
if (curr.subjectType !== prev.subjectType || curr.subjectId !== prev.subjectId) {
|
|
262
|
+
if (curr.subjectType && curr.subjectId) {
|
|
263
|
+
fetchSubjectChain(curr.subjectType, curr.subjectId, curr.subjectMode, null);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (curr.flowWorkflowId !== prev.flowWorkflowId) {
|
|
267
|
+
if (curr.flowWorkflowId) {
|
|
209
268
|
// Flow opened
|
|
210
|
-
fetchFlow(curr.
|
|
211
|
-
startFlowPolling(curr.
|
|
212
|
-
fetchLogs(curr.
|
|
213
|
-
|
|
269
|
+
fetchFlow(curr.flowWorkflowId);
|
|
270
|
+
startFlowPolling(curr.flowWorkflowId);
|
|
271
|
+
fetchLogs(curr.flowWorkflowId);
|
|
272
|
+
stopWorkflowPolling();
|
|
214
273
|
}
|
|
215
274
|
else {
|
|
216
275
|
// Flow closed
|
|
@@ -228,23 +287,45 @@ export const createQueryEngine = (transport) => {
|
|
|
228
287
|
case "ui/filter_changed":
|
|
229
288
|
fetchEvents();
|
|
230
289
|
break;
|
|
231
|
-
case "ui/
|
|
232
|
-
|
|
290
|
+
case "ui/load_more_workflows_requested":
|
|
291
|
+
fetchWorkflows({ append: true });
|
|
233
292
|
break;
|
|
234
|
-
case "ui/
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
293
|
+
case "ui/workflows_requested":
|
|
294
|
+
workflowCursor = null;
|
|
295
|
+
fetchWorkflows({ search: event.payload.search });
|
|
296
|
+
stopWorkflowPolling();
|
|
297
|
+
workflowPollTimer = setInterval(() => fetchWorkflows({ search: event.payload.search }), 5000);
|
|
239
298
|
break;
|
|
240
299
|
case "ui/aggregate_lifecycle_requested":
|
|
241
300
|
fetchAggregateLifecycle(event.payload.aggregateKey);
|
|
242
301
|
break;
|
|
302
|
+
case "ui/subject_selected": {
|
|
303
|
+
const { aggregateType, aggregateId, mode = "both" } = event.payload;
|
|
304
|
+
fetchSubjectChain(aggregateType, aggregateId, mode, null);
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case "ui/subject_mode_changed": {
|
|
308
|
+
const state = getState();
|
|
309
|
+
if (state.subjectType && state.subjectId) {
|
|
310
|
+
fetchSubjectChain(state.subjectType, state.subjectId, event.payload.mode, null);
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "ui/subject_chain_load_more": {
|
|
315
|
+
const state = getState();
|
|
316
|
+
if (state.subjectType && state.subjectId) {
|
|
317
|
+
fetchSubjectChain(state.subjectType, state.subjectId, state.subjectMode, state.subjectChainCursor);
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case "ui/event_effects_requested":
|
|
322
|
+
fetchEventEffects(event.payload.eventId);
|
|
323
|
+
break;
|
|
243
324
|
}
|
|
244
325
|
},
|
|
245
326
|
dispose: () => {
|
|
246
327
|
stopFlowPolling();
|
|
247
|
-
|
|
328
|
+
stopWorkflowPolling();
|
|
248
329
|
},
|
|
249
330
|
};
|
|
250
331
|
};
|
package/dist/engines/scrubber.js
CHANGED
package/dist/engines/url.d.ts
CHANGED
|
@@ -4,8 +4,11 @@ import type { InspectorState } from "../state";
|
|
|
4
4
|
/**
|
|
5
5
|
* URL engine — keeps the browser URL in sync with navigation state.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Mutual exclusion: `?subject=` and `?workflow=` never coexist in the URL.
|
|
8
|
+
* Navigating to a subject clears the workflow param and vice versa.
|
|
9
|
+
*
|
|
10
|
+
* For user-initiated actions, the reducer updates state directly.
|
|
11
|
+
* This engine writes the URL as a side effect.
|
|
9
12
|
*
|
|
10
13
|
* For browser-initiated navigation (back/forward), this engine dispatches
|
|
11
14
|
* location/changed so the reducer can update state from the URL.
|
package/dist/engines/url.js
CHANGED
|
@@ -1,44 +1,57 @@
|
|
|
1
1
|
function parseUrl() {
|
|
2
2
|
const params = new URLSearchParams(window.location.search);
|
|
3
|
+
const subject = params.get("subject");
|
|
4
|
+
const subjectModeRaw = params.get("subjectMode");
|
|
5
|
+
const subjectMode = subjectModeRaw === "stream" || subjectModeRaw === "descendants" || subjectModeRaw === "both"
|
|
6
|
+
? subjectModeRaw
|
|
7
|
+
: null;
|
|
8
|
+
if (subject) {
|
|
9
|
+
return { workflowId: null, handler: null, subject, subjectMode };
|
|
10
|
+
}
|
|
3
11
|
return {
|
|
4
|
-
|
|
12
|
+
workflowId: params.get("workflow"),
|
|
5
13
|
handler: params.get("handler"),
|
|
14
|
+
subject: null,
|
|
15
|
+
subjectMode: null,
|
|
6
16
|
};
|
|
7
17
|
}
|
|
8
|
-
function buildSearch(
|
|
9
|
-
const params = new URLSearchParams(
|
|
10
|
-
if (
|
|
11
|
-
params.set("
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
params.delete("handler");
|
|
18
|
+
function buildSearch(workflowId, handler) {
|
|
19
|
+
const params = new URLSearchParams();
|
|
20
|
+
if (workflowId) {
|
|
21
|
+
params.set("workflow", workflowId);
|
|
22
|
+
if (handler)
|
|
23
|
+
params.set("handler", handler);
|
|
15
24
|
}
|
|
16
|
-
if (handler && correlationId)
|
|
17
|
-
params.set("handler", handler);
|
|
18
|
-
else
|
|
19
|
-
params.delete("handler");
|
|
20
25
|
const search = params.toString();
|
|
21
26
|
return search ? `${window.location.pathname}?${search}` : window.location.pathname;
|
|
22
27
|
}
|
|
28
|
+
function buildSubjectSearch(aggregateType, aggregateId, mode) {
|
|
29
|
+
const params = new URLSearchParams();
|
|
30
|
+
params.set("subject", `${aggregateType}:${aggregateId}`);
|
|
31
|
+
if (mode !== "both")
|
|
32
|
+
params.set("subjectMode", mode);
|
|
33
|
+
return `${window.location.pathname}?${params.toString()}`;
|
|
34
|
+
}
|
|
23
35
|
/**
|
|
24
36
|
* URL engine — keeps the browser URL in sync with navigation state.
|
|
25
37
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
38
|
+
* Mutual exclusion: `?subject=` and `?workflow=` never coexist in the URL.
|
|
39
|
+
* Navigating to a subject clears the workflow param and vice versa.
|
|
40
|
+
*
|
|
41
|
+
* For user-initiated actions, the reducer updates state directly.
|
|
42
|
+
* This engine writes the URL as a side effect.
|
|
28
43
|
*
|
|
29
44
|
* For browser-initiated navigation (back/forward), this engine dispatches
|
|
30
45
|
* location/changed so the reducer can update state from the URL.
|
|
31
46
|
*/
|
|
32
47
|
export const createUrlEngine = (dispatch, _getState) => {
|
|
33
|
-
// Popstate — browser back/forward
|
|
34
48
|
const onPopState = () => {
|
|
35
49
|
dispatch({ type: "location/changed", payload: parseUrl() });
|
|
36
50
|
};
|
|
37
51
|
window.addEventListener("popstate", onPopState);
|
|
38
|
-
// Seed from current URL on init — deferred so the Machine constructor finishes first.
|
|
39
52
|
queueMicrotask(() => {
|
|
40
53
|
const initial = parseUrl();
|
|
41
|
-
if (initial.
|
|
54
|
+
if (initial.workflowId || initial.handler || initial.subject) {
|
|
42
55
|
dispatch({ type: "location/changed", payload: initial });
|
|
43
56
|
}
|
|
44
57
|
});
|
|
@@ -46,21 +59,36 @@ export const createUrlEngine = (dispatch, _getState) => {
|
|
|
46
59
|
handleEvent: (event) => {
|
|
47
60
|
switch (event.type) {
|
|
48
61
|
case "ui/flow_opened":
|
|
49
|
-
window.history.pushState(null, "", buildSearch(event.payload.
|
|
62
|
+
window.history.pushState(null, "", buildSearch(event.payload.workflowId, null));
|
|
50
63
|
break;
|
|
51
64
|
case "ui/flow_closed":
|
|
52
65
|
window.history.pushState(null, "", buildSearch(null, null));
|
|
53
66
|
break;
|
|
54
67
|
case "ui/filter_changed": {
|
|
55
68
|
const payload = event.payload;
|
|
56
|
-
if (payload.
|
|
57
|
-
window.history.pushState(null, "", buildSearch(payload.
|
|
69
|
+
if (payload.workflowId !== undefined) {
|
|
70
|
+
window.history.pushState(null, "", buildSearch(payload.workflowId, null));
|
|
58
71
|
}
|
|
59
72
|
break;
|
|
60
73
|
}
|
|
61
74
|
case "ui/handler_selected":
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
window.history.replaceState(null, "", buildSearch(new URLSearchParams(window.location.search).get("workflow"), event.payload.reactorId));
|
|
76
|
+
break;
|
|
77
|
+
case "ui/subject_selected":
|
|
78
|
+
window.history.pushState(null, "", buildSubjectSearch(event.payload.aggregateType, event.payload.aggregateId, event.payload.mode ?? "both"));
|
|
79
|
+
break;
|
|
80
|
+
case "ui/subject_mode_changed":
|
|
81
|
+
window.history.replaceState(null, "", (() => {
|
|
82
|
+
const params = new URLSearchParams(window.location.search);
|
|
83
|
+
const existing = params.get("subject");
|
|
84
|
+
if (!existing)
|
|
85
|
+
return window.location.href;
|
|
86
|
+
if (event.payload.mode === "both")
|
|
87
|
+
params.delete("subjectMode");
|
|
88
|
+
else
|
|
89
|
+
params.set("subjectMode", event.payload.mode);
|
|
90
|
+
return `${window.location.pathname}?${params.toString()}`;
|
|
91
|
+
})());
|
|
64
92
|
break;
|
|
65
93
|
}
|
|
66
94
|
},
|