Skip to content

canvas-store

The main synchronization hub that routes mutations between adapters (ECS, History, WebSocket, Persistence).

Creates a canvas-store instance.

const store = new CanvasStore({
persistence: {
documentId: 'my-document',
},
history: {
commitCheckpointAfterFrames: 60,
maxHistoryStackSize: 100,
},
websocket: {
documentId: 'my-document',
url: 'wss://api.example.com',
clientId: crypto.randomUUID(),
},
});
OptionTypeDescription
persistencePersistenceOptionsPersistence adapter options
historyHistoryOptions | trueHistory/undo-redo adapter options. Pass true for defaults.
websocketWebsocketOptionsWebSocket adapter options
OptionTypeDescription
documentIdstringUnique identifier for the document

Pass true to enable with defaults, or an object to customize:

OptionTypeDescription
commitCheckpointAfterFramesnumberFrames of inactivity before committing (default: 60)
maxHistoryStackSizenumberMaximum undo steps to keep (default: 100)
OptionTypeDescription
documentIdstringUnique identifier for the document
urlstringWebSocket server URL
clientIdstringUnique client identifier
startOfflinebooleanStart disconnected
tokenstringAuthentication token
onVersionMismatchfunctionProtocol version mismatch handler
onConnectivityChangefunctionConnection status change handler
onSyncfunctionCalled once the server’s initial document has been delivered and applied.
MethodReturnsDescription
initialize(options)Promise<void>Initialize all adapters
sync(ctx)voidSynchronize mutations (call every frame)
undo()booleanUndo last change
redo()booleanRedo last undone change
canUndo()booleanCheck if undo is available
canRedo()booleanCheck if redo is available
createCheckpoint()string | nullCreate a history checkpoint
revertToCheckpoint(id)booleanRevert all changes since checkpoint
squashToCheckpoint(id)booleanCombine changes since checkpoint into one undo step
onSettled(callback, options)voidCalled after N frames with no mutations
connect()Promise<void>Connect/reconnect WebSocket
disconnect()voidDisconnect WebSocket
setToken(token)voidReplace the websocket auth token. Sends an auth-refresh frame when online; updates the URL token for the next connect when offline
isSyncedboolean(getter) true once the server’s initial document has been delivered and applied (immediately for a local-only store).
close()voidClose all adapters

Creates a component with sync behavior and stable naming for persistence.

const Shape = defineCanvasComponent(
{
name: 'shapes',
sync: 'document',
migrations: [
{ name: 'v1', upgrade: (data) => ({ ...data, color: data.color ?? '#000' }) }
],
excludeFromHistory: ['lastSelected'],
},
{
x: field.float64(),
y: field.float64(),
color: field.string().max(16).default('#0f3460'),
},
);
OptionTypeDescription
namestringStable identifier for storage/sync
syncSyncBehaviorHow changes propagate
migrationsComponentMigration[]Data migration chain
excludeFromHistorystring[]Fields to exclude from undo/redo
PropertyTypeDescription
namestringStable storage identifier
syncSyncBehaviorSync behavior setting
migrationsComponentMigration[]Migration chain
excludeFromHistorystring[]Fields excluded from history
currentVersionstring | nullLatest migration version

Creates a singleton with sync behavior and stable naming.

const Camera = defineCanvasSingleton(
{ name: 'camera', sync: 'local' },
{
x: field.float64().default(0),
y: field.float64().default(0),
zoom: field.float64().default(1),
},
);
OptionTypeDescription
namestringStable identifier for storage/sync
syncSingletonSyncBehaviorHow changes propagate (excludes 'ephemeral')
migrationsComponentMigration[]Data migration chain
excludeFromHistorystring[]Fields to exclude from undo/redo

type SyncBehavior =
| 'document' // Persisted to database, synced to all clients
| 'ephemeral' // Synced via WebSocket only (cursors, selections)
| 'local' // Persisted locally only (preferences, camera)
| 'none'; // Not synced or stored
type SingletonSyncBehavior = Exclude<SyncBehavior, 'ephemeral'>;

Built-in component that marks entities for persistence/sync.

import { Synced } from '@woven-ecs/canvas-store';
// Add to entities you want persisted
addComponent(ctx, entity, Synced, { id: crypto.randomUUID() });
FieldTypeDescription
idstringStable UUID for persistence

Real-time multiplayer synchronization adapter. When using CanvasStore, configure via the websocket option. The adapter can also be used directly:

OptionTypeDescription
urlstringWebSocket server URL
clientIdstringUnique client identifier
documentIdstringDocument identifier
usePersistencebooleanEnable offline buffer persistence
startOfflinebooleanStart disconnected
tokenstringAuthentication token
onVersionMismatchfunctionProtocol version mismatch handler
onConnectivityChangefunctionConnection status change handler
componentsAnyCanvasComponentDef[]Component definitions for migrations
singletonsAnyCanvasSingletonDef[]Singleton definitions for migrations

interface ComponentMigration {
name: string;
supersedes?: string;
upgrade: (data: Record<string, unknown>, from: string | null) => Record<string, unknown>;
}

migrateComponentData(data, currentVersion, migrations)

Section titled “migrateComponentData(data, currentVersion, migrations)”

Migrate component data through applicable migrations.

const result = migrateComponentData(
{ x: 0, y: 0 },
null,
[{ name: 'v1', upgrade: (data) => ({ ...data, z: 0 }) }]
);
// result: { data: { x: 0, y: 0, z: 0 }, version: 'v1', changed: true }

Returns: MigrationResult

interface MigrationResult {
data: Record<string, unknown>;
version: string | null;
changed: boolean;
}

Validate migrations array for correctness. Throws on duplicate names, missing supersede targets, or conflicts.

validateMigrations([
{ name: 'v1', upgrade: (data) => data },
{ name: 'v2', upgrade: (data) => data },
]);

A map of keys to values representing component changes.

type Patch = Record<string, ComponentData>;
// Key format:
// Components: "<entityId>/<componentName>"
// Singletons: "SINGLETON/<singletonName>"
// Examples:
{ "uuid-123/Position": { _exists: true, x: 0, y: 0 } } // Add
{ "uuid-123/Position": { x: 10 } } // Update
{ "uuid-123/Position": { _exists: false } } // Delete
type ComponentData = Record<string, unknown> & {
_exists?: boolean;
_version?: string;
};
interface VersionMismatchResponse {
type: 'version-mismatch';
serverProtocolVersion: number;
}