fix(FN-673): resync tasks after dashboard stream reconnect
This commit is contained in:
@@ -289,7 +289,6 @@ describe("App engine pause (soft pause)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("App view switching", () => {
|
describe("App view switching", () => {
|
||||||
it("keeps the selected board/list view while task data changes underneath it", async () => {
|
|
||||||
it("renders Board view by default", async () => {
|
it("renders Board view by default", async () => {
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ describe("useTasks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(MockEventSource.instances).toHaveLength(2);
|
expect(MockEventSource.instances).toHaveLength(2);
|
||||||
|
expect(mockFetchTasks).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
@@ -298,6 +299,7 @@ describe("useTasks", () => {
|
|||||||
expect(result.current.tasks[0]?.title).toBe("Fresh title");
|
expect(result.current.tasks[0]?.title).toBe("Fresh title");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("SSE event: task:updated", () => {
|
describe("SSE event: task:updated", () => {
|
||||||
it("updates task fields", async () => {
|
it("updates task fields", async () => {
|
||||||
const initialTask = createMockTask({
|
const initialTask = createMockTask({
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
|||||||
import type { Task, Column, TaskCreateInput, MergeResult } from "@fusion/core";
|
import type { Task, Column, TaskCreateInput, MergeResult } from "@fusion/core";
|
||||||
import * as api from "../api";
|
import * as api from "../api";
|
||||||
|
|
||||||
|
const RECONNECT_DELAY_MS = 3000;
|
||||||
|
|
||||||
function normalizeTask(task: Task): Task {
|
function normalizeTask(task: Task): Task {
|
||||||
return {
|
return {
|
||||||
...task,
|
...task,
|
||||||
@@ -28,17 +30,47 @@ export function useTasks() {
|
|||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
const [connectionNonce, setConnectionNonce] = useState(0);
|
const [connectionNonce, setConnectionNonce] = useState(0);
|
||||||
const tasksRef = useRef(tasks);
|
const tasksRef = useRef(tasks);
|
||||||
|
const fetchVersionRef = useRef(0);
|
||||||
tasksRef.current = tasks;
|
tasksRef.current = tasks;
|
||||||
|
|
||||||
// Fetch initial tasks
|
const refreshTasks = useCallback(async () => {
|
||||||
useEffect(() => {
|
const requestVersion = ++fetchVersionRef.current;
|
||||||
api.fetchTasks().then((tasks) => setTasks(tasks.map(normalizeTask))).catch(() => setTasks([]));
|
|
||||||
|
try {
|
||||||
|
const fetchedTasks = await api.fetchTasks();
|
||||||
|
if (fetchVersionRef.current !== requestVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTasks(fetchedTasks.map(normalizeTask));
|
||||||
|
} catch {
|
||||||
|
if (fetchVersionRef.current !== requestVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTasks((current) => current);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Fetch initial tasks and recover when the tab becomes visible again.
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshTasks();
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
void refreshTasks();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
};
|
||||||
|
}, [refreshTasks]);
|
||||||
|
|
||||||
// SSE live updates
|
// SSE live updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let closedByCleanup = false;
|
let closedByCleanup = false;
|
||||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
if (connectionNonce > 0) {
|
||||||
|
void refreshTasks();
|
||||||
|
}
|
||||||
const es = new EventSource("/api/events");
|
const es = new EventSource("/api/events");
|
||||||
|
|
||||||
const handleCreated = (e: MessageEvent) => {
|
const handleCreated = (e: MessageEvent) => {
|
||||||
@@ -137,7 +169,7 @@ export function useTasks() {
|
|||||||
cleanup();
|
cleanup();
|
||||||
reconnectTimer = setTimeout(() => {
|
reconnectTimer = setTimeout(() => {
|
||||||
setConnectionNonce((current) => current + 1);
|
setConnectionNonce((current) => current + 1);
|
||||||
}, 3000);
|
}, RECONNECT_DELAY_MS);
|
||||||
};
|
};
|
||||||
|
|
||||||
es.addEventListener("task:created", handleCreated);
|
es.addEventListener("task:created", handleCreated);
|
||||||
@@ -151,7 +183,7 @@ export function useTasks() {
|
|||||||
closedByCleanup = true;
|
closedByCleanup = true;
|
||||||
cleanup();
|
cleanup();
|
||||||
};
|
};
|
||||||
}, [connectionNonce]);
|
}, [connectionNonce, refreshTasks]);
|
||||||
|
|
||||||
const createTask = useCallback(async (input: TaskCreateInput): Promise<Task> => {
|
const createTask = useCallback(async (input: TaskCreateInput): Promise<Task> => {
|
||||||
return normalizeTask(await api.createTask(input));
|
return normalizeTask(await api.createTask(input));
|
||||||
|
|||||||
Reference in New Issue
Block a user