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)
|
||||
setTimeout(() => {
|
||||
process.exit(1)
|
||||
}, 5000)
|
||||
}
|
||||
process.on('SIGINT', handleTermSignals)
|
||||
process.on('SIGTERM', handleTermSignals)
|
||||
|
|
|
@ -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)
|
||||
|
|
456
lib/realtime.js
456
lib/realtime.js
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 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) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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