Skip to content

Client Setup

Set up Canvas Store with persistence, undo/redo, and multiplayer sync:

import { World } from '@woven-ecs/core';
import { CanvasStore, Synced } from '@woven-ecs/canvas-store';
import { Position, Velocity, Shape } from './components';
const store = new CanvasStore({
persistence: {
documentId: 'my-document',
},
history: true,
websocket: {
documentId: 'my-document',
url: 'wss://your-server.com',
clientId: crypto.randomUUID(),
startOffline: false,
token: 'auth-token',
onVersionMismatch: (serverVersion) => {
alert('Please refresh to get the latest version');
},
},
});
await store.initialize({
components: [Position, Velocity, Shape],
singletons: [DocumentSettings],
});
// Create world with all components including Synced
const world = new World([Position, Velocity, Shape, Synced]);
function loop() {
world.execute((ctx) => {
// Sync changes to persistence/network/history
store.sync(ctx);
// ...the rest of your loop
});
requestAnimationFrame(loop);
}
loop();
OptionTypeDescription
persistencePersistenceOptionsPersistence configuration. See below.
historyHistoryOptions | trueHistory/undo-redo configuration. Pass true for defaults. See below.
websocketWebsocketOptionsWebSocket configuration. See below.
initialStateRecord<string, ComponentData>Initial state snapshot to load on the first sync. Useful for seeding a document with pre-built content.
OptionTypeDefaultDescription
documentIdstringRequiredUnique identifier for the document. Used to namespace IndexedDB storage.

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

OptionTypeDefaultDescription
commitCheckpointAfterFramesnumber60Number of quiet frames (no mutations) before committing pending changes to the undo stack.
maxHistoryStackSizenumber100Maximum number of undo steps to keep in history. Older entries are discarded.
OptionTypeDefaultDescription
documentIdstringRequiredUnique identifier for the document on the server.
urlstringRequiredWebSocket server URL (e.g., wss://your-server.com).
clientIdstringRequiredUnique identifier for this client. This ID gets sent to other users when broadcasting changes.
startOfflinebooleanfalseStart in offline mode without connecting. Changes are queued until connect() is called.
tokenstringundefinedAuthentication token sent as a query parameter (?token=...) to the server.
onVersionMismatchfunctionundefinedCallback invoked when server reports a protocol version mismatch. Receives the server’s protocol version number.
onConnectivityChangefunctionundefinedCallback invoked when connection status changes. Receives a boolean (true when connected, false when disconnected).

You can seed a document with pre-built content by passing an initialState snapshot. This is applied once on the first sync() call, before any adapter pulls. It’s useful for templates, default documents, or restoring a saved snapshot.

const store = new CanvasStore({
initialState: {
'entity-1/Block': { _exists: true, position: [100, 200], size: [50, 50] },
'entity-1/Shape': { _exists: true, kind: 'square' },
'SINGLETON/Camera': { _exists: true, zoom: 1.5 },
},
history: true,
});
await store.initialize({
components: [Block, Shape],
singletons: [Camera],
});

The initial state uses the same key format as patches: "<stableId>/<componentName>" for components and "SINGLETON/<singletonName>" for singletons. Use store.getState() to generate the initial state value you want to pass into your app.

// Track connection status via callback
const store = new CanvasStore({
websocket: {
documentId: 'my-document',
url: 'wss://your-server.com',
clientId: crypto.randomUUID(),
onConnectivityChange: (isOnline) => {
console.log(isOnline ? 'Connected to server' : 'Disconnected from server');
},
},
});
// Manually disconnect/reconnect
store.disconnect();
await store.connect();
// Clean up when done
store.close();

When offline, changes are buffered locally:

  1. Changes are saved to IndexedDB (if usePersistence: true)
  2. On reconnect, buffered changes are sent to the server
  3. Server sends any changes that happened while offline

When the client detects a version mismatch with the server (e.g. due to a new deployment), you can handle it with the onVersionMismatch callback. The typical course of action is to prompt the user to refresh the page until the client side code and server are on the same version.

const store = new CanvasStore({
websocket: {
documentId: 'my-doc',
url: 'wss://server.com',
clientId: crypto.randomUUID(),
onVersionMismatch: (serverVersion) => {
// Prompt user to refresh
alert('Please refresh to get the latest version');
},
},
});

Basic undo/redo operations:

// Undo last change
if (store.canUndo()) {
store.undo();
}
// Redo last undone change
if (store.canRedo()) {
store.redo();
}

The canvas store automatically groups changes based on the time difference between mutations. You can customize this behavior by setting commitCheckpointAfterFrames. If you want more control over when checkpoints are created, you can create them manually:

// Create checkpoint before a complex operation
const checkpoint = store.createCheckpoint();
// Make multiple changes...
moveEntities(ctx, selectedEntities);
updateProperties(ctx, selectedEntities);
// Wait for changes to settle, then squash into one undo step
store.onSettled(() => {
store.squashToCheckpoint(checkpoint);
}, { frames: 2 });

onSettled waits for a period of inactivity (no local mutations) before invoking the callback. This is useful for grouping together a series of changes into a single undo step, even if the current changes trigger additional changes.

Discard all changes since a checkpoint:

const checkpoint = store.createCheckpoint();
try {
riskyOperation(ctx);
} catch (e) {
// Revert all changes since checkpoint
store.onSettled(() => {
store.revertToCheckpoint(checkpoint);
}, { frames: 2 });
}