Skip to content

Commit 77672fa

Browse files
authored
Prevent stale dashboard refresh after operator actions (#60)
1 parent 46abf57 commit 77672fa

1 file changed

Lines changed: 81 additions & 12 deletions

File tree

src/App.tsx

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
UserCheck,
1616
Users,
1717
} from "lucide-react";
18-
import { useCallback, useEffect, useState, type Dispatch, type KeyboardEvent, type SetStateAction } from "react";
18+
import { useCallback, useEffect, useRef, useState, type Dispatch, type KeyboardEvent, type SetStateAction } from "react";
1919
import { defaultBranding, loadDeploymentBranding } from "./branding";
2020
import { demoState } from "./demoState";
2121
import type { AgentCommsState, AgentIdentity, CrossProjectGate, Forum, ForumCreationSpec, SuggestionStatus, Thread } from "./domain";
@@ -1339,6 +1339,20 @@ export function App() {
13391339
const [operatorToken] = useState(() => localStorage.getItem("agent-comms-operator-token") ?? "");
13401340
const [apiStatus, setApiStatus] = useState("demo data");
13411341
const [actionStatus, setActionStatus] = useState("");
1342+
const refreshSequenceRef = useRef(0);
1343+
const mutationEpochRef = useRef(0);
1344+
const activeOperatorMutationsRef = useRef(0);
1345+
1346+
const beginOperatorMutation = useCallback(() => {
1347+
mutationEpochRef.current += 1;
1348+
activeOperatorMutationsRef.current += 1;
1349+
let finished = false;
1350+
return () => {
1351+
if (finished) return;
1352+
finished = true;
1353+
activeOperatorMutationsRef.current = Math.max(0, activeOperatorMutationsRef.current - 1);
1354+
};
1355+
}, []);
13421356

13431357
const operatorRequest = useCallback(
13441358
async (path: string, options: RequestInit = {}) => {
@@ -1361,7 +1375,12 @@ export function App() {
13611375
[operatorToken],
13621376
);
13631377

1364-
const refreshOperatorData = useCallback(async () => {
1378+
const refreshOperatorData = useCallback(async (options?: { force?: boolean }) => {
1379+
const force = options?.force ?? false;
1380+
if (!force && activeOperatorMutationsRef.current > 0) return;
1381+
const refreshSequence = refreshSequenceRef.current + 1;
1382+
refreshSequenceRef.current = refreshSequence;
1383+
const mutationEpochAtStart = mutationEpochRef.current;
13651384
try {
13661385
const [
13671386
forumsPayload,
@@ -1384,6 +1403,13 @@ export function App() {
13841403
operatorRequest("live-conversations"),
13851404
operatorRequest("gates"),
13861405
]);
1406+
if (!force && (
1407+
refreshSequence !== refreshSequenceRef.current ||
1408+
mutationEpochAtStart !== mutationEpochRef.current ||
1409+
activeOperatorMutationsRef.current > 0
1410+
)) {
1411+
return;
1412+
}
13871413
setState((current) => ({
13881414
...current,
13891415
forums: (forumsPayload.forums ?? current.forums).map((forum: any) => ({
@@ -1497,6 +1523,13 @@ export function App() {
14971523
})));
14981524
setApiStatus(forumsPayload.previewStorage ? "preview storage" : "durable storage");
14991525
} catch (error) {
1526+
if (!force && (
1527+
refreshSequence !== refreshSequenceRef.current ||
1528+
mutationEpochAtStart !== mutationEpochRef.current ||
1529+
activeOperatorMutationsRef.current > 0
1530+
)) {
1531+
return;
1532+
}
15001533
setApiStatus(error instanceof Error ? error.message : "operator API unavailable");
15011534
}
15021535
}, [operatorRequest, operatorToken]);
@@ -1620,6 +1653,7 @@ export function App() {
16201653
};
16211654

16221655
const mintAgentToken = async (agent: AgentIdentity) => {
1656+
const finishMutation = beginOperatorMutation();
16231657
try {
16241658
const payload = await operatorRequest(`agents/${agent.id}/tokens`, {
16251659
method: "POST",
@@ -1629,23 +1663,29 @@ export function App() {
16291663
setActionStatus("Token minted. Copy it now; it will not be shown after refresh.");
16301664
} catch (error) {
16311665
setActionStatus(error instanceof Error ? error.message : "Token minting failed.");
1666+
} finally {
1667+
finishMutation();
16321668
}
16331669
};
16341670

16351671
const approveAgent = async (agentId: string) => {
1672+
const finishMutation = beginOperatorMutation();
16361673
try {
16371674
await operatorRequest("agent-approvals", {
16381675
method: "POST",
16391676
body: JSON.stringify({ agentId }),
16401677
});
1641-
await refreshOperatorData();
1678+
await refreshOperatorData({ force: true });
16421679
setActionStatus("Agent approved.");
16431680
} catch (error) {
16441681
setActionStatus(error instanceof Error ? error.message : "Approval failed.");
1682+
} finally {
1683+
finishMutation();
16451684
}
16461685
};
16471686

16481687
const updateAgentStatus = async (agentId: string, status: AgentStatus) => {
1688+
const finishMutation = beginOperatorMutation();
16491689
setState((current) => ({
16501690
...current,
16511691
agents: current.agents.map((agent) =>
@@ -1675,15 +1715,18 @@ export function App() {
16751715
body: JSON.stringify({ status }),
16761716
});
16771717
}
1678-
await refreshOperatorData();
1718+
await refreshOperatorData({ force: true });
16791719
setActionStatus(`Agent ${status}.`);
16801720
} catch (error) {
1681-
await refreshOperatorData();
1721+
await refreshOperatorData({ force: true });
16821722
setActionStatus(error instanceof Error ? error.message : "Agent status update failed.");
1723+
} finally {
1724+
finishMutation();
16831725
}
16841726
};
16851727

16861728
const updateSuggestionStatus = async (suggestionId: string, status: SuggestionStatus) => {
1729+
const finishMutation = beginOperatorMutation();
16871730
setState((current) => ({
16881731
...current,
16891732
suggestions: current.suggestions.map((suggestion) =>
@@ -1695,27 +1738,32 @@ export function App() {
16951738
method: "POST",
16961739
body: JSON.stringify({ status }),
16971740
});
1698-
await refreshOperatorData();
1741+
await refreshOperatorData({ force: true });
16991742
setActionStatus(`Suggestion ${status}.`);
17001743
} catch (error) {
17011744
setActionStatus(error instanceof Error ? error.message : "Suggestion update failed.");
1745+
} finally {
1746+
finishMutation();
17021747
}
17031748
};
17041749

17051750
const approveAndCreateForumSuggestion = async (suggestionId: string) => {
1751+
const finishMutation = beginOperatorMutation();
17061752
try {
17071753
const payload = await operatorRequest(`suggestions/${suggestionId}/approve-create-forum`, {
17081754
method: "POST",
17091755
body: JSON.stringify({}),
17101756
});
1711-
await refreshOperatorData();
1757+
await refreshOperatorData({ force: true });
17121758
if (payload.forum?.id) {
17131759
setSelectedForumId(payload.forum.id);
17141760
setView("forums");
17151761
}
17161762
setActionStatus("Suggestion approved and forum created.");
17171763
} catch (error) {
17181764
setActionStatus(error instanceof Error ? error.message : "Approve and create failed.");
1765+
} finally {
1766+
finishMutation();
17191767
}
17201768
};
17211769

@@ -1730,12 +1778,13 @@ export function App() {
17301778
setActionStatus("Forum name, slug, and description are required.");
17311779
return;
17321780
}
1781+
const finishMutation = beginOperatorMutation();
17331782
try {
17341783
const payload = await operatorRequest("forums", {
17351784
method: "POST",
17361785
body: JSON.stringify(draft),
17371786
});
1738-
await refreshOperatorData();
1787+
await refreshOperatorData({ force: true });
17391788
setCreateForumDraft(emptyForumDraft);
17401789
setCreateForumOpen(false);
17411790
if (payload.forum?.id) {
@@ -1744,6 +1793,8 @@ export function App() {
17441793
setActionStatus("Forum created.");
17451794
} catch (error) {
17461795
setActionStatus(error instanceof Error ? error.message : "Forum creation failed.");
1796+
} finally {
1797+
finishMutation();
17471798
}
17481799
};
17491800

@@ -1756,12 +1807,13 @@ export function App() {
17561807
setActionStatus("Choose two different agents.");
17571808
return;
17581809
}
1810+
const finishMutation = beginOperatorMutation();
17591811
try {
17601812
const payload = await operatorRequest("direct-conversations", {
17611813
method: "POST",
17621814
body: JSON.stringify(createConversationDraft),
17631815
});
1764-
await refreshOperatorData();
1816+
await refreshOperatorData({ force: true });
17651817
setCreateConversationDraft(emptyDirectConversationDraft);
17661818
setCreateConversationOpen(false);
17671819
if (payload.conversation?.id) {
@@ -1770,6 +1822,8 @@ export function App() {
17701822
setActionStatus(payload.existing ? "Direct conversation already exists." : "Direct conversation created.");
17711823
} catch (error) {
17721824
setActionStatus(error instanceof Error ? error.message : "Direct conversation creation failed.");
1825+
} finally {
1826+
finishMutation();
17731827
}
17741828
};
17751829

@@ -1801,6 +1855,7 @@ export function App() {
18011855
const replyToThread = async (threadId: string) => {
18021856
const bodyText = threadDrafts[threadId]?.trim();
18031857
if (!bodyText) return;
1858+
const finishMutation = beginOperatorMutation();
18041859
const id = `local_reply_${Date.now()}`;
18051860
setState((current) => ({
18061861
...current,
@@ -1832,12 +1887,15 @@ export function App() {
18321887
setActionStatus("Thread reply posted.");
18331888
} catch (error) {
18341889
setActionStatus(error instanceof Error ? error.message : "Thread reply saved locally.");
1890+
} finally {
1891+
finishMutation();
18351892
}
18361893
};
18371894

18381895
const replyToConversation = async (conversationId: string) => {
18391896
const bodyText = conversationDrafts[conversationId]?.trim();
18401897
if (!bodyText) return;
1898+
const finishMutation = beginOperatorMutation();
18411899
const id = `local_dm_${Date.now()}`;
18421900
setState((current) => ({
18431901
...current,
@@ -1879,10 +1937,13 @@ export function App() {
18791937
setActionStatus("Direct reply posted.");
18801938
} catch (error) {
18811939
setActionStatus(error instanceof Error ? error.message : "Direct reply added locally.");
1940+
} finally {
1941+
finishMutation();
18821942
}
18831943
};
18841944

18851945
const startLiveConversation = async (conversationId: string) => {
1946+
const finishMutation = beginOperatorMutation();
18861947
try {
18871948
await operatorRequest("live-conversations", {
18881949
method: "POST",
@@ -1893,27 +1954,33 @@ export function App() {
18931954
createdByHumanId: "human_shay",
18941955
}),
18951956
});
1896-
await refreshOperatorData();
1957+
await refreshOperatorData({ force: true });
18971958
setActionStatus("Live conversation mode started.");
18981959
} catch (error) {
18991960
setActionStatus(error instanceof Error ? error.message : "Live mode start failed.");
1961+
} finally {
1962+
finishMutation();
19001963
}
19011964
};
19021965

19031966
const stopLiveConversation = async (sessionId: string) => {
1967+
const finishMutation = beginOperatorMutation();
19041968
try {
19051969
await operatorRequest(`live-conversations/${sessionId}/status`, {
19061970
method: "POST",
19071971
body: JSON.stringify({ status: "stopped" }),
19081972
});
1909-
await refreshOperatorData();
1973+
await refreshOperatorData({ force: true });
19101974
setActionStatus("Live conversation mode stopped.");
19111975
} catch (error) {
19121976
setActionStatus(error instanceof Error ? error.message : "Live mode stop failed.");
1977+
} finally {
1978+
finishMutation();
19131979
}
19141980
};
19151981

19161982
const updateGateStatus = async (gateId: string, status: CrossProjectGate["status"]) => {
1983+
const finishMutation = beginOperatorMutation();
19171984
setState((current) => ({
19181985
...current,
19191986
gates: (current.gates ?? []).map((gate) => (gate.id === gateId ? { ...gate, status } : gate)),
@@ -1923,10 +1990,12 @@ export function App() {
19231990
method: "POST",
19241991
body: JSON.stringify({ status }),
19251992
});
1926-
await refreshOperatorData();
1993+
await refreshOperatorData({ force: true });
19271994
setActionStatus(`Gate ${status}.`);
19281995
} catch (error) {
19291996
setActionStatus(error instanceof Error ? error.message : "Gate update failed.");
1997+
} finally {
1998+
finishMutation();
19301999
}
19312000
};
19322001

0 commit comments

Comments
 (0)