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
text/typescript
/**
* 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
};