mirror of https://github.com/status-im/codimd.git
refactor(realtime): connection flow to queue
Signed-off-by: BoHong Li <a60814billy@gmail.com>
This commit is contained in:
parent
0b03b8e9ba
commit
17e82c11c9
3
app.js
3
app.js
|
@ -299,6 +299,9 @@ function handleTermSignals () {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
|
setTimeout(() => {
|
||||||
|
process.exit(1)
|
||||||
|
}, 5000)
|
||||||
}
|
}
|
||||||
process.on('SIGINT', handleTermSignals)
|
process.on('SIGINT', handleTermSignals)
|
||||||
process.on('SIGTERM', handleTermSignals)
|
process.on('SIGTERM', handleTermSignals)
|
||||||
|
|
|
@ -7,11 +7,20 @@ const EventEmitter = require('events').EventEmitter
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const QueueEvent = {
|
const QueueEvent = {
|
||||||
Tick: 'Tick'
|
Tick: 'Tick',
|
||||||
|
Push: 'Push',
|
||||||
|
Finish: 'Finish'
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProcessQueue extends EventEmitter {
|
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()
|
super()
|
||||||
this.max = maximumLength
|
this.max = maximumLength
|
||||||
this.triggerTime = triggerTimeInterval
|
this.triggerTime = triggerTimeInterval
|
||||||
|
@ -19,12 +28,20 @@ class ProcessQueue extends EventEmitter {
|
||||||
this.queue = []
|
this.queue = []
|
||||||
this.lock = false
|
this.lock = false
|
||||||
|
|
||||||
this.on(QueueEvent.Tick, () => {
|
this.on(QueueEvent.Tick, this.onEventProcessFunc.bind(this))
|
||||||
if (this.lock) return
|
if (proactiveMode) {
|
||||||
this.lock = true
|
this.on(QueueEvent.Push, this.onEventProcessFunc.bind(this))
|
||||||
setImmediate(() => {
|
}
|
||||||
this.process()
|
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.taskMap.set(id, true)
|
||||||
this.queue.push(task)
|
this.queue.push(task)
|
||||||
this.start()
|
this.start()
|
||||||
this.emit(QueueEvent.Tick)
|
this.emit(QueueEvent.Push)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +96,7 @@ class ProcessQueue extends EventEmitter {
|
||||||
const finishTask = () => {
|
const finishTask = () => {
|
||||||
this.lock = false
|
this.lock = false
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
this.emit(QueueEvent.Tick)
|
this.emit(QueueEvent.Finish)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
task.processingFunc().then(finishTask).catch(finishTask)
|
task.processingFunc().then(finishTask).catch(finishTask)
|
||||||
|
|
456
lib/realtime.js
456
lib/realtime.js
|
@ -38,7 +38,8 @@ const realtime = {
|
||||||
maintenance: true
|
maintenance: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const disconnectProcessQueue = new ProcessQueue(2000, 500)
|
const connectProcessQueue = new ProcessQueue({})
|
||||||
|
const disconnectProcessQueue = new ProcessQueue({})
|
||||||
const updateDirtyNoteJob = new UpdateDirtyNoteJob(realtime)
|
const updateDirtyNoteJob = new UpdateDirtyNoteJob(realtime)
|
||||||
const cleanDanglingUserJob = new CleanDanglingUserJob(realtime)
|
const cleanDanglingUserJob = new CleanDanglingUserJob(realtime)
|
||||||
const saveRevisionJob = new SaveRevisionJob(realtime)
|
const saveRevisionJob = new SaveRevisionJob(realtime)
|
||||||
|
@ -97,6 +98,46 @@ function emitCheck (note) {
|
||||||
var users = {}
|
var users = {}
|
||||||
var notes = {}
|
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()
|
disconnectProcessQueue.start()
|
||||||
updateDirtyNoteJob.start()
|
updateDirtyNoteJob.start()
|
||||||
cleanDanglingUserJob.start()
|
cleanDanglingUserJob.start()
|
||||||
|
@ -265,7 +306,8 @@ function getStatus (callback) {
|
||||||
function isReady () {
|
function isReady () {
|
||||||
return realtime.io &&
|
return realtime.io &&
|
||||||
Object.keys(notes).length === 0 && Object.keys(users).length === 0 &&
|
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
|
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
|
// TODO: test it
|
||||||
function emitOnlineUsers (socket) {
|
function emitOnlineUsers (socket) {
|
||||||
var noteId = socket.noteId
|
var noteId = socket.noteId
|
||||||
|
@ -374,48 +425,6 @@ function emitRefresh (socket) {
|
||||||
socket.emit('refresh', out)
|
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) {
|
function checkViewPermission (req, note) {
|
||||||
if (note.permission === 'private') {
|
if (note.permission === 'private') {
|
||||||
if (req.user && req.user.logged_in && req.user.id === note.owner) {
|
if (req.user && req.user.logged_in && req.user.id === note.owner) {
|
||||||
|
@ -435,65 +444,14 @@ function checkViewPermission (req, note) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var isConnectionBusy = false
|
var isConnectionBusy = false
|
||||||
var connectionSocketQueue = []
|
|
||||||
|
|
||||||
// TODO: test it
|
// TODO: test it
|
||||||
function finishConnection (socket, noteId, socketId) {
|
async function fetchFullNoteAsync (noteId) {
|
||||||
// if no valid info provided will drop the client
|
return models.Note.findOne({
|
||||||
if (!socket || !notes[noteId] || !users[socketId]) {
|
where: {
|
||||||
return interruptConnection(socket, noteId, socketId)
|
id: noteId
|
||||||
}
|
},
|
||||||
// check view permission
|
include: [{
|
||||||
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 = [{
|
|
||||||
model: models.User,
|
model: models.User,
|
||||||
as: 'owner'
|
as: 'owner'
|
||||||
}, {
|
}, {
|
||||||
|
@ -507,75 +465,51 @@ function startConnection (socket) {
|
||||||
as: 'user'
|
as: 'user'
|
||||||
}]
|
}]
|
||||||
}]
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
models.Note.findOne({
|
function buildAuthorProfilesFromNote (noteAuthors) {
|
||||||
where: {
|
const authors = {}
|
||||||
id: noteId
|
noteAuthors.forEach((author) => {
|
||||||
},
|
const profile = models.User.getProfile(author.user)
|
||||||
include: include
|
if (profile) {
|
||||||
}).then(function (note) {
|
authors[author.userId] = {
|
||||||
if (!note) {
|
userid: author.userId,
|
||||||
return failConnection(404, 'note not found', socket)
|
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
|
function makeNewServerNote (note) {
|
||||||
var lastchangeuserprofile = note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null
|
const authors = buildAuthorProfilesFromNote(note.authors)
|
||||||
|
|
||||||
var body = note.content
|
return {
|
||||||
var createtime = note.createdAt
|
id: note.id,
|
||||||
var updatetime = note.lastchangeAt
|
alias: note.alias,
|
||||||
var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback)
|
title: note.title,
|
||||||
|
owner: note.ownerId,
|
||||||
var authors = {}
|
ownerprofile: note.owner ? models.User.getProfile(note.owner) : null,
|
||||||
for (var i = 0; i < note.authors.length; i++) {
|
permission: note.permission,
|
||||||
var author = note.authors[i]
|
lastchangeuser: note.lastchangeuserId,
|
||||||
var profile = models.User.getProfile(author.user)
|
lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null,
|
||||||
if (profile) {
|
socks: [],
|
||||||
authors[author.userId] = {
|
users: {},
|
||||||
userid: author.userId,
|
tempUsers: {},
|
||||||
color: author.color,
|
createtime: moment(note.createdAt).valueOf(),
|
||||||
photo: profile.photo,
|
updatetime: moment(note.lastchangeAt).valueOf(),
|
||||||
name: profile.name
|
server: new ot.EditorSocketIOServer(note.content, [], note.id, ifMayEdit, operationCallback),
|
||||||
}
|
authors: authors,
|
||||||
}
|
authorship: note.authorship
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: test it
|
// TODO: test it
|
||||||
function failConnection (code, err, socket) {
|
function failConnection (code, err, socket) {
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
// clear error socket in queue
|
|
||||||
clearSocketQueue(connectionSocketQueue, socket)
|
|
||||||
connectNextSocket()
|
|
||||||
// emit error info
|
// emit error info
|
||||||
socket.emit('info', {
|
socket.emit('info', {
|
||||||
code: code
|
code: code
|
||||||
|
@ -655,7 +589,7 @@ function updateUserData (socket, user) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function canEditNote(notePermission, noteOwnerId, currentUserId) {
|
function canEditNote (notePermission, noteOwnerId, currentUserId) {
|
||||||
switch (notePermission) {
|
switch (notePermission) {
|
||||||
case 'freely':
|
case 'freely':
|
||||||
return true
|
return true
|
||||||
|
@ -736,85 +670,137 @@ function updateHistory (userId, note, time) {
|
||||||
if (note.server) history.updateHistory(userId, noteId, note.server.document, time)
|
if (note.server) history.updateHistory(userId, noteId, note.server.document, time)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserPool () {
|
function getUniqueColorPerNote(noteId, maxAttempt = 10) {
|
||||||
return users
|
// 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) {
|
function queueForConnect (socket) {
|
||||||
return users[userId]
|
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 () {
|
// update user note history
|
||||||
return notes
|
exports.updateHistory(user.userid, note)
|
||||||
}
|
|
||||||
|
|
||||||
function getNoteFromNotePool (noteId) {
|
exports.emitOnlineUsers(socket)
|
||||||
return notes[noteId]
|
exports.emitRefresh(socket)
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: test it
|
const socketClient = new RealtimeClientConnection(socket)
|
||||||
function connection (socket) {
|
socketClient.registerEventHandler()
|
||||||
if (realtime.maintenance) return
|
|
||||||
exports.parseNoteIdFromSocket(socket, function (err, noteId) {
|
if (config.debug) {
|
||||||
if (err) {
|
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)
|
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)
|
function connection (socket) {
|
||||||
socketClient.registerEventHandler()
|
if (realtime.maintenance) return
|
||||||
|
queueForConnect(socket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: test it
|
// TODO: test it
|
||||||
function terminate () {
|
function terminate () {
|
||||||
disconnectProcessQueue.stop()
|
disconnectProcessQueue.stop()
|
||||||
|
connectProcessQueue.stop()
|
||||||
updateDirtyNoteJob.stop()
|
updateDirtyNoteJob.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -823,25 +809,31 @@ exports.extractNoteIdFromSocket = extractNoteIdFromSocket
|
||||||
exports.parseNoteIdFromSocket = parseNoteIdFromSocket
|
exports.parseNoteIdFromSocket = parseNoteIdFromSocket
|
||||||
exports.updateNote = updateNote
|
exports.updateNote = updateNote
|
||||||
exports.failConnection = failConnection
|
exports.failConnection = failConnection
|
||||||
exports.isDuplicatedInSocketQueue = isDuplicatedInSocketQueue
|
|
||||||
exports.updateUserData = updateUserData
|
exports.updateUserData = updateUserData
|
||||||
exports.startConnection = startConnection
|
|
||||||
exports.emitRefresh = emitRefresh
|
exports.emitRefresh = emitRefresh
|
||||||
exports.emitUserStatus = emitUserStatus
|
exports.emitUserStatus = emitUserStatus
|
||||||
exports.emitOnlineUsers = emitOnlineUsers
|
exports.emitOnlineUsers = emitOnlineUsers
|
||||||
exports.checkViewPermission = checkViewPermission
|
exports.checkViewPermission = checkViewPermission
|
||||||
exports.getNoteFromNotePool = getNoteFromNotePool
|
|
||||||
exports.getUserFromUserPool = getUserFromUserPool
|
exports.getUserFromUserPool = getUserFromUserPool
|
||||||
exports.buildUserOutData = buildUserOutData
|
exports.buildUserOutData = buildUserOutData
|
||||||
exports.getNotePool = getNotePool
|
|
||||||
exports.emitCheck = emitCheck
|
exports.emitCheck = emitCheck
|
||||||
exports.disconnectSocketOnNote = disconnectSocketOnNote
|
exports.disconnectSocketOnNote = disconnectSocketOnNote
|
||||||
exports.queueForDisconnect = queueForDisconnect
|
exports.queueForDisconnect = queueForDisconnect
|
||||||
exports.terminate = terminate
|
exports.terminate = terminate
|
||||||
exports.getUserPool = getUserPool
|
|
||||||
exports.updateHistory = updateHistory
|
exports.updateHistory = updateHistory
|
||||||
exports.ifMayEdit = ifMayEdit
|
exports.ifMayEdit = ifMayEdit
|
||||||
|
exports.parseNoteIdFromSocketAsync = parseNoteIdFromSocketAsync
|
||||||
exports.disconnectProcessQueue = disconnectProcessQueue
|
exports.disconnectProcessQueue = disconnectProcessQueue
|
||||||
exports.notes = notes
|
|
||||||
exports.users = users
|
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
|
exports.saveRevisionJob = saveRevisionJob
|
||||||
|
|
|
@ -22,7 +22,7 @@ describe('ProcessQueue', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not accept more than maximum task', () => {
|
it('should not accept more than maximum task', () => {
|
||||||
const queue = new ProcessQueue(2)
|
const queue = new ProcessQueue({ maximumLength: 2 })
|
||||||
const task = {
|
const task = {
|
||||||
id: 1,
|
id: 1,
|
||||||
processingFunc: async () => {
|
processingFunc: async () => {
|
||||||
|
@ -36,7 +36,7 @@ describe('ProcessQueue', function () {
|
||||||
|
|
||||||
it('should run task every interval', (done) => {
|
it('should run task every interval', (done) => {
|
||||||
const runningClock = []
|
const runningClock = []
|
||||||
const queue = new ProcessQueue(2)
|
const queue = new ProcessQueue({ maximumLength: 2 })
|
||||||
const task = async () => {
|
const task = async () => {
|
||||||
runningClock.push(clock.now)
|
runningClock.push(clock.now)
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ describe('ProcessQueue', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not crash when repeat stop queue', () => {
|
it('should not crash when repeat stop queue', () => {
|
||||||
const queue = new ProcessQueue(2, 10)
|
const queue = new ProcessQueue({ maximumLength: 2, triggerTimeInterval: 10 })
|
||||||
try {
|
try {
|
||||||
queue.stop()
|
queue.stop()
|
||||||
queue.stop()
|
queue.stop()
|
||||||
|
@ -74,7 +74,7 @@ describe('ProcessQueue', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should run process when queue is empty', (done) => {
|
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')
|
const processSpy = sinon.spy(queue, 'process')
|
||||||
queue.start()
|
queue.start()
|
||||||
clock.tick(100)
|
clock.tick(100)
|
||||||
|
@ -85,7 +85,7 @@ describe('ProcessQueue', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should run process although error occurred', (done) => {
|
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 () => {
|
const failedTask = sinon.spy(async () => {
|
||||||
throw new Error('error')
|
throw new Error('error')
|
||||||
})
|
})
|
||||||
|
@ -107,7 +107,7 @@ describe('ProcessQueue', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ignore trigger when event not complete', (done) => {
|
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 processSpy = sinon.spy(queue, 'process')
|
||||||
const longTask = async () => {
|
const longTask = async () => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -4,13 +4,14 @@
|
||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const mock = require('mock-require')
|
const mock = require('mock-require')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const { removeModuleFromRequireCache, makeMockSocket } = require('./utils')
|
const { removeModuleFromRequireCache, makeMockSocket, removeLibModuleCache } = require('./utils')
|
||||||
|
|
||||||
describe('realtime#update note is dirty timer', function () {
|
describe('realtime#update note is dirty timer', function () {
|
||||||
let realtime
|
let realtime
|
||||||
let clock
|
let clock
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
removeLibModuleCache()
|
||||||
clock = sinon.useFakeTimers({
|
clock = sinon.useFakeTimers({
|
||||||
toFake: ['setInterval']
|
toFake: ['setInterval']
|
||||||
})
|
})
|
||||||
|
@ -69,7 +70,7 @@ describe('realtime#update note is dirty timer', function () {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
assert(note2.server.isDirty === false)
|
assert(note2.server.isDirty === false)
|
||||||
done()
|
done()
|
||||||
}, 5)
|
}, 10)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not do anything when note missing', function (done) {
|
it('should not do anything when note missing', function (done) {
|
||||||
|
|
|
@ -37,96 +37,6 @@ function removeModuleFromRequireCache (modulePath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('realtime', function () {
|
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 () {
|
describe('checkViewPermission', function () {
|
||||||
// role -> guest, loggedInUser, loggedInOwner
|
// role -> guest, loggedInUser, loggedInOwner
|
||||||
const viewPermission = {
|
const viewPermission = {
|
||||||
|
|
|
@ -9,6 +9,27 @@ const { makeMockSocket, removeModuleFromRequireCache } = require('./utils')
|
||||||
|
|
||||||
describe('realtime#socket event', function () {
|
describe('realtime#socket event', function () {
|
||||||
const noteId = 'note123'
|
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 realtime
|
||||||
let clientSocket
|
let clientSocket
|
||||||
let modelsMock
|
let modelsMock
|
||||||
|
@ -16,7 +37,7 @@ describe('realtime#socket event', function () {
|
||||||
let configMock
|
let configMock
|
||||||
let clock
|
let clock
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function (done) {
|
||||||
clock = sinon.useFakeTimers({
|
clock = sinon.useFakeTimers({
|
||||||
toFake: ['setInterval']
|
toFake: ['setInterval']
|
||||||
})
|
})
|
||||||
|
@ -25,9 +46,14 @@ describe('realtime#socket event', function () {
|
||||||
Note: {
|
Note: {
|
||||||
parseNoteTitle: (data) => (data),
|
parseNoteTitle: (data) => (data),
|
||||||
destroy: sinon.stub().returns(Promise.resolve(1)),
|
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 = {
|
configMock = {
|
||||||
fullversion: '1.5.0',
|
fullversion: '1.5.0',
|
||||||
minimumCompatibleVersion: '1.0.0'
|
minimumCompatibleVersion: '1.0.0'
|
||||||
|
@ -41,27 +67,50 @@ describe('realtime#socket event', function () {
|
||||||
mock('../../lib/history', {})
|
mock('../../lib/history', {})
|
||||||
mock('../../lib/models', modelsMock)
|
mock('../../lib/models', modelsMock)
|
||||||
mock('../../lib/config', configMock)
|
mock('../../lib/config', configMock)
|
||||||
|
mock('../../lib/ot', require('../testDoubles/otFake'))
|
||||||
realtime = require('../../lib/realtime')
|
realtime = require('../../lib/realtime')
|
||||||
|
|
||||||
// get all socket event handler
|
// 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) {
|
clientSocket.on = function (event, func) {
|
||||||
eventFuncMap.set(event, func)
|
eventFuncMap.set(event, func)
|
||||||
}
|
}
|
||||||
realtime.maintenance = false
|
realtime.maintenance = false
|
||||||
sinon.stub(realtime, 'parseNoteIdFromSocket').callsFake((socket, callback) => {
|
|
||||||
/* eslint-disable-next-line */
|
realtime.io = (function () {
|
||||||
callback(null, noteId)
|
const roomMap = new Map()
|
||||||
})
|
return {
|
||||||
|
to: function (roomId) {
|
||||||
|
if (!roomMap.has(roomId)) {
|
||||||
|
roomMap.set(roomId, {
|
||||||
|
emit: sinon.stub()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return roomMap.get(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}())
|
||||||
|
|
||||||
const wrappedFuncs = []
|
const wrappedFuncs = []
|
||||||
wrappedFuncs.push(sinon.stub(realtime, 'failConnection'))
|
|
||||||
wrappedFuncs.push(sinon.stub(realtime, 'updateUserData'))
|
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)
|
realtime.connection(clientSocket)
|
||||||
|
|
||||||
wrappedFuncs.forEach((wrappedFunc) => {
|
setTimeout(() => {
|
||||||
wrappedFunc.restore()
|
wrappedFuncs.forEach((wrappedFunc) => {
|
||||||
})
|
wrappedFunc.restore()
|
||||||
|
})
|
||||||
|
done()
|
||||||
|
}, 50)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
@ -70,6 +119,7 @@ describe('realtime#socket event', function () {
|
||||||
mock.stopAll()
|
mock.stopAll()
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
clock.restore()
|
clock.restore()
|
||||||
|
clientSocket = null
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('refresh', function () {
|
describe('refresh', function () {
|
||||||
|
@ -126,7 +176,7 @@ describe('realtime#socket event', function () {
|
||||||
it('should not call emitUserStatus when note not exists', () => {
|
it('should not call emitUserStatus when note not exists', () => {
|
||||||
const userStatusFunc = eventFuncMap.get('user status')
|
const userStatusFunc = eventFuncMap.get('user status')
|
||||||
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
|
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
|
||||||
realtime.notes = {}
|
realtime.deleteAllNoteFromPool()
|
||||||
realtime.users[clientSocket.id] = {}
|
realtime.users[clientSocket.id] = {}
|
||||||
const userData = {
|
const userData = {
|
||||||
idle: true,
|
idle: true,
|
||||||
|
@ -232,6 +282,7 @@ describe('realtime#socket event', function () {
|
||||||
|
|
||||||
it('should not return user list when note not exists', function () {
|
it('should not return user list when note not exists', function () {
|
||||||
const onlineUsersFunc = eventFuncMap.get('online users')
|
const onlineUsersFunc = eventFuncMap.get('online users')
|
||||||
|
realtime.deleteAllNoteFromPool()
|
||||||
onlineUsersFunc()
|
onlineUsersFunc()
|
||||||
assert(clientSocket.emit.called === false)
|
assert(clientSocket.emit.called === false)
|
||||||
})
|
})
|
||||||
|
@ -256,12 +307,13 @@ describe('realtime#socket event', function () {
|
||||||
const userChangedFunc = eventFuncMap.get('user changed')
|
const userChangedFunc = eventFuncMap.get('user changed')
|
||||||
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
|
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
|
||||||
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
|
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
|
||||||
|
realtime.deleteAllNoteFromPool()
|
||||||
userChangedFunc()
|
userChangedFunc()
|
||||||
assert(updateUserDataStub.called === false)
|
assert(updateUserDataStub.called === false)
|
||||||
assert(emitOnlineUsersStub.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')
|
const userChangedFunc = eventFuncMap.get('user changed')
|
||||||
realtime.notes[noteId] = {
|
realtime.notes[noteId] = {
|
||||||
users: {}
|
users: {}
|
||||||
|
@ -414,29 +466,28 @@ describe('realtime#socket event', function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
realtime.notes[noteId] = {
|
realtime.deleteAllNoteFromPool()
|
||||||
|
realtime.addNote({
|
||||||
|
id: noteId,
|
||||||
owner: ownerId
|
owner: ownerId
|
||||||
}
|
})
|
||||||
|
|
||||||
realtime.io = {
|
|
||||||
to: function () {
|
|
||||||
return {
|
|
||||||
emit: sinon.stub()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkViewPermissionSpy = sinon.spy(realtime, 'checkViewPermission')
|
checkViewPermissionSpy = sinon.spy(realtime, 'checkViewPermission')
|
||||||
permissionFunc = eventFuncMap.get('permission')
|
permissionFunc = eventFuncMap.get('permission')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should disconnect when lose view permission', function (done) {
|
it('should disconnect when lose view permission', function (done) {
|
||||||
realtime.notes[noteId].permission = 'editable'
|
realtime.getNoteFromNotePool(noteId).permission = 'editable'
|
||||||
realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
|
realtime.getNoteFromNotePool(noteId).socks = [clientSocket, undefined, otherClient]
|
||||||
|
|
||||||
permissionFunc('private')
|
permissionFunc('private')
|
||||||
|
|
||||||
setTimeout(() => {
|
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(checkViewPermissionSpy.callCount === 2)
|
||||||
assert(otherClient.emit.calledOnce)
|
assert(otherClient.emit.calledOnce)
|
||||||
assert(otherClient.disconnect.calledOnce)
|
assert(otherClient.disconnect.calledOnce)
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue