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 Syncedconst 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();Configuration Options
Section titled “Configuration Options”CanvasStore Options
Section titled “CanvasStore Options”| Option | Type | Description |
|---|---|---|
persistence | PersistenceOptions | Persistence configuration. See below. |
history | HistoryOptions | true | History/undo-redo configuration. Pass true for defaults. See below. |
websocket | WebsocketOptions | WebSocket configuration. See below. |
initialState | Record<string, ComponentData> | Initial state snapshot to load on the first sync. Useful for seeding a document with pre-built content. |
Persistence Options
Section titled “Persistence Options”| Option | Type | Default | Description |
|---|---|---|---|
documentId | string | Required | Unique identifier for the document. Used to namespace IndexedDB storage. |
History Options
Section titled “History Options”Pass true to enable with defaults, or an object to customize:
| Option | Type | Default | Description |
|---|---|---|---|
commitCheckpointAfterFrames | number | 60 | Number of quiet frames (no mutations) before committing pending changes to the undo stack. |
maxHistoryStackSize | number | 100 | Maximum number of undo steps to keep in history. Older entries are discarded. |
WebSocket Options
Section titled “WebSocket Options”| Option | Type | Default | Description |
|---|---|---|---|
documentId | string | Required | Unique identifier for the document on the server. |
url | string | Required | WebSocket server URL (e.g., wss://your-server.com). |
clientId | string | Required | Unique identifier for this client. This ID gets sent to other users when broadcasting changes. |
startOffline | boolean | false | Start in offline mode without connecting. Changes are queued until connect() is called. |
token | string | undefined | Authentication token sent as a query parameter (?token=...) to the server. |
onVersionMismatch | function | undefined | Callback invoked when server reports a protocol version mismatch. Receives the server’s protocol version number. |
onConnectivityChange | function | undefined | Callback invoked when connection status changes. Receives a boolean (true when connected, false when disconnected). |
Initial State
Section titled “Initial State”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.
Connection Management
Section titled “Connection Management”// Track connection status via callbackconst 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/reconnectstore.disconnect();await store.connect();
// Clean up when donestore.close();Offline Support
Section titled “Offline Support”When offline, changes are buffered locally:
- Changes are saved to IndexedDB (if
usePersistence: true) - On reconnect, buffered changes are sent to the server
- Server sends any changes that happened while offline
Version Mismatch Handling
Section titled “Version Mismatch Handling”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'); }, },});History
Section titled “History”Basic undo/redo operations:
// Undo last changeif (store.canUndo()) { store.undo();}
// Redo last undone changeif (store.canRedo()) { store.redo();}Checkpoints
Section titled “Checkpoints”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 operationconst checkpoint = store.createCheckpoint();
// Make multiple changes...moveEntities(ctx, selectedEntities);updateProperties(ctx, selectedEntities);
// Wait for changes to settle, then squash into one undo stepstore.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.
Reverting Changes
Section titled “Reverting 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 });}