🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1003 lines
25 KiB
Markdown
1003 lines
25 KiB
Markdown
# 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<string, any>
|
|
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<string, { userId: string; lastSeen: number }> = new Map()
|
|
private subscriptions: Map<string, Set<string>> = 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<void> {
|
|
this.running = true
|
|
this.emit('ready')
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
this.running = false
|
|
this.clients.clear()
|
|
this.subscriptions.clear()
|
|
}
|
|
|
|
async handleClientConnect(clientId: string, userId: string): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<WebSocketMessage[]> {
|
|
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<void> {
|
|
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<string, any>): 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<void> {
|
|
this.userId = userId
|
|
await this.client.connect(userId)
|
|
}
|
|
|
|
async sendHighlightCreate(highlight: BibleHighlight): Promise<void> {
|
|
this.client.send('highlight:create', highlight)
|
|
}
|
|
|
|
async sendHighlightUpdate(highlight: BibleHighlight): Promise<void> {
|
|
this.client.send('highlight:update', highlight)
|
|
}
|
|
|
|
async sendHighlightDelete(highlightId: string): Promise<void> {
|
|
this.client.send('highlight:delete', { highlightId })
|
|
}
|
|
|
|
private async handleHighlightCreate(data: BibleHighlight): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<RealtimeSyncManager | null>(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 (
|
|
<Tooltip title="Real-time sync active">
|
|
<Chip
|
|
data-testid="sync-status-realtime"
|
|
icon={<SignalCellularAltIcon sx={{ color: 'primary.main' }} />}
|
|
label="Live Sync"
|
|
variant="filled"
|
|
color="primary"
|
|
size="small"
|
|
sx={{ fontWeight: 500, animation: 'pulse 2s infinite' }}
|
|
/>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
```
|
|
|
|
**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
|