Skip to Content
API Referencecollab-adapter

@stesura/collab-adapter

The framework-free client side of Pitter Patter collaboration: the collab and presence plugins, the errors the sync loops raise, and the wire protocol shared with @stesura/collab-adapter-server. React apps mount it through @stesura/collab-adapter-react, which composes these plugins for you.

EntryContents
@stesura/collab-adapterPlugins, errors, presence, transport types
@stesura/collab-adapter/protocolWire constants, body and presence types, reportCollabError: what clients and servers share, with no React or ProseMirror view in its graph
@stesura/collab-adapter/styles.cssRemote cursor styles; import once

Plugins

composeCollabPlugins

Returns [collab, presence, ...plugins]. The presence plugin renders remote carets and selections itself; import @stesura/collab-adapter/styles.css for their styles.

Plain prosemirror-history works alongside: collab-commit tags remote transactions like prosemirror-collab does, so undo reverts only local edits (covered by collab-adapter-local/src/__tests__/history-with-collab-commit.spec.ts).

Parameters:
NameTypeDefault
__0{ version: number; plugins: Plugin<any>[]; presenceConfig?: PresencePluginConfig; onEditRefused?: (steps: number, limit: number) => void; }
Returns:
Plugin<any>[]

docSizeGuard

Add it to the root editor’s extraPlugins to refuse a paste that would push the document past the server’s cap, instead of losing it to a 413:

import { docSizeGuard } from "@stesura/collab-adapter"; useCollabEditor({ extraPlugins: ({ parentEditorId }) => parentEditorId ? [] : [docSizeGuard({ maxBytes: 60 * 1024 * 1024, onReject: () => toast("That paste is too large") })], });

Refuses local transactions that would grow the document past maxBytes. UX only: the server enforces its own cap whatever the client does; this turns a refused commit into a refused paste, before it reaches anyone else.

Remote commits and hydration (tagged by the collab plugin) always pass: the server already accepted them, and refusing one here would desync this client.

Parameters:
NameTypeDefault
__0DocSizeGuardOptions
Returns:
Plugin<any>

Options for docSizeGuard.

NameTypeDefault
maxBytesnumber

Largest document accepted, in bytes of compacted JSON: the measure the server’s maxStoredDocBytes / COLLAB_MAX_DOC_BYTES applies. Set it somewhat below the server cap so the edit is refused here, with a message, rather than by a 413 that discards it.

maxStepBytesnumber

Largest single transaction accepted, in bytes of its steps’ JSON: what one commit body carries, so the measure the server’s per-request maxContentBytes applies. Optional.

onReject(info: { bytes: number; maxBytes: number; }) => void

Called when a transaction is refused, e.g. to toast.

presence

The remote cursor plugin composeCollabPlugins includes.

Renders remote selections and carets from presence indicators, one per user: of a user’s sessions (tabs, devices), only the most recently moved one renders.

An indicator is placed once, when its snapshot arrives, and its decorations are then mapped through each transaction. Recomputing positions on every transaction instead made cursors jump while typing, since step maps are easy to mis-tag around acks and rebases.

Indicator positions refer to the confirmed document at the indicator’s version, so the plugin keeps a ledger of confirmed-layer step maps, one entry per version bump:

  • remote receive (doc changed): the tr is invert(unconfirmed) + commit + rebased(unconfirmed), so the commit’s maps are the middle slice;
  • own-commit ack (doc unchanged): the maps of the unconfirmed steps the ack consumed.

Placement maps through the ledger from the indicator’s version to ours, then through the local unconfirmed steps. An indicator ahead of our version keeps its previous placement (if any) and waits in pending until its commit lands; one older than the ledger is not rendered. The ledger is pruned below the oldest live indicator, minus PRUNE_SLACK.

Parameters:
NameTypeDefault
configPresencePluginConfig
Returns:
Plugin<PresencePluginState>

Configuration for presence.

Accessibility: carets are aria-hidden (they sit inside the prose), so the host must provide an off-document presence surface, such as CollabPresenceRoster from @stesura/collab-adapter-react (a labelled list plus a polite live region, fed by useDocPresence).

NameTypeDefault
getPresenceColor(userId: string) => string

Maps a userId to a cursor color. Defaults to getPresenceColor . Any CSS color works.

Security: the value is interpolated into a raw CSS declaration, so never pass user-controlled text through. Values outside hex, a bare keyword, or a single rgb()/hsl()/oklch()-style call drop the selection highlight (the caret still renders).

getDisplayName(userId: string) => string

Maps a userId to the name shown on the cursor. Defaults to the userId.

getAvatar(userId: string) => string | undefined

Maps a userId to an avatar image URL. Without one, the avatar shows the name’s initials.

cursorDisplayPresenceCursorDisplay

How carets are labelled. Defaults to "avatar" (name on hover).

getCursorLabel(userId: string, displayName?: string | undefined) => string

Builds the caret’s hover label. Defaults to English (“Ada is editing here”); localize here. displayName is the resolved getDisplayName result.

currentUserIdstring

The local user’s id; indicators with it are never rendered. The server only excludes the current clientId, so a reload, HMR remount or other tab would otherwise show a ghost caret until its TTL expires.

getCurrentUserId() => string | undefined

Live variant of PresencePluginConfig.currentUserId, read at every placement. Use it when identity resolves after mount. Takes precedence.

getPresenceColor

Default getPresenceColor: a PRESENCE_COLORS entry picked by userColorSlot, so a user’s color is stable across sessions and peers and matches their tracked changes. Collisions are expected with ten colors.

Parameters:
NameTypeDefault
userIdstring
Returns:
string

PRESENCE_COLORS is the palette it picks from: the ten Tableau 10 colors, in the order core’s track-change styles use, so a user’s caret and tracked edits match.

Collab primitives

Re-exported from prosemirror-collab-commit so there is one import surface: collab, receiveCommitTransaction, getVersion and Commit, with the CommitJSON and NodeJSON types. CollabClient is @pitter-patter/collab-client’s client with commits cut at transaction boundaries; CollabClientConfig and CommitsListener come from the same package. You only need these to build a transport of your own.

receiveCommitTransaction takes ReceiveCommitOptions: upstream’s, plus onDisagreement, called with an AckDisagreement when the ack of the client’s own commit kept other steps than the client did. unmergedEditsOf returns the client’s document with its unconfirmed edits as UnmergedEdits (null when there are none), for hosts that offer the user their version before those edits are dropped.

Errors

The sync loops report these through onError, and the React hooks turn them into docStatus. Each sets name: match on it (or use isHttpStatusError) rather than instanceof, which fails across duplicated bundles.

ErrorRaised whenFields
HttpStatusErrorA collab request got a non-2xx responsestatus, retryAfterMs
CollabDisconnectedErrorA commit was not sent because syncing has stoppeddocId
SchemaMismatchErrorThe server, the stored document or a commit uses a schema this client can’t readsource, unknown, fingerprints, version, schemaName
PresenceListenerTerminatedErrorThe presence poll hit a status retrying can’t fix (e.g. expired auth)

isHttpStatusError

Checks name + shape rather than instanceof, so it works across duplicated bundles. retryAfterMs is not required: an older bundle’s error lacks it.

Parameters:
NameTypeDefault
errorunknown
Returns:
boolean

UnknownSchemaTypes

Node and mark type names in a JSON payload that the schema does not define.

NameTypeDefault
nodesstring[]
marksstring[]

Telemetry

Every client hook, provider and factory, and the server backend, takes an onError of this shape. See Telemetry.

CollabErrorReporter

Host telemetry sink (e.g. Sentry) for failures the collab layer handles internally: retried polls, swallowed background writes, dropped optional features. Without one they only reach the console.

Parameters:
NameTypeDefault
errorunknown
contextCollabErrorContext
Returns:
void

CollabErrorContext

Where a failure happened. scope is a stable identifier ("server.receiveCommit", "react.hydrate") to group and alert on without matching messages.

NameTypeDefault
scopestring
docIdstring
level"error" | "warn"

"warn": degraded but recovered (a retried poll, a dropped optional feature). "error" (default): something was lost or stopped.

detailRecord<string, unknown>

Extra structured fields. Never carries document content.

reportCollabError

Logs a handled failure to the console, then to reporter if given. A throwing reporter is caught and logged, so it cannot break the recovery path that called it.

Parameters:
NameTypeDefault
reporterCollabErrorReporter
errorunknown
contextCollabErrorContext
Returns:
void

Transport

CollabTransportOptions

How collab requests authenticate: the headers / credentials props of the providers and factories.

NameTypeDefault
headersCollabHeaders

Sent on every request, typically a bearer token.

credentialsRequestCredentials

"include" for cookie auth against a collab server on another origin.

CollabHeaders is the type of headers: a HeadersInit, or a function (sync or async) called per request and never cached, so a rotated token is picked up. CollabFetch is the fetch signature the clients take.

Presence types

CollabUser

The local user’s collab identity (see useGuestUser in collab-adapter-react).

NameTypeDefault
idstring
namestring
colorstring
avatarstring

PresenceIndicator

One client’s selection as the server stores and fans it out. Positions refer to the confirmed document at version.

NameTypeDefault
refstring

Fresh on every send; polls compare refs to detect changes.

clientIdstring
userIdstring
anchornumber
versionnumber
movedAtnumber

Server time (ms) of this session’s last local move or edit, 0 if none. Picks which of a user’s sessions renders; a server clock so devices with skewed clocks still compare.

RoomPresenceIndicator

One client’s room-level presence, as the server stores and fans it out.

NameTypeDefault
refstring

Changes when the content changes; a heartbeat resends the same one.

clientIdstring
userIdstring
infoJsonObject

Display data (name, avatar, colour). Client-supplied: trust userId, not this.

focusRoomPresenceFocus | null
updatedAtnumber

Server time (ms) of the last save.

TypeDescription
PresenceCursorDisplayHow a remote caret is labelled: "avatar", "avatar-name" or "name".
PresenceUpdateWhat a client sends: a PresenceIndicator without movedAt, plus moved.
RoomPresenceFocusWhat a room member is on, as useRoomPresence().setFocus sets it.
RoomPresenceUpdateWhat a client sends to the room presence routes.
PresencePluginStateThe presence plugin’s state.

Protocol (/protocol)

What clients and the server agree on. Both import these from /protocol, which loads no React; the main entry doesn’t re-export them.

ExportValueDescription
COLLAB_PROTOCOL_VERSION2The protocol this client speaks, and the newest the server serves.
COLLAB_PROTOCOL_HEADER"x-collab-protocol"Request header carrying the client’s protocol version.
COLLAB_SCHEMA_HEADER"x-collab-schema"Request header carrying the client’s schema fingerprint.
DOC_SCHEMA_HEADER"x-doc-schema"Hydrate response header naming the schema the document uses.
PROTOCOL_MISMATCH_STATUS426The server doesn’t serve the client’s protocol or schema; the older side must upgrade.

Version history checkpoints travel as SnapshotMeta (the list entries) and Snapshot (one with its docJSON). isSnapshotMeta and isSnapshot validate a response body before it’s trusted.

One version-history checkpoint without its document: what the server’s GET /docs/:docId/history lists and the local backend’s listSnapshots returns.

NameTypeDefault
versionnumber

The doc version the snapshot captures. Unique per doc.

labelstring | null

User-given name; null for an automatic checkpoint.

authorIdstring | null

Who made the version: the namer, the restorer, or the committer for automatic ones.

restoredFromnumber | null

Set when this version is a restore: the version it restored.

createdAtnumber

Epoch milliseconds.

The 426 bodies are ProtocolMismatchBody or SchemaMismatchBody (together CollabMismatchBody); SchemaCheckBody is what the client posts to the schema-check route. Json and JsonObject type the JSON the protocol carries.

Body of a PROTOCOL_MISMATCH_STATUS answer for a schema this server does not serve.

NameTypeDefault
kind"schema"
errorstring
clientstring

The fingerprint the client sent.

serverstring
schemaNamestring

The named schema the document uses; absent from servers before named schemas.

Body of a PROTOCOL_MISMATCH_STATUS answer for an unserved protocol version.

NameTypeDefault
kind"protocol"

Absent from servers older than the schema handshake.

errorstring
protocolnumber
supported{ min: number; max: number; }

Inclusive.

Last updated on