diff --git a/services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersManager.coffee b/services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersManager.coffee index 72254c6dd1..eb008738ba 100644 --- a/services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersManager.coffee +++ b/services/web/app/coffee/Features/ConnectedUsers/ConnectedUsersManager.coffee @@ -11,6 +11,8 @@ ONE_HOUR_IN_S = 60 * 60 ONE_DAY_IN_S = ONE_HOUR_IN_S * 24 FOUR_DAYS_IN_S = ONE_DAY_IN_S * 4 +USER_TIMEOUT_IN_S = ONE_HOUR_IN_S + buildProjectSetKey = (project_id)-> return "users_in_project:#{project_id}" buildUserKey = (project_id, user_id)-> return "connected_user:#{project_id}:#{user_id}" @@ -25,7 +27,9 @@ module.exports = (cb)-> rclient.expire buildProjectSetKey(project_id), FOUR_DAYS_IN_S, cb (cb)-> - rclient.setex buildUserKey(project_id, user_id), ONE_HOUR_IN_S, new Date(), cb + rclient.hset buildUserKey(project_id, user_id), "connected_at", new Date(), cb + (cb)-> + rclient.expire buildUserKey(project_id, user_id), USER_TIMEOUT_IN_S, cb ], (err)-> if err? logger.err err:err, project_id:project_id, user_id:user_id, "problem marking user as connected" @@ -43,13 +47,25 @@ module.exports = ], callback _getConnectedUser: (project_id, user_id, callback)-> - rclient.get buildUserKey(project_id, user_id), (err, result)-> + rclient.hgetall buildUserKey(project_id, user_id), (err, result)-> if !result? - connected = false + result = + connected : false + user_id:user_id else - connected = true + result.connected = true + result.user_id = user_id + + callback err, result + + setUserCursorPosition: (project_id, user_id, cursorData, callback)-> + async.series [ + (cb)-> + rclient.hset buildUserKey(project_id, user_id), "cursorData", JSON.stringify(cursorData), cb + (cb)-> + rclient.expire buildUserKey(project_id, user_id), USER_TIMEOUT_IN_S, cb + ], callback - callback err, {connected:connected, user_id:user_id} getConnectedUsers: (project_id, callback)-> self = @ @@ -60,8 +76,5 @@ module.exports = async.series jobs, (err, users)-> users = _.filter users, (user)-> user.connected - user_ids = _.map users, (user)-> - user.user_id - callback err, user_ids - + callback err, users diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index e830c3cdc8..7fc67e12f2 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -126,6 +126,7 @@ module.exports = EditorController = cursorData.email = email if email? if first_name? and last_name? cursorData.name = first_name + " " + last_name + ConnectedUsersManager.setUserCursorPosition(project_id, user_id, cursorData, ->) else cursorData.name = "Anonymous" EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData) diff --git a/services/web/test/UnitTests/coffee/ConnectedUsersManager/ConnectedUsersManagerTests.coffee b/services/web/test/UnitTests/coffee/ConnectedUsersManager/ConnectedUsersManagerTests.coffee index b15e314c51..32253255b1 100644 --- a/services/web/test/UnitTests/coffee/ConnectedUsersManager/ConnectedUsersManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/ConnectedUsersManager/ConnectedUsersManagerTests.coffee @@ -25,6 +25,8 @@ describe "ConnectedUsersManager", -> del:sinon.stub() smembers:sinon.stub() expire:sinon.stub() + hset:sinon.stub() + hgetall:sinon.stub() tk.freeze(new Date()) @ConnectedUsersManager = SandboxedModule.require modulePath, requires: @@ -40,14 +42,14 @@ describe "ConnectedUsersManager", -> describe "markUserAsConnected", -> beforeEach -> - @rClient.setex.callsArgWith(3) + @rClient.hset.callsArgWith(3) @rClient.sadd.callsArgWith(2) @rClient.expire.callsArgWith(2) it "should set a key with the date and give it a ttl", (done)-> @ConnectedUsersManager.markUserAsConnected @project_id, @user_id, (err)=> - @rClient.setex.calledWith("connected_user:#{@project_id}:#{@user_id}", 60 * 60, new Date()).should.equal true + @rClient.hset.calledWith("connected_user:#{@project_id}:#{@user_id}", "connected_at", new Date()).should.equal true done() it "should push the user_id on to the project list", (done)-> @@ -60,6 +62,11 @@ describe "ConnectedUsersManager", -> @rClient.expire.calledWith("users_in_project:#{@project_id}", 24 * 4 * 60 * 60).should.equal true done() + it "should add a ttl to the connected user so it stays clean", (done)-> + @ConnectedUsersManager.markUserAsConnected @project_id, @user_id, (err)=> + @rClient.expire.calledWith("connected_user:#{@project_id}:#{@user_id}", 60 * 60).should.equal true + done() + describe "markUserAsDisconnected", -> beforeEach -> @rClient.srem.callsArgWith(2) @@ -85,14 +92,14 @@ describe "ConnectedUsersManager", -> describe "_getConnectedUser", -> it "should get the user returning connected if there is a value", (done)-> - @rClient.get.callsArgWith(1, null, new Date()) + @rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), cursorData:{row:1}}) @ConnectedUsersManager._getConnectedUser @project_id, @user_id, (err, result)=> result.connected.should.equal true result.user_id.should.equal @user_id done() it "should get the user returning connected if there is a value", (done)-> - @rClient.get.callsArgWith(1) + @rClient.hgetall.callsArgWith(1) @ConnectedUsersManager._getConnectedUser @project_id, @user_id, (err, result)=> result.connected.should.equal false result.user_id.should.equal @user_id @@ -114,11 +121,25 @@ describe "ConnectedUsersManager", -> it "should only return the users in the list which are still in redis", (done)-> @ConnectedUsersManager.getConnectedUsers @project_id, (err, users)=> users.length.should.equal 2 - users[0].should.equal @users[0] - users[1].should.equal @users[2] + users[0].should.deep.equal {user_id:@users[0], connected:true} + users[1].should.deep.equal {user_id:@users[2], connected:true} + done() + + describe "setUserCursorPosition", -> + + beforeEach -> + @cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' } + @rClient.hset.callsArgWith(3) + @rClient.expire.callsArgWith(2) + + it "should add the cursor data to the users hash", (done)-> + @ConnectedUsersManager.setUserCursorPosition @project_id, @user_id, @cursorData, (err)=> + @rClient.hset.calledWith("connected_user:#{@project_id}:#{@user_id}", "cursorData", JSON.stringify(@cursorData)).should.equal true done() - - + it "should add the ttl on", (done)-> + @ConnectedUsersManager.setUserCursorPosition @project_id, @user_id, @cursorData, (err)=> + @rClient.expire.calledWith("connected_user:#{@project_id}:#{@user_id}", 60 * 60).should.equal true + done() diff --git a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee index 6240e601f6..eacc03f6ee 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee @@ -56,6 +56,7 @@ describe "EditorController", -> @ConnectedUsersManager = markUserAsDisconnected:sinon.stub() markUserAsConnected:sinon.stub() + setUserCursorPosition:sinon.stub() @EditorController = SandboxedModule.require modulePath, requires: "../../infrastructure/Server" : io : @io @@ -262,11 +263,14 @@ describe "EditorController", -> describe "updateClientPosition", -> beforeEach -> @EditorRealTimeController.emitToRoom = sinon.stub() + @ConnectedUsersManager.setUserCursorPosition.callsArgWith(3) @update = { doc_id: @doc_id = "doc-id-123" row: @row = 42 column: @column = 37 } + + describe "with a logged in user", -> beforeEach -> @clientParams = { @@ -279,18 +283,21 @@ describe "EditorController", -> @client.get = (param, callback) => callback null, @clientParams[param] @EditorController.updateClientPosition @client, @update + @populatedCursorData = + doc_id: @doc_id, + id: @client.id + name: "#{@first_name} #{@last_name}" + row: @row + column: @column + email: @email + user_id: @user_id + it "should send the update to the project room with the user's name", -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "clientTracking.clientUpdated", { - doc_id: @doc_id, - id: @client.id - name: "#{@first_name} #{@last_name}" - row: @row - column: @column - email: @email - user_id: @user_id - }) - .should.equal true + @EditorRealTimeController.emitToRoom.calledWith(@project_id, "clientTracking.clientUpdated", @populatedCursorData).should.equal true + + it "should send the cursor data to the connected user manager", (done)-> + @ConnectedUsersManager.setUserCursorPosition.calledWith(@project_id, @user_id, @populatedCursorData).should.equal true + done() describe "with an anonymous user", -> beforeEach -> @@ -311,6 +318,9 @@ describe "EditorController", -> }) .should.equal true + it "should not send cursor data to the connected user manager", (done)-> + @ConnectedUsersManager.setUserCursorPosition.called.should.equal false + done() describe "addUserToProject", -> beforeEach ->