What is yjs?
Yjs is a high-performance, CRDT-based (Conflict-free Replicated Data Type) library that enables real-time collaboration on shared data structures. It is designed to be used in collaborative applications where multiple users can edit the same data simultaneously without conflicts.
What are yjs's main functionalities?
Shared Text Editing
Yjs allows multiple users to collaboratively edit a shared text document in real-time. The changes are synchronized across all connected clients using WebRTC.
const Y = require('yjs');
const { WebrtcProvider } = require('y-webrtc');
const doc = new Y.Doc();
const provider = new WebrtcProvider('my-room-name', doc);
const yText = doc.getText('shared-text');
yText.insert(0, 'Hello, world!');
// Listen for changes
yText.observe(event => {
console.log('Text changed:', yText.toString());
});
Shared Array
Yjs supports shared arrays, allowing users to collaboratively manipulate arrays in real-time. Changes are synchronized across all clients.
const Y = require('yjs');
const { WebrtcProvider } = require('y-webrtc');
const doc = new Y.Doc();
const provider = new WebrtcProvider('my-room-name', doc);
const yArray = doc.getArray('shared-array');
yArray.push(['item1', 'item2']);
// Listen for changes
yArray.observe(event => {
console.log('Array changed:', yArray.toArray());
});
Shared Map
Yjs provides shared maps, enabling users to collaboratively edit key-value pairs in real-time. Changes are synchronized across all clients.
const Y = require('yjs');
const { WebrtcProvider } = require('y-webrtc');
const doc = new Y.Doc();
const provider = new WebrtcProvider('my-room-name', doc);
const yMap = doc.getMap('shared-map');
yMap.set('key1', 'value1');
yMap.observe(event => {
console.log('Map changed:', yMap.toJSON());
});
Undo/Redo
Yjs includes an UndoManager that allows users to undo and redo changes made to shared data structures, providing a better user experience in collaborative applications.
const Y = require('yjs');
const { WebrtcProvider } = require('y-webrtc');
const { UndoManager } = require('yjs');
const doc = new Y.Doc();
const provider = new WebrtcProvider('my-room-name', doc);
const yText = doc.getText('shared-text');
const undoManager = new UndoManager(yText);
yText.insert(0, 'Hello, world!');
undoManager.undo();
undoManager.redo();
Other packages similar to yjs
automerge
Automerge is a library for building collaborative applications using CRDTs. It provides similar functionality to Yjs, such as real-time collaboration on shared data structures. However, Automerge is known for its simplicity and ease of use, while Yjs is often praised for its performance and flexibility.
sharedb
ShareDB is a real-time database backend based on Operational Transformation (OT). It allows multiple users to collaborate on the same data in real-time. While ShareDB uses OT, Yjs uses CRDTs, which can be more efficient and easier to reason about in some scenarios.
gun
Gun is a decentralized, real-time, graph database that supports real-time collaboration. It uses a different approach compared to Yjs, focusing on decentralized data storage and synchronization. Gun is highly scalable and can be used for building large-scale collaborative applications.
Yjs is a framework for offline-first p2p shared editing on structured data like
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
most of the complexity of concurrent editing. For additional information, demos,
and tutorials visit y-js.org.
:warning: Checkout the v13 docs for the upcoming release :warning:
Extensions
Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
- Connector - a communication protocol that propagates changes to the clients
- Database - a database to store your changes
- one or more Types - that represent the shared data
Connectors, Databases, and Types are available as modules that extend Yjs. Here
is a list of the modules we know of:
Connectors
Database adapters
Name | Description |
---|
memory | In-memory storage. |
indexeddb | Offline storage for the browser |
leveldb | Persistent storage for node apps |
Types
Name | Description |
---|
map | A shared Map implementation. Maps from text to any stringify-able object |
array | A shared Array implementation |
xml | An implementation of the DOM. You can create a two way binding to Browser DOM objects |
text | Collaborate on text. Supports two way binding to the Ace Editor, CodeMirror, Monaco, textareas, input elements, and HTML elements (e.g. <h1>, or <p>) |
richtext | Collaborate on rich text. Supports two way binding to the Quill Rich Text Editor |
Other
Use it!
Install Yjs, and its modules with bower, or
npm.
Bower
bower install --save yjs y-array % add all y-* modules you want to use
You only need to include the y.js
file. Yjs is able to automatically require
missing modules.
<script src="./bower_components/yjs/y.js"></script>
CDN
<script src="https://cdn.jsdelivr.net/npm/yjs@12/dist/y.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-map@10/dist/y-map.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-text@9/dist/y-text.js"></script>
// ..
// do the same for all modules you want to use
Npm
npm install --save yjs % add all y-* modules you want to use
If you don't include via script tag, you have to explicitly include all modules!
(Same goes for other module systems)
var Y = require('yjs')
require('y-array')(Y) // add the y-array type to Yjs
require('y-websockets-client')(Y)
require('y-memory')(Y)
require('y-map')(Y)
require('y-text')(Y)
// ..
// do the same for all modules you want to use
ES6 Syntax
import Y from 'yjs'
import yArray from 'y-array'
import yWebsocketsClient from 'y-webrtc'
import yMemory from 'y-memory'
import yMap from 'y-map'
import yText from 'y-text'
// ..
Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
Text editing example
Install dependencies
bower i yjs y-memory y-webrtc y-array y-text
Here is a simple example of a shared textarea
<!DOCTYPE html>
<html>
<body>
<script src="./bower_components/yjs/y.js"></script>
<script>
Y({
db: {
name: 'memory'
},
connector: {
name: 'webrtc',
room: 'my-room'
},
sourceDir: './bower_components',
share: {
textarea: 'Text'
}
}).then(function (y) {
y.share.textarea.bind(document.querySelector('textarea'))
})
</script>
<textarea></textarea>
</body>
</html>
Get Help & Give Help
There are some friendly people on who are eager to help, and answer questions. Please join!
Report any issues to the
Github issue page! I try to fix them very
soon, if possible.
API
Y(options)
- Y.extend(module1, module2, ..)
- Add extensions to Y
Y.extend(require('y-webrtc'))
has the same semantics as
require('y-webrtc')(Y)
- options.db
- Will be forwarded to the database adapter. Specify the database adaper on
options.db.name
. - Have a look at the used database adapter repository to see all available
options.
- options.connector
- Will be forwarded to the connector adapter. Specify the connector adaper on
options.connector.name
. - All our connectors implement a
room
property. Clients that specify the
same room share the same data. - All of our connectors specify an
url
property that defines the connection
endpoint of the used connector.
- All of our connectors also have a default connection endpoint that you can
use for development.
- Set
options.connector.generateUserId = true
in order to genenerate a
userid, instead of receiving one from the server. This way the Y(..)
is
immediately going to be resolved, without waiting for any confirmation from
the server. Use with caution. - Have a look at the used connector repository to see all available options.
- Only if you know what you are doing: Set
options.connector.preferUntransformed = true
in order receive the shared
data untransformed. This is very efficient as the database content is simply
copied to this client. This does only work if this client receives content
from only one client.
- options.sourceDir (browser only)
- Path where all y-* modules are stored
- Defaults to
/bower_components
- Not required when running on
nodejs
/ iojs
- When using nodejs you need to manually extend Yjs:
var Y = require('yjs')
// you have to require a db, connector, and *all* types you use!
require('y-memory')(Y)
require('y-webrtc')(Y)
require('y-map')(Y)
// ..
- options.share
- Specify on
options.share[arbitraryName]
types that are shared among all
users. - E.g. Specify
options.share[arbitraryName] = 'Array'
to require y-array and
create an y-array type on y.share[arbitraryName]
. - If userA doesn't specify
options.share[arbitraryName]
, it won't be
available for userA. - If userB specifies
options.share[arbitraryName]
, it still won't be
available for userA. But all the updates are send from userB to userA. - In contrast to y-map, types on
y.share.*
cannot be overwritten or deleted.
Instead, they are merged among all users. This feature is only available on
y.share.*
- Weird behavior: It is supported that two users specify different types with
the same property name.
E.g. userA specifies
options.share.x = 'Array'
, and userB specifies
options.share.x = 'Text'
. But they only share data if they specified the
same type with the same property name
- options.type (browser only)
- Array of modules that Yjs needs to require, before instantiating a shared
type.
- By default Yjs requires the specified database adapter, the specified
connector, and all modules that are used in
options.share.*
- Put all types here that you intend to use, but are not used in y.share.*
Instantiated Y object (y)
Y(options)
returns a promise that is fulfilled when..
- All modules are loaded
- The specified database adapter is loaded
- The specified connector is loaded
- All types are included
- The connector is initialized, and a unique user id is set (received from the
server)
- Note: When using y-indexeddb, a retrieved user id is stored on
localStorage
The promise returns an instance of Y. We denote it with a lower case y
.
- y.share.*
- Instances of the types you specified on options.share.*
- y.share.* can only be defined once when you instantiate Y!
- y.connector is an instance of Y.AbstractConnector
- y.connector.onUserEvent(function (event) {..})
- Observe user events (event.action is either 'userLeft' or 'userJoined')
- y.connector.whenSynced(listener)
listener
is executed when y synced with at least one user.listener
is not called when no other user is in the same room.- y-websockets-client aways waits to sync with the server
- y.connector.disconnect()
- Force to disconnect this instance from the other instances
- y.connector.connect()
- Try to reconnect to the other instances (needs to be supported by the
connector)
- Not supported by y-xmpp
- y.close()
- Destroy this object.
- Destroys all types (they will throw weird errors if you still use them)
- Disconnects from the other instances (via connector)
- Returns a promise
- y.destroy()
- calls y.close()
- Removes all data from the database
- Returns a promise
- y.db.stopGarbageCollector()
- Stop the garbage collector. Call y.db.garbageCollect() to continue garbage
collection
- y.db.gc :: Boolean
- y.db.gcTimeout :: Number (defaults to 50000 ms)
- Time interval between two garbage collect cycles
- It is required that all instances exchanged all messages after two garbage
collect cycles (after 100000 ms per default)
- y.db.userId :: String
- The used user id for this client. Never overwrite this
Logging
Yjs uses debug for logging. The flag
y*
enables logging for all y-* components. You can selectively remove
components you are not interested in: E.g. The flag y*,-y:connector-message
will not log the long y:connector-message
messages.
Enable logging in Node.js
DEBUG=y* node app.js
Remove the colors in order to log to a file:
DEBUG_COLORS=0 DEBUG=y* node app.js > log
Enable logging in the browser
localStorage.debug = 'y*'
License
Yjs is licensed under the MIT License.