Yjs

用于构建协作软件的共享数据类型。「Shared data types for building collaborative software」

Github stars Tracking Chart

Yjs

一个具有强大的共享数据抽象的 CRDT 框架。

Yjs 是一个 CRDT 实现,它以共享类型的形式公开其内部数据结构。共享类型是像 Map 或 Array 这样常见的数据类型,具有超强的功能:更改会自动分发到其他对等方,并在没有合并冲突的情况下进行合并。

Yjs 与网络无关(p2p!),支持许多现有的富文本编辑器,离线编辑,版本快照,撤销/重做和共享游标。它的扩展性很好,用户数量不受限制,甚至非常适合大型文档。

如果你正在寻找专业的(付费)支持来构建协作式或分布式应用,请发邮件给我们:yjs@tag1consulting.com。否则,你可以在我们的讨论区找到帮助。

谁在使用 Yjs

  • Relm 一个团队合作和社区的协作游戏世界。
  • nput 一个协作记事的应用。
  • oom.sh 一款集成了协作绘图、编辑和编码工具的会议应用程序。
  • http://coronavirustechhandbook.com/ 一个由数千名不同的人编辑的合作维基,致力于快速和复杂地应对冠状病毒的爆发和后续影响。
  • Nimbus Note 一个由 Nimbus Web 设计的笔记应用。
  • JoeDocs 一个开放的协作式维基。
  • Pluxbox RadioManager 一个基于 Web 的应用程序,用于协作组织电台广播。
  • Cattaz 一个可以在 wiki 页面中运行自定义应用程序的 wiki。

概述

此存储库包含一个共享类型的集合,可以观察这些类型的更改并同时进行操作。网络功能和双向绑定在单独的模块中实现。

绑定(Bindings)

名称 光标 绑定 演示
ProseMirror heavy_check_mark y-prosemirror demo
Quill heavy_check_mark y-quill demo
CodeMirror heavy_check_mark y-codemirror demo
Monaco heavy_check_mark y-monaco demo

提供商(Providers)

设置客户之间的沟通,管理意识信息,以及存储共享数据以供离线使用是相当麻烦的。提供商为您管理这一切,是您的协作应用的完美起点。

y-webrtc
使用 WebRTC 点对点传播文件更新。点对点通过信令服务器交换信令数据。可以使用公开的信令服务器。通过信令服务器进行的通信可以通过提供共享秘密进行加密,保持连接信息和共享文档的私密性。
y-websocket
包含一个简单的 websocket 后端和一个连接到该后端的 websocket 客户端的模块。后台可以扩展到 leveldb 数据库中持久化更新。
y-indexeddb
有效地将文档更新坚持到浏览器的 indexeddb 数据库中。文档立即可用,只需要通过网络提供商同步差异。
y-dat
[WIP]使用 multifeed 将文档更新有效地写入 dat 网络。每个客户机都有一个仅附加的 CRDT 本地更新日志(hypercore)。multified 管理和同步超线程,y-dat 监听更改并将其应用于 Yjs 文档。

入门

用你喜欢的软件包管理器安装 Yjs 和一个提供商。

npm i yjs y-websocket

启动 y-websocket 服务器

PORT=1234 node ./node_modules/y-websocket/bin/server.js

示例:观察类型(Observe types)

const yarray = doc.getArray('my-array')
yarray.observe(event => {
  console.log('yarray was modified')
})
// 每次本地或远程客户端修改 yarray 时,都会调用观察者
yarray.insert(0, ['val']) // => "yarray was modified"

示例:嵌套类型(Nest types)

记住,共享类型只是普通的数据类型。唯一的限制是,一个共享类型在共享文档中只能存在一次。

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

现在你明白了如何在共享文档上定义类型了。接下来你可以跳转到 演示仓库 或继续阅读 API 文档。

示例:使用和组合提供者

任何一个 Yjs 供应商都可以相互组合。所以你可以通过不同的网络技术来同步数据。

在大多数情况下,你希望将网络提供者(如y-websocket或y-webrtc)与持久化提供者(浏览器中的y-indexeddb)结合使用。持久性允许你更快地加载文档,并持久化离线时创建的数据。

在本演示中,我们将两个不同的网络提供者与一个持久性提供者结合起来。

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()
// 这允许您立即获取(缓存)文档数据
const indexeddbProvider = new IndexeddbPersistence('count-demo', ydoc)
indexeddbProvider.whenSynced.then(() => {
  console.log('loaded data from indexed db')
})
// 将客户端与 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
)
// 产生和的数的数组
const yarray = ydoc.getArray('count')
// 观察总数的变化
yarray.observe(event => {
  // print updates when the data changes
  console.log('new sum: ' + yarray.toArray().reduce((a,b) => a + b))
})
// 总和加1
yarray.push([1]) // => "new sum: 1"

API

import * as Y from 'yjs'

(恕删略。请参考自述文件)

Yjs CRDT 算法

用于协作编辑的无冲突复制数据类型(CRDT)是操作转换(OT)的另一种方法。这两种方法的一个非常简单的区别是,OT 试图转换索引位置以确保收敛(所有客户端最终得到的是相同的内容),而 CRDT 使用数学模型,通常不涉及索引转换,如链接列表。OT 是目前文本上共享编辑的事实标准。支持共享编辑的 OT 方法如果没有一个中心真理源(中心服务器),就需要太多的记账工作,在实践中是不可行的。CRDTs 更适合分布式系统,它提供了额外的保证,即文档可以与远程客户端同步,并且不需要中央真实源。

Yjs 实现了 本论文 所述算法的修改版本。本文解释了在 CRDT 模型上的简单优化,并对 Yjs 中的性能特性做了更多的介绍。更多关于具体实现的信息可以在 INTERNALS.md本篇 Yjs 代码库的演练中获得。

适合共享文本编辑的 CRDT 的苦恼是,它们的体积只会越来越大。有的 CRDT 虽然不会变大,但却不具备共享文本编辑所需的特性(如意图保存)。Yjs 在原有算法的基础上做了很多改进,减少了文档只增长大小的折衷。我们不能在保证结构的唯一顺序的同时,对删除的结构(墓碑)进行垃圾回收。但是我们可以:1.将前面的结构合并到一个结构中,以减少元信息量;2.如果结构被删除,我们可以从结构中删除内容;3.如果我们不再关心结构的顺序,我们可以垃圾回收墓碑(例如,如果父结构被删除)。

举个例子:

如果用户按顺序插入元素,结构将被合并成一个结构。例如:array.insert(0, ['a']), array.insert(0, ['b']); 首先表示为两个结构([{id: {client, clock: 0}, content: 'a'}, {id: {client, clock: 1}, content: 'b'}),然后合并成一个结构。[{id: {client, clock: 0}, content: 'ab'}]。

当一个包含内容的结构(如 ItemString)被删除时,该结构将被一个不再包含内容的 ItemDeleted 代替。

当一个类型被删除时,所有的子元素都会转化为 GC 结构。一个 GC 结构只表示一个结构的存在以及它被删除。如果 id 相邻,GC 结构总是可以与其他 GC 结构合并。
特别是在处理结构化内容时(例如在 ProseMirror 上的共享编辑),当对随机文档编辑进行基准测试时,这些改进会产生非常好的结果。在实践中,它们显示出了更好的结果,因为用户通常会按顺序编辑文本,导致结构很容易被合并。基准测试表明,即使在最坏的情况下,用户从右到左编辑文本,Yjs 也能实现良好的性能,即使是巨大的文档。

状态向量

Yjs 有能力在同步两个客户端时只交换差异。我们使用 lamport 时间戳来识别 structs,并跟踪客户端创建它们的顺序。每个结构都有一个 struct.id = { client: number, clock: number},它唯一地标识一个结构。我们将每个客户端的下一个预期时钟定义为状态向量。这个数据结构类似于版本向量数据结构。但是我们只用状态向量来描述本地文档的状态,所以我们可以计算远程客户端丢失的结构。我们不使用它来跟踪因果关系。

许可和作者

Yjs 和所有相关项目都是 MIT 授权

Yjs 是基于我在 RWTH i5 的学生时代的研究。现在我在业余时间从事 Yjs 的研究。
通过在 GitHub Sponsors 上捐款来资助这个项目,或者雇佣作为你的合作应用的承包商。

 

(The first version translated by vz on 2020.10.03)

Main metrics

Overview
Name With Owneryjs/yjs
Primary LanguageJavaScript
Program languageJavaScript (Language Count: 2)
PlatformLinux, Mac, Windows
License:Other
所有者活动
Created At2014-07-29 19:29:45
Pushed At2025-04-19 13:33:10
Last Commit At2024-04-12 16:00:14
Release Count323
Last Release Namev13.6.26 (Posted on 2025-04-18 16:12:26)
First Release Namev0.0.0 (Posted on )
用户参与
Stargazers Count19k
Watchers Count121
Fork Count660
Commits Count1.9k
Has Issues Enabled
Issues Count475
Issue Open Count98
Pull Requests Count162
Pull Requests Open Count11
Pull Requests Close Count56
项目设置
Has Wiki Enabled
Is Archived
Is Fork
Is Locked
Is Mirror
Is Private

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.

: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.

Sponsorship

Please contribute to the project financially - especially if your company relies
on Yjs. Become a Sponsor

Professional Support

  • 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
    .

Who is using Yjs

  • 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.
  • 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.
  • btw - Personal website builder
  • AWS SageMaker - Machine Learning Service
  • 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

Table of Contents

Overview

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.

Bindings

Name Cursors Binding Demo
ProseMirror                                                   y-prosemirror demo
Quill y-quill demo
CodeMirror y-codemirror demo
Monaco y-monaco demo
Slate slate-yjs demo
BlockSuite (native) demo
Lexical (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

Providers

Setting 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

Tooling

Ports

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.

Getting Started

Install Yjs and a provider with your favorite package manager:

npm i yjs y-websocket

Start the y-websocket server:

PORT=1234 node ./node_modules/y-websocket/bin/server.cjs

Example: Observe types

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"

Example: Nest types

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.

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.

Example: Using and combining providers

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.

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"

API

import * as Y from 'yjs'

Shared Types

Y.Doc

const doc = new Y.Doc()

Y.Doc Events

Document Updates

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

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

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.

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.

// 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.

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.applyUpdateY.applyUpdateV2. Also when listening to updates
you need to specifically need listen for V2 events e.g. yDoc.on('updateV2', …).
We also support conversion functions between both formats:
Y.convertUpdateFormatV1ToV2 & Y.convertUpdateFormatV2ToV1.

Update API

Relative Positions

When 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 relative
position is attached to c. When a remote user modifies the document by
inserting a character before the cursor, the cursor will stay attached to the
character c. 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

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)

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)

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.UndoManager

Yjs 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.

const ytext = doc.getText('text')
const undoManager = new Y.UndoManager(ytext)

ytext.insert(0, 'abc')
undoManager.undo()
ytext.toString() // => ''
undoManager.redo()
ytext.toString() // => 'abc'

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.

// 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 can
selectively specify which changes should be tracked by UndoManager. The
UndoManager instance is always added to trackedOrigins.

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 part
                 //           of `trackedTransactionOrigins`)
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` and
                 //        `CustomBinding` 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.

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'))
})

Yjs CRDT Algorithm

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
    first represented as two structs ([{id: {client, clock: 0}, content: 'a'}, {id: {client, clock: 1}, content: 'b'}) and then merged into a single
    struct: [{id: {client, clock: 0}, content: 'ab'}].
  2. When a struct that contains content (e.g. ItemString) is deleted, the
    struct will be replaced with an ItemDeleted that does not contain content
    anymore.
  3. When a type is deleted, all child elements are transformed to GC structs. A
    GC 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.

State Vector

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.

License and Author

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.