FOUNDRY
C8 Platform
← 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