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({
2 rpcAddr: 'https://api.yorkie.dev',
3 apiKey: 'xxxxxxxxxxxxxxxxxxxx',
4});
5await 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.

Creating a Client with Auth Token

You can configure authentication for the client by setting up an authTokenInjector to provide tokens for Auth Webhook verification. If a codes.Unauthenticated error occurs, the authTokenInjector will be called again with the webhook's response reason, enabling automatic token refresh. For more information about Auth Webhook, please refer to the Auth Webhook.

1const client = new yorkie.Client({
2 rpcAddr: 'https://api.yorkie.dev',
3 apiKey: 'xxxxxxxxxxxxxxxxxxxx',
4 authTokenInjector: async (reason) => {
5 // Handle token refresh logic based on webhook response
6 if (reason === 'token expired') {
7 return await refreshAccessToken();
8 }
9 return accessToken;
10 },
11});
Measuring MAU

You can measure MAU(Monthly Active Users) by setting the userID metadata when creating a client. The userID should be a unique identifier for each user.

1const client = new yorkie.Client({
2 rpcAddr: 'https://api.yorkie.dev',
3 apiKey: 'xxxxxxxxxxxxxxxxxxxx',
4 metadata: { userID: 'user-1234' },
5});

You can check the MAU in the Dashboard by selecting the project and clicking the overview tab.

Deactivate the Client

When the client is no longer needed, you can deactivate it to release resources and disconnect from the server.

1await client.deactivate();

This will detach all documents attached to the client for efficient garbage collection.

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 is SyncMode.Realtime, which automatically pushes and pulls changes. If you set it to SyncMode.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});
6
7// Another client tries to attach with initialRoot option:
8await client.attach(doc, {
9 initialRoot: {
10 list: [1, 2, 3], // this update will be discarded
11 counter: new yorkie.Counter(yorkie.IntType, 0), // this update will be applied
12 },
13});
14
15// final state
16// 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});
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// final state
12// 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 something
4}

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 document
4 }
5
6 if (event.type === 'watched') {
7 // event.value: A user has joined the document editing in online
8 }
9
10 if (event.type === 'unwatched') {
11 // event.value: A user has left the document editing
12 }
13
14 if (event.type === 'presence-changed') {
15 // event.value: A user has updated their presence
16 }
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 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

Converting Document Data to Plain JavaScript Objects

You can convert Yorkie's JSONObject types to plain JavaScript objects using the toJS() method. This is useful when you need to serialize document data or integrate with other libraries that expect plain objects.

1const root = doc.getRoot();
2
3// Converting the entire root to a plain object
4const plainObject = root.toJS();
5console.log(plainObject); // Plain JavaScript object
6
7// Converting specific nested objects
8const plainTodos = root.todos.toJS();
9const plainObj = root.obj.toJS();
The toJS() method creates a deep copy of the data as a plain JavaScript object, removing all Yorkie-specific metadata and proxies.

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;
9
10 // 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: DocSyncStatus.Synced and DocSyncStatus.SyncFailed.

1const unsubscribe = doc.subscribe('sync', (event) => {
2 if (event.value === DocSyncStatus.Synced) {
3 // The document is synchronized with the server.
4 } else if (event.value === DocSyncStatus.SyncFailed) {
5 // The document failed to synchronize with the server.
6 }
7});

For more information about DocSyncStatus, please refer to the DocSyncStatus.

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 DocStatus.Attached, DocStatus.Detached, and DocStatus.Removed.

1const unsubscribe = doc.subscribe('status', (event) => {
2 if (event.value.status === DocStatus.Attached) {
3 // The document is attached to the client.
4 } else if (event.value.status === DocStatus.Detached) {
5 // The document is detached from the client.
6 } else if (event.value.status === DocStatus.Removed) {
7 // The document is removed and cannot be edited
8 }
9});

For more information about DocStatus, please refer to the DocStatus.

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 inefficient storage and memory usage. To 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 Threshold.

Document.subscribe('auth-error')

You can subscribe to authentication error events using doc.subscribe('auth-error', callback). This event is triggered when an unauthenticated error occurs during PushPull or WatchDocuments operations, specifically when the system automatically refreshes the token and retries the operation.

1const unsubscribe = doc.subscribe('auth-error', (event) => {
2 console.log(event.value);
3 // event.value contains:
4 // - reason: string
5 // - method: 'PushPull' | 'WatchDocuments'
6});

This subscription allows you to monitor when token refreshes occur due to authentication errors.
For more information about Auth Webhook, please refer to the Auth Webhook.

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 disconnected. Manual handling is required for synchronization.

1// Enable automatic synchronization of both local and remote changes.
2await client.changeSyncMode(doc, SyncMode.Realtime);
3
4// Only push local changes automatically.
5await client.changeSyncMode(doc, SyncMode.RealtimePushOnly);
6
7// Synchronization turned off, but the watch stream remains active.
8await client.changeSyncMode(doc, SyncMode.RealtimeSyncOff);
9
10// Synchronization turned off, and the watch stream is disconnected.
11await client.changeSyncMode(doc, SyncMode.Manual);
12await client.sync(doc); // Trigger synchronization manually using the sync function.

Managing Document Revisions

Yorkie provides revision management capabilities that allow you to save snapshots of your document at specific points in time, browse through document history, and restore previous versions. This is useful for implementing features like version control, undo/redo functionality, or audit trails in your collaborative applications.

Creating a Revision

You can create a revision (snapshot) of the current document state using client.createRevision(). A revision captures the entire document state at the moment of creation.

1// Create a revision with a label
2const revision = await client.createRevision(doc, 'v1.0');
3
4// Create a revision with a label and description
5const revision = await client.createRevision(
6 doc,
7 'Feature complete',
8 'Added user authentication and profile management'
9);

Parameters:

  • doc: The document to create a revision for
  • label: A short, descriptive label for the revision (required)
  • description (Optional): A detailed description of what changed in this revision

Revisions are stored on the server and can be accessed by any client with access to the document. They persist independently of the document's current state.

Listing Revisions

Retrieve a list of all revisions for a document using client.listRevisions(). This returns an array of revision summaries containing metadata about each revision.

1// Get the first 50 revisions (newest first)
2const revisions = await client.listRevisions(doc);
3
4// Get revisions with pagination
5const revisions = await client.listRevisions(doc, {
6 pageSize: 20,
7 offset: 0,
8 isForward: false, // false = newest first, true = oldest first
9});
10
11// Display revision information
12revisions.forEach(revision => {
13 console.log(`ID: ${revision.id}`);
14 console.log(`Label: ${revision.label}`);
15 console.log(`Description: ${revision.description}`);
16 console.log(`Created at: ${revision.createdAt}`);
17 console.log(`Server Sequence: ${revision.serverSeq}`);
18});

Available options:

  • pageSize (Optional, default: 50): Number of revisions to retrieve per request
  • offset (Optional, default: 0): Number of revisions to skip for pagination
  • isForward (Optional, default: false): Sort order - false for newest first, true for oldest first

Each revision summary includes:

  • id: Unique identifier for the revision
  • label: The label provided when creating the revision
  • description: Optional description of the revision
  • createdAt: Timestamp when the revision was created
  • serverSeq: Server sequence number at the time of revision creation
Getting Revision Details

Get detailed information about a specific revision, including its full snapshot data, using client.getRevision():

1// Get a specific revision by ID
2const revision = await client.getRevision(doc, revisionId);
3
4// Access the revision's snapshot, YSON representation
5const snapshot = revision.snapshot;
6console.log('Revision data:', snapshot);

The returned revision object contains:

  • All metadata from the revision summary
  • snapshot: document state at the time of the revision, YSON representation

The snapshot contains the full document data as it existed when the revision was created. This data can be used for comparison, preview, or restoration purposes.

Parsing YSON Snapshots

Revision snapshots are stored in YSON (Yorkie JSON) format, which is Yorkie's internal representation of document data. The JS SDK provides a YSON parser that allows you to parse these snapshots and convert them to usable data structures in your application.

This is particularly useful when you need to:

  • Display a preview of a revision's content
  • Compare different revisions
  • Extract specific data from historical snapshots
  • Convert Yorkie data types (like Text) to plain strings
1import { YSON } from '@yorkie-js/sdk';
2
3// Get a revision with its snapshot
4const revision = await client.getRevision(doc, revisionId);
5
6if (revision.snapshot) {
7 try {
8 // Parse the YSON snapshot into Yorkie's internal data structure
9 const root = YSON.parse(revision.snapshot);
10
11 // Access the parsed data
12 console.log(root.title); // Access primitive values
13 console.log(root.todos); // Access arrays
14 console.log(root.obj.name); // Access nested objects
15 } catch (error) {
16 console.error('Failed to parse YSON:', error);
17 }
18}

Parsing with TypeScript types:

You can provide type information for better type safety when parsing YSON snapshots:

1import { YSON } from '@yorkie-js/sdk';
2
3type DocType = {
4 title: string;
5 content: YSON.Text;
6 todos: Array<string>;
7};
8
9const revision = await client.getRevision(doc, revisionId);
10
11if (revision.snapshot) {
12 const root = YSON.parse<DocType>(revision.snapshot);
13 // root is now typed as DocType
14 console.log(root.title);
15}

Converting Text to String:

When working with Yorkie's Text type in snapshots, you can use YSON.textToString() to convert it to a plain string:

1import { YSON } from '@yorkie-js/sdk';
2
3const revision = await client.getRevision(doc, revisionId);
4
5if (revision.snapshot) {
6 const root = YSON.parse<{ content: YSON.Text }>(revision.snapshot);
7
8 // Convert Yorkie Text to plain string
9 const plainText = YSON.textToString(root.content);
10 console.log(plainText);
11
12 // You can now use this string in your application
13 // For example, display it in a text editor or compare with other versions
14}

The YSON parser is designed to handle Yorkie's internal data structures. If you're working with standard Document data, you may also consider using the toJS() method on the live document for a simpler conversion to plain JavaScript objects.

Restoring a Revision

Restore the document to a previous revision using client.restoreRevision(). This replaces the current document state with the state from the specified revision.

1// Restore to a specific revision
2await client.restoreRevision(doc, revisionId);
3
4// Sync to ensure the restored state is propagated
5await client.sync();

Restoring a revision replaces the entire current document state with the revision's snapshot. This operation cannot be undone automatically, so consider creating a new revision before restoring to preserve the current state if needed.

All clients currently attached to the document will receive the restored state through their sync mechanisms.

Here's a complete example of using the revision API:

1// Create a client and attach a document
2const client = new yorkie.Client({
3 rpcAddr: 'https://api.yorkie.dev',
4 apiKey: 'xxxxxxxxxxxxxxxxxxxx',
5});
6await client.activate();
7
8const doc = new yorkie.Document('my-document');
9await client.attach(doc);
10
11// Make some changes
12doc.update((root) => {
13 root.title = 'My Document';
14 root.content = 'Initial content';
15});
16
17// Create a revision to save this state
18const v1 = await client.createRevision(doc, 'v1.0', 'Initial version');
19
20// Make more changes
21doc.update((root) => {
22 root.content = 'Updated content';
23});
24
25// Create another revision
26const v2 = await client.createRevision(doc, 'v2.0', 'Updated content');
27
28// List all revisions
29const revisions = await client.listRevisions(doc);
30console.log(`Found ${revisions.length} revisions`);
31
32// Get details of a specific revision
33const revisionDetails = await client.getRevision(doc, v1.id);
34console.log('V1.0 snapshot:', revisionDetails.snapshot);
35
36// Restore to v1.0
37await client.restoreRevision(doc, v1.id);
38await client.sync();
39
40console.log('Restored to v1.0');

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);

When detaching a document, you can provide additional options to control the detachment behavior:

1await client.detach(doc, {
2 removeIfNotAttached: true
3});

Available options:

  • removeIfNotAttached (Optional, boolean): When set to true, the document will be completely removed from the server if no other clients are attached to it. This is useful for cleaning up temporary documents or sessions. Default is false.
Use removeIfNotAttached: true carefully, as it will permanently delete the document from the server if no other clients are currently attached. This action cannot be undone.
Project-level Remove on Detach Setting

The Remove on Detach setting is a project-level configuration that enforces server-side removal when a detach leaves the document with zero attached clients. It automatically applies to all documents in the project; you don't need to pass removeIfNotAttached: true on individual detach calls.

When the Remove on Detach setting is enabled, it overrides any removeIfNotAttached value passed to client.detach(). Even if you set removeIfNotAttached: false, the server will remove the document once no other clients are attached at detach time.

Document Limits

Yorkie enforces certain limits on document operations to ensure system stability and optimal performance. These limits can be configured through the Yorkie Dashboard's Project Settings.

When you modify limit settings in the dashboard, the changes may take up to 10 minutes to be applied across all servers due to caching. During this period, different servers might temporarily have different limit values.

Max Attachments per Document

When you attach a document, the server keeps track of the number of clients subscribed to it. The Max Attachments per Document setting determines how many clients can be attached to a single document simultaneously. If a client attempts to attach when this limit is reached, the server will reject the attachment with an error.

If the attachment fails due to the limit, the client will need to retry the attachment by either reloading the SDK or calling attach again, as there is no automatic retry mechanism.

Max Subscribers per Document

When a client attaches to a document, it creates a subscription stream to receive real-time updates. The Max Subscribers per Document setting determines how many clients can maintain subscription streams simultaneously for a single document. If a client attempts to create a subscription stream when this limit is reached, the server will reject the request.

Unlike attachment limits, the SDK automatically handles subscription limit errors by retrying the connection (1 retry per second).

When the subscription limit is exceeded, you can monitor the connection status through the Document.subscribe('connection') callback. The SDK will receive a StreamConnectionStatus.Disconnected event, indicating that the watch stream is disconnected.

1doc.subscribe('connection', (event) => {
2 if (event.value === StreamConnectionStatus.Disconnected) {
3 // The connection is disconnected due to subscription limit or other issues
4 console.log('Watch stream disconnected, retrying automatically...');
5 }
6});
Max Document Size Limit

When a client attaches to a document, the maximum allowed size is determined by the maxSizePerDocument setting of the associated project. This limit is evaluated at each attachment, so the same document may be subject to different size constraints depending on the client's project configuration.

A document’s size consists of two main components: live data and garbage-collected (GC) data. Each of these includes both content data (e.g., visible text or structured objects) and meta data (e.g., CRDT metadata for synchronization and conflict resolution). As a result, the actual document size may exceed what is visibly rendered in the editor.
For a breakdown of how data size is measured, see resource.ts.

The size limit is strictly enforced during local updates. If an update causes the document to exceed the limit, it is rejected locally and not pushed to the server.

However, remote updates are not subject to this limit and are always applied, regardless of size. This ensures state convergence across clients—a critical requirement for distributed collaborative systems.

Garbage-collected data is automatically reduced over time through system-managed garbage collection. While manual GC is not exposed, the system triggers GC under certain conditions. For instance, clients that remain inactive beyond the configured Client Deactivate Threshold are automatically deactivated, improving GC efficiency and reducing document GC size over time.
For details on GC and housekeeping, refer to the housekeeping design document.

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, 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};
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});

Channel

Channel is a communication channel that enables real-time pub/sub messaging and presence tracking between clients. Unlike Documents which persist and synchronize data, Channels are designed for ephemeral, real-time communication such as chat messages, notifications, and presence indicators.

Creating a Channel

You can create a Channel with a unique identifier and attach it to the Client:

1const channel = new yorkie.Channel('room-123');
2await client.attach(channel);

The channel key is used to identify the Channel in Yorkie. Clients with the same channel key can communicate with each other through pub/sub messaging.

Hierarchical Channel Keys

Channels support a hierarchical structure using periods (.) as separators, allowing you to organize channels into logical groups and subgroups. This is particularly useful for managing complex applications with multiple rooms, sections, or nested contexts.

1// Create channels with hierarchical keys
2const roomChannel = new yorkie.Channel('room-1');
3const sectionChannel = new yorkie.Channel('room-1.section-a');
4const userChannel = new yorkie.Channel('room-1.section-a.user-123');
5
6await client.attach(roomChannel);
7await client.attach(sectionChannel);
8await client.attach(userChannel);

Each level in the hierarchy maintains its own presence count and can be used independently. For example:

  • room-1 might track all users in a game room
  • room-1.section-a might track users in a specific section of that room
  • room-1.section-a.user-123 might track presence for a specific user's context

Channel Key Restrictions:

When using hierarchical channel keys, certain patterns are not allowed:

  1. Keys cannot start or end with a period:

    • .room-1
    • room-1.
    • .room-1.section-1.
  2. Keys cannot contain consecutive periods:

    • room-1..section-1
  3. Valid examples:

    • room-1
    • room-1.section-1
    • room-1.section-1.user-123

Important: Distribute First-Level Channel Keys

Yorkie servers determine which server handles a channel based on a combination of your project and the first-level key of the channel. Using the same first-level key for all channels means all your channels will be managed by the same server, which can lead to performance bottlenecks and uneven load distribution.

Good practice - Distributed first-level keys:

1new yorkie.Channel('game-1.room-a');
2new yorkie.Channel('game-2.room-b');
3new yorkie.Channel('chat-1.thread-1');
4new yorkie.Channel('notification-1.user-123');

Bad practice - Same first-level key:

1// All channels share 'app' as first level - avoid this!
2new yorkie.Channel('app.game-1');
3new yorkie.Channel('app.game-2');
4new yorkie.Channel('app.chat-1');
5new yorkie.Channel('app.notification-1');

Design your channel key structure to naturally distribute traffic across different first-level prefixes for optimal performance.

You can query hierarchical channel information using the GetChannels API, which allows you to retrieve presence counts for specific channels and optionally include counts from all sub-levels.

Publishing and Subscribing to Messages

Channels support a pub/sub pattern for broadcasting ephemeral messages to all connected clients:

1// Subscribe to messages on a specific topic
2const unsubscribe = channel.subscribe('chat', (event) => {
3 console.log(`[${event.topic}] ${event.payload.message}`);
4 console.log(`From: ${event.publisher}`);
5});
6
7// Broadcast a message to all clients in the same channel
8await channel.broadcast('chat', { message: 'Hello, world!' });

The event object contains:

  • topic: The topic name you subscribed to
  • payload: The message data (must be JSON serializable)
  • publisher: The client ID of the message sender

You can subscribe to multiple topics on the same channel:

1// Subscribe to different topics
2channel.subscribe('chat', (event) => {
3 console.log('Chat message:', event.payload);
4});
5
6channel.subscribe('notification', (event) => {
7 console.log('Notification:', event.payload);
8});
9
10// Broadcast to different topics
11await channel.broadcast('chat', { message: 'Hello!' });
12await channel.broadcast('notification', { type: 'user-joined', user: 'Alice' });

Broadcast messages are ephemeral and only delivered to clients currently connected to the channel. Messages are not persisted or stored, so clients that join later will not receive previous messages.

Tracking Online Presence

Channels automatically track the number of connected clients, making it easy to display "users online" counters:

1// Subscribe to presence changes
2const unsubscribe = channel.subscribe('presence', (event) => {
3 console.log(`Users online: ${event.count}`);
4});
5
6// Get the current presence count
7const count = channel.getPresenceCount();
8console.log(`Currently ${count} users online`);

The presence count is automatically updated when clients connect or disconnect from the channel.

Detaching a Channel

When you're done with a channel, detach it to clean up resources and stop receiving messages:

1await client.detach(channel);

Use Cases

Channels are ideal for:

  • Chat applications: Real-time messaging without persistence
  • Live notifications: Temporary alerts and updates
  • Presence indicators: Showing who's online or in a room
  • Cursor sharing: Broadcasting cursor positions in collaborative tools
  • Event broadcasting: Sending signals or triggers between clients

For persistent data that needs to be synchronized and stored, use Document instead.

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.TrivialMost verbose level, displays all logs
LogLevel.DebugDetailed information for debugging
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.