forked from Meteor-Community-Packages/meteor-user-status
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathstatus.coffee
212 lines (178 loc) · 6.71 KB
/
status.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
###
Apparently, the new api.export takes care of issues here. No need to attach to global namespace.
See http://shiggyenterprises.wordpress.com/2013/09/09/meteor-packages-in-coffeescript-0-6-5/
We may want to make UserSessions a server collection to take advantage of indices.
Will implement if someone has enough online users to warrant it.
###
UserConnections = new Meteor.Collection("user_status_sessions")
statusEvents = new (Npm.require('events').EventEmitter)()
###
Multiplex login/logout events to status.online
###
statusEvents.on "connectionLogin", (advice) ->
Meteor.users.update advice.userId,
$set:
'status.online': true,
'status.lastLogin': advice.loginTime
return
statusEvents.on "connectionLogout", (advice) ->
conns = UserConnections.find(userId: advice.userId).fetch()
if conns.length is 0
# Go offline if we are the last connection for this user
# This includes removing all idle information
Meteor.users.update advice.userId,
$set: {'status.online': false }
$unset:
'status.idle': null
'status.lastActivity': null
else if _.every(conns, (c) -> c.idle)
###
All remaining connections are idle:
- If the last active connection quit, then we should go idle with the most recent activity
- If an idle connection quit, nothing should happen; specifically, if the
most recently active idle connection quit, we shouldn't tick the value backwards.
This may result in a no-op so we can be smart and skip the update.
###
return if advice.lastActivity? # The dropped connection was already idle
Meteor.users.update advice.userId,
$set:
'status.idle': true
'status.lastActivity': _.max(_.pluck conns, "lastActivity")
return
###
Multiplex idle/active events to status.idle
TODO: Hopefully this is quick because it's all in memory, but we can use indices if it turns out to be slow
TODO: There is a race condition when switching between tabs, leaving the user inactive while idle goes from one tab to the other.
It can probably be smoothed out.
###
statusEvents.on "connectionIdle", (advice) ->
conns = UserConnections.find(userId: advice.userId).fetch()
return unless _.every(conns, (c) -> c.idle)
# Set user to idle if all the connections are idle
# This will not be the most recent idle across a disconnection, so we use max
# TODO: the race happens here where everyone was idle when we looked for them but now one of them isn't.
Meteor.users.update advice.userId,
$set:
'status.idle': true
'status.lastActivity': _.max(_.pluck conns, "lastActivity")
return
statusEvents.on "connectionActive", (advice) ->
Meteor.users.update advice.userId,
$unset:
'status.idle': null
'status.lastActivity': null
return
# Clear any online users on startup (they will re-add themselves)
# Having no status.online is equivalent to status.online = false (above)
# but it is unreasonable to set the entire users collection to false on startup.
Meteor.startup ->
Meteor.users.update {}
, $unset: {
"status.online": null
"status.idle": null
"status.lastActivity": null
}
, {multi: true}
###
Local session modifification functions - also used in testing
###
addSession = (userId, connectionId, date, ipAddr) ->
UserConnections.upsert connectionId,
$set: {
userId: userId
ipAddr: ipAddr
loginTime: date
}
statusEvents.emit "connectionLogin",
userId: userId
connectionId: connectionId
ipAddr: ipAddr
loginTime: date
return
removeSession = (userId, connectionId, date) ->
conn = UserConnections.findOne(connectionId)
# Don't emit this again if the connection was already closed
return unless conn?
UserConnections.update connectionId,
$set:
logoutTime: date
loggedOut: true
idle: true
statusEvents.emit "connectionLogout",
userId: userId
connectionId: connectionId
lastActivity: conn?.lastActivity # If this connection was idle, pass the last activity we saw
logoutTime: date
return
idleSession = (userId, connectionId, date) ->
UserConnections.update connectionId,
$set: {
idle: true
lastActivity: date
}
statusEvents.emit "connectionIdle",
userId: userId
connectionId: connectionId
lastActivity: date
return
activeSession = (userId, connectionId, date) ->
UserConnections.update connectionId,
$set: { idle: false }
$unset: { lastActivity: null }
statusEvents.emit "connectionActive",
userId: userId
connectionId: connectionId
lastActivity: date
return
# pub/sub trick as referenced in http://stackoverflow.com/q/10257958/586086
# TODO: replace this with Meteor.onConnection and login hooks.
Meteor.publish null, ->
# Return null explicitly if this._session is not available, i.e.:
# https://github.com/arunoda/meteor-fast-render/issues/41
return null unless @_session
date = new Date() # compute this as early as possible
userId = @_session.userId
return null unless @_session.socket? # Or there is nothing to close!
connection = @_session.connectionHandle
connectionId = @_session.id # same as connection.id
# Untrack connection on logout
unless userId?
# TODO: this could be replaced with a findAndModify once it's supported on Collections
existing = UserConnections.findOne(connectionId)
return null unless existing? # Probably new session
removeSession(existing.userId, connectionId, date)
return null
# Add socket to open connections
addSession(userId, connectionId, date, connection.clientAddress)
# Remove socket on close
@_session.socket.on "close", Meteor.bindEnvironment ->
removeSession(userId, connectionId, new Date())
, (e) ->
Meteor._debug "Exception from connection close callback:", e
return null
# TODO the below methods only care about logged in users.
# We can extend this to all users. (See also client code)
# We can trust the timestamp here because it was sent from a TimeSync value.
Meteor.methods
"user-status-idle": (timestamp) ->
return unless @userId
idleSession(@userId, @connection.id, new Date(timestamp))
return
"user-status-active": (timestamp) ->
return unless @userId
# We only use timestamp because it's when we saw activity *on the client*
# as opposed to just being notified it.
# It is probably more accurate even if a few hundred ms off
# due to how long the message took to get here.
activeSession(@userId, @connection.id, new Date(timestamp))
return
# Exported variable
UserStatus =
connections: UserConnections
events: statusEvents
# Internal functions, exported for testing
StatusInternals =
addSession: addSession
removeSession: removeSession
idleSession: idleSession
activeSession: activeSession