Real-time collaboration
ProseKit integrates with two CRDT libraries for real-time collaboration: Yjs and Loro. Each ships as its own extension that binds a CRDT document to the editor. Network transport, persistence, and presence are handled by the underlying CRDT, while ProseKit only wires it into ProseMirror.
See the live editors:
Install peer dependencies
Section titled “Install peer dependencies”npm install yjs y-prosemirrorFor WebSocket sync, also install a provider:
npm install y-websocketWire the editor
Section titled “Wire the editor”import 'prosekit/extensions/yjs/style.css'
import { defineBasicExtension function defineBasicExtension(): BasicExtensionDefine a basic extension that includes some common functionality. You can
copy this function and customize it to your needs.
It's a combination of the following extension functions:
-
{@link
defineDoc
}
-
{@link
defineText
}
-
{@link
defineParagraph
}
-
{@link
defineHeading
}
-
{@link
defineList
}
-
{@link
defineBlockquote
}
-
{@link
defineImage
}
-
{@link
defineHorizontalRule
}
-
{@link
defineHardBreak
}
-
{@link
defineTable
}
-
{@link
defineCodeBlock
}
-
{@link
defineItalic
}
-
{@link
defineBold
}
-
{@link
defineUnderline
}
-
{@link
defineStrike
}
-
{@link
defineCode
}
-
{@link
defineLink
}
-
{@link
defineBaseKeymap
}
-
{@link
defineBaseCommands
}
-
{@link
defineHistory
}
-
{@link
defineGapCursor
}
-
{@link
defineVirtualSelection
}
-
{@link
defineModClickPrevention
}@public } from 'prosekit/basic'
import { createEditor function createEditor<E extends Extension>(options: EditorOptions<E>): Editor<E>@public , union function union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)Merges multiple extensions into one. You can pass multiple extensions as
arguments or a single array containing multiple extensions.@throwsIf no extensions are provided.@example```ts
function defineFancyNodes() {
return union(
defineFancyParagraph(),
defineFancyHeading(),
)
}
```@example```ts
function defineFancyNodes() {
return union([
defineFancyParagraph(),
defineFancyHeading(),
])
}
```@public } from 'prosekit/core'
import { defineYjs function defineYjs(options: YjsOptions): YjsExtension@public } from 'prosekit/extensions/yjs'
import { WebsocketProvider class WebsocketProviderWebsocket Provider for Yjs. Creates a websocket connection to sync the shared document.
The document name is attached to the provided url. I.e. the following example
creates a websocket connection to http://localhost:1234/my-document-name@example import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
const doc = new Y.Doc()
const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc)@extendsObservableV2<{ 'connection-close': (event: CloseEvent | null, provider: WebsocketProvider) => any, 'status': (event: { status: 'connected' | 'disconnected' | 'connecting' }) => any, 'connection-error': (event: Event, provider: WebsocketProvider) => any, 'sync': (state: boolean) => any }> } from 'y-websocket'
import * as Y import Y from 'yjs'
const ydoc const ydoc: Y.Doc = new Y import Y .Doc new Doc({ guid, collectionid, gc, gcFilter, meta, autoLoad, shouldLoad }?: DocOpts): Y.Doc
export Doc
@paramopts configuration ()
const provider const provider: WebsocketProvider = new WebsocketProvider new WebsocketProvider(serverUrl: string, roomname: string, doc: Y.Doc, { connect, awareness, params, protocols, WebSocketPolyfill, resyncInterval, maxBackoffTime, disableBc }?: {
connect?: boolean | undefined;
awareness?: Awareness | undefined;
params?: {
[x: string]: string;
} | undefined;
protocols?: string[] | undefined;
WebSocketPolyfill?: {
new (url: string | URL, protocols?: string | string[] | undefined): WebSocket;
prototype: WebSocket;
readonly CLOSED: number;
readonly CLOSING: number;
readonly CONNECTING: number;
readonly OPEN: number;
} | undefined;
resyncInterval?: number | undefined;
maxBackoffTime?: number | undefined;
disableBc?: boolean | undefined;
}): WebsocketProvider
@paramserverUrl@paramroomname@paramdoc@paramopts@paramopts.connect@paramopts.awareness@paramopts.params specify url parameters@paramopts.protocols specify websocket protocols@paramopts.WebSocketPolyfill Optionall provide a WebSocket polyfill@paramopts.resyncInterval Request server state every `resyncInterval` milliseconds@paramopts.maxBackoffTime Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff)@paramopts.disableBc Disable cross-tab BroadcastChannel communication (
'wss://your-server.example/sync',
'my-room',
ydoc const ydoc: Y.Doc ,
)
const editor const editor: Editor<Union<readonly [BasicExtension, YjsExtension]>> = createEditor createEditor<Union<readonly [BasicExtension, YjsExtension]>>(options: EditorOptions<Union<readonly [BasicExtension, YjsExtension]>>): Editor<Union<readonly [BasicExtension, YjsExtension]>>@public ({
extension EditorOptions<Union<readonly [BasicExtension, YjsExtension]>>.extension: Union<readonly [BasicExtension, YjsExtension]>The extension to use when creating the editor. : union union<readonly [BasicExtension, YjsExtension]>(exts_0: BasicExtension, exts_1: YjsExtension): Union<readonly [BasicExtension, YjsExtension]> (+1 overload)Merges multiple extensions into one. You can pass multiple extensions as
arguments or a single array containing multiple extensions.@throwsIf no extensions are provided.@example```ts
function defineFancyNodes() {
return union(
defineFancyParagraph(),
defineFancyHeading(),
)
}
```@example```ts
function defineFancyNodes() {
return union([
defineFancyParagraph(),
defineFancyHeading(),
])
}
```@public (
defineBasicExtension function defineBasicExtension(): BasicExtensionDefine a basic extension that includes some common functionality. You can
copy this function and customize it to your needs.
It's a combination of the following extension functions:
-
{@link
defineDoc
}
-
{@link
defineText
}
-
{@link
defineParagraph
}
-
{@link
defineHeading
}
-
{@link
defineList
}
-
{@link
defineBlockquote
}
-
{@link
defineImage
}
-
{@link
defineHorizontalRule
}
-
{@link
defineHardBreak
}
-
{@link
defineTable
}
-
{@link
defineCodeBlock
}
-
{@link
defineItalic
}
-
{@link
defineBold
}
-
{@link
defineUnderline
}
-
{@link
defineStrike
}
-
{@link
defineCode
}
-
{@link
defineLink
}
-
{@link
defineBaseKeymap
}
-
{@link
defineBaseCommands
}
-
{@link
defineHistory
}
-
{@link
defineGapCursor
}
-
{@link
defineVirtualSelection
}
-
{@link
defineModClickPrevention
}@public (),
defineYjs function defineYjs(options: YjsOptions): YjsExtension@public ({ doc YjsOptions.doc: Y.DocThe Yjs instance handles the state of shared data. : ydoc const ydoc: Y.Doc , awareness YjsOptions.awareness: AwarenessThe Awareness instance. : provider const provider: WebsocketProvider .awareness WebsocketProvider.awareness: Awareness }),
),
})User awareness
Section titled “User awareness”provider.awareness.setLocalStateField('user', {
name: 'Alice',
color: '#2563eb',
})The Yjs extension's CSS handles the visual rendering of remote cursors and selections.
Persistence
Section titled “Persistence”For local persistence, pair the provider with y-indexeddb:
npm install y-indexeddbimport { IndexeddbPersistence } from 'y-indexeddb'
const persistence = new IndexeddbPersistence('my-room', ydoc)Yjs handles offline edits automatically: changes queue locally and sync once the WebSocket reconnects.
Install peer dependencies
Section titled “Install peer dependencies”npm install loro-crdt loro-prosemirrorWire the editor
Section titled “Wire the editor”defineLoro accepts the Loro document plus exactly one of awareness or presence (cursor / presence info). Pick whichever your app already uses to track collaborators.
import 'prosekit/extensions/loro/style.css'
import { LoroDoc class LoroDoc<T extends Record<string, Container> = Record<string, Container>>
interface LoroDoc<T extends Record<string, Container> = Record<string, Container>>
The CRDTs document. Loro supports different CRDTs include [**List**](LoroList),
[**RichText**](LoroText), [**Map**](LoroMap) and [**Movable Tree**](LoroTree),
you could build all kind of applications by these.
**Important:** Loro is a pure library and does not handle network protocols.
It is the responsibility of the user to manage the storage, loading, and synchronization
of the bytes exported by Loro in a manner suitable for their specific environment.@example```ts
import { LoroDoc } from "loro-crdt"
const loro = new LoroDoc();
const text = loro.getText("text");
const list = loro.getList("list");
const map = loro.getMap("Map");
const tree = loro.getTree("tree");
``` } from 'loro-crdt'
import { CursorAwareness class CursorAwareness , type LoroDocType type LoroDocType = LoroDoc<{
doc: LoroMap<LoroNodeContainerType>;
data: LoroMap;
}>
} from 'loro-prosemirror'
import { defineBasicExtension function defineBasicExtension(): BasicExtensionDefine a basic extension that includes some common functionality. You can
copy this function and customize it to your needs.
It's a combination of the following extension functions:
-
{@link
defineDoc
}
-
{@link
defineText
}
-
{@link
defineParagraph
}
-
{@link
defineHeading
}
-
{@link
defineList
}
-
{@link
defineBlockquote
}
-
{@link
defineImage
}
-
{@link
defineHorizontalRule
}
-
{@link
defineHardBreak
}
-
{@link
defineTable
}
-
{@link
defineCodeBlock
}
-
{@link
defineItalic
}
-
{@link
defineBold
}
-
{@link
defineUnderline
}
-
{@link
defineStrike
}
-
{@link
defineCode
}
-
{@link
defineLink
}
-
{@link
defineBaseKeymap
}
-
{@link
defineBaseCommands
}
-
{@link
defineHistory
}
-
{@link
defineGapCursor
}
-
{@link
defineVirtualSelection
}
-
{@link
defineModClickPrevention
}@public } from 'prosekit/basic'
import { createEditor function createEditor<E extends Extension>(options: EditorOptions<E>): Editor<E>@public , union function union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)Merges multiple extensions into one. You can pass multiple extensions as
arguments or a single array containing multiple extensions.@throwsIf no extensions are provided.@example```ts
function defineFancyNodes() {
return union(
defineFancyParagraph(),
defineFancyHeading(),
)
}
```@example```ts
function defineFancyNodes() {
return union([
defineFancyParagraph(),
defineFancyHeading(),
])
}
```@public } from 'prosekit/core'
import { defineLoro function defineLoro(options: LoroOptions): LoroExtension@public } from 'prosekit/extensions/loro'
const doc const doc: LoroDocType : LoroDocType type LoroDocType = LoroDoc<{
doc: LoroMap<LoroNodeContainerType>;
data: LoroMap;
}>
= new LoroDoc new LoroDoc<{
doc: LoroMap<LoroNodeContainerType>;
data: LoroMap;
}>(): LoroDoc<{
doc: LoroMap<LoroNodeContainerType>;
data: LoroMap;
}>
Create a new loro document.
New document will have a random peer id. ()
const awareness const awareness: CursorAwareness = new CursorAwareness new CursorAwareness(peer: PeerID, timeout?: number): CursorAwareness (doc const doc: LoroDocType .peerIdStr LoroDoc<{ doc: LoroMap<LoroNodeContainerType>; data: LoroMap; }>.peerIdStr: `${number}`Get peer id in decimal string. )
const editor const editor: Editor<Union<readonly [BasicExtension, LoroExtension]>> = createEditor createEditor<Union<readonly [BasicExtension, LoroExtension]>>(options: EditorOptions<Union<readonly [BasicExtension, LoroExtension]>>): Editor<Union<readonly [BasicExtension, LoroExtension]>>@public ({
extension EditorOptions<Union<readonly [BasicExtension, LoroExtension]>>.extension: Union<readonly [BasicExtension, LoroExtension]>The extension to use when creating the editor. : union union<readonly [BasicExtension, LoroExtension]>(exts_0: BasicExtension, exts_1: LoroExtension): Union<readonly [BasicExtension, LoroExtension]> (+1 overload)Merges multiple extensions into one. You can pass multiple extensions as
arguments or a single array containing multiple extensions.@throwsIf no extensions are provided.@example```ts
function defineFancyNodes() {
return union(
defineFancyParagraph(),
defineFancyHeading(),
)
}
```@example```ts
function defineFancyNodes() {
return union([
defineFancyParagraph(),
defineFancyHeading(),
])
}
```@public (
defineBasicExtension function defineBasicExtension(): BasicExtensionDefine a basic extension that includes some common functionality. You can
copy this function and customize it to your needs.
It's a combination of the following extension functions:
-
{@link
defineDoc
}
-
{@link
defineText
}
-
{@link
defineParagraph
}
-
{@link
defineHeading
}
-
{@link
defineList
}
-
{@link
defineBlockquote
}
-
{@link
defineImage
}
-
{@link
defineHorizontalRule
}
-
{@link
defineHardBreak
}
-
{@link
defineTable
}
-
{@link
defineCodeBlock
}
-
{@link
defineItalic
}
-
{@link
defineBold
}
-
{@link
defineUnderline
}
-
{@link
defineStrike
}
-
{@link
defineCode
}
-
{@link
defineLink
}
-
{@link
defineBaseKeymap
}
-
{@link
defineBaseCommands
}
-
{@link
defineHistory
}
-
{@link
defineGapCursor
}
-
{@link
defineVirtualSelection
}
-
{@link
defineModClickPrevention
}@public (),
defineLoro function defineLoro(options: LoroOptions): LoroExtension@public ({ doc LoroOptions.doc: LoroDocTypeThe Loro instance handles the state of shared data. , awareness LoroOptions.awareness?: CursorAwareness | undefinedThe (legacy) Awareness instance. One of `awareness` or `presence` must be provided. }),
),
})Network sync
Section titled “Network sync”Loro's transport is bring-your-own. The CRDT exposes binary updates you forward over any channel (WebSocket, libp2p, BroadcastChannel, etc):
const ws = new WebSocket('wss://your-server.example/loro')
doc.subscribe((event) => {
if (event.by === 'local') {
const update = doc.export({ mode: 'update', from: event.fromVersion })
ws.send(update)
}
})
ws.onmessage = (msg) => {
doc.import(new Uint8Array(msg.data))
}Persistence and snapshots
Section titled “Persistence and snapshots”const snapshot = doc.export({ mode: 'snapshot' })
localStorage.setItem('doc.loro', JSON.stringify(Array.from(snapshot)))
// later:
const stored = localStorage.getItem('doc.loro')
if (stored) {
doc.import(new Uint8Array(JSON.parse(stored)))
}