1 // ==========================================================================
  2 // Project:   SproutCore Costello - Property Observing Library
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require('ext/function');
  9 sc_require('private/observer_set');
 10 
 11 
 12 //@if(debug)
 13 // When in debug mode, users can log deferred calls (such as .invokeOnce()) by
 14 // setting SC.LOG_DEFERRED_CALLS.  We'll declare the variable explicitly to make
 15 // life easier for people who want to enter it inside consoles that auto-
 16 // complete.
 17 if (!SC.LOG_DEFERRED_CALLS) SC.LOG_DEFERRED_CALLS = false;
 18 //@endif
 19 
 20 /**
 21   @class
 22 
 23   The run loop provides a universal system for coordinating events within
 24   your application.  The run loop processes timers as well as pending
 25   observer notifications within your application.
 26 
 27   To use a RunLoop within your application, you should make sure your event
 28   handlers always begin and end with SC.RunLoop.begin() and SC.RunLoop.end()
 29 
 30   The RunLoop is important because bindings do not fire until the end of
 31   your run loop is reached.  This improves the performance of your
 32   application.
 33 
 34   Example:
 35 
 36   This is how you could write your mouseup handler in jQuery:
 37 
 38         $('#okButton').on('click', function () {
 39           SC.run(function () {
 40 
 41             // handle click event...
 42 
 43           }); // allows bindings to trigger...
 44         });
 45 
 46   @extends SC.Object
 47   @since SproutCore 1.0
 48 */
 49 SC.RunLoop = SC.Object.extend(
 50   /** @scope SC.RunLoop.prototype */ {
 51 
 52   /**
 53     Call this method whenver you begin executing code.
 54 
 55     This is typically invoked automatically for you from event handlers and
 56     the timeout handler.  If you call setTimeout() or setInterval() yourself,
 57     you may need to invoke this yourself.
 58 
 59     @returns {SC.RunLoop} receiver
 60   */
 61   beginRunLoop: function () {
 62     this._start = new Date().getTime(); // can't use Date.now() in runtime
 63 
 64     //@if(debug)
 65     if (SC.LOG_OBSERVERS) {
 66       SC.Logger.log("-- SC.RunLoop.beginRunLoop at %@".fmt(this._start));
 67     }
 68     //@endif
 69 
 70     this._runLoopInProgress = YES;
 71     this._flushinvokeNextQueue();
 72     return this;
 73   },
 74 
 75   /**
 76     YES when a run loop is in progress
 77 
 78     @property
 79     @type Boolean
 80   */
 81   isRunLoopInProgress: function () {
 82     return this._runLoopInProgress;
 83   }.property(),
 84 
 85   /**
 86     Call this method whenever you are done executing code.
 87 
 88     This is typically invoked automatically for you from event handlers and
 89     the timeout handler.  If you call setTimeout() or setInterval() yourself
 90     you may need to invoke this yourself.
 91 
 92     @returns {SC.RunLoop} receiver
 93   */
 94   endRunLoop: function () {
 95     // at the end of a runloop, flush all the delayed actions we may have
 96     // stored up.  Note that if any of these queues actually run, we will
 97     // step through all of them again.  This way any changes get flushed
 98     // out completely.
 99 
100     //@if(debug)
101     if (SC.LOG_OBSERVERS) {
102       SC.Logger.log("-- SC.RunLoop.endRunLoop ~ flushing application queues");
103     }
104     //@endif
105 
106     this.flushAllPending();
107 
108     this._start = null;
109 
110     //@if(debug)
111     if (SC.LOG_OBSERVERS) {
112       SC.Logger.log("-- SC.RunLoop.endRunLoop ~ End");
113     }
114     //@endif
115 
116     SC.RunLoop.lastRunLoopEnd = Date.now();
117     this._runLoopInProgress = NO;
118 
119     // If there are members in the invokeNextQueue, be sure to schedule another
120     // run of the run loop.
121     var queue = this._invokeNextQueue;
122     if (queue && queue.getMembers().length) {
123       this._invokeNextTimeout = this.scheduleRunLoop(SC.RunLoop.lastRunLoopEnd);
124     }
125 
126     return this;
127   },
128 
129   /**
130     Repeatedly flushes all bindings, observers, and other queued functions until all queues are empty.
131   */
132   flushAllPending: function () {
133     var didChange;
134 
135     do {
136       didChange = this.flushApplicationQueues();
137       if (!didChange) didChange = this._flushinvokeLastQueue();
138     } while (didChange);
139   },
140 
141 
142   /**
143     Invokes the passed target/method pair once at the end of the runloop.
144     You can call this method as many times as you like and the method will
145     only be invoked once.
146 
147     Usually you will not call this method directly but use invokeOnce()
148     defined on SC.Object.
149 
150     Note that in development mode only, the object and method that call this
151     method will be recorded, for help in debugging scheduled code.
152 
153     @param {Object} target
154     @param {Function} method
155     @returns {SC.RunLoop} receiver
156   */
157   invokeOnce: function (target, method) {
158     //@if(debug)
159     // Calling invokeOnce outside of a run loop causes problems when coupled
160     // with time dependent methods invokeNext and invokeLater, because the
161     // time dependent methods execute at the start of the next run of the run
162     // loop and may fire before the invokeOnce code in this case.
163     var isRunLoopInProgress = this.get('isRunLoopInProgress');
164     if (!isRunLoopInProgress) {
165       SC.warn("Developer Warning: invokeOnce called outside of the run loop, which may cause unexpected problems. Check the stack trace below for what triggered this, and see http://blog.sproutcore.com/1-10-upgrade-invokefoo/ for more.");
166       console.trace();
167     }
168     //@endif
169     // normalize
170     if (method === undefined) {
171       method = target;
172       target = this;
173     }
174 
175     var deferredCallLoggingInfo;      // Used only in debug mode
176 
177     //@if(debug)
178     // When in debug mode, SC.Object#invokeOnce() will pass in the originating
179     // method, target, and stack.  That way, we'll record the interesting parts
180     // rather than having most of these calls seemingly coming from
181     // SC.Object#invokeOnce().
182     //
183     // If it was not specified, we'll record the originating function ourselves.
184     var shouldLog = SC.LOG_DEFERRED_CALLS;
185     if (shouldLog) {
186       var originatingTarget = arguments[2],
187           originatingMethod = arguments[3],
188           originatingStack  = arguments[4];
189 
190       if (!originatingTarget) originatingTarget = null;   // More obvious when debugging
191       if (!originatingMethod) {
192         originatingStack  = SC._getRecentStack();
193         originatingMethod = originatingStack[0];
194       }
195 
196       deferredCallLoggingInfo = {
197         originatingTarget: originatingTarget,
198         originatingMethod: originatingMethod,
199         originatingStack:  originatingStack
200       };
201     }
202     //@endif
203 
204     if (typeof method === "string") method = target[method];
205     if (!this._invokeQueue) this._invokeQueue = SC.ObserverSet.create();
206     if (method) this._invokeQueue.add(target, method, undefined, deferredCallLoggingInfo);
207     return this;
208   },
209 
210   /**
211     Invokes the passed target/method pair at the very end of the run loop,
212     once all other delayed invoke queues have been flushed.  Use this to
213     schedule cleanup methods at the end of the run loop once all other work
214     (including rendering) has finished.
215 
216     If you call this with the same target/method pair multiple times it will
217     only invoke the pair only once at the end of the runloop.
218 
219     Usually you will not call this method directly but use invokeLast()
220     defined on SC.Object.
221 
222     Note that in development mode only, the object and method that call this
223     method will be recorded, for help in debugging scheduled code.
224 
225     @param {Object} target
226     @param {Function} method
227     @returns {SC.RunLoop} receiver
228   */
229   invokeLast: function (target, method) {
230     //@if(debug)
231     // Calling invokeLast outside of a run loop causes problems when coupled
232     // with time dependent methods invokeNext and invokeLater, because the
233     // time dependent methods execute at the start of the next run of the run
234     // loop and may fire before the invokeLast code in this case.
235     var isRunLoopInProgress = this.get('isRunLoopInProgress');
236     if (!isRunLoopInProgress) {
237       SC.warn("Developer Warning: invokeLast called outside of the run loop, which may cause unexpected problems. Check the stack trace below for what triggered this, and see http://blog.sproutcore.com/1-10-upgrade-invokefoo/ for more.");
238       console.trace();
239     }
240     //@endif
241 
242     // normalize
243     if (method === undefined) {
244       method = target;
245       target = this;
246     }
247 
248     var deferredCallLoggingInfo;      // Used only in debug mode
249 
250     //@if(debug)
251     // When in debug mode, SC.Object#invokeOnce() will pass in the originating
252     // method, target, and stack.  That way, we'll record the interesting parts
253     // rather than having most of these calls seemingly coming from
254     // SC.Object#invokeOnce().
255     //
256     // If it was not specified, we'll record the originating function ourselves.
257     var shouldLog = SC.LOG_DEFERRED_CALLS;
258     if (shouldLog) {
259       var originatingTarget = arguments[2],
260           originatingMethod = arguments[3],
261           originatingStack  = arguments[4];
262 
263       if (!originatingTarget) originatingTarget = null;   // More obvious when debugging
264       if (!originatingMethod) {
265         originatingStack  = SC._getRecentStack();
266         originatingMethod = originatingStack[0];
267       }
268 
269       deferredCallLoggingInfo = {
270         originatingTarget: originatingTarget,
271         originatingMethod: originatingMethod,
272         originatingStack:  originatingStack
273       };
274     }
275     //@endif
276 
277 
278     if (typeof method === "string") method = target[method];
279     if (!this._invokeLastQueue) this._invokeLastQueue = SC.ObserverSet.create();
280     this._invokeLastQueue.add(target, method, undefined, deferredCallLoggingInfo);
281     return this;
282   },
283 
284   /**
285     Invokes the passed target/method pair once at the beginning of the next
286     run of the run loop, before any other methods (including events) are
287     processed.  Use this to defer painting to make views more responsive.
288 
289     If you call this with the same target/method pair multiple times it will
290     only invoke the pair only once at the beginning of the next runloop.
291 
292     Usually you will not call this method directly but use invokeNext()
293     defined on SC.Object.
294 
295     @param {Object} target
296     @param {Function} method
297     @returns {SC.RunLoop} receiver
298   */
299   invokeNext: function (target, method) {
300     // normalize
301     if (method === undefined) {
302       method = target;
303       target = this;
304     }
305 
306     if (typeof method === "string") method = target[method];
307     if (!this._invokeNextQueue) this._invokeNextQueue = SC.ObserverSet.create();
308     this._invokeNextQueue.add(target, method);
309     return this;
310   },
311 
312   /**
313     Executes any pending events at the end of the run loop.  This method is
314     called automatically at the end of a run loop to flush any pending
315     queue changes.
316 
317     The default method will invoke any one time methods and then sync any
318     bindings that might have changed.  You can override this method in a
319     subclass if you like to handle additional cleanup.
320 
321     This method must return YES if it found any items pending in its queues
322     to take action on.  endRunLoop will invoke this method repeatedly until
323     the method returns NO.  This way if any if your final executing code
324     causes additional queues to trigger, then can be flushed again.
325 
326     @returns {Boolean} YES if items were found in any queue, NO otherwise
327   */
328   flushApplicationQueues: function () {
329     var hadContent = NO,
330         // execute any methods in the invokeQueue.
331         queue = this._invokeQueue;
332     if (queue && queue.getMembers().length) {
333       this._invokeQueue = null; // reset so that a new queue will be created
334       hadContent = YES; // needs to execute again
335       queue.invokeMethods();
336     }
337 
338     // flush any pending changed bindings.  This could actually trigger a
339     // lot of code to execute.
340     return SC.Binding.flushPendingChanges() || hadContent;
341   },
342 
343   _flushinvokeLastQueue: function () {
344     var queue = this._invokeLastQueue, hadContent = NO;
345     if (queue && queue.getMembers().length) {
346       this._invokeLastQueue = null; // reset queue.
347       hadContent = YES; // has targets!
348       if (hadContent) queue.invokeMethods();
349     }
350     return hadContent;
351   },
352 
353   _flushinvokeNextQueue: function () {
354     var queue = this._invokeNextQueue, hadContent = NO;
355     if (queue && queue.getMembers().length) {
356       this._invokeNextQueue = null; // reset queue.
357       hadContent = YES; // has targets!
358       if (hadContent) queue.invokeMethods();
359     }
360     return hadContent;
361   },
362 
363   /** @private
364     Schedules the run loop to run at the given time.  If the run loop is
365     already scheduled to run earlier nothing will change, but if the run loop
366     is not scheduled or it is scheduled later, then it will be rescheduled
367     to the value of nextTimeoutAt.
368 
369     @returns timeoutID {Number} The ID of the timeout to start the next run of the run loop
370   */
371   scheduleRunLoop: function (nextTimeoutAt) {
372     /*jshint eqnull:true*/
373     // If there is no run loop scheduled or if the scheduled run loop is later, reschedule.
374     if (this._timeoutAt == null || this._timeoutAt > nextTimeoutAt) {
375       // clear existing...
376       if (this._timeout) { clearTimeout(this._timeout); }
377 
378       // reschedule
379       var delay = Math.max(0, nextTimeoutAt - Date.now());
380       this._timeout = setTimeout(this._timeoutDidFire, delay);
381       this._timeoutAt = nextTimeoutAt;
382     }
383 
384     return this._timeout;
385   },
386 
387   /** @private
388     Invoked when a timeout actually fires.  Simply cleanup, then begin and end
389     a runloop. Note that this function will be called with 'this' set to the
390     global context, hence the need to lookup the current run loop.
391   */
392   _timeoutDidFire: function () {
393     var rl = SC.RunLoop.currentRunLoop;
394     rl._timeout = rl._timeoutAt = rl._invokeNextTimeout = null; // cleanup
395     SC.run();  // begin/end runloop to trigger timers.
396   },
397 
398   /** @private Unschedule the run loop that is scheduled for the given timeoutID */
399   unscheduleRunLoop: function () {
400     // Don't unschedule if the timeout is shared with an invokeNext timeout.
401     if (!this._invokeNextTimeout) {
402       clearTimeout(this._timeout);
403       this._timeout = this._timeoutAt = null; // cleanup
404     }
405   }
406 
407 });
408 
409 
410 //@if(debug)
411  /**
412   Will return the recent stack as a hash with numerical keys, for nice output
413   in some browsers’ debuggers.  The “recent” stack is capped at 6 entries.
414 
415   This is used by, amongst other places, SC.LOG_DEFERRED_CALLS.
416 
417   @returns {Hash}
418 */
419 SC._getRecentStack = function () {
420   var currentFunction = arguments.callee.caller,
421       i               = 0,
422       stack           = {},
423       first           = YES,
424       functionName;
425 
426   while (currentFunction  &&  i < 10) {
427     // Skip ourselves!
428     if (first) {
429       first = NO;
430     } else {
431       functionName = currentFunction.displayName || currentFunction.toString().substring(0, 40);
432       stack[i++]   = functionName;
433     }
434     currentFunction = currentFunction.caller;
435   }
436 
437   return stack;
438 };
439 //@endif
440 
441 
442 
443 /**
444   The current run loop.  This is created automatically the first time you
445   call begin().
446 
447   @type SC.RunLoop
448 */
449 SC.RunLoop.currentRunLoop = null;
450 
451 /**
452   The default RunLoop class.  If you choose to extend the RunLoop, you can
453   set this property to make sure your class is used instead.
454 
455   @type Class
456 */
457 SC.RunLoop.runLoopClass = SC.RunLoop;
458 
459 /**
460   Begins a new run loop on the currentRunLoop.  If you are already in a
461   runloop, this method has no effect.
462 
463   @returns {SC.RunLoop} receiver
464 */
465 SC.RunLoop.begin = function () {
466   var runLoop = this.currentRunLoop;
467   if (!runLoop) runLoop = this.currentRunLoop = this.runLoopClass.create();
468   runLoop.beginRunLoop();
469   return this;
470 };
471 
472 /**
473   Ends the run loop on the currentRunLoop.  This will deliver any final
474   pending notifications and schedule any additional necessary cleanup.
475 
476   @returns {SC.RunLoop} receiver
477 */
478 SC.RunLoop.end = function () {
479   var runLoop = this.currentRunLoop;
480   if (!runLoop) {
481     throw new Error("SC.RunLoop.end() called outside of a runloop!");
482   }
483   runLoop.endRunLoop();
484   return this;
485 };
486 
487 
488 /**
489   Call this to kill the current run loop--stopping all propagation of bindings
490   and observers and clearing all timers.
491 
492   This is useful if you are popping up an error catcher: you need a run loop
493   for the error catcher, but you don't want the app itself to continue
494   running.
495 */
496 SC.RunLoop.kill = function () {
497   this.currentRunLoop = this.runLoopClass.create();
498   return this;
499 };
500 
501 /**
502   Returns YES when a run loop is in progress
503 
504   @return {Boolean}
505 */
506 SC.RunLoop.isRunLoopInProgress = function () {
507   if (this.currentRunLoop) return this.currentRunLoop.get('isRunLoopInProgress');
508   return NO;
509 };
510 
511 /**
512   Executes a passed function in the context of a run loop. If called outside a
513   runloop, starts and ends one. If called inside an existing runloop, is
514   simply executes the function unless you force it to create a nested runloop.
515 
516   If an exception is thrown during execution, we give an error catcher the
517   opportunity to handle it before allowing the exception to bubble again.
518 
519   @param {Function} callback callback to execute
520   @param {Object} target context for callback
521   @param {Boolean} if YES, starts/ends a new runloop even if one is already running
522 */
523 SC.run = function (callback, target, forceNested) {
524   var alreadyRunning = SC.RunLoop.isRunLoopInProgress();
525 
526   // Only use a try/catch block if we have an ExceptionHandler
527   // since in some browsers try/catch causes a loss of the backtrace
528   if (SC.ExceptionHandler && SC.ExceptionHandler.enabled) {
529     try {
530       if (forceNested || !alreadyRunning) SC.RunLoop.begin();
531       if (callback) callback.call(target);
532       if (forceNested || !alreadyRunning) SC.RunLoop.end();
533     } catch (e) {
534       var handled = SC.ExceptionHandler.handleException(e);
535 
536       // If the exception was not handled, throw it again so the browser
537       // can deal with it (and potentially use it for debugging).
538       // (We don't throw it in IE because the user will see two errors)
539       if (!handled && !SC.browser.isIE) {
540         throw e;
541       }
542     }
543   } else {
544     if (forceNested || !alreadyRunning) SC.RunLoop.begin();
545     if (callback) callback.call(target);
546     if (forceNested || !alreadyRunning) SC.RunLoop.end();
547   }
548 };
549 
550 /**
551   Wraps the passed function in code that ensures a run loop will
552   surround it when run.
553 */
554 SC.RunLoop.wrapFunction = function (func) {
555   var ret = function () {
556     var alreadyRunning = SC.RunLoop.isRunLoopInProgress();
557     if (!alreadyRunning) SC.RunLoop.begin();
558     var ret = arguments.callee.wrapped.apply(this, arguments);
559     if (!alreadyRunning) SC.RunLoop.end();
560     return ret;
561   };
562   ret.wrapped = func;
563   return ret;
564 };
565