2019-07-18 06:25:10 -04:00
|
|
|
logger = require 'logger-sharelatex'
|
2019-07-24 11:25:45 -04:00
|
|
|
metrics = require "metrics-sharelatex"
|
2019-07-18 06:25:10 -04:00
|
|
|
{EventEmitter} = require 'events'
|
|
|
|
|
|
|
|
IdMap = new Map() # keep track of whether ids are from projects or docs
|
2019-07-24 10:41:25 -04:00
|
|
|
RoomEvents = new EventEmitter() # emits {project,doc}-active and {project,doc}-empty events
|
2019-07-18 06:25:10 -04:00
|
|
|
|
|
|
|
# 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 = RoomManager =
|
|
|
|
|
2019-07-23 12:02:09 -04:00
|
|
|
joinProject: (client, project_id, callback = () ->) ->
|
|
|
|
@joinEntity client, "project", project_id, callback
|
2019-07-18 06:25:10 -04:00
|
|
|
|
2019-07-23 12:02:09 -04:00
|
|
|
joinDoc: (client, doc_id, callback = () ->) ->
|
|
|
|
@joinEntity client, "doc", doc_id, callback
|
2019-07-18 06:25:10 -04:00
|
|
|
|
|
|
|
leaveDoc: (client, doc_id) ->
|
2019-07-22 06:23:33 -04:00
|
|
|
@leaveEntity client, "doc", doc_id
|
2019-07-18 06:25:10 -04:00
|
|
|
|
|
|
|
leaveProjectAndDocs: (client) ->
|
2019-07-19 03:56:38 -04:00
|
|
|
# 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.
|
2019-07-18 06:25:10 -04:00
|
|
|
for id in @_roomsClientIsIn(client)
|
|
|
|
entity = IdMap.get(id)
|
2019-07-22 06:23:33 -04:00
|
|
|
@leaveEntity client, entity, id
|
2019-07-18 06:25:10 -04:00
|
|
|
|
2019-07-24 09:30:48 -04:00
|
|
|
emitOnCompletion: (promiseList, eventName) ->
|
|
|
|
result = Promise.all(promiseList)
|
|
|
|
result.then () -> RoomEvents.emit(eventName)
|
|
|
|
result.catch (err) -> RoomEvents.emit(eventName, err)
|
|
|
|
|
2019-07-18 06:25:10 -04:00
|
|
|
eventSource: () ->
|
|
|
|
return RoomEvents
|
|
|
|
|
2019-07-23 12:02:09 -04:00
|
|
|
joinEntity: (client, entity, id, callback) ->
|
2019-07-18 06:25:10 -04:00
|
|
|
beforeCount = @_clientsInRoom(client, id)
|
2019-07-26 03:07:49 -04:00
|
|
|
# client joins room immediately but joinDoc request does not complete
|
|
|
|
# until room is subscribed
|
|
|
|
client.join id
|
2019-07-18 06:25:10 -04:00
|
|
|
# is this a new room? if so, subscribe
|
2019-07-23 12:02:09 -04:00
|
|
|
if beforeCount == 0
|
2019-07-18 06:25:10 -04:00
|
|
|
logger.log {entity, id}, "room is now active"
|
2019-07-23 12:02:09 -04:00
|
|
|
RoomEvents.once "#{entity}-subscribed-#{id}", (err) ->
|
2019-07-24 10:41:25 -04:00
|
|
|
# only allow the client to join when all the relevant channels have subscribed
|
2019-07-26 03:07:49 -04:00
|
|
|
logger.log {client: client.id, entity, id, beforeCount}, "client joined new room and subscribed to channel"
|
2019-07-23 12:02:09 -04:00
|
|
|
callback(err)
|
2019-07-18 06:25:10 -04:00
|
|
|
RoomEvents.emit "#{entity}-active", id
|
|
|
|
IdMap.set(id, entity)
|
2019-07-24 11:25:45 -04:00
|
|
|
# keep track of the number of listeners
|
|
|
|
metrics.gauge "room-listeners", RoomEvents.eventNames().length
|
2019-07-23 12:02:09 -04:00
|
|
|
else
|
|
|
|
logger.log {client: client.id, entity, id, beforeCount}, "client joined existing room"
|
|
|
|
client.join id
|
|
|
|
callback()
|
2019-07-18 06:25:10 -04:00
|
|
|
|
2019-07-22 06:23:33 -04:00
|
|
|
leaveEntity: (client, entity, id) ->
|
2019-07-18 06:25:10 -04:00
|
|
|
client.leave id
|
|
|
|
afterCount = @_clientsInRoom(client, id)
|
2019-07-23 12:02:09 -04:00
|
|
|
logger.log {client: client.id, entity, id, afterCount}, "client left room"
|
2019-07-18 06:25:10 -04:00
|
|
|
# is the room now empty? if so, unsubscribe
|
2019-07-22 06:23:33 -04:00
|
|
|
if !entity?
|
|
|
|
logger.error {entity: id}, "unknown entity when leaving with id"
|
|
|
|
return
|
2019-07-23 12:02:09 -04:00
|
|
|
if afterCount == 0
|
2019-07-18 06:25:10 -04:00
|
|
|
logger.log {entity, id}, "room is now empty"
|
|
|
|
RoomEvents.emit "#{entity}-empty", id
|
2019-07-22 06:23:33 -04:00
|
|
|
IdMap.delete(id)
|
2019-07-24 11:25:45 -04:00
|
|
|
metrics.gauge "room-listeners", RoomEvents.eventNames().length
|
2019-07-22 06:23:33 -04:00
|
|
|
|
|
|
|
# internal functions below, these access socket.io rooms data directly and
|
|
|
|
# will need updating for socket.io v2
|
|
|
|
|
|
|
|
_clientsInRoom: (client, room) ->
|
|
|
|
nsp = client.namespace.name
|
|
|
|
name = (nsp + '/') + room;
|
|
|
|
return (client.manager?.rooms?[name] || []).length
|
|
|
|
|
|
|
|
_roomsClientIsIn: (client) ->
|
|
|
|
roomList = for fullRoomPath of client.manager.roomClients?[client.id] when fullRoomPath isnt ''
|
|
|
|
# strip socket.io prefix from room to get original id
|
|
|
|
[prefix, room] = fullRoomPath.split('/', 2)
|
|
|
|
room
|
|
|
|
return roomList
|