Просмотр исходного кода

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

History OT type: operate on parsed EditOperations

GitOrigin-RevId: dbb35789736958d4ef398e566400d6e9a0e49e8b
Eric Mc Sween 1 год назад
Родитель
Сommit
7a556cf1fd

+ 18 - 3
services/web/frontend/js/features/ide-react/editor/share-js-doc.ts

@@ -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)

+ 49 - 66
services/web/frontend/js/features/ide-react/editor/share-js-history-ot-type.ts

@@ -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)
             )
+            inputCursor += op.length
+            trackedChangesInvalidated = true
           }
+        }
 
-          if (trackedChangesInvalidated) {
-            this.emit('tracked-changes-invalidated')
-          }
+        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.

+ 2 - 0
services/web/frontend/js/features/ide-react/editor/types/document.ts

@@ -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[]
 }

+ 1 - 1
services/web/frontend/js/features/source-editor/extensions/realtime.ts

@@ -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
         }
       }