Yjs Integration
Ganimede uses Yjs (Conflict-free Replicated Data Types) for real-time synchronization between the backend and all connected clients. This enables collaborative editing and consistent state across all instances.Why Yjs?
Yjs provides:- Automatic conflict resolution: Multiple users can edit simultaneously without conflicts
- Offline support: Changes can be made offline and synced later
- Efficient sync: Only deltas are transmitted over the network
- Strong eventual consistency: All clients converge to the same state
- Undo/redo: Built-in transaction history
Architecture Overview
YDoc Structure
The YDoc contains all notebook state using Yjs shared types:Shared Types
YArray
Ordered list with insert/delete operations:YMap
Key-value map with set/delete operations:YText
Collaborative text editing with character-level operations:Transactions
All YDoc mutations must happen within a transaction. This ensures atomicity and efficient sync. Backend (Python):WebSocket Providers
Frontend: y-websocket
Location:ui/src/stores/_notebook.js
Backend: ypy-websocket
Location:api/ganimede/main.py
Observation Patterns
Yjs provides observers to react to changes.Frontend (JavaScript)
Backend (Python)
Python Yjs doesn’t support observers in the same way. Instead, the backend updates YDoc and relies on WebSocket sync to propagate changes.Data Flow Examples
Example 1: User Edits Cell Source
Example 2: Backend Updates Cell State
Example 3: Cell Execution Output
Graph Structures
The notebook uses two graph structures for cell organization:Next-Prev Graph (np_graph)
Defines sequential execution flow:Parent-Children Graph (pc_graph)
Defines tissue grouping (heading cells with children):Initialization Flow
Backend (main.py:47)
Frontend (_notebook.js)
Undo/Redo
Frontend (_notebook.js:58):ycells and can undo/redo them.
Awareness Protocol
Awareness is a separate protocol for ephemeral state (cursors, selections).Performance Considerations
-
Batch updates: Use transactions to batch multiple changes
-
Observe efficiently: Use
observe()instead ofobserveDeep()when possible -
Avoid frequent toJSON(): Cache the JSON representation and only update on changes
-
Stop capturing for non-undoable operations:
Conflict Resolution
Yjs automatically resolves conflicts:- YText: Character-level CRDT (similar to OT)
- YArray: Position-based inserts with unique identifiers
- YMap: Last-write-wins for each key
Key Patterns
- YDoc as single source of truth: All state flows through YDoc
- Transactions for atomicity: Batch related changes
- Observers for reactivity: YDoc changes trigger UI updates
- Derived state: Compute reverse graphs (cp_graph from pc_graph)
- Separation of concerns: Yjs for state, Comms for commands
- Awareness for presence: Cursors and ephemeral state
Related
- Backend Architecture - How backend updates YDoc
- Frontend Architecture - How frontend observes YDoc