/** * 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; 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 { 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('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; } }