Files
cairnquire/.project/sync-plan-browser.md

10 KiB

Plan B: Browser-First Sync (Progressive Web App)

AGENT INSTRUCTION — READ FIRST

When you complete a phase or are ready for a code review, you must notify the opencode session that created this plan. Use the opencode HTTP API to send a message to session ses_225742b62ffeOSelYyCe4IPwxL.

Message format:

Subject: Plan B Review Ready — Phase X Complete
Body:
- Phase completed: [e.g., Phase 1]
- Files touched: [list of files added/modified]
- Summary: [2-3 sentence description of what was implemented]
- Tests: [which tests were added or updated]
- Blockers: [any issues or decisions needed]

OpenCode API Specification:

POST https://api.opencode.ai/v1/sessions/ses_225742b62ffeOSelYyCe4IPwxL/messages
Content-Type: application/json
Authorization: Bearer <OPENCODE_API_KEY>

Request body:
{
  "role": "assistant",
  "content": "Subject: Plan B Review Ready — Phase X Complete\n\nBody:\n- Phase completed: ...\n- Files touched: ...\n- Summary: ...\n- Tests: ...\n- Blockers: ..."
}

Wait for a response before starting the next phase unless explicitly told otherwise.

Goal

Extend the existing web application so it works offline, supports concurrent editing, and syncs changes when reconnected. No native client needed — works on macOS, Windows, Linux, iOS, Android via browser.

Architecture

Current State (Already Built)

  • Server: Go HTTP + SQLite + file watcher + WebSocket hub
  • Browser: Go-served HTML + embedded scripts + IndexedDB cache + navigator.onLine detection
  • Documents served at /:path* with index.md default
  • Real-time updates pushed via WebSocket when files change on disk

What We Add

Browser (existing + new):
├── cache.js (exists)        # IndexedDB: docs, content, hashes
├── realtime.js (exists)     # WebSocket, offline indicator
├── sync.js (new)            # Sync engine: queue, conflict detection, state machine
├── editor.js (new)          # ProseMirror or textarea with operational transforms
└── sw.js (new)              # Service Worker for offline serving

Server (existing + new):
├── internal/sync/ (new)
│   ├── protocol.go          # Browser sync protocol (simpler than Plan A)
│   ├── queue.go             # Server-side edit queue per document
│   ├── conflict.go          # Three-way merge for text
│   └── broadcast.go         # WebSocket broadcast for live collaboration
├── internal/auth/ (new)     # Sessions + WebSocket auth
└── httpserver/ (existing)
    ├── handlers.go          # Add: POST /api/documents/:path, WebSocket auth
    └── websocket.go         # Extend: handle edit messages

Sync Protocol v1 (Browser)

Data Model

  • Document: {path, content, hash, version}
  • Client Edit: {path, baseVersion, newHash, patch} where patch = diff from base
  • Server Ack: {path, version, status: accepted|conflict}
  • Server Broadcast: {type: update, path, newContent, newVersion, author}

Endpoints

GET /api/documents          # List all docs (cached in IndexedDB)
GET /api/documents/:path    # Get document content (cached in IndexedDB)
POST /api/documents/:path   # Save document (requires auth)
  Body: {content, baseVersion, clientHash}
  Response: {version, status, conflict?}

WS /api/ws                  # WebSocket (exists, extend for auth + edits)
  Client → Server: {type: "edit", path, patch, baseVersion}
  Server → Client: {type: "ack", path, version}
  Server → Client: {type: "broadcast", path, patch, author}
  Client → Server: {type: "cursor", path, position}  // Phase 2

Protocol Flow (Online)

1. User opens /some-doc
2. Browser fetches content, caches in IndexedDB
3. User edits → client generates patch (diff)
4. Client sends WS: {type: "edit", path, patch, baseVersion}
5. Server validates: baseVersion == current version?
   - Yes: apply patch, increment version, broadcast to other clients
   - No: return conflict, client must merge
6. Other clients receive broadcast, update editor if viewing same doc

Protocol Flow (Offline)

1. User goes offline (detected by navigator.onLine + fetch failure)
2. Offline banner appears
3. User edits → changes queued in IndexedDB: `pending_edits` store
4. User navigates → served from IndexedDB cache
5. User comes back online
6. Client replays queue: for each pending edit, POST to server
7. Server responds with ack or conflict
8. Conflicts shown in UI for user resolution

Conflict Resolution (Text)

  • Three-way merge using diff3 algorithm
  • If auto-merge succeeds: apply silently, show subtle notification
  • If auto-merge fails: show side-by-side diff in UI
  • User chooses: keep local, keep server, or edit merged result
  • Markdown-aware: try to merge at paragraph/section boundaries

Browser Sync Engine

IndexedDB Schema

const DB_NAME = 'mdhub';
const DB_VERSION = 2;

const stores = {
  documents: 'path',        // {path, title, hash, version, syncedAt}
  content: 'path',          // {path, content, hash, fetchedAt}
  pending_edits: 'id',      // {id, path, patch, baseVersion, createdAt, retries}
  sync_state: 'key'         // {key: 'lastSync', value: timestamp}
};

sync.js Responsibilities

  • queueEdit(path, patch): Add to pending_edits, try to send immediately
  • syncPending(): Replay queue when online, handle acks/conflicts
  • applyBroadcast(path, patch): Apply real-time update from other user
  • detectConflict(base, local, server): Run diff3, return merged or null
  • saveToIndexedDB(path, content): Cache for offline reading

Service Worker (sw.js)

  • Intercept navigation requests (/:path*)
  • If offline: serve from IndexedDB cache
  • If online: network first, update cache in background
  • Cache static assets (JS, CSS, fonts) for app shell
  • Enables "Add to Home Screen" PWA behavior

Editor

  • Start with <textarea> + diff generation (simple, robust)
  • Phase 2: ProseMirror for structured editing, operational transforms
  • Auto-save: debounced 2s, queues edit immediately
  • Show sync status: unsaved → saving → saved → (offline: queued)

Server Changes

1. Auth Middleware

// Extend existing session system
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Check session cookie
        // Set user in context
        // For WebSocket: authenticate on connection open
    })
}

2. Document Write Endpoint

func handleDocumentSave(w http.ResponseWriter, r *http.Request) {
    path := chi.URLParam(r, "*")
    var req struct {
        Content     string `json:"content"`
        BaseVersion int    `json:"baseVersion"`
        ClientHash  string `json:"clientHash"`
    }
    
    // Verify baseVersion matches current
    // If mismatch: return 409 Conflict with server version
    // If match: save to disk, update version, broadcast WS
}

3. WebSocket Extension

type WSMessage struct {
    Type        string `json:"type"` // edit, ack, broadcast, cursor
    Path        string `json:"path"`
    Patch       string `json:"patch"`       // unified diff
    BaseVersion int    `json:"baseVersion"`
    Author      string `json:"author"`
}

func (h *Hub) handleEdit(msg WSMessage, client *Client) {
    // Validate auth
    // Apply to document
    // Broadcast to other clients viewing same path
    // Send ack to sender
}

4. Versioned Documents

  • Add version column to documents table (integer, auto-increment per doc)
  • Add document_versions table for history:
    CREATE TABLE document_versions (
        id INTEGER PRIMARY KEY,
        path TEXT NOT NULL,
        version INTEGER NOT NULL,
        content_hash TEXT NOT NULL,
        author TEXT,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(path, version)
    );
    
  • Current content always on disk; versions are content hashes referencing store

Implementation Phases

Phase 1: Offline Reading (1-2 weeks)

  • IndexedDB cache for documents and content (done)
  • Offline indicator UI (done)
  • Service Worker for offline navigation
  • Cache static assets for PWA
  • Show "cached" badge on offline-loaded pages

Phase 2: Edit + Queue (2 weeks)

  • Simple textarea editor at /:path/edit
  • Auto-save with debounce
  • Queue edits in IndexedDB when offline
  • Replay queue on reconnect with exponential backoff
  • Visual "sync pending" indicator

Phase 3: Conflict Resolution (1-2 weeks)

  • Server versioning (add version to documents table)
  • POST /api/documents/:path with version check
  • Three-way merge on client (diff3 library)
  • Conflict UI: side-by-side or inline diff
  • Auto-merge for non-overlapping changes

Phase 4: Real-Time Collaboration (2 weeks)

  • WebSocket edit messages
  • Operational transforms or patch-based sync
  • Presence awareness (who's editing, cursor positions)
  • Broadcast updates to all connected clients
  • Optimistic UI updates (show edit immediately, rollback on conflict)

Phase 5: PWA Polish (1 week)

  • Manifest.json for "Add to Home Screen"
  • App shell architecture (instant load)
  • Background sync API for reliable offline queue
  • Push notifications for mentions/comments (future)

Pros

  • Builds directly on existing architecture (no new binary)
  • Works everywhere with a browser (no platform-specific code)
  • Simpler auth (reuse session cookies)
  • Real-time collaboration possible
  • PWA can feel like native app

Cons

  • Requires internet for editing (unless Phase 2 offline queue works well)
  • Browser storage limits (~60MB+ with storage permission)
  • Can't use native editors (VS Code, etc.)
  • WebSocket reliability on mobile/bad networks
  • Conflict UX constrained by browser capabilities

Decision Needed

  • Do we need native editor integration (Plan A) or is browser enough?
  • Can we build Plan B first, then extract core sync for Plan A later?

Shared with Plan A

Both plans eventually need:

  • Authentication/authorization (API keys vs sessions)
  • Document versioning
  • Conflict resolution algorithms
  • Content-addressed storage (already exists)