Skip to Content
API Referencecollab-adapter-local

@stesura/collab-adapter-local

An offline collab backend with no server: the authority runs in the browser against IndexedDB, and tabs of one browser co-edit in real time. Mount it with useLocalCollabEditor from @stesura/collab-adapter-react; behaviour and caveats are in Local (offline, multi-tab).

Backend

createLocalCollabBackend

useLocalCollabEditor uses a shared backend per schema by default. Create your own to set dbName, snapshotInterval, migrations or createInitialDoc, and keep it referentially stable:

import { createLocalCollabBackend } from "@stesura/collab-adapter-local"; const backend = createLocalCollabBackend({ schema, dbName: "my-app", schemaVersion: 2, migrate });

The fully offline collab backend: the pitter-patter engine running entirely in the browser, with IndexedDB in place of Postgres, BroadcastChannel in place of Redis, and createLocalAuthority in place of @pitter-patter/collab-server. Multiple tabs on the same dbName co-edit with the same guarantees server collab gives separate clients.

Parameters:
NameTypeDefault
__0LocalCollabBackendConfig
Returns:
LocalCollabBackend

LocalCollabBackendConfig

Options for createLocalCollabBackend.

NameTypeDefault
schemaSchema<any, any>

ProseMirror schema used to rebase/apply steps authority-side.

dbNamestring

IndexedDB database name. One database holds any number of docs.

snapshotIntervalnumber

Write a full snapshot (and prune old commits) every N commits.

historyEverynumber

Write a version-history snapshot every N versions (0: never; named versions and restores still work). Version 0 is always kept, captured on a doc’s first commit. Default 64, as the server’s snapshots.every.

createInitialDoc(schema: Schema<any, any>, docId: string) => NodeJSON

Initial document for unknown docIds (default: empty topNode).

schemaVersionnumber

App schema stamp; bump together with a migrate callback. See migrate.ts.

migrateMigrateFn
listenTimeoutMsnumber

Long-poll timeout of listenForCommit (wake-up is via broadcast; this is only the convergence backstop).

onErrorCollabErrorReporter

Telemetry sink for failures the backend handles internally — a terminated IndexedDB connection, a blocked upgrade. Without one they are console-only.

LocalCollabBackend

What createLocalCollabBackend returns: the in-browser counterpart of the server backend.

NameTypeDefault
receiveCommit(docId: string, commit: CommitJSON, options?: { seed?: string; } | undefined) => Promise<void>

Order, rebase, and persist a client commit (the local sendCommit).

The rejection types are a real API contract, not an implementation detail: useLocalCollabEditor branches on the error name to decide between reloading, surfacing “deleted”, and giving up.

listenForCommit(docId: string, version: number, options?: { signal?: AbortSignal; } | undefined) => Promise<LocalCommitBatch>

Commits newer than version, with the doc row’s lineage nonce; resolves on new commits or after timeout.

getInitialDoc(docId: string) => Promise<DocSnapshot & { seed: string; }>

Initial hydration read. Seeds unknown docIds; runs schema migration. seed identifies the row’s lineage — pass it back on receiveCommit and to LocalListener so a delete + re-seed can’t go unnoticed.

exportDoc(docId: string) => Promise<{ docJSON: NodeJSON; version: number; }>

The current head doc — the designed seam for “take this doc online”. Never seeds.

hasDoc(docId: string) => Promise<boolean>

Whether the doc row exists. Never seeds.

headVersion(docId: string) => Promise<number | undefined>

The stored head version, or undefined when the row is missing. Never seeds.

deleteDoc(docId: string) => Promise<void>

Delete the doc row and its commits, then broadcast the deletion so other tabs’ listeners fail with DocDeletedError. Deleting an unknown docId is a no-op, not an error.

listDocs() => Promise<DocListEntry[]>

Every doc row in the database, for a document picker. Never seeds.

listSnapshots(docId: string, opts?: { limit?: number; before?: number; } | undefined) => Promise<SnapshotMeta[]>

History of docId, newest first, without documents; before pages by version.

getSnapshot(docId: string, version: number) => Promise<Snapshot | null>

One history version with its document, or null.

saveNamedSnapshot(docId: string, label: string, authorId?: string | undefined) => Promise<{ version: number; }>

Names the doc’s current version (renames it when already named).

restoreSnapshot(docId: string, version: number, authorId?: string | undefined) => Promise<{ version: number; }>

Makes a history version the current content, as a commit every open tab receives. The head it replaces is snapshotted with it, so the restore can be undone from history. Resolves the new head; unchanged when the content already matched.

onDocDeleted(docId: string, callback: () => void) => () => void

Calls callback when docId is deleted, whether by this tab or another. The seam for state that lives beside the document — comment threads — to go with it: this backend does not know about them. The channel object that posts a deletion never receives it, so this backend’s own deletions are dispatched to its subscribers directly.

dbNamestring

The IndexedDB database name; namespaces whatever is stored beside the docs.

requestPersistence() => Promise<boolean>

Ask the browser not to evict this origin’s storage under pressure.

close() => void

MigrateFn

Upgrades a stored document from schema version from to to (schemaVersion), run when a doc is read under a newer schemaVersion than it was stored with. The doc’s commits are discarded.

Parameters:
NameTypeDefault
docJSONNodeJSON
fromnumber
tonumber
Returns:
NodeJSON

Other exports

ExportDescription
DEFAULT_DB_NAME"stesura-collab-local", the database used when dbName is omitted.
DocListEntryOne row of listDocs(): { docId, version, updatedAt }.
DocSnapshot{ docJSON, version, lastUpdatedTimestamp }, the same shape as the server’s.
DocRow, SeededDocRowA stored doc row: a snapshot at snapshotVersion, the head version and schemaVersion; seeded rows always carry their lineage nonce.
LocalListenerThe CommitsListener the hook polls with; it surfaces DocDeletedError and StaleClientError.
LocalCommitBatchWhat listenForCommit resolves to: commits plus the row’s lineage nonce.

Errors

Each sets name, so match by name rather than instanceof across bundles. useLocalCollabEditor handles the ones it can recover from.

ErrorMeaning
SchemaMigrationRequiredErrorStored schemaVersion differs and no migrate applies
DocMigratedErrorAnother tab migrated the doc under this one; reload (the hook does)
DocDeletedErrorThe doc row is gone (hook: docStatus: "deleted")
StaleClientErrorThe client is behind the retained commits; reload (the hook does)
LocalStoreCorruptErrorThe retained commits can’t rebuild the head; terminal
LocalStoreConfigErrorThe schema can’t build an initial document; configuration error
LocalBackendClosedErrorUsed after close()
LocalContentionErrorLost the IndexedDB version race past the retry budget; treat as a bug
SnapshotNotFoundErrorA history version has no snapshot: never written, or deleted with its doc
Last updated on