Skip to content

canvas-store-server

Runtime-agnostic connection helper. Parses the wire-protocol query parameters, runs your authorize, registers the session, wires onTokenRefresh to the same authorize, and returns a handle to forward WS events into.

const conn = await acceptConnection({
socket,
url: req.url ?? '',
request: req, // optional, passed through to authorize
manager,
authorize: async ({ roomId, clientId, token, request }) => ({
permissions: 'readwrite',
metadata: { /* anything */ },
}),
roomOptions: (roomId) => ({ /* RoomOptions for first connect to this room */ }),
})
// Forward events from your runtime:
ws.on('message', (data) => conn.onMessage(String(data)))
ws.on('close', conn.onClose)
ws.on('error', conn.onError)
OptionTypeDescription
socketWebSocketLikeThe connected socket.
urlstringConnect URL — full (wss://host/?…) or path-and-query (/?…).
requestunknownOptional runtime request object passed verbatim to authorize.
managerRoomManagerThe room manager.
authorizefunction(info) => { permissions, metadata? } | Promise<...>. Called on connect AND on every auth-refresh frame. Throw to reject.
roomOptionsfunction(roomId) => Omit<RoomOptions, 'onTokenRefresh'>. Applied when the room is first created; ignored on subsequent connects to an existing room.
FieldTypeDescription
roomIdstringParsed from the URL’s roomId query parameter.
clientIdstringParsed from the URL’s clientId query parameter.
tokenstringThe presented token — from the URL on connect, from the auth-refresh frame on refresh.
requestTRequest | undefinedThe original request object — undefined on token refresh.
FieldTypeDescription
sessionIdstringThe room session ID assigned after authorization.
roomRoomThe room this session is attached to.
getMetadata()TMeta | undefinedReturns the latest metadata for this session.
onMessage(data)voidForward an incoming WS message string.
onClose()voidCall when the WS closes.
onError()voidCall when the WS errors.

Standalone helper that pulls { roomId, clientId, token } out of a connect URL. Exposed so consumers building custom flows can keep the wire-protocol contract in one place. Throws ConnectRequestError with a code (missing-room-id / missing-client-id / missing-token / invalid-url) on malformed input.


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
onTokenRefreshfunction-Called when a client sends an auth-refresh frame mid-session. Signature: (room, { sessionId, clientId, token }) => { permissions, metadata? } | Promise<...>. Returns the new permissions and optionally replacement metadata; throw to drop the session. Connect-time auth is the caller’s responsibility — verify before calling handleSocketConnect. Owned by acceptConnection when you’re using that helper.
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
getSessionMetadata(sessionId)unknownGet the metadata attached to a session via handleSocketConnect or onTokenRefresh.
setSessionMetadata(sessionId, metadata)voidReplace the metadata for a session.
close()voidClose room and all connections
room.handleSocketConnect({
socket: websocket,
clientId: 'client-uuid',
permissions: 'readwrite', // or 'readonly'
metadata: { /* optional, per-session value the room will hold */ },
});

The caller is expected to have authenticated the connection before this point and resolved permissions accordingly. Most users should use acceptConnection instead — it handles URL parsing, authorization, and refresh wiring for you.


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 | AuthRefreshRequest;
interface PatchRequest {
type: 'patch';
messageId: string;
documentPatches?: Patch[];
ephemeralPatches?: Patch[];
}
interface ReconnectRequest {
type: 'reconnect';
lastTimestamp: number;
protocolVersion: number;
documentPatches?: Patch[];
ephemeralPatches?: Patch[];
}

Sent by a client to swap the auth token on a live connection. Routed to the room’s onAuthRefresh handler, which is expected to verify the new token and update permissions via setSessionPermissions. Throwing closes the session.

interface AuthRefreshRequest {
type: 'auth-refresh';
token: string;
}

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;
}