Kevin Jahns

Yjs Workshop
BOB 2025

Single User Applications

  • VSCode
  • LibreOffice
  • Inkscape

=> everything we do nowadays is collaborative

Web Applications

  • Wordpress
  • Drupal
  • ..

Conflict Resolution: Last Writer Wins

Conflict Resolution: Duplication

Manual Conflict Resolution

Avoid Conflicts Through Coordination

  • Slack, Matrix
  • Only users assigned to a ticket may manipulate
  • WordPress Workflow: There is a dedicated person who writes content into WordPress

Realtime & Automatic Conflict Resolution

We can make anything collaborative..

Agenda

  • Yjs data types
  • Sync Yjs documents
  • Use Yjs extensions
  • collaborative react applications?

    Goal: collaborative, local-first (note-taking, drawing, ..) app
git clone https://github.com/yjs/bob-2025

Yjs

  • observable data types
    • Y.Map
    • Y.Array
    • Y.Text
    • Y.Xml*
  • automatic conflict resolution
  • network agnostic

Shared Data Types

import * as Y from 'yjs'

const ydoc = new Y.Doc()

const ymap = ydoc.getMap('map')
// const yarray = ydoc.getArray('array')
// const ytext = ydoc.getText('text')
// const yxml = ydoc.getXmlElement('el')

ymap.observe(event => {
  console.log(event.changes.keys)
})

ymap.set('my key', 'new value') // => { "my key": { action: 'add', oldValue: undefined } }

Exercise:

  • Observe a Y.Array<number>
  • If it has more than 10 elements, delete the smallest element.

Bonus: build a counter CRDT

Yjs always syncs

Updates contain a SET of operations

  • ydoc1 => { op1, op2 }
  • ydoc2 => { op2, op3 }
  • merge(ydoc1, ydoc2) => { op1, op2, op3 }

Yjs always syncs

import * as Y from 'yjs'

const ydoc1 = new Y.Doc()
const ymap1 = ydoc1.getMap()
ymap1.set('a', 1)

const ydoc2 = new Y.Doc()
const ymap2 = ydoc2.getMap()
ymap2.set('b', 2)

Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc1))
Y.applyUpdate(ydoc1, Y.encodeStateAsUpdate(ydoc2))

console.log(ymap1.toJSON()) // => { a: 1, b: 2 }
console.log(ymap2.toJSON()) // => { a: 1, b: 2 }

Sync only the differences

import * as Y from 'yjs'

const ydoc1 = new Y.Doc()
const ymap1 = ydoc1.getMap()
ymap1.set('a', 1)

const ydoc2 = new Y.Doc()
const ymap2 = ydoc2.getMap()
ymap2.set('b', 2)

// Sync the differences from ydoc2 into ydoc1
// The state vector describes what ydoc1 contains
const syncStep1 = Y.encodeStateVector(ydoc1)
// Only encode the differences into a update message
const syncStep2 = Y.encodeStateAsUpdate(ydoc2, syncStep1)
// Apply the differences to ydoc1
Y.applyUpdate(ydoc1, syncStep2)

console.log(ymap1.toJSON()) // => { a: 1, b: 2 }

Exercise 2: Sync two Yjs documents

  • Create two Yjs documents
  • Listen to `update` events and apply the update on the other Yjs document.

=> Immediately continue with providers

Providers

  • y-websocket
  • y-indexeddb
  • y-webrtc

Exercise:

  • Add y-websocket to one of your documents from the previous exercise
  • Room name: "bob-2025"

Use inspect.yjs.dev

Use Bindings

y-quill setup

+ possibly set up indexeddb

React with SyncedStore

SyncedStore

import React from "react";
import { useSyncedStore } from "@syncedstore/react";
import { store } from "./store";

export default function App() {
  const state = useSyncedStore(store);

  return (
    <div>
      <p>Todo items:</p>
      <ul>
        {state.todos.map((todo, i) => (
          <li key={i} style={{ textDecoration: todo.completed ? "line-through" : "" }}>
            <label>
              <input type="checkbox" checked={todo.completed} onClick={() => (todo.completed = !todo.completed)} />
              {todo.title}
            </label>
          </li>
        ))}
      </ul>
      <input
        onKeyPress={(event) => {
          if (event.key === "Enter") {
            state.todos.push({ completed: false, title: event.target.value });
            target.value = "";
          }
        }}
      />
    </div>
  );
}

SyncedStore

Relm.us

Yjs Ecosystem

  • y-webrtc
  • y-websocket
  • y-redis
  • hocuspocus
  • liveblocks
  • y-sweet
  • yrs-warp
  • matrix-crdt
  • ..
  • y-prosemirror
  • y-quill
  • y-codemirror
  • y-ace
  • y-monaco
  • lexical
  • Rows n' Columns
  • ...

Editor Bindings

Connectors

Persistence

  • y-indexeddb
  • y-leveldb
  • ystream

Cloud Providers

  • Affine Cloud
  • Liveblocks
  • y-sweet Cloud
  • Hocuspocus Cloud
  • Partykit / Cloudflare

Y CRDT

Yjs ported to different programming languages

Work of Bartosz Sypytkowski and many others

Ycs

Exercise

  • Start one of the examples in the `bob-2025` repository
  • Add y-indexeddb
  • inspect the structure of the document

Tutorial: How to break a CRDT :)

Local-First business appeal

  • Quick prototyping
  • Realtime & collaborative
  • Little setup
  • No backend logic
  • (Pay as you go)
  • (snapshots)
  • (offline support)

Yjs in practice

  • You have an existing application
       => Sync to Yjs types
     
  • Existing communication protocol
       => Implement provider
     
  • Auth
     
  • Existing database with documents.
       => Store Yjs docs in a database

Control

Control

Image is CC: https://www.flickr.com/photos/nossreh/10987353554

Data

Conflict Resolution

Bonus exercises

  • Implement an efficient Counter CRDT (addition and subtraction)

Centrality

source: https://www.printablee.com/post_50-states-printable-out-maps_185196/

Centrality

Why can't my favorite file syncing service sync directly to my phone. Instead I first upload to the US, and then sync back

Centrality / Central Place of Control

+ Easier to coordinate

 

- No direct sync

- No sync if service is not reachable

- Harder to scale

Conflict-free RDTs: CRDTs

Alternatively: Coordination-free RDT
                         I.e. RDTs that don't require centrality

Shared Control

Data

Conflict Resolution

Sync Peer-to-Peer

Sync on the Edge

Make it Available Offline

twitter.com/kevin_jahns

Made with Slides.com