Merge pull request #49 from sharelatex/ja-record-snapshots-of-ranges

Record a snapshot to mongo when a doc's comments/changes get collapsed
This commit is contained in:
James Allen 2019-04-16 14:34:51 +01:00 committed by GitHub
commit 2a1e843558
11 changed files with 286 additions and 22 deletions

View file

@ -1,13 +1,15 @@
RangesTracker = require "./RangesTracker" RangesTracker = require "./RangesTracker"
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
_ = require "lodash"
module.exports = RangesManager = module.exports = RangesManager =
MAX_COMMENTS: 500 MAX_COMMENTS: 500
MAX_CHANGES: 2000 MAX_CHANGES: 2000
applyUpdate: (project_id, doc_id, entries = {}, updates = [], newDocLines, callback = (error, new_entries) ->) -> applyUpdate: (project_id, doc_id, entries = {}, updates = [], newDocLines, callback = (error, new_entries, ranges_were_collapsed) ->) ->
{changes, comments} = entries {changes, comments} = _.cloneDeep(entries)
rangesTracker = new RangesTracker(changes, comments) rangesTracker = new RangesTracker(changes, comments)
emptyRangeCountBefore = RangesManager._emptyRangesCount(rangesTracker)
for update in updates for update in updates
rangesTracker.track_changes = !!update.meta.tc rangesTracker.track_changes = !!update.meta.tc
if !!update.meta.tc if !!update.meta.tc
@ -29,9 +31,11 @@ module.exports = RangesManager =
logger.error {err: error, project_id, doc_id, newDocLines, updates}, "error validating ranges" logger.error {err: error, project_id, doc_id, newDocLines, updates}, "error validating ranges"
return callback(error) return callback(error)
emptyRangeCountAfter = RangesManager._emptyRangesCount(rangesTracker)
rangesWereCollapsed = emptyRangeCountAfter > emptyRangeCountBefore
response = RangesManager._getRanges rangesTracker response = RangesManager._getRanges rangesTracker
logger.log {project_id, doc_id, changesCount: response.changes?.length, commentsCount: response.comments?.length}, "applied updates to ranges" logger.log {project_id, doc_id, changesCount: response.changes?.length, commentsCount: response.comments?.length, rangesWereCollapsed}, "applied updates to ranges"
callback null, response callback null, response, rangesWereCollapsed
acceptChanges: (change_ids, ranges, callback = (error, ranges) ->) -> acceptChanges: (change_ids, ranges, callback = (error, ranges) ->) ->
{changes, comments} = ranges {changes, comments} = ranges
@ -60,3 +64,13 @@ module.exports = RangesManager =
response ?= {} response ?= {}
response.comments = rangesTracker.comments response.comments = rangesTracker.comments
return response return response
_emptyRangesCount: (ranges) ->
count = 0
for comment in (ranges.comments or [])
if comment.op.c == ""
count++
for change in (ranges.changes or []) when change.op.i?
if change.op.i == ""
count++
return count

View file

@ -1,6 +1,5 @@
Settings = require('settings-sharelatex') Settings = require('settings-sharelatex')
rclient = require("redis-sharelatex").createClient(Settings.redis.documentupdater) rclient = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
_ = require('underscore')
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
metrics = require('./Metrics') metrics = require('./Metrics')
Errors = require "./Errors" Errors = require "./Errors"

View file

@ -0,0 +1,42 @@
{db, ObjectId} = require "./mongojs"
module.exports = SnapshotManager =
recordSnapshot: (project_id, doc_id, version, pathname, lines, ranges, callback) ->
try
project_id = ObjectId(project_id)
doc_id = ObjectId(doc_id)
catch error
return callback(error)
db.docSnapshots.insert {
project_id, doc_id, version, lines, pathname,
ranges: SnapshotManager.jsonRangesToMongo(ranges),
ts: new Date()
}, callback
# Suggested indexes:
# db.docSnapshots.createIndex({project_id:1, doc_id:1})
# db.docSnapshots.createIndex({ts:1},{expiresAfterSeconds: 30*24*3600)) # expires after 30 days
jsonRangesToMongo: (ranges) ->
return null if !ranges?
updateMetadata = (metadata) ->
if metadata?.ts?
metadata.ts = new Date(metadata.ts)
if metadata?.user_id?
metadata.user_id = SnapshotManager._safeObjectId(metadata.user_id)
for change in ranges.changes or []
change.id = SnapshotManager._safeObjectId(change.id)
updateMetadata(change.metadata)
for comment in ranges.comments or []
comment.id = SnapshotManager._safeObjectId(comment.id)
if comment.op?.t?
comment.op.t = SnapshotManager._safeObjectId(comment.op.t)
updateMetadata(comment.metadata)
return ranges
_safeObjectId: (data) ->
try
return ObjectId(data)
catch error
return data

View file

@ -4,13 +4,14 @@ RealTimeRedisManager = require "./RealTimeRedisManager"
ShareJsUpdateManager = require "./ShareJsUpdateManager" ShareJsUpdateManager = require "./ShareJsUpdateManager"
HistoryManager = require "./HistoryManager" HistoryManager = require "./HistoryManager"
Settings = require('settings-sharelatex') Settings = require('settings-sharelatex')
_ = require("underscore") _ = require("lodash")
async = require("async") async = require("async")
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
Metrics = require "./Metrics" Metrics = require "./Metrics"
Errors = require "./Errors" Errors = require "./Errors"
DocumentManager = require "./DocumentManager" DocumentManager = require "./DocumentManager"
RangesManager = require "./RangesManager" RangesManager = require "./RangesManager"
SnapshotManager = require "./SnapshotManager"
Profiler = require "./Profiler" Profiler = require "./Profiler"
module.exports = UpdateManager = module.exports = UpdateManager =
@ -76,10 +77,11 @@ module.exports = UpdateManager =
return callback(error) if error? return callback(error) if error?
if !lines? or !version? if !lines? or !version?
return callback(new Errors.NotFoundError("document not found: #{doc_id}")) return callback(new Errors.NotFoundError("document not found: #{doc_id}"))
previousVersion = version
ShareJsUpdateManager.applyUpdate project_id, doc_id, update, lines, version, (error, updatedDocLines, version, appliedOps) -> ShareJsUpdateManager.applyUpdate project_id, doc_id, update, lines, version, (error, updatedDocLines, version, appliedOps) ->
profile.log("sharejs.applyUpdate") profile.log("sharejs.applyUpdate")
return callback(error) if error? return callback(error) if error?
RangesManager.applyUpdate project_id, doc_id, ranges, appliedOps, updatedDocLines, (error, new_ranges) -> RangesManager.applyUpdate project_id, doc_id, ranges, appliedOps, updatedDocLines, (error, new_ranges, ranges_were_collapsed) ->
UpdateManager._addProjectHistoryMetadataToOps(appliedOps, pathname, projectHistoryId, lines) UpdateManager._addProjectHistoryMetadataToOps(appliedOps, pathname, projectHistoryId, lines)
profile.log("RangesManager.applyUpdate") profile.log("RangesManager.applyUpdate")
return callback(error) if error? return callback(error) if error?
@ -88,7 +90,19 @@ module.exports = UpdateManager =
return callback(error) if error? return callback(error) if error?
HistoryManager.recordAndFlushHistoryOps project_id, doc_id, appliedOps, doc_ops_length, project_ops_length, (error) -> HistoryManager.recordAndFlushHistoryOps project_id, doc_id, appliedOps, doc_ops_length, project_ops_length, (error) ->
profile.log("recordAndFlushHistoryOps") profile.log("recordAndFlushHistoryOps")
callback(error) return callback(error) if error?
if ranges_were_collapsed
logger.log {project_id, doc_id, previousVersion, lines, ranges, update}, "update collapsed some ranges, snapshotting previous content"
# Do this last, since it's a mongo call, and so potentially longest running
# If it overruns the lock, it's ok, since all of our redis work is done
SnapshotManager.recordSnapshot project_id, doc_id, previousVersion, pathname, lines, ranges, (error) ->
if error?
logger.error {err: error, project_id, doc_id, version, lines, ranges}, "error recording snapshot"
return callback(error)
else
callback()
else
callback()
lockUpdatesAndDo: (method, project_id, doc_id, args..., callback) -> lockUpdatesAndDo: (method, project_id, doc_id, args..., callback) ->
profile = new Profiler("lockUpdatesAndDo", {project_id, doc_id}) profile = new Profiler("lockUpdatesAndDo", {project_id, doc_id})

View file

@ -0,0 +1,7 @@
Settings = require "settings-sharelatex"
mongojs = require "mongojs"
db = mongojs(Settings.mongo.url, ["docSnapshots"])
module.exports =
db: db
ObjectId: mongojs.ObjectId

View file

@ -291,6 +291,11 @@
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
"dev": true "dev": true
}, },
"bson": {
"version": "1.0.9",
"from": "bson@~1.0.4",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.0.9.tgz"
},
"buffer-crc32": { "buffer-crc32": {
"version": "0.2.1", "version": "0.2.1",
"from": "buffer-crc32@0.2.1", "from": "buffer-crc32@0.2.1",
@ -301,6 +306,11 @@
"from": "buffer-equal-constant-time@1.0.1", "from": "buffer-equal-constant-time@1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz"
}, },
"buffer-shims": {
"version": "1.0.0",
"from": "buffer-shims@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
},
"builtin-modules": { "builtin-modules": {
"version": "3.0.0", "version": "3.0.0",
"from": "builtin-modules@>=3.0.0 <4.0.0", "from": "builtin-modules@>=3.0.0 <4.0.0",
@ -364,7 +374,7 @@
}, },
"coffee-script": { "coffee-script": {
"version": "1.7.1", "version": "1.7.1",
"from": "coffee-script@>=1.7.0 <1.8.0", "from": "coffee-script@1.7.1",
"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz" "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz"
}, },
"combined-stream": { "combined-stream": {
@ -491,6 +501,11 @@
"from": "duplexify@>=3.6.0 <4.0.0", "from": "duplexify@>=3.6.0 <4.0.0",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz" "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz"
}, },
"each-series": {
"version": "1.0.0",
"from": "each-series@^1.0.0",
"resolved": "https://registry.npmjs.org/each-series/-/each-series-1.0.0.tgz"
},
"ecc-jsbn": { "ecc-jsbn": {
"version": "0.1.1", "version": "0.1.1",
"from": "ecc-jsbn@>=0.1.1 <0.2.0", "from": "ecc-jsbn@>=0.1.1 <0.2.0",
@ -816,7 +831,7 @@
}, },
"lodash": { "lodash": {
"version": "4.17.4", "version": "4.17.4",
"from": "lodash@>=4.14.0 <5.0.0", "from": "lodash@4.17.4",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz" "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz"
}, },
"lodash.defaults": { "lodash.defaults": {
@ -1093,6 +1108,43 @@
"from": "module-details-from-path@>=1.0.3 <2.0.0", "from": "module-details-from-path@>=1.0.3 <2.0.0",
"resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz" "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz"
}, },
"mongodb": {
"version": "2.2.36",
"from": "mongodb@^2.2.31",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.36.tgz",
"dependencies": {
"es6-promise": {
"version": "3.2.1",
"from": "es6-promise@3.2.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz"
},
"process-nextick-args": {
"version": "1.0.7",
"from": "process-nextick-args@>=1.0.6 <1.1.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
},
"readable-stream": {
"version": "2.2.7",
"from": "readable-stream@2.2.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz"
},
"string_decoder": {
"version": "1.0.3",
"from": "string_decoder@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz"
}
}
},
"mongodb-core": {
"version": "2.1.20",
"from": "mongodb-core@2.1.20",
"resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.20.tgz"
},
"mongojs": {
"version": "2.6.0",
"from": "mongojs@2.6.0",
"resolved": "https://registry.npmjs.org/mongojs/-/mongojs-2.6.0.tgz"
},
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"from": "ms@2.0.0", "from": "ms@2.0.0",
@ -1158,6 +1210,11 @@
"from": "parse-duration@>=0.1.1 <0.2.0", "from": "parse-duration@>=0.1.1 <0.2.0",
"resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz" "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz"
}, },
"parse-mongo-url": {
"version": "1.1.1",
"from": "parse-mongo-url@^1.1.1",
"resolved": "https://registry.npmjs.org/parse-mongo-url/-/parse-mongo-url-1.1.1.tgz"
},
"parse-ms": { "parse-ms": {
"version": "2.0.0", "version": "2.0.0",
"from": "parse-ms@>=2.0.0 <3.0.0", "from": "parse-ms@>=2.0.0 <3.0.0",
@ -1405,6 +1462,11 @@
} }
} }
}, },
"require_optional": {
"version": "1.0.1",
"from": "require_optional@~1.0.0",
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz"
},
"require-in-the-middle": { "require-in-the-middle": {
"version": "3.1.0", "version": "3.1.0",
"from": "require-in-the-middle@>=3.0.0 <4.0.0", "from": "require-in-the-middle@>=3.0.0 <4.0.0",
@ -1420,6 +1482,11 @@
"from": "resolve@>=1.5.0 <2.0.0", "from": "resolve@>=1.5.0 <2.0.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz" "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz"
}, },
"resolve-from": {
"version": "2.0.0",
"from": "resolve-from@^2.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz"
},
"retry-axios": { "retry-axios": {
"version": "0.3.2", "version": "0.3.2",
"from": "retry-axios@>=0.3.2 <0.4.0", "from": "retry-axios@>=0.3.2 <0.4.0",
@ -1585,12 +1652,22 @@
"from": "through2@>=2.0.3 <3.0.0", "from": "through2@>=2.0.3 <3.0.0",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz" "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz"
}, },
"thunky": {
"version": "1.0.3",
"from": "thunky@^1.0.2",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz"
},
"timekeeper": { "timekeeper": {
"version": "2.0.0", "version": "2.0.0",
"from": "timekeeper@>=2.0.0 <3.0.0", "from": "timekeeper@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.0.0.tgz", "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.0.0.tgz",
"dev": true "dev": true
}, },
"to-mongodb-core": {
"version": "2.0.0",
"from": "to-mongodb-core@^2.0.0",
"resolved": "https://registry.npmjs.org/to-mongodb-core/-/to-mongodb-core-2.0.0.tgz"
},
"tough-cookie": { "tough-cookie": {
"version": "2.3.3", "version": "2.3.3",
"from": "tough-cookie@>=2.3.3 <2.4.0", "from": "tough-cookie@>=2.3.3 <2.4.0",
@ -1612,11 +1689,6 @@
"from": "uid2@0.0.2", "from": "uid2@0.0.2",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.2.tgz" "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.2.tgz"
}, },
"underscore": {
"version": "1.2.2",
"from": "underscore@1.2.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.2.2.tgz"
},
"uri-js": { "uri-js": {
"version": "4.2.2", "version": "4.2.2",
"from": "uri-js@>=4.2.2 <5.0.0", "from": "uri-js@>=4.2.2 <5.0.0",

View file

@ -23,16 +23,17 @@
"async": "^2.5.0", "async": "^2.5.0",
"coffee-script": "~1.7.0", "coffee-script": "~1.7.0",
"express": "3.3.4", "express": "3.3.4",
"lodash": "^4.17.4",
"logger-sharelatex": "^1.6.0", "logger-sharelatex": "^1.6.0",
"lynx": "0.0.11", "lynx": "0.0.11",
"metrics-sharelatex": "^2.1.1", "metrics-sharelatex": "^2.1.1",
"mongojs": "^2.6.0",
"redis-sharelatex": "^1.0.5", "redis-sharelatex": "^1.0.5",
"request": "2.25.0", "request": "2.25.0",
"requestretry": "^1.12.0", "requestretry": "^1.12.0",
"sandboxed-module": "~0.2.0", "sandboxed-module": "~0.2.0",
"settings-sharelatex": "^1.1.0", "settings-sharelatex": "^1.1.0",
"sinon": "~1.5.2", "sinon": "~1.5.2"
"underscore": "1.2.2"
}, },
"devDependencies": { "devDependencies": {
"bunyan": "~0.22.1", "bunyan": "~0.22.1",

View file

@ -4,11 +4,15 @@ chai.should()
expect = chai.expect expect = chai.expect
async = require "async" async = require "async"
{db, ObjectId} = require "../../../app/js/mongojs"
MockWebApi = require "./helpers/MockWebApi" MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient" DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp" DocUpdaterApp = require "./helpers/DocUpdaterApp"
describe "Ranges", -> describe "Ranges", ->
before (done) ->
DocUpdaterApp.ensureRunning done
describe "tracking changes from ops", -> describe "tracking changes from ops", ->
before (done) -> before (done) ->
@project_id = DocUpdaterClient.randomId() @project_id = DocUpdaterClient.randomId()
@ -306,3 +310,63 @@ describe "Ranges", ->
ranges = data.ranges ranges = data.ranges
expect(ranges.changes).to.be.undefined expect(ranges.changes).to.be.undefined
done() done()
describe "deleting text surrounding a comment", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@doc_id = DocUpdaterClient.randomId()
MockWebApi.insertDoc @project_id, @doc_id, {
lines: ["foo bar baz"]
version: 0
ranges: {
comments: [{
op: { c: "a", p: 5, tid: @tid = DocUpdaterClient.randomId() }
metadata:
user_id: @user_id
ts: new Date()
}]
}
}
@updates = [{
doc: @doc_id
op: [{ d: "foo ", p: 0 }]
v: 0
meta: { user_id: @user_id }
}, {
doc: @doc_id
op: [{ d: "bar ", p: 0 }]
v: 1
meta: { user_id: @user_id }
}]
jobs = []
for update in @updates
do (update) =>
jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
async.series jobs, (error) ->
throw error if error?
setTimeout () =>
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
throw error if error?
done()
, 200
it "should write a snapshot from before the destructive change", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
return done(error) if error?
db.docSnapshots.find {
project_id: ObjectId(@project_id),
doc_id: ObjectId(@doc_id)
}, (error, docSnapshots) =>
return done(error) if error?
expect(docSnapshots.length).to.equal 1
expect(docSnapshots[0].version).to.equal 1
expect(docSnapshots[0].lines).to.deep.equal ["bar baz"]
expect(docSnapshots[0].ranges.comments[0].op).to.deep.equal {
c: "a",
p: 1,
tid: @tid
}
done()

View file

@ -3,7 +3,7 @@ chai = require('chai')
should = chai.should() should = chai.should()
modulePath = "../../../../app/js/ProjectManager.js" modulePath = "../../../../app/js/ProjectManager.js"
SandboxedModule = require('sandboxed-module') SandboxedModule = require('sandboxed-module')
_ = require('underscore') _ = require('lodash')
describe "ProjectManager", -> describe "ProjectManager", ->
beforeEach -> beforeEach ->

View file

@ -50,8 +50,9 @@ describe "RangesManager", ->
it "should return the modified the comments and changes", -> it "should return the modified the comments and changes", ->
@callback.called.should.equal true @callback.called.should.equal true
[error, entries] = @callback.args[0] [error, entries, ranges_were_collapsed] = @callback.args[0]
expect(error).to.be.null expect(error).to.be.null
expect(ranges_were_collapsed).to.equal false
entries.comments[0].op.should.deep.equal { entries.comments[0].op.should.deep.equal {
c: "three " c: "three "
p: 8 p: 8
@ -180,6 +181,36 @@ describe "RangesManager", ->
expect(error).to.not.be.null expect(error).to.not.be.null
expect(error.message).to.equal("Change ({\"op\":{\"i\":\"five\",\"p\":15},\"metadata\":{\"user_id\":\"user-id-123\"}}) doesn't match text (\"our \")") expect(error.message).to.equal("Change ({\"op\":{\"i\":\"five\",\"p\":15},\"metadata\":{\"user_id\":\"user-id-123\"}}) doesn't match text (\"our \")")
describe "with an update that collapses a range", ->
beforeEach ->
@updates = [{
meta:
user_id: @user_id
op: [{
d: "one"
p: 0
t: "thread-id-1"
}]
}]
@entries = {
comments: [{
op:
c: "n"
p: 1
t: "thread-id-2"
metadata:
user_id: @user_id
}]
changes: []
}
@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
it "should return ranges_were_collapsed == true", ->
@callback.called.should.equal true
[error, entries, ranges_were_collapsed] = @callback.args[0]
expect(ranges_were_collapsed).to.equal true
describe "acceptChanges", -> describe "acceptChanges", ->
beforeEach -> beforeEach ->
@RangesManager = SandboxedModule.require modulePath, @RangesManager = SandboxedModule.require modulePath,

View file

@ -23,6 +23,7 @@ describe "UpdateManager", ->
"settings-sharelatex": @Settings = {} "settings-sharelatex": @Settings = {}
"./DocumentManager": @DocumentManager = {} "./DocumentManager": @DocumentManager = {}
"./RangesManager": @RangesManager = {} "./RangesManager": @RangesManager = {}
"./SnapshotManager": @SnapshotManager = {}
"./Profiler": class Profiler "./Profiler": class Profiler
log: sinon.stub().returns { end: sinon.stub() } log: sinon.stub().returns { end: sinon.stub() }
end: sinon.stub() end: sinon.stub()
@ -169,7 +170,7 @@ describe "UpdateManager", ->
@project_ops_length = sinon.stub() @project_ops_length = sinon.stub()
@pathname = '/a/b/c.tex' @pathname = '/a/b/c.tex'
@DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges, @pathname, @projectHistoryId) @DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges, @pathname, @projectHistoryId)
@RangesManager.applyUpdate = sinon.stub().yields(null, @updated_ranges) @RangesManager.applyUpdate = sinon.stub().yields(null, @updated_ranges, false)
@ShareJsUpdateManager.applyUpdate = sinon.stub().yields(null, @updatedDocLines, @version, @appliedOps) @ShareJsUpdateManager.applyUpdate = sinon.stub().yields(null, @updatedDocLines, @version, @appliedOps)
@RedisManager.updateDocument = sinon.stub().yields(null, @doc_ops_length, @project_ops_length) @RedisManager.updateDocument = sinon.stub().yields(null, @doc_ops_length, @project_ops_length)
@RealTimeRedisManager.sendData = sinon.stub() @RealTimeRedisManager.sendData = sinon.stub()
@ -239,6 +240,25 @@ describe "UpdateManager", ->
it "should call the callback with the error", -> it "should call the callback with the error", ->
@callback.calledWith(@error).should.equal true @callback.calledWith(@error).should.equal true
describe "when ranges get collapsed", ->
beforeEach ->
@RangesManager.applyUpdate = sinon.stub().yields(null, @updated_ranges, true)
@SnapshotManager.recordSnapshot = sinon.stub().yields()
@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
it "should call SnapshotManager.recordSnapshot", ->
@SnapshotManager.recordSnapshot
.calledWith(
@project_id,
@doc_id,
@version,
@pathname,
@lines,
@ranges
)
.should.equal true
describe "_addProjectHistoryMetadataToOps", -> describe "_addProjectHistoryMetadataToOps", ->
it "should add projectHistoryId, pathname and doc_length metadata to the ops", -> it "should add projectHistoryId, pathname and doc_length metadata to the ops", ->
lines = [ lines = [