{"id":13761259,"url":"https://github.com/ms-jpq/lua-async-await","last_synced_at":"2025-05-16T09:04:58.889Z","repository":{"id":37276836,"uuid":"261040544","full_name":"ms-jpq/lua-async-await","owner":"ms-jpq","description":"Async Await in 90 lines of code.","archived":false,"fork":false,"pushed_at":"2024-11-19T02:54:01.000Z","size":270,"stargazers_count":342,"open_issues_count":8,"forks_count":21,"subscribers_count":5,"default_branch":"neo","last_synced_at":"2025-04-09T11:08:43.214Z","etag":null,"topics":["lua","neovim"],"latest_commit_sha":null,"homepage":"","language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ms-jpq.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-05-03T23:29:11.000Z","updated_at":"2025-04-05T13:57:30.000Z","dependencies_parsed_at":"2024-01-15T03:58:59.238Z","dependency_job_id":"685974ca-4c83-4e8f-a88a-8354aa28e9b3","html_url":"https://github.com/ms-jpq/lua-async-await","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ms-jpq%2Flua-async-await","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ms-jpq%2Flua-async-await/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ms-jpq%2Flua-async-await/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ms-jpq%2Flua-async-await/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ms-jpq","download_url":"https://codeload.github.com/ms-jpq/lua-async-await/tar.gz/refs/heads/neo","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254501557,"owners_count":22081528,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["lua","neovim"],"created_at":"2024-08-03T13:01:46.230Z","updated_at":"2025-05-16T09:04:53.880Z","avatar_url":"https://github.com/ms-jpq.png","language":"Lua","readme":"# [Lua Async Await](https://ms-jpq.github.io/lua-async-await)\n\nAsync Await in [90 lines](https://github.com/ms-jpq/lua-async-await/blob/master/lua/async.lua) of code.\n\nOriginally written for Neovim, because it uses the same `libuv` eventloop from NodeJS.\n\n**Works for any LUA code**\n\n## Special Thanks\n\n[svermeulen](https://github.com/svermeulen) for fixing [inability to return functions](https://github.com/ms-jpq/lua-async-await/issues/2).\n\n## Preface\n\nThis tutorial assumes that you are familiar with the concept of `async` `await`\n\nYou will also need to read through the [first 500 words](https://www.lua.org/pil/9.1.html) of how coroutines work in lua.\n\n## [Luv](https://github.com/luvit/luv)\n\nNeovim use [libuv](https://github.com/libuv/libuv) for async, the same monster that is the heart of NodeJS.\n\nThe `libuv` bindings are exposed through `luv` for lua, this is accessed using `vim.loop`.\n\nMost of the `luv` APIs are similar to that of NodeJS, ie in the form of\n\n`API :: (param1, param2, callback)`\n\nOur goal is avoid the dreaded calback hell.\n\n## Preview\n\n```lua\nlocal a = require \"async\"\n\nlocal do_thing = a.sync(function (val)\n  local o = a.wait(async_func())\n  return o + val\nend)\n\nlocal main = a.sync(function ()\n  local thing = a.wait(do_thing()) -- composable!\n\n  local x = a.wait(async_func())\n  local y, z = a.wait_all{async_func(), async_func()}\nend)\n\nmain()\n```\n\n## [Coroutines](https://www.lua.org/pil/9.1.html)\n\nIf you don't know how coroutines work, go read the section on generators on [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators).\n\nIt is in js, but the idea is identical, and the examples are much better.\n\n---\n\nHere is an example of coroutines in Lua:\n\nNote that in Lua code `coroutine` is not a coroutine, it is an namespace.\n\nTo avoid confusion, I will follow the convention used in the Lua book, and use `thread` to denote coroutines in code.\n\n```lua\nlocal co = coroutine\n\nlocal thread = co.create(function ()\n  local x, y, z = co.yield(something)\n  return 12\nend)\n\nlocal cont, ret = co.resume(thread, x, y, z)\n```\n\n---\n\nNotice the similarities with `async` `await`\n\nIn both `async` `await` and `coroutines`, the LHS of the assignment statements receives values from the RHS.\n\nThis is how it works in all synchronous assignments. Except, we can defer the transfer of the values from RHS.\n\nThe idea is that we will make RHS send values to LHS, when RHS is ready.\n\n## Synchronous Coroutines\n\nTo warm up, we will do a synchronous version first, where the RHS is always ready.\n\nHere is how you send values to a coroutine:\n\n```lua\nco.resume(thread, x, y, z)\n```\n\n---\n\nThe idea is that we will repeat this until the coroutine has been \"unrolled\"\n\n```lua\nlocal pong = function (thread)\n  local nxt = nil\n  nxt = function (cont, ...)\n    if not cont\n      then return ...\n      else return nxt(co.resume(thread, ...))\n    end\n  end\n  return nxt(co.resume(thread))\nend\n```\n\n---\n\nif we give `pong` some coroutine, it will recursively run the coroutine until completion\n\n```lua\nlocal thread = co.create(function ()\n  local x = co.yield(1)\n  print(x)\n  local y, z = co.yield(2, 3)\n  print(y)\nend)\n\npong(thread)\n```\n\nWe can expect to see `1`, `2 3` printed.\n\n## [Thunk](https://stackoverflow.com/questions/2641489/what-is-a-thunk)\n\nOnce you understand how the synchronous `pong` works, we are super close!\n\nBut before we make the asynchronous version, we need to learn one more simple concept.\n\nFor our purposes a `Thunk` is function whose purpose is to invoke a callback.\n\ni.e. It adds a transformation of `(arg, callback) -\u003e void` to `arg -\u003e (callback -\u003e void) -\u003e void`\n\n```lua\nlocal read_fs = function (file)\n  local thunk = function (callback)\n    fs.read(file, callback)\n  end\n  return thunk\nend\n```\n\n---\n\nThis too, is a process that can be automated:\n\n```lua\nlocal wrap = function (func)\n  local factory = function (...)\n    local params = {...}\n    local thunk = function (step)\n      table.insert(params, step)\n      return func(unpack(params))\n    end\n    return thunk\n  end\n  return factory\nend\n\nlocal thunk = wrap(fs.read)\n```\n\nSo why do we need this?\n\n## Async Await\n\nThe answer is simple! We will use thunks for our RHS!\n\n---\n\nWith that said, we will still need one more magic trick, and that is to make a `step` function.\n\nThe sole job of the `step` funciton is to take the place of the callback to all the thunks.\n\nIn essence, on every callback, we take 1 step forward in the coroutine.\n\n```lua\nlocal pong = function (func, callback)\n  assert(type(func) == \"function\", \"type error :: expected func\")\n  local thread = co.create(func)\n  local step = nil\n  step = function (...)\n    local stat, ret = co.resume(thread, ...)\n    assert(stat, ret)\n    if co.status(thread) == \"dead\" then\n      (callback or function () end)(ret)\n    else\n      assert(type(ret) == \"function\", \"type error :: expected func\")\n      ret(step)\n    end\n  end\n  step()\nend\n```\n\nNotice that we also make pong call a callback once it is done.\n\n---\n\nWe can see it in action here:\n\n```lua\nlocal echo = function (...)\n  local args = {...}\n  local thunk = function (step)\n    step(unpack(args))\n  end\n  return thunk\nend\n\nlocal thread = co.create(function ()\n  local x, y, z = co.yield(echo(1, 2, 3))\n  print(x, y, z)\n  local k, f, c = co.yield(echo(4, 5, 6))\n  print(k, f, c)\nend)\n\npong(thread)\n```\n\nWe can expect this to print `1 2 3` and `4 5 6`\n\nNote, we are using a synchronous `echo` for illustration purposes. It doesn't matter when the `callback` is invoked. The whole mechanism is agnostic to timing.\n\nYou can think of async as the more generalized version of sync.\n\nYou can run an asynchronous version in the last section.\n\n## Await All\n\nOne more benefit of thunks, is that we can use them to inject arbitrary computation.\n\nSuch as joining together many thunks.\n\n```lua\nlocal join = function (thunks)\n  local len = table.getn(thunks)\n  local done = 0\n  local acc = {}\n\n  local thunk = function (step)\n    if len == 0 then\n      return step()\n    end\n    for i, tk in ipairs(thunks) do\n      local callback = function (...)\n        acc[i] = {...}\n        done = done + 1\n        if done == len then\n          step(unpack(acc))\n        end\n      end\n      tk(callback)\n    end\n  end\n  return thunk\nend\n```\n\nThis way we can perform `await_all` on many thunks as if they are a single one.\n\n## More Sugar\n\nAll this explicit handling of coroutines are abit ugly. The good thing is that we can completely hide the implementation detail to the point where we don't even need to require the `coroutine` namespace!\n\nSimply wrap the coroutine interface with some friendly helpers\n\n```lua\nlocal pong = function (func, callback)\n  local thread = co.create(func)\n  ...\nend\n\nlocal await = function (defer)\n  return co.yield(defer)\nend\n\nlocal await_all = function (defer)\n  return co.yield(join(defer))\nend\n```\n\n## Composable\n\nAt this point we are almost there, just one more step!\n\n```lua\nlocal sync = wrap(pong)\n```\n\nWe `wrap` `pong` into a thunk factory, so that calling it is no different than yielding other thunks. This is how we can compose together our `async` `await`.\n\nIt's thunks all the way down.\n\n## Tips and Tricks\n\nIn Neovim, we have something called `textlock`, which prevents many APIs from being called unless you are in the main event loop.\n\nThis will prevent you from essentially modifying any Neovim states once you have invoked a `vim.loop` funciton, which run in a seperate loop.\n\nHere is how you break back to the main loop:\n\n```lua\nlocal main_loop = function (f)\n  vim.schedule(f)\nend\n```\n\n```lua\na.sync(function ()\n  -- do something in other loop\n  a.wait(main_loop)\n  -- you are back!\nend)()\n```\n\n## Plugin!\n\nI have bundle up this tutorial as a vim plugin, you can install it the usual way.\n\n`Plug 'ms-jpq/lua-async-await', {'branch': 'neo'}`\n\nand then call the test functions like so:\n\n`:LuaAsyncExample`\n\n`:LuaSyncExample`\n\n`:LuaTextlockFail`\n\n`:LuaTextLockSucc`\n","funding_links":[],"categories":["Lua"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fms-jpq%2Flua-async-await","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fms-jpq%2Flua-async-await","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fms-jpq%2Flua-async-await/lists"}