mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 08:23:36 -05:00
537e97be73
See the docs of OError.tag: https://github.com/overleaf/o-error#long-stack-traces-with-oerrortag (currently at 221dd902e7bfa0ee92de1ea5a3cbf3152c3ceeb4) I am tagging all errors at each async hop. Most of the controller code will only ever see already tagged errors -- or new errors created in our app code. They should have enough info that we do not need to tag them again.
165 lines
6.1 KiB
JavaScript
165 lines
6.1 KiB
JavaScript
/* eslint-disable
|
|
camelcase,
|
|
*/
|
|
const logger = require('logger-sharelatex')
|
|
const metrics = require('metrics-sharelatex')
|
|
const { EventEmitter } = require('events')
|
|
const OError = require('@overleaf/o-error')
|
|
|
|
const IdMap = new Map() // keep track of whether ids are from projects or docs
|
|
const RoomEvents = new EventEmitter() // emits {project,doc}-active and {project,doc}-empty events
|
|
|
|
// Manage socket.io rooms for individual projects and docs
|
|
//
|
|
// The first time someone joins a project or doc we emit a 'project-active' or
|
|
// 'doc-active' event.
|
|
//
|
|
// When the last person leaves a project or doc, we emit 'project-empty' or
|
|
// 'doc-empty' event.
|
|
//
|
|
// The pubsub side is handled by ChannelManager
|
|
|
|
module.exports = {
|
|
joinProject(client, project_id, callback) {
|
|
this.joinEntity(client, 'project', project_id, callback)
|
|
},
|
|
|
|
joinDoc(client, doc_id, callback) {
|
|
this.joinEntity(client, 'doc', doc_id, callback)
|
|
},
|
|
|
|
leaveDoc(client, doc_id) {
|
|
this.leaveEntity(client, 'doc', doc_id)
|
|
},
|
|
|
|
leaveProjectAndDocs(client) {
|
|
// what rooms is this client in? we need to leave them all. socket.io
|
|
// will cause us to leave the rooms, so we only need to manage our
|
|
// channel subscriptions... but it will be safer if we leave them
|
|
// explicitly, and then socket.io will just regard this as a client that
|
|
// has not joined any rooms and do a final disconnection.
|
|
const roomsToLeave = this._roomsClientIsIn(client)
|
|
logger.log({ client: client.id, roomsToLeave }, 'client leaving project')
|
|
for (const id of roomsToLeave) {
|
|
const entity = IdMap.get(id)
|
|
this.leaveEntity(client, entity, id)
|
|
}
|
|
},
|
|
|
|
emitOnCompletion(promiseList, eventName) {
|
|
Promise.all(promiseList)
|
|
.then(() => RoomEvents.emit(eventName))
|
|
.catch((err) => RoomEvents.emit(eventName, err))
|
|
},
|
|
|
|
eventSource() {
|
|
return RoomEvents
|
|
},
|
|
|
|
joinEntity(client, entity, id, callback) {
|
|
const beforeCount = this._clientsInRoom(client, id)
|
|
// client joins room immediately but joinDoc request does not complete
|
|
// until room is subscribed
|
|
client.join(id)
|
|
// is this a new room? if so, subscribe
|
|
if (beforeCount === 0) {
|
|
logger.log({ entity, id }, 'room is now active')
|
|
RoomEvents.once(`${entity}-subscribed-${id}`, function (err) {
|
|
// only allow the client to join when all the relevant channels have subscribed
|
|
if (err) {
|
|
OError.tag(err, 'error joining', { entity, id })
|
|
return callback(err)
|
|
}
|
|
logger.log(
|
|
{ client: client.id, entity, id, beforeCount },
|
|
'client joined new room and subscribed to channel'
|
|
)
|
|
callback(err)
|
|
})
|
|
RoomEvents.emit(`${entity}-active`, id)
|
|
IdMap.set(id, entity)
|
|
// keep track of the number of listeners
|
|
metrics.gauge('room-listeners', RoomEvents.eventNames().length)
|
|
} else {
|
|
logger.log(
|
|
{ client: client.id, entity, id, beforeCount },
|
|
'client joined existing room'
|
|
)
|
|
client.join(id)
|
|
callback()
|
|
}
|
|
},
|
|
|
|
leaveEntity(client, entity, id) {
|
|
// Ignore any requests to leave when the client is not actually in the
|
|
// room. This can happen if the client sends spurious leaveDoc requests
|
|
// for old docs after a reconnection.
|
|
// This can now happen all the time, as we skip the join for clients that
|
|
// disconnect before joinProject/joinDoc completed.
|
|
if (!this._clientAlreadyInRoom(client, id)) {
|
|
logger.log(
|
|
{ client: client.id, entity, id },
|
|
'ignoring request from client to leave room it is not in'
|
|
)
|
|
return
|
|
}
|
|
client.leave(id)
|
|
const afterCount = this._clientsInRoom(client, id)
|
|
logger.log(
|
|
{ client: client.id, entity, id, afterCount },
|
|
'client left room'
|
|
)
|
|
// is the room now empty? if so, unsubscribe
|
|
if (!entity) {
|
|
logger.error({ entity: id }, 'unknown entity when leaving with id')
|
|
return
|
|
}
|
|
if (afterCount === 0) {
|
|
logger.log({ entity, id }, 'room is now empty')
|
|
RoomEvents.emit(`${entity}-empty`, id)
|
|
IdMap.delete(id)
|
|
metrics.gauge('room-listeners', RoomEvents.eventNames().length)
|
|
}
|
|
},
|
|
|
|
// internal functions below, these access socket.io rooms data directly and
|
|
// will need updating for socket.io v2
|
|
|
|
// The below code makes some assumptions that are always true for v0
|
|
// - we are using the base namespace '', so room names are '/<ENTITY>'
|
|
// https://github.com/socketio/socket.io/blob/e4d61b1be65ac3313a85da111a46777aa8d4aae3/lib/manager.js#L62
|
|
// https://github.com/socketio/socket.io/blob/e4d61b1be65ac3313a85da111a46777aa8d4aae3/lib/manager.js#L1018
|
|
// - client.namespace is a Namespace
|
|
// https://github.com/socketio/socket.io/blob/e4d61b1be65ac3313a85da111a46777aa8d4aae3/lib/namespace.js#L204
|
|
// https://github.com/socketio/socket.io/blob/e4d61b1be65ac3313a85da111a46777aa8d4aae3/lib/socket.js#L40
|
|
// - client.manager is a Manager
|
|
// https://github.com/socketio/socket.io/blob/e4d61b1be65ac3313a85da111a46777aa8d4aae3/lib/namespace.js#L204
|
|
// https://github.com/socketio/socket.io/blob/e4d61b1be65ac3313a85da111a46777aa8d4aae3/lib/socket.js#L41
|
|
// - a Manager has
|
|
// - `.rooms={'NAMESPACE/ENTITY': []}` and
|
|
// - `.roomClients={'CLIENT_ID': {'...': true}}`
|
|
// https://github.com/socketio/socket.io/blob/e4d61b1be65ac3313a85da111a46777aa8d4aae3/lib/manager.js#L287-L288
|
|
// https://github.com/socketio/socket.io/blob/e4d61b1be65ac3313a85da111a46777aa8d4aae3/lib/manager.js#L444-L455
|
|
|
|
_clientsInRoom(client, room) {
|
|
const clients = client.manager.rooms['/' + room] || []
|
|
return clients.length
|
|
},
|
|
|
|
_roomsClientIsIn(client) {
|
|
const rooms = client.manager.roomClients[client.id] || {}
|
|
return (
|
|
Object.keys(rooms)
|
|
// drop the namespace
|
|
.filter((room) => room !== '')
|
|
// room names are composed as '<NAMESPACE>/<ROOM>' and the default
|
|
// namespace is empty (see comments above), just drop the '/'
|
|
.map((fullRoomPath) => fullRoomPath.slice(1))
|
|
)
|
|
},
|
|
|
|
_clientAlreadyInRoom(client, room) {
|
|
const rooms = client.manager.roomClients[client.id] || {}
|
|
return !!rooms['/' + room]
|
|
}
|
|
}
|