FRE-600: Implement Phase 1 WebSocket + Yjs CRDT foundation
- Create TypeScript and Vite configuration for SolidJS - Implement Yjs document structure for screenplay collaboration - Build WebSocket connection manager with exponential backoff reconnection - Create CRDT document manager with undo/redo support - Set up WebSocket sync server with JWT authentication - Add SolidJS reactive bindings for Yjs shared types - Build collaborative editor component - Write unit tests for CRDT operations - Document implementation in analysis/fre600_websocket_foundation.md Architecture: Yjs chosen over Automerge for better ecosystem and Tauri compatibility. WebSocket for sync, WebRTC for video. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
137
src/lib/collaboration/crdt-document.ts
Normal file
137
src/lib/collaboration/crdt-document.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* CRDT Document Manager
|
||||
* Coordinates Yjs document lifecycle, persistence, and sync
|
||||
*/
|
||||
|
||||
import { Doc, Text, Map as YMap, UndoManager } 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;
|
||||
createUndoStack(): UndoManager;
|
||||
createRedoStack(): UndoManager;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export class CRDTDocument implements CRDTDocumentManager {
|
||||
private doc: Doc | null = null;
|
||||
private connection: WebSocketConnectionManager | null = null;
|
||||
private undoManager: UndoManager | null = null;
|
||||
private redoManager: 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/redo managers
|
||||
const sharedTypes = getOrCreateSharedTypes(this.doc);
|
||||
this.undoManager = new UndoManager([sharedTypes.text], {
|
||||
captureTimeout: 1000,
|
||||
});
|
||||
|
||||
this.redoManager = 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.applyUpdate(update, origin);
|
||||
}
|
||||
|
||||
createUndoStack(): UndoManager {
|
||||
if (!this.undoManager) {
|
||||
throw new Error('Document not initialized. Call initialize() first.');
|
||||
}
|
||||
return this.undoManager;
|
||||
}
|
||||
|
||||
createRedoStack(): UndoManager {
|
||||
if (!this.redoManager) {
|
||||
throw new Error('Document not initialized. Call initialize() first.');
|
||||
}
|
||||
return this.redoManager;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.undoManager) {
|
||||
this.undoManager.destroy();
|
||||
this.undoManager = null;
|
||||
}
|
||||
|
||||
if (this.redoManager) {
|
||||
this.redoManager.destroy();
|
||||
this.redoManager = null;
|
||||
}
|
||||
|
||||
if (this.connection) {
|
||||
this.connection.disconnect();
|
||||
this.connection = null;
|
||||
}
|
||||
|
||||
if (this.doc) {
|
||||
this.doc = null;
|
||||
}
|
||||
|
||||
this.projectId = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user