refactor(realtime): extract "refresh" and "user status" to class

1. extract client socket event handler "refresh" and "user status"
to SocketClient class
2. add testcase for this changes

Signed-off-by: BoHong Li <a60814billy@gmail.com>
This commit is contained in:
BoHong Li 2019-05-24 12:41:53 +08:00
parent 85fc2297ac
commit 48cebc0ccc
No known key found for this signature in database
GPG Key ID: 9696D5590D58290F
2 changed files with 508 additions and 28 deletions

View File

@ -86,7 +86,7 @@ setInterval(function () {
if (note.server.isDirty) {
if (config.debug) logger.info('updater found dirty note: ' + key)
note.server.isDirty = false
updateNote(note, function (err, _note) {
exports.updateNote(note, function (err, _note) {
// handle when note already been clean up
if (!notes[key] || !notes[key].server) return callback(null, null)
if (!_note) {
@ -209,7 +209,7 @@ setInterval(function () {
saverSleep = true
}
})
}, 60000 * 5)
}, 5 * 60 * 1000) // 5 mins
function getStatus (callback) {
models.Note.count().then(function (notecount) {
@ -737,14 +737,60 @@ function updateHistory (userId, note, time) {
if (note.server) history.updateHistory(userId, noteId, note.server.document, time)
}
function getUserPool () {
return users
}
function getUserFromUserPool (userId) {
return users[userId]
}
function getNotePool () {
return notes
}
function getNoteFromNotePool (noteId) {
return notes[noteId]
}
class SocketClient {
constructor (socket) {
this.socket = socket
}
registerEventHandler () {
// received client refresh request
this.socket.on('refresh', this.refreshEventHandler.bind(this))
// received user status
this.socket.on('user status', this.userStatusEventHandler.bind(this))
}
refreshEventHandler () {
exports.emitRefresh(this.socket)
}
userStatusEventHandler (data) {
const noteId = this.socket.noteId
const user = getUserFromUserPool(this.socket.id)
if (!noteId || !getNoteFromNotePool(noteId) || !user) return
if (config.debug) { logger.info('SERVER received [' + noteId + '] user status from [' + this.socket.id + ']: ' + JSON.stringify(data)) }
if (data) {
user.idle = data.idle
user.type = data.type
}
exports.emitUserStatus(this.socket)
}
}
function connection (socket) {
if (realtime.maintenance) return
parseNoteIdFromSocket(socket, function (err, noteId) {
exports.parseNoteIdFromSocket(socket, function (err, noteId) {
if (err) {
return failConnection(500, err, socket)
return exports.failConnection(500, err, socket)
}
if (!noteId) {
return failConnection(404, 'note id not found', socket)
return exports.failConnection(404, 'note id not found', socket)
}
if (isDuplicatedInSocketQueue(connectionSocketQueue, socket)) return
@ -785,30 +831,15 @@ function connection (socket) {
idle: false,
type: null
}
updateUserData(socket, users[socket.id])
exports.updateUserData(socket, users[socket.id])
// start connection
connectionSocketQueue.push(socket)
startConnection(socket)
exports.startConnection(socket)
})
// received client refresh request
socket.on('refresh', function () {
emitRefresh(socket)
})
// received user status
socket.on('user status', function (data) {
var noteId = socket.noteId
var user = users[socket.id]
if (!noteId || !notes[noteId] || !user) return
if (config.debug) { logger.info('SERVER received [' + noteId + '] user status from [' + socket.id + ']: ' + JSON.stringify(data)) }
if (data) {
user.idle = data.idle
user.type = data.type
}
emitUserStatus(socket)
})
const socketClient = new SocketClient(socket)
socketClient.registerEventHandler()
// received note permission change request
socket.on('permission', function (permission) {
@ -963,3 +994,14 @@ function connection (socket) {
exports = module.exports = realtime
exports.extractNoteIdFromSocket = extractNoteIdFromSocket
exports.parseNoteIdFromSocket = parseNoteIdFromSocket
exports.updateNote = updateNote
exports.finishUpdateNote = finishUpdateNote
exports.failConnection = failConnection
exports.isDuplicatedInSocketQueue = isDuplicatedInSocketQueue
exports.updateUserData = updateUserData
exports.startConnection = startConnection
exports.emitRefresh = emitRefresh
exports.emitUserStatus = emitUserStatus
exports.notes = notes
exports.users = users

View File

@ -2,21 +2,25 @@
/* eslint-env node, mocha */
const io = require('socket.io-client')
const http = require('http')
const mock = require('mock-require')
const assert = require('assert')
const sinon = require('sinon')
function makeMockSocket (headers, query) {
return {
id: Math.round(Math.random() * 10000),
handshake: {
headers: Object.assign({}, headers),
query: Object.assign({}, query)
}
},
on: sinon.fake()
}
}
function removeModuleFromRequireCache (modulePath) {
delete require.cache[require.resolve(modulePath)]
}
describe('realtime', function () {
describe('extractNoteIdFromSocket', function () {
beforeEach(() => {
@ -101,4 +105,438 @@ describe('realtime', function () {
})
})
})
describe('parseNoteIdFromSocket', function () {
let realtime
beforeEach(() => {
mock('../lib/logger', {})
mock('../lib/history', {})
mock('../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
callback(null, noteId)
}
}
})
mock('../lib/config', {})
})
afterEach(() => {
removeModuleFromRequireCache('../lib/realtime')
mock.stopAll()
})
it('should return null when socket not send noteId', function () {
realtime = require('../lib/realtime')
const mockSocket = makeMockSocket()
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, null])
})
describe('noteId exists', function () {
beforeEach(() => {
mock('../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
callback(null, noteId)
}
}
})
})
it('should return noteId when noteId exists', function () {
realtime = require('../lib/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../lib/realtime')
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, noteId])
})
})
describe('noteId not exists', function () {
beforeEach(() => {
mock('../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
callback(null, null)
}
}
})
})
it('should return null when noteId not exists', function () {
realtime = require('../lib/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../lib/realtime')
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, null])
})
})
describe('parse note error', function () {
beforeEach(() => {
mock('../lib/models', {
Note: {
parseNoteId: function (noteId, callback) {
/* eslint-disable-next-line */
callback('error', null)
}
}
})
})
it('should return error when noteId parse error', function () {
realtime = require('../lib/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../lib/realtime')
const fakeCallback = sinon.fake()
realtime.parseNoteIdFromSocket(mockSocket, fakeCallback)
assert(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, ['error', null])
})
})
})
describe('update note is dirty timer', function () {
let realtime
beforeEach(() => {
mock('../lib/logger', {
error: () => {
}
})
mock('../lib/history', {})
mock('../lib/models', {
Revision: {
saveAllNotesRevision: () => {
}
}
})
mock('../lib/config', {})
})
afterEach(() => {
removeModuleFromRequireCache('../lib/realtime')
mock.stopAll()
})
it('should update note when note is dirty', (done) => {
const clock = sinon.useFakeTimers()
realtime = require('../lib/realtime')
sinon.stub(realtime, 'updateNote').callsFake(function (note, callback) {
callback(null, null)
})
const socketIoEmitFake = sinon.fake()
realtime.io = {
to: sinon.stub().callsFake(function () {
return {
emit: socketIoEmitFake
}
})
}
realtime.notes['note1'] = {
server: {
isDirty: false
},
socks: []
}
let note2 = {
server: {
isDirty: true
},
socks: []
}
realtime.notes['note2'] = note2
clock.tick(1000)
clock.restore()
setTimeout(() => {
assert(note2.server.isDirty === false)
done()
}, 50)
})
})
describe('updateNote', function () {
let realtime, fakeNote
beforeEach(() => {
mock('../lib/logger', {
error: () => {
}
})
mock('../lib/history', {})
mock('../lib/models', {
Note: {
findOne: async function () {
return fakeNote
}
}
})
mock('../lib/config', {})
})
afterEach(() => {
mock.stopAll()
})
it('should return null when note not found', function (done) {
fakeNote = null
realtime = require('../lib/realtime')
sinon.stub(realtime, 'finishUpdateNote').callsFake(function (a, b, callback) {
callback(null, b)
})
const fakeCallback = sinon.fake()
realtime.updateNote({ id: '123' }, fakeCallback)
setTimeout(() => {
assert.ok(fakeCallback.called)
assert.deepStrictEqual(fakeCallback.getCall(0).args, [null, null])
sinon.restore()
done()
}, 50)
})
})
describe('finishUpdateNote', function () {
let realtime
beforeEach(() => {
mock('../lib/logger', {})
mock('../lib/history', {})
mock('../lib/models', {
Note: {
parseNoteTitle: (data) => (data)
}
})
mock('../lib/config', {})
realtime = require('../lib/realtime')
})
afterEach(() => {
removeModuleFromRequireCache('../lib/realtime')
mock.stopAll()
})
it('return null when note is null', () => {
const fakeCallback = sinon.fake()
realtime.finishUpdateNote(null, {}, fakeCallback)
assert.ok(fakeCallback.calledOnce)
assert.deepStrictEqual(fakeCallback.lastCall.args, [null, null])
})
})
describe('connection', function () {
let realtime
beforeEach(() => {
mock('../lib/logger', {
error: () => {
}
})
mock('../lib/history', {})
mock('../lib/models', {
Note: {
parseNoteTitle: (data) => (data)
}
})
mock('../lib/config', {})
realtime = require('../lib/realtime')
})
afterEach(() => {
removeModuleFromRequireCache('../lib/realtime')
mock.stopAll()
sinon.restore()
})
describe('fail', function () {
it('should fast return when server not start', () => {
const mockSocket = makeMockSocket()
realtime.maintenance = true
const spy = sinon.spy(realtime, 'parseNoteIdFromSocket')
realtime.connection(mockSocket)
assert(!spy.called)
})
it('should failed when parse noteId occur error', () => {
const mockSocket = makeMockSocket()
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback('error', null)
})
const failConnectionSpy = sinon.stub(realtime, 'failConnection')
realtime.connection(mockSocket)
assert(parseNoteIdFromSocketSpy.called)
assert(failConnectionSpy.calledOnce)
assert.deepStrictEqual(failConnectionSpy.lastCall.args, [500, 'error', mockSocket])
})
it('should failed when noteId not exists', () => {
const mockSocket = makeMockSocket()
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback(null, null)
})
const failConnectionSpy = sinon.stub(realtime, 'failConnection')
realtime.connection(mockSocket)
assert(parseNoteIdFromSocketSpy.called)
assert(failConnectionSpy.calledOnce)
assert.deepStrictEqual(failConnectionSpy.lastCall.args, [404, 'note id not found', mockSocket])
})
})
it('should success connect', function () {
const mockSocket = makeMockSocket()
const noteId = 'note123'
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback(null, noteId)
})
const failConnectionStub = sinon.stub(realtime, 'failConnection')
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
const startConnectionStub = sinon.stub(realtime, 'startConnection')
realtime.connection(mockSocket)
assert.ok(parseNoteIdFromSocketSpy.calledOnce)
assert(failConnectionStub.called === false)
assert(updateUserDataStub.calledOnce)
assert(startConnectionStub.calledOnce)
assert(mockSocket.on.callCount === 11)
})
})
describe('socket event', function () {
let realtime
const noteId = "note123"
let clientSocket
const eventFuncMap = new Map()
beforeEach(() => {
mock('../lib/logger', {
error: () => {
}
})
mock('../lib/history', {})
mock('../lib/models', {
Note: {
parseNoteTitle: (data) => (data)
}
})
mock('../lib/config', {})
realtime = require('../lib/realtime')
// get all socket event handler
clientSocket = makeMockSocket()
clientSocket.on = function (event, func) {
eventFuncMap.set(event, func)
}
realtime.maintenance = false
sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
/* eslint-disable-next-line */
callback(null, noteId)
})
sinon.stub(realtime, 'failConnection')
sinon.stub(realtime, 'updateUserData')
sinon.stub(realtime, 'startConnection')
realtime.connection(clientSocket)
})
afterEach(() => {
removeModuleFromRequireCache('../lib/realtime')
mock.stopAll()
sinon.restore()
})
describe('refresh', function () {
it('should call refresh', () => {
const refreshFunc = eventFuncMap.get('refresh')
const emitRefreshStub = sinon.stub(realtime, 'emitRefresh')
refreshFunc()
assert(emitRefreshStub.calledOnce)
assert.deepStrictEqual(emitRefreshStub.lastCall.args[0], clientSocket)
})
})
describe('user status', function () {
it('should call emitUserStatus and update user data', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes[noteId] = {}
const userData = {
idle: true,
type: 'xs'
}
userStatusFunc(userData)
assert(emitUserStatusStub.calledOnce)
assert.deepStrictEqual(emitUserStatusStub.lastCall.args[0], clientSocket)
assert(realtime.users[clientSocket.id].idle === true)
assert(realtime.users[clientSocket.id].type === 'xs')
})
it('should call emitUserStatus without userdata', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes[noteId] = {}
userStatusFunc()
assert(emitUserStatusStub.calledOnce)
assert.deepStrictEqual(emitUserStatusStub.lastCall.args[0], clientSocket)
assert(realtime.users[clientSocket.id].idle === false)
assert(realtime.users[clientSocket.id].type === null)
})
it('should not call emitUserStatus when user not exists', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes[noteId] = {}
delete realtime.users[clientSocket.id]
const userData = {
idle: true,
type: 'xs'
}
userStatusFunc(userData)
assert(emitUserStatusStub.called === false)
})
it('should not call emitUserStatus when note not exists', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes = {}
realtime.users[clientSocket.id] = {}
const userData = {
idle: true,
type: 'xs'
}
userStatusFunc(userData)
assert(emitUserStatusStub.called === false)
})
})
})
})