Conflict-free Replicated Data Type (CRDT) framework for real-time collaborative applications
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.
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;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;
}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);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"); // DeleteText-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 });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 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");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