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

76 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### 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):**
- [x] 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
- [x] Docker Compose: `DB_USER: supabase_admin` (superuser) + `SELF_HOST_TENANT_NAME: realtime` para Realtime (ver memoria `project_realtime_config`)
**Vitest (`packages/test-utils/tests/`):**
- [x] `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 ✅
- [x] `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.ts`**escritos 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`.
- [x] `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/`):**
- [x] `session.test.ts` — S-01 (ruta full-screen), S-02 (check mueve a CHECKED), S-03 (Finish → completed → redirect a /lists) ✅
- [x] `realtime.test.ts` — R-E-01 (INSERT cruza sesiones), R-E-02 (UPDATE cruza sesiones) ✅
- [x] `offline.test.ts` — O-01 (banner offline + optimistic survive), O-02 (reconectar → flush + persistencia server-side) ✅
**pgTAP (`supabase/tests/`):**
- [x] `004_realtime_publication.sql` — P-01..P-04: publication + REPLICA IDENTITY FULL asegurados ✅
#### 2b.1 Sincronización Realtime
- [x] Suscripción a `Postgres Changes` de Supabase Realtime sobre `shopping_items` filtrada por `list_id`
- [ ] Suscripción a `Presence`**bloqueada por bug upstream `handle_out/3` en `supabase/realtime` v2.76.5/v2.83.0**; reactivar al actualizar
- [x] 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
- [x] Instalación y configuración de `idb` (wrapper de IndexedDB)
- [x] Esquema local en IndexedDB: `pending_ops`, `lists_cache`, `items_cache`
- [x] 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.
- [x] Fallback manual de sync: al detectar `online` en el evento del navegador, flush de la cola (cubre Safari que no soporta Background Sync API)
- [x] Indicador de estado de sincronización en la UI: `synced | syncing | offline`
#### 2b.3 Modo Compra
- [x] Ruta `/lists/[id]/session` — UI dedicada al Modo Compra:
- [x] Ítems en formato grande (botón 56×56, fila 72px) optimizado para pulgar
- [x] Reordenamiento al marcar: pendientes arriba, marcados abajo (`animate:flip` Svelte)
- [ ] Indicador de presencia (bloqueado por el bug upstream anterior)
- [x] Banner offline reusa `SyncBanner` de 2b.2
- [x] 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
- [x] `just test-all` → 0 failures (16 pgTAP + 59 Vitest integration + 6 Vitest unit + 22 Playwright = **103 verdes**, 2 skipped por bug upstream de presence)
- [x] Prueba E2E de sincronización cruzando sesiones de navegador: `realtime.test.ts` (R-E-01, R-E-02)
- [x] 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`.
---