Shared Editing Library
npm install yjs> A CRDT framework with a powerful abstraction of shared data
Yjs is a CRDT implementation that exposes its internal
data structure as shared types. Shared types are common data types like Map
or Array with superpowers: changes are automatically distributed to other
peers and merged without merge conflicts.
Yjs is network agnostic (p2p!), supports many existing **rich text
editors, offline editing, version snapshots, undo/redo** and
shared cursors. It scales well with an unlimited number of users and is well
suited for even large documents.
* Demos: https://github.com/yjs/yjs-demos
* Discuss: https://discuss.yjs.dev
* Chat: Gitter | Discord
* Benchmark Yjs vs. Automerge:
https://github.com/dmonad/crdt-benchmarks
* Podcast "Yjs Deep Dive into real time collaborative editing solutions":
* Podcast "Google Docs-style editing in Gutenberg with the YJS framework":
:construction_worker_woman: If you are looking for professional support, please
consider supporting this project via a "support contract" on
GitHub Sponsors. I will attend your issues
quicker and we can discuss questions and problems in regular video conferences.
Otherwise you can find help on our community discussion board.
Please contribute to the project financially - especially if your company relies
on Yjs. 
* Support Contract with the Maintainer -
By contributing financially to the open-source Yjs project, you can receive
professional support directly from the author. This includes the opportunity for
weekly video calls to discuss your specific challenges.
* Synergy Codes - Specializing in
consulting and developing real-time collaborative editing solutions for visual
apps, Synergy Codes focuses on interactive diagrams, complex graphs, charts, and
various data visualization types. Their expertise empowers developers to build
engaging and interactive visual experiences leveraging the power of Yjs. See
their work in action at Visual Collaboration
Showcase.
* AFFiNE A local-first, privacy-first, open source
knowledge base. :star2:
* Huly - Open Source All-in-One Project Management Platform :star2:
* Cargo Site builder for designers and artists :star2:
* Gitbook Knowledge management for technical teams :star2:
* Evernote Note-taking app :star2:
* Lessonspace Enterprise platform for virtual
classrooms and online training :star2:
* Ellipsus - Collaborative writing app for storytelling etc.
Supports versioning, change attribution, and "blame". A solution for the whole
publishing process (also selling) :star:
* Dynaboard Build web apps collaboratively. :star:
* Relm A collaborative gameworld for teamwork and
community. :star:
* Room.sh A meeting application with integrated
collaborative drawing, editing, and coding tools. :star:
* Nimbus Note A note-taking app designed by
Nimbus Web. :star:
* Pluxbox RadioManager A web-based app to
collaboratively organize radio broadcasts. :star:
* modyfi - Modyfi is the design platform built for
multidisciplinary designers. Design, generate, animate, and more — without
switching between apps. :star:
* Sana A learning platform with collaborative text
editing powered by Yjs.
* Serenity Notes End-to-end encrypted
collaborative notes app.
* PRSM Collaborative mind-mapping and system visualisation.
(source)
* Alldone A next-gen project management and
collaboration platform.
* Living Spec A modern way for product teams to collaborate.
* Slidebeamer Presentation app.
* BlockSurvey End-to-end encryption for your forms/surveys.
* Skiff Private, decentralized workspace.
* JupyterLab Collaborative computational Notebooks
* JupyterCad Extension to
JupyterLab that enables collaborative editing of 3d FreeCAD Models.
* JupyterGIS Collaborative GIS
(Geographic Information System) editor in Jupyter
* Hyperquery A collaborative data workspace for
sharing analyses, documentation, spreadsheets, and dashboards.
* Nosgestesclimat The french carbon
footprint calculator has a group P2P mode based on yjs
* oorja.io Online meeting spaces extensible with
collaborative apps, end-to-end encrypted.
* LegendKeeper Collaborative campaign planner and
worldbuilding app for tabletop RPGs.
* IllumiDesk Build courses and content with A.I.
* btw Open-source Medium alternative
* AWS SageMaker Tools for building Machine
Learning Models
* linear Streamline issues, projects, and product roadmaps.
* Arkiter - Live interview software
* Appflowy - They use Yrs
* Multi.app - Multiplayer app sharing: Point, draw and edit
in shared apps as if they're on your computer. They are using Yrs.
* AppMaster A No-Code platform for creating
production-ready applications with source code generation.
* Synthesia - Collaborative Video Editor
* thinkdeli - A fast and simple notes app powered by AI
* ourboard - A collaborative whiteboard
application
* Ellie.ai - Data Product Design and Collaboration
* GoPeer - Collaborative tutoring
* screen.garden - Collaborative backend for PKM apps.
* NextCloud - Content Collaboration Platform
* keystatic - git-based CMS
* QDAcity - Collaborative qualitative data analysis platform
* Kanbert - Project management software
* Eclipse Theia - A cloud & desktop
IDE that runs in the browser.
* ScienHub - Collaborative LaTeX editor in the browser.
* Open Collaboration Tools - Collaborative
editing for your IDE or custom editor
* Typst - Compose, edit, and automate technical documents
* Kedyou - Digital workspaces for tutoring
* Lightpage - Personal living notebook
* reearth-flow -
Collaboratively calculate and convert various data
* Overview
* Bindings
* Providers
* Tooling
* Ports
* Getting Started
* API
* Shared Types
* Y.Doc
* Document Updates
* Relative Positions
* Y.UndoManager
* Yjs CRDT Algorithm
* License and Author
This repository contains a collection of shared types that can be observed for
changes and manipulated concurrently. Network functionality and two-way-bindings
are implemented in separate modules.
| Name | Cursors | Binding | Demo |
|---|:-:|---|---|
| ProseMirror | ✔ | y-prosemirror | demo |
| Quill | ✔ | y-quill | demo |
| CodeMirror | ✔ | y-codemirror | demo |
| Monaco | ✔ | y-monaco | demo |
| Ace | ✔ | y-ace | |
| Slate | ✔ | slate-yjs | demo |
| BlockSuite | ✔ | (native) | demo |
| Lexical | ✔ | (native) | demo |
| BlockNote | ✔ | y-prosemirror | demo |
| Tiptap | ✔ | y-prosemirror | demo |
| Milkdown | ✔ | y-prosemirror | demo |
| Superdoc | ✔ | (native) | demo |
| valtio | | valtio-yjs | demo |
| immer | | immer-yjs | demo |
| React | | react-yjs | demo |
| React / Vue / Svelte / MobX | | SyncedStore | demo |
| mobx-keystone | | mobx-keystone-yjs | demo |
| PSPDFKit | | yjs-pspdfkit | demo |
| Rows n'Columns | ✔ | @rowsncolumns/y-spreadsheet | |
Tools that extend the core functionality of Yjs.
YMultiDocUndoManager (undo/redo across Yjs docs) andYKeyValue (optimized key-value store).insert,delete, and move operations for folder-likeSetting up the communication between clients, managing awareness information,
and storing shared data for offline usage is quite a hassle. Providers
manage all that for you and are the perfect starting point for your
collaborative app.
> This list of providers is incomplete. Please open PRs to add your providers to
> this list!
#### Connection Providers
#### Persistence Providers
* y-sweet debugger
* liveblocks devtools
* Yjs inspector
There are several Yjs-compatible ports to other programming languages.
* y-octo - Rust implementation by
AFFiNE
* y-crdt - Rust implementation with multiple
language bindings to other languages
* yrs - Rust interface
* ypy - Python binding
* yrb - Ruby binding
* yswift - Swift binding
* yffi - C-FFI
* ywasm - WASM binding
* y_ex - Elixir bindings
* ycs - .Net compatible C# implementation.
Install Yjs and a provider with your favorite package manager:
``sh`
npm i yjs y-websocket
Start the y-websocket server:
`sh`
PORT=1234 node ./node_modules/y-websocket/bin/server.cjs
`js
import * as Y from 'yjs';
const doc = new Y.Doc();
const yarray = doc.getArray('my-array')
yarray.observe(event => {
console.log('yarray was modified')
})
// every time a local or remote client modifies yarray, the observer is called
yarray.insert(0, ['val']) // => "yarray was modified"
`
Remember, shared types are just plain old data types. The only limitation is
that a shared type must exist only once in the shared document.
`js`
const ymap = doc.getMap('map')
const foodArray = new Y.Array()
foodArray.insert(0, ['apple', 'banana'])
ymap.set('food', foodArray)
ymap.get('food') === foodArray // => true
ymap.set('fruit', foodArray) // => Error! foodArray is already defined
Now you understand how types are defined on a shared document. Next you can jump
to the demo repository or continue reading
the API docs.
Any of the Yjs providers can be combined with each other. So you can sync data
over different network technologies.
In most cases you want to use a network provider (like y-websocket or y-webrtc)
in combination with a persistence provider (y-indexeddb in the browser).
Persistence allows you to load the document faster and to persist data that is
created while offline.
For the sake of this demo we combine two different network providers with a
persistence provider.
`js
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'
import { WebsocketProvider } from 'y-websocket'
import { IndexeddbPersistence } from 'y-indexeddb'
const ydoc = new Y.Doc()
// this allows you to instantly get the (cached) documents data
const indexeddbProvider = new IndexeddbPersistence('count-demo', ydoc)
indexeddbProvider.whenSynced.then(() => {
console.log('loaded data from indexed db')
})
// Sync clients with the y-webrtc provider.
const webrtcProvider = new WebrtcProvider('count-demo', ydoc)
// Sync clients with the y-websocket provider
const websocketProvider = new WebsocketProvider(
'wss://demos.yjs.dev', 'count-demo', ydoc
)
// array of numbers which produce a sum
const yarray = ydoc.getArray('count')
// observe changes of the sum
yarray.observe(event => {
// print updates when the data changes
console.log('new sum: ' + yarray.toArray().reduce((a,b) => a + b))
})
// add 1 to the sum
yarray.push([1]) // => "new sum: 1"
`
`js`
import * as Y from 'yjs'
Y.Array
A shareable Array-like type that supports efficient insert/delete of elements
at any position. Internally it uses a linked list of Arrays that is split when
necessary.
const yarray = new Y.Array()
Y.Array.from(Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>):
Y.Array
parent:Y.AbstractType|null
insert(index:number, content:Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>)
Insert content at index. Note that content is an array of elements.
I.e. array.insert(0, [1]) splices the list and inserts 1 at
position 0.
push(Array<Object|boolean|Array|string|number|null|Uint8Array|Y.Type>)
unshift(Array<Object|boolean|Array|string|number|null|Uint8Array|Y.Type>)
delete(index:number, length:number)
get(index:number)
slice(start:number, end:number):Array<Object|boolean|Array|string|number|null|Uint8Array|Y.Type>
length:number
forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type,
index:number, array: Y.Array))
map(function(T, number, YArray):M):Array<M>
clone(): Y.Array
Clone all values into a fresh Y.Array instance. The returned type can be
included into the Yjs document.
toArray():Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>
toJSON():Array<Object|boolean|Array|string|number|null>
Copies the content of this YArray to a new Array. It transforms all child types
to JSON using their toJSON method.
[Symbol.Iterator]
Returns an YArray Iterator that contains the values for each index in the array.
for (let value of yarray) { .. }
observe(function(YArrayEvent, Transaction):void)
Adds an event listener to this type that will be called synchronously every time
this type is modified. In the case this type is modified in the event listener,
the event listener will be called again after the current event listener returns.
unobserve(function(YArrayEvent, Transaction):void)
Removes an observe event listener from this type.
observeDeep(function(Array<YEvent>, Transaction):void)
Adds an event listener to this type that will be called synchronously every time
this type or any of its children is modified. In the case this type is modified
in the event listener, the event listener will be called again after the current
event listener returns. The event listener receives all Events created by itself
or any of its children.
unobserveDeep(function(Array<YEvent>, Transaction):void)
Removes an observeDeep event listener from this type.
Y.Map
A shareable Map type.
const ymap = new Y.Map()
parent:Y.AbstractType|null
size: number
get(key:string):object|boolean|string|number|null|Uint8Array|Y.Type
set(key:string, value:object|boolean|string|number|null|Uint8Array|Y.Type)
delete(key:string)
has(key:string):boolean
clear()
clone():Y.Map
toJSON():Object<string, Object|boolean|Array|string|number|null|Uint8Array>
Copies the [key,value] pairs of this YMap to a new Object.It
transforms all child types to JSON using their toJSON method.
forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type,
key:string, map: Y.Map))
Execute the provided function once for every key-value pair.
[Symbol.Iterator]
Returns an Iterator of [key, value] pairs.
for (let [key, value] of ymap) { .. }
entries()
Returns an Iterator of [key, value] pairs.
values()
Returns an Iterator of all values.
keys()
Returns an Iterator of all keys.
observe(function(YMapEvent, Transaction):void)
Adds an event listener to this type that will be called synchronously every time
this type is modified. In the case this type is modified in the event listener,
the event listener will be called again after the current event listener returns.
unobserve(function(YMapEvent, Transaction):void)
Removes an observe event listener from this type.
observeDeep(function(Array<YEvent>, Transaction):void)
Adds an event listener to this type that will be called synchronously every time
this type or any of its children is modified. In the case this type is modified
in the event listener, the event listener will be called again after the current
event listener returns. The event listener receives all Events created by itself
or any of its children.
unobserveDeep(function(Array<YEvent>, Transaction):void)
Removes an observeDeep event listener from this type.
Y.Text
A shareable type that is optimized for shared editing on text. It allows to
assign properties to ranges in the text. This makes it possible to implement
rich-text bindings to this type.
This type can also be transformed to the
delta format. Similarly the
YTextEvents compute changes as deltas.
const ytext = new Y.Text()
parent:Y.AbstractType|null
insert(index:number, content:string, [formattingAttributes:Object<string,string>])
Insert a string at index and assign formatting attributes to it.
ytext.insert(0, 'bold text', { bold: true })
delete(index:number, length:number)
format(index:number, length:number, formattingAttributes:Object<string,string>)
applyDelta(delta: Delta, opts:Object<string,any>)
See Quill Delta
Can set options for preventing remove ending newLines, default is true.
ytext.applyDelta(delta, { sanitize: false })
length:number
toString():string
toJSON():string
toString
toDelta():Delta
Transforms this type to a Quill Delta
observe(function(YTextEvent, Transaction):void)
Adds an event listener to this type that will be called synchronously every time
this type is modified. In the case this type is modified in the event listener,
the event listener will be called again after the current event listener returns.
unobserve(function(YTextEvent, Transaction):void)
Removes an observe event listener from this type.
observeDeep(function(Array<YEvent>, Transaction):void)
Adds an event listener to this type that will be called synchronously every time
this type or any of its children is modified. In the case this type is modified
in the event listener, the event listener will be called again after the current
event listener returns. The event listener receives all Events created by itself
or any of its children.
unobserveDeep(function(Array<YEvent>, Transaction):void)
Removes an observeDeep event listener from this type.
Y.XmlFragment
A container that holds an Array of Y.XmlElements.
const yxml = new Y.XmlFragment()
parent:Y.AbstractType|null
firstChild:Y.XmlElement|Y.XmlText|null
insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)
delete(index:number, length:number)
get(index:number)
slice(start:number, end:number):Array<Y.XmlElement|Y.XmlText>
length:number
clone():Y.XmlFragment
toArray():Array<Y.XmlElement|Y.XmlText>
toDOM():DocumentFragment
toString():string
toJSON():string
toString.
createTreeWalker(filter: function(AbstractType<any>):boolean):Iterable
observe(function(YXmlEvent, Transaction):void)
Adds an event listener to this type that will be called synchronously every time
this type is modified. In the case this type is modified in the event listener,
the event listener will be called again after the current event listener returns.
unobserve(function(YXmlEvent, Transaction):void)
Removes an observe event listener from this type.
observeDeep(function(Array<YEvent>, Transaction):void)
Adds an event listener to this type that will be called synchronously every time
this type or any of its children is modified. In the case this type is modified
in the event listener, the event listener will be called again after the current
event listener returns. The event listener receives all Events created by itself
or any of its children.
unobserveDeep(function(Array<YEvent>, Transaction):void)
Removes an observeDeep event listener from this type.
Y.XmlElement
A shareable type that represents an XML Element. It has a nodeName,
attributes, and a list of children. But it makes no effort to validate its
content and be actually XML compliant.
const yxml = new Y.XmlElement()
parent:Y.AbstractType|null
firstChild:Y.XmlElement|Y.XmlText|null
nextSibling:Y.XmlElement|Y.XmlText|null
prevSibling:Y.XmlElement|Y.XmlText|null
insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)
delete(index:number, length:number)
get(index:number)
length:number
setAttribute(attributeName:string, attributeValue:string)
removeAttribute(attributeName:string)
getAttribute(attributeName:string):string
getAttributes():Object<string,string>
get(i:number):Y.XmlElement|Y.XmlText
slice(start:number, end:number):Array<Y.XmlElement|Y.XmlText>
clone():Y.XmlElement
toArray():Array<Y.XmlElement|Y.XmlText>
toDOM():Element
toString():string
toJSON():string
toString.
observe(function(YXmlEvent, Transaction):void)
Adds an event listener to this type that will be called synchronously every
time this type is modified. In the case this type is modified in the event
listener, the event listener will be called again after the current event
listener returns.
unobserve(function(YXmlEvent, Transaction):void)
Removes an observe event listener from this type.
observeDeep(function(Array<YEvent>, Transaction):void)
Adds an event listener to this type that will be called synchronously every time
this type or any of its children is modified. In the case this type is modified
in the event listener, the event listener will be called again after the current
event listener returns. The event listener receives all Events created by itself
or any of its children.
unobserveDeep(function(Array<YEvent>, Transaction):void)
Removes an observeDeep event listener from this type.
`js`
const doc = new Y.Doc()
clientIDgc
in order to disable gc and be able to restore old content. See https://github.com/yjs/yjs#yjs-crdt-algorithm
for more information about gc in Yjs.
transact(function(Transaction):void [, origin:any])update event are called after each transaction. You shoulddoc.transact(() => { yarray.insert(..); ymap.set(..) })origintransaction.origin andon('update', (update, origin) => ..).toJSON():anyydoc.getType(..)).get(string, Y.[TypeClass]):[Type]getArray(string):Y.Arrayy.get(string, Y.Array).getMap(string):Y.Mapy.get(string, Y.Map).getText(string):Y.Texty.get(string, Y.Text).getXmlElement(string, string):Y.XmlElementy.get(string, Y.XmlElement).getXmlFragment(string):Y.XmlFragmenty.get(string, Y.XmlFragment).on(string, function)off(string, function)#### Y.Doc Events
on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void)
to receive V2 events.
on('beforeTransaction', function(Y.Transaction, Y.Doc):void)on('afterTransaction', function(Y.Transaction, Y.Doc):void)on('beforeAllTransactions', function(Y.Doc):void)on('afterAllTransactions', function(Y.Doc, Array<Y.Transaction>):void)Changes on the shared document are encoded into document updates. Document
updates are commutative and idempotent. This means that they can be applied
in any order and multiple times.
#### Example: Listen to update events and apply them on remote client
`js
const doc1 = new Y.Doc()
const doc2 = new Y.Doc()
doc1.on('update', update => {
Y.applyUpdate(doc2, update)
})
doc2.on('update', update => {
Y.applyUpdate(doc1, update)
})
// All changes are also applied to the other document
doc1.getArray('myarray').insert(0, ['Hello doc2, you got this?'])
doc2.getArray('myarray').get(0) // => 'Hello doc2, you got this?'
`
Yjs internally maintains a state vector that denotes the next
expected clock from each client. In a different interpretation it holds the
number of structs created by each client. When two clients sync, you can either
exchange the complete document structure or only the differences by sending the
state vector to compute the differences.
#### Example: Sync two clients by exchanging the complete document structure
`js`
const state1 = Y.encodeStateAsUpdate(ydoc1)
const state2 = Y.encodeStateAsUpdate(ydoc2)
Y.applyUpdate(ydoc1, state2)
Y.applyUpdate(ydoc2, state1)
#### Example: Sync two clients by computing the differences
This example shows how to sync two clients with the minimal amount of exchanged
data by computing only the differences using the state vector of the remote
client. Syncing clients using the state vector requires another roundtrip, but
can save a lot of bandwidth.
`js`
const stateVector1 = Y.encodeStateVector(ydoc1)
const stateVector2 = Y.encodeStateVector(ydoc2)
const diff1 = Y.encodeStateAsUpdate(ydoc1, stateVector2)
const diff2 = Y.encodeStateAsUpdate(ydoc2, stateVector1)
Y.applyUpdate(ydoc1, diff2)
Y.applyUpdate(ydoc2, diff1)
#### Example: Syncing clients without loading the Y.Doc
It is possible to sync clients and compute delta updates without loading the Yjs
document to memory. Yjs exposes an API to compute the differences directly on the
binary document updates.
`js
// encode the current state as a binary buffer
let currentState1 = Y.encodeStateAsUpdate(ydoc1)
let currentState2 = Y.encodeStateAsUpdate(ydoc2)
// now we can continue syncing clients using state vectors without using the Y.Doc
ydoc1.destroy()
ydoc2.destroy()
const stateVector1 = Y.encodeStateVectorFromUpdate(currentState1)
const stateVector2 = Y.encodeStateVectorFromUpdate(currentState2)
const diff1 = Y.diffUpdate(currentState1, stateVector2)
const diff2 = Y.diffUpdate(currentState2, stateVector1)
// sync clients
currentState1 = Y.mergeUpdates([currentState1, diff2])
currentState2 = Y.mergeUpdates([currentState2, diff1])
`
#### Obfuscating Updates
If one of your users runs into a weird bug (e.g. the rich-text editor throws
error messages), then you don't have to request the full document from your
user. Instead, they can obfuscate the document (i.e. replace the content with
meaningless generated content) before sending it to you. Note that someone might
still deduce the type of content by looking at the general structure of the
document. But this is much better than requesting the original document.
Obfuscated updates contain all the CRDT-related data that is required for
merging. So it is safe to merge obfuscated updates.
`javascript`
const ydoc = new Y.Doc()
// perform some changes..
ydoc.getText().insert(0, 'hello world')
const update = Y.encodeStateAsUpdate(ydoc)
// the below update contains scrambled data
const obfuscatedUpdate = Y.obfuscateUpdate(update)
const ydoc2 = new Y.Doc()
Y.applyUpdate(ydoc2, obfuscatedUpdate)
ydoc2.getText().toString() // => "00000000000"
#### Using V2 update format
Yjs implements two update formats. By default you are using the V1 update format.
You can opt-in into the V2 update format which provides much better compression.
It is not yet used by all providers. However, you can already use it if
you are building your own provider. All below functions are available with the
suffix "V2". E.g. Y.applyUpdate ⇒ Y.applyUpdateV2. Also when listening to updatesyDoc.on('updateV2', …)
you need to specifically need listen for V2 events e.g. .Y.convertUpdateFormatV1ToV2
We also support conversion functions between both formats: & Y.convertUpdateFormatV2ToV1.
#### Update API
Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])transactionOrigin that will be stored ontransaction.originydoc.on('update', (update, origin) => ..).Y.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8ArrayY.encodeStateVector(Y.Doc):Uint8ArrayY.mergeUpdates(Array<Uint8Array>)Y.encodeStateVectorFromUpdate(Uint8Array): Uint8ArrayY.diffUpdate(update: Uint8Array, stateVector: Uint8Array): Uint8ArrayY.encodeStateAsUpdate(ydoc, stateVector) but worksconvertUpdateFormatV1ToV2convertUpdateFormatV2ToV1When working with collaborative documents, we often need to work with positions.
Positions may represent cursor locations, selection ranges, or even assign a
comment to a range of text. Normal index-positions (expressed as integers) are
not convenient to use because the index-range is invalidated as soon as a remote
change manipulates the document. Relative positions give you a powerful API to
express positions.
A relative position is fixated to an element in the shared document and is not
affected by remote changes. I.e. given the document "a|c", the relativec
position is attached to . When a remote user modifies the document byc
inserting a character before the cursor, the cursor will stay attached to the
character . insert(1, 'x')("a|c") = "ax|c". When the relative position is
set to the end of the document, it will stay attached to the end of the
document.
#### Example: Transform to RelativePosition and back
`js`
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
const pos = Y.createAbsolutePositionFromRelativePosition(relPos, doc)
pos.type === ytext // => true
pos.index === 2 // => true
#### Example: Send relative position to remote client (json)
`js`
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
const encodedRelPos = JSON.stringify(relPos)
// send encodedRelPos to remote client..
const parsedRelPos = JSON.parse(encodedRelPos)
const pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc)
pos.type === remoteytext // => true
pos.index === 2 // => true
#### Example: Send relative position to remote client (Uint8Array)
`js`
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
const encodedRelPos = Y.encodeRelativePosition(relPos)
// send encodedRelPos to remote client..
const parsedRelPos = Y.decodeRelativePosition(encodedRelPos)
const pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc)
pos.type === remoteytext // => true
pos.index === 2 // => true
Y.createRelativePositionFromTypeIndex(type:Uint8Array|Y.Type, index: number
[, assoc=0])
assoc >= 0). By default, the position associatesassoc < 0, then the relative position associates with the character
Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc):
{ type: Y.AbstractType, index: number, assoc: number } | null
Y.encodeRelativePosition(RelativePosition):Uint8Array
Y.decodeRelativePosition(Uint8Array):RelativePositionYjs ships with an Undo/Redo manager for selective undo/redo of changes on a
Yjs type. The changes can be optionally scoped to transaction origins.
`js
const ytext = doc.getText('text')
const undoManager = new Y.UndoManager(ytext)
ytext.insert(0, 'abc')
undoManager.undo()
ytext.toString() // => ''
undoManager.redo()
ytext.toString() // => 'abc'
`
constructor(scope:Y.AbstractType|Array<Y.AbstractType>
[, {captureTimeout:number,trackedOrigins:Set<any>,deleteFilter:function(item):boolean}])undo()redo()stopCapturing()
on('stack-item-added', { stackItem: { meta: Map<any,any> }, type: 'undo'
| 'redo' })
StackItem is added to the
on('stack-item-updated', { stackItem: { meta: Map<any,any> }, type: 'undo'
| 'redo' })
StackItem is updated.
on('stack-item-popped', { stackItem: { meta: Map<any,any> }, type: 'undo'
| 'redo' })
StackItem is popped from
on('stack-cleared', { undoStackCleared: boolean, redoStackCleared: boolean })
#### Example: Stop Capturing
UndoManager merges Undo-StackItems if they are created within time-gap
smaller than options.captureTimeout. Call um.stopCapturing() so that the next
StackItem won't be merged.
`js`
// without stopCapturing
ytext.insert(0, 'a')
ytext.insert(1, 'b')
undoManager.undo()
ytext.toString() // => '' (note that 'ab' was removed)
// with stopCapturing
ytext.insert(0, 'a')
undoManager.stopCapturing()
ytext.insert(0, 'b')
undoManager.undo()
ytext.toString() // => 'a' (note that only 'b' was removed)
#### Example: Specify tracked origins
Every change on the shared document has an origin. If no origin was specified,
it defaults to null. By specifying trackedOrigins you canUndoManager
selectively specify which changes should be tracked by . ThetrackedOrigins
UndoManager instance is always added to .
`js
class CustomBinding {}
const ytext = doc.getText('text')
const undoManager = new Y.UndoManager(ytext, {
trackedOrigins: new Set([42, CustomBinding])
})
ytext.insert(0, 'abc')
undoManager.undo()
ytext.toString() // => 'abc' (does not track because origin null and not parttrackedTransactionOrigins
// of )
ytext.delete(0, 3) // revert change
doc.transact(() => {
ytext.insert(0, 'abc')
}, 42)
undoManager.undo()
ytext.toString() // => '' (tracked because origin is an instance of trackedTransactionorigins)
doc.transact(() => {
ytext.insert(0, 'abc')
}, 41)
undoManager.undo()
ytext.toString() // => 'abc' (not tracked because 41 is not an instance of
// trackedTransactionorigins)
ytext.delete(0, 3) // revert change
doc.transact(() => {
ytext.insert(0, 'abc')
}, new CustomBinding())
undoManager.undo()
ytext.toString() // => '' (tracked because origin is a CustomBinding andCustomBinding
// is in trackedTransactionorigins)`
#### Example: Add additional information to the StackItems
When undoing or redoing a previous action, it is often expected to restore
additional meta information like the cursor location or the view on the
document. You can assign meta-information to Undo-/Redo-StackItems.
`js
const ytext = doc.getText('text')
const undoManager = new Y.UndoManager(ytext, {
trackedOrigins: new Set([42, CustomBinding])
})
undoManager.on('stack-item-added', event => {
// save the current cursor location on the stack-item
event.stackItem.meta.set('cursor-location', getRelativeCursorLocation())
})
undoManager.on('stack-item-popped', event => {
// restore the current cursor location on the stack-item
restoreCursorLocation(event.stackItem.meta.get('cursor-location'))
})
`
Conflict-free replicated data types (CRDT) for collaborative editing are an
alternative approach to operational transformation (OT). A very simple
differentiation between the two approaches is that OT attempts to transform
index positions to ensure convergence (all clients end up with the same
content), while CRDTs use mathematical models that usually do not involve index
transformations, like linked lists. OT is currently the de-facto standard for
shared editing on text. OT approaches that support shared editing without a
central source of truth (a central server) require too much bookkeeping to be
viable in practice. CRDTs are better suited for distributed systems, provide
additional guarantees that the document can be synced with remote clients, and
do not require a central source of truth.
Yjs implements a modified version of the algorithm described in this
paper.
This article
explains a simple optimization on the CRDT model and
gives more insight about the performance characteristics in Yjs.
More information about the specific implementation is available in
INTERNALS.md and in
this walkthrough of the Yjs codebase.
CRDTs that are suitable for shared text editing suffer from the fact that they
only grow in size. There are CRDTs that do not grow in size, but they do not
have the characteristics that are beneficial for shared text editing (like
intention preservation). Yjs implements many improvements to the original
algorithm that diminish the trade-off that the document only grows in size. We
can't garbage collect deleted structs (tombstones) while ensuring a unique
order of the structs. But we can 1. merge preceding structs into a single
struct to reduce the amount of meta information, 2. we can delete content from
the struct if it is deleted, and 3. we can garbage collect tombstones if we
don't care about the order of the structs anymore (e.g. if the parent was
deleted).
Examples:
1. If a user inserts elements in sequence, the struct will be merged into a
single struct. E.g. text.insert(0, 'a'), text.insert(1, 'b'); is[{id: {client, clock: 0}, content: 'a'},
first represented as two structs (
{id: {client, clock: 1}, content: 'b'}) and then merged into a single[{id: {client, clock: 0}, content: 'ab'}]
struct: .ItemString
2. When a struct that contains content (e.g. ) is deleted, theItemDeleted
struct will be replaced with an that does not contain contentGC
anymore.
3. When a type is deleted, all child elements are transformed to structs. AGC
struct only denotes the existence of a struct and that it is deleted.GC
structs can always be merged with other GC structs if the id's are
adjacent.
Especially when working on structured content (e.g. shared editing on
ProseMirror), these improvements yield very good results when
benchmarking random document edits.
In practice they show even better results, because users usually edit text in
sequence, resulting in structs that can easily be merged. The benchmarks show
that even in the worst case scenario that a user edits text from right to left,
Yjs achieves good performance even for huge documents.
Yjs has the ability to exchange only the differences when syncing two clients.
We use lamport timestamps to identify structs and to track in which order a
client created them. Each struct has an struct.id = { client: number, clock:
number} that uniquely identifies a struct. We define the next expected clock`
by each client as the state vector. This data structure is similar to the
version vectors data structure.
But we use state vectors only to describe the state of the local document, so we
can compute the missing struct of the remote client. We do not use it to track
causality.
Yjs and all related projects are MIT licensed.
Yjs is based on my research as a student at the RWTH
i5. Now I am working on Yjs in my spare time.
Fund this project by donating on GitHub Sponsors
or hiring me as a contractor for your collaborative
app.