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

event-system.mddocs/

Event System

Comprehensive event handling for observing changes to shared types with detailed change information. The event system provides fine-grained notifications about all modifications to shared data structures.

Capabilities

Base YEvent Class

Foundation class for all Yjs events providing common change information.

/**
 * Base event class for all Yjs type changes
 */
abstract class YEvent<T> {
  /** The type that was changed */
  readonly target: T;
  
  /** Current event target (may differ from target in event bubbling) */
  readonly currentTarget: AbstractType<any>;
  
  /** Transaction that triggered this event */
  readonly transaction: Transaction;
  
  /** Path from document root to the changed type */
  readonly path: Array<string | number>;
  
  /** Detailed information about all changes in this event */
  readonly changes: {
    added: Set<Item>;
    deleted: Set<Item>;
    keys: Map<string, { action: 'add' | 'update' | 'delete'; oldValue: any }>;
    delta: Array<any>;
  };
  
  /** Changes in Quill Delta format */
  readonly delta: Array<any>;
  
  /** Map of changed keys with their change types and old values */
  readonly keys: Map<string, { action: 'add' | 'update' | 'delete'; oldValue: any }>;
}

Event Methods:

/**
 * Check if a struct was added in this event
 * @param struct - Struct to check
 * @returns True if struct was added
 */
adds(struct: AbstractStruct): boolean;

/**
 * Check if a struct was deleted in this event
 * @param struct - Struct to check
 * @returns True if struct was deleted
 */
deletes(struct: AbstractStruct): boolean;

AbstractType Observer Methods

All shared types inherit observer methods for event handling.

/**
 * Observer methods available on all AbstractType instances
 */
interface AbstractType<EventType> {
  /**
   * Add an observer for direct changes to this type
   * @param f - Observer function to call on changes
   */
  observe(f: (event: EventType, transaction: Transaction) => void): void;
  
  /**
   * Remove an observer for direct changes
   * @param f - Observer function to remove
   */
  unobserve(f: (event: EventType, transaction: Transaction) => void): void;
  
  /**
   * Add an observer for changes to this type or any nested types
   * @param f - Observer function to call on deep changes
   */
  observeDeep(f: (events: Array<YEvent<any>>, transaction: Transaction) => void): void;
  
  /**
   * Remove a deep observer
   * @param f - Deep observer function to remove
   */
  unobserveDeep(f: (events: Array<YEvent<any>>, transaction: Transaction) => void): void;
}

YArrayEvent

Array-specific event providing detailed information about array changes.

/**
 * Event fired when YArray changes
 */
class YArrayEvent extends YEvent<YArray<any>> {
  /** The YArray that changed */
  readonly target: YArray<any>;
  
  /** Changes in Delta format showing insertions and deletions */
  readonly changes: {
    added: Set<Item>;
    deleted: Set<Item>;
    delta: Array<{ retain?: number; insert?: any[]; delete?: number }>;
  };
}

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const yarray = doc.getArray<string>("fruits");

// Observe array changes
yarray.observe((event, transaction) => {
  console.log("Array changed by:", transaction.origin);
  
  // Process delta changes
  event.changes.delta.forEach((change) => {
    if (change.insert) {
      console.log("Inserted:", change.insert);
    }
    if (change.delete) {
      console.log("Deleted:", change.delete, "items");
    }
    if (change.retain) {
      console.log("Retained:", change.retain, "items");
    }
  });
  
  // Check specific items
  event.changes.added.forEach((item) => {
    console.log("Added item at index:", item.index);
  });
});

// Trigger changes
yarray.push(["apple", "banana"]);
yarray.delete(0, 1);

YMapEvent

Map-specific event providing information about key-value changes.

/**
 * Event fired when YMap changes
 */
class YMapEvent extends YEvent<YMap<any>> {
  /** The YMap that changed */
  readonly target: YMap<any>;
  
  /** Set of keys that changed in this event */
  readonly keysChanged: Set<string>;
  
  /** Detailed key changes with actions and old values */
  readonly keys: Map<string, { action: 'add' | 'update' | 'delete'; oldValue: any }>;
}

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const ymap = doc.getMap<any>("settings");

// Observe map changes
ymap.observe((event, transaction) => {
  console.log("Map changed, keys affected:", Array.from(event.keysChanged));
  
  // Process each changed key
  event.keys.forEach((change, key) => {
    switch (change.action) {
      case 'add':
        console.log(`Added key "${key}" with value:`, ymap.get(key));
        break;
      case 'update':
        console.log(`Updated key "${key}" from:`, change.oldValue, "to:", ymap.get(key));
        break;
      case 'delete':
        console.log(`Deleted key "${key}" with old value:`, change.oldValue);
        break;
    }
  });
});

// Trigger changes
ymap.set("theme", "dark");
ymap.set("theme", "light"); // Update
ymap.delete("theme"); // Delete

YTextEvent

Text-specific event providing information about text and formatting changes.

/**
 * Event fired when YText changes
 */
class YTextEvent extends YEvent<YText> {
  /** The YText that changed */
  readonly target: YText;
  
  /** Whether child list (content) changed */
  readonly childListChanged: boolean;
  
  /** Set of attribute keys that changed */
  readonly keysChanged: Set<string>;
  
  /** Changes in Delta format for text operations */
  readonly changes: {
    added: Set<Item>;
    deleted: Set<Item>;
    keys: Map<string, { action: 'add' | 'update' | 'delete'; oldValue: any }>;
    delta: Array<any>;
  };
}

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const ytext = doc.getText("document");

// Observe text changes
ytext.observe((event, transaction) => {
  if (event.childListChanged) {
    console.log("Text content changed");
    
    // Process delta for text changes
    event.changes.delta.forEach((op) => {
      if (op.retain) {
        console.log(`Retained ${op.retain} characters`);
      }
      if (op.insert) {
        console.log(`Inserted: "${op.insert}"`);
        if (op.attributes) {
          console.log("With attributes:", op.attributes);
        }
      }
      if (op.delete) {
        console.log(`Deleted ${op.delete} characters`);
      }
    });
  }
  
  if (event.keysChanged.size > 0) {
    console.log("Text attributes changed:", Array.from(event.keysChanged));
  }
});

// Trigger changes
ytext.insert(0, "Hello");
ytext.insert(5, " World", { bold: true });
ytext.format(0, 5, { italic: true });

YXmlEvent

XML-specific event providing information about XML structure and attribute changes.

/**
 * Event fired when XML types change
 */
class YXmlEvent extends YEvent<YXmlElement | YXmlText | YXmlFragment> {
  /** The XML type that changed */
  readonly target: YXmlElement | YXmlText | YXmlFragment;
  
  /** Whether child elements changed */
  readonly childListChanged: boolean;
  
  /** Set of attributes that changed (for YXmlElement) */
  readonly attributesChanged: Set<string>;
}

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const xmlFragment = doc.getXmlFragment("document");

// Observe XML changes
xmlFragment.observeDeep((events, transaction) => {
  events.forEach((event) => {
    if (event instanceof Y.YXmlEvent) {
      console.log("XML changed:", event.target.constructor.name);
      
      if (event.childListChanged) {
        console.log("Child elements changed");
      }
      
      if (event.attributesChanged.size > 0) {
        console.log("Attributes changed:", Array.from(event.attributesChanged));
      }
    }
  });
});

// Create XML structure
const element = new Y.XmlElement("div");
element.setAttribute("class", "container");
xmlFragment.push([element]);

Deep Observation

Deep observers receive events from nested types and provide comprehensive change tracking.

/**
 * Deep observer function signature
 */
type DeepObserver = (events: Array<YEvent<any>>, transaction: Transaction) => void;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const rootMap = doc.getMap("root");

// Set up nested structure
const nestedArray = new Y.Array();
const nestedMap = new Y.Map();
rootMap.set("array", nestedArray);
rootMap.set("map", nestedMap);

// Deep observer catches all nested changes
rootMap.observeDeep((events, transaction) => {
  console.log(`Received ${events.length} events in transaction`);
  
  events.forEach((event, index) => {
    console.log(`Event ${index}:`);
    console.log("  Type:", event.target.constructor.name);
    console.log("  Path:", event.path);
    
    if (event instanceof Y.YArrayEvent) {
      console.log("  Array delta:", event.changes.delta);
    } else if (event instanceof Y.YMapEvent) {
      console.log("  Map keys changed:", Array.from(event.keysChanged));
    }
  });
});

// Changes to nested types trigger deep observer
nestedArray.push(["item1", "item2"]);
nestedMap.set("key", "value");

Event Performance Patterns

Efficient Event Handling:

import * as Y from "yjs";

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

// Efficient: Process changes in batches
yarray.observe((event) => {
  // Single event handler processes all changes at once
  const insertedItems = [];
  const deletedCount = event.changes.delta.reduce((count, op) => {
    if (op.insert) insertedItems.push(...op.insert);
    if (op.delete) count += op.delete;
    return count;
  }, 0);
  
  console.log(`Batch: +${insertedItems.length}, -${deletedCount}`);
});

// Multiple operations in single transaction = single event
doc.transact(() => {
  yarray.push(["a", "b", "c"]);
  yarray.delete(0, 1);
  yarray.push(["d"]);
});

Event Filtering:

import * as Y from "yjs";

const doc = new Y.Doc();
const ytext = doc.getText("document");

// Filter events by origin
ytext.observe((event, transaction) => {
  // Only process user-initiated changes
  if (transaction.origin === "user-input") {
    console.log("Processing user input:", event.changes.delta);
  }
});

// Filter by change type
ytext.observe((event) => {
  const hasTextChanges = event.changes.delta.some(op => op.insert || op.delete);
  const hasFormatChanges = event.changes.delta.some(op => op.attributes);
  
  if (hasTextChanges) {
    console.log("Text content changed");
  }
  if (hasFormatChanges) {
    console.log("Formatting changed");
  }
});

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