JS SDK
Through Yorkie JS SDK, you can efficiently build collaborative applications. On the client-side implementation, you can create Documents that are automatically synced with remote peers with minimal effort.
If you want to install the SDK, refer to the Getting Started with JS SDK.
Client
Client
is a regular client that communicates with the server. It has documents and sends local changes to the server to synchronize with other replicas remotely.
Creating a Client
You can create a Client using new yorkie.Client()
. After the Client has been activated, it is connected to the server and ready to use.
1const client = new yorkie.Client('https://api.yorkie.dev', {2 apiKey: 'xxxxxxxxxxxxxxxxxxxx',3});4await client.activate();
The API key is used to identify the project in Yorkie. You can get the API key of the project you created in the Dashboard.
Document
Document
is a primary data type in Yorkie, which provides a JSON-like updating experience that makes it easy to represent your application's model.
A Document
can be updated without being attached to the client, and its changes are automatically propagated to other clients when the Document
is attached to the Client
or when the network is restored.
Creating a Document
You can create a Document using yorkie.Document()
. Let's create a Document with a key and attach it to the Client.
1const doc = new yorkie.Document('doc-1');
The document key is used to identify the Document in Yorkie. It is a string that can be freely defined by the user.
However, it is allowed to use only a-z
, A-Z
, 0-9
, -
, .
, _
, ~
and must be less than 120 characters.
Attaching the Document
When you attach, the client notifies the server that it is subscribing to this document. If the document does not exist on the server, it will be created, and any local changes that occurred will be updated to the server's document. If the server already has a document associated with the provided key, it sends the existing changes to the client, which are then applied to synchronize the document.
Once attached, the document becomes synchronized with other clients. This ensures that any modifications made by one client are instantly propagated to other clients collaborating on the same document.
The second argument is options.
initialPresence
: Sets the initial presence of the client that attaches the document. The presence is shared with other users participating in the document. It must be serializable to JSON.syncMode
(Optional): Specifies synchronization modes. The default value isSyncMode.Realtime
, which automatically pushes and pulls changes. If you set it toSyncMode.Manual
, you'll need to manually handle synchronization.
1await client.attach(doc, {2 initialPresence: { color: 'blue', cursor: { x: 0, y: 0 } },3 syncMode: SyncMode.Manual,4});
Initializing root
The root is used to manage the application's data, such as primitives, arrays, Counters, and Text in a form within the Document
. You can set the initial values when calling Document.attach()
using the initialRoot
option.
1await client.attach(doc, {2 initialRoot: {3 list: [1, 2, 3],4 counter: new yorkie.Counter(yorkie.IntType, 0),5 },6});
The initial values are partially applied. For each element in initialRoot
:
- If the key doesn't exist, the element will be applied.
- If the key already exists in the Document, that element will be discarded. Users don't need to worry about overwriting existing valid counters.
1await client.attach(doc, {2 initialRoot: {3 list: [],4 },5});67// Another client tries to attach with initialRoot option:8await client.attach(doc, {9 initialRoot: {10 list: [1, 2, 3], // this update will be discarded11 counter: new yorkie.Counter(yorkie.IntType, 0), // this update will be applied12 },13});1415// final state16// root = {17// list: [],18// counter: {}19// }
We support element types for Primitives, and Custom CRDT types.
Elements added by initialRoot
are not sent to the server during the attach
process. They are applied locally to the Document after push-pull during attach
.
Updating presence
The Document.update()
method allows you to make changes to the state of the current user's presence.
Specific properties provided will be changed. The existing presence object will be updated by merging the new changes. In other words, properties not specified in the update function will remain unchanged.
1doc.update((root, presence) => {2 presence.set({ cursor: { x: 1, y: 1 } });3});45// final state6// presence = { color: 'blue', cursor: { x: 1, y: 1 } }7// we can see that the changes made were merged and the final state of the current user's presence is as we desire
Note, the properties provided will be replaced entirely and not merely updated.
For example:
1await client.attach(doc, {2 // let's say 'color' is a property of cursor3 initialPresence: {cursor: { x: 0, y: 0, color: 'red'}},4});56doc.update((root, presence) => {7 // we want to change the x y coordinates of our cursor8 presence.set({ cursor: { x: 1, y: 1 } });9});1011// final state12// presence = { cursor: { x: 1, y: 1 } }13// we can see that all properties inside cursor get replaced (i.e. we lose the property 'color')
Getting presence
You can get the presences of the current client and other clients participating in the document.
Document.getPresence(clientID)
It returns the presence of a specific client.
1doc.getPresence(client.getID()); // { color: 'blue', cursor: { x: 1, y: 1 } }
Document.getMyPresence()
It returns the presence of the current client that has attached to the document.
1doc.getMyPresence(); // { color: 'blue', cursor: { x: 1, y: 1 } }
Document.getPresences()
It returns an array about all clients currently participating in the document. Each entry in the array includes the client's id and presence.
1const users = doc.getPresences();2for (const { clientID, presence } of users ) {3 // Do something4}
Here is an example of showing a list of users participating in the collaborative application.
Document.subscribe('presence')
This method allows you to subscribe to presence-related changes. You'll be notified whenever clients watch, unwatch, or modify their presence.
The initialized
event occurs when the client list needs to be initialized.
For example, this happens when you first connect a watch stream to a document, when the connection is lost, or when it is reconnected.
Subscribe before attaching the document to ensure you receive the initial initialized
event.
1const unsubscribe = doc.subscribe('presence', (event) => {2 if (event.type === 'initialized') {3 // event.value: Array of users currently participating in the document4 }56 if (event.type === 'watched') {7 // event.value: A user has joined the document editing in online8 }910 if (event.type === 'unwatched') {11 // event.value: A user has left the document editing12 }1314 if (event.type === 'presence-changed') {15 // event.value: A user has updated their presence16 }17});
Use my-presence
and others
topics to distinguish between your own events and those of others.
Document.subscribe('my-presence')
This method is specifically for subscribing to changes in the presence of the current client that has attached to the document.
The possible event.type are: initialized
, presence-changed
.
1const unsubscribe = doc.subscribe('my-presence', (event) => {2 // Do something3});
Document.subscribe('others')
This method enables you to subscribe to changes in the presence of other clients participating in the document.
The possible event.type are: watched
, unwatched
, presence-changed
.
1const unsubscribe = doc.subscribe('others', (event) => {2 if (event.type === 'watched') {3 addUser(event.value);4 }56 if (event.type === 'unwatched') {7 removeUser(event.value);8 }910 if (event.type === 'presence-changed') {11 updateUser(event.value);12 }13});
Editing the Document
Document.update(changeFn, message)
enables you to modify a Document. The optional message
allows you to add a description to the change. If the Document is attached to the Client, all changes are automatically synchronized with other Clients.
1const message = 'update document for test';2doc.update((root) => {3 root.todos = [];4 root.todos.push('todo-1');5 root.obj = {6 name: 'yorkie',7 age: 14,8 };9 root.counter = new yorkie.Counter(yorkie.IntType, 0);10 root.counter.increase(1);11}, message);
Under the hood, root
in the update
function creates a change
, a set of operations, using a JavaScript proxy. Every element has its unique ID, created by the logical clock. This ID is used by Yorkie to track which object is which.
You can get the contents of the Document using document.getRoot()
.
1const root = doc.getRoot();2console.log(root.todos); // ["todo-1"]3console.log(root.obj); // {"name":"yorkie","age":14}4console.log(root.obj.name); // "yorkie"5console.log(root.counter.getValue()); // 1
Subscribing to Document
You can subscribe to various events occurring in the Document, such as changes, connection status, synchronization status, and all events by using the document.subscribe()
method.
By subscribing to these events, you can update the UI in real-time and handle exceptions that may occur during synchronization.
Document.subscribe()
A Document can be modified by changes generated remotely or locally in Yorkie. Whenever the Document is modified, change events are triggered and you can subscribe to these events using the document.subscribe(callback)
method.
By subscribing to changes in the Document, you can receive updates in real-time, which is useful for updating the UI when the Document changes.
The callback is called with an event object, and the event.type
property indicates the source of the change, which can be one of the following values: local-change
, remote-change
, or snapshot
.
1const unsubscribe = doc.subscribe((event) => {2 if (event.type === 'snapshot') {3 // `snapshot` delivered when the entire document is updated from the server.4 } else if (event.type === 'local-change') {5 // `local-change` delivered when calling document.update from the current client.6 } else if (event.type === 'remote-change') {7 // `remote-change` delivered when the document is updated from other clients.8 const { message, operations } = event.value;910 // You can access the operations that have been applied to the document.11 for (const op of operations) {12 // e.g.) { type: 'increase', value: 1, path: '$.counter' }13 switch (op.type) {14 case 'increase':15 // ...16 break;17 }18 }19 }20});
When the event.type
is local-change
or remote-change
, the event.value
is a changeInfo, which has {operations, message}
properties.
For more information about changeInfo for document events, please refer to the ChangeInfo.
The event.rawChange
value for local-change
and remote-change
events, and the event.value.snapshot
for snapshot
event, are set only when enableDevtools
option is configured as true
.
The snapshot
event is triggered when a snapshot is received from the server.
This occurs when the changes that a document needs to fetch from the server exceed a certain SnapshotThreshold
.
Instead of sending numerous changes, the server sends a snapshot of the document.
In such cases, it is essential to update with data from the Yorkie Document.
Refer to the example code for handling snapshots in CodeMirror.
If a client has not synchronized for a prolonged period and then makes a sync request, it might receive a snapshot
event.
Ensure your application processes these snapshot events correctly to maintain document synchronization.
Document.subscribe('$.path')
Additionally, you can subscribe to changes for a specific path in the Document using doc.subscribe(path, callback)
with a path argument, such as $.todos
, where the $
sign indicates the root of the document.
The callback function is called when the target path and its nested values are changed.
With this feature, you can easily subscribe to changes for a specific part of the document and perform different actions based on the updated values.
1// The event is triggered when the value of the path("$.todos") is changed.2const unsubscribe = doc.subscribe('$.todos', (event) => {3 // You can access the updated value of the path.4 const target = doc.getValueByPath('$.todos');5});
Document.subscribe('connection')
After attaching the document to the client, the document is continuously synchronized with the server in real-time. This is achieved by maintaining a watch stream connection between the client and the server, which allows the client to receive events and updates from other users.
To monitor the connection status of the stream, you can use a callback function that is triggered whenever the connection status changes. The possible values for event.value
are StreamConnectionStatus.Connected
and StreamConnectionStatus.Disconnected
.
When the watch stream is disconnected, it indicates that the user is offline and will not receive real-time updates from other users.
1const unsubscribe = doc.subscribe('connection', (event) => {2 if (event.value === StreamConnectionStatus.Connected) {3 // The watch stream is connected.4 } else if (event.value === StreamConnectionStatus.Disconnected) {5 // The watch stream is disconnected.6 }7});
For more information about StreamConnectionStatus
, please refer to the StreamConnectionStatus.
Document.subscribe('sync')
If the document is attached to the client in SyncMode.Realtime
, the document is automatically synchronized with the server in real-time.
Under this mode, the document executes synchronization in the background, and you can track the synchronization status using the sync
event. The possible event.value
values are: DocumentSyncStatus.Synced
and DocumentSyncStatus.SyncFailed
.
1const unsubscribe = doc.subscribe('sync', (event) => {2 if (event.value === DocumentSyncStatus.Synced) {3 // The document is synchronized with the server.4 } else if (event.value === DocumentSyncStatus.SyncFailed) {5 // The document failed to synchronize with the server.6 }7});
For more information about DocumentSyncStatus
, please refer to the DocumentSyncStatus.
Document.subscribe('status')
You can subscribe to the status of the document using doc.subscribe('status', callback)
. The possible values for event.value.status
are DocumentStatus.Attached
and DocumentStatus.Detached
.
1const unsubscribe = doc.subscribe('status', (event) => {2 if (event.value.status === DocumentStatus.Attached) {3 // The document is attached to the client.4 } else if (event.value.status === DocumentStatus.Detached) {5 // The document is detached from the client.6 }7});
For more information about DocumentStatus
, please refer to the DocumentStatus.
In Web-based applications, it is hard to detect when the user closes the browser or navigates to another page. In such cases, the document may remain attached to the client, which can lead to unefficient storage and memory usage.
For handle this, Yorkie provides one of housekeeping feature, client-deactivate-threshold
, which is a time interval to automatically deactivate the clients that have not been used for a certain period.
If the client is deactivated due to inactivity, the document will be detached from the client, and you will receive a DocumentStatus.Detached
event through the doc.subscribe('status', callback)
method.
For more information about client-deactivate-threshold
, please refer to the Client Deactivate Threashold.
Document.subscribe('all')
You can subscribe to all events occurring in the document by using document.subscribe('all', callback)
. This is used for displaying events in Devtools extension.
Events received from the callback function are of type TransactionEvent
, which is an Array<DocEvent>
. TransactionEvent represents a collection of events occurring within a single transaction (e.g., doc.update()
).
1const unsubscribe = doc.subscribe('all', (transactionEvent) => {2 for (const docEvent of transactionEvent) {3 console.log(docEvent);4 }5});
Changing Synchronization Mode
To change the synchronization mode for a document, you can use client.changeSyncMode(doc, syncMode)
.
Yorkie offers four SyncModes:
-
SyncMode.Realtime
: Local changes are automatically pushed to the server, and remote changes are pulled from the server. -
SyncMode.RealtimePushOnly
: Only local changes are pushed, and remote changes are not pulled. -
SyncMode.RealtimeSyncOff
: Changes are not synchronized, but the watch stream remains active. -
SyncMode.Manual
: Synchronization no longer occurs in real-time, and the watch stream is disconneted. Manual handling is required for synchronization.
1// Enable automatic synchronization of both local and remote changes.2await client.changeSyncMode(doc, SyncMode.Realtime);34// Only push local changes automatically.5await client.changeSyncMode(doc, SyncMode.RealtimePushOnly);67// Synchronization turned off, but the watch stream remains active.8await client.changeSyncMode(doc, SyncMode.RealtimeSyncOff);910// Synchronization turned off, and the watch stream is disconneted.11await client.changeSyncMode(doc, SyncMode.Manual);12await client.sync(doc); // Trigger synchronization manually using the sync function.
Detaching the Document
If the document is no longer used, it should be detached to increase the efficiency of GC removing CRDT tombstones. For more information about GC, please refer to Garbage Collection.
1await client.detach(doc);
Custom CRDT types
Custom CRDT types are data types that can be used for special applications such as text editors and counters, unlike general JSON data types such as Object
and Array
. Custom CRDT types can be created in the callback function of document.update
.
Text
Text
provides supports for collaborative text editing. In addition, contents in Text
can have attributes; for example, characters can be bold, italic, or underlined.
1doc.update((root) => {2 root.text = new yorkie.Text(); // {"text":""}3 root.text.edit(0, 0, 'hello'); // {"text":"hello"}4 root.text.edit(0, 1, 'H'); // {"text":"Hello"}5 root.text.setStyle(0, 1, { bold: true }); // {"text":"<b>H</b>ello"}6});
Selection using presence
The temporary client information, such as text selection, does not need to be stored in the document permanently.
Instead, it can be effectively shared using presence
.
When transmitting text selection information, it is essential to convert the index
, which can vary based on the text state, into the position
used by Yorkie.Text
.
This converted position selection can then be sent and applied through presence.
Here is an example where presence is used to share text selection between users in CodeMirror editor.
- When the text selection is changed:
1// Update selection through text editing2codemirror.on('beforeChange', (cm, change) => {3 doc.update((root, presence) => {4 const range = root.content.edit(from, to, content); // return updated index range5 presence.set({6 selection: root.content.indexRangeToPosRange(range), // update presence7 });8 });9});1011// Update selection using mouse or keyboard12codemirror.on('beforeSelectionChange', (cm, change) => {13 const fromIdx = cm.indexFromPos(change.ranges[0].anchor);14 const toIdx = cm.indexFromPos(change.ranges[0].head);15 doc.update((root, presence) => {16 presence.set({17 selection: root.content.indexRangeToPosRange([fromIdx, toIdx]), // update presence18 });19 });20});
- When applying other user's selection changes:
1doc.subscribe('others', (event) => {2 if (event.type === 'presence-changed') {3 const { clientID, presence } = event.value;4 const range = doc.getRoot().content.posRangeToIndexRange(presence.selection);5 // Handle the updated selection in the editor6 }7});
Text selection can be efficiently shared using presence. Please refer to the following example for a complete code:
- An example of Text co-editing with CodeMirror: CodeMirror example
- An example of Text co-editing with Quill: Quill example
Counter
Counter
supports integer types changing with addition and subtraction. If an integer data needs to be modified simultaneously, Counter
should be used instead of primitives.
1doc.update((root) => {2 root.counter = new yorkie.Counter(yorkie.IntType, 1); // {"counter":1}3 root.counter.increase(2); // {"counter":3}4 root.counter.increase(3); // {"counter":6}5 root.counter.increase(-4); // {"counter":2}6});
TypeScript Support
To use the Document more strictly, you can use type variable in TypeScript when creating a Document.
1import yorkie, { JSONArray } from 'yorkie-js-sdk';2type DocType = {3 list: JSONArray<number>;4 text: yorkie.Text;5};6type PresenceType = {7 username: string;8 color: string;9};1011const doc = new yorkie.Document<DocType, PresenceType>('key');12await client.attach(doc, {13 initialPresence: {14 username: 'alice',15 color: 'blue',16 },17});18doc.update((root, presence) => {19 root.list = [1, 2, 3];20 root.text = new yorkie.Text();21 presence.set({ color: 'red' });22});
Logger Options
The Logger outputs events occurring within the SDK to the console for debugging purposes. To modify these options, you can use the setLogLevel
function.
1import { setLogLevel, LogLevel } from 'yorkie-js-sdk';23setLogLevel(LogLevel.Error); // Display logs with Error or higher
The available log levels for setLogLevel
are:
LogLevel | Description |
---|---|
LogLevel.Trivial | Most verbose level, displays all logs |
LogLevel.Debug | Detailed information for debugging |
LogLevel.Info | General information |
LogLevel.Warn | Warnings and potential issues |
LogLevel.Error | Errors and unexpected behavior |
LogLevel.Fatal | Critical errors, may lead to termination |
Adjust the log level for flexible control over log verbosity in your application.
Reference
For details on how to use the JS SDK, please refer to JS SDK Reference.