[project-history] add support for resync of history-ot ranges (#26475)

* [project-history] add support for resync of history-ot ranges

* [project-history] avoid compressing sharejs and history-ot upgrades

* [document-updater] improve error message of some assertions

... by migrating the assertions like this:
```diff
-stub.calledWith().should.equal(true)
+stub.should.have.been.calledWith()
```
```diff
-stub.called.should.equal(false)
+stub.should.not.have.been.called
```

* [document-updater] move content field in resyncDocContent

* [document-updater] add support for resync of history-ot ranges

GitOrigin-RevId: e6104686a26934a5f25a8f095cbe00c163fbbaa7
This commit is contained in:
Jakob Ackermann
2025-06-19 08:05:18 +00:00
committed by Copybot
parent 5b39c76aa8
commit adf399fb95
9 changed files with 1579 additions and 74 deletions
+134 -5
View File
@@ -23,6 +23,7 @@ import { isInsert, isDelete } from './Utils.js'
/**
* @import { Comment as HistoryComment, TrackedChange as HistoryTrackedChange } from 'overleaf-editor-core'
* @import { CommentRawData, TrackedChangeRawData } from 'overleaf-editor-core/lib/types'
* @import { Comment, Entity, ResyncDocContentUpdate, RetainOp, TrackedChange } from './types'
* @import { TrackedChangeTransition, TrackingDirective, TrackingType, Update } from './types'
* @import { ProjectStructureUpdate } from './types'
@@ -764,11 +765,19 @@ class SyncUpdateExpander {
}
const persistedComments = file.getComments().toArray()
await this.queueUpdatesForOutOfSyncComments(
update,
pathname,
persistedComments
)
if (update.resyncDocContent.historyOTRanges) {
this.queueUpdatesForOutOfSyncCommentsHistoryOT(
update,
pathname,
file.getComments().toRaw()
)
} else {
await this.queueUpdatesForOutOfSyncComments(
update,
pathname,
persistedComments
)
}
const persistedChanges = file.getTrackedChanges().asSorted()
await this.queueUpdatesForOutOfSyncTrackedChanges(
@@ -825,6 +834,91 @@ class SyncUpdateExpander {
return expandedUpdate
}
/**
* Queue updates for out of sync comments
*
* @param {ResyncDocContentUpdate} update
* @param {string} pathname
* @param {CommentRawData[]} persistedComments
*/
queueUpdatesForOutOfSyncCommentsHistoryOT(
update,
pathname,
persistedComments
) {
const expectedComments =
update.resyncDocContent.historyOTRanges?.comments ?? []
const expectedCommentsById = new Map(
expectedComments.map(comment => [comment.id, comment])
)
const persistedCommentsById = new Map(
persistedComments.map(comment => [comment.id, comment])
)
// Delete any persisted comment that is not in the expected comment list.
for (const persistedComment of persistedComments) {
if (!expectedCommentsById.has(persistedComment.id)) {
this.expandedUpdates.push({
doc: update.doc,
op: [{ deleteComment: persistedComment.id }],
meta: {
pathname,
resync: true,
origin: this.origin,
ts: update.meta.ts,
},
})
}
}
for (const expectedComment of expectedComments) {
const persistedComment = persistedCommentsById.get(expectedComment.id)
if (
persistedComment &&
commentRangesAreInSyncHistoryOT(persistedComment, expectedComment)
) {
if (expectedComment.resolved === persistedComment.resolved) {
// Both comments are identical; do nothing
} else {
// Only the resolved state differs
this.expandedUpdates.push({
doc: update.doc,
op: [
{
commentId: expectedComment.id,
resolved: expectedComment.resolved,
},
],
meta: {
pathname,
resync: true,
origin: this.origin,
ts: update.meta.ts,
},
})
}
} else {
// New comment or ranges differ
this.expandedUpdates.push({
doc: update.doc,
op: [
{
commentId: expectedComment.id,
ranges: expectedComment.ranges,
resolved: expectedComment.resolved,
},
],
meta: {
pathname,
resync: true,
origin: this.origin,
ts: update.meta.ts,
},
})
}
}
}
/**
* Queue updates for out of sync comments
*
@@ -951,6 +1045,7 @@ class SyncUpdateExpander {
for (const transition of getTrackedChangesTransitions(
persistedChanges,
expectedChanges,
update.resyncDocContent.historyOTRanges?.trackedChanges || [],
expectedContent.length
)) {
if (transition.pos > cursor) {
@@ -1018,6 +1113,25 @@ class SyncUpdateExpander {
}
}
/**
* Compares the ranges in the persisted and expected comments
*
* @param {CommentRawData} persistedComment
* @param {CommentRawData} expectedComment
*/
function commentRangesAreInSyncHistoryOT(persistedComment, expectedComment) {
if (persistedComment.ranges.length !== expectedComment.ranges.length) {
return false
}
for (let i = 0; i < persistedComment.ranges.length; i++) {
const persistedRange = persistedComment.ranges[i]
const expectedRange = expectedComment.ranges[i]
if (persistedRange.pos !== expectedRange.pos) return false
if (persistedRange.length !== expectedRange.length) return false
}
return true
}
/**
* Compares the ranges in the persisted and expected comments
*
@@ -1049,11 +1163,13 @@ function commentRangesAreInSync(persistedComment, expectedComment) {
*
* @param {readonly HistoryTrackedChange[]} persistedChanges
* @param {TrackedChange[]} expectedChanges
* @param {TrackedChangeRawData[]} persistedChangesHistoryOT
* @param {number} docLength
*/
function getTrackedChangesTransitions(
persistedChanges,
expectedChanges,
persistedChangesHistoryOT,
docLength
) {
/** @type {TrackedChangeTransition[]} */
@@ -1076,6 +1192,19 @@ function getTrackedChangesTransitions(
})
}
for (const change of persistedChangesHistoryOT) {
transitions.push({
stage: 'expected',
pos: change.range.pos,
tracking: change.tracking,
})
transitions.push({
stage: 'expected',
pos: change.range.pos + change.range.length,
tracking: { type: 'none' },
})
}
for (const change of expectedChanges) {
const op = change.op
const pos = op.hpos ?? op.p
@@ -169,7 +169,9 @@ export function concatUpdatesWithSameVersion(updates) {
lastUpdate.op != null &&
lastUpdate.v === update.v &&
lastUpdate.doc === update.doc &&
lastUpdate.pathname === update.pathname
lastUpdate.pathname === update.pathname &&
EditOperationBuilder.isValid(update.op[0]) ===
EditOperationBuilder.isValid(lastUpdate.op[0])
) {
lastUpdate.op = lastUpdate.op.concat(update.op)
if (update.meta.doc_hash == null) {
+6
View File
@@ -3,6 +3,8 @@ import {
LinkedFileData,
RawEditOperation,
RawOrigin,
CommentRawData,
TrackedChangeRawData,
} from 'overleaf-editor-core/lib/types'
export type Update =
@@ -118,6 +120,10 @@ export type ResyncDocContentUpdate = {
content: string
version: number
ranges?: Ranges
historyOTRanges?: {
comments: CommentRawData[]
trackedChanges: TrackedChangeRawData[]
}
resolvedCommentIds?: string[]
}
projectHistoryId: string