Conflict-free Replicated Data Type (CRDT) framework for real-time collaborative applications
Position system that maintains references across concurrent edits and structural changes. Yjs provides both relative and absolute position types that survive concurrent modifications from multiple users.
Position that remains valid across concurrent document changes by referencing document structure.
/**
* Position that remains stable across concurrent modifications
*/
class RelativePosition {
/** Type ID reference or null */
readonly type: ID | null;
/** Type name reference or null */
readonly tname: string | null;
/** Item ID reference or null */
readonly item: ID | null;
/** Association direction (-1, 0, or 1) */
readonly assoc: number;
}Position resolved to a specific index within a type at a given document state.
/**
* Position resolved to specific index within a type
*/
class AbsolutePosition {
/** The type containing this position */
readonly type: AbstractType<any>;
/** Index within the type */
readonly index: number;
/** Association direction (-1, 0, or 1) */
readonly assoc: number;
}Functions for creating relative positions from types and indices.
/**
* Create relative position from type and index
* @param type - Type to create position in
* @param index - Index within the type
* @param assoc - Association direction (default: 0)
* @returns RelativePosition that tracks this location
*/
function createRelativePositionFromTypeIndex(
type: AbstractType<any>,
index: number,
assoc?: number
): RelativePosition;
/**
* Create relative position from JSON representation
* @param json - JSON object representing position
* @returns RelativePosition instance
*/
function createRelativePositionFromJSON(json: any): RelativePosition;Usage Examples:
import * as Y from "yjs";
const doc1 = new Y.Doc();
const ytext1 = doc1.getText("document");
ytext1.insert(0, "Hello World!");
// Create relative position at index 6 (before "World")
const relPos = Y.createRelativePositionFromTypeIndex(ytext1, 6);
// Position remains valid after other users make changes
const doc2 = new Y.Doc();
const ytext2 = doc2.getText("document");
// Simulate receiving updates from another user
const update1 = Y.encodeStateAsUpdate(doc1);
Y.applyUpdate(doc2, update1);
// Other user inserts text at beginning
ytext2.insert(0, "Hi! ");
// Create update and apply back to doc1
const update2 = Y.encodeStateAsUpdate(doc2);
Y.applyUpdate(doc1, update2);
// Original position still points to correct location
const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, doc1);
console.log("Position now at index:", absPos?.index); // Adjusted indexFunctions for converting between relative and absolute positions.
/**
* Convert relative position to absolute position
* @param rpos - Relative position to convert
* @param doc - Document to resolve position in
* @returns AbsolutePosition or null if position cannot be resolved
*/
function createAbsolutePositionFromRelativePosition(
rpos: RelativePosition,
doc: Doc
): AbsolutePosition | null;
/**
* Compare two relative positions for equality
* @param a - First relative position
* @param b - Second relative position
* @returns True if positions are equal
*/
function compareRelativePositions(a: RelativePosition | null, b: RelativePosition | null): boolean;Usage Examples:
import * as Y from "yjs";
const doc = new Y.Doc();
const ytext = doc.getText("document");
ytext.insert(0, "Hello World!");
// Create multiple relative positions
const pos1 = Y.createRelativePositionFromTypeIndex(ytext, 0); // Start
const pos2 = Y.createRelativePositionFromTypeIndex(ytext, 6); // Before "World"
const pos3 = Y.createRelativePositionFromTypeIndex(ytext, ytext.length); // End
// Convert to absolute positions
const abs1 = Y.createAbsolutePositionFromRelativePosition(pos1, doc);
const abs2 = Y.createAbsolutePositionFromRelativePosition(pos2, doc);
const abs3 = Y.createAbsolutePositionFromRelativePosition(pos3, doc);
console.log("Start position:", abs1?.index); // 0
console.log("Middle position:", abs2?.index); // 6
console.log("End position:", abs3?.index); // 12
// Compare positions
console.log("pos1 equals pos2:", Y.compareRelativePositions(pos1, pos2)); // false
console.log("pos1 equals pos1:", Y.compareRelativePositions(pos1, pos1)); // trueFunctions for serializing and deserializing relative positions.
/**
* Encode relative position to binary format
* @param rpos - Relative position to encode
* @returns Binary representation as Uint8Array
*/
function encodeRelativePosition(rpos: RelativePosition): Uint8Array;
/**
* Decode relative position from binary format
* @param uint8Array - Binary data to decode
* @returns RelativePosition instance
*/
function decodeRelativePosition(uint8Array: Uint8Array): RelativePosition;
/**
* Convert relative position to JSON format
* @param rpos - Relative position to convert
* @returns JSON representation
*/
function relativePositionToJSON(rpos: RelativePosition): any;Usage Examples:
import * as Y from "yjs";
const doc = new Y.Doc();
const ytext = doc.getText("document");
ytext.insert(0, "Hello World!");
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 6);
// Serialize to binary
const binary = Y.encodeRelativePosition(relPos);
console.log("Binary size:", binary.length);
// Deserialize from binary
const restoredPos = Y.decodeRelativePosition(binary);
// Serialize to JSON
const json = Y.relativePositionToJSON(relPos);
console.log("JSON:", json);
// Restore from JSON
const posFromJSON = Y.createRelativePositionFromJSON(json);
// All positions should be equivalent
console.log("Original equals restored:", Y.compareRelativePositions(relPos, restoredPos));
console.log("Original equals from JSON:", Y.compareRelativePositions(relPos, posFromJSON));Association determines behavior when content is inserted exactly at the position.
/**
* Association values:
* -1: Position moves left when content inserted at this location
* 0: Default behavior
* 1: Position moves right when content inserted at this location
*/
type PositionAssociation = -1 | 0 | 1;Usage Examples:
import * as Y from "yjs";
const doc = new Y.Doc();
const ytext = doc.getText("document");
ytext.insert(0, "Hello");
// Create positions with different associations at index 5
const leftAssoc = Y.createRelativePositionFromTypeIndex(ytext, 5, -1);
const defaultAssoc = Y.createRelativePositionFromTypeIndex(ytext, 5, 0);
const rightAssoc = Y.createRelativePositionFromTypeIndex(ytext, 5, 1);
// Insert text at position 5
ytext.insert(5, " World");
// Check where positions ended up
const leftAbs = Y.createAbsolutePositionFromRelativePosition(leftAssoc, doc);
const defaultAbs = Y.createAbsolutePositionFromRelativePosition(defaultAssoc, doc);
const rightAbs = Y.createAbsolutePositionFromRelativePosition(rightAssoc, doc);
console.log("Left association:", leftAbs?.index); // 5 (before inserted content)
console.log("Default association:", defaultAbs?.index); // 5 or 11 (implementation dependent)
console.log("Right association:", rightAbs?.index); // 11 (after inserted content)Cursor Tracking:
import * as Y from "yjs";
class CursorTracker {
private doc: Y.Doc;
private ytext: Y.Text;
private positions: Map<string, Y.RelativePosition>;
constructor(doc: Y.Doc, textName: string) {
this.doc = doc;
this.ytext = doc.getText(textName);
this.positions = new Map();
}
setCursor(userId: string, index: number) {
const relPos = Y.createRelativePositionFromTypeIndex(this.ytext, index);
this.positions.set(userId, relPos);
}
getCursor(userId: string): number | null {
const relPos = this.positions.get(userId);
if (!relPos) return null;
const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, this.doc);
return absPos?.index ?? null;
}
getAllCursors(): Map<string, number> {
const cursors = new Map<string, number>();
this.positions.forEach((relPos, userId) => {
const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, this.doc);
if (absPos) {
cursors.set(userId, absPos.index);
}
});
return cursors;
}
}
// Usage
const doc = new Y.Doc();
const tracker = new CursorTracker(doc, "document");
tracker.setCursor("alice", 10);
tracker.setCursor("bob", 20);
// Cursors automatically adjust as document changes
const ytext = doc.getText("document");
ytext.insert(0, "Prefix ");
console.log("Alice cursor:", tracker.getCursor("alice")); // Adjusted position
console.log("Bob cursor:", tracker.getCursor("bob")); // Adjusted positionSelection Ranges:
import * as Y from "yjs";
interface SelectionRange {
start: Y.RelativePosition;
end: Y.RelativePosition;
userId: string;
}
class SelectionTracker {
private doc: Y.Doc;
private selections: Map<string, SelectionRange>;
constructor(doc: Y.Doc) {
this.doc = doc;
this.selections = new Map();
}
setSelection(userId: string, type: Y.AbstractType<any>, startIndex: number, endIndex: number) {
const start = Y.createRelativePositionFromTypeIndex(type, startIndex);
const end = Y.createRelativePositionFromTypeIndex(type, endIndex);
this.selections.set(userId, { start, end, userId });
}
getSelection(userId: string): { start: number; end: number } | null {
const selection = this.selections.get(userId);
if (!selection) return null;
const startAbs = Y.createAbsolutePositionFromRelativePosition(selection.start, this.doc);
const endAbs = Y.createAbsolutePositionFromRelativePosition(selection.end, this.doc);
if (!startAbs || !endAbs) return null;
return {
start: startAbs.index,
end: endAbs.index
};
}
}Position Persistence:
import * as Y from "yjs";
// Save positions to storage
function savePositions(positions: Map<string, Y.RelativePosition>): string {
const serialized = Array.from(positions.entries()).map(([key, pos]) => ({
key,
position: Y.relativePositionToJSON(pos)
}));
return JSON.stringify(serialized);
}
// Restore positions from storage
function loadPositions(data: string): Map<string, Y.RelativePosition> {
const serialized = JSON.parse(data);
const positions = new Map<string, Y.RelativePosition>();
serialized.forEach(({ key, position }) => {
const relPos = Y.createRelativePositionFromJSON(position);
positions.set(key, relPos);
});
return positions;
}
// Usage
const doc = new Y.Doc();
const ytext = doc.getText("document");
ytext.insert(0, "Sample text");
const positions = new Map();
positions.set("cursor1", Y.createRelativePositionFromTypeIndex(ytext, 7));
positions.set("cursor2", Y.createRelativePositionFromTypeIndex(ytext, 12));
// Save to storage
const saved = savePositions(positions);
localStorage.setItem("positions", saved);
// Later: restore from storage
const restored = loadPositions(localStorage.getItem("positions")!);
// Positions remain valid across sessions
restored.forEach((relPos, key) => {
const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, doc);
console.log(`${key} at index:`, absPos?.index);
});Install with Tessl CLI
npx tessl i tessl/npm-yjs