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

410 lines (338 loc) 10.4 kB
/** * Operational Transform Implementation for Real-time Collaboration * Based on proven OT algorithms for conflict-free collaborative editing */ export enum OperationType { Retain = 'retain', Insert = 'insert', Delete = 'delete' } export interface BaseOperation { type: OperationType; author: string; timestamp: number; id: string; } export interface RetainOperation extends BaseOperation { type: OperationType.Retain; length: number; attributes?: Record<string, any>; } export interface InsertOperation extends BaseOperation { type: OperationType.Insert; text: string; attributes?: Record<string, any>; } export interface DeleteOperation extends BaseOperation { type: OperationType.Delete; length: number; } export type Operation = RetainOperation | InsertOperation | DeleteOperation; export interface OperationMeta { author: string; timestamp: number; sessionId: string; documentId: string; clientId: string; revision: number; } /** * Represents a single operation component */ export class Op { public readonly type: OperationType; public readonly length?: number; public readonly text?: string; public readonly attributes?: Record<string, any>; constructor(op: Partial<Operation>) { this.type = op.type!; this.length = (op as any).length; this.text = (op as any).text; this.attributes = (op as any).attributes; } static retain(length: number, attributes?: Record<string, any>): Op { return new Op({ type: OperationType.Retain, length, attributes }); } static insert(text: string, attributes?: Record<string, any>): Op { return new Op({ type: OperationType.Insert, text, attributes }); } static delete(length: number): Op { return new Op({ type: OperationType.Delete, length }); } isRetain(): this is Op & { length: number } { return this.type === OperationType.Retain; } isInsert(): this is Op & { text: string } { return this.type === OperationType.Insert; } isDelete(): this is Op & { length: number } { return this.type === OperationType.Delete; } getLength(): number { if (this.isRetain() || this.isDelete()) { return this.length; } if (this.isInsert()) { return this.text.length; } return 0; } equals(other: Op): boolean { if (this.type !== other.type) return false; switch (this.type) { case OperationType.Retain: return this.length === other.length && JSON.stringify(this.attributes) === JSON.stringify(other.attributes); case OperationType.Insert: return this.text === other.text && JSON.stringify(this.attributes) === JSON.stringify(other.attributes); case OperationType.Delete: return this.length === other.length; default: return false; } } canMergeWith(other: Op): boolean { if (this.type !== other.type) return false; switch (this.type) { case OperationType.Retain: case OperationType.Delete: return JSON.stringify(this.attributes) === JSON.stringify(other.attributes); case OperationType.Insert: return JSON.stringify(this.attributes) === JSON.stringify(other.attributes); default: return false; } } merge(other: Op): Op { if (!this.canMergeWith(other)) { throw new Error('Cannot merge incompatible operations'); } switch (this.type) { case OperationType.Retain: return Op.retain(this.length! + other.length!, this.attributes); case OperationType.Insert: return Op.insert(this.text! + other.text!, this.attributes); case OperationType.Delete: return Op.delete(this.length! + other.length!); default: throw new Error('Unknown operation type'); } } toJSON(): any { const result: any = { type: this.type }; if (this.length !== undefined) result.length = this.length; if (this.text !== undefined) result.text = this.text; if (this.attributes !== undefined) result.attributes = this.attributes; return result; } static fromJSON(json: any): Op { return new Op(json); } } /** * Represents a sequence of operations */ export class Delta { public ops: Op[]; public meta?: OperationMeta; constructor(ops: Op[] = [], meta?: OperationMeta) { this.ops = ops; this.meta = meta; } static fromOps(ops: Op[]): Delta { return new Delta(ops); } retain(length: number, attributes?: Record<string, any>): Delta { if (length <= 0) return this; const op = Op.retain(length, attributes); return this.push(op); } insert(text: string, attributes?: Record<string, any>): Delta { if (text.length === 0) return this; const op = Op.insert(text, attributes); return this.push(op); } delete(length: number): Delta { if (length <= 0) return this; const op = Op.delete(length); return this.push(op); } push(newOp: Op): Delta { const ops = [...this.ops]; if (ops.length === 0) { ops.push(newOp); } else { const lastOp = ops[ops.length - 1]; if (lastOp.canMergeWith(newOp)) { ops[ops.length - 1] = lastOp.merge(newOp); } else { ops.push(newOp); } } return new Delta(ops, this.meta); } compose(other: Delta): Delta { const result = new Delta(); let thisIndex = 0; let otherIndex = 0; while (thisIndex < this.ops.length || otherIndex < other.ops.length) { const thisOp = this.ops[thisIndex]; const otherOp = other.ops[otherIndex]; if (!thisOp) { result.ops.push(otherOp); otherIndex++; } else if (!otherOp) { result.ops.push(thisOp); thisIndex++; } else if (thisOp.isInsert()) { result.ops.push(thisOp); thisIndex++; } else if (otherOp.isDelete()) { result.ops.push(otherOp); otherIndex++; } else { const thisLength = thisOp.getLength(); const otherLength = otherOp.getLength(); if (thisLength === otherLength) { if (otherOp.isInsert()) { result.ops.push(otherOp); } else { result.ops.push(thisOp); } thisIndex++; otherIndex++; } else if (thisLength < otherLength) { if (otherOp.isInsert()) { result.ops.push(otherOp); } else { result.ops.push(thisOp); } // Split the other operation if (otherOp.isRetain()) { other.ops[otherIndex] = Op.retain(otherLength - thisLength, otherOp.attributes); } else if (otherOp.isDelete()) { other.ops[otherIndex] = Op.delete(otherLength - thisLength); } thisIndex++; } else { if (otherOp.isInsert()) { result.ops.push(otherOp); } else { if (thisOp.isRetain()) { result.ops.push(Op.retain(otherLength, thisOp.attributes)); } else { result.ops.push(Op.delete(otherLength)); } } // Split this operation if (thisOp.isRetain()) { this.ops[thisIndex] = Op.retain(thisLength - otherLength, thisOp.attributes); } else if (thisOp.isDelete()) { this.ops[thisIndex] = Op.delete(thisLength - otherLength); } otherIndex++; } } } return result.normalize(); } apply(text: string): string { let result = ''; let index = 0; for (const op of this.ops) { if (op.isRetain()) { result += text.slice(index, index + op.length); index += op.length; } else if (op.isInsert()) { result += op.text; } else if (op.isDelete()) { index += op.length; } } return result; } invert(text: string): Delta { const inverted = new Delta(); let index = 0; for (const op of this.ops) { if (op.isRetain()) { inverted.retain(op.length, op.attributes); index += op.length; } else if (op.isInsert()) { inverted.delete(op.text.length); } else if (op.isDelete()) { inverted.insert(text.slice(index, index + op.length)); index += op.length; } } return inverted; } normalize(): Delta { const normalized = new Delta(); for (const op of this.ops) { if (op.getLength() > 0) { normalized.push(op); } } return normalized; } getLength(): number { return this.ops.reduce((length, op) => { if (op.isInsert()) { return length + op.text.length; } else if (op.isRetain()) { return length + op.length; } return length; }, 0); } slice(start: number, end?: number): Delta { const sliced = new Delta(); let index = 0; for (const op of this.ops) { const opLength = op.getLength(); if (index + opLength <= start) { index += opLength; continue; } if (end !== undefined && index >= end) { break; } const sliceStart = Math.max(0, start - index); const sliceEnd = end !== undefined ? Math.min(opLength, end - index) : opLength; const sliceLength = sliceEnd - sliceStart; if (sliceLength > 0) { if (op.isRetain()) { sliced.retain(sliceLength, op.attributes); } else if (op.isInsert()) { sliced.insert(op.text.slice(sliceStart, sliceEnd), op.attributes); } else if (op.isDelete()) { sliced.delete(sliceLength); } } index += opLength; } return sliced; } toJSON(): any { return { ops: this.ops.map(op => op.toJSON()), meta: this.meta }; } static fromJSON(json: any): Delta { const ops = json.ops.map((op: any) => Op.fromJSON(op)); return new Delta(ops, json.meta); } toString(): string { return JSON.stringify(this.toJSON()); } equals(other: Delta): boolean { if (this.ops.length !== other.ops.length) return false; return this.ops.every((op, index) => op.equals(other.ops[index])); } clone(): Delta { return Delta.fromJSON(this.toJSON()); } }