mirror of https://github.com/status-im/codimd.git
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:
parent
85fc2297ac
commit
48cebc0ccc
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue