Merge pull request #18342 from overleaf/em-tracking-props-none

Separate handling of "none" tracking type in operations

GitOrigin-RevId: b07ee5320ac1a9c63e3b0059aade1a1564819943
This commit is contained in:
Eric Mc Sween 2024-05-16 08:23:57 -04:00 committed by Copybot
parent f5aa0a7a93
commit 5d0190da0f
17 changed files with 139 additions and 114 deletions

View file

@ -0,0 +1,28 @@
// @ts-check
/**
* @typedef {import('../types').ClearTrackingPropsRawData} ClearTrackingPropsRawData
*/
class ClearTrackingProps {
constructor() {
this.type = 'none'
}
/**
* @param {any} other
* @returns {boolean}
*/
equals(other) {
return other instanceof ClearTrackingProps
}
/**
* @returns {ClearTrackingPropsRawData}
*/
toRaw() {
return { type: 'none' }
}
}
module.exports = ClearTrackingProps

View file

@ -1,10 +1,11 @@
// @ts-check // @ts-check
const Range = require('../range') const Range = require('../range')
const TrackedChange = require('./tracked_change') const TrackedChange = require('./tracked_change')
const TrackingProps = require('../file_data/tracking_props')
/** /**
* @typedef {import("../types").TrackingDirective} TrackingDirective
* @typedef {import("../types").TrackedChangeRawData} TrackedChangeRawData * @typedef {import("../types").TrackedChangeRawData} TrackedChangeRawData
* @typedef {import("../file_data/tracking_props")} TrackingProps
*/ */
class TrackedChangeList { class TrackedChangeList {
@ -211,7 +212,7 @@ class TrackedChangeList {
/** /**
* @param {number} cursor * @param {number} cursor
* @param {number} length * @param {number} length
* @param {{tracking?: TrackingProps}} opts * @param {{tracking?: TrackingDirective}} opts
*/ */
applyRetain(cursor, length, opts = {}) { applyRetain(cursor, length, opts = {}) {
// If there's no tracking info, leave everything as-is // If there's no tracking info, leave everything as-is
@ -263,7 +264,7 @@ class TrackedChangeList {
newTrackedChanges.push(trackedChange) newTrackedChanges.push(trackedChange)
} }
} }
if (opts.tracking?.type === 'delete' || opts.tracking?.type === 'insert') { if (opts.tracking instanceof TrackingProps) {
// This is a new tracked change // This is a new tracked change
const newTrackedChange = new TrackedChange(retainedRange, opts.tracking) const newTrackedChange = new TrackedChange(retainedRange, opts.tracking)
newTrackedChanges.push(newTrackedChange) newTrackedChanges.push(newTrackedChange)

View file

@ -6,14 +6,14 @@
class TrackingProps { class TrackingProps {
/** /**
* *
* @param {'insert' | 'delete' | 'none'} type * @param {'insert' | 'delete'} type
* @param {string} userId * @param {string} userId
* @param {Date} ts * @param {Date} ts
*/ */
constructor(type, userId, ts) { constructor(type, userId, ts) {
/** /**
* @readonly * @readonly
* @type {'insert' | 'delete' | 'none'} * @type {'insert' | 'delete'}
*/ */
this.type = type this.type = type
/** /**

View file

@ -5,6 +5,7 @@ const {
InvalidInsertionError, InvalidInsertionError,
UnprocessableError, UnprocessableError,
} = require('../errors') } = require('../errors')
const ClearTrackingProps = require('../file_data/clear_tracking_props')
const TrackingProps = require('../file_data/tracking_props') const TrackingProps = require('../file_data/tracking_props')
/** /**
@ -13,6 +14,7 @@ const TrackingProps = require('../file_data/tracking_props')
* @typedef {import('../types').RawInsertOp} RawInsertOp * @typedef {import('../types').RawInsertOp} RawInsertOp
* @typedef {import('../types').RawRetainOp} RawRetainOp * @typedef {import('../types').RawRetainOp} RawRetainOp
* @typedef {import('../types').RawRemoveOp} RawRemoveOp * @typedef {import('../types').RawRemoveOp} RawRemoveOp
* @typedef {import('../types').TrackingDirective} TrackingDirective
*/ */
class ScanOp { class ScanOp {
@ -218,7 +220,7 @@ class InsertOp extends ScanOp {
class RetainOp extends ScanOp { class RetainOp extends ScanOp {
/** /**
* @param {number} length * @param {number} length
* @param {TrackingProps | undefined} tracking * @param {TrackingDirective | undefined} tracking
*/ */
constructor(length, tracking = undefined) { constructor(length, tracking = undefined) {
super() super()
@ -227,7 +229,7 @@ class RetainOp extends ScanOp {
} }
/** @type {number} */ /** @type {number} */
this.length = length this.length = length
/** @type {TrackingProps | undefined} */ /** @type {TrackingDirective | undefined} */
this.tracking = tracking this.tracking = tracking
} }
@ -263,7 +265,11 @@ class RetainOp extends ScanOp {
throw new Error('retain operation must have a number property') throw new Error('retain operation must have a number property')
} }
if (op.tracking) { if (op.tracking) {
return new RetainOp(op.r, TrackingProps.fromRaw(op.tracking)) const tracking =
op.tracking.type === 'none'
? new ClearTrackingProps()
: TrackingProps.fromRaw(op.tracking)
return new RetainOp(op.r, tracking)
} }
return new RetainOp(op.r) return new RetainOp(op.r)
} }

View file

@ -26,12 +26,15 @@ const {
TooLongError, TooLongError,
} = require('../errors') } = require('../errors')
const Range = require('../range') const Range = require('../range')
const ClearTrackingProps = require('../file_data/clear_tracking_props')
const TrackingProps = require('../file_data/tracking_props') const TrackingProps = require('../file_data/tracking_props')
/** /**
* @typedef {import('../file_data/string_file_data')} StringFileData * @typedef {import('../file_data/string_file_data')} StringFileData
* @typedef {import('../types').RawTextOperation} RawTextOperation * @typedef {import('../types').RawTextOperation} RawTextOperation
* @typedef {import('../operation/scan_op').ScanOp} ScanOp * @typedef {import('../operation/scan_op').ScanOp} ScanOp
* @typedef {import('../file_data/tracked_change_list')} TrackedChangeList * @typedef {import('../file_data/tracked_change_list')} TrackedChangeList
* @typedef {import('../types').TrackingDirective} TrackingDirective
*/ */
/** /**
@ -91,7 +94,7 @@ class TextOperation extends EditOperation {
/** /**
* Skip over a given number of characters. * Skip over a given number of characters.
* @param {number | {r: number}} n * @param {number | {r: number}} n
* @param {{tracking?: TrackingProps}} opts * @param {{tracking?: TrackingDirective}} opts
* @returns {TextOperation} * @returns {TextOperation}
*/ */
retain(n, opts = {}) { retain(n, opts = {}) {
@ -376,11 +379,7 @@ class TextOperation extends EditOperation {
let removeTrackingInfoIfNeeded let removeTrackingInfoIfNeeded
if (op.tracking) { if (op.tracking) {
removeTrackingInfoIfNeeded = new TrackingProps( removeTrackingInfoIfNeeded = new ClearTrackingProps()
'none',
op.tracking.userId,
op.tracking.ts
)
} }
for (const trackedChange of previousRanges) { for (const trackedChange of previousRanges) {
@ -575,10 +574,15 @@ class TextOperation extends EditOperation {
} }
} else if (op1 instanceof InsertOp && op2 instanceof RetainOp) { } else if (op1 instanceof InsertOp && op2 instanceof RetainOp) {
const opts = { const opts = {
// Prefer the latter tracking info
tracking: op2.tracking ?? op1.tracking,
commentIds: op1.commentIds, commentIds: op1.commentIds,
} }
if (op2.tracking instanceof TrackingProps) {
// Prefer the tracking info on the second operation
opts.tracking = op2.tracking
} else if (!(op2.tracking instanceof ClearTrackingProps)) {
// The second operation does not cancel the first operation's tracking
opts.tracking = op1.tracking
}
if (op1.insertion.length > op2.length) { if (op1.insertion.length > op2.length) {
operation.insert(op1.insertion.slice(0, op2.length), opts) operation.insert(op1.insertion.slice(0, op2.length), opts)
op1 = new InsertOp( op1 = new InsertOp(
@ -692,9 +696,9 @@ class TextOperation extends EditOperation {
// Simple case: retain/retain // Simple case: retain/retain
// If both have tracking info, we use the one from op1 // If both have tracking info, we use the one from op1
/** @type {TrackingProps | undefined} */ /** @type {TrackingProps | ClearTrackingProps | undefined} */
let operation1primeTracking let operation1primeTracking
/** @type {TrackingProps | undefined} */ /** @type {TrackingProps | ClearTrackingProps | undefined} */
let operation2primeTracking let operation2primeTracking
if (op1.tracking) { if (op1.tracking) {
operation1primeTracking = op1.tracking operation1primeTracking = op1.tracking

View file

@ -1,4 +1,6 @@
import Blob from './blob' import Blob from './blob'
import TrackingProps from './file_data/tracking_props'
import ClearTrackingProps from './file_data/clear_tracking_props'
export type BlobStore = { export type BlobStore = {
getBlob(hash: string): Promise<Blob | null> getBlob(hash: string): Promise<Blob | null>
@ -32,11 +34,17 @@ export type TrackedChangeRawData = {
} }
export type TrackingPropsRawData = { export type TrackingPropsRawData = {
type: 'insert' | 'delete' | 'none' type: 'insert' | 'delete'
userId: string userId: string
ts: string ts: string
} }
export type ClearTrackingPropsRawData = {
type: 'none'
}
export type TrackingDirective = TrackingProps | ClearTrackingProps
export type StringFileRawData = { export type StringFileRawData = {
content: string content: string
comments?: CommentRawData[] comments?: CommentRawData[]
@ -58,7 +66,7 @@ export type RawRetainOp =
| { | {
r: number r: number
commentIds?: string[] commentIds?: string[]
tracking?: TrackingPropsRawData tracking?: TrackingPropsRawData | ClearTrackingPropsRawData
} }
| number | number

View file

@ -1,4 +1,5 @@
const TrackingProps = require('../../lib/file_data/tracking_props') const TrackingProps = require('../../lib/file_data/tracking_props')
const ClearTrackingProps = require('../../lib/file_data/clear_tracking_props')
const TextOperation = require('../../lib/operation/text_operation') const TextOperation = require('../../lib/operation/text_operation')
const random = require('./random') const random = require('./random')
@ -19,7 +20,7 @@ function randomTextOperation(str, commentIds) {
const trackedChange = const trackedChange =
Math.random() < 0.1 Math.random() < 0.1
? new TrackingProps( ? new TrackingProps(
random.element(['insert', 'delete', 'none']), random.element(['insert', 'delete']),
random.element(['user1', 'user2', 'user3']), random.element(['user1', 'user2', 'user3']),
new Date( new Date(
random.element([ random.element([
@ -41,6 +42,8 @@ function randomTextOperation(str, commentIds) {
}) })
} else if (r < 0.4) { } else if (r < 0.4) {
operation.remove(l) operation.remove(l)
} else if (r < 0.5) {
operation.retain(l, { tracking: new ClearTrackingProps() })
} else { } else {
operation.retain(l, { tracking: trackedChange }) operation.retain(l, { tracking: trackedChange })
} }

View file

@ -15,6 +15,7 @@ const TextOperation = ot.TextOperation
const StringFileData = require('../lib/file_data/string_file_data') const StringFileData = require('../lib/file_data/string_file_data')
const { RetainOp, InsertOp, RemoveOp } = require('../lib/operation/scan_op') const { RetainOp, InsertOp, RemoveOp } = require('../lib/operation/scan_op')
const TrackingProps = require('../lib/file_data/tracking_props') const TrackingProps = require('../lib/file_data/tracking_props')
const ClearTrackingProps = require('../lib/file_data/clear_tracking_props')
describe('TextOperation', function () { describe('TextOperation', function () {
const numTrials = 500 const numTrials = 500
@ -496,11 +497,7 @@ describe('TextOperation', function () {
new TextOperation() new TextOperation()
.retain(4) .retain(4)
.retain(4, { .retain(4, {
tracking: TrackingProps.fromRaw({ tracking: new ClearTrackingProps(),
ts: '2024-01-01T00:00:00.000Z',
type: 'none',
userId: 'user2',
}),
}) })
.retain(3) .retain(3)
) )
@ -508,6 +505,42 @@ describe('TextOperation', function () {
content: 'foo bar baz', content: 'foo bar baz',
}) })
}) })
it('it removes the tracking from an insert if operation 2 resolves it', function () {
expect(
compose(
new StringFileData('foo bar baz'),
new TextOperation()
.retain(4)
.insert('quux ', {
tracking: TrackingProps.fromRaw({
ts: '2023-01-01T00:00:00.000Z',
type: 'insert',
userId: 'user1',
}),
})
.retain(7),
new TextOperation()
.retain(6)
.retain(5, {
tracking: new ClearTrackingProps(),
})
.retain(5)
)
).to.deep.equal({
content: 'foo quux bar baz',
trackedChanges: [
{
range: { pos: 4, length: 2 },
tracking: {
ts: '2023-01-01T00:00:00.000Z',
type: 'insert',
userId: 'user1',
},
},
],
})
})
}) })
describe('transform', function () { describe('transform', function () {

View file

@ -1,6 +1,7 @@
// @ts-check // @ts-check
const TrackedChangeList = require('../lib/file_data/tracked_change_list') const TrackedChangeList = require('../lib/file_data/tracked_change_list')
const TrackingProps = require('../lib/file_data/tracking_props') const TrackingProps = require('../lib/file_data/tracking_props')
const ClearTrackingProps = require('../lib/file_data/clear_tracking_props')
const { expect } = require('chai') const { expect } = require('chai')
/** @typedef {import('../lib/types').TrackedChangeRawData} TrackedChangeRawData */ /** @typedef {import('../lib/types').TrackedChangeRawData} TrackedChangeRawData */
@ -766,11 +767,7 @@ describe('TrackedChangeList', function () {
}, },
]) ])
trackedChanges.applyRetain(0, 10, { trackedChanges.applyRetain(0, 10, {
tracking: TrackingProps.fromRaw({ tracking: new ClearTrackingProps(),
type: 'none',
userId: 'user1',
ts: '2024-01-01T00:00:00.000Z',
}),
}) })
expect(trackedChanges.length).to.equal(0) expect(trackedChanges.length).to.equal(0)
expect(trackedChanges.toRaw()).to.deep.equal([]) expect(trackedChanges.toRaw()).to.deep.equal([])
@ -788,11 +785,7 @@ describe('TrackedChangeList', function () {
}, },
]) ])
trackedChanges.applyRetain(0, 10, { trackedChanges.applyRetain(0, 10, {
tracking: TrackingProps.fromRaw({ tracking: new ClearTrackingProps(),
type: 'none',
userId: 'user2',
ts: '2024-01-01T00:00:00.000Z',
}),
}) })
expect(trackedChanges.length).to.equal(0) expect(trackedChanges.length).to.equal(0)
expect(trackedChanges.toRaw()).to.deep.equal([]) expect(trackedChanges.toRaw()).to.deep.equal([])
@ -810,11 +803,7 @@ describe('TrackedChangeList', function () {
}, },
]) ])
trackedChanges.applyRetain(0, 10, { trackedChanges.applyRetain(0, 10, {
tracking: TrackingProps.fromRaw({ tracking: new ClearTrackingProps(),
type: 'none',
userId: 'user1',
ts: '2024-01-01T00:00:00.000Z',
}),
}) })
expect(trackedChanges.length).to.equal(0) expect(trackedChanges.length).to.equal(0)
expect(trackedChanges.toRaw()).to.deep.equal([]) expect(trackedChanges.toRaw()).to.deep.equal([])
@ -832,11 +821,7 @@ describe('TrackedChangeList', function () {
}, },
]) ])
trackedChanges.applyRetain(0, 10, { trackedChanges.applyRetain(0, 10, {
tracking: TrackingProps.fromRaw({ tracking: new ClearTrackingProps(),
type: 'none',
userId: 'user2',
ts: '2024-01-01T00:00:00.000Z',
}),
}) })
expect(trackedChanges.length).to.equal(0) expect(trackedChanges.length).to.equal(0)
expect(trackedChanges.toRaw()).to.deep.equal([]) expect(trackedChanges.toRaw()).to.deep.equal([])

View file

@ -208,11 +208,7 @@ const RangesManager = {
op = { op = {
p: change.op.p, p: change.op.p,
r: change.op.i, r: change.op.i,
tracking: { tracking: { type: 'none' },
type: 'none',
userId: change.metadata.user_id,
ts: change.metadata.ts,
},
} }
if (unacceptedDeletes > 0) { if (unacceptedDeletes > 0) {
op.hpos = op.p + unacceptedDeletes op.hpos = op.p + unacceptedDeletes

View file

@ -1,4 +1,7 @@
import { TrackingPropsRawData } from 'overleaf-editor-core/lib/types' import {
TrackingPropsRawData,
ClearTrackingPropsRawData,
} from 'overleaf-editor-core/lib/types'
/** /**
* An update coming from the editor * An update coming from the editor
@ -97,7 +100,7 @@ export type HistoryInsertOp = InsertOp & {
export type HistoryRetainOp = RetainOp & { export type HistoryRetainOp = RetainOp & {
hpos?: number hpos?: number
tracking?: TrackingPropsRawData tracking?: TrackingPropsRawData | ClearTrackingPropsRawData
} }
export type HistoryDeleteOp = DeleteOp & { export type HistoryDeleteOp = DeleteOp & {

View file

@ -682,11 +682,7 @@ describe('RangesManager', function () {
{ {
r: 'lorem', r: 'lorem',
p: 0, p: 0,
tracking: { tracking: { type: 'none' },
type: 'none',
userId: this.user_id,
ts: ranges.changes[0].metadata.ts,
},
}, },
], ],
}, },
@ -702,11 +698,7 @@ describe('RangesManager', function () {
{ {
r: 'ipsum', r: 'ipsum',
p: 15, p: 15,
tracking: { tracking: { type: 'none' },
type: 'none',
userId: this.user_id,
ts: ranges.changes[1].metadata.ts,
},
}, },
], ],
}, },
@ -868,11 +860,7 @@ describe('RangesManager', function () {
r: 'xxx ', r: 'xxx ',
p: 6, p: 6,
hpos: 11, hpos: 11,
tracking: { tracking: { type: 'none' },
type: 'none',
userId: this.user_id,
ts: ranges.changes[1].metadata.ts,
},
}, },
], ],
}, },

View file

@ -309,11 +309,7 @@ class OperationsBuilder {
if (isInsert(op)) { if (isInsert(op)) {
if (op.trackedDeleteRejection) { if (op.trackedDeleteRejection) {
this.retain(op.i.length, { this.retain(op.i.length, {
tracking: { tracking: { type: 'none' },
type: 'none',
userId: update.meta.user_id,
ts: new Date(update.meta.ts).toISOString(),
},
}) })
} else { } else {
const opts = {} const opts = {}

View file

@ -140,11 +140,13 @@ export type RawOrigin = {
kind: string kind: string
} }
export type TrackingProps = { export type TrackingProps =
type: 'insert' | 'delete' | 'none' | { type: 'none' }
userId: string | {
ts: string type: 'insert' | 'delete'
} userId: string
ts: string
}
export type RawScanOp = export type RawScanOp =
| number | number

View file

@ -2142,11 +2142,7 @@ describe('ChunkTranslator', function () {
4, // Hell|o [planet ]world, this is a test 4, // Hell|o [planet ]world, this is a test
{ {
r: 4, r: 4,
tracking: { tracking: { type: 'none' },
type: 'none',
userId: this.author1.id,
ts: '2024-01-01T00:00:00.000Z',
},
}, // Hello pl|[anet ]world, this is a test }, // Hello pl|[anet ]world, this is a test
{ {
r: 3, r: 3,
@ -2375,11 +2371,7 @@ describe('ChunkTranslator', function () {
2, // Heo |{TEST }planet world, [this] is a test 2, // Heo |{TEST }planet world, [this] is a test
{ {
r: 5, r: 5,
tracking: { tracking: { type: 'none' },
type: 'none',
userId: '65f1baeb3cee7c3563fbd1d1',
ts: '2024-03-27T13:20:23.253Z',
},
}, // Heo TEST| planet world, [this] is a test }, // Heo TEST| planet world, [this] is a test
14, // Heo TEST planet world, |[this] is a test 14, // Heo TEST planet world, |[this] is a test
-4, // Heo TEST planet world, | is a test -4, // Heo TEST planet world, | is a test
@ -2557,11 +2549,7 @@ describe('ChunkTranslator', function () {
-2, // He|o planet world, [this] {TEST }is a test -2, // He|o planet world, [this] {TEST }is a test
{ {
r: 39, r: 39,
tracking: { tracking: { type: 'none' },
type: 'none',
userId: this.author1.id,
ts: this.date.toISOString(),
},
}, },
], // He|o planet world, this TEST is a test ], // He|o planet world, this TEST is a test
}, },

View file

@ -106,7 +106,7 @@ describe('UpdateCompressor', function () {
(this.op2 = { (this.op2 = {
p: 9, p: 9,
r: 'baz', r: 'baz',
tracking: { type: 'none', ts: this.ts1, userId: this.user_id }, tracking: { type: 'none' },
}), }),
(this.op3 = { p: 6, i: 'bar' }), (this.op3 = { p: 6, i: 'bar' }),
], ],

View file

@ -625,11 +625,7 @@ describe('UpdateTranslator', function () {
{ {
p: 3, p: 3,
r: 'lo', r: 'lo',
tracking: { tracking: { type: 'none' },
type: 'none',
ts: this.timestamp,
userId: this.user_id,
},
}, },
], ],
v: this.version, v: this.version,
@ -658,11 +654,7 @@ describe('UpdateTranslator', function () {
3, 3,
{ {
r: 2, r: 2,
tracking: { tracking: { type: 'none' },
type: 'none',
ts: this.timestamp,
userId: this.user_id,
},
}, },
15, 15,
], ],
@ -1044,11 +1036,7 @@ describe('UpdateTranslator', function () {
3, 3,
{ {
r: 3, r: 3,
tracking: { tracking: { type: 'none' },
type: 'none',
userId: this.user_id,
ts: new Date(this.timestamp).toISOString(),
},
}, },
14, 14,
], ],
@ -1123,11 +1111,7 @@ describe('UpdateTranslator', function () {
3, 3,
{ {
r: 17, r: 17,
tracking: { tracking: { type: 'none' },
type: 'none',
userId: this.user_id,
ts: new Date(this.timestamp).toISOString(),
},
}, },
3, 3,
-18, -18,