mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-23 20:02:23 +00:00
d9e0215aee
Camel case variables in real-time GitOrigin-RevId: 0a35d2e39d54c258bed017ea4dcdbf9a19a2a9b1
161 lines
6 KiB
JavaScript
161 lines
6 KiB
JavaScript
const logger = require('@overleaf/logger')
|
|
const metrics = require('@overleaf/metrics')
|
|
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, projectId, callback) {
|
|
this.joinEntity(client, 'project', projectId, callback)
|
|
},
|
|
|
|
joinDoc(client, docId, callback) {
|
|
this.joinEntity(client, 'doc', docId, callback)
|
|
},
|
|
|
|
leaveDoc(client, docId) {
|
|
this.leaveEntity(client, 'doc', docId)
|
|
},
|
|
|
|
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.debug({ 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.debug({ 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.debug(
|
|
{ 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.debug(
|
|
{ client: client.id, entity, id, beforeCount },
|
|
'client joined existing room'
|
|
)
|
|
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.debug(
|
|
{ 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.debug(
|
|
{ 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.debug({ 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]
|
|
},
|
|
}
|