10 KiB
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.onLinedetection - Documents served at
/:path*withindex.mddefault - 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 immediatelysyncPending(): Replay queue when online, handle acks/conflictsapplyBroadcast(path, patch): Apply real-time update from other userdetectConflict(base, local, server): Run diff3, return merged or nullsaveToIndexedDB(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
versioncolumn todocumentstable (integer, auto-increment per doc) - Add
document_versionstable 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
versionto documents table) POST /api/documents/:pathwith 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)