2020-02-17 12:34:21 -05:00
|
|
|
/* eslint-disable
|
|
|
|
camelcase,
|
|
|
|
handle-callback-err,
|
|
|
|
new-cap,
|
|
|
|
no-throw-literal,
|
|
|
|
no-unused-vars,
|
|
|
|
*/
|
|
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
|
|
// Fix any style issues and re-enable lint.
|
2020-02-17 12:34:04 -05:00
|
|
|
/*
|
|
|
|
* decaffeinate suggestions:
|
|
|
|
* DS101: Remove unnecessary use of Array.from
|
|
|
|
* DS103: Rewrite code to no longer use __guard__
|
|
|
|
* DS207: Consider shorter variations of null checks
|
|
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
|
|
*/
|
2020-02-17 12:34:28 -05:00
|
|
|
let oneMinute, twoMegabytes, UpdateCompressor
|
|
|
|
const strInject = (s1, pos, s2) => s1.slice(0, pos) + s2 + s1.slice(pos)
|
|
|
|
const strRemove = (s1, pos, length) => s1.slice(0, pos) + s1.slice(pos + length)
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
const { diff_match_patch } = require('../lib/diff_match_patch')
|
|
|
|
const dmp = new diff_match_patch()
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
module.exports = UpdateCompressor = {
|
|
|
|
NOOP: 'noop',
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
// Updates come from the doc updater in format
|
|
|
|
// {
|
|
|
|
// op: [ { ... op1 ... }, { ... op2 ... } ]
|
|
|
|
// meta: { ts: ..., user_id: ... }
|
|
|
|
// }
|
|
|
|
// but it's easier to work with on op per update, so convert these updates to
|
|
|
|
// our compressed format
|
|
|
|
// [{
|
|
|
|
// op: op1
|
|
|
|
// meta: { start_ts: ... , end_ts: ..., user_id: ... }
|
|
|
|
// }, {
|
|
|
|
// op: op2
|
|
|
|
// meta: { start_ts: ... , end_ts: ..., user_id: ... }
|
|
|
|
// }]
|
|
|
|
convertToSingleOpUpdates(updates) {
|
|
|
|
const splitUpdates = []
|
|
|
|
for (const update of Array.from(updates)) {
|
|
|
|
// Reject any non-insert or delete ops, i.e. comments
|
|
|
|
const ops = update.op.filter(o => o.i != null || o.d != null)
|
|
|
|
if (ops.length === 0) {
|
|
|
|
splitUpdates.push({
|
|
|
|
op: UpdateCompressor.NOOP,
|
|
|
|
meta: {
|
|
|
|
start_ts: update.meta.start_ts || update.meta.ts,
|
|
|
|
end_ts: update.meta.end_ts || update.meta.ts,
|
|
|
|
user_id: update.meta.user_id
|
|
|
|
},
|
|
|
|
v: update.v
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
for (const op of Array.from(ops)) {
|
|
|
|
splitUpdates.push({
|
|
|
|
op,
|
|
|
|
meta: {
|
|
|
|
start_ts: update.meta.start_ts || update.meta.ts,
|
|
|
|
end_ts: update.meta.end_ts || update.meta.ts,
|
|
|
|
user_id: update.meta.user_id
|
|
|
|
},
|
|
|
|
v: update.v
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return splitUpdates
|
|
|
|
},
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
concatUpdatesWithSameVersion(updates) {
|
|
|
|
const concattedUpdates = []
|
|
|
|
for (const update of Array.from(updates)) {
|
|
|
|
const lastUpdate = concattedUpdates[concattedUpdates.length - 1]
|
|
|
|
if (lastUpdate != null && lastUpdate.v === update.v) {
|
|
|
|
if (update.op !== UpdateCompressor.NOOP) {
|
|
|
|
lastUpdate.op.push(update.op)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const nextUpdate = {
|
|
|
|
op: [],
|
|
|
|
meta: update.meta,
|
|
|
|
v: update.v
|
|
|
|
}
|
|
|
|
if (update.op !== UpdateCompressor.NOOP) {
|
|
|
|
nextUpdate.op.push(update.op)
|
|
|
|
}
|
|
|
|
concattedUpdates.push(nextUpdate)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return concattedUpdates
|
|
|
|
},
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
compressRawUpdates(lastPreviousUpdate, rawUpdates) {
|
|
|
|
if (
|
|
|
|
__guard__(
|
|
|
|
lastPreviousUpdate != null ? lastPreviousUpdate.op : undefined,
|
|
|
|
x => x.length
|
|
|
|
) > 1
|
|
|
|
) {
|
|
|
|
// if the last previous update was an array op, don't compress onto it.
|
|
|
|
// The avoids cases where array length changes but version number doesn't
|
|
|
|
return [lastPreviousUpdate].concat(
|
|
|
|
UpdateCompressor.compressRawUpdates(null, rawUpdates)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (lastPreviousUpdate != null) {
|
|
|
|
rawUpdates = [lastPreviousUpdate].concat(rawUpdates)
|
|
|
|
}
|
|
|
|
let updates = UpdateCompressor.convertToSingleOpUpdates(rawUpdates)
|
|
|
|
updates = UpdateCompressor.compressUpdates(updates)
|
|
|
|
return UpdateCompressor.concatUpdatesWithSameVersion(updates)
|
|
|
|
},
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
compressUpdates(updates) {
|
|
|
|
if (updates.length === 0) {
|
|
|
|
return []
|
|
|
|
}
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
let compressedUpdates = [updates.shift()]
|
|
|
|
for (const update of Array.from(updates)) {
|
|
|
|
const lastCompressedUpdate = compressedUpdates.pop()
|
|
|
|
if (lastCompressedUpdate != null) {
|
|
|
|
compressedUpdates = compressedUpdates.concat(
|
|
|
|
UpdateCompressor._concatTwoUpdates(lastCompressedUpdate, update)
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
compressedUpdates.push(update)
|
|
|
|
}
|
|
|
|
}
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
return compressedUpdates
|
|
|
|
},
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
MAX_TIME_BETWEEN_UPDATES: (oneMinute = 60 * 1000),
|
|
|
|
MAX_UPDATE_SIZE: (twoMegabytes = 2 * 1024 * 1024),
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
_concatTwoUpdates(firstUpdate, secondUpdate) {
|
|
|
|
let offset
|
|
|
|
firstUpdate = {
|
|
|
|
op: firstUpdate.op,
|
|
|
|
meta: {
|
|
|
|
user_id: firstUpdate.meta.user_id || null,
|
|
|
|
start_ts: firstUpdate.meta.start_ts || firstUpdate.meta.ts,
|
|
|
|
end_ts: firstUpdate.meta.end_ts || firstUpdate.meta.ts
|
|
|
|
},
|
|
|
|
v: firstUpdate.v
|
|
|
|
}
|
|
|
|
secondUpdate = {
|
|
|
|
op: secondUpdate.op,
|
|
|
|
meta: {
|
|
|
|
user_id: secondUpdate.meta.user_id || null,
|
|
|
|
start_ts: secondUpdate.meta.start_ts || secondUpdate.meta.ts,
|
|
|
|
end_ts: secondUpdate.meta.end_ts || secondUpdate.meta.ts
|
|
|
|
},
|
|
|
|
v: secondUpdate.v
|
|
|
|
}
|
2014-01-27 11:26:58 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
if (firstUpdate.meta.user_id !== secondUpdate.meta.user_id) {
|
|
|
|
return [firstUpdate, secondUpdate]
|
|
|
|
}
|
2014-01-27 11:26:58 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
if (
|
|
|
|
secondUpdate.meta.start_ts - firstUpdate.meta.end_ts >
|
|
|
|
UpdateCompressor.MAX_TIME_BETWEEN_UPDATES
|
|
|
|
) {
|
|
|
|
return [firstUpdate, secondUpdate]
|
|
|
|
}
|
2014-01-27 11:26:58 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
const firstOp = firstUpdate.op
|
|
|
|
const secondOp = secondUpdate.op
|
2016-01-26 07:23:21 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
const firstSize =
|
|
|
|
(firstOp.i != null ? firstOp.i.length : undefined) ||
|
|
|
|
(firstOp.d != null ? firstOp.d.length : undefined)
|
|
|
|
const secondSize =
|
|
|
|
(secondOp.i != null ? secondOp.i.length : undefined) ||
|
|
|
|
(secondOp.d != null ? secondOp.d.length : undefined)
|
2016-01-26 07:23:21 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
// Two inserts
|
|
|
|
if (
|
|
|
|
firstOp.i != null &&
|
|
|
|
secondOp.i != null &&
|
|
|
|
firstOp.p <= secondOp.p && secondOp.p <= firstOp.p + firstOp.i.length &&
|
|
|
|
firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
|
|
|
|
) {
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
meta: {
|
|
|
|
start_ts: firstUpdate.meta.start_ts,
|
|
|
|
end_ts: secondUpdate.meta.end_ts,
|
|
|
|
user_id: firstUpdate.meta.user_id
|
|
|
|
},
|
|
|
|
op: {
|
|
|
|
p: firstOp.p,
|
|
|
|
i: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i)
|
|
|
|
},
|
|
|
|
v: secondUpdate.v
|
|
|
|
}
|
|
|
|
]
|
|
|
|
// Two deletes
|
|
|
|
} else if (
|
|
|
|
firstOp.d != null &&
|
|
|
|
secondOp.d != null &&
|
|
|
|
secondOp.p <= firstOp.p && firstOp.p <= secondOp.p + secondOp.d.length &&
|
|
|
|
firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
|
|
|
|
) {
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
meta: {
|
|
|
|
start_ts: firstUpdate.meta.start_ts,
|
|
|
|
end_ts: secondUpdate.meta.end_ts,
|
|
|
|
user_id: firstUpdate.meta.user_id
|
|
|
|
},
|
|
|
|
op: {
|
|
|
|
p: secondOp.p,
|
|
|
|
d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d)
|
|
|
|
},
|
|
|
|
v: secondUpdate.v
|
|
|
|
}
|
|
|
|
]
|
|
|
|
// An insert and then a delete
|
|
|
|
} else if (
|
|
|
|
firstOp.i != null &&
|
|
|
|
secondOp.d != null &&
|
|
|
|
firstOp.p <= secondOp.p && secondOp.p <= firstOp.p + firstOp.i.length
|
|
|
|
) {
|
|
|
|
offset = secondOp.p - firstOp.p
|
|
|
|
const insertedText = firstOp.i.slice(offset, offset + secondOp.d.length)
|
|
|
|
// Only trim the insert when the delete is fully contained within in it
|
|
|
|
if (insertedText === secondOp.d) {
|
|
|
|
const insert = strRemove(firstOp.i, offset, secondOp.d.length)
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
meta: {
|
|
|
|
start_ts: firstUpdate.meta.start_ts,
|
|
|
|
end_ts: secondUpdate.meta.end_ts,
|
|
|
|
user_id: firstUpdate.meta.user_id
|
|
|
|
},
|
|
|
|
op: {
|
|
|
|
p: firstOp.p,
|
|
|
|
i: insert
|
|
|
|
},
|
|
|
|
v: secondUpdate.v
|
|
|
|
}
|
|
|
|
]
|
|
|
|
} else {
|
|
|
|
// This will only happen if the delete extends outside the insert
|
|
|
|
return [firstUpdate, secondUpdate]
|
|
|
|
}
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
// A delete then an insert at the same place, likely a copy-paste of a chunk of content
|
|
|
|
} else if (
|
|
|
|
firstOp.d != null &&
|
|
|
|
secondOp.i != null &&
|
|
|
|
firstOp.p === secondOp.p
|
|
|
|
) {
|
|
|
|
offset = firstOp.p
|
|
|
|
const diff_ops = this.diffAsShareJsOps(firstOp.d, secondOp.i)
|
|
|
|
if (diff_ops.length === 0) {
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
// Noop
|
|
|
|
meta: {
|
|
|
|
start_ts: firstUpdate.meta.start_ts,
|
|
|
|
end_ts: secondUpdate.meta.end_ts,
|
|
|
|
user_id: firstUpdate.meta.user_id
|
|
|
|
},
|
|
|
|
op: {
|
|
|
|
p: firstOp.p,
|
|
|
|
i: ''
|
|
|
|
},
|
|
|
|
v: secondUpdate.v
|
|
|
|
}
|
|
|
|
]
|
|
|
|
} else {
|
|
|
|
return diff_ops.map(function(op) {
|
|
|
|
op.p += offset
|
|
|
|
return {
|
|
|
|
meta: {
|
|
|
|
start_ts: firstUpdate.meta.start_ts,
|
|
|
|
end_ts: secondUpdate.meta.end_ts,
|
|
|
|
user_id: firstUpdate.meta.user_id
|
|
|
|
},
|
|
|
|
op,
|
|
|
|
v: secondUpdate.v
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return [firstUpdate, secondUpdate]
|
|
|
|
}
|
|
|
|
},
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
ADDED: 1,
|
|
|
|
REMOVED: -1,
|
|
|
|
UNCHANGED: 0,
|
|
|
|
diffAsShareJsOps(before, after, callback) {
|
|
|
|
if (callback == null) {
|
|
|
|
callback = function(error, ops) {}
|
|
|
|
}
|
|
|
|
const diffs = dmp.diff_main(before, after)
|
|
|
|
dmp.diff_cleanupSemantic(diffs)
|
2020-02-17 12:34:04 -05:00
|
|
|
|
2020-02-17 12:34:28 -05:00
|
|
|
const ops = []
|
|
|
|
let position = 0
|
|
|
|
for (const diff of Array.from(diffs)) {
|
|
|
|
const type = diff[0]
|
|
|
|
const content = diff[1]
|
|
|
|
if (type === this.ADDED) {
|
|
|
|
ops.push({
|
|
|
|
i: content,
|
|
|
|
p: position
|
|
|
|
})
|
|
|
|
position += content.length
|
|
|
|
} else if (type === this.REMOVED) {
|
|
|
|
ops.push({
|
|
|
|
d: content,
|
|
|
|
p: position
|
|
|
|
})
|
|
|
|
} else if (type === this.UNCHANGED) {
|
|
|
|
position += content.length
|
|
|
|
} else {
|
|
|
|
throw 'Unknown type'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ops
|
|
|
|
}
|
|
|
|
}
|
2020-02-17 12:34:04 -05:00
|
|
|
|
|
|
|
function __guard__(value, transform) {
|
2020-02-17 12:34:28 -05:00
|
|
|
return typeof value !== 'undefined' && value !== null
|
|
|
|
? transform(value)
|
|
|
|
: undefined
|
|
|
|
}
|