fix(coding): preserve renderer recovery state
This commit is contained in:
@@ -216,6 +216,27 @@ function withRejectedNode(
|
||||
});
|
||||
}
|
||||
|
||||
function withUnreconciledOptimisticNodes(
|
||||
reducer: ConversationReducerState,
|
||||
requests: Record<string, CodingPromptRequestState>,
|
||||
): ConversationReducerState {
|
||||
const snapshot = reducer.snapshot;
|
||||
if (!snapshot) return reducer;
|
||||
const existingRequestIds = requestIdsInSnapshot(snapshot);
|
||||
const nodes = Object.values(requests).flatMap((request) => (
|
||||
request.nodeId
|
||||
&& request.status !== 'rejected'
|
||||
&& !existingRequestIds.has(request.clientRequestId)
|
||||
? [optimisticNode(request.nodeId, request.clientRequestId, request.submittedDraft)]
|
||||
: []
|
||||
));
|
||||
if (nodes.length === 0) return reducer;
|
||||
return replaceConversationSnapshot(reducer, {
|
||||
...snapshot,
|
||||
nodes: [...snapshot.nodes, ...nodes],
|
||||
});
|
||||
}
|
||||
|
||||
function snapshotIsOlder(
|
||||
current: ConversationSnapshot | null,
|
||||
incoming: ConversationSnapshot,
|
||||
@@ -241,11 +262,33 @@ export function createCodingConversationStore(
|
||||
): StoreApi<CodingConversationStoreState> {
|
||||
const deps = { ...defaultDependencies(), ...dependencies };
|
||||
const snapshotLoads = new Map<string, Promise<ConversationSnapshot>>();
|
||||
const recoveryPatches = new Map<string, CodingConversationPatchEvent[]>();
|
||||
let eventSource: EventSource | null = null;
|
||||
let connectFlight: Promise<void> | null = null;
|
||||
let connectionGeneration = 0;
|
||||
let store!: StoreApi<CodingConversationStoreState>;
|
||||
|
||||
return createStore<CodingConversationStoreState>((set, get) => ({
|
||||
const bufferRecoveryPatch = (event: CodingConversationPatchEvent) => {
|
||||
const buffered = recoveryPatches.get(event.conversationId) ?? [];
|
||||
if (buffered.some((candidate) => (
|
||||
candidate.workerGeneration === event.workerGeneration && candidate.seq === event.seq
|
||||
))) return;
|
||||
recoveryPatches.set(
|
||||
event.conversationId,
|
||||
[...buffered, event].sort((left, right) => (
|
||||
left.workerGeneration - right.workerGeneration || left.seq - right.seq
|
||||
)),
|
||||
);
|
||||
};
|
||||
|
||||
const replayRecoveryPatches = (conversationId: string) => {
|
||||
const buffered = recoveryPatches.get(conversationId);
|
||||
if (!buffered?.length) return;
|
||||
recoveryPatches.delete(conversationId);
|
||||
for (const event of buffered) store.getState().applyPatchEvent(event);
|
||||
};
|
||||
|
||||
store = createStore<CodingConversationStoreState>((set, get) => ({
|
||||
selectedConversationId: null,
|
||||
entriesByConversationId: {},
|
||||
summariesByConversationId: {},
|
||||
@@ -316,6 +359,8 @@ export function createCodingConversationStore(
|
||||
})
|
||||
.finally(() => {
|
||||
if (snapshotLoads.get(conversationId) === flight) snapshotLoads.delete(conversationId);
|
||||
const entry = get().entriesByConversationId[conversationId];
|
||||
if (!entry?.reducer.invalidation) replayRecoveryPatches(conversationId);
|
||||
});
|
||||
snapshotLoads.set(conversationId, flight);
|
||||
return flight;
|
||||
@@ -556,7 +601,14 @@ export function createCodingConversationStore(
|
||||
if (!current.reducer.invalidation && snapshotIsOlder(current.reducer.snapshot, snapshot)) {
|
||||
return state;
|
||||
}
|
||||
const reducer = replaceConversationSnapshot(current.reducer, snapshot);
|
||||
const requests = withoutReconciledRequests(
|
||||
state.requestsByConversationId[event.conversationId],
|
||||
snapshot,
|
||||
);
|
||||
const reducer = withUnreconciledOptimisticNodes(
|
||||
replaceConversationSnapshot(current.reducer, snapshot),
|
||||
requests,
|
||||
);
|
||||
const entry: CodingConversationEntry = {
|
||||
...current,
|
||||
reducer,
|
||||
@@ -572,17 +624,25 @@ export function createCodingConversationStore(
|
||||
summariesByConversationId: summariesWithEntry(state.summariesByConversationId, entry),
|
||||
requestsByConversationId: {
|
||||
...state.requestsByConversationId,
|
||||
[event.conversationId]: withoutReconciledRequests(
|
||||
state.requestsByConversationId[event.conversationId],
|
||||
snapshot,
|
||||
),
|
||||
[event.conversationId]: requests,
|
||||
},
|
||||
};
|
||||
});
|
||||
if (!snapshotLoads.has(event.conversationId)) {
|
||||
replayRecoveryPatches(event.conversationId);
|
||||
}
|
||||
},
|
||||
|
||||
applyPatchEvent(event) {
|
||||
if (event.type !== 'patch') return;
|
||||
const currentBeforePatch = get().entriesByConversationId[event.conversationId];
|
||||
if (currentBeforePatch?.reducer.invalidation) {
|
||||
bufferRecoveryPatch(event);
|
||||
if (!snapshotLoads.has(event.conversationId)) {
|
||||
void get().loadSnapshot(event.conversationId, true).catch(() => undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let recover = false;
|
||||
set((state) => {
|
||||
const current = state.entriesByConversationId[event.conversationId] ?? emptyEntry();
|
||||
@@ -626,6 +686,7 @@ export function createCodingConversationStore(
|
||||
}
|
||||
},
|
||||
}));
|
||||
return store;
|
||||
}
|
||||
|
||||
export const codingConversationStore = createCodingConversationStore();
|
||||
|
||||
Reference in New Issue
Block a user