@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.
| Entry | Contents |
|---|---|
@stesura/collab-adapter | Plugins, errors, presence, transport types |
@stesura/collab-adapter/protocol | Wire constants, body and presence types, reportCollabError: what clients and servers share, with no React or ProseMirror view in its graph |
@stesura/collab-adapter/styles.css | Remote 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).
| Name | Type | Default |
|---|---|---|
__0 | { version: number; plugins: Plugin<any>[]; presenceConfig?: PresencePluginConfig; onEditRefused?: (steps: number, limit: number) => void; } |
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.
| Name | Type | Default |
|---|---|---|
__0 | DocSizeGuardOptions |
Options for docSizeGuard.
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.
| Name | Type | Default |
|---|---|---|
config | PresencePluginConfig |
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).
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.
| Name | Type | Default |
|---|---|---|
userId | 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.
| Error | Raised when | Fields |
|---|---|---|
HttpStatusError | A collab request got a non-2xx response | status, retryAfterMs |
CollabDisconnectedError | A commit was not sent because syncing has stopped | docId |
SchemaMismatchError | The server, the stored document or a commit uses a schema this client can’t read | source, unknown, fingerprints, version, schemaName |
PresenceListenerTerminatedError | The 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.
| Name | Type | Default |
|---|---|---|
error | unknown |
UnknownSchemaTypes
Node and mark type names in a JSON payload that the schema does not define.
| Name | Type | Default |
|---|---|---|
nodes | string[] | |
marks | string[] |
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.
| Name | Type | Default |
|---|---|---|
error | unknown | |
context | CollabErrorContext |
CollabErrorContext
Where a failure happened. scope is a stable identifier ("server.receiveCommit",
"react.hydrate") to group and alert on without matching messages.
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.
| Name | Type | Default |
|---|---|---|
reporter | CollabErrorReporter | |
error | unknown | |
context | CollabErrorContext |
Transport
CollabTransportOptions
How collab requests authenticate: the headers / credentials props of the providers and factories.
| Name | Type | Default |
|---|---|---|
headers | CollabHeadersSent on every request, typically a bearer token. | |
credentials | RequestCredentials
|
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).
| Name | Type | Default |
|---|---|---|
id | string | |
name | string | |
color | string | |
avatar | string |
PresenceIndicator
One client’s selection as the server stores and fans it out. Positions refer to the confirmed
document at version.
RoomPresenceIndicator
One client’s room-level presence, as the server stores and fans it out.
| Type | Description |
|---|---|
PresenceCursorDisplay | How a remote caret is labelled: "avatar", "avatar-name" or "name". |
PresenceUpdate | What a client sends: a PresenceIndicator without movedAt, plus moved. |
RoomPresenceFocus | What a room member is on, as useRoomPresence().setFocus sets it. |
RoomPresenceUpdate | What a client sends to the room presence routes. |
PresencePluginState | The 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.
| Export | Value | Description |
|---|---|---|
COLLAB_PROTOCOL_VERSION | 2 | The 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_STATUS | 426 | The 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.
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.
| Name | Type | Default |
|---|---|---|
kind | "schema" | |
error | string | |
client | stringThe fingerprint the client sent. | |
server | string | |
schemaName | stringThe named schema the document uses; absent from servers before named schemas. |
Body of a PROTOCOL_MISMATCH_STATUS answer for an unserved protocol version.
| Name | Type | Default |
|---|---|---|
kind | "protocol"Absent from servers older than the schema handshake. | |
error | string | |
protocol | number | |
supported | { min: number; max: number; }Inclusive. |