diff --git a/services/project-history/app/js/UpdateTranslator.js b/services/project-history/app/js/UpdateTranslator.js index 43b9f48270..aa3e98b35b 100644 --- a/services/project-history/app/js/UpdateTranslator.js +++ b/services/project-history/app/js/UpdateTranslator.js @@ -449,6 +449,10 @@ class OperationsBuilder { * @param {TrackingDirective} [opts.tracking] */ retain(length, opts = {}) { + // A zero-length retain is a no-op, they are dropped in overleaf-editor-core. + if (length === 0) { + return + } if (opts.tracking) { this.textOperation.push({ r: length, ...opts }) } else { diff --git a/services/project-history/test/unit/js/UpdateTranslator/UpdateTranslatorTests.js b/services/project-history/test/unit/js/UpdateTranslator/UpdateTranslatorTests.js index f285fb4c24..7ed946c3e5 100644 --- a/services/project-history/test/unit/js/UpdateTranslator/UpdateTranslatorTests.js +++ b/services/project-history/test/unit/js/UpdateTranslator/UpdateTranslatorTests.js @@ -672,6 +672,59 @@ describe('UpdateTranslator', function () { ]) }) + it('drops zero-length retains carrying tracking data', function () { + // A zero-length retain (e.g. an empty `r` string produced by the resync + // mechanism) must not be emitted as a zero-length tracked delete. + const updates = [ + { + update: { + doc: this.doc_id, + op: [ + { + p: 3, + r: '', + tracking: { type: 'delete', userId: this.user_id, ts: 0 }, + }, + ], + v: this.version, + meta: { + user_id: this.user_id, + ts: new Date(this.timestamp).getTime(), + pathname: '/main.tex', + doc_length: 20, + }, + }, + }, + ] + + const changes = this.UpdateTranslator.convertToChanges( + this.project_id, + updates + ).map(change => change.toRaw()) + + // The empty tracked retain is dropped, leaving a plain retain of the + // whole document. + expect(changes).to.deep.equal([ + { + authors: [], + operations: [ + { + pathname: 'main.tex', + textOperation: [20], + }, + ], + v2Authors: [this.user_id], + timestamp: this.timestamp, + v2DocVersions: { + '59bfd450e3028c4d40a1e9ab': { + pathname: 'main.tex', + v: 0, + }, + }, + }, + ]) + }) + it('can translate insertions at the start and end (with zero retained)', function () { const updates = [ {