Skip to content

Commit bf24d44

Browse files
Programcodeclaude
andcommitted
fix(debate): keep a pin saved while an older load was in flight
A pin load reads the table and answers later. If a pin was still saving when the load read the table, but finished before the load answered, the load's older rows replaced it and the pin vanished from the screen. A load now also replays every change that was still saving when it started. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent ad43294 commit bf24d44

2 files changed

Lines changed: 23 additions & 2 deletions

File tree

‎src/lib/stores/debate-context-store.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ function ensureOverride(current: DebateContextOverride | undefined, debateId: st
5454
}
5555

5656
// Every local pin change, per debate and in order. A load replays the changes
57-
// it may have missed: ones made after it started, and ones still saving.
57+
// it may have missed: ones made after it started, and ones that were still
58+
// saving at any point during the load, because its read may predate them.
5859
// `before` is the result's state before this change; if the change fails, the
5960
// result goes back to it.
6061
interface PinChange { seq: number; ref: DebateOutputRef; pinned: boolean; before: boolean; saving: boolean }
@@ -81,6 +82,7 @@ export const useDebateContextStore = create<DebateContextState>()((set, get) =>
8182
loadPins: async (debateId) => {
8283
const startedAt = pinSeq;
8384
const generation = pinGeneration;
85+
const savingAtStart = new Set((pinChanges[debateId] ?? []).filter((change) => change.saving));
8486
const { data, error } = await supabase
8587
.from('debate_context_pins')
8688
.select('item_kind,item_id')
@@ -97,7 +99,7 @@ export const useDebateContextStore = create<DebateContextState>()((set, get) =>
9799
? [{ kind: row.item_kind as DebateContextOutputKind, id: row.item_id }]
98100
: []);
99101
for (const change of pinChanges[debateId] ?? []) {
100-
if (change.seq > startedAt || change.saving) pins = withPin(pins, change.ref, change.pinned);
102+
if (change.seq > startedAt || change.saving || savingAtStart.has(change)) pins = withPin(pins, change.ref, change.pinned);
101103
}
102104
set((state) => ({
103105
pins: { ...state.pins, [debateId]: pins },

‎src/test/debate/debate-context.test.tsx‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,25 @@ describe('Pinned results', () => {
288288
expect(useDebateContextStore.getState().pins[TEST_DEBATE_ID]).toEqual([caseRef, attackRef]);
289289
});
290290

291+
it('keeps a pin that finished saving while an older load was in flight', async () => {
292+
const insert = deferred<{ error: null }>();
293+
const load = deferred<{ data: unknown; error: null }>();
294+
supabaseMock.from
295+
.mockImplementationOnce((() => ({ insert: () => insert.promise })) as never)
296+
.mockImplementationOnce((() => ({ select: () => ({ eq: () => load.promise }) })) as never);
297+
298+
const pinning = useDebateContextStore.getState().togglePin(TEST_DEBATE_ID, caseRef);
299+
await new Promise((done) => setTimeout(done, 0));
300+
// The load reads the table before the insert commits, then answers after it.
301+
const loading = useDebateContextStore.getState().loadPins(TEST_DEBATE_ID);
302+
insert.resolve({ error: null });
303+
await pinning;
304+
load.resolve({ data: [], error: null });
305+
await loading;
306+
307+
expect(useDebateContextStore.getState().pins[TEST_DEBATE_ID]).toEqual([caseRef]);
308+
});
309+
291310
it('saves a quick pin and unpin of the same result in the order they were made', async () => {
292311
const insert = deferred<{ error: null }>();
293312
const calls: string[] = [];

0 commit comments

Comments
 (0)