fix(web): replace actor_id self-event filtering with idempotent cache updates

actor_id identifies the user, not the browser tab. Filtering WS events
by actor_id broke multi-tab sync — other tabs of the same user would
silently miss updates. Instead, make all WS cache handlers idempotent
(dedup checks on add, no-op on duplicate merge/filter) so mutations and
WS events coexist safely without filtering.

- WSClient: pass actor_id to event handlers for future per-handler use
- use-realtime-sync: remove isSelf() gating from onAny and specific handlers
- useCreateIssue: add .some() dedup guard + onSettled invalidation
- use-issue-reactions: remove payload-level self-filter (dedup already present)
- use-issue-timeline: remove payload-level self-filter on comment:created,
  reaction:added, reaction:removed (dedup already present)
- Clean up useCallback deps that no longer reference userId

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-04-08 13:57:24 +08:00
parent 88c2f4ddc4
commit d58f6cdb33
7 changed files with 20 additions and 32 deletions

View file

@ -4,7 +4,7 @@ import { useEffect } from "react";
import type { WSEventType } from "@/shared/types";
import { useWS } from "./provider";
type EventHandler = (payload: unknown) => void;
type EventHandler = (payload: unknown, actorId?: string) => void;
/**
* Hook that subscribes to a WebSocket event and calls the handler.

View file

@ -22,7 +22,7 @@ const WS_URL =
? `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/ws`
: "ws://localhost:8080/ws");
type EventHandler = (payload: unknown) => void;
type EventHandler = (payload: unknown, actorId?: string) => void;
interface WSContextValue {
subscribe: (event: WSEventType, handler: EventHandler) => () => void;

View file

@ -86,20 +86,16 @@ export function useRealtimeSync(ws: WSClient | null) {
]);
const unsubAny = ws.onAny((msg) => {
const myUserId = useAuthStore.getState().user?.id;
if (msg.actor_id && msg.actor_id === myUserId) {
logger.debug("skipping self-event", msg.type);
return;
}
if (specificEvents.has(msg.type)) return;
const prefix = msg.type.split(":")[0] ?? "";
const refresh = refreshMap[prefix];
if (refresh) debouncedRefresh(prefix, refresh);
});
// --- Specific event handlers (granular updates, no full refetch) ---
// NOTE: ws.on() passes msg.payload (no actor_id). Self-event suppression
// requires WSClient changes to expose actor_id — tracked as separate task.
// --- Specific event handlers (granular cache updates) ---
// No self-event filtering: actor_id identifies the USER, not the TAB.
// Filtering by actor_id would block other tabs of the same user.
// Instead, both mutations and WS handlers use dedup checks to be idempotent.
const unsubIssueUpdated = ws.on("issue:updated", (p) => {
const { issue } = p as IssueUpdatedPayload;