Files
collective-lists/plan/fase-2b-realtime-modo-compra.md
Oier Bravo Urtasun cb07f67a69 feat(realtime): full Fase 2b.0 test suite + infra hardening
Extend the 2b.0 red-phase coverage: presence, RLS isolation, offline-queue
contract, and Playwright scaffolds for the shopping-session UX. Only the
presence test is blocked on an upstream Realtime bug (documented below); all
other Realtime paths are proven end-to-end.

Tests
- realtime-postgres-changes.test.ts: now uses afterEach socket disconnect
  (Vitest singleFork kept Phoenix sockets alive between files, leaking state)
- realtime-isolation.test.ts (new): R-I-01 David (guest, same collective)
  receives events; R-I-02 Eva (non-member) receives NONE — RLS is enforced
  server-side per subscribed JWT
- realtime-presence.test.ts (new, describe.skip): two-client roster + leave
  assertions ready, gated on upstream bug
- sync-queue.test.ts (new, describe.skip): 7 placeholders pinning the
  apps/web/src/lib/sync/queue.ts contract (Q-01..Q-04 enqueue / in-order
  flush / retry; F-01..F-03 online-event flush + last-write-wins)
- apps/web/tests/e2e/{session,realtime,offline}.test.ts (new, describe.skip):
  S-01..S-03, R-E-01, R-E-02, O-01, O-02 pinning the Modo Compra UI contract
- vitest.config.ts: fileParallelism=false, drop singleFork so each file gets
  a fresh worker fork — prevents RealtimeClient singleton leakage

Infra
- db-init/00-role-passwords.sh: pre-create `realtime` schema with
  AUTHORIZATION supabase_admin. Realtime's per-tenant migrator creates tables
  INSIDE the schema but does not create the schema itself; without this the
  first tenant connect fails with "schema realtime does not exist"
- docker-compose.dev.yml: adopt the upstream supabase/supabase Realtime env
  shape — SEED_SELF_HOST=true + RUN_JANITOR=true + RLIMIT_NOFILE=10000 +
  drop the custom `command: eval seeds` (that bypasses the normal supervisor
  startup and was observed to cause GenServer crashes under load).
  Version pinned to v2.76.5 to match the official docker-compose.

Known upstream bug
- Realtime v2.76.5 and v2.83.0 both crash on presence_diff:
  `(UndefinedFunctionError) RealtimeChannel.handle_out/3 is undefined`.
  postgres_changes + isolation unaffected (they don't traverse handle_out).
  Presence tests stay `describe.skip` until a fixed upstream is released.

Totals: 59 Vitest passed (+ 9 skipped awaiting implementation/upstream),
16 pgTAP, 15 Playwright (+ 7 E2E scaffolded for 2b UI) = 90 green.

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

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

5.2 KiB
Raw Blame History

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

Estado: 🚧 En curso — tests 2b.0 verdes (Realtime infra lista) 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.
  • [~] sync-queue.test.ts / sync-flush.test.tsescritos como describe.skip con el contrato esperado (Q-01..Q-04, F-01..F-03). Reactivar al crear apps/web/src/lib/sync/queue.ts.

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

  • [~] session.test.ts — S-01..S-03 escritos como describe.skip (requieren la ruta /lists/[id]/session)
  • [~] realtime.test.ts — R-E-01, R-E-02 escritos como describe.skip (requieren suscripción Realtime en la UI)
  • [~] offline.test.ts — O-01, O-02 escritos como describe.skip (requieren el módulo de sync y el banner offline)

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 Presence de Supabase Realtime para indicador de miembros presentes en lista
  • 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):
    • 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
    • Fallback manual online event (Safari no soporta Background Sync API)
  • 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 (mínimo 64px de altura), optimizado para pulsar con el pulgar
    • Reordenamiento automático al marcar: pendientes arriba, marcados abajo (animación fluida con flip)
    • Indicador de presencia: avatares de miembros que tienen la lista abierta simultáneamente
    • Banner sin conexión cuando no hay red (los cambios se guardan en local)
    • Botón prominente "Finish shopping" — confirmación modal → lista pasa a completed
    • Notificación push (si el usuario ha dado permiso) a otros miembros al finalizar
  • Rendimiento: 60fps en gama media, tap < 100ms (validar en Chrome DevTools con throttling)

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

  • just test-all → 0 failures, incluyendo los nuevos de 2b.0
  • Prueba manual de sincronización: dos dispositivos, misma lista, cambios visibles en < 1 segundo
  • Prueba manual de offline: desconectar red, mutar, reconectar, verificar sync

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.