batch3 T6 review fixes: remove rollback closure, flush-error resync
- remove: capture snapshot inside setPanes functional updater to avoid stale-closure rollback under concurrent renders - flushPendingState: call refresh() on PATCH failure so server truth and optimistic local state can't silently diverge - Drop body.state! non-null assertion via narrowed local Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,21 +19,6 @@ export function usePanes(sessionId: string | undefined): {
|
|||||||
const pendingState = useRef<Map<string, PaneState>>(new Map());
|
const pendingState = useRef<Map<string, PaneState>>(new Map());
|
||||||
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const flushPendingState = useCallback(() => {
|
|
||||||
if (debounceTimer.current !== null) {
|
|
||||||
clearTimeout(debounceTimer.current);
|
|
||||||
debounceTimer.current = null;
|
|
||||||
}
|
|
||||||
const snapshot = new Map(pendingState.current);
|
|
||||||
pendingState.current.clear();
|
|
||||||
for (const [id, state] of snapshot) {
|
|
||||||
// fire-and-forget; caller surface handles errors via hook error state
|
|
||||||
void api.panes.update(id, { state }).catch((err) => {
|
|
||||||
setError(err instanceof Error ? err.message : 'pane operation failed');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
setPanes(null);
|
setPanes(null);
|
||||||
@@ -51,6 +36,23 @@ export function usePanes(sessionId: string | undefined): {
|
|||||||
}
|
}
|
||||||
}, [sessionId]);
|
}, [sessionId]);
|
||||||
|
|
||||||
|
const flushPendingState = useCallback(async () => {
|
||||||
|
if (debounceTimer.current !== null) {
|
||||||
|
clearTimeout(debounceTimer.current);
|
||||||
|
debounceTimer.current = null;
|
||||||
|
}
|
||||||
|
const updates = Array.from(pendingState.current.entries());
|
||||||
|
pendingState.current.clear();
|
||||||
|
if (updates.length === 0) return;
|
||||||
|
try {
|
||||||
|
await Promise.all(updates.map(([id, state]) => api.panes.update(id, { state })));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'pane state PATCH failed');
|
||||||
|
// server truth may diverge from optimistic local state; resync
|
||||||
|
void refresh();
|
||||||
|
}
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
// Fetch on mount / sessionId change; preserve previous list while reloading
|
// Fetch on mount / sessionId change; preserve previous list while reloading
|
||||||
// (loading=true but panes stays non-null after first fetch to avoid flash)
|
// (loading=true but panes stays non-null after first fetch to avoid flash)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -99,7 +101,8 @@ export function usePanes(sessionId: string | undefined): {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Coalesce: last state wins within debounce window
|
// Coalesce: last state wins within debounce window
|
||||||
pendingState.current.set(id, body.state!);
|
const nextState = body.state;
|
||||||
|
pendingState.current.set(id, nextState);
|
||||||
|
|
||||||
if (debounceTimer.current !== null) {
|
if (debounceTimer.current !== null) {
|
||||||
clearTimeout(debounceTimer.current);
|
clearTimeout(debounceTimer.current);
|
||||||
@@ -124,21 +127,24 @@ export function usePanes(sessionId: string | undefined): {
|
|||||||
|
|
||||||
const remove = useCallback(
|
const remove = useCallback(
|
||||||
async (id: string): Promise<void> => {
|
async (id: string): Promise<void> => {
|
||||||
// Optimistic remove
|
// Optimistic remove — capture snapshot inside functional updater to avoid stale closure
|
||||||
const previous = panes;
|
let snapshot: Pane[] | null = null;
|
||||||
setPanes((prev) => (prev ? prev.filter((p) => p.id !== id) : prev));
|
setPanes((prev) => {
|
||||||
|
snapshot = prev;
|
||||||
|
return prev ? prev.filter((p) => p.id !== id) : prev;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.panes.remove(id);
|
await api.panes.remove(id);
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Rollback
|
// Rollback to the truly-most-recent value captured above
|
||||||
setPanes(previous);
|
setPanes(snapshot);
|
||||||
setError(err instanceof Error ? err.message : 'pane operation failed');
|
setError(err instanceof Error ? err.message : 'pane operation failed');
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[panes, refresh]
|
[refresh]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { panes, loading, error, refresh, create, update, remove };
|
return { panes, loading, error, refresh, create, update, remove };
|
||||||
|
|||||||
Reference in New Issue
Block a user