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

@ -34,8 +34,6 @@ export function useIssueReactions(issueId: string, userId?: string) {
(payload: unknown) => {
const { reaction, issue_id } = payload as IssueReactionAddedPayload;
if (issue_id !== issueId) return;
if (reaction.actor_type === "member" && reaction.actor_id === userId)
return;
qc.setQueryData<IssueReaction[]>(
issueKeys.reactions(issueId),
(old) => {
@ -45,7 +43,7 @@ export function useIssueReactions(issueId: string, userId?: string) {
},
);
},
[qc, issueId, userId],
[qc, issueId],
),
);
@ -55,7 +53,6 @@ export function useIssueReactions(issueId: string, userId?: string) {
(payload: unknown) => {
const p = payload as IssueReactionRemovedPayload;
if (p.issue_id !== issueId) return;
if (p.actor_type === "member" && p.actor_id === userId) return;
qc.setQueryData<IssueReaction[]>(
issueKeys.reactions(issueId),
(old) =>
@ -69,7 +66,7 @@ export function useIssueReactions(issueId: string, userId?: string) {
),
);
},
[qc, issueId, userId],
[qc, issueId],
),
);

View file

@ -63,11 +63,6 @@ export function useIssueTimeline(issueId: string, userId?: string) {
(payload: unknown) => {
const { comment } = payload as CommentCreatedPayload;
if (comment.issue_id !== issueId) return;
if (
comment.author_type === "member" &&
comment.author_id === userId
)
return;
qc.setQueryData<TimelineEntry[]>(
issueKeys.timeline(issueId),
(old) => {
@ -77,7 +72,7 @@ export function useIssueTimeline(issueId: string, userId?: string) {
},
);
},
[qc, issueId, userId],
[qc, issueId],
),
);
@ -161,11 +156,6 @@ export function useIssueTimeline(issueId: string, userId?: string) {
(payload: unknown) => {
const { reaction, issue_id } = payload as ReactionAddedPayload;
if (issue_id !== issueId) return;
if (
reaction.actor_type === "member" &&
reaction.actor_id === userId
)
return;
qc.setQueryData<TimelineEntry[]>(
issueKeys.timeline(issueId),
(old) =>
@ -177,7 +167,7 @@ export function useIssueTimeline(issueId: string, userId?: string) {
}),
);
},
[qc, issueId, userId],
[qc, issueId],
),
);
@ -187,7 +177,6 @@ export function useIssueTimeline(issueId: string, userId?: string) {
(payload: unknown) => {
const p = payload as ReactionRemovedPayload;
if (p.issue_id !== issueId) return;
if (p.actor_type === "member" && p.actor_id === userId) return;
qc.setQueryData<TimelineEntry[]>(
issueKeys.timeline(issueId),
(old) =>
@ -207,7 +196,7 @@ export function useIssueTimeline(issueId: string, userId?: string) {
}),
);
},
[qc, issueId, userId],
[qc, issueId],
),
);