/* Implementation of HTML Timers (setInterval/setTimeout) based on sleep.
*
* This file is provided under the following terms (MIT License):
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Copyright 2012 Kevin Locke <kevin@kevinlocke.name>
*/
/*jslint bitwise: true, evil: true */
/**
* Adds methods to implement the HTML5 WindowTimers interface on a given
* object.
*
* Adds the following methods:
* <ul>
* <li>clearInterval</li>
* <li>clearTimeout</li>
* <li>setInterval</li>
* <li>setTimeout</li>
* </ul>
*
* See http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html
* for the complete specification of these methods.
*
* Example Usage
* Browser/nodeJS compatibility in UnityBase:
* <pre><code>// Note: "this" refers to the global object in this example
* var WindowTimer = require('WindowTimer');
* var timerLoop = WindowTimer.makeWindowTimer(this, function (ms) { sleep(ms); });
*
* setTimeout(function(){console.log('second function end');}, 2000);
* setTimeout(function(){console.log('first function end');}, 1000);
* console.log('before loop');
* timerLoop();
* console.log('after loop');
*
* </code></pre>
*
* For more esoteric uses, timerLoop will return instead of sleeping if passed
* <code>true</code> which will run only events which are pending at the moment
* timerLoop is called:
* <pre><code>// Note: "this" refers to the global object in this example
* var timerLoop = makeWindowTimer(this, java.lang.Thread.sleep);
*
* // Run code which may add intervals/timeouts
*
* while (timerLoop(true)) {
* print("Still waiting...");
* // Do other work here, possibly adding more intervals/timeouts
* }
* </code></pre>
*
* @module WindowTimer
*/
var WindowTimer = {};
/**
* @method makeWindowTimer
*
* @param {Object} target Object to which the methods should be added.
* @param {Function} sleep A function which sleeps for a specified number of
* milliseconds.
* @return {Function} The function which runs the scheduled timers.
*/
function makeWindowTimer(target, sleep) {
"use strict";
var counter = 1,
inCallback = false,
// Map handle -> timer
timersByHandle = {},
// Min-heap of timers by time then handle, index 0 unused
timersByTime = [ null ];
/** Compares timers based on scheduled time and handle. */
function timerCompare(t1, t2) {
// Note: Only need less-than for our uses
return t1.time < t2.time ? -1 :
(t1.time === t2.time && t1.handle < t2.handle ? -1 : 0);
}
/** Fix the heap invariant which may be violated at a given index */
function heapFixDown(heap, i, lesscmp) {
var j, tmp;
j = i * 2;
while (j < heap.length) {
if (j + 1 < heap.length &&
lesscmp(heap[j + 1], heap[j]) < 0) {
j = j + 1;
}
if (lesscmp(heap[i], heap[j]) < 0) {
break;
}
tmp = heap[j];
heap[j] = heap[i];
heap[i] = tmp;
i = j;
j = i * 2;
}
}
/** Fix the heap invariant which may be violated at a given index */
function heapFixUp(heap, i, lesscmp) {
var j, tmp;
while (i > 1) {
j = i >> 1; // Integer div by 2
if (lesscmp(heap[j], heap[i]) < 0) {
break;
}
tmp = heap[j];
heap[j] = heap[i];
heap[i] = tmp;
i = j;
}
}
/** Remove the minimum element from the heap */
function heapPop(heap, lesscmp) {
heap[1] = heap[heap.length - 1];
heap.pop();
heapFixDown(heap, 1, lesscmp);
}
/** Create a timer and schedule code to run at a given time */
function addTimer(code, delay, repeat, argsIfFn) {
var handle, timer;
if (typeof code !== "function") {
code = String(code);
argsIfFn = null;
}
delay = Number(delay) || 0;
if (inCallback) {
delay = Math.max(delay, 4);
}
// Note: Must set handle after argument conversion to properly
// handle conformance test in HTML5 spec.
handle = counter;
counter += 1;
timer = {
args: argsIfFn,
cancel: false,
code: code,
handle: handle,
repeat: repeat ? Math.max(delay, 4) : 0,
time: new Date().getTime() + delay
};
timersByHandle[handle] = timer;
timersByTime.push(timer);
heapFixUp(timersByTime, timersByTime.length - 1, timerCompare);
return handle;
}
/** Cancel an existing timer */
function cancelTimer(handle, repeat) {
var timer;
if (timersByHandle.hasOwnProperty(handle)) {
timer = timersByHandle[handle];
if (repeat === (timer.repeat > 0)) {
timer.cancel = true;
}
}
}
function clearInterval(handle) {
cancelTimer(handle, true);
}
target.clearInterval = clearInterval;
function clearTimeout(handle) {
cancelTimer(handle, false);
}
target.clearTimeout = clearTimeout;
function setInterval(code, delay) {
return addTimer(
code,
delay,
true,
Array.prototype.slice.call(arguments, 2)
);
}
target.setInterval = setInterval;
function setTimeout(code, delay) {
return addTimer(
code,
delay,
false,
Array.prototype.slice.call(arguments, 2)
);
}
target.setTimeout = setTimeout;
return function timerLoop(nonblocking) {
var now, timer;
// Note: index 0 unused in timersByTime
while (timersByTime.length > 1) {
timer = timersByTime[1];
if (timer.cancel) {
delete timersByHandle[timer.handle];
heapPop(timersByTime, timerCompare);
} else {
now = new Date().getTime();
if (timer.time <= now) {
inCallback = true;
try {
if (typeof timer.code === "function") {
timer.code.apply(undefined, timer.args);
} else {
eval(timer.code);
}
} finally {
inCallback = false;
}
if (timer.repeat > 0 && !timer.cancel) {
timer.time += timer.repeat;
heapFixDown(timersByTime, 1, timerCompare);
} else {
delete timersByHandle[timer.handle];
heapPop(timersByTime, timerCompare);
}
} else if (!nonblocking) {
sleep(timer.time - now);
} else {
return true;
}
}
}
return false;
};
}
if (typeof exports === "object") {
exports.makeWindowTimer = makeWindowTimer;
}