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>
5.2 KiB
5.2 KiB
Fase 2b — Sincronización en Tiempo Real y Modo Compra
Estado: 🚧 En curso — tests 2b.0 verdes (Realtime infra lista)
Duración estimada: 2–3 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ñadeshopping_itemsyshopping_listsa la publicaciónsupabase_realtime; poneREPLICA IDENTITY FULLen ambas - Docker Compose:
DB_USER: supabase_admin(superuser) +SELF_HOST_TENANT_NAME: realtimepara Realtime (ver memoriaproject_realtime_config)
Vitest (packages/test-utils/tests/):
realtime-postgres-changes.test.ts— R-01 INSERT broadcast, R-02 UPDATE con fila completa, R-03 filtrolist_idaí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.ts— escritos perodescribe.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 memoriaproject_realtime_config. - [~]
sync-queue.test.ts/sync-flush.test.ts— escritos comodescribe.skipcon el contrato esperado (Q-01..Q-04, F-01..F-03). Reactivar al crearapps/web/src/lib/sync/queue.ts.
Playwright (apps/web/tests/e2e/):
- [~]
session.test.ts— S-01..S-03 escritos comodescribe.skip(requieren la ruta/lists/[id]/session) - [~]
realtime.test.ts— R-E-01, R-E-02 escritos comodescribe.skip(requieren suscripción Realtime en la UI) - [~]
offline.test.ts— O-01, O-02 escritos comodescribe.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 Changesde Supabase Realtime sobreshopping_itemsfiltrada porlist_id - Suscripción a
Presencede 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_atremoto > 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_opsantes de llamar a Supabase - Service Worker (
@vite-pwa/sveltekit+ Workbox):- NOTA TÉCNICA: SvelteKit intercepta
src/service-worker.tsy bloquea imports externos (solo permite$service-workery$env/static/public). Para usar Workbox coninjectManifesthay que nombrar el ficherosrc/sw.ts(SvelteKit no lo reconoce como SW) y configurarstrategies: 'injectManifest',filename: 'sw.ts'envite.config.ts. Actualmente el plugin usagenerateSW(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_opsal recuperar conexión - Fallback manual
onlineevent (Safari no soporta Background Sync API)
- NOTA TÉCNICA: SvelteKit intercepta
- Fallback manual de sync: al detectar
onlineen 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óncuando 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.