Просмотр исходного кода

Import ConnectedUsersManager from web

James Allen 12 лет назад
Родитель
Сommit
f7482014ce

+ 78 - 0
services/real-time/app/coffee/ConnectedUsersManager.coffee

@@ -0,0 +1,78 @@
+async = require("async")
+Settings = require('settings-sharelatex')
+logger = require("logger-sharelatex")
+redis = require("redis-sharelatex")
+rclient = redis.createClient(Settings.redis.web)
+
+
+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 / 4
+
+buildProjectSetKey = (project_id)-> return "clients_in_project:#{project_id}"
+buildUserKey = (project_id, client_id)-> return "connected_user:#{project_id}:#{client_id}"
+
+
+module.exports =
+
+	# Use the same method for when a user connects, and when a user sends a cursor
+	# update. This way we don't care if the connected_user key has expired when
+	# we receive a cursor update. 
+	updateUserPosition: (project_id, client_id, user, cursorData, callback = (err)->)->
+		logger.log project_id:project_id, client_id:client_id, "marking user as connected"
+
+		multi = rclient.multi()
+		
+		multi.sadd   buildProjectSetKey(project_id), client_id
+		multi.expire buildProjectSetKey(project_id), FOUR_DAYS_IN_S
+		
+		multi.hset buildUserKey(project_id, client_id), "last_updated_at", Date.now()
+		multi.hset buildUserKey(project_id, client_id), "user_id", user._id
+		multi.hset buildUserKey(project_id, client_id), "first_name", user.first_name
+		multi.hset buildUserKey(project_id, client_id), "last_name", user.last_name
+		multi.hset buildUserKey(project_id, client_id), "email", user.email
+		
+		if cursorData?
+			multi.hset buildUserKey(project_id, client_id), "cursorData", JSON.stringify(cursorData)
+		multi.expire buildUserKey(project_id, client_id), USER_TIMEOUT_IN_S
+		
+		multi.exec (err)->
+			if err?
+				logger.err err:err, project_id:project_id, client_id:client_id, "problem marking user as connected"
+			callback(err)
+
+	markUserAsDisconnected: (project_id, client_id, callback)->
+		logger.log project_id:project_id, client_id:client_id, "marking user as disconnected"
+		multi = rclient.multi()
+		multi.srem buildProjectSetKey(project_id), client_id
+		multi.expire buildProjectSetKey(project_id), FOUR_DAYS_IN_S
+		multi.del buildUserKey(project_id, client_id)
+		multi.exec callback
+
+
+	_getConnectedUser: (project_id, client_id, callback)->
+		rclient.hgetall buildUserKey(project_id, client_id), (err, result)->
+			if !result?
+				result =
+					connected : false
+					client_id:client_id
+			else
+				result.connected = true
+				result.client_id = client_id
+				if result.cursorData?
+					result.cursorData = JSON.parse(result.cursorData)
+			callback err, result
+
+	getConnectedUsers: (project_id, callback)->
+		self = @
+		rclient.smembers buildProjectSetKey(project_id), (err, results)->
+			jobs = results.map (client_id)->
+				(cb)->
+					self._getConnectedUser(project_id, client_id, cb)
+			async.series jobs, (err, users)->
+				users = users.filter (user) ->
+					user.connected
+				callback err, users
+

+ 154 - 0
services/real-time/test/unit/coffee/ConnectedUsersManagerTests.coffee

@@ -0,0 +1,154 @@
+
+should = require('chai').should()
+SandboxedModule = require('sandboxed-module')
+assert = require('assert')
+path = require('path')
+sinon = require('sinon')
+modulePath = path.join __dirname, "../../../app/js/ConnectedUsersManager"
+expect = require("chai").expect
+tk = require("timekeeper")
+
+
+describe "ConnectedUsersManager", ->
+
+	beforeEach ->
+
+		@settings =
+			redis:
+				web:{}
+		@rClient =
+			auth:->
+			setex:sinon.stub()
+			sadd:sinon.stub()
+			get: sinon.stub()
+			srem:sinon.stub()
+			del:sinon.stub()
+			smembers:sinon.stub()
+			expire:sinon.stub()
+			hset:sinon.stub()
+			hgetall:sinon.stub()
+			exec:sinon.stub()
+			multi: => return @rClient
+		tk.freeze(new Date())
+
+		@ConnectedUsersManager = SandboxedModule.require modulePath, requires:
+			"settings-sharelatex":@settings
+			"logger-sharelatex": log:->
+			"redis-sharelatex": createClient:=> 
+				return @rClient
+		@client_id = "32132132"
+		@project_id = "dskjh2u21321"
+		@user = {
+			_id: "user-id-123"
+			first_name: "Joe"
+			last_name: "Bloggs"
+			email: "joe@example.com"
+		}
+		@cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' }
+
+	afterEach -> 
+		tk.reset()
+
+	describe "updateUserPosition", ->
+		beforeEach ->
+			@rClient.exec.callsArgWith(0)
+
+		it "should set a key with the date and give it a ttl", (done)->
+			@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
+				@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "last_updated_at", Date.now()).should.equal true
+				done()
+
+		it "should set a key with the user_id", (done)->
+			@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
+				@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "user_id", @user._id).should.equal true
+				done()
+
+		it "should set a key with the first_name", (done)->
+			@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
+				@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "first_name", @user.first_name).should.equal true
+				done()
+
+		it "should set a key with the last_name", (done)->
+			@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
+				@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "last_name", @user.last_name).should.equal true
+				done()
+
+		it "should set a key with the email", (done)->
+			@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
+				@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "email", @user.email).should.equal true
+				done()
+
+		it "should push the client_id on to the project list", (done)->
+			@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
+				@rClient.sadd.calledWith("clients_in_project:#{@project_id}", @client_id).should.equal true
+				done()
+
+		it "should add a ttl to the project set so it stays clean", (done)->
+			@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
+				@rClient.expire.calledWith("clients_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.updateUserPosition @project_id, @client_id, @user, null, (err)=>
+				@rClient.expire.calledWith("connected_user:#{@project_id}:#{@client_id}", 60 * 15).should.equal true
+				done()
+
+		it "should set the cursor position when provided", (done)->
+			@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, @cursorData, (err)=>
+				@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "cursorData", JSON.stringify(@cursorData)).should.equal true
+				done()
+
+	describe "markUserAsDisconnected", ->
+		beforeEach ->
+			@rClient.exec.callsArgWith(0)
+
+		it "should remove the user from the set", (done)->
+			@ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=>
+				@rClient.srem.calledWith("clients_in_project:#{@project_id}", @client_id).should.equal true
+				done()
+
+		it "should delete the connected_user string", (done)->
+			@ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=>
+				@rClient.del.calledWith("connected_user:#{@project_id}:#{@client_id}").should.equal true
+				done()
+
+		it "should add a ttl to the connected user set so it stays clean", (done)->
+			@ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=>
+				@rClient.expire.calledWith("clients_in_project:#{@project_id}", 24 * 4 * 60 * 60).should.equal true
+				done()
+
+	describe "_getConnectedUser", ->
+
+		it "should get the user returning connected if there is a value", (done)->
+			cursorData = JSON.stringify(cursorData:{row:1})
+			@rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), cursorData})
+			@ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=>
+				result.connected.should.equal true
+				result.client_id.should.equal @client_id
+				done()
+
+		it "should get the user returning connected if there is a value", (done)->
+			@rClient.hgetall.callsArgWith(1)
+			@ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=>
+				result.connected.should.equal false
+				result.client_id.should.equal @client_id
+				done()
+
+	describe "getConnectedUsers", ->
+
+		beforeEach ->
+			@users = ["1234", "5678", "9123"]
+			@rClient.smembers.callsArgWith(1, null, @users)
+			@ConnectedUsersManager._getConnectedUser = sinon.stub()
+			@ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[0]).callsArgWith(2, null, {connected:true, client_id:@users[0]})
+			@ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[1]).callsArgWith(2, null, {connected:false, client_id:@users[1]})
+			@ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[2]).callsArgWith(2, null, {connected:true, client_id:@users[2]})
+
+
+		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.deep.equal {client_id:@users[0], connected:true}
+				users[1].should.deep.equal {client_id:@users[2], connected:true}
+				done()
+