forked from Meteor-Community-Packages/meteor-user-status
-
Notifications
You must be signed in to change notification settings - Fork 0
/
monitor.coffee
191 lines (147 loc) · 5.85 KB
/
monitor.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
###
The idle monitor watches for mouse, keyboard, and blur events,
and reports idle status to the server.
It uses TimeSync to report accurate time.
Everything is reactive, of course!
###
# State variables
monitorId = null
idle = false
lastActivityTime = undefined
monitorDep = new Deps.Dependency
idleDep = new Deps.Dependency
activityDep = new Deps.Dependency
focused = true
# These settings are internal or exported for test only
MonitorInternals = {
idleThreshold: null
idleOnBlur: false
computeState: (lastActiveTime, currentTime, isWindowFocused) ->
inactiveTime = currentTime - lastActiveTime
return true if MonitorInternals.idleOnBlur and not isWindowFocused
return if (inactiveTime > MonitorInternals.idleThreshold) then true else false
connectionChange: (isConnected, wasConnected) ->
# We only need to do something if we reconnect and we are idle
# Don't get idle status reactively, as this function only
# takes care of reconnect status and doesn't care if it changes.
# Note that userId does not change during a resume login, as designed by Meteor.
# However, the idle state is tied to the connection and not the userId.
if isConnected and !wasConnected and idle
MonitorInternals.reportIdle(lastActivityTime)
onWindowBlur: ->
focused = false
monitor()
onWindowFocus: ->
focused = true
# Focusing should count as an action, otherwise "active" event may be
# triggered at some point in the past!
monitor(true)
reportIdle: (time) ->
Meteor.call "user-status-idle", time
reportActive: (time) ->
Meteor.call "user-status-active", time
}
start = (settings) ->
throw new Error("Can't start idle monitor until synced to server") unless TimeSync.isSynced()
throw new Error("Idle monitor is already active. Stop it first.") if monitorId
settings = settings || {}
# The amount of time before a user is marked idle
MonitorInternals.idleThreshold = settings.threshold || 60000
# Don't check too quickly; it doesn't matter anyway: http://stackoverflow.com/q/15871942/586086
interval = Math.max(settings.interval || 1000, 1000)
# Whether blurring the window should immediately cause the user to go idle
MonitorInternals.idleOnBlur = if settings.idleOnBlur? then settings.idleOnBlur else false
# Set new monitoring interval
monitorId = Meteor.setInterval(monitor, interval)
monitorDep.changed()
# Reset last activity; can't count inactivity from some arbitrary time
unless lastActivityTime?
lastActivityTime = Deps.nonreactive -> TimeSync.serverTime()
activityDep.changed()
monitor()
return
stop = ->
throw new Error("Idle monitor is not running.") unless monitorId
Meteor.clearInterval(monitorId)
monitorId = null
lastActivityTime = undefined # If monitor started again, we shouldn't re-use this time
monitorDep.changed()
if idle # Un-set any idleness
idle = false
idleDep.changed()
# need to run this because the Deps below won't re-run when monitor is off
MonitorInternals.reportActive( Deps.nonreactive -> TimeSync.serverTime() )
return
monitor = (setAction) ->
# Ignore focus/blur events when we aren't monitoring
return unless monitorId
# We use setAction here to not have to call serverTime twice. Premature optimization?
currentTime = Deps.nonreactive -> TimeSync.serverTime()
# Can't monitor if we haven't synced with server yet, or lost our sync.
return unless currentTime?
# Update action as long as we're not blurred and idling on blur
# We ignore actions that happen while a client is blurred, if idleOnBlur is set.
if setAction and (focused or !MonitorInternals.idleOnBlur)
lastActivityTime = currentTime
activityDep.changed()
newIdle = MonitorInternals.computeState(lastActivityTime, currentTime, focused)
if newIdle isnt idle
idle = newIdle
idleDep.changed()
return
touch = ->
unless monitorId
Meteor._debug("Cannot touch as idle monitor is not running.")
return
monitor(true) # Check for an idle state change right now
isIdle = ->
idleDep.depend()
return idle
isMonitoring = ->
monitorDep.depend()
return monitorId?
lastActivity = ->
return unless isMonitoring()
activityDep.depend()
return lastActivityTime
Meteor.startup ->
# Listen for mouse and keyboard events on window
# TODO other stuff - e.g. touch events?
$(window).on "click keydown", -> monitor(true)
# catch window blur events when requested and where supported
# We'll use jQuery here instead of window.blur so that other code can attach blur events:
# http://stackoverflow.com/q/22415296/586086
$(window).blur MonitorInternals.onWindowBlur
$(window).focus MonitorInternals.onWindowFocus
# First check initial state if window loaded while blurred
# Some browsers don't fire focus on load: http://stackoverflow.com/a/10325169/586086
focused = document.hasFocus()
# Report idle status whenever connection changes
Deps.autorun ->
# Don't report idle state unless we're monitoring
return unless isMonitoring()
# XXX These will buffer across a disconnection - do we want that?
# The idle report will result in a duplicate message (with below)
# The active report will result in a null op.
if isIdle()
MonitorInternals.reportIdle(lastActivityTime)
else
# If we were inactive, report that we are active again to the server
MonitorInternals.reportActive(lastActivityTime)
return
# If we reconnect and we were idle, make sure we send that upstream
wasConnected = Meteor.status().connected
Deps.autorun ->
connected = Meteor.status().connected
MonitorInternals.connectionChange(connected, wasConnected)
wasConnected = connected
return
# export functions for starting and stopping idle monitor
UserStatus = {
startMonitor: start
stopMonitor: stop
pingMonitor: touch
isIdle: isIdle
isMonitoring: isMonitoring
lastActivity: lastActivity
}