Skip to content

Commit 9a51a58

Browse files
committed
fix(web): handle overlapping target persistence rollbacks
1 parent d72d417 commit 9a51a58

2 files changed

Lines changed: 157 additions & 8 deletions

File tree

packages/web/src/features/workspace/actions/use-persist-workspace-last-viewed-target.test.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,111 @@ describe("usePersistWorkspaceLastViewedTarget", () => {
164164

165165
expect(store.get(lastViewedTargetAtom)?.workspaceId).toBe("ws-2");
166166
});
167+
168+
it("rolls back to the last confirmed target when overlapping writes both fail", async () => {
169+
const store = createStore();
170+
const firstWrite = createDeferred<{ workspaceId: string; updatedAt: number }>();
171+
const secondWrite = createDeferred<{ workspaceId: string; updatedAt: number }>();
172+
const sendCommand = vi
173+
.fn()
174+
.mockImplementationOnce(() => firstWrite.promise)
175+
.mockImplementationOnce(() => secondWrite.promise);
176+
177+
store.set(wsClientAtom, {
178+
sendCommand,
179+
subscribe: vi.fn(() => () => {}),
180+
} as never);
181+
store.set(lastViewedTargetAtom, {
182+
workspaceId: "ws-0",
183+
updatedAt: 1,
184+
});
185+
186+
const { result } = renderHook(() => usePersistWorkspaceLastViewedTarget(), {
187+
wrapper: wrapperFor(store),
188+
});
189+
190+
let firstRequest!: Promise<unknown>;
191+
let secondRequest!: Promise<unknown>;
192+
193+
act(() => {
194+
firstRequest = result.current({ workspaceId: "ws-1" });
195+
secondRequest = result.current({ workspaceId: "ws-2" });
196+
});
197+
198+
expect(store.get(lastViewedTargetAtom)?.workspaceId).toBe("ws-2");
199+
200+
await act(async () => {
201+
firstWrite.reject(
202+
new CommandResultError({
203+
code: "write_failed",
204+
message: "failed",
205+
})
206+
);
207+
await firstRequest;
208+
});
209+
210+
expect(store.get(lastViewedTargetAtom)?.workspaceId).toBe("ws-2");
211+
212+
await act(async () => {
213+
secondWrite.reject(
214+
new CommandResultError({
215+
code: "write_failed",
216+
message: "failed",
217+
})
218+
);
219+
await secondRequest;
220+
});
221+
222+
expect(store.get(lastViewedTargetAtom)?.workspaceId).toBe("ws-0");
223+
});
224+
225+
it("rolls back to an older confirmed target when a newer overlapping write fails", async () => {
226+
const store = createStore();
227+
const firstWrite = createDeferred<{ workspaceId: string; updatedAt: number }>();
228+
const secondWrite = createDeferred<{ workspaceId: string; updatedAt: number }>();
229+
const sendCommand = vi
230+
.fn()
231+
.mockImplementationOnce(() => firstWrite.promise)
232+
.mockImplementationOnce(() => secondWrite.promise);
233+
234+
store.set(wsClientAtom, {
235+
sendCommand,
236+
subscribe: vi.fn(() => () => {}),
237+
} as never);
238+
store.set(lastViewedTargetAtom, {
239+
workspaceId: "ws-0",
240+
updatedAt: 1,
241+
});
242+
243+
const { result } = renderHook(() => usePersistWorkspaceLastViewedTarget(), {
244+
wrapper: wrapperFor(store),
245+
});
246+
247+
let firstRequest!: Promise<unknown>;
248+
let secondRequest!: Promise<unknown>;
249+
250+
act(() => {
251+
firstRequest = result.current({ workspaceId: "ws-1" });
252+
secondRequest = result.current({ workspaceId: "ws-2" });
253+
});
254+
255+
await act(async () => {
256+
firstWrite.resolve({ workspaceId: "ws-1", updatedAt: 11 });
257+
await firstRequest;
258+
});
259+
260+
expect(store.get(lastViewedTargetAtom)?.workspaceId).toBe("ws-2");
261+
262+
await act(async () => {
263+
secondWrite.reject(
264+
new CommandResultError({
265+
code: "write_failed",
266+
message: "failed",
267+
})
268+
);
269+
await secondRequest;
270+
});
271+
272+
expect(store.get(lastViewedTargetAtom)?.workspaceId).toBe("ws-1");
273+
});
167274
});

packages/web/src/features/workspace/actions/use-persist-workspace-last-viewed-target.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { WorkspaceLastViewedTarget } from "@coder-studio/core";
2+
import type { Store } from "jotai";
23
import { useAtomValue, useSetAtom, useStore } from "jotai";
34
import { useCallback } from "react";
45
import { lastViewedTargetAtom } from "../../../atoms/app-ui";
@@ -9,6 +10,32 @@ interface PersistWorkspaceLastViewedTargetInput {
910
sessionId?: string;
1011
}
1112

13+
const confirmedTargetByStore = new WeakMap<Store, WorkspaceLastViewedTarget | null>();
14+
const optimisticTargetsByStore = new WeakMap<Store, WeakSet<WorkspaceLastViewedTarget>>();
15+
16+
function getOptimisticTargets(store: Store): WeakSet<WorkspaceLastViewedTarget> {
17+
const existing = optimisticTargetsByStore.get(store);
18+
if (existing) {
19+
return existing;
20+
}
21+
22+
const next = new WeakSet<WorkspaceLastViewedTarget>();
23+
optimisticTargetsByStore.set(store, next);
24+
return next;
25+
}
26+
27+
function getConfirmedTarget(store: Store): WorkspaceLastViewedTarget | null {
28+
const currentTarget = store.get(lastViewedTargetAtom);
29+
const optimisticTargets = getOptimisticTargets(store);
30+
31+
if (currentTarget && !optimisticTargets.has(currentTarget)) {
32+
confirmedTargetByStore.set(store, currentTarget);
33+
return currentTarget;
34+
}
35+
36+
return confirmedTargetByStore.get(store) ?? null;
37+
}
38+
1239
export function usePersistWorkspaceLastViewedTarget() {
1340
const dispatch = useAtomValue(dispatchCommandAtom);
1441
const setLastViewedTarget = useSetAtom(lastViewedTargetAtom);
@@ -17,6 +44,8 @@ export function usePersistWorkspaceLastViewedTarget() {
1744
return useCallback(
1845
async ({ workspaceId, sessionId }: PersistWorkspaceLastViewedTargetInput) => {
1946
const lastViewedTarget = store.get(lastViewedTargetAtom);
47+
const optimisticTargets = getOptimisticTargets(store);
48+
getConfirmedTarget(store);
2049

2150
if (!workspaceId) {
2251
return null;
@@ -34,25 +63,38 @@ export function usePersistWorkspaceLastViewedTarget() {
3463
sessionId,
3564
updatedAt: Date.now(),
3665
};
37-
const previousTarget = lastViewedTarget;
66+
optimisticTargets.add(optimisticTarget);
3867
setLastViewedTarget(optimisticTarget);
3968

4069
const result = await dispatch<WorkspaceLastViewedTarget>("workspace.lastViewedTarget.set", {
4170
workspaceId,
4271
sessionId,
4372
});
4473

45-
if (store.get(lastViewedTargetAtom) !== optimisticTarget) {
46-
return store.get(lastViewedTargetAtom);
74+
const latestTarget = store.get(lastViewedTargetAtom);
75+
76+
if (result.ok && result.data) {
77+
confirmedTargetByStore.set(store, result.data);
78+
79+
if (latestTarget === optimisticTarget) {
80+
optimisticTargets.delete(optimisticTarget);
81+
setLastViewedTarget(result.data);
82+
return result.data;
83+
}
84+
85+
optimisticTargets.delete(optimisticTarget);
86+
return latestTarget;
4787
}
4888

49-
if (!result.ok || !result.data) {
50-
setLastViewedTarget(previousTarget);
51-
return optimisticTarget;
89+
if (latestTarget === optimisticTarget) {
90+
const confirmedTarget = getConfirmedTarget(store);
91+
optimisticTargets.delete(optimisticTarget);
92+
setLastViewedTarget(confirmedTarget);
93+
return confirmedTarget;
5294
}
5395

54-
setLastViewedTarget(result.data);
55-
return result.data;
96+
optimisticTargets.delete(optimisticTarget);
97+
return latestTarget;
5698
},
5799
[dispatch, setLastViewedTarget, store]
58100
);

0 commit comments

Comments
 (0)