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

snapshot-system.mddocs/

Snapshot System

Time-travel functionality for document state at specific points with diff capabilities. Yjs snapshots provide immutable views of document state that enable version control, branching, and historical analysis.

Capabilities

Snapshot Class

Immutable representation of document state at a specific point in time.

/**
 * Immutable snapshot of document state
 */
class Snapshot {
  /** Delete set at snapshot time */
  readonly ds: DeleteSet;
  
  /** State vector at snapshot time */
  readonly sv: Map<number, number>;
}

Creating Snapshots

Functions for creating snapshots from documents and components.

/**
 * Create snapshot of current document state
 * @param doc - Document to snapshot
 * @returns Snapshot of current state
 */
function snapshot(doc: Doc): Snapshot;

/**
 * Create snapshot from delete set and state vector
 * @param ds - Delete set at snapshot time
 * @param sv - State vector at snapshot time
 * @returns Constructed snapshot
 */
function createSnapshot(ds: DeleteSet, sv: Map<number, number>): Snapshot;

/**
 * Empty snapshot constant representing initial state
 */
const emptySnapshot: Snapshot;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const yarray = doc.getArray("items");

// Take initial snapshot
const initialSnapshot = Y.snapshot(doc);

// Make changes
yarray.push(["item1", "item2"]);

// Take snapshot after changes
const afterChangesSnapshot = Y.snapshot(doc);

// Take another snapshot after more changes
yarray.delete(0, 1);
const finalSnapshot = Y.snapshot(doc);

console.log("Snapshots are different:", 
  !Y.equalSnapshots(initialSnapshot, afterChangesSnapshot));

Snapshot Comparison

Functions for comparing snapshots and checking relationships.

/**
 * Compare two snapshots for equality
 * @param snap1 - First snapshot
 * @param snap2 - Second snapshot
 * @returns True if snapshots represent the same state
 */
function equalSnapshots(snap1: Snapshot, snap2: Snapshot): boolean;

/**
 * Check if snapshot contains a specific update
 * @param snapshot - Snapshot to check
 * @param update - Binary update to check for
 * @returns True if update is contained in snapshot
 */
function snapshotContainsUpdate(snapshot: Snapshot, update: Uint8Array): boolean;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const yarray = doc.getArray("items");

// Create snapshots at different states
const snap1 = Y.snapshot(doc);

yarray.push(["item1"]);
const snap2 = Y.snapshot(doc);

yarray.push(["item2"]);
const snap3 = Y.snapshot(doc);

// Compare snapshots
console.log("snap1 equals snap2:", Y.equalSnapshots(snap1, snap2)); // false
console.log("snap2 equals snap3:", Y.equalSnapshots(snap2, snap3)); // false

// Check if update is contained in snapshot
const update = Y.encodeStateAsUpdate(doc);
console.log("Current update in snap3:", Y.snapshotContainsUpdate(snap3, update)); // true
console.log("Current update in snap1:", Y.snapshotContainsUpdate(snap1, update)); // false

Document Creation from Snapshots

Functions for creating new documents from snapshot states.

/**
 * Create document from snapshot state
 * @param originDoc - Original document the snapshot was taken from
 * @param snapshot - Snapshot to recreate state from
 * @param newDoc - Optional existing document to apply state to
 * @returns Document in the snapshot state
 */
function createDocFromSnapshot(originDoc: Doc, snapshot: Snapshot, newDoc?: Doc): Doc;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const yarray = doc.getArray("items");

// Build up document state
yarray.push(["item1", "item2", "item3"]);
const snapshot1 = Y.snapshot(doc);

yarray.delete(1, 1); // Remove "item2"
const snapshot2 = Y.snapshot(doc);

yarray.push(["item4"]);
const finalSnapshot = Y.snapshot(doc);

// Create documents from different snapshots
const docFromSnap1 = Y.createDocFromSnapshot(doc, snapshot1);
const docFromSnap2 = Y.createDocFromSnapshot(doc, snapshot2);

console.log("Snap1 array:", docFromSnap1.getArray("items").toArray()); // ["item1", "item2", "item3"]
console.log("Snap2 array:", docFromSnap2.getArray("items").toArray()); // ["item1", "item3"]
console.log("Current array:", doc.getArray("items").toArray()); // ["item1", "item3", "item4"]

// Create document with specific client ID
const newDoc = new Y.Doc({ clientID: 999 });
const restoredDoc = Y.createDocFromSnapshot(doc, snapshot1, newDoc);
console.log("Restored doc client ID:", restoredDoc.clientID); // 999

Snapshot Serialization

Functions for encoding and decoding snapshots for storage or transmission.

/**
 * Encode snapshot to binary format (V1)
 * @param snapshot - Snapshot to encode
 * @returns Binary representation
 */
function encodeSnapshot(snapshot: Snapshot): Uint8Array;

/**
 * Decode snapshot from binary format (V1)
 * @param buf - Binary snapshot data
 * @returns Decoded snapshot
 */
function decodeSnapshot(buf: Uint8Array): Snapshot;

/**
 * Encode snapshot to binary format (V2, optimized)
 * @param snapshot - Snapshot to encode
 * @returns Binary representation in V2 format
 */
function encodeSnapshotV2(snapshot: Snapshot): Uint8Array;

/**
 * Decode snapshot from binary format (V2)
 * @param buf - Binary snapshot data in V2 format
 * @returns Decoded snapshot
 */
function decodeSnapshotV2(buf: Uint8Array): Snapshot;

Usage Examples:

import * as Y from "yjs";

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

const snapshot = Y.snapshot(doc);

// Encode snapshot (V1)
const encodedV1 = Y.encodeSnapshot(snapshot);
console.log("V1 encoded size:", encodedV1.length);

// Encode snapshot (V2, typically smaller)
const encodedV2 = Y.encodeSnapshotV2(snapshot);
console.log("V2 encoded size:", encodedV2.length);

// Decode snapshots
const decodedV1 = Y.decodeSnapshot(encodedV1);
const decodedV2 = Y.decodeSnapshotV2(encodedV2);

// Verify equality
console.log("V1 decoded equals original:", Y.equalSnapshots(snapshot, decodedV1));
console.log("V2 decoded equals original:", Y.equalSnapshots(snapshot, decodedV2));

// Store/retrieve from storage
localStorage.setItem("document-snapshot", JSON.stringify(Array.from(encodedV2)));
const stored = new Uint8Array(JSON.parse(localStorage.getItem("document-snapshot")!));
const restoredSnapshot = Y.decodeSnapshotV2(stored);

Snapshot-based Type Operations

Functions for getting type values at specific snapshot states.

/**
 * Convert YArray to JavaScript array at snapshot state
 * @param type - YArray to convert
 * @param snapshot - Snapshot state to use
 * @returns Array content at snapshot time
 */
function typeListToArraySnapshot(type: YArray<any>, snapshot: Snapshot): Array<any>;

/**
 * Get YMap value at snapshot state
 * @param type - YMap to query
 * @param key - Key to get value for
 * @param snapshot - Snapshot state to use
 * @returns Value at snapshot time or undefined
 */
function typeMapGetSnapshot(type: YMap<any>, key: string, snapshot: Snapshot): any;

/**
 * Get all YMap entries at snapshot state
 * @param type - YMap to query
 * @param snapshot - Snapshot state to use
 * @returns Object with all key-value pairs at snapshot time
 */
function typeMapGetAllSnapshot(type: YMap<any>, snapshot: Snapshot): { [key: string]: any };

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const yarray = doc.getArray("items");
const ymap = doc.getMap("metadata");

// Build initial state
yarray.push(["item1", "item2"]);
ymap.set("count", 2);
ymap.set("created", "2023-01-01");
const snapshot1 = Y.snapshot(doc);

// Make changes
yarray.push(["item3"]);
ymap.set("count", 3);
ymap.set("modified", "2023-01-02");
ymap.delete("created");
const snapshot2 = Y.snapshot(doc);

// Query historical values
const arrayAtSnap1 = Y.typeListToArraySnapshot(yarray, snapshot1);
const arrayAtSnap2 = Y.typeListToArraySnapshot(yarray, snapshot2);

console.log("Array at snapshot1:", arrayAtSnap1); // ["item1", "item2"]
console.log("Array at snapshot2:", arrayAtSnap2); // ["item1", "item2", "item3"]

const countAtSnap1 = Y.typeMapGetSnapshot(ymap, "count", snapshot1);
const countAtSnap2 = Y.typeMapGetSnapshot(ymap, "count", snapshot2);

console.log("Count at snapshot1:", countAtSnap1); // 2
console.log("Count at snapshot2:", countAtSnap2); // 3

const allMetaAtSnap1 = Y.typeMapGetAllSnapshot(ymap, snapshot1);
const allMetaAtSnap2 = Y.typeMapGetAllSnapshot(ymap, snapshot2);

console.log("Metadata at snapshot1:", allMetaAtSnap1); // {count: 2, created: "2023-01-01"}
console.log("Metadata at snapshot2:", allMetaAtSnap2); // {count: 3, modified: "2023-01-02"}

Advanced Snapshot Patterns

Version Control System:

import * as Y from "yjs";

interface Version {
  id: string;
  timestamp: number;
  message: string;
  snapshot: Y.Snapshot;
}

class VersionControl {
  private doc: Y.Doc;
  private versions: Version[] = [];

  constructor(doc: Y.Doc) {
    this.doc = doc;
    // Save initial version
    this.saveVersion("Initial version");
  }

  saveVersion(message: string): string {
    const versionId = `v${Date.now()}`;
    const version: Version = {
      id: versionId,
      timestamp: Date.now(),
      message,
      snapshot: Y.snapshot(this.doc)
    };
    
    this.versions.push(version);
    return versionId;
  }

  getVersions(): Version[] {
    return [...this.versions];
  }

  restoreVersion(versionId: string): Y.Doc | null {
    const version = this.versions.find(v => v.id === versionId);
    if (!version) return null;
    
    return Y.createDocFromSnapshot(this.doc, version.snapshot);
  }

  compareVersions(versionId1: string, versionId2: string): boolean {
    const v1 = this.versions.find(v => v.id === versionId1);
    const v2 = this.versions.find(v => v.id === versionId2);
    
    if (!v1 || !v2) return false;
    return Y.equalSnapshots(v1.snapshot, v2.snapshot);
  }
}

// Usage
const doc = new Y.Doc();
const vc = new VersionControl(doc);

const yarray = doc.getArray("items");
yarray.push(["item1"]);
const v1 = vc.saveVersion("Added item1");

yarray.push(["item2"]);
const v2 = vc.saveVersion("Added item2");

// Restore to previous version
const restoredDoc = vc.restoreVersion(v1);
console.log("Restored array:", restoredDoc?.getArray("items").toArray()); // ["item1"]

Branching System:

import * as Y from "yjs";

interface Branch {
  name: string;
  baseSnapshot: Y.Snapshot;
  doc: Y.Doc;
}

class BranchManager {
  private mainDoc: Y.Doc;
  private branches: Map<string, Branch> = new Map();

  constructor(doc: Y.Doc) {
    this.mainDoc = doc;
  }

  createBranch(name: string, fromSnapshot?: Y.Snapshot): Y.Doc {
    const baseSnapshot = fromSnapshot || Y.snapshot(this.mainDoc);
    const branchDoc = Y.createDocFromSnapshot(this.mainDoc, baseSnapshot);
    
    this.branches.set(name, {
      name,
      baseSnapshot,
      doc: branchDoc
    });
    
    return branchDoc;
  }

  getBranch(name: string): Y.Doc | null {
    return this.branches.get(name)?.doc || null;
  }

  mergeBranch(branchName: string): boolean {
    const branch = this.branches.get(branchName);
    if (!branch) return false;

    // Get changes made in branch
    const branchStateVector = Y.encodeStateVector(branch.baseSnapshot.sv);
    const branchChanges = Y.encodeStateAsUpdate(branch.doc, branchStateVector);
    
    // Apply to main document
    Y.applyUpdate(this.mainDoc, branchChanges);
    
    return true;
  }

  deleteBranch(name: string): boolean {
    return this.branches.delete(name);
  }
}

// Usage
const mainDoc = new Y.Doc();
const bm = new BranchManager(mainDoc);

// Work on main
mainDoc.getArray("items").push(["main-item"]);

// Create feature branch
const featureBranch = bm.createBranch("feature-xyz");
featureBranch.getArray("items").push(["feature-item"]);

// Merge back to main
bm.mergeBranch("feature-xyz");

console.log("Main after merge:", mainDoc.getArray("items").toArray());
// ["main-item", "feature-item"]

Snapshot-based Diff:

import * as Y from "yjs";

function diffSnapshots(doc: Y.Doc, snap1: Y.Snapshot, snap2: Y.Snapshot): any {
  const doc1 = Y.createDocFromSnapshot(doc, snap1);
  const doc2 = Y.createDocFromSnapshot(doc, snap2);
  
  const diff: any = {};
  
  // Compare shared types
  doc1.share.forEach((type1, name) => {
    const type2 = doc2.share.get(name);
    
    if (type1 instanceof Y.YArray && type2 instanceof Y.YArray) {
      const arr1 = type1.toArray();
      const arr2 = type2.toArray();
      if (JSON.stringify(arr1) !== JSON.stringify(arr2)) {
        diff[name] = { before: arr1, after: arr2 };
      }
    } else if (type1 instanceof Y.YMap && type2 instanceof Y.YMap) {
      const obj1 = type1.toJSON();
      const obj2 = type2.toJSON();
      if (JSON.stringify(obj1) !== JSON.stringify(obj2)) {
        diff[name] = { before: obj1, after: obj2 };
      }
    }
  });
  
  return diff;
}

// Usage
const doc = new Y.Doc();
const yarray = doc.getArray("items");

yarray.push(["item1", "item2"]);
const snap1 = Y.snapshot(doc);

yarray.delete(0, 1);
yarray.push(["item3"]);
const snap2 = Y.snapshot(doc);

const diff = diffSnapshots(doc, snap1, snap2);
console.log("Diff:", diff);
// { items: { before: ["item1", "item2"], after: ["item2", "item3"] } }

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