Document Store
Woven Canvas provides built-in persistence to IndexedDB and optional WebSocket synchronization.
Local Persistence
Section titled “Local Persistence”Enable local persistence with a document ID:
<script setup lang="ts">import { WovenCanvas } from "@woven-canvas/vue";
const storeOptions = { persistence: { documentId: "my-canvas", },};</script>
<template> <WovenCanvas :store="storeOptions" /></template>Changes are automatically saved to IndexedDB. When the page reloads, the canvas restores to its previous state.
Multiple Documents
Section titled “Multiple Documents”Use different document IDs for separate canvases:
<template> <WovenCanvas :store="{ persistence: { documentId: 'project-a' } }" /> <WovenCanvas :store="{ persistence: { documentId: 'project-b' } }" /></template>Undo/Redo
Section titled “Undo/Redo”History is automatically tracked. Use keyboard shortcuts or commands:
- Undo:
Ctrl+Z(orCmd+Zon Mac) - Redo:
Ctrl+YorCtrl+Shift+Z
Programmatically:
import { Undo, Redo } from "@woven-canvas/core";
nextEditorTick((ctx) => { Undo.spawn(ctx); // Undo last action Redo.spawn(ctx); // Redo last undone action});Sync Behaviors
Section titled “Sync Behaviors”Each component can specify how it should be synchronized:
| Behavior | Persisted | Synced | Use Case |
|---|---|---|---|
persist | Yes | Yes | Document content |
ephemeral | No | Yes | Cursor position, selection |
local | Yes | No | User preferences |
none | No | No | Temporary UI state |
Define sync behavior when creating components:
import { defineCanvasComponent, field } from "@woven-canvas/vue";
// Synced and persisted (default)const DocumentData = defineCanvasComponent( "doc-data", { title: field.string(), }, { sync: "persist" },);
// Synced but not persistedconst CursorState = defineCanvasComponent( "cursor", { x: field.float32(), y: field.float32(), }, { sync: "ephemeral" },);
// Local onlyconst UserPrefs = defineCanvasComponent( "prefs", { theme: field.string().default("dark"), }, { sync: "local" },);
// Runtime onlyconst HoverState = defineCanvasComponent( "hover", { entityId: field.uint32(), }, { sync: "none" },);Server Sync
Section titled “Server Sync”Connect to a WebSocket server for backup and collaboration:
<script setup lang="ts">import { WovenCanvas } from "@woven-canvas/vue";
const storeOptions = { persistence: { documentId: "shared-canvas", }, websocket: { url: "wss://your-server.com/sync", documentId: "shared-canvas", },};</script>
<template> <WovenCanvas :store="storeOptions" /></template>User Identity
Section titled “User Identity”Pass user data to identify participants:
<script setup lang="ts">const user = { userId: "user-123", // Stable user ID name: "Alice", // Display name color: "#3b82f6", // Cursor/selection color avatar: "https://...", // Avatar URL};</script>
<template> <WovenCanvas :editor="{ user }" :store="storeOptions" /></template>If not provided, random values are generated.
Presence Awareness
Section titled “Presence Awareness”Users automatically see:
- Cursors — Other users’ cursor positions in real-time
- Selections — What other users have selected (shown with their color)
- Avatars — Connected users displayed in the corner
Customizing Presence UI
Section titled “Customizing Presence UI”Replace the user presence display:
<template> <WovenCanvas> <template #user-presence="{ users }"> <div class="my-avatars"> <div v-for="user in users" :key="user.sessionId"> <img :src="user.avatar" :alt="user.name" /> <span>{{ user.name }}</span> </div> </div> </template> </WovenCanvas></template>Replace cursor rendering:
<template> <WovenCanvas> <template #user-cursors="{ users, currentSessionId, camera }"> <div v-for="user in users" :key="user.sessionId" v-show="user.sessionId !== currentSessionId" :style="{ position: 'absolute', left: `${(user.cursorX - camera.left) * camera.zoom}px`, top: `${(user.cursorY - camera.top) * camera.zoom}px`, }" > <CursorIcon :color="user.color" /> <span>{{ user.name }}</span> </div> </template> </WovenCanvas></template>Selection Highlighting
Section titled “Selection Highlighting”When another user selects a block, it’s highlighted with their color. This uses the held property in block data:
<template #block:my-block="{ entityId, block, held }"> <div :style="{ outline: held?.sessionId ? `2px solid ${getUserColor(held.sessionId)}` : 'none', }" > <!-- block content --> </div></template>Conflict Resolution
Section titled “Conflict Resolution”Woven Canvas uses last-write-wins conflict resolution. When two users edit the same property simultaneously:
- Both changes are applied locally immediately
- Changes are sent to the server
- The server broadcasts the final state
- All clients converge to the same state
For most canvas operations (moving blocks, changing colors), this works intuitively.
Offline Support
Section titled “Offline Support”The canvas works offline by default:
- Changes are saved locally to IndexedDB
- When offline, users can continue editing
- When reconnected, changes sync automatically
- Conflicts are resolved using last-write-wins
Monitor connection status:
<template> <WovenCanvas :store="storeOptions"> <template #offline-indicator="{ isOnline }"> <div v-if="!isOnline" class="offline-banner">Working offline</div> </template> </WovenCanvas></template>WebSocket Options
Section titled “WebSocket Options”const storeOptions = { websocket: { url: "wss://your-server.com/sync", documentId: "my-document", onConnectivityChange: (online: boolean) => { console.log("Connection:", online ? "online" : "offline"); }, onVersionMismatch: (serverVersion: number) => { console.log("Protocol version mismatch"); }, },};