mirror of
https://github.com/block/buzz.git
synced 2026-11-03 07:51:22 +00:00
desktop: treat duplicate-on-add as idempotent success for reactions
Perci caught a blocking gap on Tyler's exact repro: the relay already holds the reactor's kind:7 (it never came back over the #h live sub, so the cache didn't render it). Clicking again ran onMutate (optimistic write) -> addReaction rejected with "duplicate: reaction already exists" -> onError rolled the optimistic reaction back -> we were straight back to "duplicate but nothing visible". The optimistic write was being undone on precisely the path it was meant to fix. Swallow duplicate-on-add in mutationFn (reusing the existing isDuplicateReactionError predicate that Pulse already uses) so onError never runs and the optimistic reaction survives. Other failures and the remove path still propagate and roll back. Adds a regression test pinning the relay error string the swallow depends on. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
parent
1e6e0eefb8
commit
d93c7d78f8
@@ -38,6 +38,7 @@ import {
|
||||
MIN_TOP_LEVEL_ROWS_PER_FETCH,
|
||||
pageOlderMessagesUntilRowFloor,
|
||||
} from "@/features/messages/lib/pageOlderMessages";
|
||||
import { isDuplicateReactionError } from "@/features/pulse/lib/noteActions";
|
||||
import {
|
||||
KIND_REACTION,
|
||||
KIND_STREAM_MESSAGE,
|
||||
@@ -575,7 +576,18 @@ export function useToggleReactionMutation() {
|
||||
emoji,
|
||||
queryClient.getQueryData<CustomEmoji[]>(customEmojiQueryKey),
|
||||
);
|
||||
await addReaction(eventId, emoji, emojiUrl);
|
||||
try {
|
||||
await addReaction(eventId, emoji, emojiUrl);
|
||||
} catch (error) {
|
||||
// The relay already has this reaction (its `e`-only kind:7 never came
|
||||
// back over the `#h` live sub, so the cache didn't render it and the
|
||||
// user clicked again — Tyler's exact repro). That's idempotent
|
||||
// success: swallow it so onError doesn't roll back the optimistic
|
||||
// write and leave us back at "duplicate but nothing visible".
|
||||
if (!isDuplicateReactionError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (_error, _variables, context) => {
|
||||
if (context) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import test from "node:test";
|
||||
import { createOptimisticReaction } from "./optimisticReaction.ts";
|
||||
import { formatTimelineMessages } from "./formatTimelineMessages.ts";
|
||||
import { sortMessages } from "./messageQueryKeys.ts";
|
||||
import { isDuplicateReactionError } from "@/features/pulse/lib/noteActions";
|
||||
import { KIND_REACTION } from "@/shared/constants/kinds";
|
||||
|
||||
const CHANNEL_ID = "36411e44-0e2d-4cfe-bd6e-567eb169db9f";
|
||||
@@ -101,3 +102,24 @@ test("custom-emoji optimistic reaction carries the NIP-30 emoji tag", () => {
|
||||
);
|
||||
assert.equal(optimistic.content, ":party:");
|
||||
});
|
||||
|
||||
test("duplicate-on-add is recognized so the optimistic write is kept, not rolled back", () => {
|
||||
// Tyler's exact repro: the relay already holds the reactor's ✅ (it never
|
||||
// came back over the `#h` live sub, so the cache didn't render it). Clicking
|
||||
// again writes the optimistic reaction, then `addReaction` rejects with the
|
||||
// relay's duplicate message. useToggleReactionMutation swallows precisely
|
||||
// this error so onError never runs and the optimistic reaction survives —
|
||||
// turning "duplicate but nothing visible" into a rendered ✅. This pins the
|
||||
// relay error string that swallow depends on; if it drifts, the bug returns.
|
||||
const relayDuplicate = new Error(
|
||||
"relay rejected event: duplicate: reaction already exists",
|
||||
);
|
||||
assert.equal(isDuplicateReactionError(relayDuplicate), true);
|
||||
|
||||
// Any other failure must still propagate (onError rolls back) — we only
|
||||
// treat duplicate-on-add as idempotent success.
|
||||
assert.equal(
|
||||
isDuplicateReactionError(new Error("relay rejected event: rate-limited")),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user