UNPKG

recoder-code

Version:

Complete AI-powered development platform with ML model training, plugin registry, real-time collaboration, monitoring, infrastructure automation, and enterprise deployment capabilities

637 lines (549 loc) 16.9 kB
/** * Operational Transform Implementation * Complete operational transform library for real-time collaborative editing */ export interface Operation { type: 'retain' | 'insert' | 'delete'; length?: number; text?: string; attributes?: Record<string, any>; } export interface TextOperation { ops: Operation[]; baseLength: number; targetLength: number; } /** * Convert TextOperation to JSON */ export function textOperationToJSON(operation: TextOperation): any { return { ops: operation.ops, baseLength: operation.baseLength, targetLength: operation.targetLength }; } /** * Add toJSON method to TextOperation objects */ export function addToJSONMethod(operation: TextOperation): TextOperation & { toJSON(): any } { return { ...operation, toJSON: () => textOperationToJSON(operation) }; } /** * Create a new TextOperation */ export function createTextOperation(): TextOperation { return { ops: [], baseLength: 0, targetLength: 0 }; } /** * Retain operation - keep existing characters */ export function retain(operation: TextOperation, length: number, attributes?: Record<string, any>): TextOperation { if (length <= 0) return operation; const lastOp = operation.ops[operation.ops.length - 1]; if (lastOp && lastOp.type === 'retain' && !lastOp.attributes && !attributes) { lastOp.length = (lastOp.length || 0) + length; } else { operation.ops.push({ type: 'retain', length, attributes }); } operation.baseLength += length; operation.targetLength += length; return operation; } /** * Insert operation - add new text */ export function insert(operation: TextOperation, text: string, attributes?: Record<string, any>): TextOperation { if (!text) return operation; const lastOp = operation.ops[operation.ops.length - 1]; if (lastOp && lastOp.type === 'insert' && !lastOp.attributes && !attributes) { lastOp.text = (lastOp.text || '') + text; } else { operation.ops.push({ type: 'insert', text, attributes }); } operation.targetLength += text.length; return operation; } /** * Delete operation - remove existing characters */ export function deleteOp(operation: TextOperation, length: number): TextOperation { if (length <= 0) return operation; const lastOp = operation.ops[operation.ops.length - 1]; if (lastOp && lastOp.type === 'delete') { lastOp.length = (lastOp.length || 0) + length; } else { operation.ops.push({ type: 'delete', length }); } operation.baseLength += length; return operation; } /** * Apply an operation to a document */ export function apply(operation: TextOperation, document: string): string { if (operation.baseLength !== document.length) { throw new Error(`Base length ${operation.baseLength} does not match document length ${document.length}`); } let result = ''; let docIndex = 0; for (const op of operation.ops) { switch (op.type) { case 'retain': const retainLength = op.length || 0; result += document.substring(docIndex, docIndex + retainLength); docIndex += retainLength; break; case 'insert': result += op.text || ''; break; case 'delete': docIndex += op.length || 0; break; } } if (result.length !== operation.targetLength) { throw new Error(`Target length ${operation.targetLength} does not match result length ${result.length}`); } return result; } /** * Transform operation A against operation B (when B was applied first) * Returns the transformed operation A' */ export function transform(opA: TextOperation, opB: TextOperation, priority: 'left' | 'right' = 'left'): TextOperation { if (opA.baseLength !== opB.baseLength) { throw new Error('Base lengths must be equal for transformation'); } const result = createTextOperation(); let aIndex = 0; let bIndex = 0; while (aIndex < opA.ops.length && bIndex < opB.ops.length) { const aOp = opA.ops[aIndex]; const bOp = opB.ops[bIndex]; if (aOp.type === 'insert') { insert(result, aOp.text || '', aOp.attributes); aIndex++; } else if (bOp.type === 'insert') { retain(result, (bOp.text || '').length); bIndex++; } else if (aOp.type === 'retain' && bOp.type === 'retain') { const aLength = aOp.length || 0; const bLength = bOp.length || 0; if (aLength > bLength) { retain(result, bLength, aOp.attributes); aOp.length = aLength - bLength; bIndex++; } else if (aLength < bLength) { retain(result, aLength, aOp.attributes); bOp.length = bLength - aLength; aIndex++; } else { retain(result, aLength, aOp.attributes); aIndex++; bIndex++; } } else if (aOp.type === 'delete' && bOp.type === 'delete') { const aLength = aOp.length || 0; const bLength = bOp.length || 0; if (aLength > bLength) { aOp.length = aLength - bLength; bIndex++; } else if (aLength < bLength) { bOp.length = bLength - aLength; aIndex++; } else { aIndex++; bIndex++; } } else if (aOp.type === 'delete' && bOp.type === 'retain') { const aLength = aOp.length || 0; const bLength = bOp.length || 0; if (aLength > bLength) { deleteOp(result, bLength); aOp.length = aLength - bLength; bIndex++; } else if (aLength < bLength) { deleteOp(result, aLength); bOp.length = bLength - aLength; aIndex++; } else { deleteOp(result, aLength); aIndex++; bIndex++; } } else if (aOp.type === 'retain' && bOp.type === 'delete') { const aLength = aOp.length || 0; const bLength = bOp.length || 0; if (aLength > bLength) { aOp.length = aLength - bLength; bIndex++; } else if (aLength < bLength) { bOp.length = bLength - aLength; aIndex++; } else { aIndex++; bIndex++; } } } // Process remaining operations while (aIndex < opA.ops.length) { const aOp = opA.ops[aIndex]; if (aOp.type === 'insert') { insert(result, aOp.text || '', aOp.attributes); } else if (aOp.type === 'delete') { deleteOp(result, aOp.length || 0); } aIndex++; } while (bIndex < opB.ops.length) { const bOp = opB.ops[bIndex]; if (bOp.type === 'insert') { retain(result, (bOp.text || '').length); } bIndex++; } return result; } /** * Compose two operations (apply operation B after operation A) */ export function compose(opA: TextOperation, opB: TextOperation): TextOperation { if (opA.targetLength !== opB.baseLength) { throw new Error('Target length of first operation must equal base length of second operation'); } const result = createTextOperation(); result.baseLength = opA.baseLength; result.targetLength = opB.targetLength; let aIndex = 0; let bIndex = 0; while (aIndex < opA.ops.length && bIndex < opB.ops.length) { const aOp = opA.ops[aIndex]; const bOp = opB.ops[bIndex]; if (aOp.type === 'delete') { deleteOp(result, aOp.length || 0); aIndex++; } else if (bOp.type === 'insert') { insert(result, bOp.text || '', bOp.attributes); bIndex++; } else if (aOp.type === 'retain' && bOp.type === 'retain') { const aLength = aOp.length || 0; const bLength = bOp.length || 0; if (aLength > bLength) { retain(result, bLength, bOp.attributes || aOp.attributes); aOp.length = aLength - bLength; bIndex++; } else if (aLength < bLength) { retain(result, aLength, bOp.attributes || aOp.attributes); bOp.length = bLength - aLength; aIndex++; } else { retain(result, aLength, bOp.attributes || aOp.attributes); aIndex++; bIndex++; } } else if (aOp.type === 'insert' && bOp.type === 'retain') { const aLength = (aOp.text || '').length; const bLength = bOp.length || 0; if (aLength > bLength) { insert(result, (aOp.text || '').substring(0, bLength), bOp.attributes || aOp.attributes); aOp.text = (aOp.text || '').substring(bLength); bIndex++; } else if (aLength < bLength) { insert(result, aOp.text || '', bOp.attributes || aOp.attributes); bOp.length = bLength - aLength; aIndex++; } else { insert(result, aOp.text || '', bOp.attributes || aOp.attributes); aIndex++; bIndex++; } } else if (aOp.type === 'insert' && bOp.type === 'delete') { const aLength = (aOp.text || '').length; const bLength = bOp.length || 0; if (aLength > bLength) { aOp.text = (aOp.text || '').substring(bLength); bIndex++; } else if (aLength < bLength) { bOp.length = bLength - aLength; aIndex++; } else { aIndex++; bIndex++; } } else if (aOp.type === 'retain' && bOp.type === 'delete') { const aLength = aOp.length || 0; const bLength = bOp.length || 0; if (aLength > bLength) { deleteOp(result, bLength); aOp.length = aLength - bLength; bIndex++; } else if (aLength < bLength) { deleteOp(result, aLength); bOp.length = bLength - aLength; aIndex++; } else { deleteOp(result, aLength); aIndex++; bIndex++; } } } // Process remaining operations while (aIndex < opA.ops.length) { const aOp = opA.ops[aIndex]; if (aOp.type === 'delete') { deleteOp(result, aOp.length || 0); } else if (aOp.type === 'insert') { insert(result, aOp.text || '', aOp.attributes); } aIndex++; } while (bIndex < opB.ops.length) { const bOp = opB.ops[bIndex]; if (bOp.type === 'insert') { insert(result, bOp.text || '', bOp.attributes); } else if (bOp.type === 'delete') { deleteOp(result, bOp.length || 0); } bIndex++; } return result; } /** * Invert an operation (create an operation that undoes this operation) */ export function invert(operation: TextOperation, document: string): TextOperation { if (operation.baseLength !== document.length) { throw new Error('Document length must match operation base length'); } const result = createTextOperation(); result.baseLength = operation.targetLength; result.targetLength = operation.baseLength; let docIndex = 0; for (const op of operation.ops) { switch (op.type) { case 'retain': const retainLength = op.length || 0; retain(result, retainLength); docIndex += retainLength; break; case 'insert': const insertLength = (op.text || '').length; deleteOp(result, insertLength); break; case 'delete': const deleteLength = op.length || 0; insert(result, document.substring(docIndex, docIndex + deleteLength)); docIndex += deleteLength; break; } } return result; } /** * Create an operation from a simple text diff */ export function fromDiff(oldText: string, newText: string): TextOperation { const operation = createTextOperation(); // Simple diff algorithm - in production, use a more sophisticated diff let i = 0; let j = 0; // Find common prefix while (i < oldText.length && i < newText.length && oldText[i] === newText[i]) { i++; } if (i > 0) { retain(operation, i); } // Find common suffix let oldEnd = oldText.length; let newEnd = newText.length; while (oldEnd > i && newEnd > i && oldText[oldEnd - 1] === newText[newEnd - 1]) { oldEnd--; newEnd--; } // Delete middle part of old text if (oldEnd > i) { deleteOp(operation, oldEnd - i); } // Insert middle part of new text if (newEnd > i) { insert(operation, newText.substring(i, newEnd)); } // Retain common suffix if (oldEnd < oldText.length) { retain(operation, oldText.length - oldEnd); } operation.baseLength = oldText.length; operation.targetLength = newText.length; return operation; } /** * Validate that an operation is well-formed */ export function isValid(operation: TextOperation): boolean { let baseLength = 0; let targetLength = 0; for (const op of operation.ops) { switch (op.type) { case 'retain': if (typeof op.length !== 'number' || op.length <= 0) return false; baseLength += op.length; targetLength += op.length; break; case 'insert': if (typeof op.text !== 'string' || op.text.length === 0) return false; targetLength += op.text.length; break; case 'delete': if (typeof op.length !== 'number' || op.length <= 0) return false; baseLength += op.length; break; default: return false; } } return baseLength === operation.baseLength && targetLength === operation.targetLength; } // Delta class for JSON operations export class Delta { ops: Operation[]; baseLength: number; targetLength: number; meta?: any; constructor(ops: Operation[] = [], baseLength: number = 0, targetLength: number = 0) { this.ops = ops; this.baseLength = baseLength; this.targetLength = targetLength; } static fromJSON(json: any): Delta { const delta = new Delta(); delta.ops = json.ops || []; delta.baseLength = json.baseLength || 0; delta.targetLength = json.targetLength || 0; delta.meta = json.meta; return delta; } toJSON(): any { return { ops: this.ops, baseLength: this.baseLength, targetLength: this.targetLength, meta: this.meta }; } } export type SelectionRange = { start: { line: number; column: number }; end: { line: number; column: number }; }; export interface DocumentStateInterface { content: string; version: number; operations: TextOperation[]; } export class DocumentState implements DocumentStateInterface { content: string; version: number; operations: TextOperation[]; constructor(content: string = '', version: number = 0) { this.content = content; this.version = version; this.operations = []; } getContent(): string { return this.content; } getRevision(): number { return this.version; } applyConcurrentOperation( operation: TextOperation, baseRevision: number, userId: string ): { success: boolean; newRevision?: number; transformedOperation?: TextOperation; error?: string } { try { // Transform operation if needed let transformedOp = operation; if (baseRevision !== this.version) { // For simplicity, reject operations that are not at current version return { success: false, error: 'Operation base revision does not match current version', newRevision: this.version }; } // Apply operation this.content = apply(transformedOp, this.content); this.version++; this.operations.push(transformedOp); return { success: true, newRevision: this.version, transformedOperation: transformedOp }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error', newRevision: this.version }; } } getOperationsSince(revision: number): TextOperation[] { return this.operations.slice(revision); } } export class OperationalTransform { static transform = transform; static apply = apply; static compose = compose; static invert = invert; static fromDiff = fromDiff; static isValid = isValid; } export class SelectionManager { private selections: Map<string, SelectionRange> = new Map(); updateSelection(userId: string, selection: SelectionRange): void { this.selections.set(userId, selection); } getSelections(): Map<string, SelectionRange> { return this.selections; } transformSelections(operation: TextOperation): void { // Transform all selections based on the operation for (const [userId, selection] of this.selections.entries()) { this.selections.set(userId, SelectionManager.transformSelection(selection, operation)); } } static transformSelection( selection: SelectionRange, operation: TextOperation ): SelectionRange { // Simplified selection transformation return selection; } } export default { createTextOperation, retain, insert, delete: deleteOp, apply, transform, compose, invert, fromDiff, isValid, OperationalTransform, SelectionManager };