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 normal client that can communicate with the server. It has documents and sends changes of the document from local to the server to synchronize with other replicas remotely.

Creating a Client

We 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.

Subscribing to Client events

We can use client.subscribe to subscribe to client-based events, such as status-changed, stream-connection-status-changed.

1const unsubscribe = client.subscribe((event) => {
2 if (event.type === 'status-changed') {
3 console.log(event.value); // 'activated' or 'deactivated'
4 } else if (event.type === 'stream-connection-status-changed') {
5 console.log(event.value); // 'connected' or 'disconnected'
6 }
7});

By using the value of the stream-connection-status-changed event, it is possible to determine whether the Client is connected to the network.

If you want to know about other client events, please refer to the ClientEvent, and ClientEventType.

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

We 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.
  • isRealtimeSync(Optional): Specifies whether to enable real-time synchronization. The default value is true, which means synchronization occurs automatically. If set to false, you should manually control the synchronization.
1await client.attach(doc, {
2 initialPresence: { color: 'blue', cursor: { x: 0, y: 0 } },
3 isRealtimeSync: true,
4});

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});
4
5// final state
6// 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 cursor
3 initialPresence: {cursor: { x: 0, y: 0, color: 'red'}},
4});
5
6doc.update((root, presence) => {
7 // we want to change the x y coordinates of our cursor
8 presence.set({ cursor: { x: 1, y: 1 } });
9});
10
11
12// final state
13// presence = { cursor: { x: 1, y: 1 } }
14// we can see that all properties inside cursor get replaced (i.e. we lose the property 'color')

Getting presence

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 something
4}

Displaying users

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 all presence-related changes. By subscribing to these events, you can be notified when specific changes occur within the document, such as clients attaching, detaching, or modifying their presence.

1const unsubscribe = doc.subscribe("presence", (event) => {
2 if (event.type === "initialized") {
3 // Array of other users currently participating in the document
4 // event.value;
5 }
6
7 if (event.type === "watched") {
8 // A user has joined the document editing in online
9 // event.value;
10 }
11
12 if (event.type === "unwatched") {
13 // A user has left the document editing
14 // event.value;
15 }
16
17 if (event.type === "presence-changed") {
18 // A user has updated their presence
19 // event.value;
20 }
21});
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 something
3});
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 }
5
6 if (event.type === "unwatched") {
7 removeUser(event.value);
8 }
9
10 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

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 we can subscribe to these events using the document.subscribe(callback).

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.

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.

1const unsubscribe = doc.subscribe((event) => {
2 if (event.type === 'local-change') {
3 console.log(event);
4 } else if (event.type === 'remote-change') {
5 // `message` delivered when calling document.update
6 const { message, operations } = event.value;
7
8 for (const op of operations) {
9 // ex) { type: 'increase', value: 1, path: '$.counter' }
10 switch (op.type) {
11 case 'increase':
12 // Do something...
13 break;
14 }
15 }
16 }
17});
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.

1const unsubscribe = doc.subscribe('$.todos', (event) => {
2 // The callback will be called when the root.todos or any of its nested values change.
3 const target = doc.getValueByPath('$.todos') // you can get the value by path
4 // Do something...
5});

Changing Synchronization Setting

To change the synchronization setting for a document, you can use client.pause(doc) and client.resume(doc).

When you pause a document, the synchronization process will no longer occur in realtime, and you will need to manually execute the synchronization to ensure that the changes are propagated to other clients.

To resume the realtime synchronization, you can call client.resume(doc).

1// 1. Pause real-time sync
2await client.pause(doc);
3await client.sync(doc); // To perform synchronization, you need to manually call the sync function
4
5// 2. Resume real-time sync
6await client.resume(doc);

Changing Synchronization Mode

By default, Yorkie synchronizes a document in push-pull mode, where local changes are pushed to the server, and remote changes are pulled from the server.

If you only want to send your changes and not receive remote changes, you can use push-only mode.

For realtime synchronization, you can use client.pauseRemoteChanges(doc) and client.resumeRemoteChanges(doc).

For manual synchronization, you can pass the desired sync mode to client.sync(doc, syncMode).

1// Pause remote changes for realtime sync
2client.pauseRemoteChanges(doc);
3// Resume remote changes for realtime sync
4client.resumeRemoteChanges(doc);
5
6// Manual sync in Push-Only mode
7await client.sync(doc, SyncMode.PushOnly);
8// Manual sync in Push-Pull mode
9await client.sync(doc, SyncMode.PushPull);

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 editing
2codemirror.on('beforeChange', (cm, change) => {
3 doc.update((root, presence) => {
4 const range = root.content.edit(from, to, content); // return updated index range
5 presence.set({
6 selection: root.content.indexRangeToPosRange(range), // update presence
7 });
8 });
9});
10
11// Update selection using mouse or keyboard
12codemirror.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 presence
18 });
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 editor
6 }
7});

Text selection can be efficiently shared using presence. Please refer to the following example for a complete code:

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, we 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};
10
11const 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';
2
3setLogLevel(LogLevel.Error); // Display logs with Error or higher

The available log levels for setLogLevel are:

LogLevelDescription
LogLevel.TrivialLowest level of verbosity
LogLevel.DebugDebugging information
LogLevel.InfoGeneral information
LogLevel.WarnWarnings and potential issues
LogLevel.ErrorErrors and unexpected behavior
LogLevel.FatalCritical 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.