forked from Diggsey/meteor-server-deps
-
Notifications
You must be signed in to change notification settings - Fork 7
/
server.coffee
396 lines (311 loc) · 10.7 KB
/
server.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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
import {Tracker} from 'meteor/tracker'
import Fiber from 'fibers'
import Future from 'fibers/future'
# Tracker.Computation constructor is private, so we are using this object as a guard.
# External code cannot access this, and will not be able to directly construct a
# Tracker.Computation instance.
privateObject = {}
# Guard object for fiber utils.
guard = {}
nextId = 1
class TrackerInstance
constructor: ->
@active = false
@currentComputation = null
@pendingComputations = []
@willFlush = false
@inFlush = null
@inRequireFlush = false
@inCompute = false
@throwFirstError = false
@afterFlushCallbacks = []
setCurrentComputation: (computation) ->
@currentComputation = computation
@active = !!computation
# Copied from tracker.js.
_debugFunc: ->
return Meteor._debug if Meteor?._debug
if console?.error
return ->
console.error.apply console, arguments
return ->
# Copied from tracker.js.
_maybeSuppressMoreLogs: (messagesLength) ->
if typeof Meteor isnt "undefined"
if Meteor._suppressed_log_expected()
Meteor._suppress_log(messagesLength - 1)
# Copied from tracker.js.
_throwOrLog: (from, error) ->
if @throwFirstError
throw error
else
printArgs = ["Exception from Tracker " + from + " function:"]
if error.stack and error.message and error.name
idx = error.stack.indexOf error.message
if idx < 0 or idx > error.name.length + 2
message = error.name + ": " + error.message
printArgs.push message
printArgs.push error.stack
@_maybeSuppressMoreLogs printArgs.length
for printArg in printArgs
@_debugFunc() printArg
_deferAndTransfer: (func) ->
# Defer execution of a function, which will create a new fiber. Make the resulting
# fiber share ownership of the same tracker instance as it will serve only as its
# extension for executing its flushes.
Meteor.defer =>
assert not Fiber.current._trackerInstance
try
Fiber.current._trackerInstance = @
func()
finally
Fiber.current._trackerInstance = null
requireFlush: ->
return if @willFlush
@_deferAndTransfer =>
@_runFlush
fromRequireFlush: true
@willFlush = true
_runFlush: (options) ->
if @inFlush instanceof Future
# If there are two runs from requireFlush in sequence, we simply skip the second one, the first
# one is still in progress.
return if options?.fromRequireFlush
# We wait for the previous flush from requireFlush to finish before continuing.
@inFlush.wait()
assert not @inFlush
# If already in flush and this is a flush from requireFlush, just skip it.
return if @inFlush and options?.fromRequireFlush
throw new Error "Can't call Tracker.flush while flushing" if @inFlush
if @inCompute
if options?.fromRequireFlush
# If this fiber is currently running a computation and a require flush has been
# deferred, we need to defer again and retry.
@_deferAndTransfer =>
@_runFlush options
return
throw new Error "Can't flush inside Tracker.autorun"
# If this is a run from requireFlush, provide a future so that calls to flush can wait on it.
if options?.fromRequireFlush
@inFlush = new Future()
else
@inFlush = true
@willFlush = true
@throwFirstError = !!options?.throwFirstError
recomputedCount = 0
finishedTry = false
try
while @pendingComputations.length or @afterFlushCallbacks.length
while @pendingComputations.length
computation = @pendingComputations.shift()
computation._recompute()
if computation._needsRecompute()
@pendingComputations.unshift computation
if not options?.finishSynchronously and ++recomputedCount > 1000
finishedTry = true
return
if @afterFlushCallbacks.length
func = @afterFlushCallbacks.shift()
try
func()
catch error
@_throwOrLog "afterFlush", error
finishedTry = true
finally
# We first have to set @inFlush to null, then we can return.
inFlush = @inFlush
unless finishedTry
@inFlush = null
inFlush.return() if inFlush instanceof Future
@_runFlush
finishSynchronously: options?.finishSynchronously
throwFirstError: false
@willFlush = false
@inFlush = null
inFlush.return() if inFlush instanceof Future
if @pendingComputations.length or @afterFlushCallbacks.length
throw new Error "still have more to do?" if options?.finishSynchronously
Meteor.setTimeout =>
@requireFlush()
,
10 # ms
Tracker._trackerInstance = ->
Meteor._nodeCodeMustBeInFiber()
Fiber.current._trackerInstance ?= new TrackerInstance()
Tracker.flush = (options) ->
Tracker._trackerInstance()._runFlush
finishSynchronously: true
throwFirstError: options?._throwFirstError
Tracker.inFlush = ->
Tracker._trackerInstance().inFlush
Tracker.autorun = (func, options) ->
throw new Error "Tracker.autorun requires a function argument" unless typeof func is "function"
c = new Tracker.Computation func, Tracker.currentComputation, options?.onError, privateObject
if Tracker.active
Tracker.onInvalidate ->
c.stop()
c
Tracker.nonreactive = (f) ->
trackerInstance = Tracker._trackerInstance()
previous = trackerInstance.currentComputation
trackerInstance.setCurrentComputation null
try
return f()
finally
trackerInstance.setCurrentComputation previous
Tracker.onInvalidate = (f) ->
throw new Error "Tracker.onInvalidate requires a currentComputation" unless Tracker.active
Tracker.currentComputation.onInvalidate f
Tracker.afterFlush = (f) ->
trackerInstance = Tracker._trackerInstance()
trackerInstance.afterFlushCallbacks.push f
trackerInstance.requireFlush()
# Compatibility with the client-side Tracker. On node.js we can use defineProperties to define getters.
Object.defineProperties Tracker,
currentComputation:
get: ->
Tracker._trackerInstance().currentComputation
active:
get: ->
Tracker._trackerInstance().active
class Tracker.Computation
constructor: (func, @_parent, @_onError, _private) ->
throw new Error "Tracker.Computation constructor is private; use Tracker.autorun" if _private isnt privateObject
@stopped = false
@invalidated = false
@firstRun = true
@_id = nextId++
@_onInvalidateCallbacks = []
@_onStopCallbacks = []
@_beforeRunCallbacks = []
@_afterRunCallbacks = []
@_recomputing = false
@_trackerInstance = Tracker._trackerInstance()
onException = (error) =>
throw error if @firstRun
if @_onError
@_onError error
else
@_trackerInstance._throwOrLog "recompute", error
@_func = Meteor.bindEnvironment func, onException, @
errored = true
try
@_compute()
errored = false
finally
@firstRun = false
@stop() if errored
onInvalidate: (f) ->
FiberUtils.ensure =>
throw new Error "onInvalidate requires a function" unless typeof f is "function"
if @invalidated
Tracker.nonreactive =>
f @
else
@_onInvalidateCallbacks.push f
onStop: (f) ->
FiberUtils.ensure =>
throw new Error "onStop requires a function" unless typeof f is "function"
if @stopped
Tracker.nonreactive =>
f @
else
@_onStopCallbacks.push f
beforeRun: (f) ->
throw new Error "beforeRun requires a function" unless typeof f is "function"
@_beforeRunCallbacks.push f
afterRun: (f) ->
throw new Error "afterRun requires a function" unless typeof f is "function"
@_afterRunCallbacks.push f
invalidate: ->
FiberUtils.ensure =>
# TODO: Why some tests freeze if we wrap this method into FiberUtils.synchronize?
if not @invalidated
if not @_recomputing and not @stopped
@_trackerInstance.requireFlush()
@_trackerInstance.pendingComputations.push @
@invalidated = true
for callback in @_onInvalidateCallbacks
Tracker.nonreactive =>
callback @
@_onInvalidateCallbacks = []
stop: ->
FiberUtils.ensure =>
FiberUtils.synchronize guard, @_id, =>
return if @stopped
@stopped = true
@invalidate()
while @_onStopCallbacks.length
callback = @_onStopCallbacks.shift()
Tracker.nonreactive =>
callback @
# Runs an arbitrary function inside the computation. This allows breaking many assumptions, so use it very carefully.
_runInside: (func) ->
FiberUtils.synchronize guard, @_id, =>
Meteor._nodeCodeMustBeInFiber()
previousTrackerInstance = Tracker._trackerInstance()
Fiber.current._trackerInstance = @_trackerInstance
previousComputation = @_trackerInstance.currentComputation
@_trackerInstance.setCurrentComputation @
previousInCompute = @_trackerInstance.inCompute
@_trackerInstance.inCompute = true
try
func @
finally
Fiber.current._trackerInstance = previousTrackerInstance
@_trackerInstance.setCurrentComputation previousComputation
@_trackerInstance.inCompute = previousInCompute
_compute: ->
FiberUtils.synchronize guard, @_id, =>
@invalidated = false
@_runInside (computation) =>
while @_beforeRunCallbacks.length
callback = @_beforeRunCallbacks.shift()
Tracker.nonreactive =>
callback @
@_func.call null, @
while @_afterRunCallbacks.length
callback = @_afterRunCallbacks.shift()
Tracker.nonreactive =>
callback @
_needsRecompute: ->
@invalidated and not @stopped
_recompute: ->
FiberUtils.synchronize guard, @_id, =>
assert not @_recomputing
@_recomputing = true
try
if @_needsRecompute()
@_compute()
finally
@_recomputing = false
flush: ->
FiberUtils.ensure =>
return if @_recomputing
@_recompute()
run: ->
FiberUtils.ensure =>
@invalidate()
@flush()
class Tracker.Dependency
constructor: ->
@_dependentsById = {}
depend: (computation) ->
unless computation
return false unless Tracker.active
computation = Tracker.currentComputation
id = computation._id
if id not of @_dependentsById
@_dependentsById[id] = computation
computation.onInvalidate =>
delete @_dependentsById[id]
return true
false
changed: ->
for id, computation of @_dependentsById
computation.invalidate()
hasDependents: ->
for id, computation of @_dependentsById
return true
false
export {Tracker}