- 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>
123 lines
3.4 KiB
TypeScript
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;
|
|
}
|
|
}
|