multica/_features/infra-event-bus-ws.json
Naiyuan Qing 9127e543d5 feat: add event bus, WS workspace isolation, and global store migration
- Add internal event bus (server/internal/events/) with synchronous
  pub/sub and panic isolation per listener
- Upgrade WebSocket Hub to workspace-scoped rooms with JWT auth
  and membership verification on connect
- Add 10 new WS event types (comment CRUD, inbox read/archive,
  agent create/delete, workspace/member events)
- Refactor all handlers and TaskService to publish events via Bus
  instead of direct Hub.Broadcast calls
- Add WS broadcast listener that routes events to correct workspace
- Frontend: WSClient sends token + workspace_id on connect with
  auto-reconnect refetch
- Frontend: centralized useRealtimeSync hook dispatches all WS
  events to global Zustand stores
- Migrate issues and inbox pages from local useState to global
  useIssueStore/useInboxStore
- Make store addIssue/addItem idempotent to prevent duplicates
- Remove dead packages/hooks/src/use-realtime.ts
- Add feature tracking files for 4 planned features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:08:27 +08:00

78 lines
5.1 KiB
JSON

{
"id": "infra-event-bus-ws",
"name": "Infrastructure: Event Bus + WS Isolation + Global Store",
"status": "done",
"createdAt": "2026-03-25",
"completedAt": "2026-03-25",
"description": "Foundation layer: internal event bus to decouple handlers from side-effects, WebSocket workspace isolation to fix multi-tenancy data leakage, and frontend global Zustand store with centralized WS sync.",
"currentState": "All tasks complete. Backend: Event Bus (server/internal/events/bus.go) with pub/sub + panic isolation, Hub upgraded to workspace-scoped rooms with JWT auth, all 11 handler broadcast calls replaced with Bus.Publish, 10 new event types added, inbox/workspace/agent handlers now emit events. Frontend: WSClient sends token+workspace_id, useRealtimeSync hook centralizes WS→store sync with reconnect refetch, issues/inbox pages migrated to global stores, dead use-realtime.ts removed. Go build + typecheck both pass.",
"decisions": [
"Event bus is in-process Go pub/sub (not external MQ), synchronous execution with recover isolation",
"Hub upgraded to room-based: map[workspaceID]map[*Client]bool, BroadcastToWorkspace replaces Broadcast",
"WS auth via query param ?token=xxx&workspace_id=yyy, parsed in HandleWebSocket before upgrade",
"Client struct gains userID + workspaceID fields, set during WS handshake",
"Frontend uses packages/store (useIssueStore, useInboxStore, useAgentStore) as single source of truth",
"useRealtimeSync() called once inside WSProvider, handles all WS event -> store updates",
"WS reconnect triggers refetch of issues + inbox + agents to recover missed events",
"Comment events stay page-local on issue detail page (not in global store)",
"Inbox creation stays in handlers and TaskService for now (complex business logic), will extract to bus listeners later",
"Broadcast() kept for daemon events (no workspace scope), BroadcastToWorkspace() for all user-facing events"
],
"tasks": [
{
"task": "Backend: Create internal event bus (server/internal/events/bus.go)",
"done": true,
"scope": "Bus struct with Publish/Subscribe, Event type with Type/WorkspaceID/ActorType/ActorID/Payload. Synchronous dispatch with recover per listener. Unit test for pub/sub + panic isolation."
},
{
"task": "Backend: Register event listeners for WS broadcast",
"done": true,
"scope": "Listener that receives any event and calls Hub.BroadcastToWorkspace. Covers: issue CRUD, comment CRUD, agent status, inbox new/read/archive, task lifecycle events."
},
{
"task": "Backend: Register event listeners for inbox creation",
"done": false,
"scope": "Deferred: inbox creation stays in handlers/TaskService for now. Will extract to bus listeners when adding new notification triggers (inbox-notifications feature)."
},
{
"task": "Backend: Refactor handlers to publish events instead of direct broadcast/inbox",
"done": true,
"scope": "All handlers (issue, comment, agent, inbox, workspace, daemon) emit events via bus.Publish. Remove direct h.broadcast() calls. Task service also emits events via Bus.Publish."
},
{
"task": "Backend: Upgrade Hub to workspace-scoped rooms",
"done": true,
"scope": "Hub.rooms map, Client has userID+workspaceID, BroadcastToWorkspace method. HandleWebSocket validates JWT from ?token query param before upgrade. Reject unauthenticated connections."
},
{
"task": "Backend: Add missing WS event types to protocol",
"done": true,
"scope": "Added: EventCommentCreated/Updated/Deleted, EventInboxRead, EventInboxArchived, EventAgentCreated, EventAgentDeleted, EventWorkspaceUpdated, EventMemberAdded, EventMemberRemoved to both protocol/events.go and packages/types/src/events.ts."
},
{
"task": "Frontend: WSClient sends token on connect",
"done": true,
"scope": "WSClient.connect() builds URL with ?token=xxx&workspace_id=yyy. setAuth() method sets credentials. WSProvider reads token from localStorage, workspace from store. Reconnects when workspace changes."
},
{
"task": "Frontend: Implement useRealtimeSync() hook",
"done": true,
"scope": "Called inside WSProvider. Subscribes to issue/inbox/agent WS events → dispatches to global stores. onReconnect refetches issues+inbox+agents. Comment events excluded (page-local)."
},
{
"task": "Frontend: Migrate issues page from useState to useIssueStore",
"done": true,
"scope": "Issues page reads from useIssueStore. Filters applied locally via useMemo. Initial fetch populates store. WS event handlers removed (handled by useRealtimeSync). Drag-drop uses store for optimistic updates."
},
{
"task": "Frontend: Migrate inbox page from useState to useInboxStore",
"done": true,
"scope": "Inbox page reads from useInboxStore. Sorting applied locally via useMemo. WS handler removed. markRead updates store directly."
},
{
"task": "Frontend: Clean up dead store code",
"done": true,
"scope": "Removed packages/hooks/src/use-realtime.ts. Updated packages/hooks/src/index.ts. No duplicate WS subscriptions remain in pages."
}
]
}