-- Access to the viewer's time-delay facilities local leap = require 'leap' local util = require 'util' local timers = {} local function dbg(...) end -- local dbg = require 'printf' timers.Timer = {} -- delay: time in seconds until callback -- callback: 'wait', or function to call when timer fires (self:tick if nil) -- iterate: if non-nil, call callback repeatedly until it returns non-nil -- (ignored if 'wait') function timers.Timer:new(delay, callback, iterate) local obj = setmetatable({}, self) self.__index = self if callback == 'wait' then dbg('scheduleAfter(%d):', delay) sequence = leap.generate('Timers', {op='scheduleAfter', after=delay}) -- ignore the immediate return dbg('scheduleAfter(%d) -> %s', delay, sequence.next()) -- this call is where we wait for real dbg('next():') dbg('next() -> %s', sequence.next()) sequence.done() return end callback = callback or function() obj:tick() end local calls = 0 if iterate then -- With iterative timers, beware of running a timer callback which -- performs async actions lasting longer than the timer interval. The -- lengthy callback suspends, allowing leap to retrieve the next -- event, which is a timer tick. leap calls a new instance of the -- callback, even though the previous callback call is still -- suspended... etc. 'in_callback' defends against that recursive -- case. Rather than re-enter the suspended callback, drop the -- too-soon timer event. (We could count the too-soon timer events and -- iterate calling the callback, but it's a bathtub problem: the -- callback could end up getting farther and farther behind.) local in_callback = false obj.id = leap.eventstream( 'Timers', {op='scheduleEvery', every=delay}, function (event) local reqid = event.reqid calls += 1 if calls == 1 then dbg('timer(%s) first callback', reqid) -- discard the first (immediate) response: don't call callback return nil else if in_callback then dbg('dropping timer(%s) callback %d', reqid, calls) else dbg('timer(%s) callback %d', reqid, calls) in_callback = true local ret = callback(event) in_callback = false return ret end end end ).reqid else -- (not iterate) obj.id = leap.eventstream( 'Timers', {op='scheduleAfter', after=delay}, function (event) calls += 1 -- Arrange to return nil the first time, true the second. This -- callback is called immediately with the response to -- 'scheduleAfter', and if we immediately returned true, we'd -- be done, and the subsequent timer event would be discarded. if calls == 1 then -- Caller doesn't expect an immediate callback. return nil else callback(event) -- Since caller doesn't want to iterate, the value -- returned by the callback is irrelevant: just stop after -- this one and only call. return true end end ).reqid end return obj end util.classctor(timers.Timer) function timers.Timer:tick() error('Pass a callback to Timer:new(), or override Timer:tick()') end function timers.Timer:cancel() local ok = leap.request('Timers', {op='cancel', id=self.id}).ok leap.cancelreq(self.id) return ok end function timers.Timer:isRunning() return leap.request('Timers', {op='isRunning', id=self.id}).running end -- returns (true, seconds left) for a live timer, else (false, 0) function timers.Timer:timeUntilCall() local result = leap.request('Timers', {op='timeUntilCall', id=self.id}) return result.ok, result.remaining end return timers