-
Notifications
You must be signed in to change notification settings - Fork 4
/
tracker.coffee
289 lines (206 loc) · 7.59 KB
/
tracker.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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# Copyright 2015, Quixey Inc.
# All rights reserved.
#
# Licensed under the Modified BSD License found in the
# LICENSE file in the root directory of this source tree.
# A few things to augment Meteor's Tracker.
#
# We monkey patch the tracker package because
# we want other Meteor packages like "mongo"
# to use the same global object.
dummyComputation = Tracker.autorun ->
if dummyComputation._id isnt 1
# A computation was already created by one of the packages that jframework
# uses, which means it may be too late for our monkey patch to work right.
console.warn "JFramework's attempt to monkey patch Tracker might not work."
dummyComputation.stop()
Tracker.active = false
Tracker.currentComputation = null
setCurrentComputation = (c) ->
Tracker.currentComputation = c
Tracker.active = c?
_debugFunc = -> Meteor?._debug ? console?.log ? ->
withNoYieldsAllowed = (f) ->
if not Meteor? or Meteor.isClient then f
else ->
args = arguments
Meteor._noYieldsAllowed -> f.apply null, args
Tracker.pendingComputations = []
# true if a Tracker.flush is scheduled, or if we are in Tracker.flush now
Tracker.willFlush = false
# true if we are in Tracker.flush now
Tracker.inFlush = false
# true if we are computing a computation now, either first time
# or recompute. This matches Tracker.active unless we are inside
# Tracker.nonreactive, which nullifies currentComputation even though
# an enclosing computation may still be running.
Tracker.inCompute = false
Tracker.afterFlushCallbacks = []
requireFlush = ->
if not Tracker.willFlush
setTimeout Tracker.flush, 1
Tracker.willFlush = true
# Tracker.Computation constructor is visible but private
# (throws an error if you try to call it)
Tracker._constructingComputation = false;
Tracker.Computation = (f, creator) ->
if not Tracker._constructingComputation
throw new Error "Tracker.Computation constructor is private;
use Tracker.autorun"
Tracker._constructingComputation = false
@stopped = false
@invalidated = false
@_id = J.getNextId()
if J.debugGraph then J.graph[@_id] = @
@_onInvalidateCallbacks = []
@creator = creator
@_func = f
@firstRun = true
@_compute()
@firstRun = false
Tracker.Computation::onInvalidate = (f) ->
unless _.isFunction f
throw new Error "onInvalidate requires a function"
if Meteor.isServer
f = Meteor.bindEnvironment f
if @invalidated
Tracker.nonreactive =>
withNoYieldsAllowed(f) @
else
@_onInvalidateCallbacks.push f
Tracker.Computation::invalidate = ->
return if @invalidated
@invalidated = true
if not @stopped
Tracker.pendingComputations.push @
if Tracker.pendingComputations.length > 1 and (
@sortKey < Tracker.pendingComputations[Tracker.pendingComputations.length - 2].sortKey
)
J.inc 'pcSort'
Tracker.pendingComputations.sort (a, b) ->
if a.sortKey < b.sortKey then -1
else if a.sortKey > b.sortKey then 1
else 0
requireFlush()
# Callbacks can't add callbacks, because
# self.invalidated is true
for f in @_onInvalidateCallbacks
Tracker.nonreactive => withNoYieldsAllowed(f) @
@_onInvalidateCallbacks = []
Tracker.Computation::stop = ->
if not @stopped
@stopped = true
@invalidate()
Tracker.Computation::_compute = ->
i = Tracker.pendingComputations.indexOf @
if i >= 0
Tracker.pendingComputations.splice i, 1
@invalidated = false
previous = Tracker.currentComputation
setCurrentComputation @
previousInCompute = Tracker.inCompute
Tracker.inCompute = true
try
withNoYieldsAllowed(@_func) @
finally
setCurrentComputation previous
Tracker.inCompute = previousInCompute
Tracker.Computation::debug = ->
console.group "Computation[#{@_id}]"
if @autoVar
@autoVar.debug()
else if @component
@component.debug()
console.groupEnd()
Tracker.flush = ->
# console.debug "Tracker.flush!"
if Tracker.inFlush
throw new Error "Can't call Tracker.flush while flushing"
if Tracker.inCompute
throw new Error "Can't flush inside Tracker.autorun"
Tracker.inFlush = true
Tracker.willFlush = true
while Tracker.pendingComputations.length or Tracker.afterFlushCallbacks.length
# Recompute all pending computations
while Tracker.pendingComputations.length
comp = Tracker.pendingComputations.shift()
# console.debug 'recompute'
comp._compute() unless comp.stopped
J.inc 'recompute'
if Tracker.afterFlushCallbacks.length
J.inc 'afterFlush'
# Call one afterFlush callback, which may
# invalidate more computations
afc = Tracker.afterFlushCallbacks.shift()
# console.debug 'afterFlush', afc
afc.func.call null
# console.debug 'flush done'
Tracker.willFlush = false
Tracker.inFlush = false
Tracker.autorun = (f, sortKey = 0.5) ->
if not _.isFunction f
throw new Error "Tracker.autorun requires a function argument"
if not _.isNumber sortKey
throw new Error "Tracker.autorun sortKey must be a number"
if Meteor.isServer
f = Meteor.bindEnvironment f
Tracker._constructingComputation = true
c = new Tracker.Computation f, Tracker.currentComputation
c.sortKey = sortKey
if Tracker.active
Tracker.onInvalidate -> c.stop()
c
Tracker.nonreactive = (f) ->
previous = Tracker.currentComputation
setCurrentComputation null
ret = f()
setCurrentComputation previous
ret
Tracker.onInvalidate = (f) ->
if not Tracker.active
throw new Error "Tracker.onInvalidate requires a currentComputation"
Tracker.currentComputation.onInvalidate f
Tracker.afterFlush = (f, sortKey = 0.5) ->
if Meteor.isServer
f = Meteor.bindEnvironment f
unless _.isNumber(sortKey)
throw new Error "afterFlush sortKey must be a number (lower comes first)"
Tracker.afterFlushCallbacks.push func: f, sortKey: sortKey
if Tracker.afterFlushCallbacks.length > 1 and (
sortKey < Tracker.afterFlushCallbacks[Tracker.afterFlushCallbacks.length - 2].sortKey
)
Tracker.afterFlushCallbacks.sort (a, b) ->
if a.sortKey < b.sortKey then -1
else if a.sortKey > b.sortKey then 1
else 0
requireFlush()
class Tracker.Dependency
# Like Meteor's Tracker.Dependency except that a "creator",
# i.e. a reactive data source, should be able to freely read
# its own reactive values as it's mutating them without
# invalidating itself.
# But the creator should still invalidate if it reads
# its own values which other objects then mutate.
constructor: (creator) ->
@creator =
if creator is undefined
Tracker.currentComputation
else
creator
@_dependents = []
depend: (computation = Tracker.currentComputation) ->
return false if not computation?
if computation in @_dependents
false
else
@_dependents.push computation
computation.onInvalidate =>
i = @_dependents.indexOf computation
@_dependents.splice i, 1
true
changed: ->
for computation in _.clone @_dependents
unless computation is Tracker.currentComputation is @creator
computation.invalidate()
hasDependents: ->
@_dependents.length > 0