Coder Social home page Coder Social logo

y-websocket'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-websocket's People

Contributors

abdel-monaam-aouini avatar andrew-d-jackson avatar andrewtrinttest avatar aryzing avatar broofa avatar canadaduane avatar chenmins avatar dependabot[bot] avatar dmonad avatar ellisonbg avatar fubhy avatar hanspagel avatar himself65 avatar infomiho avatar istvank avatar jayaddison avatar jblyberg avatar karthikraju391 avatar kkeybbs avatar lakhoune avatar lijie1129 avatar mansehej avatar nemanja-tosic avatar strdr4605 avatar tommoor avatar war1644 avatar zhuangya 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  avatar  avatar  avatar

y-websocket's Issues

initUser: Cannot read property 'NaN' of undefined

Checklist

Describe the bug

Exception TypeError: Cannot read property 'NaN' of undefined in the JS console (see screenshots).

To Reproduce

After including setUserMapping on the client, like so:

const userData = new Y.PermanentUserData(ydoc);
userData.setUserMapping(ydoc, ydoc.clientID, user.id);

provider = new WebsocketProvider(
  "ws://localhost:1234",
  roomname,
  ydoc,
  {
    resyncInterval: 10 * 1000,
  }
);

I am regularly receiving this error in the console – I believe it's when rejoining a previously joined websocket. I can't find documentation for PermanentUserData, so could be using it wrong somehow? If you need additional context let me know what I can grab, it's happening enough that I can reproduce on demand.

Screenshots

image

image

Environment Information

Huly®: YJS-488

Read-only mode

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.
I'm building a collaborative application https://arrowbox.co and I want people to share their documents in read-only mode

Describe the solution you'd like
Some way to open a WS connection in read-only mode. While still allowing write connections from different clients

Describe alternatives you've considered
Tried this solution https://github.com/stoplightio/y-websocket/pull/1/files but it failed with

const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder);
                                                                           ^

TypeError: contentRefs[(info & binary.BITS5)] is not a function
    at readItemContent (file:///Users/adelnizamutdinov/Projects/diagrams/node_modules/yjs/dist/yjs.mjs:9426:76)
    at readClientsStructRefs (file:///Users/adelnizamutdinov/Projects/diagrams/node_modules/yjs/dist/yjs.mjs:1308:13)
    at file:///Users/adelnizamutdinov/Projects/diagrams/node_modules/yjs/dist/yjs.mjs:1535:16
    at transact (file:///Users/adelnizamutdinov/Projects/diagrams/node_modules/yjs/dist/yjs.mjs:3198:5)
    at readUpdateV2 (file:///Users/adelnizamutdinov/Projects/diagrams/node_modules/yjs/dist/yjs.mjs:1528:3)
    at readUpdate (file:///Users/adelnizamutdinov/Projects/diagrams/node_modules/yjs/dist/yjs.mjs:1607:58)
    at readSyncMessage (file:///Users/adelnizamutdinov/Projects/diagrams/back/dist/src/sync.js:82:17)

Additional context
Discussion https://discuss.yjs.dev/t/read-only-or-one-way-only-sync/135/9

Using y-websocket in Node.js: Depends on `window`

Describe the bug
I’m trying to use y-websocket in Node.js (for testing purposes). As described in the README I’m passing ws as a WebsocketPolyfill, like this:

const wsProvider = new WebsocketProvider('ws://localhost:1234', 'my-roomname', doc, { WebSocketPolyfill: require('ws') })

When I try to run this code there are two parts that expect to run in a Browser context:

  1. window.addEventListener('beforeunload', () => {
    awarenessProtocol.removeAwarenessStates(this.awareness, [doc.clientID], 'window unload')
    })
  2. https://github.com/dmonad/lib0/blob/01005b5435f7c4e91fb8ea9933e734cc0a6c3451/broadcastchannel.js#L45:L45

To Reproduce

  1. Execute the above code in a Node.js environment
  2. See errors

Expected behavior
I’d expect y-websocket to use window only when available.

Environment Information

  • Node.js
  • Latest version of y-websocket

Additional context
I don’t know if it’s dumb using y-websocket in Node.js, but using Cypress feels like overkill. ¯\_(ツ)_/¯

Status not updating

Describe the bug
Status not updating

To Reproduce
wsProvider.on('status', event => {
console.log(event.status) // logs "connected" or "disconnected"
})

wsProvider.destroy()

on('status') not being called

Environment Information

No way to define `path` for WebSocket client

Describe the bug I can't see the way to define the path property for the WebSocket client. When I use URL or room to emulate the path value to match URL, requests go to the server, but the connection never setup as expected. It never passes the handleUpgrade method.

To Reproduce Server:

const wssYjs = new WebSocket.Server({ path: '/yjs', noServer: true});
wssYjs.on( 'connection', ywsUtils.setupWSConnection );

// `app` is Express.js app, `app.server` is https node server here
app.server.on('upgrade', async (request, socket, head) => {
  // async auth is here
  wssYjs.handleUpgrade(request, socket, head, (ws) => {
    console.log('=========\n\n'); // ← THIS NEVER HAPPEN if I define path on websocket server
  
    wssYjs.emit('connection', ws, request);
  } );
});

and on the client (I tried different combinations but the connection is dropping anyway)

const socketUrl = 'wss://localhost:3000`; // alternative: 'wss://localhost:3000/yjs`
const room = 'yjs/example'; // alternative: `example`
const wsProvider = new WebsocketProvider(socketUrl, room, doc, {
  params: getSocketAuthParams(user)
});

Expected behavior I expect to have a way to define the same path I use on the WS server and that would establish the connection as expected

const wsProvider = new WebsocketProvider(socketUrl, room, doc, {
  path: '/yjs',
  params: getSocketAuthParams(user)
});

Environment Information

Huly®: YJS-513

Disconnect on Auth Failure:

This is related to the thread here:

#7

Is your feature request related to a problem? Please describe.
Currently emitting a message to trigger the message handler for unauthenticated requests continues reconnect attempts, despite getting console.warns according to the authProtocols here:

https://github.com/yjs/y-websocket/blob/master/src/y-websocket.js#L52-L54

Describe the solution you'd like
As far as I can tell, there are two ways the API can stop reconnect attempts from the API:

  1. Explicitly close and NOT reconnect the websocket when a message this condition is hit.
  2. Have the websocket close the connection on the server, and handle that case on the onclose function on the websocket provider as suggested by the comment here

Describe alternatives you've considered
An alternative might be to export the messageHandlers so that the user has more control on websocket connections handle them on an as needed basis

Additional context
Unless I'm missing something, it seems like the websocket will continuously try to connect to the API despite receiving the unauthenticated request

Alert message shown regularly while websocket attempts to reconnect

Checklist

Describe the bug I'm able to reproduce receiving this alert with ease by killing the websocket server locally and letting it retry to connect for a few minutes. Reporting as requested – I feel strongly that this alert should be changed out for an error or warning in the console btw, as it's existence makes deploying in production a problem.

Screenshots

image

image

You can see the error being triggered regularly, but also quite erratically.

image

To Reproduce Steps to reproduce the behavior:

  1. Use the module, connect to a socket server successfully
  2. Kill the socket server process
  3. Leave the frontend retrying for several minutes

Expected behavior

There should be no alert message shown to users.

Environment Information

Additional context

Happy to provide any extra debugging or info that might be needed

Huly®: YJS-490

WebsocketProvider needs to wait to send sync step 1 until after it knows the server is listening

Checklist

Describe the bug WebsocketProvider needs to wait to send sync step 1 until after it knows the server has setup its listener (has received sync step 1 from the server)

To Reproduce Steps to reproduce the behavior:

  1. Add delay to server between websocket connection and listening to events on the websocket
  2. Observe client send sync step 1
  3. Message is never processed by the server
  4. Server sends sync step 1
  5. Client never reaches 'synced' state because the server never responds to the client's sync step 1 (because the server wasn't listening at the time) with the proper next step

Expected behavior Client and server exchange sync step 1 messages, and server initiates the next step after processing the client's first step

Screenshots None

Environment Information

  • Browser & Node server
  • "y-websocket": "1.3.18"
  • "yjs": "13.5.24"

Additional context We are executing some async code as part of the handleUpgrade callback. This is causing delays with setting up the message listener on the websocket connection. There may be another way to handle this, but I found that moving the send of sync step 1 from the client into the handler for messageSync when the message is of type 'messageYjsSyncStep1' solved the issue.

Maybe there is a better way to handle this on the server so it awaits sending the connect to the websocket?

Huly®: YJS-515

Next major release: Make this a proper esm module

Note to myself:

Make this a proper ESM module by setting type: "module" in package.json. Currently, the bin folder contains commonjs modules (with file ending of .js) and the src folder contains ecmascript modules (also with the file ending .js). This is invalid to the spec.

But to keep compatibility, we have to keep the current format until the next major release because some projects require the bin module using an absolute path.

Websocket Server with HTTPS callback is not working

Describe the bug
The HTTP callback is failing to execute. The server seems to be trying to call the method whenever there is a change of the text editor content, but it gives the following error message in the console:
Callback request error. Error: socket hang up at connResetException (internal/errors.js:609:14) at Socket.socketOnEnd (_http_client.js:459:23) at Socket.emit (events.js:326:22) at endReadableNT (_stream_readable.js:1223:12) at processTicksAndRejections (internal/process/task_queues.js:84:21) { code: 'ECONNRESET'

To Reproduce
Steps to reproduce the behavior:

  1. Go to https://github.com/BitPhinix/slate-yjs-example
  2. Clone the repository locally
  3. Run npm install
  4. Modify the start:server command to "set HOST=localhost&&set PORT=1234&&set CALLBACK_URL=https://localhost:5001/weatherforecast&&set CALLBACK_OBJECTS={\"prosemirror\":\"XmlFragment\"}&& npx y-websocket-server"
  5. Run npm run start:server

Expected behavior
I expect that the HTTP method will be called instead of getting that error message in the console.

Screenshots
image

Environment Information

  • Chrome 88.0.4324.150 / Node.js v12.19.0
  • Yjs version: npm ls yjs

'sync' event is fired before document is loaded

Describe the bug

Sync is called with isSynced true, but my document item is empty. This behavior is intermittent.

I need to add a delay before accessing the document.

Here is a helper function that includes a delay that resolves the problem. Without the delay, my doc.getText().length is sometimes '0', even though a document exists in the backend.

My test show the bigger the document, the more delay i need to add.

For reference, my backend is the published server example using a leveldb store.

await waitForSync()
console.log(doc.getText().length)
async function waitForSync() {
    return new Promise((resolve, reject) => {
      if (wsProvider.synced) {
        setTimeout(() => {
          return resolve(undefined)
        }, 200)
      }
      wsProvider.once('sync', () => {
        setTimeout(() => {
          resolve(undefined)
        }, 200)
      })
    });
  }

Unable to run y-websocket lib with y-quill demo locally

I'm able to build and run the y-websocket library locally.

  • Downloaded the y-websocket library.
  • npm ci
  • npm run dist

on demo folder

  • npm i /path/to/local/folder/y-websocket
  • npm run start

After running the demo i don;t see any sync and updates are getting applied. But I can see the awareness updates are getting applied properly

Expected behavior

  • After building dist locally and installing locally it should work file
  • Awareness updates are getting applied properly

**note: if I'm installing [email protected] through npm it works fine...

Environment Information

  • node version 12
  • Yjs version 13.5.12
  • y-quill 0.1.3
  • quill 1.3.7

Additional context
Code:
Quill.js

/* eslint-env browser */

import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { QuillBinding } from 'y-quill'
import Quill from 'quill'
import QuillCursors from 'quill-cursors'

Quill.register('modules/cursors', QuillCursors);

window.addEventListener('load', () => {
  const ydoc = new Y.Doc()
  const provider = new WebsocketProvider('ws://localhost:8080/doc/edit', 'quill-demo2', ydoc)
  const ytext = ydoc.getText('quill')
  const editorContainer = document.createElement('div')
  editorContainer.setAttribute('id', 'editor')
  document.body.insertBefore(editorContainer, null)

  const editor = new Quill(editorContainer, {
    modules: {
      cursors: true,
      toolbar: [
        [{ header: [1, 2, false] }],
        ['bold', 'italic', 'underline'],
        ['image', 'code-block']
      ],
      history: {
        userOnly: true
      }
    },
    placeholder: 'Start collaborating...',
    theme: 'snow' // or 'bubble'
  })

  const binding = new QuillBinding(ytext, editor, provider.awareness)

  /*
  // Define user name and user name
  // Check the quill-cursors package on how to change the way cursors are rendered
  provider.awareness.setLocalStateField('user', {
    name: 'Typing Jimmy',
    color: 'blue'
  })
  */

  const connectBtn = document.getElementById('y-connect-btn')
  connectBtn.addEventListener('click', () => {
    if (provider.shouldConnect) {
      provider.disconnect()
      connectBtn.textContent = 'Connect'
    } else {
      provider.connect()
      connectBtn.textContent = 'Disconnect'
    }
  })

  // @ts-ignore
  window.example = { provider, ydoc, ytext, binding, Y }
})

Packge.json

{
  "name": "yjs-quill-demo",
  "version": "1.0.0",
  "description": "Yjs ❤ Quill",
  "author": "Kevin Jahns <[email protected]>",
  "license": "UNLICENSE",
  "scripts": {
    "dist": "webpack --mode=production",
    "start": "webpack-dev-server --open-page quill.html"
  },
  "dependencies": {
    "quill": "^1.3.7",
    "quill-cursors": "^2.2.1",
    "webpack": "^4.46.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.2",
    "y-quill": "0.1.3",
    "y-websocket": "file:../y-websocket",
    "yjs": "^13.4.14"
  }
}

Additional sync guarantees

The server should check regularly (at least shortly after the session is created) if the client is synced up. We debugged one case when the websocket connection apparently "loses" the initial syncStep1 message from the server, and this is one way to fix it.

@vwall

Will work on this this week. You can pull the changes and check if you can still reproduce the issue.

Huly®: YJS-489

I can't check sync status

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
In readme we have info about synced property of WS provider with description "True if this instance is currently connected and synced with the server."

First of all, this property marked as private in typescript declaration, so we have to cast types to get value.

Well, i have checked value of this property to know current status and this property is always false.

How it work? How i can get current sync status for the document?

To Reproduce
Steps to reproduce the behavior:

  1. Create instance of WebsocketProvider
  2. Wait getting data for your doc
  3. Check doc.provider.synced any time

Expected behavior
Property is reflect of current synchronization status and it's false when document is not synced and true when we got all data

Actual behavior
Property always false, so we can't know sync status

Screenshots
If applicable, add screenshots to help explain your problem.

Environment Information

  • Browser
  • y-websocket: 1.3.17, yjs 13.5.17

Additional context
I need check current sync status, so sync event is not that thing i need

Emitting changed rooms

We're using yjs-websocket as a centralized sync layer for prosemirror collaboration. This is essential a cache of the latest (online) state, and not the final store.

I would like to pass a HTTP endpoint, which is a callback-endpoint triggered with the room-id. (this will technically be a publish endpoint, and applications can then implement their own subscription and do other actions based on this).

I can work with @Mansehej to build a patch, so CALLBACK_SERVER env variable can be setup, and called. Additionally CALLBACK_TIMEOUT can be configured, as well as CALLBACK_DEBOUNCE_TIME. Failed/timeout response will not be retried.

This could also be achieved with integrating with proper pub/sub services, but it seems like a HTTP based callback is the best/broadest way of building this to be used by a host of applications.

Will such a PR be in the scope of the project. (I envision it will go well with persistent state - it'll make yjs websocket a good cache store, and suitable for offline-first applications)

Huly®: YJS-485

Make y-websocket run on node.js so that websocket server for y-websocket can be tested

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. I wrote tests for a ws-server for y-websocket using lib0/testing (since I don't know any other testing-lib that works with esm). When I tried to run those tests, I got an error message regarding usage of window in y-websocket (from this line). These type of things prevent running y-websocket in a node.js environment and writing and running automated tests for it

Describe the solution you'd like y-websocket should be runnable on node.js. If some help is needed, then I can copy over the testing-code which tries to use y-websocket in node.js env from my private repo to a public repo, and that repo can be used to verify if y-websocket can be run on backend.

Describe alternatives you've considered not testing the ws server which y-websocket connects to is the only alternative, but it doesn't bring much confidence

Additional context test-code:

import * as t from 'lib0/testing.js';
import * as Y from 'yjs';
import faker from 'faker';
import WS from 'ws';
import {WebsocketProvider} from 'y-websocket';
import { nockCheckAuth, nockGetUpdates, nockPostUpdate, wsUrl } from './index.js';

export const testSingleDoc = async (tc: t.TestCase) => {
  const id = faker.datatype.uuid();
  const token = faker.lorem.word();
  const doc = new Y.Doc();
  const updates: string[] = []

  nockCheckAuth(token);
  nockGetUpdates(token, id, updates);
  nockPostUpdate(token, id, updates);
  const wsProvider = new WebsocketProvider(wsUrl(), id, doc, {params: {token}, WebSocketPolyfill: WS as any})

  const items = doc.getArray('items');
  items.push([faker.random.word()]);

  await new Promise<void>(resolve => setTimeout(resolve, 1000));
  wsProvider.destroy();
}

error-message

[1/1] app: single doc
  ReferenceError: window is not defined
      at new WebsocketProvider (/home/k/Projects/proj/yjs-proto/sw/crdt-ws/node_modules/y-websocket/src/y-websocket.js:280:5)
      at testSingleDoc (file:///home/k/Projects/proj/yjs-proto/sw/crdt-ws/src/__tests__/app.test.ts:17:22)
      at run (file:///home/k/Projects/proj/yjs-proto/sw/crdt-ws/node_modules/lib0/testing.js:158:17)
      at runTests (file:///home/k/Projects/proj/yjs-proto/sw/crdt-ws/node_modules/lib0/testing.js:552:27)
      at file:///home/k/Projects/proj/yjs-proto/sw/crdt-ws/src/__tests__/main.ts:8:11
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
Failure: single doc in 3.54ms
repeat: npm run test -- --filter "\[1/" 

> 1 test failed

Huly®: YJS-501

Awareness and Doc memory leak

Describe the bug

The beforeunload listener isn't removed on destroy, an Awareness instance and a Doc instance are captured via closure and cannot be garbage collected in the browser. See: https://github.com/yjs/y-websocket/blob/master/src/y-websocket.js#L280

To Reproduce

  1. Instantiate provider
  2. Destroy it
  3. Make a memory snapshot and look for retainers of Awareness/Doc

Expected behavior

Awareness/Doc should be garbage collected after destroying the provider.

Environment Information

  • Browser

Additional

Memory profile after destroying the provider.

Cant separate data with different Y.Doc instances

import React from 'react';
import * as Y from 'yjs'

import ClickToEdit from "./ClickToEdit";

function App() {
    const ydoc1 = new Y.Doc()
    const ydoc2 = new Y.Doc()
    const demoText = "Here is the new text"

  return (
    <div>
        <ClickToEdit shouldUpdate={true} room={'a'} ydoc={ydoc1} text={demoText} /><br/>
        <ClickToEdit shouldUpdate={true} room={'b'} ydoc={ydoc2} text={demoText} />
    </div>
  );
}

export default App;


import React, {useCallback, useEffect, useState} from 'react';
import {useQuill} from "react-quilljs";
import 'quill/dist/quill.snow.css'; // Add css for snow theme

import {WebsocketProvider} from 'y-websocket'
import {QuillBinding} from 'y-quill'
import Quill from 'quill'


function ClickToEdit(props) {
	const {quill, quillRef} = useQuill();

	const [provider, setProvider] = useState(null)

	useEffect(() => {
		if (quill) {
			setProvider(new WebsocketProvider('ws://localhost:1234', 'quill' + props.room, props.ydoc))
		}
	}, [quill]);


	useEffect(() => {
		if (provider !== null) {
			const yQuillTextYtype = props.ydoc.getText('quill')
			const binding = new QuillBinding(yQuillTextYtype, quill, provider.awareness)
			const handleObserve = e => {
				if (quill && e.delta[0]) {
					const text = e.delta[0].insert
					quill.clipboard.dangerouslyPasteHTML(props.text)
					yQuillTextYtype.unobserve(handleObserve)
				}
			}
			yQuillTextYtype.observe(handleObserve)

		}
	}, [provider])

	return (
		<div style={{width: 500, height: 300}}>
			<div ref={quillRef}/>
		</div>
	);
}

export default ClickToEdit;

My expectation here is that I would have separate data in the two input fields. However its the same, even though I have different rooms, and different Doc instances.

Huly®: YJS-480

Question: why server need a centralized doc ?

hey all, i am a bit confused about the collab-demo:https://github.com/yjs/y-prosemirror with y-websocket here.

in the demo, server holds a centralized doc, ws listen to client update and applyUpdate on 'server-ydoc', then broadcast server update to each 'client-ydoc' which will turn into prosemirror's transaction. I am new to yjs, in my opinion, CRDT is not basd on centralized node, why not server broadcast all client updates to each client directly since document updates are commutative and idempotent?

image

image

the README.MD 's two suggestion is also based on sync client update to server then broadcast update which occurred in the server side, why should we do like that?
image

thanks.

WebsocketProvider instance leaving open handles after calling .destroy in node.js

Checklist

Describe the bug
When y-websocket is used in node.js environment (for writing tests) with ws providing the WebSocketPolyfill, it works fine, but after calling yWsProvider.destroy(), y-websocket leaves some unknown handles open which prevent the node process from exiting.

To Reproduce
Steps to reproduce the behavior:

  1. clone this repo and cd into it
  2. run npm run ci
  3. run cp .env.example .env.test.local
  4. fill up .env.test.local
  5. run npm run watch, and then open a new terminal
  6. run npm run test - observe that the tests do not exit - forcefully terminate the test process with Ctrl+C
  7. go to src/__tests__/app.test.ts, comment out line 21 & line 27 - wait for tsc to finish compilation
  8. run npm run test again - observe that tests exit

Expected behavior
If you follow the steps in To Reproduce, then ideally in step no. 6, the process should exit, with wsp.destroy() successfully terminating everything.

Screenshots
NA

Environment Information

Additional context
This bug is important for making the websocket server for y-websocket testable

Invalid messages crash the server in a way that is un-preventable

When you send up an invalid message that isn't binary (in this example I sent up a string like looks like this "...`") it throws an exception that is un-catchable by anyone using this library

  • Create a server using y-websocket
  • Connect to the server using a tool like postman
  • Send up the message "..`" as a string
  • Bug: The server will crash with the following error:
Error: Integer out of range!
    at Object.readVarUint (/projects/yjs-project/node_modules/lib0/decoding.js:247:13)
    at messageListener (/projects/yjs-project/node_modules/y-websocket/bin/utils.js:167:32)
    at WebSocket.<anonymous> (/projects/yjs-project/node_modules/y-websocket/bin/utils.js:236:69)
    at WebSocket.emit (events.js:375:28)
    at WebSocket.emit (domain.js:470:12)
    at Receiver.receiverOnMessage (/projects/yjs-project/node_modules/ws/lib/websocket.js:835:20)
    at Receiver.emit (events.js:375:28)
    at Receiver.emit (domain.js:470:12)
    at Receiver.dataMessage (/projects/yjs-project/node_modules/ws/lib/receiver.js:437:14)
    at Receiver.getData (/projects/yjs-project/node_modules/ws/lib/receiver.js:367:17)
    at Receiver.startLoop (/projects/yjs-project/node_modules/ws/lib/receiver.js:143:22)
    at Receiver._write (/projects/yjs-project/node_modules/ws/lib/receiver.js:78:10)
    at writeOrBuffer (internal/streams/writable.js:358:12)
    at Receiver.Writable.write (internal/streams/writable.js:303:10)
    at Socket.socketOnData (/projects/yjs-project/node_modules/ws/lib/websocket.js:910:35)
    at Socket.emit (events.js:375:28)

This makes sense, the packet was not valid for y-websocket so it makes sense to throw. However the problem is that currently there is no way to catch this in a try catch block or prevent this from crashing the server because the error happens inside conn.on('message'). This means that it's very easy to DDoS y-websocket servers, by sending messages like the above and bringing them down. I got around this issue by forking y-websocket and adding a try catch in the utils.js messageListener function.

Ideally it would be great to catch this error and emit it somewhere were the end user can respond to it (conn.on('error') or doc.on('error)?)

Environment Information

  • yjs@^13.4.12
  • y-websocket v1.3.16
  • node v14

Note: I am using the utils directly, and am not using the src/y-websocket.js code in my project, but I assume this error will happen there too

Compile errors

I'm getting the following two errors in a fresh project created with vue-cli:

 warning  in ./node_modules/y-prosemirror/src/plugins/sync-plugin.js

"export 'Item' (imported as 'Y') was not found in 'yjs'

 warning  in ./node_modules/y-prosemirror/src/plugins/undo-plugin.js

"export 'UndoManager' was not found in 'yjs'

Huly®: YJS-477

References to window object crash node scripts using WebsocketProvider

Checklist

Describe the bug
When using WebsocketProvider in a node script, y-websocket.js and lib0 reference window, making the script crash respectively with:

ReferenceError: window is not defined
    at new WebsocketProvider (/.../node_modules/y-websocket/dist/y-websocket.cjs:267:5)
ReferenceError: addEventListener is not defined
    at new LocalStoragePolyfill (node_modules/lib0/dist/broadcastchannel-49d2cc56.cjs:30:5)
    at node_modules/lib0/dist/broadcastchannel-49d2cc56.cjs:51:16
    at Object.setIfUndefined (node_modules/lib0/dist/map-28a001c9.cjs:51:24)
    at getChannel (node_modules/lib0/dist/broadcastchannel-49d2cc56.cjs:49:7)
    at Object.subscribe (node_modules/lib0/dist/broadcastchannel-49d2cc56.cjs:68:32)
    at WebsocketProvider.connectBc (node_modules/y-websocket/dist/y-websocket.cjs:311:10)
    at WebsocketProvider.connect (node_modules/y-websocket/dist/y-websocket.cjs:362:12)
    at new WebsocketProvider (node_modules/y-websocket/dist/y-websocket.cjs:279:12)

To Reproduce
Steps to reproduce the behavior:

  1. Run a node script like:
const Y = require("yjs");
const { WebsocketProvider } = require("y-websocket");
const doc = new Y.Doc();

const wsProvider = new WebsocketProvider(
  "ws://localhost:1234",
  "test-room",
  doc,
  { WebSocketPolyfill: require("ws") }
);
  1. See error

Expected behavior
No error.

Environment Information

  • Node.js
  • "y-websocket": "^1.3.9",
  • "yjs": "^13.4.12"

Additional context

This code successfully works around the issue:

window = {
  addEventListener: () => null,
};
addEventListener = () => null;

Unable to run demo

Hi! Sorry if this isn't the right place to ask... I'm trying to run the y-websockets-server locally and can't seem to get the client to work. I cloned this repo as well as the demo repo, and I run PORT=1234 npx y-websockets-server, which results in an output of:

Running y-websockets-server on port 1234

In my client demo, I create a connection with:

const provider = new WebsocketProvider('ws://localhost:1234', 'my-roomname', ydoc)

I don't see a connection occur. Any ideas why?

Huly®: YJS-482

Cannot do require('y-websocket/bin/utils.js') since v1.3.13

Describe the bug
I've written my own websocket server based on y-websocket/bin/server.js but instead of rewriting y-websocket/bin/utils.js I've been requiring that file like this:

const yWebsocketUtils = require('y-websocket/bin/utils.js')

but since v1.3.13 (where explicit "exports" was added to package.json) I now get this error in Node.js:

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './bin/utils.js' is not defined by "exports" in /node_modules/y-websocket/package.json

Expected behavior
Being able to require subpaths in y-websocket to help custom websocket servers.

Environment Information

  • Node.js v12
  • y-websocket v1.3.13

Improve documentation: explain how to use PubSub server

I'm thinking about building a Yjs+Atlaskit collaborative editor for our service. The existing system is running behind two redundant load balancers pushing requests to three redundant worker servers talking to another set of redundant servers running SQL database. As such, I'd like to setup the collaborative editor to have no single point of failure. The README.md suggest that PubSub is the way to go. The target is to build editor that can use existing system authentication and obviously I need to serialize the documents somehow from/to y-websocket so that I don't lose documents. Also, we have lots of existing documents in HTML5/XHTML format and it would be great if I could import such data as the initial value for the document to be modified with this kind of editor setup.

However, the README.md should have more documentation about this or a link to other documents specifying the required steps. I can help documenting the process (submit pull requests for documentation) if I get enough pointers how to proceed.

This issue is specifically about PubSub server setup. If permanent storage needs additional documentation, it should be tracked by another issue.

Huly®: YJS-497

Memory leak?

There are two issues. I am reporting them as separate issues because I don't know whether they are related, but they might be.

Describe the bug
When running y-websocket, version 1.3.1, the memory used by node increases until it exceeds the RAM available.

In more detail
Using the set up recommended in the README, i.e.

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

the memory used by node grows unreasonably. When y-websocket is started, it looks like this:

Private  +   Shared  =  RAM used	Program
141.3 MiB + 411.5 KiB = 141.7 MiB	node

Twenty four hours later, it looks like this:

 1.6 GiB + 324.5 KiB =   1.6 GiB	node

At this point I have to restart it as I don't have that much free RAM.

Expected behavior
What I am expecting is that the RAM used by node remains more or less constant

Environment

System: a AWS EC2 Linux 2 server
Node: v14.4.0
Implemented using a system service, with:

[Unit]
Description=websocket-server
[Service]
ExecStart=/data/cress/prism/node_modules/.bin/y-websocket-server
Restart=always
RestartSec=10
User=root
Group=root
Environment='PATH=/home/ec2-user/.nvm/versions/node/v14.4.0/bin' 'YPERSISTENCE=/data/cress/prism/dbDir'
# Output to syslog
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=websocket-server
[Install]
WantedBy=multi-user.target

ReferenceError: require is not defined

Bug Running

HOST=localhost PORT=1234 npx y-websocket-server

yields

ReferenceError: require is not defined

To reproduce:

npm install y-websocket

(installs 1.3.14) Then

HOST=localhost PORT=1234 npx y-websocket-server

which generates:

file:///xxxxxxxx/node_modules/y-websocket/bin/server.js:6
const WebSocket = require('ws')
                  ^

ReferenceError: require is not defined
    at file:///xxxxxxx/node_modules/y-websocket/bin/server.js:6:19
    at ModuleJob.run (internal/modules/esm/module_job.js:146:23)
    at async Loader.import (internal/modules/esm/loader.js:165:24)
    at async Object.loadESM (internal/process/esm_loader.js:68:5)

Expected behavior a notice to say that y-webserver is running on port 1234

Environment Information node v14.15.1 y-webserver 1.3.14

y-webserver Version 1.3..11 didn't have this problem.

Huly®: YJS-504

Memory leak in server implementation

Checklist

Describe the bug
A clear and concise description of what the bug is.

To Reproduce
Steps to reproduce the behavior:

  1. Start the server, take a heap snapshot with the node debugger
  2. Open a document on the client, causing a websocket connection and WSSharedDoc to be loaded into memory
  3. Close the document on the client, causing websocket connection to close
  4. Take a heap snapshot with the node debugger
  5. WSSharedDoc remains in memory, it looks like the interval timer is retaining the reference

Expected behavior

WSSharedDoc should no longer be in memory once all clients are disconnected. I believe the fix is relatively simple – we need to create a destroy method on WSSharedDoc that clears up the Awareness. I'll be attempting this as a fix soon.

Screenshots

Note this heap screenshot is from a local server with no clients connected:

image

Environment Information

  • Browser / Node.js: Node 12
  • Yjs version: I'm using my fork of the library @tommoor/[email protected] as it includes the critical fix for a race condition from this PR: #39

Export `docs` in utils.js

Is your feature request related to a problem? Please describe.
I'd like to use y-websocket as a dependency to my server, rather than copy-paste code from y-websocket. Currently, the code in bin/utils.js does not expose its docs data, so my server code cannot access the internal docs.

Specifically: I am trying to create an HTTP API endpoint that copies one YDoc to another YDoc, and I believe I need access to the internal docs map to do so.

Describe the solution you'd like
I'd like there to be an exports.docs = docs after const docs = new Map()

Describe alternatives you've considered
I'm looking for guidance--is this a reasonable thing to export? Should I create a PR?

npm install failed

npm verb stack Error: ENOENT: no such file or directory, chmod '/Users/monsterstep/dev/parcel-playground/ot/server/node_modules/y-websocket/bin/server.js'
npm verb cwd /Users/monsterstep/dev/parcel-playground/ot/server
npm verb Darwin 18.6.0
npm verb argv "/Users/monsterstep/.nvm/versions/node/v10.15.0/bin/node" "/Users/monsterstep/.nvm/versions/node/v10.15.0/bin/npm" "i" "-S" "y-websocket" "--verbose"
npm verb node v10.15.0
npm verb npm  v6.9.0
npm ERR! path /Users/monsterstep/dev/parcel-playground/ot/server/node_modules/y-websocket/bin/server.js
npm ERR! code ENOENT
npm ERR! errno -2
npm ERR! syscall chmod
npm ERR! enoent ENOENT: no such file or directory, chmod '/Users/monsterstep/dev/parcel-playground/ot/server/node_modules/y-websocket/bin/server.js'
npm ERR! enoent This is related to npm not being able to find a file.
npm ERR! enoent
npm verb exit [ -2, true ]

Huly®: YJS-476

Fragmented frames behind proxy

Describe the bug When running the WebSocket server provided in this repository behind a Traefik proxy it fails in larger documents with:

│   /var/www/node_modules/lib0/dist/buffer-65d3ece5.cjs:1562                                                        │
│   const createUint8ArrayViewFromArrayBuffer = (buffer, byteOffset, length) => new Uint8Array(buffer, byteOffset   │
│   , length);                                                                                                      │
│                                                                               ^                                   │
│   2021-01-11T12:40:24.275187961Z                                                                                  │
│   RangeError: Invalid typed array length: 121030123                                                               │
│       at new Uint8Array (<anonymous>)                                                                             │
│       at createUint8ArrayViewFromArrayBuffer (/var/www/node_modules/lib0/dist/buffer-65d3ece5.cjs:1562:77)        │
│       at readUint8Array (/var/www/node_modules/lib0/dist/buffer-65d3ece5.cjs:956:16)                              │
│       at Object.readVarUint8Array (/var/www/node_modules/lib0/dist/buffer-65d3ece5.cjs:971:38)                    │
│       at readSyncStep2 (/var/www/node_modules/y-protocols/dist/sync.cjs:86:31)                                    │
│       at Object.readSyncMessage (/var/www/node_modules/y-protocols/dist/sync.cjs:120:7)                           │
│       at messageListener (/var/www/bin/utils.js:171:20)                                                           │
│       at WebSocket.<anonymous> (/var/www/bin/utils.js:236:69)                                                     │
│       at WebSocket.emit (node:events:376:20)                                                                      │
│       at Receiver.receiverOnMessage (/var/www/node_modules/ws/lib/websocket.js:789:20)

It seems that Traefik fragments frames or adapts frame size (which is compliant to the RFC). The WebSocket server uses the arraybuffer binary type and therefore - in my understanding - cannot deal with this. I don't understand how the y-js protocol works in detail thus I have no idea if this thing can be fixed or not.

See this issue for more information: traefik/traefik#4446

To Reproduce Run the y-webserver server behind Traefik and edit a larger document so that the frame size is above 4kb.

Expected behavior The server should not fail with an error code.

Environment Information

  • Tested Node 13 - 15

Thanks a ton for your work, any help is highly appreciated. ❤️

Huly®: YJS-493

clear document from leveldb after writeState

When persisting the document updates using persistence.bind() document updates are saved, when writeState is called the document is destroyed in memory but seems to live on in leveldb. Which would unnecessarily takes up space when it's already persisted to another db in writeState.

Describe the solution you'd like

after writeState is called (https://github.com/yjs/y-websocket/blob/master/bin/utils.js#L204):

    if (ldb) {
      ldb.clearDocument(doc.name)
    }

Huly®: YJS-516

Disconnects and reconnects every 30 seconds

Describe the bug

The client disconnects and then reconnects from the websocket server every 30 seconds

To Reproduce

Set up a client running yjs. Intermittently, it will do something similar to this:

...
prism.js:209 21:00:34: connected to room CMD-HMM-NTO-XML
prism.js:209 21:01:06: disconnected from room CMD-HMM-NTO-XML
prism.js:209 21:01:07: connected to room CMD-HMM-NTO-XML
prism.js:209 21:01:39: disconnected from room CMD-HMM-NTO-XML
prism.js:209 21:01:40: connected to room CMD-HMM-NTO-XML
prism.js:209 21:02:12: disconnected from room CMD-HMM-NTO-XML
prism.js:209 21:02:13: connected to room CMD-HMM-NTO-XML

I have not discovered for certain what triggers this. It may occur after resuming from a debugger break, but it does not happen every time.

Environment

Client code:

const wsProvider = new WebsocketProvider(
		'wss://cress.soc.surrey.ac.uk/wss',
		'prism' + room,
		doc
	);

wsProvider.on('status', (event) => {
		console.log(
			new Date().toLocaleTimeString() +
				': ' +
				event.status +
				(event.status == 'connected' ? ' to' : ' from') +
				' room ' +
				room
		); // logs "connected" or "disconnected"
	});

yjs: 13.2.0
y-websocket: 1.3.1
npm: 6.14.7
browser: Chrome 84
websocket server: y-websocket: 1.3.1 (slightly modified to accept wss:// connections)

y-websocket/bin/utils.js file is not accessible anymore the custom persistor solution is not applicable anymore

I admire yjs but, there might be a small problem that happened in y-websocket.

In this issue yjs/yjs#170 you purposed the solution which is the code below but, this code is no longer working with adding the exports field in the y-websocket and the new node.js versions only allowing subpath exports using the exports field if it exists now we are not able to access y-websocket/bin/utils.js

now we are not allowed to require or import y-websocket/bin/utils.js if there is an alternative please tell us how or I can raise a PR to solve this issue and add the utils in the exports field.

NOTE: I am trying to use y-websocket version 1.3.16 Node.js version v12.22.4

what do you suggest ?

#!/usr/bin/env node

/**
 * @type {any}
 */
const WebSocket = require('ws')
const http = require('http')
const wss = new WebSocket.Server({ noServer: true })
const utils = require('y-websocket/bin/utils.js')
const setupWSConnection = utils.setupWSConnection

const port = process.env.PORT || 1234

const server = http.createServer((request, response) => {
  response.writeHead(200, { 'Content-Type': 'text/plain' })
  response.end('okay')
})

/*
 Persistence must have the following signature:
{ bindState: function(string,WSSharedDoc):void, writeState:function(string,WSSharedDoc):Promise }
*/
utils.setPersistence({
  bindState: (documentName, doc) => {
    // Here you listen to granular document updates and store them in the database
    // You don't have to do this, but it ensures that you don't lose content when the server crashes
    // See https://github.com/yjs/yjs#Document-Updates for documentation on how to encode 
    // document updates
  },
  writeState: (string, doc) => {
    // This is called when all connections to the document are closed.
    // In the future, this method might also be called in intervals or after a certain number of updates.
    return new Prosime(resolve => {
      // When the returned Promise resolves, the document will be destroyed.
      // So make sure that the document really has been written to the database.
      resolve()
    })
  }
})

wss.on('connection', setupWSConnection)

server.on('upgrade', (request, socket, head) => {
  // You may check auth of request here..
  /**
   * @param {any} ws
   */
  const handleAuth = ws => {
    wss.emit('connection', ws, request)
  }
  wss.handleUpgrade(request, socket, head, handleAuth)
})

server.listen(port)

console.log('running on port', port)

Huly®: YJS-511

The `getContent` method references the method `getXmlElement` which does not appear to exist.

Checklist

  • Are you reporting a bug? Use github issues for bug reports and feature requests. For general questions, please use https://discuss.yjs.dev/

Describe the bug

the function getContent references getXmlElement on a Y.Doc which does not appear to exist.

case 'XmlElement': return doc.getXmlElement(objName)

https://github.com/yjs/yjs/blob/dc9717ecd0650315d87129cff795dc5116f8bcf2/src/utils/Doc.js

To Reproduce I'm not sure that code branch can ever be reached but I wanted to let you know regardless.

Environment Information

  • y.js 13.5.6 per y-websocket's dependency
  • y-websocket - latest

Huly®: YJS-512

Implement another authentication approach aside from cookie support.

From the gitter channel @WinstonFasset:

Yes my first choice was to use a header for auth, but I was unable to get it to work in the browser even using a protocol header hack that was supposed to work (I was using Chrome, which apparently strips security headers). Similarly basic auth didn't work. Querystring parameter and cookie both worked. Incidentally, the feathers-js API server I'm using for REST can also do websockets pretty well. When enabled it will use messages for auth rather than any of the mechanisms I just described. I lean towards that approach but don't see a good way to implement it with YJS.

setLocalStateField() stops working after reconnecting to the server

Description
When clients disconnect from the server, the awareness states are removed. When clients reconnect to the server, and try to set the local state through setLocalStateField() that won’t happen, because there is no state to update.

As a result the list of connected users (= awareness states) stays empty after losing the connection to the server, even when the it’s back online and all clients reconnected successfully.

Reproduction

  1. Open an editor demo with a list of connected users
  2. Stop the server
  3. Restart the server

Expected behavior
I’d expect the clients to fetch up all awareness states again after a reconnect.

Environment Information

  • y-websocket 1.3.7

I’m sending a PR with proposed changes to fix this issue.

Improved connection error handling in the client

Description
We’re building an open source WebSocket server based on y-websocket, called hocuspocus. A lot of developers struggle to debug issues with it, for example when authentication fails. We’d like to improve the experience for developers.

For example, currently, the y-websocket client tries to reconnect, no matter what the error is, without communicating a reason. That can be frustrating to debug.

Also, that could cause a DDOS attack on the server in a production environment, even with a few users, when they all try to reconnect again and again. (We had this in the tiptap documentation example.)

The solution I’d like to have
I’d like to implement an error handling in the client, but I’m not sure if that’s in the scope of this project and we should send a PR or create a fork of y-websocket.

For the “Authentication failed“ example the flow could look like that:

  • The authentication fails (e. g. unknown token)
  • The backend sends a WebSocket message, and/or use WebSocket CloseEvent codes to communicate the error to the client.
  • The client stops trying to reconnect, or slows down the reconnection attempts.
  • The error could be logged to the console, making debugging easier.
  • (Optional) Enabling/disabling console output could be done based on an environment variable and/or based on a setting

Alternatives I’ve considered

  1. Creating a PR, but it’s probably hard to keep it consistent in y-websocket client, server and hocuspocus?
  2. Creating a fork, would allow us to add enhancements freely, but doubles the burden of maintenance.
  3. Not doing anything, but that’s causing a lot of work with feedback/issues/emails with people needing help to debug.

Honestly, not sure what’s the best way. I’d love to have your opinion on this.

Additional context
There’s already code in the client to slow down reconnects, but that doesn’t seem to work in our environments or at least in some cases, but I couldn’t find time to debug that. Fixing/improving that would at least help with “DDOS attacks” while developing, but probably not that much with debugging. Could be a first step though.

Using the WebSocket CloseEvent with y-websocket: #7 (comment)
Stale PR, adding the WebSocket CloseEvent to the status event: https://github.com/yjs/y-websocket/pull/59/files#diff-bf6cbbe721bbeff64c972b548f6291dc24bba8fd0e1cef5b91d87513c6a0b362R105
Stale PR, maybe I can finish that one? #48

Authentication testing

The proposed auth method should be tested. A client shouldn't try to reconnect after auth has been rejected and the server should properly close the connection.

From @WinstonFasset in the Gitter channel:

I got auth working a while back based on that marker in bin/server.js, but it's been a bit of a struggle and is still a bit of a mess. If anyone else has had any success I'd love to know what they did.
One question is how do you authenticate? A cookie? A request parameter? Send the message on a socket? I went with an access_token querystring parameter (over SSL)
the other thing I wasn't sure of is how to reject access. I tried closing/destroying/terminating the socket and it caused the browser to start spooling up infinite new websockets, so I set a 10s timeout before closing.
also once I added this I started seeing server crashes caused by what I think was a memory leak
here's the code I ended up with. apologies for the messiness. some of it may not be strictly necessary:

server.on('upgrade', (request, socket, upgradeHead) => {
  var head = new Buffer(upgradeHead.length);
  upgradeHead.copy(head);
  upgradeHead = null
  // You may check auth of request here..
  // or check cookie here
  // console.log('CHECK AUTH', request.url, request.headers.cookie)
  /**
   * @param {any} ws
   */
  const handleAuth = async ws => {
    const parsed = url.parse(request.url, true)
    const access_token = parsed.query.access_token
    const docName = parsed.pathname
    // console.log('parsed', parsed)
    request.url = (parsed.origin||'') + docName
    const access = await verifyAccess(access_token, docName)
    const { user } = access
    if (user) {
      // console.log('connecting inner request url', request.url)
      request.user = user
      wss.emit('connection', ws, request)
      // release ref?
      socket.unref()
      socket = null
    }
    // if (true || request.headers.cookie.indexOf('tehsecretpassword')> -1) {
      // } 
    else {
      console.log('access denied', request.url)
      // socket.close();
      setTimeout(() => {
        socket.destroy();
        socket = null;
      }, 10000)
      // socket.close()
      // socket.terminate()
      return;
    }
  }
  wss.handleUpgrade(request, socket, head, handleAuth)
})

Exception/crash when sending garbage to websocket

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
One can crash y-websocker server by sending over wire number greater than 2*2**31.

To Reproduce

  1. start the server: node ./front/node_modules/y-websocket/bin/server.js
  2. open websocket connection: websocat ws://localhost:1234
  3. send 4294967296 (confirm with newline)
  4. send 4294967297

Expected behavior
No exceptions and no crash.

Environment Information

  • node version 17.3.0
  • y-websocket version 1.3.18

Additional context
Idk whether this is problem related to ws or y-websocket. Unfortunately I currently have no time to investigate it further. I'm reporting just in case I forget about it in future.

BTW I suppose y-websocket isn't production-grade software right now, but maybe we can at least not crash whole server upon receiving garbage.

And obligatorily: much thanks for your work!!

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.