overleaf/services/real-time/app/js/RoomManager.js
Jakob Ackermann aa9d6c8dc9 [misc] reland decaff cleanup (#166)
* [misc] decaff cleanup: RoomManager

* [misc] decaff cleanup: RedisClientManager

* [misc] decaff cleanup: SafeJsonParse

* [misc] decaff cleanup: WebApiManager

* [misc] decaff cleanup: WebsocketController

* [misc] decaff cleanup: WebsocketLoadBalancer

* [misc] decaff cleanup: SessionSockets

* [misc] decaff cleanup: HttpController

* [misc] decaff cleanup: HttpApiController

* [misc] decaff cleanup: HealthCheckManager

* [misc] decaff cleanup: EventLogger

* [misc] decaff cleanup: Errors

o-error will eliminate most of it -- when we migrate over.

* [misc] decaff cleanup: DrainManager

* [misc] decaff cleanup: DocumentUpdaterManager

* [misc] decaff cleanup: DocumentUpdaterController: no-unused-vars

* [misc] decaff cleanup: DocumentUpdaterController: Array.from

* [misc] decaff cleanup: DocumentUpdaterController: implicit return

* [misc] decaff cleanup: DocumentUpdaterController: IIFE

* [misc] decaff cleanup: DocumentUpdaterController: null checks

* [misc] decaff cleanup: DocumentUpdaterController: simpler loops

* [misc] decaff cleanup: DocumentUpdaterController: move module name def

* [misc] decaff cleanup: ConnectedUsersManager: handle-callback-err

* [misc] decaff cleanup: ConnectedUsersManager: implicit returns

* [misc] decaff cleanup: ConnectedUsersManager: null checks

* [misc] decaff cleanup: ChannelManager: no-unused-vars

* [misc] decaff cleanup: ChannelManager: implicit returns

* [misc] decaff cleanup: ChannelManager: other cleanup

- var -> const
- drop variable assignment before return

* [misc] decaff cleanup: AuthorizationManager: handle-callback-err

Note: This requires a change in WebsocketController to provide a dummy
 callback.

* [misc] decaff cleanup: AuthorizationManager: Array.from

* [misc] decaff cleanup: AuthorizationManager: implicit returns

* [misc] decaff cleanup: AuthorizationManager: null checks

* [misc] decaff cleanup: Router: handle-callback-err

* [misc] decaff cleanup: Router: standard/no-callback-literal

* [misc] decaff cleanup: Router: Array.from

* [misc] decaff cleanup: Router: implicit returns

* [misc] decaff cleanup: Router: refactor __guard__ wrapper

* [misc] decaff cleanup: Router: null checks

And a minor bug fix: user.id -> user._id

* [misc] decaff cleanup: Router: move variable declarations to assignments

* [misc] decaff cleanup: app: implicit returns

* [misc] decaff cleanup: app: __guard__

* [misc] decaff cleanup: app: null checks

* [misc] decaff cleanup: app: function definitions

* [misc] decaff cleanup: app: drop unused next argument

* [misc] decaff cleanup: app: var -> const
2020-07-07 11:06:02 +01:00

160 lines
5.9 KiB
JavaScript

/* eslint-disable
camelcase,
*/
const logger = require('logger-sharelatex')
const metrics = require('metrics-sharelatex')
const { EventEmitter } = require('events')
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
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]
}
}