diff --git a/async.lua b/async.lua new file mode 100644 index 0000000..1152ad2 --- /dev/null +++ b/async.lua @@ -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 diff --git a/async.spec.lua b/async.spec.lua new file mode 100644 index 0000000..a621055 --- /dev/null +++ b/async.spec.lua @@ -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) \ No newline at end of file diff --git a/http.spec.lua b/http.spec.lua index 646ad21..e1ef9d1 100644 --- a/http.spec.lua +++ b/http.spec.lua @@ -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() @@ -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) diff --git a/init.lua b/init.lua index 0a94bec..f855a35 100644 --- a/init.lua +++ b/init.lua @@ -8,6 +8,7 @@ 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() @@ -15,6 +16,7 @@ if minetest.get_modpath("mtt") and mtt.enabled then 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 diff --git a/readme.md b/readme.md index cf979c1..a0e9ac9 100644 --- a/readme.md +++ b/readme.md @@ -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 @@ -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) +
+ ![Yo dawg](yo.jpg) +
\ No newline at end of file