yjs

Yjs provides six shared types. You'll mostly use three:

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "yjs" with this command: npx skills add epicenterhq/epicenter/epicenterhq-epicenter-yjs

Yjs CRDT Patterns

Core Concepts

Shared Types

Yjs provides six shared types. You'll mostly use three:

  • Y.Map

  • Key-value pairs (like JavaScript Map)

  • Y.Array

  • Ordered lists (like JavaScript Array)

  • Y.Text

  • Rich text with formatting

The other three (Y.XmlElement , Y.XmlFragment , Y.XmlText ) are for rich text editor integrations.

Client ID

Every Y.Doc gets a random clientID on creation. This ID is used for conflict resolution—when two clients write to the same key simultaneously, the higher clientID wins, not the later timestamp.

const doc = new Y.Doc(); console.log(doc.clientID); // Random number like 1090160253

From dmonad (Yjs creator):

"The 'winner' is decided by ydoc.clientID of the document (which is a generated number). The higher clientID wins."

— GitHub issue #520

The actual comparison in source (updates.js#L357):

return dec2.curr.id.client - dec1.curr.id.client; // Higher clientID wins

This is deterministic (all clients converge to same state) but not intuitive (later edits can lose).

Shared Types Cannot Move

Once you add a shared type to a document, it can never be moved. "Moving" an item in an array is actually delete + insert. Yjs doesn't know these operations are related.

Critical Patterns

  1. Single-Writer Keys (Counters, Votes, Presence)

Problem: Multiple writers updating the same key causes lost writes.

// BAD: Both clients read 5, both write 6, one click lost function increment(ymap) { const count = ymap.get('count') || 0; ymap.set('count', count + 1); }

Solution: Partition by clientID. Each writer owns their key.

// GOOD: Each client writes to their own key function increment(ymap) { const key = ymap.doc.clientID; const count = ymap.get(key) || 0; ymap.set(key, count + 1); }

function getCount(ymap) { let sum = 0; for (const value of ymap.values()) { sum += value; } return sum; }

  1. Fractional Indexing (Reordering)

Problem: Drag-and-drop reordering with delete+insert causes duplicates and lost updates.

// BAD: "Move" = delete + insert = broken function move(yarray, from, to) { const [item] = yarray.delete(from, 1); yarray.insert(to, [item]); }

Solution: Add an index property. Sort by index. Reordering = updating a property.

// GOOD: Reorder by changing index property function move(yarray, from, to) { const sorted = [...yarray].sort((a, b) => a.get('index') - b.get('index')); const item = sorted[from];

const earlier = from > to;
const before = sorted[earlier ? to - 1 : to];
const after = sorted[earlier ? to : to + 1];

const start = before?.get('index') ?? 0;
const end = after?.get('index') ?? 1;

// Add randomness to prevent collisions
const index = (end - start) * (Math.random() + Number.MIN_VALUE) + start;
item.set('index', index);

}

  1. Nested Structures for Conflict Avoidance

Problem: Storing entire objects under one key means any property change conflicts with any other.

// BAD: Alice changes nullable, Bob changes default, one loses schema.set('title', { type: 'text', nullable: true, default: 'Untitled', });

Solution: Use nested Y.Maps so each property is a separate key.

// GOOD: Each property is independent const titleSchema = schema.get('title'); // Y.Map titleSchema.set('type', 'text'); titleSchema.set('nullable', true); titleSchema.set('default', 'Untitled'); // Alice and Bob edit different keys = no conflict

Storage Optimization

Y.Map vs Y.Array for Key-Value Data

Y.Map tombstones retain the key forever. Every ymap.set(key, value) creates a new internal item and tombstones the previous one.

For high-churn key-value data (frequently updated rows), consider YKeyValue from yjs/y-utility :

// YKeyValue stores {key, val} pairs in Y.Array // Deletions are structural, not per-key tombstones import { YKeyValue } from 'y-utility/y-keyvalue';

const kv = new YKeyValue(yarray); kv.set('myKey', { data: 'value' });

When to use Y.Map: Bounded keys, rarely changing values (settings, config). When to use YKeyValue: Many keys, frequent updates, storage-sensitive.

Epoch-Based Compaction

If your architecture uses versioned snapshots, you get free compaction:

// Compact a Y.Doc by re-encoding current state const snapshot = Y.encodeStateAsUpdate(doc); const freshDoc = new Y.Doc({ guid: doc.guid }); Y.applyUpdate(freshDoc, snapshot); // freshDoc has same content, no history overhead

Common Mistakes

  1. Assuming "Last Write Wins" Means Timestamps

It doesn't. Higher clientID wins, not later timestamp. Design around this or add explicit timestamps with y-lwwmap .

  1. Using Y.Array Position for User-Controlled Order

Array position is for append-only data (logs, chat). User-reorderable lists need fractional indexing.

  1. Forgetting Document Integration

Y types must be added to a document before use:

// BAD: Orphan Y.Map const orphan = new Y.Map(); orphan.set('key', 'value'); // Works but doesn't sync

// GOOD: Attached to document const attached = doc.getMap('myMap'); attached.set('key', 'value'); // Syncs to peers

  1. Storing Non-Serializable Values

Y types store JSON-serializable data. No functions, no class instances, no circular references.

  1. Expecting Moves to Preserve Identity

// This creates a NEW item, not a moved item yarray.delete(0); yarray.push([sameItem]); // Different Y.Map instance internally

Any concurrent edits to the "moved" item are lost because you deleted the original.

  1. Accessing Raw Y.Doc Shared Types for Document Content

Document Y.Docs use a timeline model (Y.Array('timeline') with nested typed entries). Never access raw shared types on the ydoc directly—use the handle methods:

// BAD: bypasses the timeline const ytext = handle.ydoc.getText('content');

// GOOD: use handle methods (timeline-backed) handle.read(); // string I/O handle.write('hello'); handle.asText(); // Y.Text for editor binding handle.asRichText(); // Y.XmlFragment for richtext binding handle.asSheet(); // SheetBinding for spreadsheet binding

See the workspace-api skill for the full DocumentHandle API.

Debugging Tips

Inspect Document State

console.log(doc.toJSON()); // Full document as plain JSON

Check Client IDs

// See who would win a conflict console.log('My ID:', doc.clientID);

Watch for Tombstone Bloat

If documents grow unexpectedly, check for:

  • Frequent Y.Map key overwrites

  • "Move" operations on arrays

  • Missing epoch compaction

References

  • Learn Yjs - Interactive tutorials

  • Yjs Documentation - API reference

  • Yjs INTERNALS.md - How Yjs works internally

  • GitHub issue #520 - Conflict resolution discussion with dmonad

  • yjs/y-utility - YKeyValue and helpers

  • y-lwwmap - Timestamp-based LWW

  • fractional-indexing - Production library

  • YATA paper - Academic foundation

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

svelte

No summary provided by upstream source.

Repository SourceNeeds Review
General

documentation

No summary provided by upstream source.

Repository SourceNeeds Review
General

writing-voice

No summary provided by upstream source.

Repository SourceNeeds Review
General

git

No summary provided by upstream source.

Repository SourceNeeds Review