Files
FrenoCorp/src/lib/collaboration/crdt-document.ts
Michael Freno 7c684a42cc FRE-600: Fix code review blockers
- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-25 00:08:01 -04:00

123 lines
3.4 KiB
TypeScript

/**
* CRDT Document Manager
* Coordinates Yjs document lifecycle, persistence, and sync
*/
import { Doc, Text, Map as YMap, UndoManager, applyUpdate } from 'yjs';
import { WebSocketConnection, WebSocketConnectionManager } from './websocket-connection';
import { createScreenplayDoc, getOrCreateSharedTypes, ScreenplayMetadata } from './yjs-document';
export interface CRDTDocumentManager {
initialize(projectId: string, serverUrl: string, authToken: string): Promise<Doc>;
getText(type: string): Text;
getMetadata(): ScreenplayMetadata;
getProvider(): any; // WebSocketProvider
applyRemoteUpdate(update: Uint8Array, origin: string): void;
getUndoManager(): UndoManager;
destroy(): void;
}
export class CRDTDocument implements CRDTDocumentManager {
private doc: Doc | null = null;
private connection: WebSocketConnectionManager | null = null;
private undoManager: UndoManager | null = null;
private projectId: string | null = null;
async initialize(
projectId: string,
serverUrl: string,
authToken: string
): Promise<Doc> {
this.projectId = projectId;
// Create Yjs document
this.doc = createScreenplayDoc(projectId, {});
// Initialize WebSocket connection
this.connection = new WebSocketConnection({
serverUrl,
documentName: `project-${projectId}`,
authToken,
reconnectInterval: 1000,
maxReconnectInterval: 30000,
});
// Connect to WebSocket server
await this.connection.connect();
// Get the provider to access the synced document
const provider = this.connection.getProvider();
// Sync local document with remote state
// Yjs WebSocketProvider handles this automatically on connect
// Initialize undo manager (single instance handles both undo and redo)
const sharedTypes = getOrCreateSharedTypes(this.doc);
this.undoManager = new UndoManager([sharedTypes.text], {
captureTimeout: 1000,
});
return this.doc;
}
getText(type: string = 'main'): Text {
if (!this.doc) {
throw new Error('Document not initialized. Call initialize() first.');
}
return this.doc.getText(type);
}
getMetadata(): ScreenplayMetadata {
if (!this.doc) {
throw new Error('Document not initialized. Call initialize() first.');
}
const meta = this.doc.getMap<ScreenplayMetadata>('metadata');
return meta.toJSON() as ScreenplayMetadata;
}
getProvider(): any {
if (!this.connection) {
throw new Error('Connection not initialized. Call initialize() first.');
}
return this.connection.getProvider();
}
applyRemoteUpdate(update: Uint8Array, origin: string): void {
if (!this.doc) {
throw new Error('Document not initialized.');
}
// Apply the update to the document
// Yjs handles the CRDT merge automatically
this.doc.transact(() => {
applyUpdate(this.doc!, update);
}, origin);
}
getUndoManager(): UndoManager {
if (!this.undoManager) {
throw new Error('Document not initialized. Call initialize() first.');
}
return this.undoManager;
}
destroy(): void {
if (this.undoManager) {
this.undoManager.destroy();
this.undoManager = null;
}
if (this.connection) {
this.connection.disconnect();
this.connection = null;
}
if (this.doc) {
this.doc.destroy();
this.doc = null;
}
this.projectId = null;
}
}