Giter Site home page Giter Site logo

y-webrtc's Introduction

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.

👷‍♀️ 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. 🌟
  • Huly - Open Source All-in-One Project Management Platform :star2:
  • Cargo Site builder for designers and artists 🌟
  • Gitbook Knowledge management for technical teams 🌟
  • Evernote Note-taking app 🌟
  • Lessonspace Enterprise platform for virtual classrooms and online training 🌟
  • [Ellipsus]{ellipsus.com} - Collaborative writing app for storytelling etc. Supports versioning, change attribution, and "blame". A solution for the whole publishing process (also selling) ⭐
  • Dynaboard Build web apps collaboratively. ⭐
  • Relm A collaborative gameworld for teamwork and community. ⭐
  • Room.sh A meeting application with integrated collaborative drawing, editing, and coding tools. ⭐
  • Nimbus Note A note-taking app designed by Nimbus Web. ⭐
  • Pluxbox RadioManager A web-based app to collaboratively organize radio broadcasts. ⭐
  • modyfi - Modyfi is the design platform built for multidisciplinary designers. Design, generate, animate, and more — without switching between apps. ⭐
  • 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 applicaiton
  • Ellie.ai - Data Product Design and Collaboration
  • GoPeer - Collaborative tutoring
  • screen.garden Collaborative backend for PKM apps.

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
valtio valtio-yjs demo
immer immer-yjs demo
React / Vue / Svelte / MobX SyncedStore demo
mobx-keystone mobx-keystone-yjs 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

y-websocket
A module that contains a simple websocket backend and a websocket client that connects to that backend. y-redis, y-sweet, ypy-websocket and Hocuspocus (see below) are alternative backends to y-websocket.
y-webrtc
Propagates document updates peer-to-peer using WebRTC. The peers exchange signaling data over signaling servers. Publically available signaling servers are available. Communication over the signaling servers can be encrypted by providing a shared secret, keeping the connection information and the shared document private.
@liveblocks/yjs
Liveblocks Yjs provides a fully hosted WebSocket infrastructure and persisted data store for Yjs documents. No configuration or maintenance is required. It also features Yjs webhook events, REST API to read and update Yjs documents, and a browser DevTools extension.
y-sweet
A standalone yjs server with persistence to S3 or filesystem. They offer a cloud service as well.
Hocuspocus
A standalone extensible yjs server with sqlite persistence, webhooks, auth and more.
PartyKit
Cloud service for building multiplayer apps.
y-libp2p
Uses libp2p to propagate updates via GossipSub. Also includes a peer-sync mechanism to catch up on missed updates.
y-dat
[WIP] Write document updates efficiently to the dat network using multifeed. Each client has an append-only log of CRDT local updates (hypercore). Multifeed manages and sync hypercores and y-dat listens to changes and applies them to the Yjs document.
Matrix-CRDT
Use Matrix as an off-the-shelf backend for Yjs by using the MatrixProvider. Use Matrix as transport and storage of Yjs updates, so you can focus building your client app and Matrix can provide powerful features like Authentication, Authorization, Federation, hosting (self-hosting or SaaS) and even End-to-End Encryption (E2EE).
yrb-actioncable
An ActionCable companion for Yjs clients. There is a fitting redis extension as well.
ypy-websocket
Websocket backend, written in Python.
Tinybase
The reactive data store for local-first apps. They support multiple CRDTs and different network technologies.
y-webxdc
Provider for sharing data in webxdc chat apps.

Persistence Providers

y-indexeddb
Efficiently persists document updates to the browsers indexeddb database. The document is immediately available and only diffs need to be synced through the network provider.
y-mongodb-provider
Adds persistent storage to a server with MongoDB. Can be used with the y-websocket provider.
@toeverything/y-indexeddb
Like y-indexeddb, but with sub-documents support and fully TypeScript.
y-fire
A database and connection provider for Yjs based on Firestore.

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

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.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()
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>
Retrieve a range of content
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>
toArray():Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>
Copies the content of this YArray to a new Array.
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
Total number of key/value pairs.
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
get(index:number)
clear()
Removes all elements from this YMap.
clone():Y.Map
Clone this type into a fresh Yjs type.
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>)
Assign formatting attributes to a range in the text
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
Transforms this type, without formatting options, into a string.
toJSON():string
See 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>
Retrieve a range of content
length:number
clone():Y.XmlFragment
Clone this type into a fresh Yjs type.
toArray():Array<Y.XmlElement|Y.XmlText>
Copies the children to a new Array.
toDOM():DocumentFragment
Transforms this type and all children to new DOM elements.
toString():string
Get the XML serialization of all descendants.
toJSON():string
See toString.
createTreeWalker(filter: function(AbstractType<any>):boolean):Iterable
Create an Iterable that walks through the children.
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
Retrieve the i-th element.
slice(start:number, end:number):Array<Y.XmlElement|Y.XmlText>
Retrieve a range of content
clone():Y.XmlElement
Clone this type into a fresh Yjs type.
toArray():Array<Y.XmlElement|Y.XmlText>
Copies the children to a new Array.
toDOM():Element
Transforms this type and all children to a new DOM element.
toString():string
Get the XML serialization of all descendants.
toJSON():string
See 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.

Y.Doc

const doc = new Y.Doc()
clientID
A unique id that identifies this client. (readonly)
gc
Whether garbage collection is enabled on this doc instance. Set `doc.gc = false` 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])
Every change on the shared document happens in a transaction. Observer calls and the update event are called after each transaction. You should bundle changes into a single transaction to reduce the amount of event calls. I.e. doc.transact(() => { yarray.insert(..); ymap.set(..) }) triggers a single change event.
You can specify an optional origin parameter that is stored on transaction.origin and on('update', (update, origin) => ..).
toJSON():any
Deprecated: It is recommended to call toJSON directly on the shared types. Converts the entire document into a js object, recursively traversing each yjs type. Doesn't log types that have not been defined (using ydoc.getType(..)).
get(string, Y.[TypeClass]):[Type]
Define a shared type.
getArray(string):Y.Array
Define a shared Y.Array type. Is equivalent to y.get(string, Y.Array).
getMap(string):Y.Map
Define a shared Y.Map type. Is equivalent to y.get(string, Y.Map).
getText(string):Y.Text
Define a shared Y.Text type. Is equivalent to y.get(string, Y.Text).
getXmlElement(string, string):Y.XmlElement
Define a shared Y.XmlElement type. Is equivalent to y.get(string, Y.XmlElement).
getXmlFragment(string):Y.XmlFragment
Define a shared Y.XmlFragment type. Is equivalent to y.get(string, Y.XmlFragment).
on(string, function)
Register an event listener on the shared type
off(string, function)
Unregister an event listener from the shared type

Y.Doc Events

on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void)
Listen to document updates. Document updates must be transmitted to all other peers. You can apply document updates in any order and multiple times. Use `updateV2` to receive V2 events.
on('beforeTransaction', function(Y.Transaction, Y.Doc):void)
Emitted before each transaction.
on('afterTransaction', function(Y.Transaction, Y.Doc):void)
Emitted after each transaction.
on('beforeAllTransactions', function(Y.Doc):void)
Transactions can be nested (e.g. when an event within a transaction calls another transaction). Emitted before the first transaction.
on('afterAllTransactions', function(Y.Doc, Array<Y.Transaction>):void)
Emitted after the last transaction is cleaned up.

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

Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])
Apply a document update on the shared document. Optionally you can specify transactionOrigin that will be stored on transaction.origin and ydoc.on('update', (update, origin) => ..).
Y.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8Array
Encode the document state as a single update message that can be applied on the remote document. Optionally specify the target state vector to only write the differences to the update message.
Y.encodeStateVector(Y.Doc):Uint8Array
Computes the state vector and encodes it into an Uint8Array.
Y.mergeUpdates(Array<Uint8Array>)
Merge several document updates into a single document update while removing duplicate information. The merged document update is always smaller than the separate updates because of the compressed encoding.
Y.encodeStateVectorFromUpdate(Uint8Array): Uint8Array
Computes the state vector from a document update and encodes it into an Uint8Array.
Y.diffUpdate(update: Uint8Array, stateVector: Uint8Array): Uint8Array
Encode the missing differences to another update message. This function works similarly to Y.encodeStateAsUpdate(ydoc, stateVector) but works on updates instead.
convertUpdateFormatV1ToV2
Convert V1 update format to the V2 update format.
convertUpdateFormatV2ToV1
Convert V2 update format to the V1 update format.

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.createRelativePositionFromTypeIndex(type:Uint8Array|Y.Type, index: number [, assoc=0])
Create a relative position fixated to the i-th element in any sequence-like shared type (if assoc >= 0). By default, the position associates with the character that comes after the specified index position. If assoc < 0, then the relative position associates with the character before the specified index position.
Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc): { type: Y.AbstractType, index: number, assoc: number } | null
Create an absolute position from a relative position. If the relative position cannot be referenced, or the type is deleted, then the result is null.
Y.encodeRelativePosition(RelativePosition):Uint8Array
Encode a relative position to an Uint8Array. Binary data is the preferred encoding format for document updates. If you prefer JSON encoding, you can simply JSON.stringify / JSON.parse the relative position instead.
Y.decodeRelativePosition(Uint8Array):RelativePosition
Decode a binary-encoded relative position to a RelativePositon object.

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'
constructor(scope:Y.AbstractType|Array<Y.AbstractType> [, {captureTimeout:number,trackedOrigins:Set<any>,deleteFilter:function(item):boolean}])
Accepts either single type as scope or an array of types.
undo()
redo()
stopCapturing()
on('stack-item-added', { stackItem: { meta: Map<any,any> }, type: 'undo' | 'redo' })
Register an event that is called when a StackItem is added to the undo- or the redo-stack.
on('stack-item-updated', { stackItem: { meta: Map<any,any> }, type: 'undo' | 'redo' })
Register an event that is called when an existing StackItem is updated. This happens when two changes happen within a "captureInterval".
on('stack-item-popped', { stackItem: { meta: Map<any,any> }, type: 'undo' | 'redo' })
Register an event that is called when a StackItem is popped from the undo- or the redo-stack.
on('stack-cleared', { undoStackCleared: boolean, redoStackCleared: boolean })
Register an event that is called when the undo- and/or the redo-stack is cleared.

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 benificial 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 preceeding 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.

y-webrtc's People

Contributors

akarachen avatar asmodehn avatar clemos avatar croatialu avatar dependabot[bot] avatar dextertanyj avatar disarticulate avatar dmonad avatar exuanbo avatar feyebrow avatar flamenco avatar liquidibrium avatar rhaldkhein avatar stephane-klein avatar yousefed avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

y-webrtc's Issues

Problem with firefox and y-webrtc

Describe the bug
I am currently building a collab whiteboard using SyncedStore (wrapper for yjs) and Sveltekit. So far everything works normally in Chrome. I use the y-webrtc provider for the SyncedStore and the connection is established successfully. However, if I use Firefox initially, no connection to the room is established. If I use a Chrome in parallel (Firefox next door still open in the non-synced tab) and establish the connection, Firefox also establishes the connection in parallel and synchronizes the document. I have attached a video below, which shows the exact procedure.

Expected behavior
Firefox connects to the y-webrtc provider just like Chrome when you open the page and synchronizes the document.

Environment Information

  • Firefox 116.0.3
  • Chrome 116.0.5845.111

Additional context

import { syncedStore, getYjsDoc } from '@syncedstore/core';
import { svelteSyncedStore } from '@syncedstore/svelte';
import { env } from '$env/dynamic/public';
import type { RetroBoard } from '$lib/types';
import { WebrtcProvider } from 'y-webrtc';

export const store = syncedStore({
	board: {} as RetroBoard
});
export const svelteStore = svelteSyncedStore(store);

const doc = getYjsDoc(store);

const provider = new WebrtcProvider('retrorealm-room', doc, {
	signaling: [env.PUBLIC_SIGNALING_SERVER],
	filterBcConns: false
});

export const awareness = provider.awareness;
webrtc_firefox_bug.mp4

public signaling servers down?

Hallo,

My app cannot connect to ['wss://signaling.yjs.dev', 'wss://y-webrtc-signaling-eu.herokuapp.com', 'wss://y-webrtc-signaling-us.herokuapp.com']
image

A few weeks back this wasn't the case.

Code used:

import { WebrtcProvider } from "y-webrtc";
import { Doc } from "yjs";

const doc = new Doc();

export const webrtcProvider = new WebrtcProvider(
  "vZ#4h2%60$1H",
  doc,
  { password: "wMrg3@4WVf^q@iK6xukh@86&fCY5GmvX" }
);

export const disconnect = () => webrtcProvider.disconnect();
export const connect = () => webrtcProvider.connect();

// Get the provider's awareness API
export const awareness = webrtcProvider.awareness;

import { Avatar, Divider, Grid, Stack, Tooltip } from "@mui/material";
import ActionDialog from "@src/components/ActionDialog";
import { MIconButton } from "@src/components/minimals/@material-extend";
import useAuth, { User } from "@src/hooks/useAuth";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router";
import { useUsers } from "y-presence";

export type YUser = User & { name: string, orderId: number | null };
export type UseUsers = Map<number, YUser>;
import { awareness } from "../syncedStores/orderSyncedStore";

export default function SyncedUsers({ order_id }: { order_id: number }) {
  const { t: dashboardT } = useTranslation("dashboard");
  const { user } = useAuth();
  const {id} = useParams()
  console.log("synced",{id})

  useEffect(() => {
    if (user && id) {
      const yUser: YUser = {
        name: `${user.first_name} ${user.last_name}`,
        ...user,
        orderId : Number(id),
      };
      awareness.setLocalState(yUser);
    }
  }, [id, user]);

  const users = useUsers(awareness) as UseUsers;
  const orderUsers = Array.from(users.entries()).filter(
    ([_, u]) => u.orderId !== order_id && u.id !== user?.id
  );
  
  return (
    <>
      <Stack gap={2} direction={"row"} ml={"auto"}>
        {orderUsers.map(([_, value]) => {
          return <YUserAvatar key={value.id} user={value} />;
        })}
      </Stack>
      {orderUsers.length >= 1 && (
        <ActionDialog title={dashboardT("warning")} open={true}>
          {dashboardT("there_are_other_users_in_this_order")}
        </ActionDialog>
      )}
    </>
  );
}

export function YUserAvatar({ user }: { user: YUser }) {
  const { t: commonT } = useTranslation("common");
  return (
    <ActionDialog
      title={`${user.name}`}
      openButton={(setOpen) => (
        <Tooltip title={user.name}>
          <MIconButton
            onClick={() => setOpen(true)}
            sx={{
              padding: 0,
              width: 44,
              height: 44,
            }}
          >
            <Avatar
              alt="User avatar"
              sx={{
                mx: "auto",
                bgcolor: "primary.main",
                color: "text.primary",
              }}
            >
              {user.first_name.slice(0, 1)}
              {user.last_name.slice(0, 1)}
            </Avatar>
          </MIconButton>
        </Tooltip>
      )}
    >
      <Grid container>
        <Grid item xs={3}>
          Email
        </Grid>
        <Grid item xs={9}>
          {user.email}
        </Grid>
        <Divider />
        <Grid item xs={3}>
          Rol
        </Grid>
        <Grid item xs={9}>
          {commonT(user.role)}
        </Grid>
        <Divider />
      </Grid>
    </ActionDialog>
  );
}

I would like to deploy my own signaling server but find the tuturial to be unclear. Could anyone help me with this?

Allow custom signalling classes

I have a websocket connection that expects a JSON-RPC envelope (Holochain). To support this I need to wrap the message that is sent over the websocket in the SignalingConn class. It would be great if the class would be defined as a constructor argument so it would not be needed to override the WebrtcProvider.connect(). Alternatively the SignalingConn class could be returned by a getSignalingConnectionClass() hook that can be overrided more easily.

const signalingConn = map.setIfUndefined(signalingConns, url, () => new SignalingConn(url))

The editor content does not sync on different browsers

Please save me some time and use the following template. In 90% of all issues I can't reproduce the problem because I don't know what exactly you are doing, in which environment, or which y-* version is responsible. Just use the following template even if you think the problem is obvious.

Checklist

Describe the bug
The editor content is sync across the tabs of the same browsers.
But it does not sync with different browsers.

To Reproduce
Steps to reproduce the behavior:

  1. Go to https://codesandbox.io/s/github/tiavina-mika/tiptap-editor-mui
  2. Open the page in another tabs
  3. Write something
  4. The content is shared accross the tabs
  5. Open the page in another browser
  6. The content is not the same

Expected behavior
The editor content should be the same on different browsers.

Environment Information

  • Browser [e.g. Chrome, Firefox]
  • Yjs version: yjs v13.6.10. y-webrtc: v10.2.6

TypeError: Assignment to constant variable

I was building my Angular application for production use where I ran into this weird issue (after some research, it turns out it isn't uncommon for production builds to have, what appears to be, breaking differences for both React and Angular - I suspect the same may apply for other frameworks).

It complains about the _docUpdateHandler function saying it is assigning something to a constant variable.

const encoder = encoding.createEncoder()

The only const I could see defined was on line 355 so I changed this to a let and the issue went away. This seems odd since this encoder variable is only being passed through to those other preceding functions.

It might be something worth looking into. Since I'm submitting my work, I'm making these hacky edits - limited by time to investigate this any further but thought I'd raise the issue if there is one that I'm not seeing :D

this.getStateVector is not a function

Hello,

I am currently trying to use yjs, the y-xmpp connector looks like it's working fine but when I try to use the same code for the WebRTC implementation it crashes.

The error appears when I execute the following code on more than one browser:

var options = {}
var connector = new Y.WebRTC("hello", options);
var y = new Y(connector)

If I run this piece of code on a single browser no error is prompted, while if I run it on two or more browsers it return the this.getStateVector is not a function error.
It looks like that the error appears when I try to create the y object while the connector has already some connection.

How to add twilo turn server in y-webrtc codemirror? Default sigalling server does not work for different network.

Checklist

[ ] Are you reporting a bug? Use github issues for bug reports and feature requests. For general questions, please use https://discuss.yjs.dev/
[ ] Try to report your issue in the correct repository. Yjs consists of many modules. When in doubt, report it to https://github.com/yjs/yjs/issues/

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

Describe the solution you'd like
A clear and concise description of what you want to happen.

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Add any other context or screenshots about the feature request here.

Library fails with missing global declarations when run inside nodejs (ES6)

I'm attempting to run a Webrtc client within browsers and nodejs.

The browser build seems to work well. The nodejs (ES6 modules) side is having some problems.

Here is some sample code that triggers the first error from nodejs (in a file named, demo.js).

import {WebrtcProvider} from 'y-webrtc';
import * as Y from 'yjs';

const id = 1;
const config = {
  signaling: ['ws://localhost:4444'],
  maxConns: 50 + Math.floor(Math.random() * 15),
  peerOpts: {},
};

const doc = new Y.Doc();
const webrtc = new WebrtcProvider(id, doc, config);

Running this with node demo.js fails with the following error:

file:///[proj]/node_modules/lib0/websocket.js:25
    const websocket = new WebSocket(wsclient.url)
                      ^

ReferenceError: WebSocket is not defined
    at setupWS (file:///[proj]/node_modules/lib0/websocket.js:25:23)
    at new WebsocketClient (file:///[proj]/node_modules/lib0/websocket.js:122:5)
    at new SignalingConn (file:///[[proj]/node_modules/y-webrtc/src/y-webrtc.js:473:5)
    at file:///[proj]/node_modules/y-webrtc/src/y-webrtc.js:610:75
    at Module.setIfUndefined (file:///[proj]/node_modules/lib0/map.js:49:24)
    at file:///[proj]/node_modules/y-webrtc/src/y-webrtc.js:610:33
    at Array.forEach (<anonymous>)
    at WebrtcProvider.connect (file:///[proj]/node_modules/y-webrtc/src/y-webrtc.js:609:24)
    at new WebrtcProvider (file:///[proj]/node_modules/y-webrtc/src/y-webrtc.js:595:10)
    at file:///proj]/demo.js:19:16

I was able to work around this issue by adding the following to the top of my application entry point (above any imports).

import WebSocket from 'ws';
Object.assign(global, {WebSocket});

This allows the service to start, but whenever a connection is made from a separate browser client, the following error is thrown:

Exit with: Error: Secure random number generation is not supported by this browser.
Use Chrome, Firefox or Internet Explorer 11
    at t.exports (/[proj]/node_modules/simple-peer/simplepeer.min.js:6:37193)
    at new p (/[proj]/node_modules/simple-peer/simplepeer.min.js:6:79527)
    at new WebrtcConn (file:///[proj]/node_modules/y-webrtc/src/y-webrtc.js:183:17)
    at file:///[proj]/teak/node_modules/y-webrtc/src/y-webrtc.js:513:68
    at Module.setIfUndefined (file:///proj]/node_modules/lib0/map.js:49:24)
    at execMessage (file:///[proj]/node_modules/y-webrtc/src/y-webrtc.js:513:23)
    at file:///[proj]/node_modules/y-webrtc/src/y-webrtc.js:530:13
    at file:///[proj]/node_modules/lib0/observable.js:73:90
    at Array.forEach (<anonymous>)
    at SignalingConn.emit (file:///[proj]/node_modules/lib0/observable.js:73:77)

After digging a little bit, it looks like y-webrtc is loading a pre-minified build of simple-peer that may have been built for the browser and not nodejs.

I was unable to get a similar global hack working for this issue, and instead forked the y-webrtc repo into a vendor folder, loaded as a git submodule and referred to that version from my node application and the npm published version from my browser client code.

I also had to add the following to my imports and update the peerOpts object:

import wrtc from 'wrtc';

const config = {
  signaling: ['ws://localhost:4444'],
  maxConns: 50 + Math.floor(Math.random() * 15),
  peerOpts: {
    wrtc,
  },
};

Unfortunately, wrtc requires node-pre-gyp and node-gyp, which seem to be in a weird state at the moment, so I had to install those both globally, delete my node_modules folder and reinstall local modules in order to get the thing to work.

Expected behavior
The library should work for either node or browser clients

Environment Information

  • node --version: v16.14.2
  • Node in ES6 module mode (e.g., package.json includes "type": "module" entry).
  • uname -a: Linux beefcake 5.13.0-39-generic #44~20.04.1-Ubuntu SMP Thu Mar 24 16:43:35 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
  • y-webrtc: 10.2.3
$ npm ls yjs
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
└── [email protected]

Additional context
FWIW, here's the commit that worked around the issue.

FWIW 2, I also tried pointing into the unminified build of simple-peer, but that did not want to build successfully for the browser (under esbuild with es6 modules) as there were nodejs modules being required in that dependency.

I understand that these issues probably aren't going to be trivial to solve from y-webrtc, so thought I'd post here in case others have the same problems, at least we can discuss workarounds.

Emit events when the connected state changes

Is your feature request related to a problem? Please describe.

I'd like a 'connect' and 'close' events to be emitted after y-webrtc has connected/disconnected so that I can show a connection status badge without the need to poll every so often the 'connected' variable in the provider

Silencing "Unable to compute message"

In my app I'm using the simple-peer method peer.send(mymessage) to get around some of the overhead(?) involved in the yjs CRDT: i.e., to broadcast cursor position. However, because of the switch statement in this library, unknown types of messages throw an error:

console.error('Unable to compute message')

Could you add a way to silence this error or otherwise allow custom messages to be handled?

emit 'sync'

Checklist

[x] Are you reporting a bug? Use github issues for bug reports and feature requests. For general questions, please use https://discuss.yjs.dev/
[x] Try to report your issue in the correct repository. Yjs consists of many modules. When in doubt, report it to https://github.com/yjs/yjs/issues/

Is your feature request related to a problem? Please describe.

When implementing a Lexical editor with the Collaborative plugin, it expects the provider to emit 'sync' so that it can initialize the state, just like y-websocket

Describe the solution you'd like

When it's clear that everything is synced, emit 'sync'

Describe alternatives you've considered

I did setTimeout(() => provider.emit('sync', [true]), 0) for now ;-)

API modifications proposal

This is a companion issue for features proposal to yrs-webscoket (y-websocket compatible Rust library).

  1. Support for passing messages of any size: right now WebRTC has a limitation over the max message size. This limit can be easier reached with bigger documents - esp. 16KiB limit for Firefox ↔ Chrome. While this should be solved at a transport level, in practice the issue stays and somebody has to fix it. We could add message chunking/assembling at y-sync protocol level ie. by using y-sync protocol extensions capabilities. Also add configuration option to message chunk size (make it 16 KiB by default for seamless support between all browsers).
  2. Support multiplexing single connection over multiple documents: right now the limitation of y-webrtc WebRTCProvider is that it requires a unique connection per document. If users have multiple active documents, they need multiple active connections. WebRTC support having multiple non-blocking data channels over a single connection and we should support this as well. Having extra data channels doesn't cost anything. The only limitation of the WebRTC protocol itself is that AFAIK all data channels must be defined up-front and they cannot be added/removed while connection is active.
  3. Support alternatives for signalling server: currently to exchange signalling info we need a hosted WebSocket server. It would be great if we could abstract that part to allow alternative options like ie. Apple's Bonjour discovery protocol. EDIT: after checking for available options, it seems that currently browsers don't support bonjour/zeroconf, the only option would be to run signalling server over ie. bluetooth, which doesn't seem to be very useful atm. for the scope of this library.

Since y-webrtc is build on top of simple-peer, some of these changes should be probably implemented over there. We might potentially contribute to that project, fork it if necessary or thing about alternatives.

Over-strict type declaration for the options of WebRtcProvider

Please save me some time and use the following template. In 90% of all issues I can't reproduce the problem because I don't know what exactly you are doing, in which environment, or which y-* version is responsible. Just use the following template even if you think the problem is obvious.

Checklist

Describe the bug

I'm using TypeScript, and want to pass the URL for a local signaling server to the WebRtcProvider. My code has:

const provider = new WebrtcProvider('testApp', ydoc, { signaling: ['wss://localhost:4444'] })

This produces an error from the TypeScript compiler, because the elements of the peerOpts structure are not marked as optional:

ERROR in src/views/YjsTest.vue:12:57
TS2345: Argument of type '{ signaling: string[]; }' is not assignable to parameter of type 
'{ signaling: string[]; password: string | null; awareness: Awareness; maxConns: number; filterBcConns: 
boolean; peerOpts: any; }'.
  Type '{ signaling: string[]; }' is missing the following properties from type '{ signaling: string[]; 
password: string | null; awareness: Awareness; maxConns: number; filterBcConns: boolean; peerOpts: any; }': 
password, awareness, maxConns, filterBcConns, peerOpts
    10 |
    11 |     const ydoc = new Y.Doc()
  > 12 |     const provider = new WebrtcProvider('dystil', ydoc, { signaling: ['http://localhost:4444'] })
       |                                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

To Reproduce
Steps to reproduce the behavior:

  1. Create a skeleton app that creates a WebRtcCompiler
  2. pass the URL of a non-default signalling server
  3. compile with TypeScript
  4. See error

Expected behavior

It should be possible to pass only a subset of the elements of the options, with the non-specified options falling back to their default values

Screenshots

Not applicable

Environment Information

  • Typescript 4.4.4
  •   "y-webrtc": "^10.2.0",
      "yjs": "^13.5.16"
    

Additional context
Add any other context about the problem here.

Use existing (3rd party) signaling server implementation

Checklist

Is your feature request related to a problem? Please describe.
Currently y-webrtc uses a custom server for signaling. I think this is not ideal because it's a comparatively niche thing. Sometimes public servers go down (#43), creating a big headache for app developers that now have to set up a new server (that might go down as well). Having a custom server also adds maintenance costs.

Describe the solution you'd like
Rely on another existing server that can be used for signaling. For example, https://github.com/peers/peerjs-server. Or, I heard, you can use https://github.com/centrifugal/centrifugo for signaling (there is a concept of rooms (channels) as well).

Describe alternatives you've considered

None

Additional context

I'm not 100% sure this is a good idea, and I didn't look into details of how the current signaling server works, or how Centrifugo works. It's just a high-level idea that I think it makes sense to strive for.

Node lock version

Trying to install this and getting an error that it requires node version 10. Downgrade to v10 and it complains about
error [email protected]: The engine "node" is incompatible with this module. Expected version ">=12". Got "10.19.0"

Allow for or extend checking connectedness of the provider

Is your feature request related to a problem? Please describe.
I want to properly check the connectedness status of the provider, I am unsure if the connected flag of the WebrtcProvider is properly representative of this status, as it is true if the room is created, but not if any networking has been done.

Describe the solution you'd like
One of the following come to mind:

  • The connection being properly represented as a flag so I can check it with e.g. promise.until from lib0
  • Connection events to resolve new promises
  • An async function in the provider connectedAsync that resolves if connected at call time or when the connection is established

Describe alternatives you've considered
Checking the connected flag, but as stated earlier, I am not sure if this is fully representative of what I would expect.

Additional context
I am not certain if a connection status can generally be determined in this configuration and if I am asking for a feature that cannot be implemented.

Use Authentication for allowing/blocking access

I like the signaling server you provide at https://github.com/yjs/y-webrtc/blob/master/bin/server.js, yet I think it makes sense for me to provide it with some authorization depending on a provided JWT token within the request. I think I know that I would have to provide such a thing at https://github.com/yjs/y-webrtc/blob/master/bin/server.js#L127. However, I do not know when I call

const provider = new WebrtcProvider('your-room-name', ydoc, { password: 'optional-room-password' })

or so, where I can send the JWT token in the request.

It would be great if you could help me here.

Reactive "connected" property

Checklist

[X] Are you reporting a bug? Use github issues for bug reports and feature requests. For general questions, please use https://discuss.yjs.dev/
[X] Try to report your issue in the correct repository. Yjs consists of many modules. When in doubt, report it to https://github.com/yjs/yjs/issues/

Is your feature request related to a problem? Please describe.
No

Describe the solution you'd like
On my GUI I'm showing a "connected"/"disconnected" icon. However, the connected property is not reactive.
It would be nice to expose the connected property as an event, or make it otherwise reactive.
I see that an "Observable" class is used, I am not familiar with it. Could it be that SyncedStore already makes this reactive?

Describe alternatives you've considered
I have no (good) alternative ideas.

Additional context
Related:
https://discuss.yjs.dev/t/vue3-syncedstore-reactivity/1683

Why keys for webrtcPeers?

Following the code in this PR's comment #6 (comment) doesn't work. provider.on("peers", (e) => sends out an event with no access to the simple-peer objects (webrtcPeers is a list of strings). I have to instead do provider.room.webrtcConns.get(peerId).peer.

It's because of this line:

webrtcPeers: Array.from(room.webrtcConns.keys()),

Error when updating text field with long string

Checklist

Describe the bug
I'm adding a long string (~300K characters) to a Y.Text type, which causes webrtc errors to show (ERR_SET_REMOTE_DESCRIPTION) on the other clients. The text is never synced. Interestingly, it's possible to add an even longer string by adding it in smaller increments.

I'm not sure if there is an inherent limit to updates when using y-webrtc, I couldn't find anything documented, so I'm filing this as a bug.

Bonus question: is there some way to catch these types of errors so I can display an error message? This is a contrived example but I'm actually running into a variation of this error in the application I'm building.

To Reproduce
Steps to reproduce the behavior:

  1. Go to https://codesandbox.io/s/cranky-framework-qmsorf?file=/src/index.js
  2. Open the sandbox again on another machine or another browser
  3. In first window, Click on "Add long string"
  4. Open console of the other window
  5. See error, and observe that the string is not synced.

As mentioned, it's easily possible to sync a string that is longer than 500k chars by repeatedly using the "Add short string" button, but adding the long string at once seems to cause problems.

Expected behavior
The string should be synced and no error thrown.

Environment Information
Happens in Chrome and FF

  • yjs 13.5.45
  • y-webrtc 10.2.4

filterBcConns = true doesn't work

Using filterBcConns: true in options still uses broadcast channels instead of a webrtc connection for tabs in the same browser. Tested on Chrome: Version 79.0.3945.79 (Official Build) (64-bit) and Firefox: 74.0 (64-bit) on Ubuntu 18.04

add STUN/TURN servers after the provider is made

services like metered.ca require using an API to get the STUN and TURN servers. the problem is that waiting for the API to respond adds unnecessary latency to syncronizing with other peers which might not need TURN.

being able to add servers after it has been created solves this.

y-webrtc doesn't work on different browsers

Content doesn't sync between different browser, or on the same browser when using the incognito mode.

I guess I am missing some configuration or something.

Any help would be appreciated.

    // frontend code...
    const doc = new Y.Doc();
    doc.clientID = randomInt(0, 100);

    const provider = new WebrtcProvider(room, doc, {
      signaling: ["ws://localhost:4444"],
    });
    const type = doc.getText("monaco");
    const awareness = provider.awareness;

    const binding = new MonacoBinding(
      type,
      editorRef.current.getModel(),
      new Set([editorRef.current]),
      awareness
    );

signaling server - https://github.com/yjs/y-webrtc/blob/master/bin/server.js

'peers' event on on SignalingConn emits 'added' peer from peer's close event

Checklist

Describe the bug

class WebrtcConn {
   ...
   peer.on('close' => ... announceSignalingInfo(room))
}

generates an announcement which triggers remote peers:

class SignalingConn {
             ...
             case 'publish':
             ...
             room.provider.emit('peers', [{
                removed: [],
                added: [data.from],
                webrtcPeers: Array.from(room.webrtcConns.keys()),
                bcPeers: Array.from(room.bcConns)
              }])

message peers then says this peer was added which is not what's happening, as peer is closing.

To Reproduce
Steps to reproduce the behavior:

Open two browsers with peers connected. Listen to room.provider.peers for 'peers' and you'll see the event.removed shows the peerId, but then another events.added shows the peer added again

Expected behavior
When a peer closes, it should not generate a peers event that has the peer added.

Screenshots
debug:

removed:webrtc:[98ee6477-5037-49ee-bd20-b8f69928dade] WebrtcProvider {…}

y-webrtc.js?9b73:270 announceSignalingInfo Room {…} true
eval @ y-webrtc.js:294
announceSignalingInfo @ y-webrtc.js:289
eval @ y-webrtc.js:245
r.emit @ simplepeer.min.js:6
eval @ simplepeer.min.js:6
y-webrtc.js?9b73:270 announceSignalingInfo Room {…} true
eval @ y-webrtc.js:294
announceSignalingInfo @ y-webrtc.js:289
eval @ y-webrtc.js:245
r.emit @ simplepeer.min.js:6
eval @ simplepeer.min.js:6
y-webrtc.js?9b73:270 announceSignalingInfo Room {…} true
eval @ y-webrtc.js:294
announceSignalingInfo @ y-webrtc.js:289
eval @ y-webrtc.js:245
r.emit @ simplepeer.min.js:6
eval @ simplepeer.min.js:6
y-webrtc.js?9b73:491 {to: "abb23ec9-66aa-4cd3-8f4c-9f9e7c58bc83", from: "98ee6477-5037-49ee-bd20-b8f69928dade", type: "signal", signal: {…}} "98ee6477-5037-49ee-bd20-b8f69928dade"
emitPeerChange @ y-webrtc.js:515
execMessage @ y-webrtc.js:532
Promise.then (async)
eval @ y-webrtc.js:539
eval @ observable.js:78
emit @ observable.js:78
websocket.onmessage @ websocket.js:50
SyncPeers.js?a801:234

added:webrtc:[98ee6477-5037-49ee-bd20-b8f69928dade] 

Environment Information
Version 89.0.4389.82 (Official Build) snap (64-bit)
"y-webrtc": "^10.1.7"
"y-protocols": "^1.0.4",
"yjs": "^13.5.2"

Error: A Yjs Doc connected to room "#room-name" already exists

Describe the bug
I'm getting an error when reconnecting and calling new WebrtcProvider() on a page refresh or after a disconnect.

To Reproduce
Steps to reproduce the behavior:

  const provider = new WebrtcProvider(name, doc, {
    password,
    signaling: servers,
    awareness,
  });

Then call provider.disconnect() or provider.destroy(), or just try connecting again with the same code and params as above.

  1. Get error: Error: A Yjs Doc connected to room "#room-name" already exists

Expected behavior
A new provider can be created and sync to the existing room without error. Should return the provider.

Environment Information

  • Happens both in Browser [Chrome] and Node [Vitest]
  • "y-webrtc": "^10.2.5"
  • "yjs": "^13.5.16"

Screenshot 2023-05-06 at 18 16 48

Screenshot 2023-05-06 at 18 16 13

Screenshot 2023-05-06 at 18 15 51

Sorry not sure why sourcemaps aren't working, but I've found where it originates:
// src/y-webrtc.js
const openRoom = (doc, provider, name, key) => {
  // there must only be one room
  if (rooms.has(name)) {
    throw error.create(`A Yjs Doc connected to room "${name}" already exists!`)
  }
  const room = new Room(doc, provider, name, key)
  rooms.set(name, /** @type {Room} */ (room))
  return room
}

Explore options for smaller footprint, browser-based webrtc clients

After installing the y-webrtc package, my unminified JavaScript bundle went from 219.7kb to 417.6kb (197.9kb difference) and my minified bundle went from 90kb to 202kb.

I was naturally curious, so I found BundleBuddy and here's the report they generated.
image

It looks like simple-peer represents the lion's share of the size increase (96kb minified) and a quick glance at the source file indicates there are quite a few nodejs modules being pulled into what is ostensibly meant to run in a browser.

I haven't dug deep into this yet, but thought I'd check in to see if anyone is already looking into ideas here.

Overall outstanding library and thanks so much for all your hard work!

TypeError: Cannot read property 'encrypt' of undefined

Describe the bug
I was trying to disconnect and connect the room but encounter an error

TypeError: Cannot read property 'encrypt' of undefined
Module.encrypt

  49 |   return /** @type {PromiseLike<Uint8Array>} */ (promise.resolve(data))
  50 | }
  51 | const iv = crypto.getRandomValues(new Uint8Array(12))
> 52 | return crypto.subtle.encrypt(
     | ^  53 |   {
  54 |     name: 'AES-GCM',
  55 |     iv

To Reproduce
Steps to reproduce the behavior:

  1. attempts to disconnect room
  2. attempts to destroy room
  3. Error produced

Expected behavior
It should properly destroy the room

Environment Information

  • Chrome
  • yjs v13.5.5, y-webrtc v10.2.0

List of signaling servers

It looks like the default one is currently down. It would be nice to provide a small list somewhere (wiki?) to allow developpers to set it up quickly :)

For example, I tried this:

var connector = new Y.WebRTC("leaflet-yjs", {url: "https://signaling.simplewebrtc.com"})

with no luck :/

Implement a peer to peer mesh topology/routing

Is your feature request related to a problem? Please describe.
Multiple webrtc connections create unnecessary load with direct connect to all users. Implement a mesh network topology.

Prior art: https://github.com/lazorfuzz/liowebrtc

Describe the solution you'd like
As seen above, past a certain threshold, implement something like a Kademlia hash table for peer-to-peer mesh network.

Useful example/potential dependency: https://github.com/jeanlauliac/kademlia-dht

Describe alternatives you've considered
I used liowebrtc initially, but the complicated handshakes and necessary wiring made it burdensome. Also, it's just another dependency that duplicates what this library seems intent on doing: improving peer to peer use of yjs

Additional context
It may make sense to do this alonside the potential fixing of #20 as it would require some heavy lifting and additional lifetime maintenance

Issue when peer got reconnected after disconnected by using webrtc provider

Describe the bug
I’m currently trying to run two instances of this demo (but changed the provider to webrtc) on my local (using two different port). When one instance (user1) got disconnected and then got reconnected again, at first both content from user1 and user will get synced again.

But when user1 tries to add new content, user2 will not receive the update from user1 anymore, but update user2 will still be received by user1 (the updates will become one-direction only).

But this issue didn’t occur when I tried this by using websocket provider. Is there some extra configuration needed to make sure y-webrtc can handle this scenario? or is this a bug on y-webrtc? please let me know if I’m missing something or if you need more information.

To Reproduce

  1. Clone this example, change the provider from websocket to webrtc, like this:
  // const provider = new WebsocketProvider('wss://demos.yjs.dev', 'prosemirror-demo', ydoc) #commented
  const provider = new WebrtcProvider('demo-ywebrtc', ydoc);
  1. Run two instances of the example on your local
  2. Make one of the instances offline
  3. Make that instance online again
  4. Now changes from that instance won't get synced with other instances anymore, but changes from other instances will still get synced with that instance.

Expected behavior
The reconnected instance's update will get propagated to other peers as usual.

Screenshots
Not a screenshot, but I’ve created a short screencast to reproduce the issue in case my explanation is not clear enough.

Environment Information

Edit: Still got the same issue on [email protected]

Additional context
I don't know if this is relevant, but this issue is also occurring on a real connection test (using two ubuntu VM connected by VirtualBox's default NAT network config, and by making one of the VM offline).

Implment Packet buffer for sending

Most browsers currently have a limit for message size:

https://stackoverflow.com/questions/15435121/what-is-the-maximum-size-of-webrtc-data-channel-messages

My testing on chrome gets the following error:
Attempting to send message of size 988606 which is larger than limit 262144

Although the spec is expected to be built into browsers, these arbitrary size limits result in no error message that I see in the console. The above comes from running debug version of chrome.

When I tried to create my own 'sync' system before switching to try y-webrtc, I used protocol buffers to wrap updates, and hashed the data to keep them order/organized. I don't have any real knowledge about best practices, however.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.