CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-yjs

Conflict-free Replicated Data Type (CRDT) framework for real-time collaborative applications

Overview
Eval results
Files

synchronization.mddocs/

Synchronization

Binary update system for efficient network synchronization and state management. Yjs provides comprehensive tools for encoding, decoding, merging, and applying document changes across distributed systems.

Capabilities

Basic Update Operations

Core functions for applying and generating document updates.

/**
 * Apply a binary update to a document (V1 format)
 * @param doc - Document to apply update to
 * @param update - Binary update data
 * @param transactionOrigin - Optional origin for the transaction
 */
function applyUpdate(doc: Doc, update: Uint8Array, transactionOrigin?: any): void;

/**
 * Apply a binary update to a document (V2 format, optimized)
 * @param doc - Document to apply update to  
 * @param update - Binary update data in V2 format
 * @param transactionOrigin - Optional origin for the transaction
 */
function applyUpdateV2(doc: Doc, update: Uint8Array, transactionOrigin?: any): void;

/**
 * Encode document state as update (V1 format)
 * @param doc - Document to encode
 * @param encodedTargetStateVector - Optional target state to encode diff against
 * @returns Binary update data
 */
function encodeStateAsUpdate(doc: Doc, encodedTargetStateVector?: Uint8Array): Uint8Array;

/**
 * Encode document state as update (V2 format, optimized)
 * @param doc - Document to encode
 * @param encodedTargetStateVector - Optional target state to encode diff against  
 * @returns Binary update data in V2 format
 */
function encodeStateAsUpdateV2(doc: Doc, encodedTargetStateVector?: Uint8Array): Uint8Array;

Usage Examples:

import * as Y from "yjs";

// Create two documents
const doc1 = new Y.Doc();
const doc2 = new Y.Doc();

// Make changes to doc1
const yarray1 = doc1.getArray("items");
yarray1.push(["apple", "banana"]);

// Generate update from doc1
const update = Y.encodeStateAsUpdate(doc1);

// Apply update to doc2
Y.applyUpdate(doc2, update);

// doc2 now has same content as doc1
const yarray2 = doc2.getArray("items");
console.log(yarray2.toArray()); // ["apple", "banana"]

// Use V2 format for better compression
const updateV2 = Y.encodeStateAsUpdateV2(doc1);
const doc3 = new Y.Doc();
Y.applyUpdateV2(doc3, updateV2);

Update Merging

Functions for combining multiple updates into single updates for efficient transmission.

/**
 * Merge multiple updates into a single update (V1 format)
 * @param updates - Array of binary updates to merge
 * @returns Single merged update
 */
function mergeUpdates(updates: Array<Uint8Array>): Uint8Array;

/**
 * Merge multiple updates into a single update (V2 format)
 * @param updates - Array of binary updates to merge
 * @returns Single merged update in V2 format
 */
function mergeUpdatesV2(updates: Array<Uint8Array>): Uint8Array;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const updates: Uint8Array[] = [];

// Collect multiple updates
const yarray = doc.getArray("items");
yarray.push(["item1"]);
updates.push(Y.encodeStateAsUpdate(doc));

yarray.push(["item2"]);
updates.push(Y.encodeStateAsUpdate(doc));

yarray.push(["item3"]);
updates.push(Y.encodeStateAsUpdate(doc));

// Merge into single update
const mergedUpdate = Y.mergeUpdates(updates);

// Apply merged update to new document
const newDoc = new Y.Doc();
Y.applyUpdate(newDoc, mergedUpdate);

console.log(newDoc.getArray("items").toArray()); // ["item1", "item2", "item3"]

// V2 merging for better compression
const mergedUpdateV2 = Y.mergeUpdatesV2(updates.map(u => Y.convertUpdateFormatV1ToV2(u)));

State Vectors

Functions for working with document state vectors that represent document synchronization state.

/**
 * Encode state vector to binary format
 * @param doc - Document or state vector map to encode
 * @returns Binary state vector
 */
function encodeStateVector(doc: Doc | Map<number, number>): Uint8Array;

/**
 * Decode state vector from binary format
 * @param encodedState - Binary state vector data
 * @returns State vector as Map
 */
function decodeStateVector(encodedState: Uint8Array): Map<number, number>;

/**
 * Get current state vector from document
 * @param doc - Document to get state from
 * @returns State vector as Map
 */
function getState(doc: Doc): Map<number, number>;

/**
 * Extract state vector from update (V1 format)
 * @param update - Binary update to extract from
 * @returns Binary state vector
 */
function encodeStateVectorFromUpdate(update: Uint8Array): Uint8Array;

/**
 * Extract state vector from update (V2 format)
 * @param update - Binary update to extract from
 * @returns Binary state vector
 */
function encodeStateVectorFromUpdateV2(update: Uint8Array): Uint8Array;

Usage Examples:

import * as Y from "yjs";

const doc1 = new Y.Doc();
const doc2 = new Y.Doc();

// Make changes to doc1
doc1.getArray("items").push(["item1", "item2"]);

// Get state vectors
const state1 = Y.getState(doc1);
const state2 = Y.getState(doc2);

console.log("Doc1 state:", state1); // Map with client states
console.log("Doc2 state:", state2); // Empty map

// Encode/decode state vectors
const encodedState = Y.encodeStateVector(doc1);
const decodedState = Y.decodeStateVector(encodedState);

console.log("Decoded state equals original:", 
  JSON.stringify([...state1]) === JSON.stringify([...decodedState]));

// Use state vector for targeted sync
const targetStateVector = Y.encodeStateVector(doc2);
const diffUpdate = Y.encodeStateAsUpdate(doc1, targetStateVector);
Y.applyUpdate(doc2, diffUpdate);

Update Diffing

Functions for computing differences between updates and state vectors.

/**
 * Compute diff between update and state vector (V1 format)
 * @param update - Binary update data
 * @param stateVector - Binary state vector to diff against
 * @returns Diff update containing only new changes
 */
function diffUpdate(update: Uint8Array, stateVector: Uint8Array): Uint8Array;

/**
 * Compute diff between update and state vector (V2 format)
 * @param update - Binary update data in V2 format
 * @param stateVector - Binary state vector to diff against
 * @returns Diff update containing only new changes
 */
function diffUpdateV2(update: Uint8Array, stateVector: Uint8Array): Uint8Array;

Usage Examples:

import * as Y from "yjs";

const doc1 = new Y.Doc();
const doc2 = new Y.Doc();

// Make changes to doc1
doc1.getArray("items").push(["item1", "item2"]);

// Get full update from doc1
const fullUpdate = Y.encodeStateAsUpdate(doc1);

// Simulate doc2 already having some of the changes
doc2.getArray("items").push(["item1"]); // Partial state

// Get state vector from doc2
const doc2State = Y.encodeStateVector(doc2);

// Compute diff - only changes doc2 doesn't have
const diffUpdate = Y.diffUpdate(fullUpdate, doc2State);

// Apply diff to doc2
Y.applyUpdate(doc2, diffUpdate);

console.log(doc2.getArray("items").toArray()); // ["item1", "item2"]

Format Conversion

Functions for converting between V1 and V2 update formats.

/**
 * Convert update from V1 to V2 format
 * @param update - Update in V1 format
 * @returns Update in V2 format
 */
function convertUpdateFormatV1ToV2(update: Uint8Array): Uint8Array;

/**
 * Convert update from V2 to V1 format
 * @param update - Update in V2 format
 * @returns Update in V1 format
 */
function convertUpdateFormatV2ToV1(update: Uint8Array): Uint8Array;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
doc.getArray("items").push(["item1", "item2"]);

// Generate V1 update
const updateV1 = Y.encodeStateAsUpdate(doc);
console.log("V1 size:", updateV1.length);

// Convert to V2 (typically smaller)
const updateV2 = Y.convertUpdateFormatV1ToV2(updateV1);
console.log("V2 size:", updateV2.length);

// Convert back to V1
const backToV1 = Y.convertUpdateFormatV2ToV1(updateV2);

// Verify they're functionally equivalent
const testDoc1 = new Y.Doc();
const testDoc2 = new Y.Doc();

Y.applyUpdate(testDoc1, updateV1);
Y.applyUpdate(testDoc2, backToV1);

console.log("Results equal:", 
  JSON.stringify(testDoc1.getArray("items").toArray()) === 
  JSON.stringify(testDoc2.getArray("items").toArray()));

Update Inspection

Functions for reading and analyzing update contents without applying them.

/**
 * Read update metadata (V1 format)
 * @param update - Binary update to analyze
 * @returns Object with from/to state vectors
 */
function parseUpdateMeta(update: Uint8Array): { 
  from: Map<number, number>; 
  to: Map<number, number>; 
};

/**
 * Read update metadata (V2 format)
 * @param update - Binary update to analyze
 * @returns Object with from/to state vectors
 */
function parseUpdateMetaV2(update: Uint8Array): { 
  from: Map<number, number>; 
  to: Map<number, number>; 
};

/**
 * Read update without applying it (V1 format)
 * @param update - Binary update data
 * @param YDoc - Doc constructor for validation
 * @returns Update content information
 */
function readUpdate(update: Uint8Array, YDoc: typeof Doc): any;

/**
 * Read update without applying it (V2 format)
 * @param update - Binary update data
 * @param YDoc - Doc constructor for validation
 * @returns Update content information
 */
function readUpdateV2(update: Uint8Array, YDoc: typeof Doc): any;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
doc.getArray("items").push(["item1", "item2"]);

const update = Y.encodeStateAsUpdate(doc);

// Analyze update metadata
const meta = Y.parseUpdateMeta(update);
console.log("From state:", meta.from);
console.log("To state:", meta.to);

// Read update contents
const updateContents = Y.readUpdate(update, Y.Doc);
console.log("Update contents:", updateContents);

// Check what changes an update would make
function analyzeUpdate(update: Uint8Array) {
  const meta = Y.parseUpdateMeta(update);
  
  console.log("Update affects clients:", Array.from(meta.to.keys()));
  
  const totalOperations = Array.from(meta.to.values()).reduce((sum, clock) => {
    const fromClock = meta.from.get(Array.from(meta.to.keys())[0]) || 0;
    return sum + (clock - fromClock);
  }, 0);
  
  console.log("Total operations:", totalOperations);
}

analyzeUpdate(update);

Advanced Synchronization Patterns

Incremental Sync:

import * as Y from "yjs";

class IncrementalSyncer {
  private doc: Y.Doc;
  private lastSyncState: Map<number, number>;

  constructor(doc: Y.Doc) {
    this.doc = doc;
    this.lastSyncState = new Map();
  }

  getIncrementalUpdate(): Uint8Array | null {
    const currentState = Y.getState(this.doc);
    
    // Check if there are changes since last sync
    const hasChanges = Array.from(currentState.entries()).some(([client, clock]) => {
      return (this.lastSyncState.get(client) || 0) < clock;
    });

    if (!hasChanges) return null;

    // Encode only changes since last sync
    const lastSyncVector = Y.encodeStateVector(this.lastSyncState);
    const incrementalUpdate = Y.encodeStateAsUpdate(this.doc, lastSyncVector);
    
    // Update sync state
    this.lastSyncState = new Map(currentState);
    
    return incrementalUpdate;
  }

  reset() {
    this.lastSyncState.clear();
  }
}

// Usage
const doc = new Y.Doc();
const syncer = new IncrementalSyncer(doc);

doc.getArray("items").push(["item1"]);
const update1 = syncer.getIncrementalUpdate(); // Contains item1

doc.getArray("items").push(["item2"]);
const update2 = syncer.getIncrementalUpdate(); // Contains only item2

const noUpdate = syncer.getIncrementalUpdate(); // null - no changes

Conflict-Free Updates:

import * as Y from "yjs";

// Ensure updates can be applied in any order
function ensureConflictFree(updates: Uint8Array[]): boolean {
  const permutations = getPermutations(updates);
  
  return permutations.every(updateOrder => {
    const doc1 = new Y.Doc();
    const doc2 = new Y.Doc();
    
    // Apply in one order
    updateOrder.forEach(update => Y.applyUpdate(doc1, update));
    
    // Apply merged update
    const merged = Y.mergeUpdates(updates);
    Y.applyUpdate(doc2, merged);
    
    // Results should be identical
    return Y.encodeStateAsUpdate(doc1).every((byte, i) => 
      byte === Y.encodeStateAsUpdate(doc2)[i]);
  });
}

function getPermutations<T>(arr: T[]): T[][] {
  if (arr.length <= 1) return [arr];
  
  const result: T[][] = [];
  for (let i = 0; i < arr.length; i++) {
    const rest = [...arr.slice(0, i), ...arr.slice(i + 1)];
    const perms = getPermutations(rest);
    perms.forEach(perm => result.push([arr[i], ...perm]));
  }
  return result;
}

Update Validation:

import * as Y from "yjs";

function validateUpdate(update: Uint8Array): { valid: boolean; errors: string[] } {
  const errors: string[] = [];
  
  try {
    // Check if update can be parsed
    const meta = Y.parseUpdateMeta(update);
    
    // Validate state vector consistency
    if (meta.from.size === 0 && meta.to.size === 0) {
      errors.push("Empty update");
    }
    
    // Check for negative clocks
    for (const [client, clock] of meta.to) {
      if (clock < 0) {
        errors.push(`Negative clock for client ${client}: ${clock}`);
      }
      
      const fromClock = meta.from.get(client) || 0;
      if (fromClock > clock) {
        errors.push(`Invalid clock sequence for client ${client}: ${fromClock} -> ${clock}`);
      }
    }
    
    // Try to read update structure
    Y.readUpdate(update, Y.Doc);
    
  } catch (error) {
    errors.push(`Parse error: ${error.message}`);
  }
  
  return { valid: errors.length === 0, errors };
}

// Usage
const doc = new Y.Doc();
doc.getArray("items").push(["item1"]);
const update = Y.encodeStateAsUpdate(doc);

const validation = validateUpdate(update);
console.log("Update valid:", validation.valid);
if (!validation.valid) {
  console.log("Errors:", validation.errors);
}

Install with Tessl CLI

npx tessl i tessl/npm-yjs

docs

document-management.md

event-system.md

index.md

position-tracking.md

shared-data-types.md

snapshot-system.md

synchronization.md

transaction-system.md

undo-redo-system.md

xml-types.md

tile.json