Files
web-cad/frontend/src/crdt/AwarenessManager.ts
T

122 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AwarenessManager Tracks user presence, cursor position, and selection.
* Uses a Y.Map inside the shared doc so awareness state syncs via the same
* raw-update WebSocket protocol as the rest of the document.
*/
import * as Y from 'yjs';
import type { YjsDocument } from './YjsDocument';
export interface UserCursor {
userId: string;
userName: string;
color: string;
x: number;
y: number;
visible: boolean;
}
export interface UserSelection {
userId: string;
elementIds: string[];
}
export interface AwarenessState {
cursors: Y.Map<UserCursor>;
selections: Y.Map<UserSelection>;
}
export class AwarenessManager {
readonly state: AwarenessState;
private yjsDoc: YjsDocument;
private userId: string;
private listeners: Set<() => void> = new Set();
constructor(yjsDoc: YjsDocument, userId: string) {
this.yjsDoc = yjsDoc;
this.userId = userId;
this.state = {
cursors: this.yjsDoc.doc.getMap<UserCursor>('awareness-cursors'),
selections: this.yjsDoc.doc.getMap<UserSelection>('awareness-selections'),
};
}
/** Set the local user's cursor position */
setCursor(x: number, y: number, userName: string, color: string): void {
this.state.cursors.set(this.userId, {
userId: this.userId,
userName,
color,
x,
y,
visible: true,
});
}
/** Hide the local user's cursor */
hideCursor(): void {
const cur = this.state.cursors.get(this.userId);
if (cur) {
this.state.cursors.set(this.userId, { ...cur, visible: false });
}
}
/** Set the local user's selection */
setSelection(elementIds: string[]): void {
this.state.selections.set(this.userId, {
userId: this.userId,
elementIds,
});
}
/** Clear the local user's selection */
clearSelection(): void {
this.state.selections.delete(this.userId);
}
/** Remove the local user from awareness (on disconnect) */
removeSelf(): void {
this.state.cursors.delete(this.userId);
this.state.selections.delete(this.userId);
}
/** Get all active cursors */
getAllCursors(): UserCursor[] {
return Array.from(this.state.cursors.values()).filter((c) => c.visible);
}
/** Get all selections */
getAllSelections(): UserSelection[] {
return Array.from(this.state.selections.values());
}
/** Get a specific user's cursor */
getCursor(userId: string): UserCursor | undefined {
return this.state.cursors.get(userId);
}
/** Get a specific user's selection */
getSelection(userId: string): UserSelection | undefined {
return this.state.selections.get(userId);
}
/** Register a callback for awareness changes */
onChange(callback: () => void): () => void {
this.listeners.add(callback);
const cursorObserver = () => this.notifyListeners();
const selectionObserver = () => this.notifyListeners();
this.state.cursors.observe(cursorObserver);
this.state.selections.observe(selectionObserver);
return () => {
this.listeners.delete(callback);
this.state.cursors.unobserve(cursorObserver);
this.state.selections.unobserve(selectionObserver);
};
}
private notifyListeners(): void {
for (const cb of this.listeners) {
cb();
}
}
}