Saving and storage
Autosave in one line, store documents wherever you like, keep images out of the document, and know what is saved when.
Autosave in one line#
createCanvasFlow(document.getElementById('board')!, { persistenceKey: 'board-42' })
With a persistenceKey, the web host saves the document to IndexedDB (falling back to localStorage) a moment after each change, restores it on load, and writes a last copy when the page is closing. It also remembers the person's own view of the board, the camera and current page, separately from the document.
Your own storage#
Storage is a three-method interface. Anything with get and set works: localStorage, IndexedDB, AsyncStorage, a REST API, a file.
interface StorageAdapter {
get(key: string): Promise<string | null> | string | null
set(key: string, value: string): Promise<void> | void
remove?(key: string): Promise<void> | void
setSync?(key: string, value: string): void // a best-effort write while the page is closing
}
Pass one to the host:
import type { StorageAdapter } from 'canvasflow'
import { createCanvasFlow } from 'canvasflow/dom'
const apiStorage: StorageAdapter = {
async get(key) {
const res = await fetch(`/api/boards/${key}`)
return res.ok ? res.text() : null
},
async set(key, value) {
await fetch(`/api/boards/${key}`, { method: 'PUT', body: value })
},
}
createCanvasFlow(document.getElementById('board')!, { persistenceKey: 'board-42', storage: apiStorage })
The built-in adapters are indexedDBAdapter() and localStorageAdapter() from canvasflow/dom, and memoryStorage() from canvasflow for tests and previews.
The persistence plugin#
On other hosts, or when you want more control, use the plugin directly:
import { createEditor, memoryStorage, persistencePlugin } from 'canvasflow'
const persistence = persistencePlugin({
key: 'board-42',
storage: memoryStorage(),
delay: 400, // ms after the last change
onStatus: (state) => console.log(state), // 'loading' | 'saving' | 'saved' | 'error'…
onLoaded: (found) => console.log(found ? 'restored' : 'new board'),
})
const editor = createEditor({ plugins: [persistence] })
await persistence.controller.flush() // save now, for example before navigating away
| Option | What it does |
|---|---|
key |
The document's storage key |
storage |
Where the document goes |
sessionStorage |
Where the person's view (camera, page) goes. Defaults to storage |
sessionKeys |
Which parts of the instance state to remember per document |
delay |
Debounce in milliseconds |
onStatus, onLoaded |
Callbacks for a "Saved" indicator and first-load decisions |
Images and other files#
By default, images are embedded in the document as data URLs. That's perfect for local-only boards and terrible for shared ones: documents grow fast, and every collaborator downloads every image on every load.
Give the editor an asset store, and images are uploaded once and referenced by URL:
import type { AssetStore } from 'canvasflow'
export const s3Assets: AssetStore = {
async upload(file, info) {
const { url } = await fetch('/api/upload-url', { method: 'POST', body: JSON.stringify(info) }).then((r) => r.json())
await fetch(url, { method: 'PUT', body: file })
return { src: url.split('?')[0] }
},
// Optional: turn a stored src into something displayable (signed URLs, a CDN).
resolve: (src) => src.replace('s3.amazonaws.com/my-bucket', 'cdn.example.com'),
}
createCanvasFlow(document.getElementById('board')!, { assets: s3Assets })
info carries the file's name, mimeType, w and h. Dropping, pasting and the image tool all go through your store.
Snapshots by hand#
Sometimes you just want the data:
const snapshot = editor.getSnapshot()
await fetch('/api/boards/42', { method: 'PUT', body: JSON.stringify(snapshot) })
editor.loadSnapshot(await fetch('/api/boards/42').then((r) => r.json()))
Snapshots carry a schema version and are migrated forward on load. See Records and the store.
Saving and collaboration together#
When a board is shared, let the collaboration layer be the source of truth and give it an offline cache instead of autosaving the document separately. Edits made offline are kept, with their merge information, and merge correctly when the connection returns.