action_cable_notifications 0.1.14 → 0.1.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6f1c452c20e07a93aa050d60b61d6a5c2fdba4a1
4
- data.tar.gz: da28fc564086ef82612b56f209bd365df1d80127
3
+ metadata.gz: 3d3881c154e82ff694803dae629b00ef8110d979
4
+ data.tar.gz: be3fcb83d77cc590afbbc82982ef4eed706b304e
5
5
  SHA512:
6
- metadata.gz: a769b28967a738637247207f4113cbebe510a2526a44099daed32be698e2207ce93b7b566b46f9344dab5092a62207bbb4f17f54246b9dc7e01b416388489cff
7
- data.tar.gz: 50f9b8030b96f4aeaf82a7a5f8b030c627c577525128e9b8e6697425b883adab7585591c3879cce64a5498a2d99bbf1c14483f6254274a69fa604159922be118
6
+ metadata.gz: 3f7ce84fcef2aea235649357959f5ffaeae93af487922a2e06fadf371419e996a454b3c4f7929f3f53e298ab2ef289828c5a0e3921bfd600a10b4101e0b2e63f
7
+ data.tar.gz: db6d782b49af060688c3bb53a8702ec421c6393f7ce50319da561405d5cb41ceeae6e67e2ae8615d3230db63d3b059c6a14f2e0501cd1114860ab81f6d9cf6ec
@@ -11,4 +11,5 @@
11
11
  // about supported directives.
12
12
  //
13
13
  //= require lodash
14
+ //= require ./tracker
14
15
  //= require ./cable_notifications
@@ -7,7 +7,7 @@
7
7
  stores: []
8
8
 
9
9
  # Register a new store
10
- registerStore: (store, callbacks) ->
11
- new_store = new CableNotifications.Store(store, callbacks)
10
+ registerStore: (store, options, callbacks) ->
11
+ new_store = new CableNotifications.Store(store, options, callbacks)
12
12
  @stores.push new_store
13
13
  new_store
@@ -1,40 +1,109 @@
1
1
  class CableNotifications.Collection
2
- constructor: (@store, @name) ->
3
- @data = []
4
- @channelInfo = null
5
-
6
2
  # Private methods
7
3
  #######################################
4
+ upstream = (command, params={}) ->
5
+ if @sync
6
+ cmd =
7
+ collection: @tableName
8
+ command: command
9
+ params: params
10
+
11
+ # If channel is connected, send command to the server
12
+ if @channel.isConnected()
13
+ @channel?.perform?('action', cmd)
14
+ # Otherwise, enqueue commands to send when connection resumes
15
+ else
16
+ @commandsCache.push {command: command, params: params, performed: false}
17
+ false
18
+ else
19
+ false
20
+
21
+ connectionChanged = () ->
22
+ if @channel.isConnected()
23
+ _.each(@commandsCache, (cmd) ->
24
+ if upstream(cmd.command, cmd.params)
25
+ cmd.performed = true
26
+ )
27
+
28
+ # Cleanup performed commands
29
+ _.remove(@commandsCache, {performed: true})
30
+
31
+ # Public methods
32
+ #######################################
33
+
34
+ constructor: (@store, @name, @tableName) ->
35
+ # Data storage array
36
+ @data = []
37
+ # Channel used to sync with upstream collection
38
+ @channel = null
39
+ # Tells changes should be synced with upstream collection
40
+ @sync = false
41
+ # Stores upstream commands when there is no connection to the server
42
+ @commandsCache = []
8
43
 
44
+ # Bind private methods to class instance
45
+ ########################################################
46
+ upstream = upstream.bind(this)
47
+ connectionChanged = connectionChanged.bind(this)
48
+
49
+ # Sync collection to ActionCable Channel
50
+ syncToChannel: (@channel) ->
51
+ @sync = true
52
+
53
+ Tracker.autorun () =>
54
+ @channel.isConnected()
55
+ connectionChanged()
56
+
57
+ # Fetch records from upstream
58
+ fetch: (params) ->
59
+ upstream("fetch", params)
60
+
61
+ # Filter records from the current collection
62
+ where: (selector={}) ->
63
+ _.filter(@data, selector)
64
+
65
+ # Find a record
9
66
  find: (selector={}) ->
10
67
  _.find(@data, selector)
11
68
 
12
- findIndex: (selector={}) ->
13
- _.findIndex(@data, selector)
69
+ # Creates a new record
70
+ create: (fields={}) ->
71
+ record = _.find(@data, {id: fields.id})
72
+ if( record )
73
+ console.warn("[create] Not expected to find an existing record with id #{fields.id}")
74
+ return
14
75
 
15
- insert: (record) ->
16
- @data.push (record)
76
+ @data.push (fields) unless @sync
17
77
 
18
- remove: (selector={}) ->
19
- index = @findIndex(selector)
20
- if index < 0
21
- console.warn("Couldn't find a matching record: #{selector}")
22
- else
23
- record = @data.splice(index, 1)
78
+ upstream("create",
79
+ fields: fields
80
+ )
81
+ fields
24
82
 
25
- update: (selector={}, fields, options) ->
26
- record = @find(selector)
83
+ # Update an existing record
84
+ update: (selector={}, fields={}, options={}) ->
85
+ record = _.find(@data, selector)
27
86
  if !record
28
87
  if options.upsert
29
- @insert(fields)
88
+ @create(fields)
30
89
  else
31
- console.warn("Couldn't find a matching record: #{selector}")
90
+ console.warn("[update] Couldn't find a matching record:", selector)
32
91
  else
33
92
  _.extend(record, fields)
93
+ upstream("update", {id: record.id, fields: fields})
94
+ record
34
95
 
35
- # http://docs.meteor.com/#/full/upsert
96
+ # Update an existing record or inserts a new one if there is no match
36
97
  upsert: (selector={}, fields) ->
37
98
  @update(selector, fields, {upsert: true})
38
99
 
39
- filter: (selector={}) ->
40
- _.filter(@data, selector)
100
+ # Destroy an existing record
101
+ destroy: (selector={}) ->
102
+ index = _.findIndex(@data, selector)
103
+ if index < 0
104
+ console.warn("[destroy] Couldn't find a matching record:", selector)
105
+ else
106
+ record = @data[index]
107
+ @data.splice(index, 1) unless @sync
108
+ upstream("destroy", {id: record.id})
109
+ record
@@ -2,25 +2,6 @@
2
2
  class CableNotifications.Store.DefaultCallbacks
3
3
  constructor: (@collections) ->
4
4
 
5
- # Helper function
6
- processPacketHelper = (packet, collection) ->
7
- index = -1
8
- record = null
9
-
10
- local_collection = @collections[collection || packet.collection].data
11
- if !local_collection
12
- console.warn("[update_many]: Collection #{collection_name} doesn't exist")
13
- else
14
- index = _.find(local_collection, (record) -> record.id == packet.id)
15
- if (index >= 0)
16
- record = local_collection[index]
17
-
18
- return {
19
- collection: local_collection
20
- index: index
21
- record: record
22
- }
23
-
24
5
  # Callbacks
25
6
  ##################################################
26
7
 
@@ -28,35 +9,21 @@ class CableNotifications.Store.DefaultCallbacks
28
9
  console.warn 'Method not implemented: collection_remove '
29
10
 
30
11
  create: (packet, collection) ->
31
- data = processPacketHelper(packet, collection)
32
- if data.record
33
- console.warn 'Expected not to find a document already present for an add: ' + data.record
34
- else
35
- data.collection.push(packet.data)
12
+ collection.create(packet.data)
36
13
 
37
14
  update: (packet, collection) ->
38
- data = processPacketHelper(packet, collection)
39
- if !data.record
40
- console.warn 'Expected to find a document to change'
41
- else if !_.isEmpty(packet.data)
42
- _.extend(data.record, packet.data)
15
+ collection.update({id: packet.id}, packet.data)
43
16
 
44
17
  update_many: (packet, collection) ->
45
- collection_name = collection || packet.collection
46
- local_collection = @collections[collection_name].data
47
- if !local_collection
48
- console.warn("[update_many]: Collection #{collection_name} doesn't exist")
49
- else
50
- _.each packet.data, (fields) ->
51
- record = _.findIndex(local_collection, (r) -> r.id == fields.id)
52
- if record>=0
53
- _.extend(local_collection[record], fields)
54
- else
55
- local_collection.push(fields)
18
+ _.each packet.data, (fields) ->
19
+ collection.update({id: fields.id}, fields)
20
+
21
+ upsert_many: (packet, collection) ->
22
+ _.each packet.data, (fields) ->
23
+ collection.upsert({id: fields.id}, fields)
56
24
 
57
25
  destroy: (packet, collection) ->
58
- data = processPacketHelper(packet, collection)
59
- if !data.record
60
- console.warn 'Expected to find a document to remove'
61
- else
62
- data.collection.splice(data.index, 1)
26
+ collection.destroy({id: packet.id})
27
+
28
+ error: (packet, collection) ->
29
+ console.error "[#{packet.cmd}]: #{packet.error}"
@@ -2,53 +2,125 @@
2
2
  #= require './default_callbacks'
3
3
 
4
4
  class CableNotifications.Store
5
- constructor: (@name, @callbacks) ->
5
+ constructor: (@name, @options={}, @callbacks) ->
6
6
  @collections = {}
7
7
  @channels = {}
8
8
 
9
9
  if !@callbacks
10
10
  @callbacks = new CableNotifications.Store.DefaultCallbacks(@collections)
11
11
 
12
- this
13
-
14
12
  # Private methods
15
13
  #######################################
16
14
 
17
- packetReceived = (channelInfo, collection) ->
15
+ # Check received packet and dispatch to the apropriate callback.
16
+ # Then call original callback
17
+ packetReceived = (channelInfo) ->
18
18
  (packet) ->
19
- @storePacket(packet, collection)
20
- channelInfo.callbacks.received?.apply channelInfo.channel, arguments
19
+ if packet?.collection
20
+ # Search if there is a collection in this Store that receives packets from the server
21
+ collection = _.find(channelInfo.collections,
22
+ {tableName: packet.collection})
23
+ if collection
24
+ dispatchPacket.call(this, packet, collection)
25
+ channelInfo.callbacks.received?.apply(channelInfo.channel, arguments)
26
+
27
+ # Dispatch received packet to registered stores
28
+ # collection overrides the collection name specified in the incoming packet
29
+ dispatchPacket = (packet, collection) ->
30
+ if packet && packet.msg
31
+ # Disables sync with upstream to prevent infinite message loop
32
+ sync = collection.sync
33
+ collection.sync = false
34
+ @callbacks[packet.msg]?(packet, collection)
35
+ collection.sync = sync
36
+
37
+ # Called when connected to a channel
38
+ channelConnected = (channelInfo) ->
39
+ () ->
40
+ channelInfo.connectedDep.changed()
41
+ channelInfo.callbacks.connected?.apply(channelInfo.channel, arguments)
42
+
43
+ # Called when disconnected from a channel
44
+ channelDisconnected = (channelInfo) ->
45
+ () ->
46
+ channelInfo.connectedDep.changed()
47
+ channelInfo.callbacks.disconnected?.apply(channelInfo.channel, arguments)
48
+
49
+ # Returns channel connection status
50
+ channelIsConnected = (channelInfo) ->
51
+ () ->
52
+ channelInfo.connectedDep.depend()
53
+ !channelInfo.channel.consumer.connection.disconnected
21
54
 
22
55
  # Public methods
23
56
  #######################################
24
57
 
25
58
  # Register a new collection
26
- registerCollection: (collection) ->
27
- if @collections[collection]
28
- console.warn '[registerCollection]: Collection already exists'
59
+ registerCollection: (name, channel, tableName) ->
60
+ tableName = name unless tableName
61
+ if @collections[name]
62
+ console.warn "[registerCollection]: Collection '#{name}' already exists"
29
63
  else
30
- @collections[collection] = new CableNotifications.Collection(this, collection)
31
- @collections[collection]
64
+ @collections[name] = new CableNotifications.Collection(this, name, tableName)
65
+ if channel
66
+ @syncToChannel(channel, @collections[name])
67
+
68
+ @collections[name]
32
69
 
33
70
  # Sync store using ActionCable received events
34
71
  # collection parameter overrides the collection name specified in the incoming packets for this channel
35
72
  syncToChannel: (channel, collection) ->
73
+ if !channel
74
+ console.warn "[syncToChannel]: Channel must be specified"
75
+ return false
76
+
77
+ if !collection
78
+ console.warn "[syncToChannel]: Collection must be specified"
79
+ return false
80
+
36
81
  channelId = JSON.parse(channel.identifier)?.channel
37
82
 
38
83
  if !channelId
39
84
  console.warn "[syncToChannel]: Channel specified doesn't have an identifier"
85
+ return false
86
+
87
+ if @collections[collection.name] < 0
88
+ console.warn "[syncToChannel]: Collection does not exists in the store"
89
+ return false
90
+
91
+ if @channels[channelId]
92
+ channelInfo = @channels[channelId]
93
+ if _.find(channelInfo.collections, {name: collection.name})
94
+ console.warn "[syncToChannel]: Collection '#{collection.name}' is already being synced with channel '#{channelId}'"
95
+ return false
96
+ else
97
+ collection.syncToChannel(channel)
98
+ channelInfo.collections.push collection
99
+
100
+ # Fetch data from uptream server
101
+ collection.fetch()
40
102
  else
103
+ # Initialize channelInfo
41
104
  @channels[channelId] =
105
+ id: channelId
42
106
  channel: channel
107
+ collections: [collection]
43
108
  callbacks: {
44
109
  received: channel.received
110
+ connected: channel.connected
111
+ disconnected: channel.disconnected
45
112
  }
113
+ connectedDep: new Tracker.Dependency
46
114
 
47
- channel.received = packetReceived(@channels[channelId], collection).bind(this)
48
- channel
115
+ channel.received = packetReceived(@channels[channelId]).bind(this)
116
+ channel.connected = channelConnected(@channels[channelId]).bind(this)
117
+ channel.disconnected = channelDisconnected(@channels[channelId]).bind(this)
118
+ channel.isConnected = channelIsConnected(@channels[channelId]).bind(this)
49
119
 
50
- # Dispatch received packet to registered stores
51
- # collection overrides the collection name specified in the incoming packet
52
- storePacket: (packet, collection) ->
53
- if packet && packet.msg
54
- @callbacks[packet.msg]?(packet, collection)
120
+ # Assigns channel to collection and turns on Sync
121
+ collection.syncToChannel(channel)
122
+
123
+ # Fetch data from upstream server
124
+ collection.fetch()
125
+
126
+ return true
@@ -0,0 +1,633 @@
1
+ /////////////////////////////////////////////////////
2
+ // Package docs at http://docs.meteor.com/#tracker //
3
+ /////////////////////////////////////////////////////
4
+
5
+ /**
6
+ * @namespace Tracker
7
+ * @summary The namespace for Tracker-related methods.
8
+ */
9
+ Tracker = {};
10
+
11
+ // http://docs.meteor.com/#tracker_active
12
+
13
+ /**
14
+ * @summary True if there is a current computation, meaning that dependencies on reactive data sources will be tracked and potentially cause the current computation to be rerun.
15
+ * @locus Client
16
+ * @type {Boolean}
17
+ */
18
+ Tracker.active = false;
19
+
20
+ // http://docs.meteor.com/#tracker_currentcomputation
21
+
22
+ /**
23
+ * @summary The current computation, or `null` if there isn't one. The current computation is the [`Tracker.Computation`](#tracker_computation) object created by the innermost active call to `Tracker.autorun`, and it's the computation that gains dependencies when reactive data sources are accessed.
24
+ * @locus Client
25
+ * @type {Tracker.Computation}
26
+ */
27
+ Tracker.currentComputation = null;
28
+
29
+ var setCurrentComputation = function (c) {
30
+ Tracker.currentComputation = c;
31
+ Tracker.active = !! c;
32
+ };
33
+
34
+ var _debugFunc = function () {
35
+ // We want this code to work without Meteor, and also without
36
+ // "console" (which is technically non-standard and may be missing
37
+ // on some browser we come across, like it was on IE 7).
38
+ //
39
+ // Lazy evaluation because `Meteor` does not exist right away.(??)
40
+ return (typeof Meteor !== "undefined" ? Meteor._debug :
41
+ ((typeof console !== "undefined") && console.error ?
42
+ function () { console.error.apply(console, arguments); } :
43
+ function () {}));
44
+ };
45
+
46
+ var _maybeSuppressMoreLogs = function (messagesLength) {
47
+ // Sometimes when running tests, we intentionally suppress logs on expected
48
+ // printed errors. Since the current implementation of _throwOrLog can log
49
+ // multiple separate log messages, suppress all of them if at least one suppress
50
+ // is expected as we still want them to count as one.
51
+ if (typeof Meteor !== "undefined") {
52
+ if (Meteor._suppressed_log_expected()) {
53
+ Meteor._suppress_log(messagesLength - 1);
54
+ }
55
+ }
56
+ };
57
+
58
+ var _throwOrLog = function (from, e) {
59
+ if (throwFirstError) {
60
+ throw e;
61
+ } else {
62
+ var printArgs = ["Exception from Tracker " + from + " function:"];
63
+ if (e.stack && e.message && e.name) {
64
+ var idx = e.stack.indexOf(e.message);
65
+ if (idx < 0 || idx > e.name.length + 2) { // check for "Error: "
66
+ // message is not part of the stack
67
+ var message = e.name + ": " + e.message;
68
+ printArgs.push(message);
69
+ }
70
+ }
71
+ printArgs.push(e.stack);
72
+ _maybeSuppressMoreLogs(printArgs.length);
73
+
74
+ for (var i = 0; i < printArgs.length; i++) {
75
+ _debugFunc()(printArgs[i]);
76
+ }
77
+ }
78
+ };
79
+
80
+ // Takes a function `f`, and wraps it in a `Meteor._noYieldsAllowed`
81
+ // block if we are running on the server. On the client, returns the
82
+ // original function (since `Meteor._noYieldsAllowed` is a
83
+ // no-op). This has the benefit of not adding an unnecessary stack
84
+ // frame on the client.
85
+ var withNoYieldsAllowed = function (f) {
86
+ if ((typeof Meteor === 'undefined') || Meteor.isClient) {
87
+ return f;
88
+ } else {
89
+ return function () {
90
+ var args = arguments;
91
+ Meteor._noYieldsAllowed(function () {
92
+ f.apply(null, args);
93
+ });
94
+ };
95
+ }
96
+ };
97
+
98
+ var nextId = 1;
99
+ // computations whose callbacks we should call at flush time
100
+ var pendingComputations = [];
101
+ // `true` if a Tracker.flush is scheduled, or if we are in Tracker.flush now
102
+ var willFlush = false;
103
+ // `true` if we are in Tracker.flush now
104
+ var inFlush = false;
105
+ // `true` if we are computing a computation now, either first time
106
+ // or recompute. This matches Tracker.active unless we are inside
107
+ // Tracker.nonreactive, which nullfies currentComputation even though
108
+ // an enclosing computation may still be running.
109
+ var inCompute = false;
110
+ // `true` if the `_throwFirstError` option was passed in to the call
111
+ // to Tracker.flush that we are in. When set, throw rather than log the
112
+ // first error encountered while flushing. Before throwing the error,
113
+ // finish flushing (from a finally block), logging any subsequent
114
+ // errors.
115
+ var throwFirstError = false;
116
+
117
+ var afterFlushCallbacks = [];
118
+
119
+ var requireFlush = function () {
120
+ if (! willFlush) {
121
+ // We want this code to work without Meteor, see debugFunc above
122
+ if (typeof Meteor !== "undefined")
123
+ Meteor._setImmediate(Tracker._runFlush);
124
+ else
125
+ setTimeout(Tracker._runFlush, 0);
126
+ willFlush = true;
127
+ }
128
+ };
129
+
130
+ // Tracker.Computation constructor is visible but private
131
+ // (throws an error if you try to call it)
132
+ var constructingComputation = false;
133
+
134
+ //
135
+ // http://docs.meteor.com/#tracker_computation
136
+
137
+ /**
138
+ * @summary A Computation object represents code that is repeatedly rerun
139
+ * in response to
140
+ * reactive data changes. Computations don't have return values; they just
141
+ * perform actions, such as rerendering a template on the screen. Computations
142
+ * are created using Tracker.autorun. Use stop to prevent further rerunning of a
143
+ * computation.
144
+ * @instancename computation
145
+ */
146
+ Tracker.Computation = function (f, parent, onError) {
147
+ if (! constructingComputation)
148
+ throw new Error(
149
+ "Tracker.Computation constructor is private; use Tracker.autorun");
150
+ constructingComputation = false;
151
+
152
+ var self = this;
153
+
154
+ // http://docs.meteor.com/#computation_stopped
155
+
156
+ /**
157
+ * @summary True if this computation has been stopped.
158
+ * @locus Client
159
+ * @memberOf Tracker.Computation
160
+ * @instance
161
+ * @name stopped
162
+ */
163
+ self.stopped = false;
164
+
165
+ // http://docs.meteor.com/#computation_invalidated
166
+
167
+ /**
168
+ * @summary True if this computation has been invalidated (and not yet rerun), or if it has been stopped.
169
+ * @locus Client
170
+ * @memberOf Tracker.Computation
171
+ * @instance
172
+ * @name invalidated
173
+ * @type {Boolean}
174
+ */
175
+ self.invalidated = false;
176
+
177
+ // http://docs.meteor.com/#computation_firstrun
178
+
179
+ /**
180
+ * @summary True during the initial run of the computation at the time `Tracker.autorun` is called, and false on subsequent reruns and at other times.
181
+ * @locus Client
182
+ * @memberOf Tracker.Computation
183
+ * @instance
184
+ * @name firstRun
185
+ * @type {Boolean}
186
+ */
187
+ self.firstRun = true;
188
+
189
+ self._id = nextId++;
190
+ self._onInvalidateCallbacks = [];
191
+ self._onStopCallbacks = [];
192
+ // the plan is at some point to use the parent relation
193
+ // to constrain the order that computations are processed
194
+ self._parent = parent;
195
+ self._func = f;
196
+ self._onError = onError;
197
+ self._recomputing = false;
198
+
199
+ var errored = true;
200
+ try {
201
+ self._compute();
202
+ errored = false;
203
+ } finally {
204
+ self.firstRun = false;
205
+ if (errored)
206
+ self.stop();
207
+ }
208
+ };
209
+
210
+ // http://docs.meteor.com/#computation_oninvalidate
211
+
212
+ /**
213
+ * @summary Registers `callback` to run when this computation is next invalidated, or runs it immediately if the computation is already invalidated. The callback is run exactly once and not upon future invalidations unless `onInvalidate` is called again after the computation becomes valid again.
214
+ * @locus Client
215
+ * @param {Function} callback Function to be called on invalidation. Receives one argument, the computation that was invalidated.
216
+ */
217
+ Tracker.Computation.prototype.onInvalidate = function (f) {
218
+ var self = this;
219
+
220
+ if (typeof f !== 'function')
221
+ throw new Error("onInvalidate requires a function");
222
+
223
+ if (self.invalidated) {
224
+ Tracker.nonreactive(function () {
225
+ withNoYieldsAllowed(f)(self);
226
+ });
227
+ } else {
228
+ self._onInvalidateCallbacks.push(f);
229
+ }
230
+ };
231
+
232
+ /**
233
+ * @summary Registers `callback` to run when this computation is stopped, or runs it immediately if the computation is already stopped. The callback is run after any `onInvalidate` callbacks.
234
+ * @locus Client
235
+ * @param {Function} callback Function to be called on stop. Receives one argument, the computation that was stopped.
236
+ */
237
+ Tracker.Computation.prototype.onStop = function (f) {
238
+ var self = this;
239
+
240
+ if (typeof f !== 'function')
241
+ throw new Error("onStop requires a function");
242
+
243
+ if (self.stopped) {
244
+ Tracker.nonreactive(function () {
245
+ withNoYieldsAllowed(f)(self);
246
+ });
247
+ } else {
248
+ self._onStopCallbacks.push(f);
249
+ }
250
+ };
251
+
252
+ // http://docs.meteor.com/#computation_invalidate
253
+
254
+ /**
255
+ * @summary Invalidates this computation so that it will be rerun.
256
+ * @locus Client
257
+ */
258
+ Tracker.Computation.prototype.invalidate = function () {
259
+ var self = this;
260
+ if (! self.invalidated) {
261
+ // if we're currently in _recompute(), don't enqueue
262
+ // ourselves, since we'll rerun immediately anyway.
263
+ if (! self._recomputing && ! self.stopped) {
264
+ requireFlush();
265
+ pendingComputations.push(this);
266
+ }
267
+
268
+ self.invalidated = true;
269
+
270
+ // callbacks can't add callbacks, because
271
+ // self.invalidated === true.
272
+ for(var i = 0, f; f = self._onInvalidateCallbacks[i]; i++) {
273
+ Tracker.nonreactive(function () {
274
+ withNoYieldsAllowed(f)(self);
275
+ });
276
+ }
277
+ self._onInvalidateCallbacks = [];
278
+ }
279
+ };
280
+
281
+ // http://docs.meteor.com/#computation_stop
282
+
283
+ /**
284
+ * @summary Prevents this computation from rerunning.
285
+ * @locus Client
286
+ */
287
+ Tracker.Computation.prototype.stop = function () {
288
+ var self = this;
289
+
290
+ if (! self.stopped) {
291
+ self.stopped = true;
292
+ self.invalidate();
293
+ for(var i = 0, f; f = self._onStopCallbacks[i]; i++) {
294
+ Tracker.nonreactive(function () {
295
+ withNoYieldsAllowed(f)(self);
296
+ });
297
+ }
298
+ self._onStopCallbacks = [];
299
+ }
300
+ };
301
+
302
+ Tracker.Computation.prototype._compute = function () {
303
+ var self = this;
304
+ self.invalidated = false;
305
+
306
+ var previous = Tracker.currentComputation;
307
+ setCurrentComputation(self);
308
+ var previousInCompute = inCompute;
309
+ inCompute = true;
310
+ try {
311
+ withNoYieldsAllowed(self._func)(self);
312
+ } finally {
313
+ setCurrentComputation(previous);
314
+ inCompute = previousInCompute;
315
+ }
316
+ };
317
+
318
+ Tracker.Computation.prototype._needsRecompute = function () {
319
+ var self = this;
320
+ return self.invalidated && ! self.stopped;
321
+ };
322
+
323
+ Tracker.Computation.prototype._recompute = function () {
324
+ var self = this;
325
+
326
+ self._recomputing = true;
327
+ try {
328
+ if (self._needsRecompute()) {
329
+ try {
330
+ self._compute();
331
+ } catch (e) {
332
+ if (self._onError) {
333
+ self._onError(e);
334
+ } else {
335
+ _throwOrLog("recompute", e);
336
+ }
337
+ }
338
+ }
339
+ } finally {
340
+ self._recomputing = false;
341
+ }
342
+ };
343
+
344
+ /**
345
+ * @summary Process the reactive updates for this computation immediately
346
+ * and ensure that the computation is rerun. The computation is rerun only
347
+ * if it is invalidated.
348
+ * @locus Client
349
+ */
350
+ Tracker.Computation.prototype.flush = function () {
351
+ var self = this;
352
+
353
+ if (self._recomputing)
354
+ return;
355
+
356
+ self._recompute();
357
+ };
358
+
359
+ /**
360
+ * @summary Causes the function inside this computation to run and
361
+ * synchronously process all reactive updtes.
362
+ * @locus Client
363
+ */
364
+ Tracker.Computation.prototype.run = function () {
365
+ var self = this;
366
+ self.invalidate();
367
+ self.flush();
368
+ };
369
+
370
+ //
371
+ // http://docs.meteor.com/#tracker_dependency
372
+
373
+ /**
374
+ * @summary A Dependency represents an atomic unit of reactive data that a
375
+ * computation might depend on. Reactive data sources such as Session or
376
+ * Minimongo internally create different Dependency objects for different
377
+ * pieces of data, each of which may be depended on by multiple computations.
378
+ * When the data changes, the computations are invalidated.
379
+ * @class
380
+ * @instanceName dependency
381
+ */
382
+ Tracker.Dependency = function () {
383
+ this._dependentsById = {};
384
+ };
385
+
386
+ // http://docs.meteor.com/#dependency_depend
387
+ //
388
+ // Adds `computation` to this set if it is not already
389
+ // present. Returns true if `computation` is a new member of the set.
390
+ // If no argument, defaults to currentComputation, or does nothing
391
+ // if there is no currentComputation.
392
+
393
+ /**
394
+ * @summary Declares that the current computation (or `fromComputation` if given) depends on `dependency`. The computation will be invalidated the next time `dependency` changes.
395
+
396
+ If there is no current computation and `depend()` is called with no arguments, it does nothing and returns false.
397
+
398
+ Returns true if the computation is a new dependent of `dependency` rather than an existing one.
399
+ * @locus Client
400
+ * @param {Tracker.Computation} [fromComputation] An optional computation declared to depend on `dependency` instead of the current computation.
401
+ * @returns {Boolean}
402
+ */
403
+ Tracker.Dependency.prototype.depend = function (computation) {
404
+ if (! computation) {
405
+ if (! Tracker.active)
406
+ return false;
407
+
408
+ computation = Tracker.currentComputation;
409
+ }
410
+ var self = this;
411
+ var id = computation._id;
412
+ if (! (id in self._dependentsById)) {
413
+ self._dependentsById[id] = computation;
414
+ computation.onInvalidate(function () {
415
+ delete self._dependentsById[id];
416
+ });
417
+ return true;
418
+ }
419
+ return false;
420
+ };
421
+
422
+ // http://docs.meteor.com/#dependency_changed
423
+
424
+ /**
425
+ * @summary Invalidate all dependent computations immediately and remove them as dependents.
426
+ * @locus Client
427
+ */
428
+ Tracker.Dependency.prototype.changed = function () {
429
+ var self = this;
430
+ for (var id in self._dependentsById)
431
+ self._dependentsById[id].invalidate();
432
+ };
433
+
434
+ // http://docs.meteor.com/#dependency_hasdependents
435
+
436
+ /**
437
+ * @summary True if this Dependency has one or more dependent Computations, which would be invalidated if this Dependency were to change.
438
+ * @locus Client
439
+ * @returns {Boolean}
440
+ */
441
+ Tracker.Dependency.prototype.hasDependents = function () {
442
+ var self = this;
443
+ for(var id in self._dependentsById)
444
+ return true;
445
+ return false;
446
+ };
447
+
448
+ // http://docs.meteor.com/#tracker_flush
449
+
450
+ /**
451
+ * @summary Process all reactive updates immediately and ensure that all invalidated computations are rerun.
452
+ * @locus Client
453
+ */
454
+ Tracker.flush = function (options) {
455
+ Tracker._runFlush({ finishSynchronously: true,
456
+ throwFirstError: options && options._throwFirstError });
457
+ };
458
+
459
+ // Run all pending computations and afterFlush callbacks. If we were not called
460
+ // directly via Tracker.flush, this may return before they're all done to allow
461
+ // the event loop to run a little before continuing.
462
+ Tracker._runFlush = function (options) {
463
+ // XXX What part of the comment below is still true? (We no longer
464
+ // have Spark)
465
+ //
466
+ // Nested flush could plausibly happen if, say, a flush causes
467
+ // DOM mutation, which causes a "blur" event, which runs an
468
+ // app event handler that calls Tracker.flush. At the moment
469
+ // Spark blocks event handlers during DOM mutation anyway,
470
+ // because the LiveRange tree isn't valid. And we don't have
471
+ // any useful notion of a nested flush.
472
+ //
473
+ // https://app.asana.com/0/159908330244/385138233856
474
+ if (inFlush)
475
+ throw new Error("Can't call Tracker.flush while flushing");
476
+
477
+ if (inCompute)
478
+ throw new Error("Can't flush inside Tracker.autorun");
479
+
480
+ options = options || {};
481
+
482
+ inFlush = true;
483
+ willFlush = true;
484
+ throwFirstError = !! options.throwFirstError;
485
+
486
+ var recomputedCount = 0;
487
+ var finishedTry = false;
488
+ try {
489
+ while (pendingComputations.length ||
490
+ afterFlushCallbacks.length) {
491
+
492
+ // recompute all pending computations
493
+ while (pendingComputations.length) {
494
+ var comp = pendingComputations.shift();
495
+ comp._recompute();
496
+ if (comp._needsRecompute()) {
497
+ pendingComputations.unshift(comp);
498
+ }
499
+
500
+ if (! options.finishSynchronously && ++recomputedCount > 1000) {
501
+ finishedTry = true;
502
+ return;
503
+ }
504
+ }
505
+
506
+ if (afterFlushCallbacks.length) {
507
+ // call one afterFlush callback, which may
508
+ // invalidate more computations
509
+ var func = afterFlushCallbacks.shift();
510
+ try {
511
+ func();
512
+ } catch (e) {
513
+ _throwOrLog("afterFlush", e);
514
+ }
515
+ }
516
+ }
517
+ finishedTry = true;
518
+ } finally {
519
+ if (! finishedTry) {
520
+ // we're erroring due to throwFirstError being true.
521
+ inFlush = false; // needed before calling `Tracker.flush()` again
522
+ // finish flushing
523
+ Tracker._runFlush({
524
+ finishSynchronously: options.finishSynchronously,
525
+ throwFirstError: false
526
+ });
527
+ }
528
+ willFlush = false;
529
+ inFlush = false;
530
+ if (pendingComputations.length || afterFlushCallbacks.length) {
531
+ // We're yielding because we ran a bunch of computations and we aren't
532
+ // required to finish synchronously, so we'd like to give the event loop a
533
+ // chance. We should flush again soon.
534
+ if (options.finishSynchronously) {
535
+ throw new Error("still have more to do?"); // shouldn't happen
536
+ }
537
+ setTimeout(requireFlush, 10);
538
+ }
539
+ }
540
+ };
541
+
542
+ // http://docs.meteor.com/#tracker_autorun
543
+ //
544
+ // Run f(). Record its dependencies. Rerun it whenever the
545
+ // dependencies change.
546
+ //
547
+ // Returns a new Computation, which is also passed to f.
548
+ //
549
+ // Links the computation to the current computation
550
+ // so that it is stopped if the current computation is invalidated.
551
+
552
+ /**
553
+ * @callback Tracker.ComputationFunction
554
+ * @param {Tracker.Computation}
555
+ */
556
+ /**
557
+ * @summary Run a function now and rerun it later whenever its dependencies
558
+ * change. Returns a Computation object that can be used to stop or observe the
559
+ * rerunning.
560
+ * @locus Client
561
+ * @param {Tracker.ComputationFunction} runFunc The function to run. It receives
562
+ * one argument: the Computation object that will be returned.
563
+ * @param {Object} [options]
564
+ * @param {Function} options.onError Optional. The function to run when an error
565
+ * happens in the Computation. The only argument it recieves is the Error
566
+ * thrown. Defaults to the error being logged to the console.
567
+ * @returns {Tracker.Computation}
568
+ */
569
+ Tracker.autorun = function (f, options) {
570
+ if (typeof f !== 'function')
571
+ throw new Error('Tracker.autorun requires a function argument');
572
+
573
+ options = options || {};
574
+
575
+ constructingComputation = true;
576
+ var c = new Tracker.Computation(
577
+ f, Tracker.currentComputation, options.onError);
578
+
579
+ if (Tracker.active)
580
+ Tracker.onInvalidate(function () {
581
+ c.stop();
582
+ });
583
+
584
+ return c;
585
+ };
586
+
587
+ // http://docs.meteor.com/#tracker_nonreactive
588
+ //
589
+ // Run `f` with no current computation, returning the return value
590
+ // of `f`. Used to turn off reactivity for the duration of `f`,
591
+ // so that reactive data sources accessed by `f` will not result in any
592
+ // computations being invalidated.
593
+
594
+ /**
595
+ * @summary Run a function without tracking dependencies.
596
+ * @locus Client
597
+ * @param {Function} func A function to call immediately.
598
+ */
599
+ Tracker.nonreactive = function (f) {
600
+ var previous = Tracker.currentComputation;
601
+ setCurrentComputation(null);
602
+ try {
603
+ return f();
604
+ } finally {
605
+ setCurrentComputation(previous);
606
+ }
607
+ };
608
+
609
+ // http://docs.meteor.com/#tracker_oninvalidate
610
+
611
+ /**
612
+ * @summary Registers a new [`onInvalidate`](#computation_oninvalidate) callback on the current computation (which must exist), to be called immediately when the current computation is invalidated or stopped.
613
+ * @locus Client
614
+ * @param {Function} callback A callback function that will be invoked as `func(c)`, where `c` is the computation on which the callback is registered.
615
+ */
616
+ Tracker.onInvalidate = function (f) {
617
+ if (! Tracker.active)
618
+ throw new Error("Tracker.onInvalidate requires a currentComputation");
619
+
620
+ Tracker.currentComputation.onInvalidate(f);
621
+ };
622
+
623
+ // http://docs.meteor.com/#tracker_afterflush
624
+
625
+ /**
626
+ * @summary Schedules a function to be called during the next flush, or later in the current flush if one is in progress, after all invalidated computations have been rerun. The function will be run once and not on subsequent flushes unless `afterFlush` is called again.
627
+ * @locus Client
628
+ * @param {Function} callback A function to call at flush time.
629
+ */
630
+ Tracker.afterFlush = function (f) {
631
+ afterFlushCallbacks.push(f);
632
+ requireFlush();
633
+ };