280 lines
10 KiB
Markdown
280 lines
10 KiB
Markdown
# 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: Preact + 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
|
|
```js
|
|
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
|
|
```go
|
|
// 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
|
|
```go
|
|
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
|
|
```go
|
|
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:
|
|
```sql
|
|
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)
|
|
- [x] IndexedDB cache for documents and content (done)
|
|
- [x] Offline indicator UI (done)
|
|
- [x] Service Worker for offline navigation
|
|
- [x] Cache static assets for PWA
|
|
- [x] 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)
|