|
@@ -1,394 +1,499 @@
|
|
|
-sinon = require "sinon"
|
|
|
|
|
-chai = require("chai")
|
|
|
|
|
-chai.should()
|
|
|
|
|
-expect = chai.expect
|
|
|
|
|
-async = require "async"
|
|
|
|
|
-Settings = require('settings-sharelatex')
|
|
|
|
|
-rclient_history = require("redis-sharelatex").createClient(Settings.redis.history) # note: this is track changes, not project-history
|
|
|
|
|
-rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history)
|
|
|
|
|
-rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
|
|
|
|
|
-Keys = Settings.redis.documentupdater.key_schema
|
|
|
|
|
-HistoryKeys = Settings.redis.history.key_schema
|
|
|
|
|
-ProjectHistoryKeys = Settings.redis.project_history.key_schema
|
|
|
|
|
-
|
|
|
|
|
-MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
|
|
|
|
|
-MockWebApi = require "./helpers/MockWebApi"
|
|
|
|
|
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
|
|
|
|
|
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
|
|
|
|
|
-
|
|
|
|
|
-describe "Applying updates to a doc", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- @lines = ["one", "two", "three"]
|
|
|
|
|
- @version = 42
|
|
|
|
|
- @update =
|
|
|
|
|
- doc: @doc_id
|
|
|
|
|
|
|
+/*
|
|
|
|
|
+ * decaffeinate suggestions:
|
|
|
|
|
+ * DS101: Remove unnecessary use of Array.from
|
|
|
|
|
+ * DS102: Remove unnecessary code created because of implicit returns
|
|
|
|
|
+ * DS207: Consider shorter variations of null checks
|
|
|
|
|
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
|
|
|
+ */
|
|
|
|
|
+const sinon = require("sinon");
|
|
|
|
|
+const chai = require("chai");
|
|
|
|
|
+chai.should();
|
|
|
|
|
+const {
|
|
|
|
|
+ expect
|
|
|
|
|
+} = chai;
|
|
|
|
|
+const async = require("async");
|
|
|
|
|
+const Settings = require('settings-sharelatex');
|
|
|
|
|
+const rclient_history = require("redis-sharelatex").createClient(Settings.redis.history); // note: this is track changes, not project-history
|
|
|
|
|
+const rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history);
|
|
|
|
|
+const rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater);
|
|
|
|
|
+const Keys = Settings.redis.documentupdater.key_schema;
|
|
|
|
|
+const HistoryKeys = Settings.redis.history.key_schema;
|
|
|
|
|
+const ProjectHistoryKeys = Settings.redis.project_history.key_schema;
|
|
|
|
|
+
|
|
|
|
|
+const MockTrackChangesApi = require("./helpers/MockTrackChangesApi");
|
|
|
|
|
+const MockWebApi = require("./helpers/MockWebApi");
|
|
|
|
|
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
|
|
|
|
|
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
|
|
|
|
|
+
|
|
|
|
|
+describe("Applying updates to a doc", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ this.lines = ["one", "two", "three"];
|
|
|
|
|
+ this.version = 42;
|
|
|
|
|
+ this.update = {
|
|
|
|
|
+ doc: this.doc_id,
|
|
|
op: [{
|
|
op: [{
|
|
|
- i: "one and a half\n"
|
|
|
|
|
|
|
+ i: "one and a half\n",
|
|
|
p: 4
|
|
p: 4
|
|
|
- }]
|
|
|
|
|
- v: @version
|
|
|
|
|
- @result = ["one", "one and a half", "two", "three"]
|
|
|
|
|
- DocUpdaterApp.ensureRunning(done)
|
|
|
|
|
-
|
|
|
|
|
- describe "when the document is not loaded", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
|
|
|
|
- sinon.spy MockWebApi, "getDocument"
|
|
|
|
|
- @startTime = Date.now()
|
|
|
|
|
- MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
|
|
|
|
|
- DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- setTimeout done, 200
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- after ->
|
|
|
|
|
- MockWebApi.getDocument.restore()
|
|
|
|
|
-
|
|
|
|
|
- it "should load the document from the web API", ->
|
|
|
|
|
- MockWebApi.getDocument
|
|
|
|
|
- .calledWith(@project_id, @doc_id)
|
|
|
|
|
- .should.equal true
|
|
|
|
|
-
|
|
|
|
|
- it "should update the doc", (done) ->
|
|
|
|
|
- DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
|
|
|
|
- doc.lines.should.deep.equal @result
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should push the applied updates to the track changes api", (done) ->
|
|
|
|
|
- rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- JSON.parse(updates[0]).op.should.deep.equal @update.op
|
|
|
|
|
- rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- result.should.equal 1
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should push the applied updates to the project history changes api", (done) ->
|
|
|
|
|
- rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- JSON.parse(updates[0]).op.should.deep.equal @update.op
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should set the first op timestamp", (done) ->
|
|
|
|
|
- rclient_project_history.get ProjectHistoryKeys.projectHistoryFirstOpTimestamp({@project_id}), (error, result) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- result.should.be.within(@startTime, Date.now())
|
|
|
|
|
- @firstOpTimestamp = result
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- describe "when sending another update", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- @timeout = 10000
|
|
|
|
|
- @second_update = Object.create(@update)
|
|
|
|
|
- @second_update.v = @version + 1
|
|
|
|
|
- DocUpdaterClient.sendUpdate @project_id, @doc_id, @second_update, (error) ->
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- setTimeout done, 200
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should not change the first op timestamp", (done) ->
|
|
|
|
|
- rclient_project_history.get ProjectHistoryKeys.projectHistoryFirstOpTimestamp({@project_id}), (error, result) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- result.should.equal @firstOpTimestamp
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- describe "when the document is loaded", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
|
|
|
|
-
|
|
|
|
|
- MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
|
|
|
|
|
- DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- sinon.spy MockWebApi, "getDocument"
|
|
|
|
|
- DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- setTimeout done, 200
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- after ->
|
|
|
|
|
- MockWebApi.getDocument.restore()
|
|
|
|
|
-
|
|
|
|
|
- it "should not need to call the web api", ->
|
|
|
|
|
- MockWebApi.getDocument.called.should.equal false
|
|
|
|
|
-
|
|
|
|
|
- it "should update the doc", (done) ->
|
|
|
|
|
- DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
|
|
|
|
- doc.lines.should.deep.equal @result
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should push the applied updates to the track changes api", (done) ->
|
|
|
|
|
- rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
|
|
|
|
|
- JSON.parse(updates[0]).op.should.deep.equal @update.op
|
|
|
|
|
- rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
|
|
|
|
|
- result.should.equal 1
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should push the applied updates to the project history changes api", (done) ->
|
|
|
|
|
- rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
|
|
|
|
|
- JSON.parse(updates[0]).op.should.deep.equal @update.op
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- describe "when the document is loaded and is using project-history only", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
|
|
|
|
-
|
|
|
|
|
- MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version, projectHistoryType: 'project-history'}
|
|
|
|
|
- DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- sinon.spy MockWebApi, "getDocument"
|
|
|
|
|
- DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- setTimeout done, 200
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- after ->
|
|
|
|
|
- MockWebApi.getDocument.restore()
|
|
|
|
|
-
|
|
|
|
|
- it "should update the doc", (done) ->
|
|
|
|
|
- DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
|
|
|
|
- doc.lines.should.deep.equal @result
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should not push any applied updates to the track changes api", (done) ->
|
|
|
|
|
- rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
|
|
|
|
|
- updates.length.should.equal 0
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should push the applied updates to the project history changes api", (done) ->
|
|
|
|
|
- rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
|
|
|
|
|
- JSON.parse(updates[0]).op.should.deep.equal @update.op
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- describe "when the document has been deleted", ->
|
|
|
|
|
- describe "when the ops come in a single linear order", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
|
|
|
|
- lines = ["", "", ""]
|
|
|
|
|
- MockWebApi.insertDoc @project_id, @doc_id, {lines: lines, version: 0}
|
|
|
|
|
- @updates = [
|
|
|
|
|
- { doc_id: @doc_id, v: 0, op: [i: "h", p: 0 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 1, op: [i: "e", p: 1 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 2, op: [i: "l", p: 2 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 3, op: [i: "l", p: 3 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 4, op: [i: "o", p: 4 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 5, op: [i: " ", p: 5 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 6, op: [i: "w", p: 6 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 7, op: [i: "o", p: 7 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 8, op: [i: "r", p: 8 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 9, op: [i: "l", p: 9 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 10, op: [i: "d", p: 10] }
|
|
|
|
|
- ]
|
|
|
|
|
- @my_result = ["hello world", "", ""]
|
|
|
|
|
- done()
|
|
|
|
|
-
|
|
|
|
|
- it "should be able to continue applying updates when the project has been deleted", (done) ->
|
|
|
|
|
- actions = []
|
|
|
|
|
- for update in @updates.slice(0,6)
|
|
|
|
|
- do (update) =>
|
|
|
|
|
- actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
|
|
|
|
|
- actions.push (callback) => DocUpdaterClient.deleteDoc @project_id, @doc_id, callback
|
|
|
|
|
- for update in @updates.slice(6)
|
|
|
|
|
- do (update) =>
|
|
|
|
|
- actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
|
|
|
|
|
-
|
|
|
|
|
- async.series actions, (error) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
|
|
|
|
- doc.lines.should.deep.equal @my_result
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should push the applied updates to the track changes api", (done) ->
|
|
|
|
|
- rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
|
|
|
|
|
- updates = (JSON.parse(u) for u in updates)
|
|
|
|
|
- for appliedUpdate, i in @updates
|
|
|
|
|
- appliedUpdate.op.should.deep.equal updates[i].op
|
|
|
|
|
-
|
|
|
|
|
- rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
|
|
|
|
|
- result.should.equal 1
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should store the doc ops in the correct order", (done) ->
|
|
|
|
|
- rclient_du.lrange Keys.docOps({doc_id: @doc_id}), 0, -1, (error, updates) =>
|
|
|
|
|
- updates = (JSON.parse(u) for u in updates)
|
|
|
|
|
- for appliedUpdate, i in @updates
|
|
|
|
|
- appliedUpdate.op.should.deep.equal updates[i].op
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- describe "when older ops come in after the delete", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
|
|
|
|
- lines = ["", "", ""]
|
|
|
|
|
- MockWebApi.insertDoc @project_id, @doc_id, {lines: lines, version: 0}
|
|
|
|
|
- @updates = [
|
|
|
|
|
- { doc_id: @doc_id, v: 0, op: [i: "h", p: 0 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 1, op: [i: "e", p: 1 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 2, op: [i: "l", p: 2 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 3, op: [i: "l", p: 3 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 4, op: [i: "o", p: 4 ] }
|
|
|
|
|
- { doc_id: @doc_id, v: 0, op: [i: "world", p: 1 ] }
|
|
|
|
|
- ]
|
|
|
|
|
- @my_result = ["hello", "world", ""]
|
|
|
|
|
- done()
|
|
|
|
|
-
|
|
|
|
|
- it "should be able to continue applying updates when the project has been deleted", (done) ->
|
|
|
|
|
- actions = []
|
|
|
|
|
- for update in @updates.slice(0,5)
|
|
|
|
|
- do (update) =>
|
|
|
|
|
- actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
|
|
|
|
|
- actions.push (callback) => DocUpdaterClient.deleteDoc @project_id, @doc_id, callback
|
|
|
|
|
- for update in @updates.slice(5)
|
|
|
|
|
- do (update) =>
|
|
|
|
|
- actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
|
|
|
|
|
-
|
|
|
|
|
- async.series actions, (error) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
|
|
|
|
- doc.lines.should.deep.equal @my_result
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- describe "with a broken update", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
|
|
|
|
- @broken_update = { doc_id: @doc_id, v: @version, op: [d: "not the correct content", p: 0 ] }
|
|
|
|
|
- MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
|
|
|
|
|
-
|
|
|
|
|
- DocUpdaterClient.subscribeToAppliedOps @messageCallback = sinon.stub()
|
|
|
|
|
-
|
|
|
|
|
- DocUpdaterClient.sendUpdate @project_id, @doc_id, @broken_update, (error) ->
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- setTimeout done, 200
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should not update the doc", (done) ->
|
|
|
|
|
- DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
|
|
|
|
- doc.lines.should.deep.equal @lines
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should send a message with an error", ->
|
|
|
|
|
- @messageCallback.called.should.equal true
|
|
|
|
|
- [channel, message] = @messageCallback.args[0]
|
|
|
|
|
- channel.should.equal "applied-ops"
|
|
|
|
|
- JSON.parse(message).should.deep.include {
|
|
|
|
|
- project_id: @project_id,
|
|
|
|
|
- doc_id: @doc_id,
|
|
|
|
|
|
|
+ }],
|
|
|
|
|
+ v: this.version
|
|
|
|
|
+ };
|
|
|
|
|
+ this.result = ["one", "one and a half", "two", "three"];
|
|
|
|
|
+ return DocUpdaterApp.ensureRunning(done);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe("when the document is not loaded", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ [this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
|
|
|
|
|
+ sinon.spy(MockWebApi, "getDocument");
|
|
|
|
|
+ this.startTime = Date.now();
|
|
|
|
|
+ MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
|
|
|
|
|
+ DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, function(error) {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return setTimeout(done, 200);
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ after(() => MockWebApi.getDocument.restore());
|
|
|
|
|
+
|
|
|
|
|
+ it("should load the document from the web API", function() {
|
|
|
|
|
+ return MockWebApi.getDocument
|
|
|
|
|
+ .calledWith(this.project_id, this.doc_id)
|
|
|
|
|
+ .should.equal(true);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it("should update the doc", function(done) {
|
|
|
|
|
+ DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
|
|
|
|
|
+ doc.lines.should.deep.equal(this.result);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it("should push the applied updates to the track changes api", function(done) {
|
|
|
|
|
+ rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
|
|
|
|
|
+ return rclient_history.sismember(HistoryKeys.docsWithHistoryOps({project_id: this.project_id}), this.doc_id, (error, result) => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ result.should.equal(1);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it("should push the applied updates to the project history changes api", function(done) {
|
|
|
|
|
+ rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it("should set the first op timestamp", function(done) {
|
|
|
|
|
+ rclient_project_history.get(ProjectHistoryKeys.projectHistoryFirstOpTimestamp({project_id: this.project_id}), (error, result) => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ result.should.be.within(this.startTime, Date.now());
|
|
|
|
|
+ this.firstOpTimestamp = result;
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return describe("when sending another update", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ this.timeout = 10000;
|
|
|
|
|
+ this.second_update = Object.create(this.update);
|
|
|
|
|
+ this.second_update.v = this.version + 1;
|
|
|
|
|
+ DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.second_update, function(error) {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return setTimeout(done, 200);
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return it("should not change the first op timestamp", function(done) {
|
|
|
|
|
+ rclient_project_history.get(ProjectHistoryKeys.projectHistoryFirstOpTimestamp({project_id: this.project_id}), (error, result) => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ result.should.equal(this.firstOpTimestamp);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe("when the document is loaded", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ [this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
|
|
|
|
|
+
|
|
|
|
|
+ MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
|
|
|
|
|
+ DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ sinon.spy(MockWebApi, "getDocument");
|
|
|
|
|
+ return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, function(error) {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return setTimeout(done, 200);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ after(() => MockWebApi.getDocument.restore());
|
|
|
|
|
+
|
|
|
|
|
+ it("should not need to call the web api", () => MockWebApi.getDocument.called.should.equal(false));
|
|
|
|
|
+
|
|
|
|
|
+ it("should update the doc", function(done) {
|
|
|
|
|
+ DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
|
|
|
|
|
+ doc.lines.should.deep.equal(this.result);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it("should push the applied updates to the track changes api", function(done) {
|
|
|
|
|
+ rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
|
|
|
|
|
+ JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
|
|
|
|
|
+ return rclient_history.sismember(HistoryKeys.docsWithHistoryOps({project_id: this.project_id}), this.doc_id, (error, result) => {
|
|
|
|
|
+ result.should.equal(1);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return it("should push the applied updates to the project history changes api", function(done) {
|
|
|
|
|
+ rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
|
|
|
|
|
+ JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe("when the document is loaded and is using project-history only", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ [this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
|
|
|
|
|
+
|
|
|
|
|
+ MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version, projectHistoryType: 'project-history'});
|
|
|
|
|
+ DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ sinon.spy(MockWebApi, "getDocument");
|
|
|
|
|
+ return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, function(error) {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return setTimeout(done, 200);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ after(() => MockWebApi.getDocument.restore());
|
|
|
|
|
+
|
|
|
|
|
+ it("should update the doc", function(done) {
|
|
|
|
|
+ DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
|
|
|
|
|
+ doc.lines.should.deep.equal(this.result);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it("should not push any applied updates to the track changes api", function(done) {
|
|
|
|
|
+ rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
|
|
|
|
|
+ updates.length.should.equal(0);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return it("should push the applied updates to the project history changes api", function(done) {
|
|
|
|
|
+ rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
|
|
|
|
|
+ JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe("when the document has been deleted", function() {
|
|
|
|
|
+ describe("when the ops come in a single linear order", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ [this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
|
|
|
|
|
+ const lines = ["", "", ""];
|
|
|
|
|
+ MockWebApi.insertDoc(this.project_id, this.doc_id, {lines, version: 0});
|
|
|
|
|
+ this.updates = [
|
|
|
|
|
+ { doc_id: this.doc_id, v: 0, op: [{i: "h", p: 0} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 1, op: [{i: "e", p: 1} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 2, op: [{i: "l", p: 2} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 3, op: [{i: "l", p: 3} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 4, op: [{i: "o", p: 4} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 5, op: [{i: " ", p: 5} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 6, op: [{i: "w", p: 6} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 7, op: [{i: "o", p: 7} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 8, op: [{i: "r", p: 8} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 9, op: [{i: "l", p: 9} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 10, op: [{i: "d", p: 10}] }
|
|
|
|
|
+ ];
|
|
|
|
|
+ this.my_result = ["hello world", "", ""];
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it("should be able to continue applying updates when the project has been deleted", function(done) {
|
|
|
|
|
+ let update;
|
|
|
|
|
+ const actions = [];
|
|
|
|
|
+ for (update of Array.from(this.updates.slice(0,6))) {
|
|
|
|
|
+ (update => {
|
|
|
|
|
+ return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
|
|
|
|
|
+ })(update);
|
|
|
|
|
+ }
|
|
|
|
|
+ actions.push(callback => DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback));
|
|
|
|
|
+ for (update of Array.from(this.updates.slice(6))) {
|
|
|
|
|
+ (update => {
|
|
|
|
|
+ return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
|
|
|
|
|
+ })(update);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async.series(actions, error => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
|
|
|
|
|
+ doc.lines.should.deep.equal(this.my_result);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it("should push the applied updates to the track changes api", function(done) {
|
|
|
|
|
+ rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
|
|
|
|
|
+ updates = (Array.from(updates).map((u) => JSON.parse(u)));
|
|
|
|
|
+ for (let i = 0; i < this.updates.length; i++) {
|
|
|
|
|
+ const appliedUpdate = this.updates[i];
|
|
|
|
|
+ appliedUpdate.op.should.deep.equal(updates[i].op);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return rclient_history.sismember(HistoryKeys.docsWithHistoryOps({project_id: this.project_id}), this.doc_id, (error, result) => {
|
|
|
|
|
+ result.should.equal(1);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return it("should store the doc ops in the correct order", function(done) {
|
|
|
|
|
+ rclient_du.lrange(Keys.docOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
|
|
|
|
|
+ updates = (Array.from(updates).map((u) => JSON.parse(u)));
|
|
|
|
|
+ for (let i = 0; i < this.updates.length; i++) {
|
|
|
|
|
+ const appliedUpdate = this.updates[i];
|
|
|
|
|
+ appliedUpdate.op.should.deep.equal(updates[i].op);
|
|
|
|
|
+ }
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return describe("when older ops come in after the delete", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ [this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
|
|
|
|
|
+ const lines = ["", "", ""];
|
|
|
|
|
+ MockWebApi.insertDoc(this.project_id, this.doc_id, {lines, version: 0});
|
|
|
|
|
+ this.updates = [
|
|
|
|
|
+ { doc_id: this.doc_id, v: 0, op: [{i: "h", p: 0} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 1, op: [{i: "e", p: 1} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 2, op: [{i: "l", p: 2} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 3, op: [{i: "l", p: 3} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 4, op: [{i: "o", p: 4} ] },
|
|
|
|
|
+ { doc_id: this.doc_id, v: 0, op: [{i: "world", p: 1} ] }
|
|
|
|
|
+ ];
|
|
|
|
|
+ this.my_result = ["hello", "world", ""];
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return it("should be able to continue applying updates when the project has been deleted", function(done) {
|
|
|
|
|
+ let update;
|
|
|
|
|
+ const actions = [];
|
|
|
|
|
+ for (update of Array.from(this.updates.slice(0,5))) {
|
|
|
|
|
+ (update => {
|
|
|
|
|
+ return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
|
|
|
|
|
+ })(update);
|
|
|
|
|
+ }
|
|
|
|
|
+ actions.push(callback => DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback));
|
|
|
|
|
+ for (update of Array.from(this.updates.slice(5))) {
|
|
|
|
|
+ (update => {
|
|
|
|
|
+ return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
|
|
|
|
|
+ })(update);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async.series(actions, error => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
|
|
|
|
|
+ doc.lines.should.deep.equal(this.my_result);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe("with a broken update", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ [this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
|
|
|
|
|
+ this.broken_update = { doc_id: this.doc_id, v: this.version, op: [{d: "not the correct content", p: 0} ] };
|
|
|
|
|
+ MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
|
|
|
|
|
+
|
|
|
|
|
+ DocUpdaterClient.subscribeToAppliedOps(this.messageCallback = sinon.stub());
|
|
|
|
|
+
|
|
|
|
|
+ DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.broken_update, function(error) {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return setTimeout(done, 200);
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it("should not update the doc", function(done) {
|
|
|
|
|
+ DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
|
|
|
|
|
+ doc.lines.should.deep.equal(this.lines);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return it("should send a message with an error", function() {
|
|
|
|
|
+ this.messageCallback.called.should.equal(true);
|
|
|
|
|
+ const [channel, message] = Array.from(this.messageCallback.args[0]);
|
|
|
|
|
+ channel.should.equal("applied-ops");
|
|
|
|
|
+ return JSON.parse(message).should.deep.include({
|
|
|
|
|
+ project_id: this.project_id,
|
|
|
|
|
+ doc_id: this.doc_id,
|
|
|
error:'Delete component does not match'
|
|
error:'Delete component does not match'
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+ describe("with enough updates to flush to the track changes api", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ [this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
|
|
|
|
|
+ const updates = [];
|
|
|
|
|
+ for (let v = 0; v <= 199; v++) { // Should flush after 100 ops
|
|
|
|
|
+ updates.push({
|
|
|
|
|
+ doc_id: this.doc_id,
|
|
|
|
|
+ op: [{i: v.toString(), p: 0}],
|
|
|
|
|
+ v
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- describe "with enough updates to flush to the track changes api", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
|
|
|
|
- updates = []
|
|
|
|
|
- for v in [0..199] # Should flush after 100 ops
|
|
|
|
|
- updates.push
|
|
|
|
|
- doc_id: @doc_id,
|
|
|
|
|
- op: [i: v.toString(), p: 0]
|
|
|
|
|
- v: v
|
|
|
|
|
-
|
|
|
|
|
- sinon.spy MockTrackChangesApi, "flushDoc"
|
|
|
|
|
-
|
|
|
|
|
- MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: 0}
|
|
|
|
|
-
|
|
|
|
|
- # Send updates in chunks to causes multiple flushes
|
|
|
|
|
- actions = []
|
|
|
|
|
- for i in [0..19]
|
|
|
|
|
- do (i) =>
|
|
|
|
|
- actions.push (cb) =>
|
|
|
|
|
- DocUpdaterClient.sendUpdates @project_id, @doc_id, updates.slice(i*10, (i+1)*10), cb
|
|
|
|
|
- async.series actions, (error) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- setTimeout done, 2000
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- after ->
|
|
|
|
|
- MockTrackChangesApi.flushDoc.restore()
|
|
|
|
|
-
|
|
|
|
|
- it "should flush the doc twice", ->
|
|
|
|
|
- MockTrackChangesApi.flushDoc.calledTwice.should.equal true
|
|
|
|
|
-
|
|
|
|
|
- describe "when there is no version in Mongo", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
|
|
|
|
- MockWebApi.insertDoc @project_id, @doc_id, {
|
|
|
|
|
- lines: @lines
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ sinon.spy(MockTrackChangesApi, "flushDoc");
|
|
|
|
|
+
|
|
|
|
|
+ MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: 0});
|
|
|
|
|
|
|
|
- update =
|
|
|
|
|
- doc: @doc_id
|
|
|
|
|
- op: @update.op
|
|
|
|
|
|
|
+ // Send updates in chunks to causes multiple flushes
|
|
|
|
|
+ const actions = [];
|
|
|
|
|
+ for (let i = 0; i <= 19; i++) {
|
|
|
|
|
+ (i => {
|
|
|
|
|
+ return actions.push(cb => {
|
|
|
|
|
+ return DocUpdaterClient.sendUpdates(this.project_id, this.doc_id, updates.slice(i*10, (i+1)*10), cb);
|
|
|
|
|
+ });
|
|
|
|
|
+ })(i);
|
|
|
|
|
+ }
|
|
|
|
|
+ async.series(actions, error => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return setTimeout(done, 2000);
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ after(() => MockTrackChangesApi.flushDoc.restore());
|
|
|
|
|
+
|
|
|
|
|
+ return it("should flush the doc twice", () => MockTrackChangesApi.flushDoc.calledTwice.should.equal(true));
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe("when there is no version in Mongo", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ [this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
|
|
|
|
|
+ MockWebApi.insertDoc(this.project_id, this.doc_id, {
|
|
|
|
|
+ lines: this.lines
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const update = {
|
|
|
|
|
+ doc: this.doc_id,
|
|
|
|
|
+ op: this.update.op,
|
|
|
v: 0
|
|
v: 0
|
|
|
- DocUpdaterClient.sendUpdate @project_id, @doc_id, update, (error) ->
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- setTimeout done, 200
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should update the doc (using version = 0)", (done) ->
|
|
|
|
|
- DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
|
|
|
|
- doc.lines.should.deep.equal @result
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- describe "when the sending duplicate ops", ->
|
|
|
|
|
- before (done) ->
|
|
|
|
|
- [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
|
|
|
|
- MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
|
|
|
|
|
-
|
|
|
|
|
- DocUpdaterClient.subscribeToAppliedOps @messageCallback = sinon.stub()
|
|
|
|
|
-
|
|
|
|
|
- # One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
|
|
|
|
|
- DocUpdaterClient.sendUpdate @project_id, @doc_id, {
|
|
|
|
|
- doc: @doc_id
|
|
|
|
|
|
|
+ };
|
|
|
|
|
+ DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, function(error) {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return setTimeout(done, 200);
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return it("should update the doc (using version = 0)", function(done) {
|
|
|
|
|
+ DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
|
|
|
|
|
+ doc.lines.should.deep.equal(this.result);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return describe("when the sending duplicate ops", function() {
|
|
|
|
|
+ before(function(done) {
|
|
|
|
|
+ [this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
|
|
|
|
|
+ MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
|
|
|
|
|
+
|
|
|
|
|
+ DocUpdaterClient.subscribeToAppliedOps(this.messageCallback = sinon.stub());
|
|
|
|
|
+
|
|
|
|
|
+ // One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
|
|
|
|
|
+ DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, {
|
|
|
|
|
+ doc: this.doc_id,
|
|
|
op: [{
|
|
op: [{
|
|
|
- i: "one and a half\n"
|
|
|
|
|
|
|
+ i: "one and a half\n",
|
|
|
p: 4
|
|
p: 4
|
|
|
- }]
|
|
|
|
|
- v: @version
|
|
|
|
|
- meta:
|
|
|
|
|
|
|
+ }],
|
|
|
|
|
+ v: this.version,
|
|
|
|
|
+ meta: {
|
|
|
source: "ikHceq3yfAdQYzBo4-xZ"
|
|
source: "ikHceq3yfAdQYzBo4-xZ"
|
|
|
- }, (error) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- setTimeout () =>
|
|
|
|
|
- DocUpdaterClient.sendUpdate @project_id, @doc_id, {
|
|
|
|
|
- doc: @doc_id
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ }, error => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return setTimeout(() => {
|
|
|
|
|
+ return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, {
|
|
|
|
|
+ doc: this.doc_id,
|
|
|
op: [{
|
|
op: [{
|
|
|
- i: "one and a half\n"
|
|
|
|
|
|
|
+ i: "one and a half\n",
|
|
|
p: 4
|
|
p: 4
|
|
|
- }]
|
|
|
|
|
- v: @version
|
|
|
|
|
- dupIfSource: ["ikHceq3yfAdQYzBo4-xZ"]
|
|
|
|
|
- meta:
|
|
|
|
|
|
|
+ }],
|
|
|
|
|
+ v: this.version,
|
|
|
|
|
+ dupIfSource: ["ikHceq3yfAdQYzBo4-xZ"],
|
|
|
|
|
+ meta: {
|
|
|
source: "ikHceq3yfAdQYzBo4-xZ"
|
|
source: "ikHceq3yfAdQYzBo4-xZ"
|
|
|
- }, (error) =>
|
|
|
|
|
- throw error if error?
|
|
|
|
|
- setTimeout done, 200
|
|
|
|
|
- , 200
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should update the doc", (done) ->
|
|
|
|
|
- DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
|
|
|
|
- doc.lines.should.deep.equal @result
|
|
|
|
|
- done()
|
|
|
|
|
- return null
|
|
|
|
|
-
|
|
|
|
|
- it "should return a message about duplicate ops", ->
|
|
|
|
|
- @messageCallback.calledTwice.should.equal true
|
|
|
|
|
- @messageCallback.args[0][0].should.equal "applied-ops"
|
|
|
|
|
- expect(JSON.parse(@messageCallback.args[0][1]).op.dup).to.be.undefined
|
|
|
|
|
- @messageCallback.args[1][0].should.equal "applied-ops"
|
|
|
|
|
- expect(JSON.parse(@messageCallback.args[1][1]).op.dup).to.equal true
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ }, error => {
|
|
|
|
|
+ if (error != null) { throw error; }
|
|
|
|
|
+ return setTimeout(done, 200);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ , 200);
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it("should update the doc", function(done) {
|
|
|
|
|
+ DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
|
|
|
|
|
+ doc.lines.should.deep.equal(this.result);
|
|
|
|
|
+ return done();
|
|
|
|
|
+ });
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return it("should return a message about duplicate ops", function() {
|
|
|
|
|
+ this.messageCallback.calledTwice.should.equal(true);
|
|
|
|
|
+ this.messageCallback.args[0][0].should.equal("applied-ops");
|
|
|
|
|
+ expect(JSON.parse(this.messageCallback.args[0][1]).op.dup).to.be.undefined;
|
|
|
|
|
+ this.messageCallback.args[1][0].should.equal("applied-ops");
|
|
|
|
|
+ return expect(JSON.parse(this.messageCallback.args[1][1]).op.dup).to.equal(true);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
|