WebSocket endpoint:
ws://localhost:8080/ws
Transport model:
- Client sends JSON messages with a
typediscriminator. - Ephemeral events are relayed directly in-memory.
- Durable structural events are committed through
canvas-servicefirst, then broadcast with committed metadata. crdt_opandsync_requestare special-cased for replay/sync.
Initial client message:
{
"type": "join",
"canvasId": "canvas-1",
"userId": "user-123"
}Server behavior:
- Adds session to room (
canvasSessions). - Tracks reverse indices (
sessionToCanvas,sessionToUserId). - Sends
presence_snapshotto the newly joined session. - Sends
canvas_bootstrapwith durable snapshot state fromcanvas-service. - Broadcasts
user_jointo peers in the same canvas.
Presence identity model:
userIdis currently tab/session-scoped (one browser tab = one participant).- Presence events represent active sessions, not deduplicated people.
Primary outbound events (from frontend/src/shared/events.ts):
cursor_move(userId,x,y)node_create(node)node_move(nodeId,x,y)node_drag_start(nodeIds[])node_drag_endnode_update(nodeId,patch)node_delete(nodeId)edge_create(edge)edge_delete(edgeId)node_select(userId,nodeId|null)crdt_op(docId,op)sync_request(docId,stateVector)draw_points(points,thickness)draw_end
presence_snapshot(users[], each item:{ id, color })canvas_bootstrap(canvasId,headVersion,nodes[],edges[])user_join(user, shape:{ id, color })- mirrored relay events for cursor/draw updates with sender
userId - committed structural events include:
versioneventIdclientOperationIduserId
- mirrored relay events include drag lifecycle (
node_drag_start,node_drag_end) user_leave(userId, tab/session participant id)sync_response(docId,ops[])
Example:
{
"type": "crdt_op",
"canvasId": "canvas-1",
"userId": "user-123",
"docId": "node-42",
"op": {
"actor": "user-123",
"seq": 14
}
}Server path:
- Build doc key:
{canvasId}::{docId}. - Build op key:
{actor}:{seq}. - Dedup via
docSeenOpKeys. - Append op deep-copy to
docOpLog. - Relay raw payload to peers.
Request example:
{
"type": "sync_request",
"canvasId": "canvas-1",
"docId": "node-42",
"stateVector": {
"user-123": 14,
"user-456": 9
}
}Response example:
{
"type": "sync_response",
"docId": "node-42",
"ops": [
{ "actor": "user-123", "seq": 15 }
]
}On socket close:
- Session removed from room and reverse indices.
user_leavebroadcast to remaining peers.- Empty canvas room registry entry removed.