Skip to content

Commit a3a2693

Browse files
Harden polling against stale responses
Co-authored-by: DanielWalnut <hetaoBackend@users.noreply.github.com>
1 parent a2f1f1f commit a3a2693

4 files changed

Lines changed: 58 additions & 44 deletions

File tree

backend/src/db.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1268,7 +1268,9 @@ export class TaskDB {
12681268
}
12691269

12701270
count_tasks(): number {
1271-
const row = this.conn.query("SELECT COUNT(*) AS count FROM tasks").get() as Row;
1271+
const row = this.conn
1272+
.query("SELECT COUNT(*) AS count FROM tasks")
1273+
.get() as Row;
12721274
return Number(row["count"]);
12731275
}
12741276

backend/tests/api-handler.test.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,18 +176,22 @@ describe("api handler", () => {
176176
const tasks = await json(new Request("http://127.0.0.1:9712/api/tasks"));
177177

178178
expect(tasks).toHaveLength(2);
179-
expect(tasks.find((task) => task.id === upstream).dependents).toEqual([
180-
downstream,
181-
]);
182179
expect(
183-
tasks.find((task) => task.id === downstream).dependencies[0],
180+
tasks.find((task: Record<string, any>) => task.id === upstream)
181+
.dependents,
182+
).toEqual([downstream]);
183+
expect(
184+
tasks.find((task: Record<string, any>) => task.id === downstream)
185+
.dependencies[0],
184186
).toMatchObject({
185187
task_id: downstream,
186188
depends_on_task_id: upstream,
187189
inject_result: 1,
188190
depends_on_title: "Upstream",
189191
});
190-
expect(tasks.find((task) => task.id === upstream).prompt).toBe("prepare");
192+
expect(
193+
tasks.find((task: Record<string, any>) => task.id === upstream).prompt,
194+
).toBe("prepare");
191195
});
192196

193197
test("GET /api/tasks summary returns only board fields", async () => {
@@ -220,9 +224,7 @@ describe("api handler", () => {
220224
throw new Error("health should use COUNT");
221225
};
222226

223-
const health = await json(
224-
new Request("http://127.0.0.1:9712/api/health"),
225-
);
227+
const health = await json(new Request("http://127.0.0.1:9712/api/health"));
226228

227229
expect(health).toEqual({ status: "ok", tasks: 1 });
228230
});

taskboard-electron/src/renderer/App.tsx

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import {
2-
memo,
3-
useState,
4-
useEffect,
5-
useCallback,
6-
useMemo,
7-
useRef,
8-
type CSSProperties,
9-
} from "react";
1+
import { memo, useState, useEffect, useCallback, useMemo, useRef, type CSSProperties } from "react";
102
import {
113
CheckCircle2,
124
GitFork,
@@ -5821,6 +5813,8 @@ export default function App() {
58215813
const [editingTask, setEditingTask] = useState<any>(null);
58225814
const [forkingTask, setForkingTask] = useState<any>(null);
58235815
const [editingHeartbeat, setEditingHeartbeat] = useState<any>(null);
5816+
const pollRequestIdRef = useRef(0);
5817+
const detailRequestIdRef = useRef(0);
58245818

58255819
// ─── Color mode ───
58265820
const [colorMode, setColorMode] = useState(() => localStorage.getItem("colorMode") || "system");
@@ -5869,8 +5863,10 @@ export default function App() {
58695863
}, []);
58705864

58715865
const poll = useCallback(async () => {
5866+
const requestId = ++pollRequestIdRef.current;
58725867
try {
58735868
const data = await fetchMainViewData(activeView, API);
5869+
if (requestId !== pollRequestIdRef.current) return;
58745870
if (data.tasks !== undefined) {
58755871
setTasks(data.tasks);
58765872
setDetail((current) => {
@@ -5885,6 +5881,7 @@ export default function App() {
58855881
setConnected(true);
58865882
setApiError(null);
58875883
} catch (err) {
5884+
if (requestId !== pollRequestIdRef.current) return;
58885885
setConnected(false);
58895886
setApiError(`Failed to refresh ${activeView}: ${err.message}`);
58905887
}
@@ -5908,32 +5905,47 @@ export default function App() {
59085905
}, [backendReady]);
59095906

59105907
const openTaskDetail = useCallback(async (task) => {
5908+
const requestId = ++detailRequestIdRef.current;
59115909
try {
5912-
setDetail(await fetchTask(task.id));
5910+
const fullTask = await fetchTask(task.id);
5911+
if (requestId === detailRequestIdRef.current) setDetail(fullTask);
59135912
} catch (e) {
5914-
setApiError(`Failed to fetch task details: ${e.message}`);
5913+
if (requestId === detailRequestIdRef.current) {
5914+
setApiError(`Failed to fetch task details: ${e.message}`);
5915+
}
59155916
}
59165917
}, []);
5918+
const closeTaskDetail = useCallback(() => {
5919+
detailRequestIdRef.current += 1;
5920+
setDetail(null);
5921+
}, []);
5922+
const switchActiveView = useCallback((view: MainView) => {
5923+
pollRequestIdRef.current += 1;
5924+
setActiveView(view);
5925+
}, []);
59175926

5918-
const handleAction = useCallback(async (action, id) => {
5919-
try {
5920-
if (action === "cancel") await cancelTask(id);
5921-
else if (action === "retry") await retryTask(id);
5922-
else if (action === "delete") {
5923-
await deleteTask(id);
5924-
setDetail((current) => (current?.id === id ? null : current));
5925-
} else if (action === "edit") {
5926-
setEditingTask(await fetchTask(id));
5927-
return;
5928-
} else if (action === "fork") {
5929-
setForkingTask(await fetchTask(id));
5930-
return;
5927+
const handleAction = useCallback(
5928+
async (action, id) => {
5929+
try {
5930+
if (action === "cancel") await cancelTask(id);
5931+
else if (action === "retry") await retryTask(id);
5932+
else if (action === "delete") {
5933+
await deleteTask(id);
5934+
setDetail((current) => (current?.id === id ? null : current));
5935+
} else if (action === "edit") {
5936+
setEditingTask(await fetchTask(id));
5937+
return;
5938+
} else if (action === "fork") {
5939+
setForkingTask(await fetchTask(id));
5940+
return;
5941+
}
5942+
poll();
5943+
} catch (e) {
5944+
setApiError(`${action} failed: ${e.message}`);
59315945
}
5932-
poll();
5933-
} catch (e) {
5934-
setApiError(`${action} failed: ${e.message}`);
5935-
}
5936-
}, [poll]);
5946+
},
5947+
[poll],
5948+
);
59375949

59385950
const handleHeartbeatAction = async (action, id) => {
59395951
try {
@@ -6396,7 +6408,7 @@ export default function App() {
63966408
].map((tab) => (
63976409
<button
63986410
key={tab.key}
6399-
onClick={() => setActiveView(tab.key as MainView)}
6411+
onClick={() => switchActiveView(tab.key as MainView)}
64006412
style={{
64016413
padding: "6px 9px",
64026414
borderRadius: 5,
@@ -6670,7 +6682,7 @@ export default function App() {
66706682
{detail && (
66716683
<DetailPanel
66726684
task={detail}
6673-
onClose={() => setDetail(null)}
6685+
onClose={closeTaskDetail}
66746686
onRespond={handleRespond}
66756687
onResume={handleResume}
66766688
/>

taskboard-electron/src/renderer/viewPolling.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,8 @@ describe("fetchMainViewData", () => {
3232
);
3333

3434
test("returns partial data without empty placeholders for unrequested views", async () => {
35-
const data = await fetchMainViewData(
36-
"heartbeats",
37-
"http://localhost/api",
38-
async () => response([{ id: 7 }]),
35+
const data = await fetchMainViewData("heartbeats", "http://localhost/api", async () =>
36+
response([{ id: 7 }]),
3937
);
4038

4139
expect(data).toEqual({ heartbeats: [{ id: 7 }] });

0 commit comments

Comments
 (0)