fix: code review — WS sync bug, Hub race condition, error feedback
- Fix useRealtimeSync never receiving WSClient (useRef → useState for re-render trigger, keeping ref for lazy subscribe callback) - Fix Hub.Run() global broadcast mutating map under RLock (same two-phase collect+cleanup pattern as BroadcastToWorkspace) - Move visibleStatuses to module-level constant (prevent useCallback recreation every render) - Replace console.error with toast.error for user-facing operations in issues page and inbox page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
759dd741bd
commit
0c52b89e40
4 changed files with 40 additions and 16 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useInboxStore } from "@multica/store";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertCircle,
|
||||
Bot,
|
||||
|
|
@ -205,7 +206,7 @@ export default function InboxPage() {
|
|||
await api.markInboxRead(id);
|
||||
useInboxStore.getState().markRead(id);
|
||||
} catch (err) {
|
||||
console.error("Failed to mark read:", err);
|
||||
toast.error("Failed to mark as read");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -218,7 +219,7 @@ export default function InboxPage() {
|
|||
setSelectedId("");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to archive:", err);
|
||||
toast.error("Failed to archive");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { useIssueStore } from "@multica/store";
|
||||
import { toast } from "sonner";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Columns3,
|
||||
|
|
@ -55,6 +56,15 @@ function formatDate(date: string): string {
|
|||
});
|
||||
}
|
||||
|
||||
const BOARD_STATUSES: IssueStatus[] = [
|
||||
"backlog",
|
||||
"todo",
|
||||
"in_progress",
|
||||
"in_review",
|
||||
"done",
|
||||
"blocked",
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Board View — Card
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -187,14 +197,7 @@ function BoardView({
|
|||
})
|
||||
);
|
||||
|
||||
const visibleStatuses: IssueStatus[] = [
|
||||
"backlog",
|
||||
"todo",
|
||||
"in_progress",
|
||||
"in_review",
|
||||
"done",
|
||||
"blocked",
|
||||
];
|
||||
const visibleStatuses = BOARD_STATUSES;
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(event: DragStartEvent) => {
|
||||
|
|
@ -358,7 +361,7 @@ function CreateIssueDialog({ onCreated }: { onCreated: (issue: Issue) => void })
|
|||
reset();
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to create issue:", err);
|
||||
toast.error("Failed to create issue");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
|
@ -491,7 +494,7 @@ export default function IssuesPage() {
|
|||
|
||||
// Persist to API
|
||||
api.updateIssue(issueId, { status: newStatus }).catch((err) => {
|
||||
console.error("Failed to update issue:", err);
|
||||
toast.error("Failed to move issue");
|
||||
// Revert on error by refetching
|
||||
api.listIssues({ limit: 200 }).then((res) => {
|
||||
useIssueStore.getState().setIssues(res.issues);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
type ReactNode,
|
||||
|
|
@ -27,6 +28,7 @@ const WSContext = createContext<WSContextValue | null>(null);
|
|||
export function WSProvider({ children }: { children: ReactNode }) {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const [wsClient, setWsClient] = useState<WSClient | null>(null);
|
||||
const wsRef = useRef<WSClient | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -38,16 +40,18 @@ export function WSProvider({ children }: { children: ReactNode }) {
|
|||
const ws = new WSClient(WS_URL);
|
||||
ws.setAuth(token, workspace.id);
|
||||
wsRef.current = ws;
|
||||
setWsClient(ws);
|
||||
ws.connect();
|
||||
|
||||
return () => {
|
||||
ws.disconnect();
|
||||
wsRef.current = null;
|
||||
setWsClient(null);
|
||||
};
|
||||
}, [user, workspace]);
|
||||
|
||||
// Centralized WS → store sync
|
||||
useRealtimeSync(wsRef.current);
|
||||
// Centralized WS → store sync (uses state so it re-subscribes when WS changes)
|
||||
useRealtimeSync(wsClient);
|
||||
|
||||
const subscribe = useCallback(
|
||||
(event: WSEventType, handler: EventHandler) => {
|
||||
|
|
|
|||
|
|
@ -92,17 +92,33 @@ func (h *Hub) Run() {
|
|||
case message := <-h.broadcast:
|
||||
// Global broadcast for daemon events (no workspace filtering)
|
||||
h.mu.RLock()
|
||||
var slow []*Client
|
||||
for _, clients := range h.rooms {
|
||||
for client := range clients {
|
||||
select {
|
||||
case client.send <- message:
|
||||
default:
|
||||
close(client.send)
|
||||
delete(clients, client)
|
||||
slow = append(slow, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
if len(slow) > 0 {
|
||||
h.mu.Lock()
|
||||
for _, client := range slow {
|
||||
room := client.workspaceID
|
||||
if clients, ok := h.rooms[room]; ok {
|
||||
if _, exists := clients[client]; exists {
|
||||
delete(clients, client)
|
||||
close(client.send)
|
||||
if len(clients) == 0 {
|
||||
delete(h.rooms, room)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue