Persistence
Save and load semantic editor state.
SpineEditor persists canonical JSON derived from the committed EditorState.
At runtime, the committed EditorState is the source of truth. Persistence stores a snapshot of that state so it can be loaded later. On load, the semantic state is reconstructed and the DOM runtime deterministically renders fresh DOM from it.
Use serializeEditorState(...) and deserializeEditorState(...) from @spine-editor/core.
Scopes
Persistence has two scopes:
type EditorStatePersistenceScope = "document" | "session";
Use document for the primary persisted record. Use session only when you need resumable editing context (selection, marks, plugin state).
document stores the durable semantic document:
{
"version": 1,
"document": {
"kind": "branch",
"type": "doc",
"attrs": {},
"children": []
}
}
This is the right scope for canonical content, server records, previews, and export pipelines. It excludes caret position, collapsed typing marks, plugin state, undo history, traces, browser selection, overlays, and DOM state.
session stores the same document plus editing context:
{
"version": 1,
"document": {},
"session": {
"selection": {},
"storedMarks": [],
"storedMarksExplicit": false,
"pluginState": {}
}
}
Use it for autosave or "continue where I left off" flows. It is not a stable cross-version interchange format. Session snapshots may depend on the same app version, plugin set, and product assumptions that created them.
Save
import { serializeEditorState } from "@spine-editor/core";
const documentJSON = serializeEditorState(engine.state, {
scope: "document",
});
const draftJSON = serializeEditorState(engine.state, {
scope: "session",
});
Load
import {
createEditorEngine,
createPlanner,
deserializeEditorState,
} from "@spine-editor/core";
import { createSpineDomRuntime } from "@spine-editor/dom";
const initialState = deserializeEditorState(savedJSON, {
scope: "document",
selectionPolicy: "end-if-default-unhinted-start",
});
const engine = createEditorEngine({
initialState,
planner: createPlanner(),
});
const runtime = createSpineDomRuntime({
engine,
rootElement,
});
runtime.mount();
With scope: "document", the runtime does not restore old HTML. It renders the saved document tree again. A saved image with semantic attrs such as src, widthPx, align, and indent is projected back into DOM from those attrs.
Use scope: "session" when loading a draft that includes saved selection and plugin state:
const initialState = deserializeEditorState(savedDraftJSON, {
scope: "session",
selectionPolicy: "preserve",
});
"preserve" keeps the saved selection. "end-if-default-unhinted-start" only moves the default unhinted 0 → 0 caret to the end of non-empty content.
Guarantees
Canonical JSON is stable and reproducible: the same supported semantic state serializes to equivalent JSON, and trusted JSON reconstructs an equivalent semantic state.
Equivalent means deterministic reconstruction within the supported domain, not identity of runtime instances.
The top-level version is the SpineEditor persistence-format version. Version 1 is the only supported format today; unsupported versions fail closed.
Deserialization rebuilds semantic nodes and cached document metrics, then applies the load-selection policy. It does not mount DOM, restore browser selection, run product migrations, replay history, restore traces, or run normalization passes through a committed transaction.
Persisted data should already satisfy the invariants of the format and the product schema that owns it. Validate and migrate data at the app boundary when it comes from outside trusted storage.
Plugin State
pluginState is opaque to core. Only store plugin-owned data that is JSON-safe, deterministic, and meaningful to restore.
Do not store runtime handles, DOM nodes, closures, host objects, or timestamps that affect behavior. Core preserves plugin JSON, but the plugin or host app must validate and migrate it.
Boundaries
- Store canonical semantic JSON from the editor state.
- Keep app metadata such as title, author, status, and permissions outside
EditorState. - Persistence captures the latest semantic state. Replay needs recorded transactions, history, or trace artifacts too.
- Keep storage adapters in the host app. Core converts state to JSON and back; the app decides where that JSON lives.