Skip to content

canvas-store-server

Manages a single collaborative document session with real-time synchronization.

Creates a room instance.

const room = new Room({
storage: new FileStorage({ dir: './data', roomId: 'my-room' }),
onDataChange: (room) => console.log('Data changed'),
onSessionRemoved: (room, { sessionId, remaining }) => {
if (remaining === 0) console.log('Room empty');
},
saveThrottleMs: 10000,
});
OptionTypeDefaultDescription
initialSnapshotRoomSnapshot-Restore from a previous snapshot
storageStorage-Pluggable persistence backend
onDataChangefunction-Called when document state changes
onSessionRemovedfunction-Called when a session disconnects
saveThrottleMsnumber10000Minimum ms between persistence saves
MethodReturnsDescription
load()Promise<void>Load state from storage
handleSocketConnect(options)stringRegister a new client connection, returns sessionId
handleSocketMessage(sessionId, data)voidProcess incoming WebSocket message
handleSocketClose(sessionId)voidHandle client disconnection
handleSocketError(sessionId)voidHandle client error
getSnapshot()RoomSnapshotGet current document state
getSessionCount()numberGet number of connected sessions
getSessions()SessionInfo[]Get all session info
getSessionPermissions(sessionId)SessionPermissionGet session permissions
setSessionPermissions(sessionId, permissions)voidUpdate session permissions
close()voidClose room and all connections
room.handleSocketConnect({
socket: websocket,
clientId: 'client-uuid',
permissions: 'readwrite', // or 'readonly'
});

Manages multiple rooms with automatic lifecycle management.

Creates a room manager instance.

const manager = new RoomManager({
createStorage: (roomId) => new FileStorage({ dir: './data', roomId }),
idleTimeout: 30000,
});
OptionTypeDefaultDescription
createStorage(roomId: string) => Storage-Factory for creating storage per room
idleTimeoutnumber30000Auto-close empty rooms after this many ms
MethodReturnsDescription
getRoom(roomId)Promise<Room>Get or create a room, loading from storage
getExistingRoom(roomId)Room | undefinedGet room only if it exists
getRoomIds()string[]List all active room IDs
closeRoom(roomId)voidClose and remove a specific room
closeAll()voidShut down all rooms

interface Storage {
load(): Promise<RoomSnapshot | null>;
save(snapshot: RoomSnapshot): Promise<void>;
}

File-based persistence using JSON files.

import { FileStorage } from '@woven-ecs/canvas-store-server';
const storage = new FileStorage({
dir: './data',
roomId: 'my-room',
});
OptionTypeDescription
dirstringDirectory for storing snapshots
roomIdstringRoom identifier (becomes filename)

In-memory storage for testing.

import { MemoryStorage } from '@woven-ecs/canvas-store-server';
const storage = new MemoryStorage();

Current protocol version constant for client-server compatibility.

import { PROTOCOL_VERSION } from '@woven-ecs/canvas-store-server';

Messages sent from client to server.

type ClientMessage = PatchRequest | ReconnectRequest;
interface PatchRequest {
type: 'patch';
messageId: string;
documentPatches?: Patch[];
ephemeralPatches?: Patch[];
}
interface ReconnectRequest {
type: 'reconnect';
lastTimestamp: number;
protocolVersion: number;
documentPatches?: Patch[];
ephemeralPatches?: Patch[];
}

Messages sent from server to client.

type ServerMessage =
| AckResponse
| PatchBroadcast
| ClientCountBroadcast
| VersionMismatchResponse;
interface AckResponse {
type: 'ack';
messageId: string;
timestamp: number;
}
interface PatchBroadcast {
type: 'patch';
documentPatches?: Patch[];
ephemeralPatches?: Patch[];
clientId: string;
timestamp: number;
}
interface ClientCountBroadcast {
type: 'clientCount';
count: number;
}
interface VersionMismatchResponse {
type: 'version-mismatch';
serverProtocolVersion: number;
}

Complete room state for persistence/restore.

interface RoomSnapshot {
timestamp: number;
state: Record<string, ComponentData>;
timestamps: Record<string, FieldTimestamps>;
}
interface SessionInfo {
sessionId: string;
clientId: string;
permissions: SessionPermission;
}
type SessionPermission = 'readonly' | 'readwrite';
type ComponentData = Record<string, unknown> & {
_exists?: boolean;
_version?: string;
};

Per-field timestamps for conflict resolution.

type FieldTimestamps = Record<string, number>;
type Patch = Record<string, ComponentData>;

Interface for WebSocket compatibility (works with any WebSocket implementation).

interface WebSocketLike {
send(data: string): void;
close(): void;
}