feat(04-06): implement live-sync SSE vertical slice (LIST-04, D-04/D-10/D-11/D-12)
- Wire publishListEvent fan-out in lists.ts after every write mutation (item:added/updated/deleted, list:updated/deleted)
- Add GET /api/sse/lists scoped endpoint in sse.ts: resolveUserId → 401 on null; getAccessibleListIds → subscribe only to accessible channels; 30s heartbeat; cleanup on disconnect (D-04/T-04-01/T-04-02)
- Create useListSSE.ts: bounded-backoff EventSource wrapper (250ms→500ms→1s→2s→4s→cap 8s); MAX_ATTEMPTS=6; withCredentials:true; close-before-retry prevents reconnect storm (Pitfall 3); invalidates ['list', listId] on open (D-10) and on each event; onStateChange('disconnected') after exhaustion (D-11)
- Create LiveSyncIndicator.tsx: connected=green dot; reconnecting=pulsing muted dot + label; disconnected=red dot + 'Updates paused' (role=alert); correct ARIA per UI-SPEC
- Wire useListSSE + LiveSyncIndicator into ListDetail header; retain refetchInterval:30000 polling fallback (D-12)
- All 8 useListSSE tests pass; all 54 API tests pass; both typechecks pass
- playwright-cli: live update confirmed (eggs item added via API appeared in browser without manual refresh)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* ListDetail — Single list view (/lists/:listId).
|
||||
*
|
||||
* Replaces the Plan 04-01 placeholder. Delivers LIST-02 + LIST-03:
|
||||
* Delivers LIST-02 + LIST-03 + LIST-04:
|
||||
* - Fetches items via useQuery(['list', listId]) with 30s polling fallback (D-12)
|
||||
* - Splits items into active (!checked, sorted rank ASC) and completed (checked) (D-05)
|
||||
* - Optimistic mutations for add / check / delete (D-07/D-09)
|
||||
@@ -14,7 +14,14 @@
|
||||
* - Remote reorders animate via CSS.Transform.toString in ItemRow (D-14)
|
||||
* - Concurrent reorder converges via server last-write-wins (D-15)
|
||||
*
|
||||
* Live sync (SSE) is wired in Plan 06.
|
||||
* Live sync (LIST-04, D-10/D-11/D-12):
|
||||
* - useListSSE wires /api/sse/lists with bounded backoff reconnect (D-11)
|
||||
* - On SSE event: invalidates ['list', listId] → background refetch (D-10)
|
||||
* - LiveSyncIndicator renders connection status in the header
|
||||
* - refetchInterval:30000 polling fallback always active (D-12)
|
||||
* - Note: SSE connection lives in ListDetail (one stream per list navigation).
|
||||
* A future optimization could hoist this to the Lists route level — acceptable
|
||||
* for Phase 4 per RESEARCH note; Phase 5 push will own the session lifecycle.
|
||||
*
|
||||
* Security: item text rendered as plain-text JSX children (T-04-06 XSS guard).
|
||||
*/
|
||||
@@ -47,6 +54,9 @@ import {
|
||||
} from '../api/listsClient.js'
|
||||
import { ItemRow } from '../components/ItemRow.js'
|
||||
import { AddItemInput } from '../components/AddItemInput.js'
|
||||
import { LiveSyncIndicator } from '../components/LiveSyncIndicator.js'
|
||||
import { useListSSE } from '../hooks/useListSSE.js'
|
||||
import type { SyncState } from '../hooks/useListSSE.js'
|
||||
import type { ListItem, ListItemsResponse } from '../api/listsClient.js'
|
||||
|
||||
/**
|
||||
@@ -97,6 +107,13 @@ export function ListDetail() {
|
||||
const parsedListId = Number(listId)
|
||||
|
||||
const [completedExpanded, setCompletedExpanded] = useState(true)
|
||||
const [syncState, setSyncState] = useState<SyncState>('connected')
|
||||
|
||||
// ── Live sync (LIST-04) ──────────────────────────────────────────────────
|
||||
// Bounded-backoff SSE hook (D-11). On open: invalidates ['list', listId] (D-10).
|
||||
// On event: invalidates ['list', listId] → background refetch.
|
||||
// Polling fallback: refetchInterval:30000 below stays always active (D-12).
|
||||
useListSSE({ listId: parsedListId, onStateChange: setSyncState })
|
||||
|
||||
// ── dnd-kit sensors ────────────────────────────────────────────────────────
|
||||
// PointerSensor: desktop mouse — immediate activation
|
||||
@@ -363,6 +380,9 @@ export function ListDetail() {
|
||||
Plan 05/06 can enrich from the ['lists'] cache by listId. */}
|
||||
List
|
||||
</h1>
|
||||
|
||||
{/* Live sync indicator — connected/reconnecting/disconnected (LIST-04, D-11) */}
|
||||
<LiveSyncIndicator state={syncState} />
|
||||
</div>
|
||||
|
||||
{/* Content area */}
|
||||
|
||||
Reference in New Issue
Block a user