Commit graph

5 commits

Author SHA1 Message Date
Till JS
fed38efb8b fix(sync): fix SSE live updates — 2 bugs found during E2E testing
Bug 1: NotifyUser() early-returned when no WebSocket clients existed,
skipping SSE subscriber notifications entirely. Fixed by restructuring
to check WS clients and SSE subscribers independently.

Bug 2: SSE stream cursor defaulted to client's `since` parameter when
no initial data existed. If `since` was in the future (or very recent),
live updates had created_at < cursor and were silently filtered out.
Fixed by defaulting cursor to now() when no initial data is returned.

Bug 3: NotifyUser used original sseSubs slice instead of sseSubsCopy
after releasing the read lock (race condition).

Verified E2E: Push from client A → SSE stream on client B receives
live change event with correct data within ~1 second.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:39:46 +02:00
Till JS
068a64b275 feat(sync): add SSE streaming endpoint for real-time sync
New endpoint GET /sync/{appId}/stream sends Server-Sent Events with
change data directly, replacing the WebSocket notification + HTTP pull
round-trip pattern.

Server (Go):
- HandleStream() in handler.go: SSE endpoint with initial sync + live streaming
- Hub.Subscribe()/Unsubscribe() in hub.go: channel-based SSE subscriber system
- Notification type for type-safe SSE events
- convertChanges() helper extracted from duplicated code
- WriteTimeout set to 0 for SSE long-lived connections

Protocol: Client connects to /sync/{appId}/stream?collections=a,b&since=...
Server sends initial changes, then streams live changes as other clients sync.
Heartbeat every 30s keeps connection alive. Push still uses POST /sync/{appId}.

WebSocket remains available as fallback (not removed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:24:10 +02:00
Till JS
f7f5c9eb3a feat(sync): add pull pagination with hasMore flag
Server now returns hasMore: true when there are more than 1000 changes
pending for a collection. Client continues pulling in a loop until
hasMore is false, using the last row's timestamp as cursor.

Prevents data loss after long offline periods where >1000 changes
accumulated for a single collection.

Server changes (Go):
- GetChangesSince() accepts limit parameter
- HandlePull() fetches limit+1, trims, sets hasMore
- SyncedUntil uses last row's timestamp when paginating

Client changes (TypeScript):
- Pull loop: while (hasMore) { fetch → apply → advance cursor }
- Cursor only persisted after all pages fetched

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:17:20 +02:00
Till JS
4ff3ceb01a harden(mana-sync): fix WebSocket auth, add validation, tests, and docs
Critical security and correctness fixes for the sync server:

Security:
- Fix WebSocket JWT validation — was completely broken (hardcoded
  "pending-auth"). Now validates JWT via JWKS, rejects invalid tokens,
  enforces 10-second auth deadline, sends auth-ok confirmation.
- Add 10 MB request body size limit (prevents OOM attacks)
- Validate op field (must be insert/update/delete)
- Validate table and id fields (must be non-empty)
- Abort sync on RecordChange failure (was silently continuing)

Correctness:
- Fix silent JSON unmarshal errors in store (now returns error)
- Copy client set before iterating in NotifyUser (prevents race)
- Add write timeout on WebSocket notifications

Testing (19 tests, 0 -> 100% for unit-testable code):
- auth: token extraction, validator init, missing auth handling
- config: defaults, env override, invalid port
- sync: op validation, changeset validation, response format,
  field change round-trip, body size constant

Documentation:
- Add CLAUDE.md with architecture, sync protocol, LWW explanation,
  API endpoints, configuration, security notes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 02:41:56 +01:00
Till JS
2e4bb9bad7 feat(local-first): add local-first architecture with Dexie.js, Go sync server, and Todo pilot
Implement the foundational local-first data layer for ManaCore apps:

- New @manacore/local-store package (Dexie.js IndexedDB, sync engine, Svelte 5 reactive queries)
- New mana-sync Go service (sync protocol, WebSocket push, field-level LWW conflict resolution)
- Todo app migrated as pilot: stores read/write IndexedDB, guest mode with onboarding seed data
- PillNavigation: prominent login pill for unauthenticated users
- SyncIndicator component showing local/syncing/offline status
- GuestWelcomeModal on first visit for Todo app
- Removed demo-mode auth_required checks from Todo components (all writes are now local)
- CSP fix for local development (localhost:3001, localhost:3050)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:17:58 +01:00