mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Big refactor to use better names and separation of concerns
This commit is contained in:
parent
8a0aa55c91
commit
533b8e59a3
8 changed files with 435 additions and 525 deletions
|
@ -1,48 +0,0 @@
|
|||
{db, ObjectId} = require "./mongojs"
|
||||
HistoryBuilder = require "./HistoryBuilder"
|
||||
logger = require "logger-sharelatex"
|
||||
|
||||
module.exports = ConversionManager =
|
||||
OPS_TO_LEAVE: 100
|
||||
|
||||
popLastCompressedOp: (doc_id, callback = (error, op) ->) ->
|
||||
db.docHistory.findAndModify
|
||||
query: { doc_id: ObjectId(doc_id) }
|
||||
fields: { docOps: { $slice: -1 } }
|
||||
update: { $pop: { docOps: 1 } }
|
||||
, (error, history = { docOps: [] }) ->
|
||||
return callback(error) if error?
|
||||
callback null, history.docOps[0]
|
||||
|
||||
insertCompressedOps: (doc_id, docOps, callback = (error) ->) ->
|
||||
db.docHistory.update {
|
||||
doc_id: ObjectId(doc_id)
|
||||
}, {
|
||||
$push:
|
||||
docOps:
|
||||
$each: docOps
|
||||
}, {
|
||||
upsert: true
|
||||
}, callback
|
||||
|
||||
convertAndSaveRawOps: (doc_id, rawOps, callback = (error) ->) ->
|
||||
length = rawOps.length
|
||||
if length == 0
|
||||
return callback()
|
||||
|
||||
|
||||
ConversionManager.popLastCompressedOp doc_id, (error, lastCompressedOp) ->
|
||||
return callback(error) if error?
|
||||
|
||||
if !lastCompressedOp?
|
||||
rawOps = rawOps.slice(0) # Clone so we can modify in place
|
||||
lastCompressedOp = rawOps.shift()
|
||||
|
||||
uncompressedOps = [lastCompressedOp].concat rawOps
|
||||
compressedOps = HistoryBuilder.compressOps uncompressedOps
|
||||
|
||||
ConversionManager.insertCompressedOps doc_id, compressedOps, (error) ->
|
||||
return callback(error) if error?
|
||||
logger.log doc_id: doc_id, rawOpsLength: length, compressedOpsLength: compressedOps.length, "compressed doc ops"
|
||||
callback()
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
strInject = (s1, pos, s2) -> s1[...pos] + s2 + s1[pos..]
|
||||
strRemove = (s1, pos, length) -> s1[...pos] + s1[(pos + length)..]
|
||||
|
||||
module.exports = HistoryBuilder =
|
||||
normalizeUpdate: (update) ->
|
||||
if update.meta.start_ts?
|
||||
return [update] # already normalized
|
||||
|
||||
updates = []
|
||||
for op in update.op
|
||||
updates.push
|
||||
op: [op]
|
||||
meta:
|
||||
start_ts: update.meta.ts
|
||||
end_ts: update.meta.ts
|
||||
user_id: update.meta.user_id
|
||||
return updates
|
||||
|
||||
compressUpdates: (rawUpdates) ->
|
||||
return [] if rawUpdates.length == 0
|
||||
normalizedUpdates = []
|
||||
for rawUpdate in rawUpdates
|
||||
normalizedUpdates = normalizedUpdates.concat HistoryBuilder.normalizeUpdate(rawUpdate)
|
||||
|
||||
return [] if normalizedUpdates.length == 0
|
||||
firstPass = [normalizedUpdates.shift()]
|
||||
for update in normalizedUpdates
|
||||
lastCompressedUpdate = firstPass.pop()
|
||||
if lastCompressedUpdate?
|
||||
firstPass = firstPass.concat HistoryBuilder._concatTwoUpdates lastCompressedUpdate, update, false
|
||||
else
|
||||
firstPass.push rawUpdate
|
||||
|
||||
return [] if firstPass.length == 0
|
||||
secondPass = [firstPass.shift()]
|
||||
for update in firstPass
|
||||
lastCompressedUpdate = secondPass.pop()
|
||||
if lastCompressedUpdate?
|
||||
secondPass = secondPass.concat HistoryBuilder._concatTwoUpdates lastCompressedUpdate, update, true
|
||||
else
|
||||
secondPass.push update
|
||||
|
||||
return secondPass
|
||||
|
||||
MAX_TIME_BETWEEN_UPDATES: oneMinute = 60 * 1000
|
||||
|
||||
_concatTwoUpdates: (firstUpdate, secondUpdate, mergeInsertsAndDeletes) ->
|
||||
firstUpdate =
|
||||
op: firstUpdate.op
|
||||
meta:
|
||||
user_id: firstUpdate.meta.user_id or null
|
||||
start_ts: firstUpdate.meta.start_ts or firstUpdate.meta.ts
|
||||
end_ts: firstUpdate.meta.end_ts or firstUpdate.meta.ts
|
||||
secondUpdate =
|
||||
op: secondUpdate.op
|
||||
meta:
|
||||
user_id: secondUpdate.meta.user_id or null
|
||||
start_ts: secondUpdate.meta.start_ts or secondUpdate.meta.ts
|
||||
end_ts: secondUpdate.meta.end_ts or secondUpdate.meta.ts
|
||||
|
||||
if firstUpdate.meta.user_id != secondUpdate.meta.user_id
|
||||
return [firstUpdate, secondUpdate]
|
||||
|
||||
if secondUpdate.meta.start_ts - firstUpdate.meta.end_ts > HistoryBuilder.MAX_TIME_BETWEEN_UPDATES
|
||||
return [firstUpdate, secondUpdate]
|
||||
|
||||
firstOp = firstUpdate.op[0]
|
||||
secondOp = secondUpdate.op[0]
|
||||
# Two inserts
|
||||
if firstOp.i? and secondOp.i? and firstOp.p <= secondOp.p <= (firstOp.p + firstOp.i.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: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i)
|
||||
]
|
||||
]
|
||||
# Two deletes
|
||||
else if firstOp.d? and secondOp.d? and secondOp.p <= firstOp.p <= (secondOp.p + 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: secondOp.p
|
||||
d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d)
|
||||
]
|
||||
]
|
||||
# An insert and then a delete
|
||||
if mergeInsertsAndDeletes and firstOp.i? and secondOp.d? and firstOp.p <= secondOp.p <= (firstOp.p + firstOp.i.length)
|
||||
offset = secondOp.p - firstOp.p
|
||||
insertedText = firstOp.i.slice(offset, offset + secondOp.d.length)
|
||||
if insertedText == secondOp.d
|
||||
insert = strRemove(firstOp.i, offset, secondOp.d.length)
|
||||
return [] if insert == ""
|
||||
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
|
||||
]
|
||||
]
|
||||
else
|
||||
# This shouldn't be possible!
|
||||
return [firstUpdate, secondUpdate]
|
||||
else if mergeInsertsAndDeletes and firstOp.d? and secondOp.i? and firstOp.p == secondOp.p
|
||||
offset = firstOp.d.indexOf(secondOp.i)
|
||||
if offset == -1
|
||||
return [firstUpdate, secondUpdate]
|
||||
headD = firstOp.d.slice(0, offset)
|
||||
inserted = firstOp.d.slice(offset, secondOp.i.length)
|
||||
tailD = firstOp.d.slice(offset + secondOp.i.length)
|
||||
headP = firstOp.p
|
||||
tailP = firstOp.p + secondOp.i.length
|
||||
updates = []
|
||||
if headD != ""
|
||||
updates.push
|
||||
meta:
|
||||
start_ts: firstUpdate.meta.start_ts
|
||||
end_ts: secondUpdate.meta.end_ts
|
||||
user_id: firstUpdate.meta.user_id
|
||||
op: [
|
||||
p: headP
|
||||
d: headD
|
||||
]
|
||||
if tailD != ""
|
||||
updates.push
|
||||
meta:
|
||||
start_ts: firstUpdate.meta.start_ts
|
||||
end_ts: secondUpdate.meta.end_ts
|
||||
user_id: firstUpdate.meta.user_id
|
||||
op: [
|
||||
p: tailP
|
||||
d: tailD
|
||||
]
|
||||
if updates.length == 2
|
||||
updates[0].meta.start_ts = updates[0].meta.end_ts = firstUpdate.meta.start_ts
|
||||
updates[1].meta.start_ts = updates[1].meta.end_ts = secondUpdate.meta.end_ts
|
||||
return updates
|
||||
|
||||
else
|
||||
return [firstUpdate, secondUpdate]
|
||||
|
54
services/track-changes/app/coffee/HistoryManager.coffee
Normal file
54
services/track-changes/app/coffee/HistoryManager.coffee
Normal file
|
@ -0,0 +1,54 @@
|
|||
{db, ObjectId} = require "./mongojs"
|
||||
UpdateCompressor = require "./UpdateCompressor"
|
||||
logger = require "logger-sharelatex"
|
||||
|
||||
module.exports = HistoryManager =
|
||||
getLastCompressedUpdate: (doc_id, callback = (error, update) ->) ->
|
||||
db.docHistory
|
||||
.find(doc_id: ObjectId(doc_id.toString()))
|
||||
.sort(timestamp: -1)
|
||||
.limit(1)
|
||||
.toArray (error, compressedUpdates) ->
|
||||
return callback(error) if error?
|
||||
return callback null, compressedUpdates[0] or null
|
||||
|
||||
deleteCompressedUpdate: (id, callback = (error) ->) ->
|
||||
db.docHistory.delete({ _id: ObjectId(id.toString()) }, callback)
|
||||
|
||||
popLastCompressedUpdate: (doc_id, callback = (error, update) ->) ->
|
||||
HistoryManager.getLastCompressedUpdate doc_id, (error, update) ->
|
||||
return callback(error) if error?
|
||||
if update?
|
||||
HistoryManager.deleteCompressedUpdate update._id, (error) ->
|
||||
return callback(error) if error?
|
||||
callback null, update
|
||||
else
|
||||
callback null, null
|
||||
|
||||
insertCompressedUpdates: (doc_id, docUpdates, callback = (error) ->) ->
|
||||
db.docHistory.update {
|
||||
doc_id: ObjectId(doc_id)
|
||||
}, {
|
||||
$push:
|
||||
docUpdates:
|
||||
$each: docUpdates
|
||||
}, {
|
||||
upsert: true
|
||||
}, callback
|
||||
|
||||
compressAndSaveRawUpdates: (doc_id, rawUpdates, callback = (error) ->) ->
|
||||
length = rawUpdates.length
|
||||
if length == 0
|
||||
return callback()
|
||||
|
||||
|
||||
HistoryManager.popLastCompressedUpdate doc_id, (error, lastCompressedUpdate) ->
|
||||
return callback(error) if error?
|
||||
|
||||
compressedUpdates = UpdateCompressor.compressRawUpdates lastCompressedUpdate, rawUpdates
|
||||
|
||||
HistoryManager.insertCompressedUpdates doc_id, compressedUpdates, (error) ->
|
||||
return callback(error) if error?
|
||||
logger.log doc_id: doc_id, rawUpdatesLength: length, compressedUpdatesLength: compressedUpdates.length, "compressed doc updates"
|
||||
callback()
|
||||
|
118
services/track-changes/app/coffee/UpdateCompressor.coffee
Normal file
118
services/track-changes/app/coffee/UpdateCompressor.coffee
Normal file
|
@ -0,0 +1,118 @@
|
|||
strInject = (s1, pos, s2) -> s1[...pos] + s2 + s1[pos..]
|
||||
strRemove = (s1, pos, length) -> s1[...pos] + s1[(pos + length)..]
|
||||
|
||||
module.exports = UpdateCompressor =
|
||||
# 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: ... }
|
||||
# }]
|
||||
convertRawUpdatesToCompressedFormat: (updates) ->
|
||||
normalizedUpdates = []
|
||||
for update in updates
|
||||
for op in update.op
|
||||
normalizedUpdates.push
|
||||
op: op
|
||||
meta:
|
||||
start_ts: update.meta.start_ts or update.meta.ts
|
||||
end_ts: update.meta.end_ts or update.meta.ts
|
||||
user_id: update.meta.user_id
|
||||
return normalizedUpdates
|
||||
|
||||
compressRawUpdates: (lastPreviousUpdate, rawUpdates) ->
|
||||
updates = UpdateCompressor.convertRawUpdatesToCompressedFormat(rawUpdates)
|
||||
if lastPreviousUpdate?
|
||||
updates.unshift(lastPreviousUpdate)
|
||||
return UpdateCompressor.compressUpdates(updates)
|
||||
|
||||
compressUpdates: (updates) ->
|
||||
return [] if updates.length == 0
|
||||
|
||||
compressedUpdates = [updates.shift()]
|
||||
for update in updates
|
||||
lastCompressedUpdate = compressedUpdates.pop()
|
||||
if lastCompressedUpdate?
|
||||
compressedUpdates = compressedUpdates.concat UpdateCompressor._concatTwoUpdates lastCompressedUpdate, update
|
||||
else
|
||||
compressedUpdates.push update
|
||||
|
||||
return compressedUpdates
|
||||
|
||||
MAX_TIME_BETWEEN_UPDATES: oneMinute = 60 * 1000
|
||||
|
||||
_concatTwoUpdates: (firstUpdate, secondUpdate) ->
|
||||
firstUpdate =
|
||||
op: firstUpdate.op
|
||||
meta:
|
||||
user_id: firstUpdate.meta.user_id or null
|
||||
start_ts: firstUpdate.meta.start_ts or firstUpdate.meta.ts
|
||||
end_ts: firstUpdate.meta.end_ts or firstUpdate.meta.ts
|
||||
secondUpdate =
|
||||
op: secondUpdate.op
|
||||
meta:
|
||||
user_id: secondUpdate.meta.user_id or null
|
||||
start_ts: secondUpdate.meta.start_ts or secondUpdate.meta.ts
|
||||
end_ts: secondUpdate.meta.end_ts or secondUpdate.meta.ts
|
||||
|
||||
if firstUpdate.meta.user_id != secondUpdate.meta.user_id
|
||||
return [firstUpdate, secondUpdate]
|
||||
|
||||
if secondUpdate.meta.start_ts - firstUpdate.meta.end_ts > UpdateCompressor.MAX_TIME_BETWEEN_UPDATES
|
||||
return [firstUpdate, secondUpdate]
|
||||
|
||||
firstOp = firstUpdate.op
|
||||
secondOp = secondUpdate.op
|
||||
# Two inserts
|
||||
if firstOp.i? and secondOp.i? and firstOp.p <= secondOp.p <= (firstOp.p + firstOp.i.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: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i)
|
||||
]
|
||||
# Two deletes
|
||||
else if firstOp.d? and secondOp.d? and secondOp.p <= firstOp.p <= (secondOp.p + 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: secondOp.p
|
||||
d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d)
|
||||
]
|
||||
# An insert and then a delete
|
||||
else if firstOp.i? and secondOp.d? and firstOp.p <= secondOp.p <= (firstOp.p + firstOp.i.length)
|
||||
offset = secondOp.p - firstOp.p
|
||||
insertedText = firstOp.i.slice(offset, offset + secondOp.d.length)
|
||||
if insertedText == secondOp.d
|
||||
insert = strRemove(firstOp.i, offset, secondOp.d.length)
|
||||
return [] if insert == ""
|
||||
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
|
||||
]
|
||||
else
|
||||
# This shouldn't be possible!
|
||||
return [firstUpdate, secondUpdate]
|
||||
|
||||
else
|
||||
return [firstUpdate, secondUpdate]
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../../app/js/ConversionManager.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "ConversionManager", ->
|
||||
beforeEach ->
|
||||
@ConversionManager = SandboxedModule.require modulePath, requires:
|
||||
"./HistoryBuilder": @HistoryBuilder = {}
|
||||
"./mongojs" : {}
|
||||
"logger-sharelatex": { log: sinon.stub() }
|
||||
@doc_id = "doc-id-123"
|
||||
@callback = sinon.stub()
|
||||
|
||||
describe "when there are no raw ops", ->
|
||||
beforeEach ->
|
||||
@ConversionManager.popLastCompressedOp = sinon.stub()
|
||||
@ConversionManager.insertCompressedOps = sinon.stub()
|
||||
@ConversionManager.convertAndSaveRawOps @doc_id, [], @callback
|
||||
|
||||
it "should not need to access the database", ->
|
||||
@ConversionManager.popLastCompressedOp.called.should.equal false
|
||||
@ConversionManager.insertCompressedOps.called.should.equal false
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
describe "when there is no compressed history to begin with", ->
|
||||
beforeEach ->
|
||||
@rawOps = ["mock-raw-op-1", "mock-raw-op-2"]
|
||||
@compressedOps = ["mock-compressed-op"]
|
||||
|
||||
@ConversionManager.popLastCompressedOp = sinon.stub().callsArgWith(1, null, null)
|
||||
@ConversionManager.insertCompressedOps = sinon.stub().callsArg(2)
|
||||
@HistoryBuilder.compressOps = sinon.stub().returns(@compressedOps)
|
||||
@ConversionManager.convertAndSaveRawOps @doc_id, @rawOps, @callback
|
||||
|
||||
it "should try to pop the last compressed op", ->
|
||||
@ConversionManager.popLastCompressedOp
|
||||
.calledWith(@doc_id)
|
||||
.should.equal true
|
||||
|
||||
it "should compress the raw ops", ->
|
||||
@HistoryBuilder.compressOps
|
||||
.calledWith(@rawOps)
|
||||
.should.equal true
|
||||
|
||||
it "should save the compressed ops", ->
|
||||
@ConversionManager.insertCompressedOps
|
||||
.calledWith(@doc_id, @compressedOps)
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
describe "when the raw ops need appending to existing history", ->
|
||||
beforeEach ->
|
||||
@rawOps = ["mock-raw-op-1", "mock-raw-op-2"]
|
||||
@lastCompressedOp = "mock-last-compressed-op-0"
|
||||
@compressedOps = ["mock-compressed-op-1"]
|
||||
|
||||
@ConversionManager.popLastCompressedOp = sinon.stub().callsArgWith(1, null, @lastCompressedOp)
|
||||
@ConversionManager.insertCompressedOps = sinon.stub().callsArg(2)
|
||||
@HistoryBuilder.compressOps = sinon.stub().returns(@compressedOps)
|
||||
@ConversionManager.convertAndSaveRawOps @doc_id, @rawOps, @callback
|
||||
|
||||
it "should try to pop the last compressed op", ->
|
||||
@ConversionManager.popLastCompressedOp
|
||||
.calledWith(@doc_id)
|
||||
.should.equal true
|
||||
|
||||
it "should compress the last compressed op and the raw ops", ->
|
||||
@HistoryBuilder.compressOps
|
||||
.calledWith([@lastCompressedOp].concat(@rawOps))
|
||||
.should.equal true
|
||||
|
||||
it "should save the compressed ops", ->
|
||||
@ConversionManager.insertCompressedOps
|
||||
.calledWith(@doc_id, @compressedOps)
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../../app/js/HistoryBuilder.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "HistoryBuilder", ->
|
||||
beforeEach ->
|
||||
@HistoryBuilder = SandboxedModule.require modulePath
|
||||
@user_id = "user-id-1"
|
||||
@other_user_id = "user-id-2"
|
||||
@ts1 = Date.now()
|
||||
@ts2 = Date.now() + 1000
|
||||
|
||||
describe "compress", ->
|
||||
describe "insert - insert", ->
|
||||
it "should append one insert to the other", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, i: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 6, i: "bar" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, i: "foobar" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should insert one insert inside the other", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, i: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 5, i: "bar" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, i: "fobaro" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should not append separated inserts", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, i: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 9, i: "bar" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, i: "foo" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 9, i: "bar" ]
|
||||
meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
describe "delete - delete", ->
|
||||
it "should append one delete to the other", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, d: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 3, d: "bar" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, d: "foobar" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should insert one delete inside the other", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, d: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 1, d: "bar" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 1, d: "bafoor" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should not append separated deletes", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, d: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 9, d: "bar" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, d: "foo" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 9, d: "bar" ]
|
||||
meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
describe "insert - delete", ->
|
||||
it "should undo a previous insert", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, i: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 5, d: "o" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, i: "fo" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should remove part of an insert from the middle", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, i: "fobaro" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 5, d: "bar" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, i: "foo" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should cancel out two opposite updates", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, i: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 3, d: "foo" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal []
|
||||
|
||||
it "should not combine separated updates", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, i: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 9, d: "bar" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, i: "foo" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 9, d: "bar" ]
|
||||
meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
describe "delete - insert", ->
|
||||
it "should redo a previous delete at the beginning", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, d: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 3, i: "f" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 4, d: "oo" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should redo a previous delete from halfway through", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, d: "foobar" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 3, i: "oo" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, d: "f" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 5, d: "bar" ]
|
||||
meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should keep words together", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, d: "abcdefghijklmnopqrstuvwxyz hello world" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 3, i: "w" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 4, i: "o" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 5, i: "r" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 6, i: "l" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 7, i: "d" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, d: "abcdefghijklmnopqrstuvwxyz hello " ]
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
|
||||
it "should not combine the ops if the insert text does not match the delete text", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, d: "foobar" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 3, i: "xy" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: [ p: 3, d: "foobar" ]
|
||||
meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 3, i: "xy" ]
|
||||
meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should cancel two equal updates", ->
|
||||
expect(@HistoryBuilder.compressUpdates [{
|
||||
op: [ p: 3, d: "foo" ]
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: [ p: 3, i: "foo" ]
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal []
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../../app/js/HistoryManager.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "HistoryManager", ->
|
||||
beforeEach ->
|
||||
@HistoryManager = SandboxedModule.require modulePath, requires:
|
||||
"./UpdateCompressor": @UpdateCompressor = {}
|
||||
"./mongojs" : {}
|
||||
"logger-sharelatex": { log: sinon.stub() }
|
||||
@doc_id = "doc-id-123"
|
||||
@callback = sinon.stub()
|
||||
|
||||
describe "when there are no raw ops", ->
|
||||
beforeEach ->
|
||||
@HistoryManager.popLastCompressedUpdate = sinon.stub()
|
||||
@HistoryManager.insertCompressedUpdates = sinon.stub()
|
||||
@HistoryManager.compressAndSaveRawUpdates @doc_id, [], @callback
|
||||
|
||||
it "should not need to access the database", ->
|
||||
@HistoryManager.popLastCompressedUpdate.called.should.equal false
|
||||
@HistoryManager.insertCompressedUpdates.called.should.equal false
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
describe "when there is no compressed history to begin with", ->
|
||||
beforeEach ->
|
||||
@rawUpdates = ["mock-raw-op-1", "mock-raw-op-2"]
|
||||
@compressedUpdates = ["mock-compressed-op"]
|
||||
|
||||
@HistoryManager.popLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null)
|
||||
@HistoryManager.insertCompressedUpdates = sinon.stub().callsArg(2)
|
||||
@UpdateCompressor.compressRawUpdates = sinon.stub().returns(@compressedUpdates)
|
||||
@HistoryManager.compressAndSaveRawUpdates @doc_id, @rawUpdates, @callback
|
||||
|
||||
it "should try to pop the last compressed op", ->
|
||||
@HistoryManager.popLastCompressedUpdate
|
||||
.calledWith(@doc_id)
|
||||
.should.equal true
|
||||
|
||||
it "should compress the raw ops", ->
|
||||
@UpdateCompressor.compressRawUpdates
|
||||
.calledWith(null, @rawUpdates)
|
||||
.should.equal true
|
||||
|
||||
it "should save the compressed ops", ->
|
||||
@HistoryManager.insertCompressedUpdates
|
||||
.calledWith(@doc_id, @compressedUpdates)
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
describe "when the raw ops need appending to existing history", ->
|
||||
beforeEach ->
|
||||
@rawUpdates = ["mock-raw-op-1", "mock-raw-op-2"]
|
||||
@lastCompressedUpdate = "mock-last-compressed-op-0"
|
||||
@compressedUpdates = ["mock-compressed-op-1"]
|
||||
|
||||
@HistoryManager.popLastCompressedUpdate = sinon.stub().callsArgWith(1, null, @lastCompressedUpdate)
|
||||
@HistoryManager.insertCompressedUpdates = sinon.stub().callsArg(2)
|
||||
@UpdateCompressor.compressRawUpdates = sinon.stub().returns(@compressedUpdates)
|
||||
@HistoryManager.compressAndSaveRawUpdates @doc_id, @rawUpdates, @callback
|
||||
|
||||
it "should try to pop the last compressed op", ->
|
||||
@HistoryManager.popLastCompressedUpdate
|
||||
.calledWith(@doc_id)
|
||||
.should.equal true
|
||||
|
||||
it "should compress the last compressed op and the raw ops", ->
|
||||
@UpdateCompressor.compressRawUpdates
|
||||
.calledWith(@lastCompressedUpdate, @rawUpdates)
|
||||
.should.equal true
|
||||
|
||||
it "should save the compressed ops", ->
|
||||
@HistoryManager.insertCompressedUpdates
|
||||
.calledWith(@doc_id, @compressedUpdates)
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../../app/js/UpdateCompressor.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "UpdateCompressor", ->
|
||||
beforeEach ->
|
||||
@UpdateCompressor = SandboxedModule.require modulePath
|
||||
@user_id = "user-id-1"
|
||||
@other_user_id = "user-id-2"
|
||||
@ts1 = Date.now()
|
||||
@ts2 = Date.now() + 1000
|
||||
|
||||
describe "convertRawUpdatesToCompressedFormat", ->
|
||||
it "should split grouped updates into individual updates", ->
|
||||
expect(@UpdateCompressor.convertRawUpdatesToCompressedFormat [{
|
||||
op: [ @op1 = { p: 0, i: "Foo" }, @op2 = { p: 6, i: "bar"} ]
|
||||
meta: { ts: @ts1, user_id: @user_id }
|
||||
}, {
|
||||
op: [ @op3 = { p: 10, i: "baz" } ]
|
||||
meta: { ts: @ts2, user_id: @other_user_id }
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: @op1,
|
||||
meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id }
|
||||
}, {
|
||||
op: @op2,
|
||||
meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id }
|
||||
}, {
|
||||
op: @op3,
|
||||
meta: { start_ts: @ts2, end_ts: @ts2, user_id: @other_user_id }
|
||||
}]
|
||||
|
||||
describe "compress", ->
|
||||
describe "insert - insert", ->
|
||||
it "should append one insert to the other", ->
|
||||
expect(@UpdateCompressor.compressUpdates [{
|
||||
op: { p: 3, i: "foo" }
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 6, i: "bar" }
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: { p: 3, i: "foobar" }
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should insert one insert inside the other", ->
|
||||
expect(@UpdateCompressor.compressUpdates [{
|
||||
op: { p: 3, i: "foo" }
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 5, i: "bar" }
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: { p: 3, i: "fobaro" }
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should not append separated inserts", ->
|
||||
expect(@UpdateCompressor.compressUpdates [{
|
||||
op: { p: 3, i: "foo" }
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 9, i: "bar" }
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: { p: 3, i: "foo" }
|
||||
meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 9, i: "bar" }
|
||||
meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
describe "delete - delete", ->
|
||||
it "should append one delete to the other", ->
|
||||
expect(@UpdateCompressor.compressUpdates [{
|
||||
op: { p: 3, d: "foo" }
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 3, d: "bar" }
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: { p: 3, d: "foobar" }
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should insert one delete inside the other", ->
|
||||
expect(@UpdateCompressor.compressUpdates [{
|
||||
op: { p: 3, d: "foo" }
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 1, d: "bar" }
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: { p: 1, d: "bafoor" }
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should not append separated deletes", ->
|
||||
expect(@UpdateCompressor.compressUpdates [{
|
||||
op: { p: 3, d: "foo" }
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 9, d: "bar" }
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: { p: 3, d: "foo" }
|
||||
meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 9, d: "bar" }
|
||||
meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
describe "insert - delete", ->
|
||||
it "should undo a previous insert", ->
|
||||
expect(@UpdateCompressor.compressUpdates [{
|
||||
op: { p: 3, i: "foo" }
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 5, d: "o" }
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: { p: 3, i: "fo" }
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should remove part of an insert from the middle", ->
|
||||
expect(@UpdateCompressor.compressUpdates [{
|
||||
op: { p: 3, i: "fobaro" }
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 5, d: "bar" }
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: { p: 3, i: "foo" }
|
||||
meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
it "should cancel out two opposite updates", ->
|
||||
expect(@UpdateCompressor.compressUpdates [{
|
||||
op: { p: 3, i: "foo" }
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 3, d: "foo" }
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal []
|
||||
|
||||
it "should not combine separated updates", ->
|
||||
expect(@UpdateCompressor.compressUpdates [{
|
||||
op: { p: 3, i: "foo" }
|
||||
meta: ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 9, d: "bar" }
|
||||
meta: ts: @ts2, user_id: @user_id
|
||||
}])
|
||||
.to.deep.equal [{
|
||||
op: { p: 3, i: "foo" }
|
||||
meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
|
||||
}, {
|
||||
op: { p: 9, d: "bar" }
|
||||
meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
|
||||
}]
|
||||
|
||||
|
Loading…
Reference in a new issue