refactor(realtime): connection flow to queue

Signed-off-by: BoHong Li <a60814billy@gmail.com>
This commit is contained in:
BoHong Li 2019-05-20 16:04:35 +08:00
parent 0b03b8e9ba
commit 17e82c11c9
No known key found for this signature in database
GPG Key ID: 9696D5590D58290F
9 changed files with 552 additions and 367 deletions

3
app.js
View File

@ -299,6 +299,9 @@ function handleTermSignals () {
})
}
}, 100)
setTimeout(() => {
process.exit(1)
}, 5000)
}
process.on('SIGINT', handleTermSignals)
process.on('SIGTERM', handleTermSignals)

View File

@ -7,11 +7,20 @@ const EventEmitter = require('events').EventEmitter
*/
const QueueEvent = {
Tick: 'Tick'
Tick: 'Tick',
Push: 'Push',
Finish: 'Finish'
}
class ProcessQueue extends EventEmitter {
constructor (maximumLength, triggerTimeInterval = 10) {
constructor ({
maximumLength = 500,
triggerTimeInterval = 5000,
// execute on push
proactiveMode = true,
// execute next work on finish
continuousMode = true
}) {
super()
this.max = maximumLength
this.triggerTime = triggerTimeInterval
@ -19,12 +28,20 @@ class ProcessQueue extends EventEmitter {
this.queue = []
this.lock = false
this.on(QueueEvent.Tick, () => {
if (this.lock) return
this.lock = true
setImmediate(() => {
this.process()
})
this.on(QueueEvent.Tick, this.onEventProcessFunc.bind(this))
if (proactiveMode) {
this.on(QueueEvent.Push, this.onEventProcessFunc.bind(this))
}
if (continuousMode) {
this.on(QueueEvent.Finish, this.onEventProcessFunc.bind(this))
}
}
onEventProcessFunc () {
if (this.lock) return
this.lock = true
setImmediate(() => {
this.process()
})
}
@ -62,7 +79,7 @@ class ProcessQueue extends EventEmitter {
this.taskMap.set(id, true)
this.queue.push(task)
this.start()
this.emit(QueueEvent.Tick)
this.emit(QueueEvent.Push)
return true
}
@ -79,7 +96,7 @@ class ProcessQueue extends EventEmitter {
const finishTask = () => {
this.lock = false
setImmediate(() => {
this.emit(QueueEvent.Tick)
this.emit(QueueEvent.Finish)
})
}
task.processingFunc().then(finishTask).catch(finishTask)

View File

@ -38,7 +38,8 @@ const realtime = {
maintenance: true
}
const disconnectProcessQueue = new ProcessQueue(2000, 500)
const connectProcessQueue = new ProcessQueue({})
const disconnectProcessQueue = new ProcessQueue({})
const updateDirtyNoteJob = new UpdateDirtyNoteJob(realtime)
const cleanDanglingUserJob = new CleanDanglingUserJob(realtime)
const saveRevisionJob = new SaveRevisionJob(realtime)
@ -97,6 +98,46 @@ function emitCheck (note) {
var users = {}
var notes = {}
function getNotePool () {
return notes
}
function isNoteExistsInPool (noteId) {
return !!notes[noteId]
}
function addNote (note) {
if (exports.isNoteExistsInPool(note.id)) return false
notes[note.id] = note
return true
}
function getNotePoolSize () {
return Object.keys(notes).length
}
function deleteNoteFromPool(noteId) {
delete notes[noteId]
}
function deleteAllNoteFromPool() {
Object.keys(notes).forEach(noteId => {
delete notes[noteId]
})
}
function getNoteFromNotePool (noteId) {
return notes[noteId]
}
function getUserPool () {
return users
}
function getUserFromUserPool (userId) {
return users[userId]
}
disconnectProcessQueue.start()
updateDirtyNoteJob.start()
cleanDanglingUserJob.start()
@ -265,7 +306,8 @@ function getStatus (callback) {
function isReady () {
return realtime.io &&
Object.keys(notes).length === 0 && Object.keys(users).length === 0 &&
connectionSocketQueue.length === 0 && !isConnectionBusy &&
!isConnectionBusy &&
connectProcessQueue.queue.length === 0 && !connectProcessQueue.lock &&
disconnectProcessQueue.queue.length === 0 && !disconnectProcessQueue.lock
}
@ -327,6 +369,15 @@ function parseNoteIdFromSocket (socket, callback) {
})
}
function parseNoteIdFromSocketAsync (socket) {
return new Promise((resolve, reject) => {
parseNoteIdFromSocket(socket, (err, id) => {
if (err) return reject(err)
resolve(id)
})
})
}
// TODO: test it
function emitOnlineUsers (socket) {
var noteId = socket.noteId
@ -374,48 +425,6 @@ function emitRefresh (socket) {
socket.emit('refresh', out)
}
// TODO: test it
function isDuplicatedInSocketQueue (queue, socket) {
for (var i = 0; i < queue.length; i++) {
if (queue[i] && queue[i].id === socket.id) {
return true
}
}
return false
}
// TODO: test it
function clearSocketQueue (queue, socket) {
for (var i = 0; i < queue.length; i++) {
if (!queue[i] || queue[i].id === socket.id) {
queue.splice(i, 1)
i--
}
}
}
// TODO: test it
function connectNextSocket () {
setTimeout(function () {
isConnectionBusy = false
if (connectionSocketQueue.length > 0) {
startConnection(connectionSocketQueue[0])
}
}, 1)
}
// TODO: test it
function interruptConnection (socket, noteId, socketId) {
if (notes[noteId]) delete notes[noteId]
if (users[socketId]) delete users[socketId]
if (socket) {
clearSocketQueue(connectionSocketQueue, socket)
} else {
connectionSocketQueue.shift()
}
connectNextSocket()
}
function checkViewPermission (req, note) {
if (note.permission === 'private') {
if (req.user && req.user.logged_in && req.user.id === note.owner) {
@ -435,65 +444,14 @@ function checkViewPermission (req, note) {
}
var isConnectionBusy = false
var connectionSocketQueue = []
// TODO: test it
function finishConnection (socket, noteId, socketId) {
// if no valid info provided will drop the client
if (!socket || !notes[noteId] || !users[socketId]) {
return interruptConnection(socket, noteId, socketId)
}
// check view permission
if (!checkViewPermission(socket.request, notes[noteId])) {
interruptConnection(socket, noteId, socketId)
return failConnection(403, 'connection forbidden', socket)
}
let note = notes[noteId]
let user = users[socketId]
// update user color to author color
if (note.authors[user.userid]) {
user.color = users[socket.id].color = note.authors[user.userid].color
}
note.users[socket.id] = user
note.socks.push(socket)
note.server.addClient(socket)
note.server.setName(socket, user.name)
note.server.setColor(socket, user.color)
// update user note history
updateHistory(user.userid, note)
emitOnlineUsers(socket)
emitRefresh(socket)
// clear finished socket in queue
clearSocketQueue(connectionSocketQueue, socket)
// seek for next socket
connectNextSocket()
if (config.debug) {
let noteId = socket.noteId
logger.info('SERVER connected a client to [' + noteId + ']:')
logger.info(JSON.stringify(user))
// logger.info(notes);
getStatus(function (data) {
logger.info(JSON.stringify(data))
})
}
}
// TODO: test it
function startConnection (socket) {
if (isConnectionBusy) return
isConnectionBusy = true
var noteId = socket.noteId
if (!noteId) {
return failConnection(404, 'note id not found', socket)
}
if (!notes[noteId]) {
var include = [{
async function fetchFullNoteAsync (noteId) {
return models.Note.findOne({
where: {
id: noteId
},
include: [{
model: models.User,
as: 'owner'
}, {
@ -507,75 +465,51 @@ function startConnection (socket) {
as: 'user'
}]
}]
})
}
models.Note.findOne({
where: {
id: noteId
},
include: include
}).then(function (note) {
if (!note) {
return failConnection(404, 'note not found', socket)
function buildAuthorProfilesFromNote (noteAuthors) {
const authors = {}
noteAuthors.forEach((author) => {
const profile = models.User.getProfile(author.user)
if (profile) {
authors[author.userId] = {
userid: author.userId,
color: author.color,
photo: profile.photo,
name: profile.name
}
var owner = note.ownerId
var ownerprofile = note.owner ? models.User.getProfile(note.owner) : null
}
})
return authors
}
var lastchangeuser = note.lastchangeuserId
var lastchangeuserprofile = note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null
function makeNewServerNote (note) {
const authors = buildAuthorProfilesFromNote(note.authors)
var body = note.content
var createtime = note.createdAt
var updatetime = note.lastchangeAt
var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback)
var authors = {}
for (var i = 0; i < note.authors.length; i++) {
var author = note.authors[i]
var profile = models.User.getProfile(author.user)
if (profile) {
authors[author.userId] = {
userid: author.userId,
color: author.color,
photo: profile.photo,
name: profile.name
}
}
}
notes[noteId] = {
id: noteId,
alias: note.alias,
title: note.title,
owner: owner,
ownerprofile: ownerprofile,
permission: note.permission,
lastchangeuser: lastchangeuser,
lastchangeuserprofile: lastchangeuserprofile,
socks: [],
users: {},
tempUsers: {},
createtime: moment(createtime).valueOf(),
updatetime: moment(updatetime).valueOf(),
server: server,
authors: authors,
authorship: note.authorship
}
return finishConnection(socket, noteId, socket.id)
}).catch(function (err) {
return failConnection(500, err, socket)
})
} else {
return finishConnection(socket, noteId, socket.id)
return {
id: note.id,
alias: note.alias,
title: note.title,
owner: note.ownerId,
ownerprofile: note.owner ? models.User.getProfile(note.owner) : null,
permission: note.permission,
lastchangeuser: note.lastchangeuserId,
lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null,
socks: [],
users: {},
tempUsers: {},
createtime: moment(note.createdAt).valueOf(),
updatetime: moment(note.lastchangeAt).valueOf(),
server: new ot.EditorSocketIOServer(note.content, [], note.id, ifMayEdit, operationCallback),
authors: authors,
authorship: note.authorship
}
}
// TODO: test it
function failConnection (code, err, socket) {
logger.error(err)
// clear error socket in queue
clearSocketQueue(connectionSocketQueue, socket)
connectNextSocket()
// emit error info
socket.emit('info', {
code: code
@ -655,7 +589,7 @@ function updateUserData (socket, user) {
}
}
function canEditNote(notePermission, noteOwnerId, currentUserId) {
function canEditNote (notePermission, noteOwnerId, currentUserId) {
switch (notePermission) {
case 'freely':
return true
@ -736,85 +670,137 @@ function updateHistory (userId, note, time) {
if (note.server) history.updateHistory(userId, noteId, note.server.document, time)
}
function getUserPool () {
return users
function getUniqueColorPerNote(noteId, maxAttempt = 10) {
// random color
let color = randomcolor()
if (!notes[noteId]) return color
const maxrandomcount = maxAttempt
let randomAttemp = 0
let found = false
do {
Object.keys(notes[noteId].users).forEach(userId => {
if (notes[noteId].users[userId].color === color) {
found = true
}
});
if (found) {
color = randomcolor()
randomAttemp++
}
} while (found && randomAttemp < maxrandomcount)
return color
}
function getUserFromUserPool (userId) {
return users[userId]
}
function queueForConnect (socket) {
connectProcessQueue.push(socket.id, async function () {
try {
const noteId = await exports.parseNoteIdFromSocketAsync(socket)
if (!noteId) {
return exports.failConnection(404, 'note id not found', socket)
}
// store noteId in this socket session
socket.noteId = noteId
// initialize user data
// random color
var color = getUniqueColorPerNote(noteId)
// create user data
users[socket.id] = {
id: socket.id,
address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address,
'user-agent': socket.handshake.headers['user-agent'],
color: color,
cursor: null,
login: false,
userid: null,
name: null,
idle: false,
type: null
}
exports.updateUserData(socket, users[socket.id])
try {
if (!isNoteExistsInPool(noteId)) {
const note = await fetchFullNoteAsync(noteId)
if (!note) {
logger.error('note not found')
// emit error info
socket.emit('info', {
code: 404
})
return socket.disconnect(true)
}
getNotePool()[noteId] = makeNewServerNote(note)
}
// if no valid info provided will drop the client
if (!socket || !notes[noteId] || !users[socket.id]) {
if (notes[noteId]) delete notes[noteId]
if (users[socket.id]) delete users[socket.id]
return
}
// check view permission
if (!exports.checkViewPermission(socket.request, notes[noteId])) {
if (notes[noteId]) delete notes[noteId]
if (users[socket.id]) delete users[socket.id]
logger.error('connection forbidden')
// emit error info
socket.emit('info', {
code: 403
})
return socket.disconnect(true)
}
let note = notes[noteId]
let user = users[socket.id]
// update user color to author color
if (note.authors[user.userid]) {
user.color = users[socket.id].color = note.authors[user.userid].color
}
note.users[socket.id] = user
note.socks.push(socket)
note.server.addClient(socket)
note.server.setName(socket, user.name)
note.server.setColor(socket, user.color)
function getNotePool () {
return notes
}
// update user note history
exports.updateHistory(user.userid, note)
function getNoteFromNotePool (noteId) {
return notes[noteId]
}
exports.emitOnlineUsers(socket)
exports.emitRefresh(socket)
// TODO: test it
function connection (socket) {
if (realtime.maintenance) return
exports.parseNoteIdFromSocket(socket, function (err, noteId) {
if (err) {
const socketClient = new RealtimeClientConnection(socket)
socketClient.registerEventHandler()
if (config.debug) {
let noteId = socket.noteId
logger.info('SERVER connected a client to [' + noteId + ']:')
logger.info(JSON.stringify(user))
// logger.info(notes);
getStatus(function (data) {
logger.info(JSON.stringify(data))
})
}
} catch (err) {
logger.error(err)
// emit error info
socket.emit('info', {
code: 500
})
return socket.disconnect(true)
}
} catch (err) {
return exports.failConnection(500, err, socket)
}
if (!noteId) {
return exports.failConnection(404, 'note id not found', socket)
}
if (isDuplicatedInSocketQueue(connectionSocketQueue, socket)) return
// store noteId in this socket session
socket.noteId = noteId
// initialize user data
// random color
var color = randomcolor()
// make sure color not duplicated or reach max random count
if (notes[noteId]) {
var randomcount = 0
var maxrandomcount = 10
var found = false
do {
Object.keys(notes[noteId].users).forEach(function (userId) {
if (notes[noteId].users[userId].color === color) {
found = true
}
})
if (found) {
color = randomcolor()
randomcount++
}
} while (found && randomcount < maxrandomcount)
}
// create user data
users[socket.id] = {
id: socket.id,
address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address,
'user-agent': socket.handshake.headers['user-agent'],
color: color,
cursor: null,
login: false,
userid: null,
name: null,
idle: false,
type: null
}
exports.updateUserData(socket, users[socket.id])
// start connection
connectionSocketQueue.push(socket)
exports.startConnection(socket)
})
}
const socketClient = new RealtimeClientConnection(socket)
socketClient.registerEventHandler()
function connection (socket) {
if (realtime.maintenance) return
queueForConnect(socket)
}
// TODO: test it
function terminate () {
disconnectProcessQueue.stop()
connectProcessQueue.stop()
updateDirtyNoteJob.stop()
}
@ -823,25 +809,31 @@ exports.extractNoteIdFromSocket = extractNoteIdFromSocket
exports.parseNoteIdFromSocket = parseNoteIdFromSocket
exports.updateNote = updateNote
exports.failConnection = failConnection
exports.isDuplicatedInSocketQueue = isDuplicatedInSocketQueue
exports.updateUserData = updateUserData
exports.startConnection = startConnection
exports.emitRefresh = emitRefresh
exports.emitUserStatus = emitUserStatus
exports.emitOnlineUsers = emitOnlineUsers
exports.checkViewPermission = checkViewPermission
exports.getNoteFromNotePool = getNoteFromNotePool
exports.getUserFromUserPool = getUserFromUserPool
exports.buildUserOutData = buildUserOutData
exports.getNotePool = getNotePool
exports.emitCheck = emitCheck
exports.disconnectSocketOnNote = disconnectSocketOnNote
exports.queueForDisconnect = queueForDisconnect
exports.terminate = terminate
exports.getUserPool = getUserPool
exports.updateHistory = updateHistory
exports.ifMayEdit = ifMayEdit
exports.parseNoteIdFromSocketAsync = parseNoteIdFromSocketAsync
exports.disconnectProcessQueue = disconnectProcessQueue
exports.notes = notes
exports.users = users
exports.getUserPool = getUserPool
exports.notes = notes
exports.getNotePool = getNotePool
exports.getNotePoolSize = getNotePoolSize
exports.isNoteExistsInPool = isNoteExistsInPool
exports.addNote = addNote
exports.getNoteFromNotePool = getNoteFromNotePool
exports.deleteNoteFromPool = deleteNoteFromPool
exports.deleteAllNoteFromPool = deleteAllNoteFromPool
exports.saveRevisionJob = saveRevisionJob

View File

@ -22,7 +22,7 @@ describe('ProcessQueue', function () {
})
it('should not accept more than maximum task', () => {
const queue = new ProcessQueue(2)
const queue = new ProcessQueue({ maximumLength: 2 })
const task = {
id: 1,
processingFunc: async () => {
@ -36,7 +36,7 @@ describe('ProcessQueue', function () {
it('should run task every interval', (done) => {
const runningClock = []
const queue = new ProcessQueue(2)
const queue = new ProcessQueue({ maximumLength: 2 })
const task = async () => {
runningClock.push(clock.now)
}
@ -62,7 +62,7 @@ describe('ProcessQueue', function () {
})
it('should not crash when repeat stop queue', () => {
const queue = new ProcessQueue(2, 10)
const queue = new ProcessQueue({ maximumLength: 2, triggerTimeInterval: 10 })
try {
queue.stop()
queue.stop()
@ -74,7 +74,7 @@ describe('ProcessQueue', function () {
})
it('should run process when queue is empty', (done) => {
const queue = new ProcessQueue(2, 100)
const queue = new ProcessQueue({ maximumLength: 2, triggerTimeInterval: 100 })
const processSpy = sinon.spy(queue, 'process')
queue.start()
clock.tick(100)
@ -85,7 +85,7 @@ describe('ProcessQueue', function () {
})
it('should run process although error occurred', (done) => {
const queue = new ProcessQueue(2, 100)
const queue = new ProcessQueue({ maximumLength: 2, triggerTimeInterval: 100 })
const failedTask = sinon.spy(async () => {
throw new Error('error')
})
@ -107,7 +107,7 @@ describe('ProcessQueue', function () {
})
it('should ignore trigger when event not complete', (done) => {
const queue = new ProcessQueue(2, 10)
const queue = new ProcessQueue({ maximumLength: 2, triggerTimeInterval: 10 })
const processSpy = sinon.spy(queue, 'process')
const longTask = async () => {
return new Promise((resolve) => {

View File

@ -0,0 +1,193 @@
/* eslint-env node, mocha */
'use strict'
const assert = require('assert')
const mock = require('mock-require')
const sinon = require('sinon')
const { createFakeLogger } = require('../testDoubles/loggerFake')
const { removeLibModuleCache, makeMockSocket } = require('./utils')
const realtimeJobStub = require('../testDoubles/realtimeJobStub')
describe('realtime#connection', function () {
describe('connection', function () {
let realtime
let modelStub
beforeEach(() => {
removeLibModuleCache()
modelStub = {
Note: {
findOne: sinon.stub()
},
User: {},
Author: {}
}
mock('../../lib/logger', createFakeLogger())
mock('../../lib/history', {})
mock('../../lib/models', modelStub)
mock('../../lib/config', {})
mock('../../lib/realtimeUpdateDirtyNoteJob', realtimeJobStub)
mock('../../lib/realtimeCleanDanglingUserJob', realtimeJobStub)
mock('../../lib/realtimeSaveRevisionJob', realtimeJobStub)
mock('../../lib/ot', require('../testDoubles/otFake'))
realtime = require('../../lib/realtime')
})
afterEach(() => {
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', (done) => {
const mockSocket = makeMockSocket()
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocketAsync').callsFake(async (socket) => {
/* eslint-disable-next-line */
throw 'error'
})
const failConnectionSpy = sinon.stub(realtime, 'failConnection')
realtime.connection(mockSocket)
setTimeout(() => {
assert(parseNoteIdFromSocketSpy.called)
assert(failConnectionSpy.calledOnce)
assert.deepStrictEqual(failConnectionSpy.lastCall.args, [500, 'error', mockSocket])
done()
}, 50)
})
it('should failed when noteId not exists', (done) => {
const mockSocket = makeMockSocket()
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocketAsync').callsFake(async (socket) => {
return null
})
const failConnectionSpy = sinon.stub(realtime, 'failConnection')
realtime.connection(mockSocket)
setTimeout(() => {
assert(parseNoteIdFromSocketSpy.called)
assert(failConnectionSpy.calledOnce)
assert.deepStrictEqual(failConnectionSpy.lastCall.args, [404, 'note id not found', mockSocket])
done()
}, 50)
})
})
it('should success connect', function (done) {
const mockSocket = makeMockSocket()
const noteId = 'note123'
realtime.maintenance = false
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocketAsync').callsFake(async (socket) => {
return noteId
})
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
realtime.connection(mockSocket)
setTimeout(() => {
assert.ok(parseNoteIdFromSocketSpy.calledOnce)
assert(updateUserDataStub.calledOnce)
done()
}, 50)
})
describe('flow', function () {
it('should establish connection', function (done) {
const noteId = 'note123'
const mockSocket = makeMockSocket(null, {
noteId: noteId
})
mockSocket.request.user.logged_in = true
mockSocket.request.user.id = 'user1'
mockSocket.noteId = noteId
realtime.maintenance = false
sinon.stub(realtime, 'parseNoteIdFromSocketAsync').callsFake(async (socket) => {
return noteId
})
const updateHistoryStub = sinon.stub(realtime, 'updateHistory')
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
const emitRefreshStub = sinon.stub(realtime, 'emitRefresh')
const failConnectionSpy = sinon.spy(realtime, 'failConnection')
let note = {
id: noteId,
authors: [
{
userId: 'user1',
color: 'red',
user: {
id: 'user1',
name: 'Alice'
}
},
{
userId: 'user2',
color: 'blue',
user: {
id: 'user2',
name: 'Bob'
}
}
]
}
modelStub.Note.findOne.returns(Promise.resolve(note))
modelStub.User.getProfile = sinon.stub().callsFake((user) => {
return user
})
sinon.stub(realtime, 'checkViewPermission').returns(true)
realtime.connection(mockSocket)
setTimeout(() => {
assert(modelStub.Note.findOne.calledOnce)
assert.deepStrictEqual(modelStub.Note.findOne.lastCall.args[0].include, [
{
model: modelStub.User,
as: 'owner'
}, {
model: modelStub.User,
as: 'lastchangeuser'
}, {
model: modelStub.Author,
as: 'authors',
include: [{
model: modelStub.User,
as: 'user'
}]
}
])
assert(modelStub.Note.findOne.lastCall.args[0].where.id === noteId)
assert(updateHistoryStub.calledOnce)
assert(emitOnlineUsersStub.calledOnce)
assert(emitRefreshStub.calledOnce)
assert(failConnectionSpy.callCount === 0)
assert(realtime.getNotePool()[noteId].id === noteId)
assert(realtime.getNotePool()[noteId].socks.length === 1)
assert.deepStrictEqual(realtime.getNotePool()[noteId].authors, {
user1: {
userid: 'user1', color: 'red', photo: undefined, name: 'Alice'
},
user2: {
userid: 'user2', color: 'blue', photo: undefined, name: 'Bob'
}
})
assert(Object.keys(realtime.getNotePool()[noteId].users).length === 1)
done()
}, 50)
})
})
})
})

View File

@ -4,13 +4,14 @@
const assert = require('assert')
const mock = require('mock-require')
const sinon = require('sinon')
const { removeModuleFromRequireCache, makeMockSocket } = require('./utils')
const { removeModuleFromRequireCache, makeMockSocket, removeLibModuleCache } = require('./utils')
describe('realtime#update note is dirty timer', function () {
let realtime
let clock
beforeEach(() => {
removeLibModuleCache()
clock = sinon.useFakeTimers({
toFake: ['setInterval']
})
@ -69,7 +70,7 @@ describe('realtime#update note is dirty timer', function () {
setTimeout(() => {
assert(note2.server.isDirty === false)
done()
}, 5)
}, 10)
})
it('should not do anything when note missing', function (done) {

View File

@ -37,96 +37,6 @@ function removeModuleFromRequireCache (modulePath) {
}
describe('realtime', function () {
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('checkViewPermission', function () {
// role -> guest, loggedInUser, loggedInOwner
const viewPermission = {

View File

@ -9,6 +9,27 @@ const { makeMockSocket, removeModuleFromRequireCache } = require('./utils')
describe('realtime#socket event', function () {
const noteId = 'note123'
const note = {
id: noteId,
authors: [
{
userId: 'user1',
color: 'red',
user: {
id: 'user1',
name: 'Alice'
}
},
{
userId: 'user2',
color: 'blue',
user: {
id: 'user2',
name: 'Bob'
}
}
]
}
let realtime
let clientSocket
let modelsMock
@ -16,7 +37,7 @@ describe('realtime#socket event', function () {
let configMock
let clock
beforeEach(function () {
beforeEach(function (done) {
clock = sinon.useFakeTimers({
toFake: ['setInterval']
})
@ -25,9 +46,14 @@ describe('realtime#socket event', function () {
Note: {
parseNoteTitle: (data) => (data),
destroy: sinon.stub().returns(Promise.resolve(1)),
update: sinon.stub().returns(Promise.resolve(1))
}
update: sinon.stub().returns(Promise.resolve(1)),
findOne: sinon.stub().returns(Promise.resolve(note))
},
User: {}
}
modelsMock.User.getProfile = sinon.stub().callsFake((user) => {
return user
})
configMock = {
fullversion: '1.5.0',
minimumCompatibleVersion: '1.0.0'
@ -41,27 +67,50 @@ describe('realtime#socket event', function () {
mock('../../lib/history', {})
mock('../../lib/models', modelsMock)
mock('../../lib/config', configMock)
mock('../../lib/ot', require('../testDoubles/otFake'))
realtime = require('../../lib/realtime')
// get all socket event handler
clientSocket = makeMockSocket()
clientSocket = makeMockSocket(null, {
noteId: noteId
})
clientSocket.request.user.logged_in = true
clientSocket.request.user.id = 'user1'
// clientSocket.noteId = noteId
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)
})
realtime.io = (function () {
const roomMap = new Map()
return {
to: function (roomId) {
if (!roomMap.has(roomId)) {
roomMap.set(roomId, {
emit: sinon.stub()
})
}
return roomMap.get(roomId)
}
}
}())
const wrappedFuncs = []
wrappedFuncs.push(sinon.stub(realtime, 'failConnection'))
wrappedFuncs.push(sinon.stub(realtime, 'updateUserData'))
wrappedFuncs.push(sinon.stub(realtime, 'startConnection'))
wrappedFuncs.push(sinon.stub(realtime, 'emitOnlineUsers'))
wrappedFuncs.push(sinon.stub(realtime, 'parseNoteIdFromSocketAsync').returns(Promise.resolve(noteId)))
wrappedFuncs.push(sinon.stub(realtime, 'updateHistory'))
wrappedFuncs.push(sinon.stub(realtime, 'emitRefresh'))
realtime.connection(clientSocket)
wrappedFuncs.forEach((wrappedFunc) => {
wrappedFunc.restore()
})
setTimeout(() => {
wrappedFuncs.forEach((wrappedFunc) => {
wrappedFunc.restore()
})
done()
}, 50)
})
afterEach(function () {
@ -70,6 +119,7 @@ describe('realtime#socket event', function () {
mock.stopAll()
sinon.restore()
clock.restore()
clientSocket = null
})
describe('refresh', function () {
@ -126,7 +176,7 @@ describe('realtime#socket event', function () {
it('should not call emitUserStatus when note not exists', () => {
const userStatusFunc = eventFuncMap.get('user status')
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
realtime.notes = {}
realtime.deleteAllNoteFromPool()
realtime.users[clientSocket.id] = {}
const userData = {
idle: true,
@ -232,6 +282,7 @@ describe('realtime#socket event', function () {
it('should not return user list when note not exists', function () {
const onlineUsersFunc = eventFuncMap.get('online users')
realtime.deleteAllNoteFromPool()
onlineUsersFunc()
assert(clientSocket.emit.called === false)
})
@ -256,12 +307,13 @@ describe('realtime#socket event', function () {
const userChangedFunc = eventFuncMap.get('user changed')
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
realtime.deleteAllNoteFromPool()
userChangedFunc()
assert(updateUserDataStub.called === false)
assert(emitOnlineUsersStub.called === false)
})
it('should direct return when note not exists', function () {
it('should direct return when note\'s users not exists', function () {
const userChangedFunc = eventFuncMap.get('user changed')
realtime.notes[noteId] = {
users: {}
@ -414,29 +466,28 @@ describe('realtime#socket event', function () {
}
}
realtime.notes[noteId] = {
realtime.deleteAllNoteFromPool()
realtime.addNote({
id: noteId,
owner: ownerId
}
realtime.io = {
to: function () {
return {
emit: sinon.stub()
}
}
}
})
checkViewPermissionSpy = sinon.spy(realtime, 'checkViewPermission')
permissionFunc = eventFuncMap.get('permission')
})
it('should disconnect when lose view permission', function (done) {
realtime.notes[noteId].permission = 'editable'
realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
realtime.getNoteFromNotePool(noteId).permission = 'editable'
realtime.getNoteFromNotePool(noteId).socks = [clientSocket, undefined, otherClient]
permissionFunc('private')
setTimeout(() => {
// should change note permission to private
assert(modelsMock.Note.update.calledOnce)
assert(modelsMock.Note.update.lastCall.args[0].permission === 'private')
assert(modelsMock.Note.update.lastCall.args[1].where.id === noteId)
// should check all connected client
assert(checkViewPermissionSpy.callCount === 2)
assert(otherClient.emit.calledOnce)
assert(otherClient.disconnect.calledOnce)

View File

@ -0,0 +1,18 @@
'use strict'
const sinon = require('sinon')
class EditorSocketIOServerFake {
constructor () {
this.addClient = sinon.stub()
this.onOperation = sinon.stub()
this.onGetOperations = sinon.stub()
this.updateSelection = sinon.stub()
this.setName = sinon.stub()
this.setColor = sinon.stub()
this.getClient = sinon.stub()
this.onDisconnect = sinon.stub()
}
}
exports.EditorSocketIOServer = EditorSocketIOServerFake