Merge pull request #26041 from overleaf/em-history-ot-type-serialize

History OT type: operate on parsed EditOperations

GitOrigin-RevId: dbb35789736958d4ef398e566400d6e9a0e49e8b
This commit is contained in:
Eric Mc Sween
2025-06-03 12:08:42 -04:00
committed by Copybot
parent f11ea06c1a
commit 7a556cf1fd
4 changed files with 71 additions and 71 deletions

View File

@@ -18,8 +18,15 @@ import { EditorFacade } from '@/features/source-editor/extensions/realtime'
import { recordDocumentFirstChangeEvent } from '@/features/event-tracking/document-first-change-event'
import getMeta from '@/utils/meta'
import { historyOTType } from './share-js-history-ot-type'
import { StringFileData, TrackedChangeList } from 'overleaf-editor-core/index'
import { StringFileRawData } from 'overleaf-editor-core/lib/types'
import {
StringFileData,
TrackedChangeList,
EditOperationBuilder,
} from 'overleaf-editor-core'
import {
StringFileRawData,
RawEditOperation,
} from 'overleaf-editor-core/lib/types'
// All times below are in milliseconds
const SINGLE_USER_FLUSH_DELAY = 2000
@@ -259,7 +266,15 @@ export class ShareJsDoc extends EventEmitter {
// issues are resolved.
processUpdateFromServer(message: Message) {
try {
this._doc._onMessage(message)
if (this.type === 'history-ot' && message.op != null) {
const ops = message.op as RawEditOperation[]
this._doc._onMessage({
...message,
op: ops.map(EditOperationBuilder.fromJSON),
})
} else {
this._doc._onMessage(message)
}
} catch (error) {
// Version mismatches are thrown as errors
debugConsole.log(error)

View File

@@ -1,5 +1,5 @@
import {
EditOperationBuilder,
EditOperation,
EditOperationTransformer,
InsertOp,
RemoveOp,
@@ -7,7 +7,6 @@ import {
StringFileData,
TextOperation,
} from 'overleaf-editor-core'
import { RawEditOperation } from 'overleaf-editor-core/lib/types'
import { ShareDoc } from '../../../../../types/share-doc'
type Api = {
@@ -32,90 +31,74 @@ const api: Api & ThisType<Api & ShareDoc & { snapshot: StringFileData }> = {
},
_register() {
this.on(
'remoteop',
(rawEditOperation: RawEditOperation[], oldSnapshot: StringFileData) => {
const operation = EditOperationBuilder.fromJSON(rawEditOperation[0])
if (operation instanceof TextOperation) {
const str = oldSnapshot.getContent()
if (str.length !== operation.baseLength)
throw new TextOperation.ApplyError(
"The operation's base length must be equal to the string's length.",
operation,
str
)
this.on('remoteop', (ops: EditOperation[], oldSnapshot: StringFileData) => {
const operation = ops[0]
if (operation instanceof TextOperation) {
const str = oldSnapshot.getContent()
if (str.length !== operation.baseLength)
throw new TextOperation.ApplyError(
"The operation's base length must be equal to the string's length.",
operation,
str
)
let outputCursor = 0
let inputCursor = 0
let trackedChangesInvalidated = false
for (const op of operation.ops) {
if (op instanceof RetainOp) {
inputCursor += op.length
outputCursor += op.length
if (op.tracking != null) {
trackedChangesInvalidated = true
}
} else if (op instanceof InsertOp) {
this.emit(
'insert',
outputCursor,
op.insertion,
op.insertion.length
)
outputCursor += op.insertion.length
trackedChangesInvalidated = true
} else if (op instanceof RemoveOp) {
this.emit(
'delete',
outputCursor,
str.slice(inputCursor, inputCursor + op.length)
)
inputCursor += op.length
let outputCursor = 0
let inputCursor = 0
let trackedChangesInvalidated = false
for (const op of operation.ops) {
if (op instanceof RetainOp) {
inputCursor += op.length
outputCursor += op.length
if (op.tracking != null) {
trackedChangesInvalidated = true
}
}
if (inputCursor !== str.length) {
throw new TextOperation.ApplyError(
"The operation didn't operate on the whole string.",
operation,
str
} else if (op instanceof InsertOp) {
this.emit('insert', outputCursor, op.insertion, op.insertion.length)
outputCursor += op.insertion.length
trackedChangesInvalidated = true
} else if (op instanceof RemoveOp) {
this.emit(
'delete',
outputCursor,
str.slice(inputCursor, inputCursor + op.length)
)
}
if (trackedChangesInvalidated) {
this.emit('tracked-changes-invalidated')
inputCursor += op.length
trackedChangesInvalidated = true
}
}
if (inputCursor !== str.length) {
throw new TextOperation.ApplyError(
"The operation didn't operate on the whole string.",
operation,
str
)
}
if (trackedChangesInvalidated) {
this.emit('tracked-changes-invalidated')
}
}
)
})
},
}
export const historyOTType = {
api,
transformX(raw1: RawEditOperation[], raw2: RawEditOperation[]) {
const [a, b] = EditOperationTransformer.transform(
EditOperationBuilder.fromJSON(raw1[0]),
EditOperationBuilder.fromJSON(raw2[0])
)
return [[a.toJSON()], [b.toJSON()]]
transformX(ops1: EditOperation[], ops2: EditOperation[]) {
const [a, b] = EditOperationTransformer.transform(ops1[0], ops2[0])
return [[a], [b]]
},
apply(snapshot: StringFileData, rawEditOperation: RawEditOperation[]) {
const operation = EditOperationBuilder.fromJSON(rawEditOperation[0])
apply(snapshot: StringFileData, ops: EditOperation[]) {
const afterFile = StringFileData.fromRaw(snapshot.toRaw())
afterFile.edit(operation)
afterFile.edit(ops[0])
return afterFile
},
compose(op1: RawEditOperation[], op2: RawEditOperation[]) {
return [
EditOperationBuilder.fromJSON(op1[0])
.compose(EditOperationBuilder.fromJSON(op2[0]))
.toJSON(),
]
compose(ops1: EditOperation[], ops2: EditOperation[]) {
return [ops1[0].compose(ops2[0])]
},
// Do not provide normalize, used by submitOp to fixup bad input.

View File

@@ -1,5 +1,6 @@
import { StringFileData } from 'overleaf-editor-core'
import { AnyOperation } from '../../../../../../types/change'
import { RawEditOperation } from 'overleaf-editor-core/lib/types'
export type Version = number
@@ -36,4 +37,5 @@ export type Message = {
doc?: string
snapshot?: string | StringFileData
type?: ShareJsTextType
op?: AnyOperation[] | RawEditOperation[]
}

View File

@@ -360,7 +360,7 @@ class HistoryOTAdapter {
let snapshotUpdated = false
for (const effect of transaction.effects) {
if (effect.is(historyOTOperationEffect)) {
this.shareDoc.submitOp(effect.value.map(op => op.toJSON()))
this.shareDoc.submitOp(effect.value)
snapshotUpdated = true
}
}