From b6620cd78d2b09d35d47a61c0e5f949a019ffb52 Mon Sep 17 00:00:00 2001 From: Andrei Date: Wed, 12 Nov 2025 08:26:16 +0000 Subject: [PATCH] docs: add Phase 2.1C implementation plan - real-time WebSocket sync design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../2025-01-12-phase-2-1c-realtime-sync.md | 1002 +++++++++++++++++ 1 file changed, 1002 insertions(+) create mode 100644 docs/plans/2025-01-12-phase-2-1c-realtime-sync.md diff --git a/docs/plans/2025-01-12-phase-2-1c-realtime-sync.md b/docs/plans/2025-01-12-phase-2-1c-realtime-sync.md new file mode 100644 index 0000000..8b3ac33 --- /dev/null +++ b/docs/plans/2025-01-12-phase-2-1c-realtime-sync.md @@ -0,0 +1,1002 @@ +# Phase 2.1C: Real-time WebSocket Sync - Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task with code reviews. + +**Goal:** Implement real-time bidirectional synchronization using WebSockets so highlights sync instantly across devices instead of every 30 seconds. + +**Architecture:** WebSocket server → Client subscriptions → Bi-directional sync → Presence indicators + +**Tech Stack:** TypeScript, Next.js, Socket.io (or ws), React, Prisma + +--- + +## Task 1: Set Up WebSocket Server Infrastructure + +**Files:** +- Create: `/root/biblical-guide/lib/websocket/server.ts` - WebSocket server +- Create: `/root/biblical-guide/lib/websocket/types.ts` - Type definitions +- Create: `/root/biblical-guide/__tests__/lib/websocket/server.test.ts` - Tests + +**Step 1: Write failing test** + +Create `/root/biblical-guide/__tests__/lib/websocket/server.test.ts`: + +```typescript +import { WebSocketServer } from '@/lib/websocket/server' +import { WebSocketMessage } from '@/lib/websocket/types' + +describe('WebSocketServer', () => { + let server: WebSocketServer + + beforeEach(() => { + server = new WebSocketServer(3011) + }) + + afterEach(() => { + server.close() + }) + + it('should initialize WebSocket server', () => { + expect(server).toBeDefined() + expect(server.getPort()).toBe(3011) + }) + + it('should have empty connections on start', () => { + expect(server.getConnectionCount()).toBe(0) + }) + + it('should emit ready event when started', (done) => { + server.on('ready', () => { + expect(server.isRunning()).toBe(true) + done() + }) + server.start() + }) + + it('should handle client connection', (done) => { + server.on('client-connect', (clientId) => { + expect(clientId).toBeDefined() + expect(server.getConnectionCount()).toBe(1) + done() + }) + server.start() + }) +}) +``` + +**Step 2: Run test to verify it fails** + +```bash +npm test -- __tests__/lib/websocket/server.test.ts +``` + +Expected output: FAIL - "WebSocketServer is not defined" + +**Step 3: Create WebSocket types** + +Create `/root/biblical-guide/lib/websocket/types.ts`: + +```typescript +export type WebSocketMessageType = + | 'highlight:create' + | 'highlight:update' + | 'highlight:delete' + | 'highlight:sync' + | 'presence:online' + | 'presence:offline' + | 'sync:request' + | 'sync:response' + +export interface WebSocketMessage { + type: WebSocketMessageType + payload: Record + timestamp: number + clientId: string +} + +export interface SyncRequest { + clientId: string + lastSyncTime: number + userId: string +} + +export interface SyncResponse { + highlights: any[] + serverTime: number + hasMore: boolean +} + +export interface ClientPresence { + clientId: string + userId: string + online: boolean + lastSeen: number +} + +export interface WebSocketServerOptions { + port: number + cors?: { + origin: string | string[] + credentials: boolean + } +} +``` + +**Step 4: Create WebSocket server** + +Create `/root/biblical-guide/lib/websocket/server.ts`: + +```typescript +import { EventEmitter } from 'events' +import { WebSocketMessage, WebSocketMessageType, ClientPresence } from './types' + +export class WebSocketServer extends EventEmitter { + private port: number + private running: boolean = false + private clients: Map = new Map() + private subscriptions: Map> = new Map() // userId -> clientIds + private messageQueue: WebSocketMessage[] = [] + + constructor(port: number) { + super() + this.port = port + } + + getPort(): number { + return this.port + } + + getConnectionCount(): number { + return this.clients.size + } + + isRunning(): boolean { + return this.running + } + + async start(): Promise { + this.running = true + this.emit('ready') + } + + async close(): Promise { + this.running = false + this.clients.clear() + this.subscriptions.clear() + } + + async handleClientConnect(clientId: string, userId: string): Promise { + this.clients.set(clientId, { userId, lastSeen: Date.now() }) + + if (!this.subscriptions.has(userId)) { + this.subscriptions.set(userId, new Set()) + } + this.subscriptions.get(userId)!.add(clientId) + + this.emit('client-connect', clientId) + } + + async handleClientDisconnect(clientId: string): Promise { + const client = this.clients.get(clientId) + if (client) { + const subscribers = this.subscriptions.get(client.userId) + if (subscribers) { + subscribers.delete(clientId) + } + this.clients.delete(clientId) + } + + this.emit('client-disconnect', clientId) + } + + async handleMessage(message: WebSocketMessage): Promise { + const client = this.clients.get(message.clientId) + if (!client) return + + this.messageQueue.push(message) + + const subscribers = this.subscriptions.get(client.userId) + if (subscribers) { + for (const subscriberId of subscribers) { + if (subscriberId !== message.clientId) { + this.emit('message-broadcast', { + message, + targetClients: [subscriberId] + }) + } + } + } + + this.emit('message-received', message) + } + + async getMessagesSince(clientId: string, timestamp: number): Promise { + return this.messageQueue.filter(m => m.timestamp > timestamp) + } + + getSubscribersForUser(userId: string): string[] { + const subs = this.subscriptions.get(userId) + return subs ? Array.from(subs) : [] + } +} +``` + +**Step 5: Run test to verify it passes** + +```bash +npm test -- __tests__/lib/websocket/server.test.ts +``` + +Expected output: PASS - all 4 tests pass + +**Step 6: Commit** + +```bash +git add lib/websocket/ __tests__/lib/websocket/ +git commit -m "feat: set up WebSocket server infrastructure" +``` + +--- + +## Task 2: Create Client-Side WebSocket Connection Manager + +**Files:** +- Create: `/root/biblical-guide/lib/websocket/client.ts` - Client connection +- Create: `/root/biblical-guide/lib/websocket/sync-manager.ts` - Sync coordination +- Test: `/root/biblical-guide/__tests__/lib/websocket/client.test.ts` + +**Step 1: Write failing test** + +Create `/root/biblical-guide/__tests__/lib/websocket/client.test.ts`: + +```typescript +import { WebSocketClient } from '@/lib/websocket/client' + +describe('WebSocketClient', () => { + let client: WebSocketClient + + beforeEach(() => { + client = new WebSocketClient('ws://localhost:3011') + }) + + afterEach(() => { + client.disconnect() + }) + + it('should initialize WebSocket client', () => { + expect(client).toBeDefined() + expect(client.isConnected()).toBe(false) + }) + + it('should emit connection event', (done) => { + client.on('connected', () => { + expect(client.isConnected()).toBe(true) + done() + }) + client.connect('user-1') + }) + + it('should handle message reception', (done) => { + client.on('highlight:create', (data) => { + expect(data).toBeDefined() + done() + }) + client.connect('user-1') + }) + + it('should queue messages when disconnected', () => { + expect(client.getQueueLength()).toBe(0) + client.send('highlight:create', { verseId: 'v-1', color: 'yellow' }) + expect(client.getQueueLength()).toBe(1) + }) +}) +``` + +**Step 2: Run test to verify it fails** + +```bash +npm test -- __tests__/lib/websocket/client.test.ts +``` + +Expected output: FAIL + +**Step 3: Create WebSocket client** + +Create `/root/biblical-guide/lib/websocket/client.ts`: + +```typescript +import { EventEmitter } from 'events' +import { WebSocketMessage, WebSocketMessageType } from './types' + +export class WebSocketClient extends EventEmitter { + private url: string + private clientId: string = `client-${Math.random().toString(36).substr(2, 9)}` + private userId: string | null = null + private connected: boolean = false + private messageQueue: WebSocketMessage[] = [] + private ws: WebSocket | null = null + private reconnectAttempts: number = 0 + private maxReconnectAttempts: number = 5 + private reconnectDelay: number = 1000 + + constructor(url: string) { + super() + this.url = url + } + + getClientId(): string { + return this.clientId + } + + isConnected(): boolean { + return this.connected && this.ws !== null && this.ws.readyState === WebSocket.OPEN + } + + getQueueLength(): number { + return this.messageQueue.length + } + + async connect(userId: string): Promise { + this.userId = userId + + return new Promise((resolve, reject) => { + try { + this.ws = new WebSocket(this.url) + + this.ws.onopen = () => { + this.connected = true + this.reconnectAttempts = 0 + this.emit('connected') + this.flushMessageQueue() + resolve() + } + + this.ws.onmessage = (event) => { + const message: WebSocketMessage = JSON.parse(event.data) + this.emit(message.type, message.payload) + this.emit('message', message) + } + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error) + this.emit('error', error) + reject(error) + } + + this.ws.onclose = () => { + this.connected = false + this.emit('disconnected') + this.attemptReconnect() + } + } catch (error) { + reject(error) + } + }) + } + + send(type: WebSocketMessageType, payload: Record): void { + const message: WebSocketMessage = { + type, + payload, + timestamp: Date.now(), + clientId: this.clientId + } + + if (this.isConnected() && this.ws) { + this.ws.send(JSON.stringify(message)) + } else { + this.messageQueue.push(message) + } + } + + private flushMessageQueue(): void { + while (this.messageQueue.length > 0) { + const message = this.messageQueue.shift() + if (message && this.ws) { + this.ws.send(JSON.stringify(message)) + } + } + } + + private attemptReconnect(): void { + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++ + const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1) + setTimeout(() => { + if (this.userId) { + this.connect(this.userId).catch(() => { + // Retry will happen in onclose + }) + } + }, delay) + } + } + + disconnect(): void { + if (this.ws) { + this.ws.close() + } + this.connected = false + this.messageQueue = [] + } +} +``` + +**Step 4: Create real-time sync manager** + +Create `/root/biblical-guide/lib/websocket/sync-manager.ts`: + +```typescript +import { WebSocketClient } from './client' +import { BibleHighlight } from '@/types' +import { addHighlight, updateHighlight, deleteHighlight } from '../highlight-manager' + +export class RealtimeSyncManager { + private client: WebSocketClient + private userId: string | null = null + + constructor(wsUrl: string) { + this.client = new WebSocketClient(wsUrl) + this.setupListeners() + } + + private setupListeners(): void { + this.client.on('highlight:create', (data) => this.handleHighlightCreate(data)) + this.client.on('highlight:update', (data) => this.handleHighlightUpdate(data)) + this.client.on('highlight:delete', (data) => this.handleHighlightDelete(data)) + this.client.on('disconnected', () => this.handleDisconnect()) + this.client.on('connected', () => this.handleConnect()) + } + + async connect(userId: string): Promise { + this.userId = userId + await this.client.connect(userId) + } + + async sendHighlightCreate(highlight: BibleHighlight): Promise { + this.client.send('highlight:create', highlight) + } + + async sendHighlightUpdate(highlight: BibleHighlight): Promise { + this.client.send('highlight:update', highlight) + } + + async sendHighlightDelete(highlightId: string): Promise { + this.client.send('highlight:delete', { highlightId }) + } + + private async handleHighlightCreate(data: BibleHighlight): Promise { + try { + await addHighlight(data) + this.client.emit('local-update', { type: 'create', highlight: data }) + } catch (error) { + console.error('Failed to create highlight from remote:', error) + } + } + + private async handleHighlightUpdate(data: BibleHighlight): Promise { + try { + await updateHighlight(data) + this.client.emit('local-update', { type: 'update', highlight: data }) + } catch (error) { + console.error('Failed to update highlight from remote:', error) + } + } + + private async handleHighlightDelete(data: { highlightId: string }): Promise { + try { + await deleteHighlight(data.highlightId) + this.client.emit('local-update', { type: 'delete', highlightId: data.highlightId }) + } catch (error) { + console.error('Failed to delete highlight from remote:', error) + } + } + + private handleConnect(): void { + console.log('WebSocket connected - real-time sync active') + } + + private handleDisconnect(): void { + console.log('WebSocket disconnected - falling back to polling') + } + + disconnect(): void { + this.client.disconnect() + } + + isConnected(): boolean { + return this.client.isConnected() + } +} +``` + +**Step 5: Run tests** + +```bash +npm test -- __tests__/lib/websocket/client.test.ts +``` + +Expected output: PASS - all 4 tests pass + +**Step 6: Commit** + +```bash +git add lib/websocket/client.ts lib/websocket/sync-manager.ts __tests__/lib/websocket/client.test.ts +git commit -m "feat: create WebSocket client and real-time sync manager" +``` + +--- + +## Task 3: Integrate Real-time Sync into BibleReaderApp + +**Files:** +- Modify: `/root/biblical-guide/components/bible/bible-reader-app.tsx` - Add WebSocket +- Create: `/root/biblical-guide/hooks/useRealtimeSync.ts` - Custom hook + +**Step 1: Create custom hook** + +Create `/root/biblical-guide/hooks/useRealtimeSync.ts`: + +```typescript +import { useEffect, useRef, useCallback } from 'react' +import { RealtimeSyncManager } from '@/lib/websocket/sync-manager' +import { BibleHighlight, HighlightColor } from '@/types' + +export function useRealtimeSync(userId: string | null, onRemoteUpdate?: (data: any) => void) { + const syncManagerRef = useRef(null) + + useEffect(() => { + if (!userId) return + + const wsUrl = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:3011' + syncManagerRef.current = new RealtimeSyncManager(wsUrl) + + syncManagerRef.current.connect(userId).catch((error) => { + console.error('Failed to connect WebSocket:', error) + }) + + if (onRemoteUpdate) { + syncManagerRef.current.client.on('local-update', onRemoteUpdate) + } + + return () => { + syncManagerRef.current?.disconnect() + } + }, [userId, onRemoteUpdate]) + + const sendHighlightCreate = useCallback((highlight: BibleHighlight) => { + syncManagerRef.current?.sendHighlightCreate(highlight) + }, []) + + const sendHighlightUpdate = useCallback((highlight: BibleHighlight) => { + syncManagerRef.current?.sendHighlightUpdate(highlight) + }, []) + + const sendHighlightDelete = useCallback((highlightId: string) => { + syncManagerRef.current?.sendHighlightDelete(highlightId) + }, []) + + const isConnected = useCallback(() => { + return syncManagerRef.current?.isConnected() ?? false + }, []) + + return { + sendHighlightCreate, + sendHighlightUpdate, + sendHighlightDelete, + isConnected + } +} +``` + +**Step 2: Update BibleReaderApp** + +Update `/root/biblical-guide/components/bible/bible-reader-app.tsx`: + +Add import: +```typescript +import { useRealtimeSync } from '@/hooks/useRealtimeSync' +``` + +Add hook usage (after other useEffect hooks): +```typescript +const { sendHighlightCreate, sendHighlightUpdate, sendHighlightDelete, isConnected } = useRealtimeSync( + userId, // from Clerk auth + (data) => { + // Handle remote updates + if (data.type === 'create' || data.type === 'update') { + const map = new Map(highlights) + map.set(data.highlight.verseId, data.highlight) + setHighlights(map) + } else if (data.type === 'delete') { + const map = new Map(highlights) + map.delete(data.highlightId) + setHighlights(map) + } + } +) +``` + +Update handleHighlightVerse to broadcast: +```typescript +async function handleHighlightVerse(color: HighlightColor = 'yellow') { + if (!selectedVerse) return + + const highlight: BibleHighlight = { + id: `h-${selectedVerse.id}-${Date.now()}`, + verseId: selectedVerse.id, + color, + createdAt: Date.now(), + updatedAt: Date.now(), + syncStatus: 'pending' + } + + try { + await addHighlight(highlight) + const newMap = new Map(highlights) + newMap.set(selectedVerse.id, highlight) + setHighlights(newMap) + + // Send via WebSocket for real-time sync + sendHighlightCreate(highlight) + } catch (error) { + console.error('Failed to highlight verse:', error) + } +} +``` + +**Step 3: Commit** + +```bash +git add hooks/useRealtimeSync.ts components/bible/bible-reader-app.tsx +git commit -m "feat: integrate real-time WebSocket sync into reader app" +``` + +--- + +## Task 4: Create WebSocket Server API Route + +**Files:** +- Create: `/root/biblical-guide/pages/api/ws.ts` - WebSocket endpoint + +**Step 1: Create WebSocket API endpoint** + +Create `/root/biblical-guide/pages/api/ws.ts`: + +```typescript +import { NextApiRequest, NextApiResponse } from 'next' +import { WebSocketServer } from 'ws' +import { getAuth } from '@clerk/nextjs/server' + +let wsServer: WebSocketServer | null = null + +const initWSServer = () => { + if (!wsServer) { + wsServer = new WebSocketServer({ noServer: true }) + + wsServer.on('connection', (ws, request) => { + const userId = (request as any).userId + + ws.on('message', (data) => { + try { + const message = JSON.parse(data.toString()) + + // Broadcast to all other clients for this user + wsServer?.clients.forEach((client) => { + if (client !== ws && client.readyState === client.OPEN) { + client.send(JSON.stringify(message)) + } + }) + } catch (error) { + console.error('Failed to process message:', error) + } + }) + + ws.on('close', () => { + console.log('Client disconnected') + }) + }) + } + return wsServer +} + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'GET') { + return res.status(405).end() + } + + try { + const { userId } = await getAuth(req) + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }) + } + + const server = initWSServer() + + // Upgrade HTTP to WebSocket + if (req.socket.readyState === 'open') { + server.handleUpgrade(req, req.socket, Buffer.alloc(0), (ws) => { + (req as any).userId = userId + server.emit('connection', ws, req) + }) + } + + res.status(200).end() + } catch (error) { + console.error('WebSocket error:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +``` + +**Step 2: Update environment variables** + +Add to `.env.local`: +```bash +NEXT_PUBLIC_WS_URL=ws://localhost:3000/api/ws +``` + +**Step 3: Commit** + +```bash +git add pages/api/ws.ts +git commit -m "feat: add WebSocket API endpoint" +``` + +--- + +## Task 5: Add Real-time Connection Status UI + +**Files:** +- Modify: `/root/biblical-guide/components/bible/sync-status-indicator.tsx` - Add real-time indicator +- Test: Add to existing component tests + +**Step 1: Update SyncStatusIndicator** + +Update `/root/biblical-guide/components/bible/sync-status-indicator.tsx`: + +Add import: +```typescript +import SignalCellularAltIcon from '@mui/icons-material/SignalCellularAlt' +``` + +Add new status in JSX before the error case: +```typescript +if (status === 'realtime') { + return ( + + } + label="Live Sync" + variant="filled" + color="primary" + size="small" + sx={{ fontWeight: 500, animation: 'pulse 2s infinite' }} + /> + + ) +} +``` + +**Step 2: Commit** + +```bash +git add components/bible/sync-status-indicator.tsx +git commit -m "feat: add real-time connection status indicator" +``` + +--- + +## Task 6: Add Tests for Real-time Sync + +**Files:** +- Create: `/root/biblical-guide/__tests__/e2e/realtime-sync.test.ts` + +**Step 1: Create E2E test** + +Create `/root/biblical-guide/__tests__/e2e/realtime-sync.test.ts`: + +```typescript +import { RealtimeSyncManager } from '@/lib/websocket/sync-manager' +import { BibleHighlight } from '@/types' + +describe('E2E: Real-time WebSocket Sync', () => { + it('should broadcast highlight create to other clients', async () => { + const highlight: BibleHighlight = { + id: 'h-1', + verseId: 'v-1', + color: 'yellow', + createdAt: Date.now(), + updatedAt: Date.now(), + syncStatus: 'synced' + } + + // Simulate two clients + const client1 = new RealtimeSyncManager('ws://localhost:3011') + const client2 = new RealtimeSyncManager('ws://localhost:3011') + + await client1.connect('user-1') + await client2.connect('user-1') + + let receivedData: BibleHighlight | null = null + client2.client.on('highlight:create', (data) => { + receivedData = data + }) + + client1.sendHighlightCreate(highlight) + + // Wait for message to propagate + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(receivedData).toEqual(highlight) + + client1.disconnect() + client2.disconnect() + }) + + it('should handle rapid updates from multiple clients', async () => { + const updates: BibleHighlight[] = [] + + const client1 = new RealtimeSyncManager('ws://localhost:3011') + const client2 = new RealtimeSyncManager('ws://localhost:3011') + + await client1.connect('user-1') + await client2.connect('user-1') + + client2.client.on('highlight:update', (data) => { + updates.push(data) + }) + + // Rapid updates + for (let i = 0; i < 10; i++) { + const highlight: BibleHighlight = { + id: `h-${i}`, + verseId: `v-${i}`, + color: i % 2 === 0 ? 'yellow' : 'blue', + createdAt: Date.now(), + updatedAt: Date.now(), + syncStatus: 'synced' + } + client1.sendHighlightUpdate(highlight) + } + + await new Promise((resolve) => setTimeout(resolve, 200)) + + expect(updates.length).toBe(10) + + client1.disconnect() + client2.disconnect() + }) + + it('should queue messages during disconnection', () => { + const client = new RealtimeSyncManager('ws://localhost:3011') + + // Don't connect + const highlight: BibleHighlight = { + id: 'h-1', + verseId: 'v-1', + color: 'yellow', + createdAt: Date.now(), + updatedAt: Date.now(), + syncStatus: 'synced' + } + + client.sendHighlightCreate(highlight) + + expect(client.client.getQueueLength()).toBeGreaterThan(0) + + client.disconnect() + }) +}) +``` + +**Step 2: Run tests** + +```bash +npm test -- __tests__/e2e/realtime-sync.test.ts +``` + +**Step 3: Commit** + +```bash +git add __tests__/e2e/realtime-sync.test.ts +git commit -m "test: add E2E tests for real-time WebSocket sync" +``` + +--- + +## Task 7: Documentation and Build Verification + +**Files:** +- Create: `/root/biblical-guide/docs/PHASE_2_1C_REALTIME_SYNC.md` - Documentation + +**Step 1: Create documentation** + +Create `/root/biblical-guide/docs/PHASE_2_1C_REALTIME_SYNC.md`: + +```markdown +# Phase 2.1C: Real-time WebSocket Sync - Completion Report + +## Overview +Real-time bidirectional synchronization of highlights across devices using WebSockets. + +## Architecture +- WebSocket server with connection management +- Client-side WebSocket connection manager +- React hook for integration +- Message broadcasting to all clients + +## Features +✅ Instant highlight updates across devices +✅ Automatic reconnection with exponential backoff +✅ Message queuing during disconnection +✅ Real-time connection status indicator +✅ Full TypeScript support +✅ Comprehensive test coverage + +## Files Added +- `lib/websocket/server.ts` - WebSocket server +- `lib/websocket/client.ts` - Client connection manager +- `lib/websocket/sync-manager.ts` - Real-time sync coordination +- `lib/websocket/types.ts` - Type definitions +- `hooks/useRealtimeSync.ts` - React integration hook +- `pages/api/ws.ts` - WebSocket API endpoint + +## Performance +- Message latency: < 50ms (local network) +- Auto-reconnect: Exponential backoff (1s, 2s, 4s, 8s, 16s) +- Queue capacity: Unlimited (with auto-flush on reconnect) +- Connection overhead: Minimal + +## Usage +```typescript +const { sendHighlightCreate, isConnected } = useRealtimeSync(userId) +sendHighlightCreate(highlight) +``` + +## Next Steps +- Delete operation support +- Presence indicators +- Analytics +- Compression for large payloads +``` + +**Step 2: Run build and tests** + +```bash +npm test 2>&1 | tail -20 +npm run build 2>&1 | tail -20 +``` + +Expected output: All tests pass, build successful + +**Step 3: Final commit** + +```bash +git add docs/PHASE_2_1C_REALTIME_SYNC.md +git commit -m "build: complete Phase 2.1C real-time WebSocket sync implementation" +``` + +--- + +## Summary + +Phase 2.1C implements: + +✅ WebSocket server infrastructure +✅ Client-side connection management +✅ Real-time bidirectional sync +✅ React hook integration +✅ Automatic reconnection +✅ Message queuing +✅ Connection status UI +✅ Comprehensive testing + +**Total effort**: ~4-5 hours for experienced developer + +**Next Phase (2.1D):** +- Delete operations +- Presence indicators (who's online) +- Advanced analytics +- Compression & optimization