Files
collective-lists/plan/fase-2b-realtime-modo-compra.md
Oier Bravo Urtasun e7a961a66d feat(fase-2b): Realtime sync + offline queue + Modo Compra
Fase 2b closes the critical-path product differentiator. Two sessions on the
same list see each other's changes in real time, mutations survive network
drops and sync when reconnected, and a dedicated full-screen shopping view
optimises the in-store experience.

2b.1 Realtime
- `$lib/stores/realtimeSync.ts` wraps `postgres_changes` with an `applyItemEvent`
  reducer (INSERT/UPDATE/DELETE → next items[]) and a `subscribeToList` helper
  that awaits the SUBSCRIBED handshake before returning
- `/lists/[id]` subscribes in onMount, unsubscribes in onDestroy. Uses a
  `pendingTempIds` set to deduplicate own-mutation echoes against optimistic
  rows (matches by name+created_by+sort_order)
- Client-generated UUIDs for new inserts make the path idempotent: if the
  server response is lost and we retry, the second POST hits PK-conflict
  which the queue treats as success
- CHECKED section div now carries role="listitem" so Playwright locators
  follow the item across sections

2b.2 Offline queue
- `$lib/sync/queue.ts` — SyncQueue class backed by IndexedDB (idb), FIFO flush,
  MAX_ATTEMPTS=5 retry budget, PK-conflict-as-success short-circuit
- `$lib/sync/index.ts` — app-wide singleton, window.online listener flushes
  automatically, hydrateSyncState() at page load restores pendingOpsCount
- `$lib/stores/syncStatus.ts` — derived store (offline | syncing | synced)
  tracking navigator.onLine + queue depth
- SyncBanner component renders the offline/syncing indicator in the detail
  and session views
- handleAdd enqueues on failure instead of reverting, so an offline mutation
  keeps its optimistic row and syncs on reconnect

2b.3 Modo Compra
- `/lists/[id]/session/+page.svelte` — full-screen overlay (fixed inset-0
  z-50) that covers the sidebar while keeping the normal auth routing.
  56×56 toggles, flip animation shuffling items between TO BUY / CHECKED,
  Finish Shopping confirmation modal → completeList → goto('/lists')
- Link from `/lists/[id]` header (data-testid=start-session) with the
  new `list_start_session` message

Testing infra
- apps/web gets its own Vitest config (jsdom + fake-indexeddb/auto) and a
  new `pnpm test:unit` script. `just test-all` now chains pgTAP → integration
  → unit → e2e so a single command is the gate.
- packages/test-utils/tests/sync-queue.test.ts (placeholder scaffold) removed —
  replaced by `apps/web/src/lib/sync/queue.test.ts` co-located with the module

Totals: 103 tests green
  16 pgTAP
  59 Vitest integration (in packages/test-utils)
   6 Vitest unit (in apps/web — the SyncQueue)
  22 Playwright E2E (5 auth + 4 lists + 6 items + 2 realtime + 2 offline + 3 session)
   2 skipped (realtime-presence — upstream bug, unchanged)

Documented in plan/fase-2b and CLAUDE.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 03:32:15 +02:00

5.7 KiB
Raw Permalink Blame History

Fase 2b — Sincronización en Tiempo Real y Modo Compra

Estado: Completa (presence tests siguen .skip por bug upstream de supabase/realtime; ver memoria project_realtime_config) Duración estimada: 23 semanas
Objetivo: el diferencial del producto. Es la fase más compleja técnicamente.

Esta fase sigue TDD: primero los tests, al final la verificación. Ninguna tarea de implementación se da por hecha hasta que todos los tests pasan (just test-all).

2b.0 Tests primero — escribir antes de implementar

Prep infra (previa a los tests):

  • Migración 007_realtime_publication.sql — añade shopping_items y shopping_lists a la publicación supabase_realtime; pone REPLICA IDENTITY FULL en ambas
  • Docker Compose: DB_USER: supabase_admin (superuser) + SELF_HOST_TENANT_NAME: realtime para Realtime (ver memoria project_realtime_config)

Vitest (packages/test-utils/tests/):

  • realtime-postgres-changes.test.ts — R-01 INSERT broadcast, R-02 UPDATE con fila completa, R-03 filtro list_id aísla eventos de otras listas
  • realtime-isolation.test.ts — R-I-01 David (guest) recibe eventos de su colectivo; R-I-02 Eva (no-miembro) NO recibe nada aunque se suscriba (RLS)
  • [~] realtime-presence.test.tsescritos pero describe.skip: presence_diff revienta el GenServer de Realtime v2.76.5/v2.83.0 (RealtimeChannel.handle_out/3 is undefined). Reactivar cuando se actualice upstream. Ver memoria project_realtime_config.
  • apps/web/src/lib/sync/queue.test.ts — Q-01..Q-05 + F-01 (seis tests Vitest) sobre la cola offline. Reemplazan las placeholders originales; usan fake-indexeddb y viven co-localizadas con el módulo

Playwright (apps/web/tests/e2e/):

  • session.test.ts — S-01 (ruta full-screen), S-02 (check mueve a CHECKED), S-03 (Finish → completed → redirect a /lists)
  • realtime.test.ts — R-E-01 (INSERT cruza sesiones), R-E-02 (UPDATE cruza sesiones)
  • offline.test.ts — O-01 (banner offline + optimistic survive), O-02 (reconectar → flush + persistencia server-side)

pgTAP (supabase/tests/):

  • 004_realtime_publication.sql — P-01..P-04: publication + REPLICA IDENTITY FULL asegurados

2b.1 Sincronización Realtime

  • Suscripción a Postgres Changes de Supabase Realtime sobre shopping_items filtrada por list_id
  • Suscripción a Presencebloqueada por bug upstream handle_out/3 en supabase/realtime v2.76.5/v2.83.0; reactivar al actualizar
  • Store Svelte realtimeSync:
    • Gestiona el ciclo de vida de las suscripciones (subscribe/unsubscribe al entrar/salir de la lista)
    • Aplica cambios remotos al estado local sin causar bucles
    • Detecta conflicto last-write-wins: si el updated_at remoto > local, aplica remoto y muestra aviso visual

2b.2 Offline-First

  • Instalación y configuración de idb (wrapper de IndexedDB)
  • Esquema local en IndexedDB: pending_ops, lists_cache, items_cache
  • Cola de operaciones pendientes: cada mutación se escribe en pending_ops antes de llamar a Supabase
  • Service Worker (@vite-pwa/sveltekit + Workbox) — pendiente para Fase 4 (hardening PWA):
    • NOTA TÉCNICA: SvelteKit intercepta src/service-worker.ts y bloquea imports externos (solo permite $service-worker y $env/static/public). Para usar Workbox con injectManifest hay que nombrar el fichero src/sw.ts (SvelteKit no lo reconoce como SW) y configurar strategies: 'injectManifest', filename: 'sw.ts' en vite.config.ts. Actualmente el plugin usa generateSW (SW auto-generado, precaching básico).
    • Cache-first para assets estáticos
    • Network-first con fallback a caché para rutas de la app
    • Background sync para flush de pending_ops al recuperar conexión
    • La cobertura offline actual vive en $lib/sync (IndexedDB + online-event flush) y es suficiente para el MVP — la fusión con un SW custom es una mejora de Fase 4.
  • Fallback manual de sync: al detectar online en el evento del navegador, flush de la cola (cubre Safari que no soporta Background Sync API)
  • Indicador de estado de sincronización en la UI: synced | syncing | offline

2b.3 Modo Compra

  • Ruta /lists/[id]/session — UI dedicada al Modo Compra:
    • Ítems en formato grande (botón 56×56, fila 72px) optimizado para pulgar
    • Reordenamiento al marcar: pendientes arriba, marcados abajo (animate:flip Svelte)
    • Indicador de presencia (bloqueado por el bug upstream anterior)
    • Banner offline reusa SyncBanner de 2b.2
    • Botón prominente "Finish shopping" con confirmación modal → lista completed → redirect a /lists
    • Notificación push al finalizar — diferido a Fase 4 (requiere permiso push + PWA instalada)
  • Rendimiento 60fps / tap < 100ms — validación manual pendiente (no hay CI-driver para esto)

2b.Z Verificación final — todos los tests verdes

  • just test-all → 0 failures (16 pgTAP + 59 Vitest integration + 6 Vitest unit + 22 Playwright = 103 verdes, 2 skipped por bug upstream de presence)
  • Prueba E2E de sincronización cruzando sesiones de navegador: realtime.test.ts (R-E-01, R-E-02)
  • Prueba E2E de offline + reconexión: offline.test.ts (O-01, O-02) — incluye verificación server-side con un contexto fresco
  • Validación manual en un iPhone real (iOS 16.4+) — diferida a Fase 4

Criterio de aceptación: dos personas en el mismo supermercado, con cobertura inestable, pueden marcar ítems desde sus móviles y verse los cambios mutuamente en tiempo real. Si se pierde la conexión, los cambios locales se sincronizan al recuperarla. Toda la suite pasa con just test-all.