Lua implementations of jQuery's Callbacks and Deferred

Hey Guys,

For work these last few months I’ve had to use Javascript, which comes with it’s own set of pros and cons to be sure. One neat thing it has though that has been really useful is jQuery; it provides a ton of functionality. Two of the jQuery libraries I’ve used the most often are the Callbacks and Deferred libraries, so I thought I’d try my hand at porting them to Lua (I used the original source from jQuery’s github repo as reference). So, here they are. When I’m not so lazy I’ll put these up on my github repo with some documentation, but for now I’ll post the code here with a link to jQuery’s documentation on Deferred and Callbacks (http://api.jquery.com/category/deferred-object/ and http://api.jquery.com/jQuery.Callbacks/).

-- Callbacks

local pairs, ipairs = pairs, ipairs
local select, type, unpack = select, type, unpack
local tinsert, tremove = table.insert, table.remove

local function _has(list, fn)
    if list and fn then
        for i, cb in ipairs(list) do
            if cb == fn then return true, i end
        end
    end
    
    return false    
end

local function _add(list, unique, ...)
    local _args = {...}
    for idx, arg in ipairs(_args) do
        local _type = type(arg)
        if _type == 'function' then
            if not(unique and _has(list, arg)) then
                tinsert(list, arg)
            end
        elseif _type == 'table' then
            _add(list, unique, unpack(arg))
        end
    end
end
           
-- options: once, memory, unique, stopOnFalse
Callbacks = function(...)
    local opt = {...}
    if next(opt) then
        for _, option in ipairs(opt) do opt[option] = true end
    end

    local this, list, stack = {}, {}, (not opt.once) and {} or nil
    local firing, fired, firingLength, firingStart, firingIndex, memory
    
    local function fire(...)
        memory = opt.memory and {...} or nil
        fired, firing, firingIndex = true, true, firingStart or 1
        firingStart, firingLength = 1, #list
        
        while list and (firingIndex <= firingLength) do
            local ret = list[firingIndex](...)
            
            if opt.stopOnFalse and (ret == false) then
                memory = nil
                break
            end
            
            firingIndex = firingIndex + 1
        end
        
        firing = false
        
        if list then
            if stack then
                if #stack > 0 then
                    fire(tremove(stack, 1))
                end
            elseif memory then
                list = {}
            else
                this:disable()
            end
        end    
    end
     
    function this:add(...)
        if list then
            local start = #list + 1
            _add(list, opt.unique, ...)
            
            if firing then
                firingLength = #list
            elseif memory then
                firingStart = start
                fire(unpack(memory))
            end
        end
        
        return self
    end
    
    function this:remove(...)
        if list then
            for _, arg in ipairs{...} do
                local inList, index = _has(list, arg)
                while inList do
                    tremove(list, index)
                    if firing then
                        if index <= firingLength then
                            firingLength = firingLength - 1 
                        end
                        if index <= firingIndex then
                            firingIndex = firingIndex - 1
                        end
                    end
                    inList, index = _has(list, arg)                    
                end
            end
        end
        
        return self
    end

    function this:fire(...)
        if list and ((not fired) or stack) then
            if firing then
                tinsert(stack, {...})
            else
                fire(...)
            end
        end
        return self
    end
    
    function this:fired() return fired end
    
    function this:empty()
        list = {}
        firingLength = 0
        return self
    end
    
    function this:has(fn)
        return fn and _has(list, fn) or (list and #list > 0)
    end
    
    function this:disable()
        list, stack, memory = nil
        return self
    end
    
    function this:disabled() return not list end
    
    function this:lock()
        stack = nil
        if not memory then self:disable() end
        return self
    end
    
    function this:locked() return not stack end
    
    return this  
end

Note: the API for specifying the options to the Callbacks() constructor is slightly different than jQuery’s:

// jQuery:
var myCallbacks = Callbacks('once unique memory stopOnFalse')

// vs Lua:
local myCallbacks = Callbacks('once', 'unique', 'memory', 'stopOnFalse')

A note about Deferred: In jQuery, Deferred objects has a method name called ‘then’. Unfortunately, ‘then’ is a reserved Lua keywords, so I’ve opted to use the name ‘pipe’ instead (incidentally, jQuery.Deferred has a deprecated method called ‘pipe’, which now forwards to ‘then’).

--Deferred

local pairs, ipairs = pairs, ipairs
local type = type
local unpack = unpack
local select = select
    
local function _extend(target, ...)
    for _, t in ipairs{...} do
        for k, v in pairs(t) do
            target[k] = v
        end
    end   
    return target
end

local _methods = { 
    {'resolve', 'done'},
    {'reject', 'fail'},
    {'progress', 'notify'}
}

function Deferred(func)
    local _state, deferred, promise = 'pending', {}, {}
    local _done = Callbacks('once', 'memory')
    local _fail = Callbacks('once', 'memory')
    local _progress = Callbacks('memory')
    
    _done:add(function() 
        _state = 'resolved'
        _fail:disable()
        _progress:lock()        
    end)
    
    _fail:add(function()
        _state = 'rejected'
        _done:disable()
        _progress:lock()        
    end)
    
    function promise:state() return _state end
    
    function promise:always(...)
        deferred:done(...):fail(...)
        return self
    end 
    
    --params: fnDone, fnFail, fnProgress
    function promise:pipe(...)
        local fns = {...}
        return Deferred(function(newDefer)
            for i, method in ipairs(_methods) do
                local fn = (type(fns[i]) == 'function') and fns[i]
                deferred[method[2]](deferred, function(...)
                    local returned = fn and fn(...)
                    if returned and type(returned.promise) == 'function' then
                        returned:promise()
                            :done(function(...) newDefer:resolve(...) end)
                            :fail(function(...) newDefer:reject(...) end)
                            :progress(function(...) newDefer:notify(...) end)
                    else
                        newDefer[method[1]](newDefer, ...)
                    end
                end)
            end
            
            fns = nil
        end):promise()
    end    
    
    function promise:promise(obj)
        return (obj) and _extend(obj, promise) or promise
    end
    
    function promise:done(...)
        _done:add(...)
        return self
    end
    
    function promise:fail(...)
        _fail:add(...)
        return self
    end
    
    function promise:progress(...)
        _progress:add(...)
        return self
    end
    
    function deferred:resolve(...)
        _done:fire(...)
        return self
    end
    
    function deferred:reject(...)
        _fail:fire(...)
        return self
    end
    
    function deferred:notify(...)
        _progress:fire(...)
        return self
    end

    promise:promise(deferred)
    if func then func(deferred) end
    return deferred
end

function when(...)
    local resolveValues = {...}
    local length, first = #resolveValues, resolveValues[1]
    
    local remaining = length
    if length == 1 and type(first.promise) ~= 'function' then
        remaining = 0
    end
    
    local resolveContexts, progressValues, progressContexts
    local deferred = (remaining == 1) and first or Deferred()

    local function updateFunc(inst, i, contexts, values)
        return function(...)
            contexts[i] = inst
            values[i] = (select('#', ...) > 1) and {...} or select(1, ...)
            if values == progressValues then
                deferred:notify(unpack(progressValues))
            else
                remaining = remaining - 1
                if remaining == 0 then
                    deferred:resolve(unpack(resolveValues))
                end
            end
        end
    end
    
    if length > 1 then
        progressValues, progressContexts, resolveContexts = {}, {}, {}
        for i = 1, length do
            local rv = resolveValues[i]
            if rv and type(rv.promise) == 'function' then
                rv:promise()
                    :progress(updateFunc(rv, i, progressContexts, progressValues))                
                    :done(updateFunc(rv, i, resolveContexts, resolveValues))
                    :fail(function(...) deferred:reject(...) end)
            else
                remaining = remaining - 1
            end
        end   
    end
    
    if remaining == 0 then
        deferred:resolve(unpack(resolveValues))
    end
    
    return deferred:promise()
end

@toadkick that’s great, but can you provide a simple example of how you’d use it (or is it the case that you’d have to be familiar with jQuery first)?

That’s the ‘documentation’ part I was referring to when I mentioned I was being lazy :smiley:

I’ll try to post a couple of simple illustrative examples in the meantime though. Both of these objects are very powerful and there is a surprising amount of things you can accomplish with them.

Callbacks() returns an object that manages a list of callbacks. It has several options which you can read about in jQuery’s documentation (‘once’, ‘memory’, ‘stopOnFalse’, ‘unique’), but one practical application I can see for a callback list with default options is an event callback system. For example:


local onTouched = Callbacks()

function setup()
    onTouched:add(function(touch)
        print('Touch callback 1', touch)
    end)

    onTouched:add(function(touch)
        print('Touch callback 2', touch)
    end)

end

function touched(touch)
    onTouched:fire(touch)
end

Every time touched() is called, all of the callbacks in the onTouched list will be executed. Using the options provides some interesting possibilities; for example, Deferred() uses 3 callback lists, all with the ‘memory’ option, and 2 with the ‘once’ option

Deferred() is useful for managing asynchronous operations, especially when there are many, and they are dependent upon each other. Generally for asynchronous operations you provide callback functions for when the operation is completed. Deferred provides an easy way to specify callbacks for success/failure cases, as well as progress updates, without bloating your function’s call signature (among other things). Here’s an example function called Request that wraps Codea’s http.request and returns a deferred object, as well as an example of specifying the callbacks:


function Request(url, paramTable)
    local deferred = Deferred()
    
    local function _success(data) deferred:resolve(data) end
    local function _fail(err) deferred:reject(err) end
    
    http.request(url, _success, _fail, paramTable)
    
    return deferred:promise()    
end

function setup()
    myRequest = Request('http://twolivesleft.com/Codea/logo.png')
        :done(function(img)
            myImage = img
        end)
        :fail(function(err)
            print('image download failed:', err)
        end)
        :always(function()
            print('always called, regardless of success or failure')
        end)
end

function draw()
    if myImage then
        sprite(myImage, WIDTH/2, HEIGHT/2)
    end
end

Another useful feature is that if you add your callback after the deferred has been resolved, your callback will be fired immediately:


local def = Deferred()

def:done(function(str)
    print(str)
end)

def:resolve('hello!')  -- callback above will be executed now

-- will be executed immediately since deferred has been resolved
def:done(function(str)
    print(str .. ' after the fact!');
end)

There’s actually a ton more you can do with deferreds; you can use when() to create a deferred that is resolved when several other deferreds are resolved, or you can use deferred:pipe() to filter/chain operations. Hopefully I’ll be able to find some time in the next few days to provide some more detailed and practical examples.