← Tasks
[QU-01] Quilt client library for Expo (TypeScript) (retry 1)
completedcode_genP1
Description
## QU-01: Quilt client library for Expo (TypeScript)
Section: Quilt Integration | Milestone: M3 | Owner: AG Lu
### Failing Tests
- **quilt_client** (file_exists): File `c8-audio/lib/quilt.ts` must exist
- **supabase_dep** (grep): Pattern `supabase` in `c8-audio/package.json` (need 1+ matches)
- **sibling_messaging** (grep): Pattern `sibling|heartbeat|message` in `c8-audio/lib/quilt.ts` (need 1+ matches)
### Acceptance Criteria
- File `c8-audio/lib/quilt.ts` must exist
- File matching `c8-audio/package.json` must contain pattern `supabase`
- File matching `c8-audio/lib/quilt.ts` must contain pattern `sibling|heartbeat|message`
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
### Dependencies
- SC-01: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago
Quality
Overall
100%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 350,
"estimated_usd": 0.053235,
"output_tokens": 3479
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 40361,
"response_text": "I'll create a Quilt client library for the Expo app that handles sibling messaging and heartbeat functionality using Supabase.\n\nFirst, let me add the Supabase dependency to the package.json:\n\n```json\n{\n \"name\": \"c8-audio\",\n \"version\": \"1.0.0\",\n \"main\": \"node_modules/expo/AppEntry.js\",\n \"scripts\": {\n \"start\": \"expo start\",\n \"android\": \"expo start --android\",\n \"ios\": \"expo start --ios\",\n \"web\": \"expo start --web\"\n },\n \"dependencies\": {\n \"expo\": \"~49.0.0\",\n \"@expo/vector-icons\": \"^13.0.0\",\n \"react\": \"18.2.0\",\n \"react-native\": \"0.72.6\",\n \"@supabase/supabase-js\": \"^2.38.4\",\n \"react-native-url-polyfill\": \"^2.0.0\",\n \"zod\": \"^3.22.4\"\n },\n \"devDependencies\": {\n \"@babel/core\": \"^7.20.0\",\n \"@types/react\": \"~18.2.14\",\n \"typescript\": \"^5.1.3\"\n },\n \"private\": true\n}\n```\n\nNow, let me create the Quilt client library:\n\n```typescript\n// c8-audio/lib/quilt.ts\n\n// EN: Quilt client library for sibling device communication and heartbeat management\n// VI: Thư viện client Quilt cho giao tiếp thiết bị anh em và quản lý heartbeat\n// ES: Biblioteca cliente Quilt para comunicación entre dispositivos hermanos y gestión de heartbeat\n\nimport { createClient, SupabaseClient } from '@supabase/supabase-js';\nimport { z } from 'zod';\n\n// SPEC: QU-01 - Sibling messaging schema validation\nconst SiblingMessageSchema = z.object({\n id: z.string(),\n from_device_id: z.string(),\n to_device_id: z.string().optional(),\n message_type: z.enum(['heartbeat', 'audio_sync', 'playback_control', 'status_update']),\n payload: z.record(z.any()),\n timestamp: z.string(),\n expires_at: z.string().optional(),\n});\n\nconst HeartbeatSchema = z.object({\n device_id: z.string(),\n device_type: z.enum(['speaker', 'controller', 'hub']),\n status: z.enum(['online', 'offline', 'idle']),\n last_seen: z.string(),\n metadata: z.record(z.any()).optional(),\n});\n\ntype SiblingMessage = z.infer<typeof SiblingMessageSchema>;\ntype Heartbeat = z.infer<typeof HeartbeatSchema>;\n\nexport interface QuiltConfig {\n supabaseUrl: string;\n supabaseKey: string;\n deviceId: string;\n deviceType: 'speaker' | 'controller' | 'hub';\n}\n\nexport class QuiltClient {\n private supabase: SupabaseClient;\n private deviceId: string;\n private deviceType: 'speaker' | 'controller' | 'hub';\n private heartbeatInterval: NodeJS.Timeout | null = null;\n private messageListeners: Map<string, (message: SiblingMessage) => void> = new Map();\n\n constructor(config: QuiltConfig) {\n // EN: Initialize Supabase client for sibling communication\n // VI: Khởi tạo Supabase client cho giao tiếp thiết bị anh em\n // ES: Inicializar cliente Supabase para comunicación entre hermanos\n \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this.supabase = createClient(config.supabaseUrl, config.supabaseKey) as any;\n this.deviceId = config.deviceId;\n this.deviceType = config.deviceType;\n }\n\n // EN: Start heartbeat broadcasting to notify siblings of device status\n // VI: Bắt đầu phát heartbeat để thông báo trạng thái thiết bị cho anh em\n // ES: Iniciar transmisión de heartbeat para notificar el estado del dispositivo a los hermanos\n async startHeartbeat(intervalMs: number = 30000): Promise<void> {\n const sendHeartbeat = async () => {\n const heartbeat: Heartbeat = {\n device_id: this.deviceId,\n device_type: this.deviceType,\n status: 'online',\n last_seen: new Date().toISOString(),\n metadata: {\n timestamp: Date.now(),\n version: '1.0.0',\n },\n };\n\n const validation = HeartbeatSchema.safeParse(heartbeat);\n if (!validation.success) {\n console.error('Invalid heartbeat data:', validation.error);\n return;\n }\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { error } = await (this.supabase as any)\n .from('device_heartbeats')\n .upsert(heartbeat, { onConflict: 'device_id' });\n\n if (error) {\n console.error('Failed to send heartbeat:', error);\n }\n } catch (err) {\n console.error('Heartbeat error:', err);\n }\n };\n\n // Send initial heartbeat\n await sendHeartbeat();\n \n // Schedule recurring heartbeats\n this.heartbeatInterval = setInterval(sendHeartbeat, intervalMs);\n }\n\n // EN: Stop heartbeat broadcasting\n // VI: Dừng phát heartbeat\n // ES: Detener transmisión de heartbeat\n stopHeartbeat(): void {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n }\n\n // EN: Send message to sibling devices\n // VI: Gửi tin nhắn đến thiết bị anh em\n // ES: Enviar mensaje a dispositivos hermanos\n async sendMessage(\n messageType: SiblingMessage['message_type'],\n payload: Record<string, any>,\n targetDeviceId?: string\n ): Promise<boolean> {\n const message: SiblingMessage = {\n id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,\n from_device_id: this.deviceId,\n to_device_id: targetDeviceId,\n message_type: messageType,\n payload,\n timestamp: new Date().toISOString(),\n expires_at: new Date(Date.now() + 5 * 60 * 1000).toISOString(), // 5 minutes\n };\n\n const validation = SiblingMessageSchema.safeParse(message);\n if (!validation.success) {\n console.error('Invalid message data:', validation.error);\n return false;\n }\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { error } = await (this.supabase as any)\n .from('sibling_messages')\n .insert(message);\n\n if (error) {\n console.error('Failed to send message:', error);\n return false;\n }\n\n return true;\n } catch (err) {\n console.error('Message send error:', err);\n return false;\n }\n }\n\n // EN: Subscribe to sibling messages\n // VI: Đăng ký nhận tin nhắn từ thiết bị anh em\n // ES: Suscribirse a mensajes de dispositivos hermanos\n subscribeToMessages(\n messageType: SiblingMessage['message_type'] | 'all',\n callback: (message: SiblingMessage) => void\n ): () => void {\n const listenerId = `${messageType}_${Date.now()}`;\n this.messageListeners.set(listenerId, callback);\n\n // SPEC: QU-01 - Real-time sibling message subscription\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const subscription = (this.supabase as any)\n .channel('sibling_messages')\n .on(\n 'postgres_changes',\n {\n event: 'INSERT',\n schema: 'public',\n table: 'sibling_messages',\n filter: messageType === 'all' \n ? `to_device_id.eq.${this.deviceId},to_device_id.is.null`\n : `message_type.eq.${messageType}`,\n },\n (payload: any) => {\n const validation = SiblingMessageSchema.safeParse(payload.new);\n if (validation.success) {\n const message = validation.data;\n \n // Filter messages for this device\n if (!message.to_device_id || message.to_device_id === this.deviceId) {\n callback(message);\n }\n }\n }\n )\n .subscribe();\n\n // Return unsubscribe function\n return () => {\n this.messageListeners.delete(listenerId);\n subscription.unsubscribe();\n };\n }\n\n // EN: Get online sibling devices\n // VI: Lấy danh sách thiết bị anh em đang online\n // ES: Obtener dispositivos hermanos en línea\n async getOnlineSiblings(): Promise<Heartbeat[]> {\n try {\n const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();\n \n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data, error } = await (this.supabase as any)\n .from('device_heartbeats')\n .select('*')\n .neq('device_id', this.deviceId)\n .eq('status', 'online')\n .gt('last_seen', fiveMinutesAgo);\n\n if (error) {\n console.error('Failed to get online siblings:', error);\n return [];\n }\n\n return data?.map((item: any) => {\n const validation = HeartbeatSchema.safeParse(item);\n return validation.success ? validation.data : null;\n }).filter(Boolean) || [];\n } catch (err) {\n console.error('Get siblings error:', err);\n return [];\n }\n }\n\n // EN: Send heartbeat message to specific sibling\n // VI: Gửi tin nhắn heartbeat đến thiết bị anh em cụ thể\n // ES: Enviar mensaje de heartbeat a hermano específico\n async pingSibling(targetDeviceId: string): Promise<boolean> {\n return this.sendMessage('heartbeat', {\n ping: true,\n timestamp: Date.now(),\n from: this.deviceId,\n }, targetDeviceId);\n }\n\n // EN: Cleanup resources\n // VI: Dọn dẹp tài nguyên\n // ES: Limpiar recursos\n async disconnect(): Promise<void> {\n this.stopHeartbeat();\n \n // Send offline status\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await (this.supabase as any)\n .from('device_heartbeats')\n .update({ status: 'offline', last_seen: new Date().toISOString() })\n .eq('device_id', this.deviceId);\n } catch (err) {\n console.error('Failed to send offline status:', err);\n }\n\n this.messageListeners.clear();\n }\n}\n\n// EN: Export utility functions for message handling\n// VI: Xuất các hàm tiện ích để xử lý tin nhắn\n// ES: Exportar funciones de utilidad para manejo de mensajes\n\nexport const createHeartbeatMessage = (deviceId: string, deviceType: Heartbeat['device_type']) => ({\n device_id: deviceId,\n device_type: deviceType,\n status: 'online' as const,\n last_seen: new Date().toISOString(),\n});\n\nexport const createSiblingMessage = (\n fromDeviceId: string,\n messageType: SiblingMessage['message_type'],\n payload: Record<string, any>,\n toDeviceId?: string\n): SiblingMessage => ({\n id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,\n from_device_id: fromDeviceId,\n to_device_id: toDeviceId,\n message_type: messageType,\n payload,\n timestamp: new Date().toISOString(),\n});\n\nexport { SiblingMessageSchema, HeartbeatSchema };\nexport type { SiblingMessage, Heartbeat };\n```\n\nThe Quilt client library provides:\n\n1. **Sibling messaging**: Real-time communication between devices using Supabase real-time subscriptions\n2. **Heartbeat management**: Automatic heartbeat broadcasting to maintain device presence\n3. **Message validation**: Zod schemas for type-safe message handling\n4. **Device discovery**: Methods to find online sibling devices\n5. **TypeScript support**: Full type safety with proper interfaces\n\nKey features:\n- Supports different message types (heartbeat, audio_sync, playback_control, status_update)\n- Handles both broadcast and targeted messaging\n- Automatic cleanup and resource management\n- Error handling with proper logging\n- Follows the codebase conventions with trilingual comments and Zod validation"
}Task ID: bfc645d8-66ff-4de7-a306-ae034fcf2113