Skip to content

Commit

Permalink
use coroutines for async/await bevavior (#2)
Browse files Browse the repository at this point in the history
* use coroutines for async/await bevavior

* add missing files

* wip

* working sync/await

* tests

* tests

* use await as function arg

* workign reject/error

* interop test

* rename

* add error testcase

* docs

---------

Co-authored-by: BuckarooBanzay <[email protected]>
  • Loading branch information
BuckarooBanzay and BuckarooBanzay authored Jul 31, 2024
1 parent ccbd4c3 commit bf75f46
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 1 deletion.
60 changes: 60 additions & 0 deletions async.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

local err_symbol = {}

local function await(p)
assert(coroutine.running(), "running inside a Promise.async() call")
local result = nil
local err = nil
local finished = false

p:next(function(...)
result = {...}
finished = true
end):catch(function(e)
err = e
finished = true
end)

while true do
if finished then
if err then
return coroutine.yield({ err = err, err_symbol = err_symbol })
else
return unpack(result)
end
else
coroutine.yield()
end
end
end

function Promise.async(fn)
local t = coroutine.create(fn)
local p = Promise.new()

local step = nil
local result = nil
local cont = nil
local _ = nil
step = function()
if coroutine.status(t) == "suspended" then
cont, result = coroutine.resume(t, await)
if not cont then
-- error in first async() level
p:reject(result)
end
if type(result) == "table" and result.err_symbol == err_symbol then
-- error in await() call
p:reject(result.err)
return
end
minetest.after(0, step)
else
-- last result from resume was the return value
p:resolve(result)
end
end
step()

return p
end
63 changes: 63 additions & 0 deletions async.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

mtt.register("Promise.async", function(callback)
local p = Promise.async(function(await)
local v = await(Promise.after(0, 42))
assert(v == 42)
local v1 = await(Promise.resolved(666))
assert(v1 == 666)
return 99
end)

p:next(function(v)
assert(v == 99)
callback()
end)
end)

mtt.register("Promise.async interop", function()
return Promise.async(function(await)
return await(Promise.async(function()
local v = await(Promise.resolved(42))
assert(v == 42)
end))
end)
end)

mtt.register("Promise.async simple", function()
return Promise.async(function(await)
local v = await(Promise.resolved(42))
assert(v == 42)
end)
end)

mtt.register("Promise.async with handle_async", function()
return Promise.async(function(await)
local v = await(Promise.resolved(42))
assert(v == 42)
v = await(Promise.handle_async(function() return 100 end))
assert(v == 100)
end)
end)

mtt.register("Promise.async rejected", function(callback)
local p = Promise.async(function(await)
await(Promise.rejected("my-err"))
end)

p:catch(function(e)
assert(type(e) == "string")
callback()
end)
end)

mtt.register("Promise.async error", function(callback)
local p = Promise.async(function()
error("stuff")
end)

p:catch(function(e)
-- "/home/user/.minetest/mods/promise/async.spec.lua:55: stuff"
assert(type(e) == "string")
callback()
end)
end)
10 changes: 9 additions & 1 deletion http.spec.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
local http = ...

local toJson = function(res) return res.json() end

mtt.register("Promise.http GET", function(callback)
Promise.http(http, "https://api.chucknorris.io/jokes/random"):next(function(res)
return res.json()
Expand All @@ -11,8 +13,14 @@ mtt.register("Promise.http GET", function(callback)
end)
end)

mtt.register("Promise.http GET with async/await", function()
return Promise.async(function(await)
local joke = await(Promise.http(http, "https://api.chucknorris.io/jokes/random"):next(toJson))
assert(type(joke.value) == "string")
end)
end)

mtt.register("Promise.http/all GET", function(callback)
local toJson = function(res) return res.json() end
local p1 = Promise.http(http, "https://api.chucknorris.io/jokes/random"):next(toJson)
local p2 = Promise.http(http, "https://api.chucknorris.io/jokes/random"):next(toJson)

Expand Down
2 changes: 2 additions & 0 deletions init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ dofile(MP.."/promise.lua")
dofile(MP.."/util.lua")
dofile(MP.."/http.lua")
dofile(MP.."/formspec.lua")
dofile(MP.."/async.lua")

if minetest.get_modpath("mtt") and mtt.enabled then
local http = minetest.request_http_api()

dofile(MP .. "/promise.spec.lua")
dofile(MP .. "/formspec.spec.lua")
dofile(MP .. "/util.spec.lua")
dofile(MP .. "/async.spec.lua")
if http then
loadfile(MP .. "/http.spec.lua")(http)
end
Expand Down
40 changes: 40 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ promise library for minetest
Features:
* Async event handling
* Utilities for formspec, emerge_area, handle_async, http and minetest.after
* async/await helpers (js example [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function))

# Examples

Expand Down Expand Up @@ -256,8 +257,47 @@ end)

**NOTE**: experimental, only works if the `to_player` property is set

# async/await with `Promise.async`

Similar to [javascripts](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) implementation async/await can be used in lua too with the help of [coroutines](https://www.lua.org/pil/9.1.html)

Example: fetch a joke with async/await
```lua
local toJson = function(res) return res.json() end

Promise.async(function(await)
local joke = await(Promise.http(http, "https://api.chucknorris.io/jokes/random"):next(toJson))
assert(type(joke.value) == "string")
-- do stuff here with the joke
end)
```

Example: sleep for a few seconds
```lua
Promise.async(function(await)
await(Promise.after(5))
-- 5 seconds passed
end)
```

`Promise.async` returns a Promise that can be used with `:next` or `await` in another async function, for example:

```lua
local toJson = function(res) return res.json() end

Promise.async(function(await)
local data = await(Promise.http(http, "https://my-api"):next(toJson))
return data.value * 200 -- "value" is a number
end):next(function(n)
-- n is the result of the multiplication in the previous function
end)
```

# License

* Code: MIT (adapted from https://github.com/Billiam/promise.lua)

<details>

![Yo dawg](yo.jpg)
</details>

0 comments on commit bf75f46

Please sign in to comment.