1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  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 /**
  9   @class
 10 
 11   A Timer executes a method after a defined period of time.  Timers are
 12   significantly more efficient than using setTimeout() or setInterval()
 13   because they are cooperatively scheduled using the run loop.  Timers are
 14   also gauranteed to fire at the same time, making it far easier to keep
 15   multiple timers in sync.
 16 
 17   ## Overview
 18 
 19   Timers were created for SproutCore as a way to efficiently defer execution
 20   of code fragments for use in Animations, event handling, and other tasks.
 21 
 22   Browsers are typically fairly inconsistent about when they will fire a
 23   timeout or interval based on what the browser is currently doing.  Timeouts
 24   and intervals are also fairly expensive for a browser to execute, which
 25   means if you schedule a large number of them it can quickly slow down the
 26   browser considerably.
 27 
 28   Timers, on the other handle, are scheduled cooperatively using the
 29   SC.RunLoop, which uses exactly one timeout to fire itself when needed and
 30   then executes by timers that need to fire on its own.  This approach can
 31   be many times faster than using timers and guarantees that timers scheduled
 32   to execute at the same time generally will do so, keeping animations and
 33   other operations in sync.
 34 
 35   ## Scheduling a Timer
 36 
 37   To schedule a basic timer, you can simply call SC.Timer.schedule() with
 38   a target and action you wish to have invoked:
 39 
 40       var timer = SC.Timer.schedule({
 41         target: myObject, action: 'timerFired', interval: 100
 42       });
 43 
 44   When this timer fires, it will call the timerFired() method on myObject.
 45 
 46   In addition to calling a method on a particular object, you can also use
 47   a timer to execute a variety of other types of code:
 48 
 49    - If you include an action name, but not a target object, then the action will be passed down the responder chain.
 50    - If you include a property path for the action property (e.g. 'MyApp.someController.someMethod'), then the method you name will be executed.
 51    - If you include a function in the action property, then the function will be executed.  If you also include a target object, the function will be called with this set to the target object.
 52 
 53   In general these properties are read-only.  Changing an interval, target,
 54   or action after creating a timer will have an unknown effect.
 55 
 56   ## Scheduling Repeating Timers
 57 
 58   In addition to scheduling one time timers, you can also schedule timers to
 59   execute periodically until some termination date.  You make a timer
 60   repeating by adding the repeats: YES property:
 61 
 62       var timer = SC.Timer.schedule({
 63         target: myObject,
 64         action: 'updateAnimation',
 65         interval: 100,
 66         repeats: YES,
 67         until: Time.now() + 1000
 68       }) ;
 69 
 70   The above example will execute the myObject.updateAnimation() every 100msec
 71   for 1 second from the current time.
 72 
 73   If you want a timer to repeat without expiration, you can simply omit the
 74   until: property.  The timer will then repeat until you invalidate it.
 75 
 76   ## Pausing and Invalidating Timers
 77 
 78   If you have created a timer but you no longer want it to execute, you can
 79   call the invalidate() method on it.  This will remove the timer from the
 80   run loop and clear certain properties so that it will not run again.
 81 
 82   You can use the invalidate() method on both repeating and one-time timers.
 83 
 84   If you do not want to invalidate a timer completely but you just want to
 85   stop the timer from execution temporarily, you can alternatively set the
 86   isPaused property to YES:
 87 
 88       timer.set('isPaused', YES) ;
 89       // Perform some critical function; timer will not execute
 90       timer.set('isPaused', NO) ;
 91 
 92   When a timer is paused, it will be scheduled and will fire like normal,
 93   but it will not actually execute the action method when it fires.  For a
 94   one time timer, this means that if you have the timer paused when it fires,
 95   it may never actually execute the action method.  For repeating timers,
 96   this means the timer will remain scheduled but simply will not execute its
 97   action while the timer is paused.
 98 
 99   ## Firing Timers
100 
101   If you need a timer to execute immediately, you can always call the fire()
102   method yourself.  This will execute the timer action, if the timer is not
103   paused.  For a one time timer, it will also invalidate the timer and remove
104   it from the run loop.  Repeating timers can be fired anytime and it will
105   not interrupt their regular scheduled times.
106 
107 
108   @extends SC.Object
109   @author Charles Jolley
110   @version 1.0
111   @since version 1.0
112 */
113 SC.Timer = SC.Object.extend(
114 /** @scope SC.Timer.prototype */ {
115 
116   /**
117     The target object whose method will be invoked when the time fires.
118 
119     You can set either a target/action property or you can pass a specific
120     method.
121 
122     @type {Object}
123     @field
124   */
125   target: null,
126 
127   /**
128     The action to execute.
129 
130     The action can be a method name, a property path, or a function.  If you
131     pass a method name, it will be invoked on the target object or it will
132     be called up the responder chain if target is null.  If you pass a
133     property path and it resolves to a function then the function will be
134     called.  If you pass a function instead, then the function will be
135     called in the context of the target object.
136 
137     @type {String, Function}
138   */
139   action: null,
140 
141   /**
142     Set if the timer should be created from a memory pool.  Normally you will
143     want to leave this set, but if you plan to use bindings or observers with
144     this timer, then you must set isPooled to NO to avoid reusing your timer.
145 
146     @type Boolean
147   */
148   isPooled: NO,
149 
150   /**
151     The time interval in milliseconds.
152 
153     You generally set this when you create the timer.  If you do not set it
154     then the timer will fire as soon as possible in the next run loop.
155 
156     @type {Number}
157   */
158   interval: 0,
159 
160   /**
161     Timer start date offset.
162 
163     The start date determines when the timer will be scheduled.  The first
164     time the timer fires will be interval milliseconds after the start
165     date.
166 
167     Generally you will not set this property yourself.  Instead it will be
168     set automatically to the current run loop start date when you schedule
169     the timer.  This ensures that all timers scheduled in the same run loop
170     cycle will execute in the sync with one another.
171 
172     The value of this property is an offset like what you get if you call
173     Date.now().
174 
175     @type {Number}
176   */
177   startTime: null,
178 
179   /**
180     YES if you want the timer to execute repeatedly.
181 
182     @type {Boolean}
183   */
184   repeats: NO,
185 
186   /**
187     Last date when the timer will execute.
188 
189     If you have set repeats to YES, then you can also set this property to
190     have the timer automatically stop executing past a certain date.
191 
192     This property should contain an offset value like startOffset.  However if
193     you set it to a Date object on create, it will be converted to an offset
194     for you.
195 
196     If this property is null, then the timer will continue to repeat until you
197     call invalidate().
198 
199     @type {Date, Number}
200   */
201   until: null,
202 
203   /**
204     Set to YES to pause the timer.
205 
206     Pausing a timer does not remove it from the run loop, but it will
207     temporarily suspend it from firing.  You should use this property if
208     you will want the timer to fire again the future, but you want to prevent
209     it from firing temporarily.
210 
211     If you are done with a timer, you should call invalidate() instead of
212     setting this property.
213 
214     @type {Boolean}
215   */
216   isPaused: NO,
217 
218   /**
219     YES onces the timer has been scheduled for the first time.
220   */
221   isScheduled: NO,
222 
223   /**
224     YES if the timer can still execute.
225 
226     This read only property will return YES as long as the timer may possibly
227     fire again in the future.  Once a timer has become invalid, it cannot
228     become valid again.
229 
230     @field
231     @type {Boolean}
232   */
233   isValid: YES,
234 
235   /**
236     Set to the current time when the timer last fired.  Used to find the
237     next 'frame' to execute.
238   */
239   lastFireTime: 0,
240 
241   /**
242     Computed property returns the next time the timer should fire.  This
243     property resets each time the timer fires.  Returns -1 if the timer
244     cannot fire again.
245 
246     @type Time
247   */
248   fireTime: function() {
249     if (!this.get('isValid')) { return -1 ; }  // not valid - can't fire
250 
251     // can't fire w/o startTime (set when schedule() is called).
252     var start = this.get('startTime');
253     if (!start || start === 0) { return -1; }
254 
255     // fire interval after start.
256     var interval = this.get('interval'), last = this.get('lastFireTime');
257     if (last < start) { last = start; } // first time to fire
258 
259     // find the next time to fire
260     var next ;
261     if (this.get('repeats')) {
262       if (interval === 0) { // 0 means fire as fast as possible.
263         next = last ; // time to fire immediately!
264 
265       // find the next full interval after start from last fire time.
266       } else {
267         next = start + (Math.floor((last - start) / interval)+1)*interval;
268       }
269 
270     // otherwise, fire only once interval after start
271     } else {
272       next = start + interval ;
273     }
274 
275     // can never have a fireTime after until
276     var until = this.get('until');
277     if (until && until>0 && next>until) next = until;
278 
279     return next ;
280   }.property('interval', 'startTime', 'repeats', 'until', 'isValid', 'lastFireTime').cacheable(),
281 
282   /**
283     Schedules the timer to execute in the runloop.
284 
285     This method is called automatically if you create the timer using the
286     schedule() class method.  If you create the timer manually, you will
287     need to call this method yourself for the timer to execute.
288 
289     @returns {SC.Timer} The receiver
290   */
291   schedule: function() {
292     if (!this.get('isValid')) return this; // nothing to do
293 
294     this.beginPropertyChanges();
295 
296     // if start time was not set explicitly when the timer was created,
297     // get it from the run loop.  This way timer scheduling will always
298     // occur in sync.
299     if (!this.startTime) this.set('startTime', SC.RunLoop.currentRunLoop.get('startTime')) ;
300 
301     // now schedule the timer if the last fire time was < the next valid
302     // fire time.  The first time lastFireTime is 0, so this will always go.
303     var next = this.get('fireTime'), last = this.get('lastFireTime');
304     if (next >= last) {
305       this.set('isScheduled', YES);
306       SC.RunLoop.currentRunLoop.scheduleTimer(this, next);
307     }
308 
309     this.endPropertyChanges() ;
310 
311     return this ;
312   },
313   /**
314     Invalidates the timer so that it will not execute again.  If a timer has
315     been scheduled, it will be removed from the run loop immediately.
316 
317     @returns {SC.Timer} The receiver
318   */
319   invalidate: function() {
320     this.beginPropertyChanges();
321     this.set('isValid', NO);
322 
323     var runLoop = SC.RunLoop.currentRunLoop;
324     if(runLoop) runLoop.cancelTimer(this);
325 
326     this.action = this.target = null ; // avoid memory leaks
327     this.endPropertyChanges();
328 
329     // return to pool...
330     if (this.get('isPooled')) SC.Timer.returnTimerToPool(this);
331     return this ;
332   },
333 
334   /**
335     Immediately fires the timer.
336 
337     If the timer is not-repeating, it will be invalidated.  If it is repeating
338     you can call this method without interrupting its normal schedule.
339 
340     @returns {void}
341   */
342   fire: function() {
343 
344     // this will cause the fireTime to recompute
345     var last = Date.now();
346     this.set('lastFireTime', last);
347 
348     var next = this.get('fireTime');
349 
350     // now perform the fire action unless paused.
351     if (!this.get('isPaused')) this.performAction() ;
352 
353      // reschedule the timer if needed...
354      if (next > last) {
355        this.schedule();
356      } else {
357        this.invalidate();
358      }
359   },
360 
361   /**
362     Actually fires the action. You can override this method if you need
363     to change how the timer fires its action.
364   */
365   performAction: function() {
366     var typeOfAction = SC.typeOf(this.action);
367 
368     // if the action is a function, just try to call it.
369     if (typeOfAction == SC.T_FUNCTION) {
370       this.action.call((this.target || this), this) ;
371 
372     // otherwise, action should be a string.  If it has a period, treat it
373     // like a property path.
374     } else if (typeOfAction === SC.T_STRING) {
375       if (this.action.indexOf('.') >= 0) {
376         var path = this.action.split('.') ;
377         var property = path.pop() ;
378 
379         var target = SC.objectForPropertyPath(path, window) ;
380         var action = target.get ? target.get(property) : target[property];
381         if (action && SC.typeOf(action) == SC.T_FUNCTION) {
382           action.call(target, this) ;
383         } else {
384           throw new Error('%@: Timer could not find a function at %@'.fmt(this, this.action));
385         }
386 
387       // otherwise, try to execute action direction on target or send down
388       // responder chain.
389       } else {
390         SC.RootResponder.responder.sendAction(this.action, this.target, this);
391       }
392     }
393   },
394 
395   init: function() {
396     sc_super();
397 
398     // convert startTime and until to times if they are dates.
399     if (this.startTime instanceof Date) {
400       this.startTime = this.startTime.getTime() ;
401     }
402 
403     if (this.until instanceof Date) {
404       this.until = this.until.getTime() ;
405     }
406   },
407 
408   /** @private - Default values to reset reused timers to. */
409   RESET_DEFAULTS: {
410     target: null, action: null,
411     isPooled: NO, isPaused: NO, isScheduled: NO, isValid: YES,
412     interval: 0, repeats: NO, until: null,
413     startTime: null, lastFireTime: 0
414   },
415 
416   /**
417     Resets the timer settings with the new settings.  This is the method
418     called by the Timer pool when a timer is reused.  You will not normally
419     call this method yourself, though you could override it if you need to
420     reset additional properties when a timer is reused.
421 
422     @params {Hash} props properties to copy over
423     @returns {SC.Timer} receiver
424   */
425   reset: function(props) {
426     if (!props) props = SC.EMPTY_HASH;
427 
428     // note: we copy these properties manually just to make them fast.  we
429     // don't expect you to use observers on a timer object if you are using
430     // pooling anyway so this won't matter.  Still notify of property change
431     // on fireTime to clear its cache.
432     this.propertyWillChange('fireTime');
433     var defaults = this.RESET_DEFAULTS ;
434     for(var key in defaults) {
435       if (!defaults.hasOwnProperty(key)) continue ;
436       this[key] = SC.none(props[key]) ? defaults[key] : props[key];
437     }
438     this.propertyDidChange('fireTime');
439     return this ;
440   },
441 
442   // ..........................................................
443   // TIMER QUEUE SUPPORT
444   //
445 
446   /** @private - removes the timer from its current timerQueue if needed.
447     return value is the new "root" timer.
448   */
449   removeFromTimerQueue: function(timerQueueRoot) {
450     var prev = this._timerQueuePrevious, next = this._timerQueueNext ;
451 
452     if (!prev && !next && timerQueueRoot !== this) return timerQueueRoot ; // not in a queue...
453 
454     // else, patch up to remove...
455     if (prev) prev._timerQueueNext = next ;
456     if (next) next._timerQueuePrevious = prev ;
457     this._timerQueuePrevious = this._timerQueueNext = null ;
458     return (timerQueueRoot === this) ? next : timerQueueRoot ;
459   },
460 
461   /** @private - schedules the timer in the queue based on the runtime. */
462   scheduleInTimerQueue: function(timerQueueRoot, runTime) {
463     this._timerQueueRunTime = runTime ;
464 
465     // find the place to begin
466     var beforeNode = timerQueueRoot;
467     var afterNode = null ;
468     while(beforeNode && beforeNode._timerQueueRunTime < runTime) {
469       afterNode = beforeNode ;
470       beforeNode = beforeNode._timerQueueNext;
471     }
472 
473     if (afterNode) {
474       afterNode._timerQueueNext = this ;
475       this._timerQueuePrevious = afterNode ;
476     }
477 
478     if (beforeNode) {
479       beforeNode._timerQueuePrevious = this ;
480       this._timerQueueNext = beforeNode ;
481     }
482 
483     // I am the new root if beforeNode === root
484     return (beforeNode === timerQueueRoot) ? this : timerQueueRoot ;
485   },
486 
487   /** @private
488     adds the receiver to the passed array of expired timers based on the
489     current time and then recursively calls the next timer.  Returns the
490     first timer that is not expired.  This is faster than iterating through
491     the timers because it does some faster cleanup of the nodes.
492   */
493   collectExpiredTimers: function(timers, now) {
494     if (this._timerQueueRunTime > now) return this ; // not expired!
495     timers.push(this);  // add to queue.. fixup next. assume we are root.
496     var next = this._timerQueueNext ;
497     this._timerQueueNext = null;
498     if (next) next._timerQueuePrevious = null;
499     return next ? next.collectExpiredTimers(timers, now) : null;
500   }
501 
502 }) ;
503 
504 /** @scope SC.Timer */
505 
506 /*
507   Created a new timer with the passed properties and schedules it to
508   execute.  This is the same as calling SC.Time.create({ props }).schedule().
509 
510   Note that unless you explicitly set isPooled to NO, this timer will be
511   pulled from a shared memory pool of timers.  You cannot using bindings or
512   observers on these timers as they may be reused for future timers at any
513   time.
514 
515   @params {Hash} props Any properties you want to set on the timer.
516   @returns {SC.Timer} new timer instance.
517 */
518 SC.Timer.schedule = function(props) {
519   // get the timer.
520   var timer ;
521   if (!props || SC.none(props.isPooled) || props.isPooled) {
522     timer = this.timerFromPool(props);
523   } else timer = this.create(props);
524   return timer.schedule();
525 } ;
526 
527 /**
528   Returns a new timer from the timer pool, copying the passed properties onto
529   the timer instance.  If the timer pool is currently empty, this will return
530   a new instance.
531 */
532 SC.Timer.timerFromPool = function(props) {
533   var timers = this._timerPool;
534   if (!timers) timers = this._timerPool = [] ;
535   var timer = timers.pop();
536   if (!timer) timer = this.create();
537   return timer.reset(props) ;
538 };
539 
540 /**
541   Returns a timer instance to the timer pool for later use.  This is done
542   automatically when a timer is invalidated if isPooled is YES.
543 */
544 SC.Timer.returnTimerToPool = function(timer) {
545   if (!this._timerPool) this._timerPool = [];
546 
547   this._timerPool.push(timer);
548   return this ;
549 };
550 
551 
552