diff --git a/CHANGELOG.md b/CHANGELOG.md index af98cbf..b551711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.1.0](https://github.com/nvim-java/lua-async-await/compare/v1.0.0...v1.1.0) (2024-03-31) + + +### Features + +* simplify APIs and re name the confusing terms ([03abf6f](https://github.com/nvim-java/lua-async-await/commit/03abf6fc368b48389fda75670321a6af7a2b53fd)) + ## 1.0.0 (2023-12-10) diff --git a/LICENSE b/LICENSE index 609bb2e..6d4febd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License -Copyright (c) 2020 whocares +Copyright (c) 2024 s1n7ax Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6f4db3e..ab2fcc2 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,98 @@ -# [Lua Async Await](https://github.com/nvim-java/lua-async-await) +# [Lua Async](https://github.com/nvim-java/lua-async) -This is basically [ms-jpq/lua-async-await](https://github.com/ms-jpq/lua-async-await) but with Promise like error handling +Synchronous like asynchronous for Lua. -Refer the original repository for more comprehensive documentation on how all this works +## What -## Why? +Take a look at before and after -A Language Server command response contains two parameters. `error` & `response`. If the error is present -then the error should be handled. - -Ex:- +**Before:** ```lua -self.client.request('workspace/executeCommand', cmd_info, function(err, res) +request('workspace/executeCommand', cmd_info, function(err, res) if err then - log.error(command .. ' failed! arguments: ', arguments, ' error: ', err) + log.error(err) else - log.debug(command .. ' success! response: ', res) + log.debug(res) end end, buffer) ``` -Promises are fine but chaining is annoying specially when you don't have arrow function like -syntactic sugar. Moreover, at the time of this is writing, Lua language server generics typing -is so primitive and cannot handle `Promise` like types. +**After:** -So I wanted Promise like error handling but without Promises. +```lua +-- on error, statement will fail throwing an error just like any synchronous API +local result = request('workspace/executeCommand', cmd_info, buffer) +log.debug(result) +``` -## How to use +## Why -Assume following is the asynchronous API +Well, callback creates callback hell. + +## How to use ```lua -local function lsp_request(callback) +local runner = require("async.runner") +local wrap = require("async.wrap") +local wait = require("async.waits.wait_with_error_handler") + +local function success_async(callback) local timer = vim.loop.new_timer() assert(timer) timer:start(2000, 0, function() -- First parameter is the error - callback('something went wrong', nil) + callback(nil, "hello world") end) end -``` - -### When no error handler defined - -This is how you can call this asynchronous API without a callback - -```lua -local M = require('sync') - -M.sync(function() - local response = M.wait_handle_error(M.wrap(lsp_request)()) -end).run() -``` - -Result: - -``` -Error executing luv callback: -test6.lua:43: unhandled error test6.lua:105: something went wrong -stack traceback: - [C]: in function 'error' - test6.lua:43: in function 'callback' - test6.lua:130: in function -``` -### When error handler is defined +local function fail_async(callback) + local timer = vim.loop.new_timer() -```lua -local M = require('sync') + assert(timer) -local main = M.sync(function() - local response = M.wait_handle_error(M.wrap(lsp_request)()) -end) - .catch(function(err) - print('error occurred ', err) + timer:start(2000, 0, function() + -- First parameter is the error + callback("something went wrong", nil) end) - .run() -``` +end -Result: +local function log(message) + vim.print(os.date("%H:%M:%S") .. " " .. message) +end -``` -error occurred test6.lua:105: something went wrong -``` +vim.cmd.messages("clear") -### When nested +local nested = runner(function() + local success_sync = wrap(success_async) + local fail_sync = wrap(fail_async) -```lua -local M = require('sync') + local success_result = wait(success_sync()) + -- here we get the result because there is no error + log("success_result is: " .. success_result) -local nested = M.sync(function() - local response = M.wait_handle_error(M.wrap(lsp_request)()) + -- following is going to fail and error will get caught by + -- the parent runner function's 'catch' + wait(fail_sync()) end) -M.sync(function() - M.wait_handle_error(nested.run) -end) +runner(function() + log("starting the execution") + -- just wait for nested runner to complete the execution + wait(nested.run) + end) .catch(function(err) - print('parent error handler ' .. err) + log("parent error handler " .. err) end) .run() ``` -Result: +### Output -``` -parent error handler test6.lua:105: test6.lua:105: something went wrong +```txt +18:44:46 starting the execution +18:44:48 success_result is: hello world +18:44:50 parent error handler ...-async-await/lua/async/waits/wait_with_error_handler.lua:14: ...-async-await/lua/async/waits/wait_with_error_handler.lua:14: something went wrong ``` diff --git a/doc/lua-async-await.txt b/doc/lua-async-await.txt index 09ffa13..1e588cf 100644 --- a/doc/lua-async-await.txt +++ b/doc/lua-async-await.txt @@ -1,133 +1,115 @@ -*lua-async-await.txt* For Neovim >= 0.9.4 Last change: 2023 December 10 +*lua-async-await.txt* For Neovim >= 0.9.4 Last change: 2024 March 31 ============================================================================== Table of Contents *lua-async-await-table-of-contents* -1. Lua Async Await |lua-async-await-lua-async-await| - - Why? |lua-async-await-why?| +1. Lua Async |lua-async-await-lua-async| + - What |lua-async-await-what| + - Why |lua-async-await-why| - How to use |lua-async-await-how-to-use| ============================================================================== -1. Lua Async Await *lua-async-await-lua-async-await* +1. Lua Async *lua-async-await-lua-async* -This is basically ms-jpq/lua-async-await - but with Promise like error -handling +Synchronous like asynchronous for Lua. -Refer the original repository for more comprehensive documentation on how all -this works +WHAT *lua-async-await-what* -WHY? *lua-async-await-why?* +Take a look at before and after -A Language Server command response contains two parameters. `error` & -`response`. If the error is present then the error should be handled. - -Ex:- +**Before:** >lua - self.client.request('workspace/executeCommand', cmd_info, function(err, res) + request('workspace/executeCommand', cmd_info, function(err, res) if err then - log.error(command .. ' failed! arguments: ', arguments, ' error: ', err) + log.error(err) else - log.debug(command .. ' success! response: ', res) + log.debug(res) end end, buffer) < -Promises are fine but chaining is annoying specially when you don’t have -arrow function like syntactic sugar. Moreover, at the time of this is writing, -Lua language server generics typing is so primitive and cannot handle -`Promise` like types. +**After:** + +>lua + -- on error, statement will fail throwing an error just like any synchronous API + local result = request('workspace/executeCommand', cmd_info, buffer) + log.debug(result) +< + -So I wanted Promise like error handling but without Promises. +WHY *lua-async-await-why* +Well, callback creates callback hell. -HOW TO USE *lua-async-await-how-to-use* -Assume following is the asynchronous API +HOW TO USE *lua-async-await-how-to-use* >lua - local function lsp_request(callback) + local runner = require("async.runner") + local wrap = require("async.wrap") + local wait = require("async.waits.wait_with_error_handler") + + local function success_async(callback) local timer = vim.loop.new_timer() assert(timer) timer:start(2000, 0, function() -- First parameter is the error - callback('something went wrong', nil) + callback(nil, "hello world") end) end -< - - -WHEN NO ERROR HANDLER DEFINED ~ - -This is how you can call this asynchronous API without a callback - ->lua - local M = require('sync') - M.sync(function() - local response = M.wait_handle_error(M.wrap(lsp_request)()) - end).run() -< - -Result: - -> - Error executing luv callback: - test6.lua:43: unhandled error test6.lua:105: something went wrong - stack traceback: - [C]: in function 'error' - test6.lua:43: in function 'callback' - test6.lua:130: in function -< - - -WHEN ERROR HANDLER IS DEFINED ~ - ->lua - local M = require('sync') + local function fail_async(callback) + local timer = vim.loop.new_timer() - local main = M.sync(function() - local response = M.wait_handle_error(M.wrap(lsp_request)()) - end) - .catch(function(err) - print('error occurred ', err) + assert(timer) + + timer:start(2000, 0, function() + -- First parameter is the error + callback("something went wrong", nil) end) - .run() -< - -Result: - -> - error occurred test6.lua:105: something went wrong -< - - -WHEN NESTED ~ - ->lua - local M = require('sync') + end - local nested = M.sync(function() - local response = M.wait_handle_error(M.wrap(lsp_request)()) - end) + local function log(message) + vim.print(os.date("%H:%M:%S") .. " " .. message) + end + + vim.cmd.messages("clear") - M.sync(function() - M.wait_handle_error(nested.run) + local nested = runner(function() + local success_sync = wrap(success_async) + local fail_sync = wrap(fail_async) + + local success_result = wait(success_sync()) + -- here we get the result because there is no error + log("success_result is: " .. success_result) + + -- following is going to fail and error will get caught by + -- the parent runner function's 'catch' + wait(fail_sync()) end) + + runner(function() + log("starting the execution") + -- just wait for nested runner to complete the execution + wait(nested.run) + end) .catch(function(err) - print('parent error handler ' .. err) + log("parent error handler " .. err) end) .run() < -Result: -> - parent error handler test6.lua:105: test6.lua:105: something went wrong +OUTPUT ~ + +>txt + 18:44:46 starting the execution + 18:44:48 success_result is: hello world + 18:44:50 parent error handler ...-async-await/lua/async/waits/wait_with_error_handler.lua:14: ...-async-await/lua/async/waits/wait_with_error_handler.lua:14: something went wrong < Generated by panvimdoc diff --git a/lua/async/runner.lua b/lua/async/runner.lua new file mode 100644 index 0000000..6aae335 --- /dev/null +++ b/lua/async/runner.lua @@ -0,0 +1,63 @@ +local wrap = require("async.wrap") +local co = coroutine + +---Runs the given function +---@param func fun(): any +---@return { run: function, catch: fun(error_handler: fun(error: any)) } +local function runner(func) + local m = { + error_handler = nil, + } + + local async_thunk_factory = wrap(function(handler, parent_handler_callback) + assert(type(handler) == "function", "type error :: expected func") + local thread = co.create(handler) + local step = nil + + step = function(...) + local ok, thunk = co.resume(thread, ...) + + -- when an error() is thrown after co-routine is resumed, obviously further + -- processing stops, and resume returns ok(false) and thunk(error) returns + -- the error message + if not ok then + if m.error_handler then + m.error_handler(thunk) + return + end + + if parent_handler_callback then + parent_handler_callback(thunk) + return + end + + error("unhandled error " .. thunk) + end + + assert(ok, thunk) + if co.status(thread) == "dead" then + if parent_handler_callback then + parent_handler_callback(thunk) + end + else + assert(type(thunk) == "function", "type error :: expected func") + thunk(step) + end + end + + step() + + return m + end) + + m.run = async_thunk_factory(func) + + m.catch = function(error_handler) + m.error_handler = error_handler + return m + end + + return m +end + +return runner diff --git a/lua/async/waits/wait.lua b/lua/async/waits/wait.lua new file mode 100644 index 0000000..38d34ed --- /dev/null +++ b/lua/async/waits/wait.lua @@ -0,0 +1,12 @@ +local co = coroutine + +---Waits for async function to be completed +---@generic T +---@param defer fun(callback: fun(result: T)) +---@return T +local function wait(defer) + assert(type(defer) == "function", "type error :: expected func") + return co.yield(defer) +end + +return wait diff --git a/lua/async/waits/wait_all.lua b/lua/async/waits/wait_all.lua new file mode 100644 index 0000000..214b595 --- /dev/null +++ b/lua/async/waits/wait_all.lua @@ -0,0 +1,35 @@ +local co = coroutine + +local function __join(thunks) + local len = #thunks + local done = 0 + local acc = {} + + local thunk = function(step) + if len == 0 then + return step() + end + for i, tk in ipairs(thunks) do + assert(type(tk) == "function", "thunk must be function") + local callback = function(...) + acc[i] = { ... } + done = done + 1 + if done == len then + step(unpack(acc)) + end + end + tk(callback) + end + end + return thunk +end + +---Waits for list of async calls to be completed +---@param defer fun(callback: fun(result: any)) +---@return any[] +local function wait_all(defer) + assert(type(defer) == "table", "type error :: expected table") + return co.yield(__join(defer)) +end + +return wait_all diff --git a/lua/async/waits/wait_with_error_handler.lua b/lua/async/waits/wait_with_error_handler.lua new file mode 100644 index 0000000..4d75fbb --- /dev/null +++ b/lua/async/waits/wait_with_error_handler.lua @@ -0,0 +1,20 @@ +local co = coroutine + +---Waits for async function to be completed but considers first parameter as +---error +---@generic T +---@param defer fun(callback: fun(result: T)) +---@return T +local function wait(defer) + assert(type(defer) == "function", "type error :: expected func") + + local err, value = co.yield(defer) + + if err then + error(err) + end + + return value +end + +return wait diff --git a/lua/async/wrap.lua b/lua/async/wrap.lua new file mode 100644 index 0000000..33dd826 --- /dev/null +++ b/lua/async/wrap.lua @@ -0,0 +1,20 @@ +---Make the given function a promise automatically assuming the callback +---function is the last argument +---@generic T +---@param func fun(..., callback: fun(result: T)): any +---@return fun(...): T +local function wrap(func) + assert(type(func) == "function", "type error :: expected func") + + local factory = function(...) + local params = { ... } + local thunk = function(step) + table.insert(params, step) + return func(unpack(params)) + end + return thunk + end + return factory +end + +return wrap diff --git a/lua/sync.lua b/lua/sync.lua deleted file mode 100644 index fd431a9..0000000 --- a/lua/sync.lua +++ /dev/null @@ -1,125 +0,0 @@ -local co = coroutine - -local wrap = function(func) - assert(type(func) == "function", "type error :: expected func") - local factory = function(...) - local params = { ... } - local thunk = function(step) - table.insert(params, step) - return func(unpack(params)) - end - return thunk - end - return factory -end - -local function async(func) - local m = { - error_handler = nil, - } - - local async_thunk_factory = wrap(function(handler, parent_handler_callback) - assert(type(handler) == "function", "type error :: expected func") - local thread = co.create(handler) - local step = nil - - step = function(...) - local ok, thunk = co.resume(thread, ...) - - -- when an error() is thrown after co-routine is resumed, obviously further - -- processing stops, and resume returns ok(false) and thunk(error) returns - -- the error message - if not ok then - if m.error_handler then - m.error_handler(thunk) - return - end - - if parent_handler_callback then - parent_handler_callback(thunk) - return - end - - error("unhandled error " .. thunk) - end - - assert(ok, thunk) - if co.status(thread) == "dead" then - if parent_handler_callback then - parent_handler_callback(thunk) - end - else - assert(type(thunk) == "function", "type error :: expected func") - thunk(step) - end - end - - step() - - return m - end) - - m.run = async_thunk_factory(func) - - m.catch = function(error_handler) - m.error_handler = error_handler - return m - end - - return m -end - --- many thunks -> single thunk -local join = function(thunks) - local len = #thunks - local done = 0 - local acc = {} - - local thunk = function(step) - if len == 0 then - return step() - end - for i, tk in ipairs(thunks) do - assert(type(tk) == "function", "thunk must be function") - local callback = function(...) - acc[i] = { ... } - done = done + 1 - if done == len then - step(unpack(acc)) - end - end - tk(callback) - end - end - return thunk -end - --- sugar over coroutine -local await = function(defer) - assert(type(defer) == "function", "type error :: expected func") - return co.yield(defer) -end - -local await_handle_error = function(defer) - assert(type(defer) == "function", "type error :: expected func") - local err, value = co.yield(defer) - - if err then - error(err) - end - - return value -end - -local await_all = function(defer) - assert(type(defer) == "table", "type error :: expected table") - return co.yield(join(defer)) -end - -return { - sync = async, - wait_handle_error = await_handle_error, - wait = await, - wait_all = await_all, - wrap = wrap, -} diff --git a/lua/tutorial.lua b/lua/tutorial.lua index 8acdb77..e7a269c 100644 --- a/lua/tutorial.lua +++ b/lua/tutorial.lua @@ -1,26 +1,54 @@ -local M = require("sync") +local runner = require("async.runner") +local wrap = require("async.wrap") +local wait = require("async.waits.wait_with_error_handler") -local function lsp_request(callback) +local function success_async(callback) local timer = vim.loop.new_timer() assert(timer) timer:start(2000, 0, function() - callback("something went wrong") + -- First parameter is the error + callback(nil, "hello world") end) end +local function fail_async(callback) + local timer = vim.loop.new_timer() + + assert(timer) + + timer:start(2000, 0, function() + -- First parameter is the error + callback("something went wrong", nil) + end) +end + +local function log(message) + vim.print(os.date("%H:%M:%S") .. " " .. message) +end + vim.cmd.messages("clear") -local nested = M.sync(function() - local response = M.wait_handle_error(M.wrap(lsp_request)()) - vim.print(response) -end) +local nested = runner(function() + local success_sync = wrap(success_async) + local fail_sync = wrap(fail_async) -M.sync(function() - M.wait_handle_error(nested.run) + local success_result = wait(success_sync()) + -- here we get the result because there is no error + log("success_result is: " .. success_result) + + -- following is going to fail and error will get caught by + -- the parent runner function's 'catch' + wait(fail_sync()) end) + +runner(function() + log("starting the execution") + -- just wait for nested runner to complete the execution + wait(nested.run) + end) .catch(function(err) - print("parent error handler " .. err) + log("parent error handler " .. err) end) .run() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy