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 in remote.

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 and peer-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 ClientEventType.

Presence

Presence is a feature that allows you to display information about users who are currently using a collaborative application. Presence is often used in collaborative applications such as document editors, chat apps, and other real-time applications.

1const clientA = new yorkie.Client('https://api.yorkie.dev', {
2 presence: {
3 username: 'alice',
4 color: 'blue',
5 },
6});
7await client.activate();
8
9const docA = new yorkie.Document('doc-1');
10await clientA.attach(docA);

Then, another Client is created and attaches a Document with the same name as before.

1const clientB = new yorkie.Client('https://api.yorkie.dev', {
2 presence: {
3 username: 'bob',
4 color: 'red',
5 },
6});
7await clientB.activate();
8
9const docB = new yorkie.Document('doc-1');
10await clientB.attach(docB);

When a new peer registers or leaves, the peers-changed event is fired, and the other peer's clientID and presence can be obtained from the event.

1const unsubscribe = clientA.subscribe((event) => {
2 if (event.type === 'peers-changed') {
3 const peers = event.value[doc.getKey()];
4 for (const [clientID, presence] of Object.entries(peers)) {
5 console.log(clientID, presence); // e.g.) presence: {username: 'bob', color: 'red'}
6 }
7 }
8});

In the code above, clientA will receive information from clientB.

Presence can include their names, colors, and other identifying details. Here is an example of how Presence might be used in a collaborative document editor:

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 peers 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');
2await client.attach(doc);

After attaching the Document to the Client, all changes to the Document are automatically synchronized with remote peers.

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.obj = {}; // {"obj":{}}
4 root.obj.num = 1; // {"obj":{"num":1}}
5 root.obj.obj = { str: 'a' }; // {"obj":{"num":1,"obj":{"str":"a"}}}
6 root.obj.arr = ['1', '2']; // {"obj":{"num":1,"obj":{"str":"a"},"arr":[1,2]}}
7}, 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.obj); // {"num":1,"obj":{"str":"a"},"arr":[1,2]}
3console.log(root.obj.num); // 1
4console.log(root.obj.obj); // {"str":"a"}
5console.log(root.obj.arr); // [1,2]

Subscribing to Document events

A Document is modified by changes generated remotely or locally in Yorkie. When the Document is modified, change events occur, to which we can subscribe using document.subscribe. Here, we can do post-processing such as repaint in the application using the path of the change events.

1doc.subscribe((event) => {
2 if (event.type === 'local-change') {
3 console.log(event);
4 } else if (event.type === 'remote-change') {
5 for (const changeInfo of event.value) {
6 // `message` delivered when calling document.update
7 console.log(changeInfo.change.message);
8 for (const path of changeInfo.paths) {
9 if (path.startsWith('$.obj.num') {
10 // root.obj.num is changed
11 } else if (path.startsWith('$.obj')) {
12 // root.obj is changed
13 }
14 }
15 }
16 }
17});

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 plain text editing. Under the hood, Text is represented as a list of characters. Compared to a regular JavaScript array, Text offers better performance. It also has selection information for sharing the cursor position.

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.select(0, 1); // {"text":"^H^ello"}
6});

An example of Text co-editing with CodeMirror: CodeMirror example

RichText

RichText is similar to Text except that we can add attributes to contents.

1doc.update((root) => {
2 root.text = new yorkie.RichText(); // {"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});

An example of RichText co-editing with Quill: Quill example

Counter

Counter supports numeric types that change with addition and subtraction. If a numeric data needs to be modified at the same time, Counter should be used instead of primitives.

1doc.update((root) => {
2 root.counter = new yorkie.Counter(1); // {"counter":1}
3 root.counter.increase(2); // {"counter":3}
4 root.counter.increase(3.5); // {"counter":6.5}
5 root.counter.increase(-3.5); // {"counter":3}
6});

TypeScript Support

To use the Document more strictly, we can use type variable in TypeScript when creating a Document.

1type DocType = {
2 list: Array<number>;
3 text: yorkie.Text;
4};
5
6const doc = new yorkie.Document<DocType>('key');
7doc.update((root) => {
8 root.list = [1, 2, 3];
9 root.text = new yorkie.Text();
10});

Reference

For details on how to use the JS SDK, please refer to JS SDK Reference.