技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
backend-realtime【数据与研究】Implement real-time features with WebSockets, Supabase Realtime, or Server-Sent Events. Use when "real-time", "live updates", "WebSocket", "notifications", "chat", "presence", "collaborative", "live data", or "instant sync".
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Degree of freedom: MIXED. Which transport and event [HIGH freedom];
existing-subscription probes and unmount cleanup [LOW freedom — run exactly].
Observe: chat mounts
useRealtimeMessagestwice; no untrack on leave;package.jsonalready has@supabase/supabase-js. Interpret: duplicate INSERT listeners; leftover presence. Classify: one channel hook with unmount cleanup; reuse Supabase Realtime — do not add Socket.io. Verify: onepostgres_changessubscription;removeChannelon unmount; no second socket lib.
removeChannel / closedebug-fe-be-integration; queue/job instead of live push → backend-patternsImplement live, collaborative features using WebSockets, Supabase Realtime, and Server-Sent Events.
Code examples assume a Next.js App Router + Supabase stack (
@/lib/supabase/client,'use client'). Adapt import paths and row types to the detected stack.
Before implementing ANY real-time feature, verify:
cat package.json | grep -i "socket\|realtime\|pusher\|ably"
rg "supabase.*channel|useSubscription|WebSocket" --type ts --type tsx
rg "on\('INSERT'\|on\('UPDATE'\|subscribe\(" --type ts
ls -la src/hooks/use*Realtime* src/lib/realtime* 2>/dev/null
rg "createClient|supabaseUrl" --type ts -l
grep -oh "SUPABASE_[A-Z_]*" .env* 2>/dev/null | sort -u # names only — never print values
Why: Real-time connections are stateful. Don't create duplicate subscriptions.
The canonical useRealtimeMessages hook (initial fetch + INSERT/DELETE
subscription with unmount cleanup) is in
references/patterns.md.
useEffect(() => {
const channel = supabase
.channel(`room:${roomId}`)
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages', filter: `room_id=eq.${roomId}` },
(payload) => setMessages((prev) => [...prev, payload.new as Message]))
.subscribe()
return () => { supabase.removeChannel(channel) }
}, [roomId, supabase])
presence:${roomId}); channel.track(currentUser) only after status === 'SUBSCRIBED'.presenceState() on the sync event; join/leave are for side effects only.channel.untrack() then supabase.removeChannel(channel).Full usePresence hook: references/patterns.md §Presence.
useRef; register .on('broadcast', { event }) handlers once, then .subscribe().channel.send({ type: 'broadcast', event, payload }); broadcast is ephemeral, never a source of truth.postgres_changes for anything that must persist.Full useBroadcast hook and collaborative-cursor usage: references/patterns.md §Broadcast.
useQuery as the data source; the realtime channel only calls queryClient.invalidateQueries({ queryKey }).postgres_changes with event: '*' on the table (optional filter); remove the channel on unmount.useRealtimeQuery(queryKey, queryFn, table, filter?), so every list gets the same behaviour.Full useRealtimeQuery hook: references/patterns.md §TanStack Query + Real-time.
useOptimistic(initial, reducer) plus useTransition; add the optimistic row with pending: true and a crypto.randomUUID() id, then await the Server Action.isPending.const [optimisticMessages, addOptimisticMessage] = useOptimistic(
initialMessages, (state, msg: Message) => [...state, msg]
)
startTransition(async () => {
addOptimisticMessage({ id: crypto.randomUUID(), content, pending: true, /* ... */ })
await addMessage(content) // Server Action
})
Full Chat component: references/patterns.md §Optimistic Updates.
ReadableStream; write data: ${JSON.stringify(x)}\n\n frames with TextEncoder.text/event-stream, Cache-Control: no-cache, Connection: keep-alive; heartbeat every 30 s.request.signal abort (clear timers, controller.close()).new EventSource(url); close in the effect cleanup and on onerror, then reconnect with backoff.Full route handler and useSSE hook: references/patterns.md §Server-Sent Events.
{ event: 'typing', payload: { userId } } on keystroke; ignore your own id on receipt.typingUsers and drop them after 3 s of silence; debounce sends with a timeoutRef.Full useTypingIndicator hook: references/patterns.md §Typing Indicators.
After implementing real-time features: