Skip to content

Commit fde003c

Browse files
committed
Don't reapply optimistic over canonical at trickle completion
The completion branch only consulted `arrival` to choose between discard and apply. `arrival` is set by the accelerator, which no-ops when `intervalID` is null — so two windows leak through: 1. Canonical lands during the 4s pre-trickle setTimeout. Accelerator runs but returns early (intervalID still null), arrival never set, the trickle runs full duration and merges the older optimistic payload on top of the canonical at the same reportActionID. Server-added markup (follow-up buttons, deep-link Pressables) gets clobbered until the next server update. 2. Canonical hadn't landed yet by completion — same path. Read persistedAction live from the trickleInputsRef at completion time and take the discard path whenever it's defined, regardless of how it got there. The visible trickle still runs to completion (start/complete logs fire as expected).
1 parent 21f3d5c commit fde003c

1 file changed

Lines changed: 9 additions & 8 deletions

File tree

src/hooks/usePendingConciergeResponse.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ function usePendingConciergeResponse(reportID: string | undefined) {
4848
// pendingResponse/tokens/fullHtml — without this snapshot, those non-content
4949
// updates would cancel the running interval and restart the reveal. The
5050
// useEffect keeps ref writes in the commit phase (React-Compiler-safe).
51-
const trickleInputsRef = useRef({pendingResponse, fullHtml, tokens, dispatchLocalDraftEvent});
51+
const trickleInputsRef = useRef({pendingResponse, fullHtml, tokens, dispatchLocalDraftEvent, persistedAction});
5252
useEffect(() => {
53-
trickleInputsRef.current = {pendingResponse, fullHtml, tokens, dispatchLocalDraftEvent};
53+
trickleInputsRef.current = {pendingResponse, fullHtml, tokens, dispatchLocalDraftEvent, persistedAction};
5454
});
5555

5656
// Reconciliation: when the canonical reportComment lands in REPORT_ACTIONS
@@ -146,12 +146,13 @@ function usePendingConciergeResponse(reportID: string | undefined) {
146146
arrivedAtElapsedMs: arrival?.elapsedMs,
147147
});
148148
dispatch('completed', snapshotTokens.at(-1) ?? snapshotHtml);
149-
// If acceleration fired, the canonical reportComment already landed in
150-
// REPORT_ACTIONS. Re-applying our older optimistic payload would clobber
151-
// server-added markup (e.g. follow-up buttons) until the next server
152-
// update. Just clear the pending state in that case — the synthetic
153-
// bubble fades into the canonical row.
154-
if (arrival) {
149+
// If the canonical reportComment is already in REPORT_ACTIONS at completion
150+
// time — whether acceleration fired or it landed during the pre-trickle
151+
// setTimeout (when accelerate runs but no-ops because intervalID is null) —
152+
// re-applying our older optimistic would clobber server-added markup
153+
// (follow-up buttons, deep-link Pressables) until the next server update.
154+
// Read live from the ref so we catch arrivals the accelerator missed.
155+
if (arrival || trickleInputsRef.current.persistedAction) {
155156
discardPendingConciergeAction(reportID);
156157
} else {
157158
applyPendingConciergeAction(reportID, reportAction);

0 commit comments

Comments
 (0)