Git Product home page Git Product logo

lua-promises's Introduction

lua-promises

Build Status

A+ promises in Lua

Why would I need promises?

Lua is normally single-threaded, so there is little need in asyncrhonous operations. However, if you use HTTP requests, or sockets, or other types of I/O - most likely your library would have asynchronous API:

readfile('file.txt', function(contents, err)
	if err then
		print('Error', err)
	else
		-- process file contents
	end
end)

Using callbacks like this can quickly become problematic (once you need to make multiple asyncrhonous actions depending on each other results). Let's imagine some protocol where we need to connect to the remote peer, perform some authentication, then send a request and finally receive some response:

connect(function(status, err)
	if err then .... end
	auth(function(token, err)
		if err then ... end
		request(token, function(res, err)
			if err then ... end
			handleresult(res)
		end)
	end)
end)

Here's how the code could be rewritten using promises API:

connect():next(function(status)
	return auth()
end):next(function(token)
	return request(token)
end):next(function(result)
	handleresult(res)
end, function(err)
	...handle error...
end)

This is cleaner and more readable, since it doesn't use lost of nested callbacks. Also it has a single place to handle all errors.

The idea is that each function return an object which can be later resolved or rejected. Various callbacks could be added to the object to get notified when the object is resolved. Such objects are called promises, deferred objects, thennables - all these names describe pretty much the same behavior.

Install

In terminal:

luarocks install --server=http://luarocks.org/dev lua-promises

In Lua code:

local deferred = require('deferred')

API

Create new promises:

  • d = deferred.new() - returns a new promise object d
  • d = deferred.all(promises) - returns a new promise object d that is resolved when all promises are resolved/rejected.
  • d = deferred.first(promises) - returns a new promise object d that is resolved as soon as the first of the promises gets resolved/rejected.
  • d = deferred.map(list, fn) - returns a new promise object d that is resolved with the values of sequential application of function fn to each element in the list. fn is expected to return promise object.

Resolve/reject:

  • d:resolve(value) - resolve promise object with value
  • d:reject(value) - reject promise object with value

Wait for the promise object:

  • d:next(cb, [errcb]) - enqueues resolve callback cb and (optionally) a rejection callback errcb. Resolve callback can be nil.

Example

local deferred = require('deferred')

--
-- Converting callback-based API into promise-based is very straightforward:
-- 
-- 1) Create promise object
-- 2) Start your asynchronous action
-- 3) Resolve promise object whenever action is finished (only first resolution
--    is accepted, others are ignored)
-- 4) Reject promise object whenever action is failed (only first rejection is
--    accepted, others are ignored)
-- 5) Return promise object letting calling side to add a chain of callbacks to
--    your asynchronous function

function read(f)
	local d = deferred.new()
	readasync(f, function(contents, err)
		if err == nil then
			d:resolve(contents)
		else
			d:reject(err)
		end
	end)
	return d
end

-- You can now use read() like this:
read('file.txt'):next(function(s)
	print('File.txt contents: ', s)
end, function(err)
	print('Error', err)
end)

Chaining promises

Promises can be chained (read A+ specs for more details). It's convenient when you need to do several asynchronous actions sequentially. Each callback can return another promise object, then further callbacks could wait for it to become resolved/rejected:

-- Reading two files sequentially:
read('first.txt'):next(function(s)
	print('File file:', s)
	return read('second.txt')
end):next(function(s)
	print('Second file:', s)
end):next(nil, function(err)
	-- error while reading first or second file
	print('Error', err)
end)

Processing lists

You can process a list of object asynchronously, so the next asynchronous action is started only when the previous one is successfully completed:

local items = {'a.txt', 'b.txt', 'c.txt'}
-- Read 3 files, one by one
deferred.map(items, read):next(function(files)
	-- here files is an array of file contents for each of the files
end, function(err)
	-- handle reading error
end)

Waiting for a group of promises

You may start multiple asynchronous actions in parallel and wait for all of them to complete:

deferred.all({
	http.get('http://example.com/first'),
	http.get('http://example.com/second'),
	http.get('http://example.com/third'),
}):next(function(results)
	-- handle results here (all requests are finished and there has been
	-- no errors)
end, function(results)
	-- handle errors here (all requests are finished and there has been
	-- at least one error)
end)

Waiting for the first promise

In some cases it's handy to wait for either of the promises. A good example is reading with timeout:

-- returns a promise that gets rejected after a certain timeout
function timeout(sec)
	local d = deferred.new()
	settimeout(function()
		d:reject('Timeout')
	end, sec)
	return d
end

deferred.first({
	read(somefile), -- resolves promise with contents, or rejects with error
	timeout(5),
}):next(function(result)
	...file was read successfully...
end, function(err)
	...either timeout or I/O error...
end)

License

Code is distributed under MIT license.

lua-promises's People

Contributors

alkino avatar boolangery avatar zserge avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

lua-promises's Issues

Runtime errors are not reported in chained promises

If I make a programming error in the first promise (in a promise chain) I get an expected runtime error but if I make a programming error in the second promise I get no runtime error.

I slightly modifed your chained promises example to verify this:

local deferred = require "lib.deferred"

function readasync(filename, cb)
  if filename == 'first.txt' then
    call_missing_function()
    cb("content1", nil)
  else
    --call_missing_function()
    cb("content2", nil)
  end
end

function read(filename)
  local d = deferred.new()
  readasync(filename, function(contents, err)
    if err == nil then
      d:resolve(contents)
    else
      d:reject(err)
    end
  end)
  return d
end

read('first.txt'):next(function(s)
  print('First file:', s)
  return read('second.txt')
end):next(function(s)
  print('Second file:', s)
end):next(nil, function(err)
  -- error while reading first or second file
  print('Error:', err)
end)

When I run the above I get an expected runtime error:

nov. 02 06:38:45.241 ERROR: Runtime error
  /Users/jocke/src/blackmode/trunk/app/ui2/main.lua:5: attempt to call global 'call_missing_function' (a nil value)
  stack traceback:
  /Users/jocke/src/blackmode/trunk/app/ui2/main.lua:5: in function 'readasync'
  /Users/jocke/src/blackmode/trunk/app/ui2/main.lua:15: in function 'read'
  /Users/jocke/src/blackmode/trunk/app/ui2/main.lua:25: in main chunk

If I change the readsync function above to this:

function readasync(filename, cb)
  if filename == 'first.txt' then
    --call_missing_function()
    cb("content1", nil)
  else
    call_missing_function()
    cb("content2", nil)
  end
end

Then I just get the custom error message and no run-time error:

Error:	/Users/jocke/src/blackmode/trunk/app/ui2/main.lua:8: attempt to call global 'call_missing_function' (a nil value)

I this to be expected?

Kind regards

Rejection propagation is incorrect

local deferred = require("deferred")

local d = deferred:new()

local function f1()
    return d
end

local function f2()
    return f1():next(
        function(res)
            return (res or "") .. "f2"
        end,
        function(err)
            print("f2 err")
            return err
        end
    )
end

local function f3()
    return f2():next(
        function(res)
            return res .. "f3"
        end,
        function(err)
            print("f3 err")
            return err
        end
    )
end

f3():next(
    function(res)
        print("suc", res)
    end,
    function(err)
        print("fail", err)
    end
)

d:reject("fail error reject")
Failure: fail error reject
suc f2f3

Tried both Lua 5.1.5 and 5.2.4, same results.

Even though I don't know if returning in error callback is the way to pass error to next stage, obviously error handling branch in f2() doesn't get called at all. Could you please explain this? Thanks.

deferred.all() returns nil when at least one reject()ion occurs

Hi, the documentation (in README.md, "Waiting for a group of promises") states that when using deferred.all() :

end, function(results)
	-- handle errors here (all requests are finished and there has been
	-- at least one error)
end)

in case of one or more reject()ions, there should be a "results" variable containing an array of all sub-promises results.

However, it seems this variable is always nil. Thus, in case at least one promise sucessfully resolv()ed, its result cannot be processed.

Please consider a reproducible testcase, which when run with LuaJIT 2.1.0beta3 returns:

	Rejected at least one promise!
	Returned KO result was:
nil	not a table

LuaRocks

Hello,

any chance you would add a rockspec and put this into LuaRocks?

Tracking hanging chains

If for some reason the :reject or :resolve method fails to execute, the chain silently stops executing. Is there any way to identify such problematic places without inserting watchdogs over all code?

Example is wrong in documentation

In the documentation, we have this code:

deferred.first(
    read(somefile), -- resolves promise with contents, or rejects with error
    timeout(5),
)

Where it's expected that deferred.first(args) will take a single table. This bug can be fixed by modifying code like this:

function M.first(...)
    local args = {...}

    local d = M.new()
    for _, v in ipairs(args) do
        v:next(function(res)
            d:resolve(res)
        end, function(err)
            d:reject(err)
        end)
    end
    return d
end

return M

Or by making the readme.md use a table wrapper first.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.