├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .luarc.json ├── LICENSE ├── Makefile ├── README.md ├── examples ├── README.md ├── coc_nvim.lua ├── demo.js ├── demo.lua ├── multiple_threads.lua ├── read_file.lua ├── uva.lua └── write_file.lua ├── lua ├── async.lua ├── promise-async │ ├── compat.lua │ ├── error.lua │ ├── loop.lua │ └── utils.lua └── promise.lua ├── rockspec ├── promise-async-1.0-0.rockspec └── promise-async-scm-0.rockspec ├── spec ├── async_spec.lua ├── error_spec.lua ├── fixtures.lua ├── helpers │ ├── basics.lua │ ├── init.lua │ ├── outputHandler.lua │ ├── reasons.lua │ └── thenables.lua ├── init.lua ├── loop_spec.lua ├── promiseA+ │ ├── 2.1.2_spec.lua │ ├── 2.1.3_spec.lua │ ├── 2.2.1_spec.lua │ ├── 2.2.2_spec.lua │ ├── 2.2.3_spec.lua │ ├── 2.2.4_spec.lua │ ├── 2.2.6_spec.lua │ ├── 2.2.7_spec.lua │ ├── 2.3.1_spec.lua │ ├── 2.3.2_spec.lua │ ├── 2.3.3_spec.lua │ └── 2.3.4_spec.lua └── promise_spec.lua └── typings ├── README.md ├── async.lua ├── loop.lua ├── promise.lua └── promiselike.lua /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 4 4 | tab_width = 4 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.lua] 10 | align_continuous_assign_statement = false 11 | space_around_table_field_list = false 12 | align_call_args = false 13 | quote_style = single 14 | 15 | [*.json,*.jsonc] 16 | indent_style = tab 17 | 18 | [*.{yaml,yml}] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | [{Makefile,**.mk}] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: [bug] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | - type: input 10 | attributes: 11 | label: 'Version (lua -v) or (nvim -v | head -n1)' 12 | placeholder: 'LuaJIT 2.1.0-beta3' 13 | validations: 14 | required: true 15 | - type: input 16 | attributes: 17 | label: 'Operating system/version' 18 | placeholder: 'ArchLinux' 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: 'How to reproduce the issue' 24 | description: 'How do you trigger this bug? Please walk us through it step by step.' 25 | value: | 26 | 1. 27 | 2. 28 | 3. 29 | ... 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | attributes: 35 | label: 'Expected behavior' 36 | description: 'Describe the behavior you expect. May include logs, images, or videos.' 37 | validations: 38 | required: true 39 | - type: textarea 40 | attributes: 41 | label: 'Actual behavior' 42 | validations: 43 | required: true 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Request an enhancement for promise-async 3 | labels: [enhancement] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Before requesting: search [existing issues](https://github.com/kevinhwang91/promise-async/labels/enhancement). 9 | - type: textarea 10 | attributes: 11 | label: "Feature description" 12 | validations: 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: "Describe the solution you'd like" 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: "Additional context" 22 | validations: 23 | required: false 24 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install lua-language-server 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | run: | 22 | cd 23 | gh release download -R sumneko/lua-language-server -p '*-linux-x64.tar.gz' -D lua-language-server 24 | tar xzf lua-language-server/* -C lua-language-server 25 | echo "${PWD}/lua-language-server/bin" >> $GITHUB_PATH 26 | export PATH="${PWD}/lua-language-server/bin:${PATH}" 27 | lua-language-server --version 28 | 29 | - name: Run Lint 30 | run: make lint 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | strategy: 15 | matrix: 16 | include: 17 | - { os: ubuntu-latest, target: nvim, version: stable } 18 | - { os: ubuntu-latest, target: nvim, version: nightly } 19 | - { os: macos-latest, target: nvim, version: stable } 20 | - { os: macos-latest, target: nvim, version: nightly } 21 | - { os: ubuntu-latest, target: lua, version: lua 5.1 } 22 | - { os: ubuntu-latest, target: lua, version: lua 5.2 } 23 | - { os: ubuntu-latest, target: lua, version: lua 5.3 } 24 | - { os: ubuntu-latest, target: lua, version: lua 5.4 } 25 | - { os: ubuntu-latest, target: lua, version: luajit 2.1.0-beta3 } 26 | - { os: macos-latest, target: lua, version: lua 5.1 } 27 | - { os: macos-latest, target: lua, version: lua 5.2 } 28 | - { os: macos-latest, target: lua, version: lua 5.3 } 29 | - { os: macos-latest, target: lua, version: lua 5.4 } 30 | - { os: macos-latest, target: lua, version: luajit 2.1.0-beta3 } 31 | fail-fast: false 32 | runs-on: ${{ matrix.os }} 33 | steps: 34 | - uses: actions/checkout@v3 35 | 36 | - if: matrix.target == 'nvim' && matrix.os == 'ubuntu-latest' 37 | name: Install Neovim on Ubuntu 38 | run: | 39 | cd 40 | curl -LO https://github.com/neovim/neovim/releases/download/${{ matrix.version }}/nvim-linux64.tar.gz 41 | tar xzf nvim-linux64.tar.gz 42 | echo "${PWD}/nvim-linux64/bin" >> $GITHUB_PATH 43 | export PATH="${PWD}/nvim-linux64/bin:${PATH}" 44 | nvim -v 45 | - if: matrix.target == 'nvim' && matrix.os == 'macos-latest' 46 | name: Install Neovim on Macos 47 | run: | 48 | cd 49 | curl -LO https://github.com/neovim/neovim/releases/download/${{ matrix.version }}/nvim-macos-arm64.tar.gz 50 | tar xzf nvim-macos-arm64.tar.gz 51 | echo "${PWD}/nvim-macos-arm64/bin" >> $GITHUB_PATH 52 | export PATH="${PWD}/nvim-macos-arm64/bin:${PATH}" 53 | nvim -v 54 | 55 | - name: Run Test 56 | run: | 57 | if [[ ${{ matrix.target }} == lua ]]; then 58 | export LUA_VERSION="${{ matrix.version }}" 59 | fi 60 | make test_${{ matrix.target }} 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", 3 | "diagnostics.disable": [ 4 | "duplicate-set-field" 5 | ], 6 | "diagnostics.globals": [ 7 | "vim", 8 | "jit", 9 | "it", 10 | "describe", 11 | "before_each", 12 | "after_each", 13 | "spy", 14 | "setup", 15 | "teardown", 16 | "done", 17 | "wait" 18 | ], 19 | "diagnostics.severity": { 20 | "undefined-field": "Hint" 21 | }, 22 | "runtime.version": "LuaJIT", 23 | "type.castNumberToInteger": true, 24 | "type.weakUnionCheck": true 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022-2023, kevinhwang91 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | DEPS ?= build 3 | 4 | LUA_VERSION ?= luajit 2.1.0-beta3 5 | NVIM_BIN ?= nvim 6 | NVIM_LUA_VERSION := $(shell $(NVIM_BIN) -v 2>/dev/null | grep -E '^Lua(JIT)?' | tr A-Z a-z) 7 | ifdef NVIM_LUA_VERSION 8 | LUA_VERSION ?= $(NVIM_LUA_VERSION) 9 | endif 10 | LUA_NUMBER := $(word 2,$(LUA_VERSION)) 11 | 12 | TARGET_DIR := $(DEPS)/$(LUA_NUMBER) 13 | 14 | HEREROCKS ?= $(DEPS)/hererocks.py 15 | HEREROCKS_URL ?= https://raw.githubusercontent.com/luarocks/hererocks/master/hererocks.py 16 | HEREROCKS_ACTIVE := source $(TARGET_DIR)/bin/activate 17 | 18 | LUAROCKS ?= $(TARGET_DIR)/bin/luarocks 19 | 20 | BUSTED ?= $(TARGET_DIR)/bin/busted 21 | BUSTED_HELPER ?= $(PWD)/spec/fixtures.lua 22 | 23 | LUV ?= $(TARGET_DIR)/lib/lua/$(LUA_NUMBER)/luv.so 24 | 25 | LUA_LS ?= $(DEPS)/lua-language-server 26 | LINT_LEVEL ?= Information 27 | 28 | all: deps 29 | 30 | deps: | $(HEREROCKS) $(BUSTED) 31 | 32 | test: test_lua test_nvim 33 | 34 | test_lua: $(BUSTED) $(LUV) 35 | @echo Test with $(LUA_VERSION) ...... 36 | @$(HEREROCKS_ACTIVE) && eval $$(luarocks path) && \ 37 | lua spec/init.lua --helper=$(BUSTED_HELPER) $(BUSTED_ARGS) 38 | 39 | ifdef NVIM_LUA_VERSION 40 | test_nvim: $(BUSTED) 41 | @echo Test with Neovim ...... 42 | @$(HEREROCKS_ACTIVE) && eval $$(luarocks path) && \ 43 | $(NVIM_BIN) --clean -n --headless -u spec/init.lua -- \ 44 | --helper=$(BUSTED_HELPER) $(BUSTED_ARGS) 45 | endif 46 | 47 | $(HEREROCKS): 48 | mkdir -p $(DEPS) 49 | curl $(HEREROCKS_URL) -o $@ 50 | 51 | $(LUAROCKS): $(HEREROCKS) 52 | $(HEREROCKS_ENV) python3 $< $(TARGET_DIR) --$(LUA_VERSION) -r latest 53 | 54 | $(BUSTED): $(LUAROCKS) 55 | $(HEREROCKS_ACTIVE) && luarocks install busted 56 | 57 | $(LUV): $(LUAROCKS) 58 | @$(HEREROCKS_ACTIVE) && [[ ! $$(luarocks which luv) ]] && \ 59 | luarocks install luv || true 60 | 61 | lint: 62 | @rm -rf $(LUA_LS) 63 | @mkdir -p $(LUA_LS) 64 | @lua-language-server --check $(PWD) --checklevel=$(LINT_LEVEL) --logpath=$(LUA_LS) 65 | @grep -q '^\[\]\s*$$' $(LUA_LS)/check.json || (cat $(LUA_LS)/check.json && exit 1) 66 | 67 | clean: 68 | rm -rf $(DEPS) 69 | 70 | .PHONY: all deps clean lint test test_nvim test_lua 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # promise-async 2 | 3 | ![GitHub Test](https://github.com/kevinhwang91/promise-async/workflows/Test/badge.svg) 4 | ![GitHub Lint](https://github.com/kevinhwang91/promise-async/workflows/Lint/badge.svg) 5 | 6 | The goal of promise-async is to port [Promise][promise] & [Async][async] from JavaScript to Lua. 7 | 8 | > A value returned by async function in JavaScript is actually a Promise Object. It's incomplete and 9 | > inflexible for using an async function wrapped by bare coroutine without Promise in almost Lua 10 | > implementation. 11 | 12 | - [Features](#features) 13 | - [Demonstration](#demonstration) 14 | - [Script](#script) 15 | - [demo.lua](#demo.lua) 16 | - [demo.js](#demo.js) 17 | - [Quickstart](#quickstart) 18 | - [Requirements](#requirements) 19 | - [Installation](#installation) 20 | - [As a plugin for Neovim platform](#as-a-plugin-for-neovim-platform) 21 | - [As a library from Luarocks](#as-a-library-from-luarocks) 22 | - [Documentation](#documentation) 23 | - [Summary](#summary) 24 | - [async](#async) 25 | - [Development](#development) 26 | - [Neovim tips](#neovim-tips) 27 | - [Run tests](#run-tests) 28 | - [Improve completion experience](#improve-completion-experience) 29 | - [Customize EventLoop](#customize-eventloop) 30 | - [Credit](#credit) 31 | - [Feedback](#feedback) 32 | - [License](#license) 33 | 34 | ## Features 35 | 36 | - API is similar to JavaScript's 37 | - Customize EventLoop in any platforms 38 | - Support Lua 5.1-5.4 and LuaJIT with an EventLoop module 39 | - Support Neovim platform 40 | 41 | ## Demonstration 42 | 43 | 44 | 45 | ### Script 46 | 47 | #### demo.lua 48 | 49 | 50 | 51 | #### demo.js 52 | 53 | 54 | 55 | ## Quickstart 56 | 57 | ### Requirements 58 | 59 | - Lua 5.1 or latter 60 | - [Luv](https://github.com/luvit/luv) 61 | 62 | > Luv is a default EventLoop for promise-async. It doesn't mean promise-async must require it. In 63 | > fact, promise-async require a general EventLoop module which Luv like. 64 | 65 | ### Installation 66 | 67 | #### As a plugin for Neovim platform 68 | 69 | Install with [Packer.nvim](https://github.com/wbthomason/packer.nvim): 70 | 71 | - As a normal plugin 72 | 73 | ```lua 74 | use {'kevinhwang91/promise-async'} 75 | ``` 76 | 77 | or 78 | 79 | - As a Luarocks plugin 80 | 81 | ```lua 82 | use_rocks {'promise-async'} 83 | ``` 84 | 85 | #### As a library from Luarocks 86 | 87 | 1. `luarocks install promise-async` 88 | 2. `luarocks install luv` or implement an EventLoop 89 | [interface](https://github.com/kevinhwang91/promise-async/blob/main/typings/loop.lua) to adapt 90 | your platform 91 | 92 | ## Documentation 93 | 94 | promise-async's API is based on [MDN-Promise][promise]. [typings/promise.lua](typings/promise.lua) 95 | is the typings with documentation of Promise class. 96 | 97 | ### Summary 98 | 99 | Summary up the API different from JavaScript. 100 | 101 | 102 | 103 | | JavaScript | Lua | 104 | | --------------------------------------------------- | ----------------------------------------------- | 105 | | `new Promise` | `Promise:new`/`Promise` | 106 | | `Promise.then` | `Promise:thenCall`, `then` is language keyword | 107 | | `Promise.catch` | `Promise:catch` | 108 | | `Promise.finally` | `Promise:finally` | 109 | | `Promise.resolve` | `Promise.resolve` | 110 | | `Promise.reject` | `Promise.reject` | 111 | | `Promise.all`: `Symbol.iterator` as iterator | `Promise.all`: `pairs` as iterator | 112 | | `Promise.allSettled`: `Symbol.iterator` as iterator | `Promise.allSettled`: `pairs` as iterator | 113 | | `Promise.any`: `Symbol.iterator` as iterator | `Promise.any`: `pairs` as iterator | 114 | | `Promise.race`: `Symbol.iterator` as iterator | `Promise.race`: `pairs` as iterator | 115 | | `async`: as keyword at the start of a function | `Async`/`Async.sync`: as a surrounding function | 116 | | `await`: as keyword | `await`/`Async.wait` as a function | 117 | 118 | 119 | 120 | ### async 121 | 122 | The environment in `Async.sync` function have been injected some new functions for compatibility or 123 | enhancement: 124 | 125 | 1. `await`: A reference of `Async.wait` function; 126 | 2. `pcall`: Be compatible with LuaJIT; 127 | 3. `xpcall`: Be compatible with LuaJIT; 128 | 129 | `async` in JavaScript return Promise object only with single result, but may carry multiple results 130 | in Lua. The resolved result of Promise object return by `async` function will be packed into a table 131 | via `{...}`. However, the result handled by `await` will be unpacked and return multiple values. 132 | 133 | ```lua 134 | local async = require('async') 135 | 136 | local function f() 137 | return 1, 2, 3 138 | end 139 | 140 | -- multiple results are packed into resolved result in Promise 141 | async(f):thenCall(function(v) 142 | print(v[1], v[2], v[3]) -- output: 1 2 3 143 | end) 144 | 145 | -- results returned by `await` 146 | async(function() 147 | local v1, v2, v3 = await(async(f)) 148 | print(v1, v2, v3) -- output: 1 2 3 149 | end) 150 | 151 | uv.run() 152 | ``` 153 | 154 | ## Development 155 | 156 | ### Neovim tips 157 | 158 | - `Promise.resolve():thenCall(cb)` is almost equivalent to `vim.schedule(cb)`. 159 | 160 | ### Run tests 161 | 162 | `make test` 163 | 164 | ### Improve completion experience 165 | 166 | Following [typings/README.md](./typings/README.md) 167 | 168 | ### Customize EventLoop 169 | 170 | TODO, refer to [loop.lua](./lua/promise-async/loop.lua) 171 | 172 | ## Credit 173 | 174 | - [Promise][promise] 175 | - [Async][async] 176 | - [promises-tests](https://github.com/promises-aplus/promises-tests) 177 | - [then/promise](https://github.com/then/promise) 178 | - [promisejs.org](https://www.promisejs.org) 179 | - [event-loop-timers-and-nexttick](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick) 180 | 181 | [promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise 182 | [async]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function 183 | 184 | ## Feedback 185 | 186 | - If you get an issue or come up with an awesome idea, don't hesitate to open an issue in github. 187 | - If you think this plugin is useful or cool, consider rewarding it a star. 188 | 189 | ## License 190 | 191 | The project is licensed under a BSD-3-clause license. See [LICENSE](./LICENSE) file for details. 192 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Make sure that modules can be found under the `package.path` and `package.cpath`. 4 | 5 | If can't find modules, please run `export LUA_PATH="$(dirname $PWD)/lua/?.lua;;"` under CWD in shell 6 | or install the modules except for this project. 7 | -------------------------------------------------------------------------------- /examples/coc_nvim.lua: -------------------------------------------------------------------------------- 1 | local promise = require('promise') 2 | local M = {} 3 | local fn = vim.fn 4 | 5 | function M.action(action, ...) 6 | local args = {...} 7 | return promise(function(resolve, reject) 8 | table.insert(args, function(err, res) 9 | if err ~= vim.NIL then 10 | reject(err) 11 | else 12 | if res == vim.NIL then 13 | res = nil 14 | end 15 | resolve(res) 16 | end 17 | end) 18 | fn.CocActionAsync(action, unpack(args)) 19 | end) 20 | end 21 | 22 | function M.runCommand(name, ...) 23 | return M.action('runCommand', name, ...) 24 | end 25 | 26 | -- 27 | -- M.action('showOutline', true) 28 | 29 | return M 30 | -------------------------------------------------------------------------------- /examples/demo.js: -------------------------------------------------------------------------------- 1 | async function defuse(ms) { 2 | return new Promise((resolve, reject) => { 3 | setTimeout(() => { 4 | resolve(ms) 5 | }, ms) 6 | }) 7 | } 8 | 9 | async function bomb(ms) { 10 | return new Promise((resolve, reject) => { 11 | setTimeout(() => { 12 | reject(ms) 13 | }, ms) 14 | }) 15 | } 16 | 17 | async function race() { 18 | return Promise.race([ 19 | defuse(500 + Math.ceil(Math.random() * 500)), 20 | bomb(800 + Math.ceil(Math.random() * 200)), 21 | ]) 22 | } 23 | 24 | async function play() { 25 | console.info('Game start!') 26 | let cnt = 0 27 | try { 28 | while (true) { 29 | let ms = await race() 30 | cnt = cnt + ms 31 | console.info(`Defuse after ${ms}ms~`) 32 | } 33 | } catch (msErr) { 34 | cnt = cnt + msErr 35 | console.info(`Bomb after ${msErr}ms~`) 36 | } 37 | 38 | console.info(`Game end after ${cnt}ms!`) 39 | 40 | await { 41 | then: function(resolve, reject) { 42 | setTimeout(() => { 43 | reject(this.message) 44 | }, 1000) 45 | }, 46 | message: 'try to throw an error :)' 47 | } 48 | } 49 | 50 | Promise.resolve().then((value) => { 51 | console.info('In next tick') 52 | }) 53 | 54 | console.info('In main') 55 | 56 | play().finally(() => { 57 | console.info('Before throwing UnhandledPromiseRejection on finally!') 58 | }) 59 | -------------------------------------------------------------------------------- /examples/demo.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: unused-local 2 | local uv = require('luv') 3 | local async = require('async') 4 | local promise = require('promise') 5 | 6 | math.randomseed(math.ceil(uv.uptime())) 7 | 8 | local function setTimeout(callback, ms) 9 | local timer = uv.new_timer() 10 | timer:start(ms, 0, function() 11 | timer:close() 12 | callback() 13 | end) 14 | return timer 15 | end 16 | 17 | local function defuse(ms) 18 | return promise:new(function(resolve, reject) 19 | setTimeout(function() 20 | resolve(ms) 21 | end, ms) 22 | end) 23 | end 24 | 25 | local function bomb(ms) 26 | -- getmetatable(promise).__call = promise.new 27 | return promise(function(resolve, reject) 28 | setTimeout(function() 29 | reject(ms) 30 | end, ms) 31 | end) 32 | end 33 | 34 | local function race() 35 | return async(function() 36 | return promise.race({ 37 | defuse(math.random(500, 1000)), 38 | bomb(math.random(800, 1000)) 39 | }) 40 | end) 41 | end 42 | 43 | local notify = vim and vim.notify or print 44 | 45 | local function play() 46 | return async(function() 47 | -- We are not in the next tick until first `await` is called. 48 | notify('Game start!') 49 | local cnt = 0 50 | xpcall(function() 51 | while true do 52 | local ms = await(race()) 53 | cnt = cnt + ms 54 | notify(('Defuse after %dms~'):format(ms)) 55 | end 56 | end, function(msErr) 57 | cnt = cnt + msErr 58 | notify(('Bomb after %dms~'):format(msErr)) 59 | end) 60 | 61 | notify(('Game end after %dms!'):format(cnt)) 62 | 63 | await { 64 | thenCall = function(self, resolve, reject) 65 | setTimeout(function() 66 | reject(self.message) 67 | end, 1000) 68 | end, 69 | message = 'try to throw an error :)' 70 | } 71 | end) 72 | end 73 | 74 | promise.resolve():thenCall(function(value) 75 | notify('In next tick') 76 | end) 77 | 78 | notify('In main') 79 | 80 | play():finally(function() 81 | print('Before throwing UnhandledPromiseRejection on finally!') 82 | end) 83 | 84 | -- uv.run will be called automatically under Neovim main loop 85 | if not vim then 86 | uv.run() 87 | end 88 | -------------------------------------------------------------------------------- /examples/multiple_threads.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: redefined-local 2 | local uv = require('luv') 3 | local promise = require('promise') 4 | local mpack = require('mpack') 5 | 6 | local asyncHandle 7 | local thread 8 | promise(function(resolve, reject) 9 | asyncHandle = uv.new_async(function(err, data) 10 | asyncHandle:close() 11 | if err then 12 | reject((mpack.unpack or mpack.decode)(err)) 13 | else 14 | resolve((mpack.unpack or mpack.decode)(data)) 15 | end 16 | end) 17 | end):thenCall(function(value) 18 | print(('Getting resolved value: %s from %s'):format(value[1], thread)) 19 | end, function(reason) 20 | print(('Getting rejected reason: %s from %s'):format(reason[1], thread)) 21 | end) 22 | 23 | thread = uv.new_thread(function(delay, asyn) 24 | local uv = require('luv') 25 | local mpack = require('mpack') 26 | local promise = require('promise') 27 | math.randomseed(math.ceil(uv.uptime())) 28 | promise(function(resolve, reject) 29 | print(tostring(uv.thread_self()) .. ' is running.') 30 | promise.loop.setTimeout(function() 31 | if math.random(1, 2) == 1 then 32 | resolve({'succeeded'}) 33 | else 34 | reject({'failed'}) 35 | end 36 | end, delay) 37 | end):thenCall(function(value) 38 | uv.async_send(asyn, nil, (mpack.pack or mpack.encode)(value)) 39 | end):catch(function(reason) 40 | uv.async_send(asyn, (mpack.pack or mpack.encode)(reason)) 41 | end) 42 | uv.run() 43 | end, 1000, asyncHandle) 44 | 45 | if not vim then 46 | uv.run() 47 | end 48 | -------------------------------------------------------------------------------- /examples/read_file.lua: -------------------------------------------------------------------------------- 1 | local uv = require('luv') 2 | local uva = require('uva') 3 | local async = require('async') 4 | 5 | local function readFile(path) 6 | return async(function() 7 | local fd = await(uva.open(path, 'r', 438)) 8 | local stat = await(uva.fstat(fd)) 9 | local data = await(uva.read(fd, stat.size, 0)) 10 | await(uva.close(fd)) 11 | return data 12 | end) 13 | end 14 | 15 | local currentPath = debug.getinfo(1, 'S').source:sub(2) 16 | print('Reading ' .. currentPath .. '......\n') 17 | readFile(currentPath):thenCall(function(value) 18 | print(value) 19 | end) 20 | 21 | if not vim then 22 | uv.run() 23 | end 24 | -------------------------------------------------------------------------------- /examples/uva.lua: -------------------------------------------------------------------------------- 1 | ---@class UvFS 2 | local M = {} 3 | 4 | local uv = require('luv') 5 | local promise = require('promise') 6 | local compat = require('promise-async.compat') 7 | 8 | local function wrap(name, argc) 9 | return function(...) 10 | local argv = {...} 11 | return promise(function(resolve, reject) 12 | argv[argc] = function(err, data) 13 | if err then 14 | reject(err) 15 | else 16 | resolve(data) 17 | end 18 | end 19 | uv[name](compat.unpack(argv)) 20 | end) 21 | end 22 | end 23 | 24 | M.close = wrap('fs_close', 2) 25 | M.open = wrap('fs_open', 4) 26 | M.read = wrap('fs_read', 4) 27 | M.unlink = wrap('fs_unlink', 2) 28 | M.write = wrap('fs_write', 4) 29 | M.mkdir = wrap('fs_mkdir', 3) 30 | M.mkdtemp = wrap('fs_mkdtemp', 2) 31 | M.mkstemp = wrap('fs_mkstemp', 2) 32 | M.rmdir = wrap('fs_rmdir', 2) 33 | M.scandir = wrap('fs_scandir', 2) 34 | M.stat = wrap('fs_stat', 2) 35 | M.fstat = wrap('fs_fstat', 2) 36 | M.lstat = wrap('fs_lstat', 2) 37 | M.rename = wrap('fs_rename', 3) 38 | M.fsync = wrap('fs_fsync', 2) 39 | M.fdatasync = wrap('fs_fdatasync', 2) 40 | M.ftruncate = wrap('fs_ftruncate', 3) 41 | M.sendfile = wrap('fs_sendfile', 5) 42 | M.access = wrap('fs_access', 3) 43 | M.chmod = wrap('fs_chmod', 3) 44 | M.fchmod = wrap('fs_fchmod', 3) 45 | M.utime = wrap('fs_utime', 4) 46 | M.futime = wrap('fs_futime', 4) 47 | M.lutime = wrap('fs_lutime', 4) 48 | M.link = wrap('fs_link', 3) 49 | M.symlink = wrap('fs_symlink', 4) 50 | M.readlink = wrap('fs_readlink', 2) 51 | M.realpath = wrap('fs_realpath', 2) 52 | M.chown = wrap('fs_chown', 4) 53 | M.fchown = wrap('fs_fchown', 4) 54 | M.lchown = wrap('fs_lchown', 4) 55 | M.copyfile = wrap('fs_copyfile', 4) 56 | 57 | -- TODO 58 | M.opendir = function(path, entries) 59 | return promise(function(resolve, reject) 60 | uv.fs_opendir(path, function(err, data) 61 | if err then 62 | reject(err) 63 | else 64 | resolve(data) 65 | end 66 | end, entries) 67 | end) 68 | end 69 | 70 | M.readdir = wrap('fs_readdir', 2) 71 | M.closedir = wrap('fs_closedir', 2) 72 | M.statfs = wrap('fs_statfs', 2) 73 | 74 | return M 75 | -------------------------------------------------------------------------------- /examples/write_file.lua: -------------------------------------------------------------------------------- 1 | local uv = require('luv') 2 | local uva = require('uva') 3 | local async = require('async') 4 | 5 | local function writeFile(path, data) 6 | return async(function() 7 | local path_ = path .. '_' 8 | local fd = await(uva.open(path_, 'w', 438)) 9 | await(uva.write(fd, data, -1)) 10 | await(uva.close(fd)) 11 | pcall(await, uva.rename(path_, path)) 12 | end) 13 | end 14 | 15 | local path = debug.getinfo(1, 'S').source:sub(2) .. '__' 16 | print('Writing ' .. path .. '......\n') 17 | writeFile(path, 'write some texts :)\n') 18 | 19 | if not vim then 20 | uv.run() 21 | end 22 | -------------------------------------------------------------------------------- /lua/async.lua: -------------------------------------------------------------------------------- 1 | local promise = require('promise') 2 | local utils = require('promise-async.utils') 3 | local compat = require('promise-async.compat') 4 | 5 | local asyncId = {'promise-async'} 6 | 7 | ---@class Async 8 | local Async = setmetatable({_id = asyncId}, { 9 | __call = function(self, executor) 10 | return self.sync(executor) 11 | end 12 | }) 13 | 14 | local packedId = {} 15 | 16 | local Packed = {_id = packedId} 17 | Packed.__index = Packed 18 | 19 | local function wrapPacked(packed) 20 | return setmetatable(packed, Packed) 21 | end 22 | 23 | local function hasPacked(o) 24 | return type(o) == 'table' and o._id == packedId 25 | end 26 | 27 | local function injectENV(fn) 28 | compat.setfenv(fn, setmetatable({ 29 | await = Async.wait, 30 | pcall = compat.pcall, 31 | xpcall = compat.xpcall 32 | }, { 33 | __index = compat.getfenv(fn) 34 | })) 35 | end 36 | 37 | function Async.sync(executor) 38 | local typ = type(executor) 39 | local isCallable, fn = utils.getCallable(executor, typ) 40 | assert(isCallable, 'a callable table or function expected, got ' .. typ) 41 | injectENV(fn) 42 | return promise:new(function(resolve, reject) 43 | local co = coroutine.create(typ == 'function' and executor or function() 44 | return executor() 45 | end) 46 | 47 | local function afterResume(status, ...) 48 | if not status then 49 | local reason = select(1, ...) 50 | reject(debug.traceback(co, reason)) 51 | return 52 | elseif coroutine.status(co) == 'dead' then 53 | local value 54 | local n = select('#', ...) 55 | if n == 1 then 56 | value = select(1, ...) 57 | elseif n > 1 then 58 | value = wrapPacked({...}) 59 | end 60 | resolve(value) 61 | return 62 | end 63 | local p = select(1, ...) 64 | return p 65 | end 66 | 67 | local function next(err, res) 68 | local p = afterResume(coroutine.resume(co, err, res)) 69 | if p then 70 | p:thenCall(function(value) 71 | next(false, value) 72 | end, function(reason) 73 | next(true, reason) 74 | end) 75 | end 76 | end 77 | 78 | next() 79 | end) 80 | end 81 | 82 | ---Export wait function to someone needs, wait function actually have injected as `await` into 83 | ---the executor of async function 84 | ---@param p Promise|any 85 | ---@return ... 86 | function Async.wait(p) 87 | p = promise.resolve(p) 88 | local err, res = coroutine.yield(p) 89 | if err then 90 | error(res, 0) 91 | elseif hasPacked(res) then 92 | return compat.unpack(res) 93 | else 94 | return res 95 | end 96 | end 97 | 98 | return Async 99 | -------------------------------------------------------------------------------- /lua/promise-async/compat.lua: -------------------------------------------------------------------------------- 1 | ---Functions are compatible with LuaJIT's. 2 | ---@class PromiseAsyncCompat 3 | local M = {} 4 | 5 | ---@return boolean 6 | function M.is51() 7 | return _G._VERSION:sub(-3) == '5.1' and not jit 8 | end 9 | 10 | if table.pack then 11 | M.pack = table.pack 12 | else 13 | M.pack = function(...) 14 | return {n = select('#', ...), ...} 15 | end 16 | end 17 | 18 | if table.unpack then 19 | M.unpack = table.unpack 20 | else 21 | M.unpack = unpack 22 | end 23 | ---@diagnostic enable: deprecated 24 | 25 | if M.is51() then 26 | local _pcall, _xpcall = pcall, xpcall 27 | local utils = require('promise-async.utils') 28 | 29 | local function yieldInCoroutine(thread, co, success, ...) 30 | if coroutine.status(co) == 'suspended' then 31 | return yieldInCoroutine(thread, co, coroutine.resume(co, coroutine.yield(...))) 32 | end 33 | return success, ... 34 | end 35 | 36 | local function doPcall(thread, f, ...) 37 | local typ = type(f) 38 | local ok, fn = utils.getCallable(f, typ) 39 | if not ok then 40 | return false, ('attempt to call a %s value'):format(typ) 41 | end 42 | local co = coroutine.create(function(...) 43 | return fn(...) 44 | end) 45 | return yieldInCoroutine(thread, co, coroutine.resume(co, ...)) 46 | end 47 | 48 | M.pcall = function(f, ...) 49 | local thread = coroutine.running() 50 | if not thread then 51 | return _pcall(f, ...) 52 | end 53 | return doPcall(thread, f, ...) 54 | end 55 | 56 | local function xpcallCatch(msgh, success, ...) 57 | if success then 58 | return true, ... 59 | end 60 | local ok, result = _pcall(msgh, ...) 61 | return false, ok and result or 'error in error handling' 62 | end 63 | 64 | M.xpcall = function(f, msgh, ...) 65 | local thread = coroutine.running() 66 | if not thread then 67 | return _xpcall(f, msgh, ...) 68 | end 69 | return xpcallCatch(msgh, doPcall(thread, f, ...)) 70 | end 71 | else 72 | M.pcall = pcall 73 | M.xpcall = xpcall 74 | end 75 | 76 | if setfenv then 77 | M.setfenv = setfenv 78 | M.getfenv = getfenv 79 | else 80 | local function findENV(f) 81 | local name = '' 82 | local value 83 | local up = 1 84 | while name do 85 | name, value = debug.getupvalue(f, up) 86 | if name == '_ENV' then 87 | return up, value 88 | end 89 | up = up + 1; 90 | end 91 | return 0 92 | end 93 | 94 | local function envHelper(f, name) 95 | if type(f) == 'number' then 96 | if f < 0 then 97 | error(([[bad argument #1 to '%s' (level must be non-negative)]]):format(name), 3) 98 | end 99 | local ok, dInfo = pcall(debug.getinfo, f + 2, 'f') 100 | if not ok or not dInfo then 101 | error(([[bad argument #1 to '%s' (invalid level)]]):format(name), 3) 102 | end 103 | f = dInfo.func 104 | elseif type(f) ~= 'function' then 105 | error(([[bad argument #1 to '%s' (number expected, got %s)]]):format(name, type(f)), 3) 106 | end 107 | return f 108 | end 109 | 110 | function M.setfenv(f, table) 111 | f = envHelper(f, 'setfenv') 112 | local up = findENV(f) 113 | if up > 0 then 114 | debug.upvaluejoin(f, up, function() 115 | return table 116 | end, 1) 117 | end 118 | return f 119 | end 120 | 121 | function M.getfenv(f) 122 | if f == 0 or f == nil then 123 | return _G 124 | end 125 | f = envHelper(f, 'getfenv') 126 | local up, value = findENV(f) 127 | return up > 0 and value or _G 128 | end 129 | end 130 | 131 | return M 132 | -------------------------------------------------------------------------------- /lua/promise-async/error.lua: -------------------------------------------------------------------------------- 1 | local errorId = {} 2 | 3 | ---@class PromiseAsyncError 4 | ---@field err any 5 | ---@field queue string[] 6 | ---@field index number 7 | local Error = {_id = errorId} 8 | Error.__index = Error 9 | 10 | local function dump(o, limit) 11 | local typ = type(o) 12 | if typ == 'string' then 13 | return o 14 | elseif typ ~= 'table' then 15 | return tostring(o) 16 | end 17 | local meta = getmetatable(o) 18 | if meta and meta.__tostring then 19 | return tostring(o) 20 | end 21 | if limit > 0 then 22 | local fmt = '%s [%s] = %s,' 23 | local s = '{' 24 | for k, v in pairs(o) do 25 | if type(k) ~= 'number' then 26 | k = '"' .. k .. '"' 27 | end 28 | s = fmt:format(s, k, dump(v, limit - 1)) 29 | end 30 | return #s == 1 and '{}' or s:sub(1, #s - 1) .. ' }' 31 | else 32 | return '{...}' 33 | end 34 | end 35 | 36 | function Error.isInstance(o) 37 | return type(o) == 'table' and o._id == errorId 38 | end 39 | 40 | local what = _G._VERSION:sub(-3) == '5.1' and 'Snl' or 'Slnt' 41 | 42 | local function outputLevelInfo(dInfo) 43 | local seg = {('\t%s:'):format(dInfo.short_src)} 44 | if dInfo.currentline > 0 then 45 | table.insert(seg, ('%d:'):format(dInfo.currentline)) 46 | end 47 | -- TODO 48 | -- lua 5.3 and 5.4 will look up global function and module function before checking 'namewhat'. 49 | -- And insert "in namewhat name" if not found 50 | if dInfo.namewhat ~= '' then 51 | table.insert(seg, (" in function '%s'"):format(dInfo.name)) 52 | else 53 | if dInfo.what == 'm' then 54 | table.insert(seg, ' in main chunk') 55 | elseif dInfo.what ~= 'C' then 56 | table.insert(seg, (' in function <%s:%d>'):format(dInfo.short_src, dInfo.linedefined)) 57 | else 58 | table.insert(seg, '?') 59 | end 60 | end 61 | if dInfo.istailcall then 62 | table.insert(seg, '\n\t(...tail calls...)') 63 | end 64 | return table.concat(seg, '') 65 | end 66 | 67 | ---@param startLevel? number 68 | ---@param skipShortSrc? string 69 | ---@param doPop? boolean 70 | ---@return PromiseAsyncError 71 | function Error:buildStack(startLevel, skipShortSrc, doPop) 72 | local level = startLevel or 1 73 | local value 74 | local thread = coroutine.running() 75 | while true do 76 | local dInfo = thread and debug.getinfo(thread, level, what) or debug.getinfo(level, what) 77 | if not dInfo or skipShortSrc == dInfo.short_src then 78 | break 79 | end 80 | value = outputLevelInfo(dInfo) 81 | level = level + 1 82 | self:push(value) 83 | end 84 | if doPop then 85 | self:pop() 86 | end 87 | return self 88 | end 89 | 90 | ---@param err any 91 | ---@return PromiseAsyncError 92 | function Error:new(err) 93 | local o = setmetatable({}, self) 94 | o.err = err 95 | o.queue = {} 96 | o.index = 0 97 | return o 98 | end 99 | 100 | function Error:__tostring() 101 | local errMsg = dump(self.err, 1) 102 | if #self.queue == 0 then 103 | return errMsg 104 | end 105 | local t = {} 106 | for i = 1, self.index do 107 | table.insert(t, self.queue[i]) 108 | end 109 | table.insert(t, errMsg) 110 | if self.index < #self.queue then 111 | table.insert(t, 'stack traceback:') 112 | end 113 | for i = self.index + 1, #self.queue do 114 | table.insert(t, self.queue[i]) 115 | end 116 | return table.concat(t, '\n') 117 | end 118 | 119 | ---@param value string 120 | function Error:unshift(value) 121 | if value then 122 | self.index = self.index + 1 123 | table.insert(self.queue, 1, value) 124 | end 125 | return #self.queue 126 | end 127 | 128 | ---@param value? string 129 | function Error:push(value) 130 | if value then 131 | table.insert(self.queue, value) 132 | end 133 | return #self.queue 134 | end 135 | 136 | ---@return string 137 | function Error:pop() 138 | return table.remove(self.queue) 139 | end 140 | 141 | ---@return any 142 | function Error:peek() 143 | return self.err 144 | end 145 | 146 | return Error 147 | -------------------------------------------------------------------------------- /lua/promise-async/loop.lua: -------------------------------------------------------------------------------- 1 | local uv = require('luv') 2 | 3 | ---@class PromiseAsyncLoop 4 | ---@field tick userdata 5 | ---@field tickCallbacks function[] 6 | ---@field tickStarted boolean 7 | ---@field idle userdata 8 | ---@field idleCallbacks function[] 9 | ---@field idleStarted boolean 10 | local EventLoop = { 11 | tick = uv.new_timer(), 12 | tickCallbacks = {}, 13 | tickStarted = false, 14 | idle = uv.new_idle(), 15 | idleCallbacks = {}, 16 | idleStarted = false 17 | } 18 | 19 | function EventLoop.setTimeout(callback, ms) 20 | local timer = uv.new_timer() 21 | timer:start(ms, 0, function() 22 | timer:close() 23 | EventLoop.callWrapper(callback) 24 | end) 25 | return timer 26 | end 27 | 28 | local function runTick() 29 | EventLoop.tickStarted = true 30 | local callbacks = EventLoop.tickCallbacks 31 | EventLoop.tickCallbacks = {} 32 | for _, cb in ipairs(callbacks) do 33 | EventLoop.callWrapper(cb) 34 | end 35 | if #EventLoop.tickCallbacks > 0 then 36 | EventLoop.tick:start(0, 0, runTick) 37 | else 38 | EventLoop.tickStarted = false 39 | end 40 | -- luv loop has invoked close method if the timer has finished 41 | -- EventLoop.tick:close() 42 | end 43 | 44 | function EventLoop.nextTick(callback) 45 | table.insert(EventLoop.tickCallbacks, callback) 46 | if not EventLoop.tickStarted then 47 | EventLoop.tick:start(0, 0, runTick) 48 | end 49 | end 50 | 51 | local function runIdle() 52 | EventLoop.idleStarted = true 53 | local callbacks = EventLoop.idleCallbacks 54 | EventLoop.idleCallbacks = {} 55 | for _, cb in ipairs(callbacks) do 56 | EventLoop.callWrapper(cb) 57 | end 58 | if #EventLoop.idleCallbacks == 0 then 59 | EventLoop.idleStarted = false 60 | EventLoop.idle:stop() 61 | end 62 | end 63 | 64 | function EventLoop.nextIdle(callback) 65 | EventLoop.nextTick(function() 66 | table.insert(EventLoop.idleCallbacks, callback) 67 | if not EventLoop.idleStarted then 68 | EventLoop.idle:start(runIdle) 69 | end 70 | end) 71 | end 72 | 73 | if vim and type(vim.schedule) == 'function' then 74 | EventLoop.callWrapper = vim.schedule 75 | else 76 | -- https://github.com/luvit/luv/pull/665 can throw the non-string error since 1.46 version 77 | function EventLoop.callWrapper(fn) fn() end 78 | end 79 | 80 | return EventLoop 81 | -------------------------------------------------------------------------------- /lua/promise-async/utils.lua: -------------------------------------------------------------------------------- 1 | ---@class PromiseAsyncUtils 2 | local M = {} 3 | 4 | ---@param o any 5 | ---@param expectedType string 6 | function M.assertType(o, expectedType) 7 | local gotType = type(o) 8 | local fmt = '%s expected, got %s' 9 | return assert(gotType == expectedType, fmt:format(expectedType, gotType)) 10 | end 11 | 12 | ---@param o any 13 | ---@param typ? string 14 | ---@return boolean, function|table|any 15 | function M.getCallable(o, typ) 16 | local ok 17 | local f 18 | local t = typ or type(o) 19 | if t == 'function' then 20 | ok, f = true, o 21 | elseif t ~= 'table' then 22 | ok, f = false, o 23 | else 24 | local meta = getmetatable(o) 25 | ok = meta and type(meta.__call) == 'function' 26 | if ok then 27 | f = meta.__call 28 | end 29 | end 30 | return ok, f 31 | end 32 | 33 | return M 34 | -------------------------------------------------------------------------------- /lua/promise.lua: -------------------------------------------------------------------------------- 1 | local utils = require('promise-async.utils') 2 | local promiseId = {'promise-async'} 3 | local errFactory = require('promise-async.error') 4 | local shortSrc = debug.getinfo(1, 'S').short_src 5 | 6 | ---@alias PromiseState 7 | ---| 1 #PENDING 8 | ---| 2 #FULFILLED 9 | ---| 3 #REJECTED 10 | local PENDING = 1 11 | local FULFILLED = 2 12 | local REJECTED = 3 13 | 14 | -- 15 | ---@class Promise 16 | ---@field state PromiseState 17 | ---@field result any 18 | ---@field queue table 19 | ---@field needHandleRejection? boolean 20 | ---@field err? PromiseAsyncError 21 | local Promise = setmetatable({_id = promiseId}, { 22 | __call = function(self, executor) 23 | return self:new(executor) 24 | end 25 | }) 26 | Promise.__index = Promise 27 | 28 | local function loadEventLoop() 29 | local success, res = pcall(require, 'promise-async.loop') 30 | assert(success, 'Promise need an EventLoop, ' .. 31 | 'luv module or a customized EventLoop module is expected.') 32 | return res 33 | end 34 | 35 | if vim then 36 | -- `require` in Neovim is hacked by its get_runtime API, may throw an error while calling 37 | -- `require` in libuv, require at once as a workaround. 38 | Promise.loop = require('promise-async.loop') 39 | else 40 | Promise.loop = setmetatable({}, { 41 | __index = function(_, key) 42 | local loop = loadEventLoop() 43 | rawset(Promise, 'loop', loop) 44 | return loop[key] 45 | end, 46 | __newindex = function(_, key, value) 47 | local loop = loadEventLoop() 48 | rawset(Promise, 'loop', loop) 49 | Promise.loop[key] = value 50 | end 51 | }) 52 | end 53 | 54 | 55 | function Promise:__tostring() 56 | local state = self.state 57 | if state == PENDING then 58 | return 'Promise { }' 59 | elseif state == REJECTED then 60 | return ('Promise { %s }'):format(tostring(self.result)) 61 | else 62 | return ('Promise { %s }'):format(tostring(self.result)) 63 | end 64 | end 65 | 66 | local function noop() end 67 | 68 | ---@param o any 69 | ---@param typ? string 70 | ---@return boolean 71 | function Promise.isInstance(o, typ) 72 | return (typ or type(o)) == 'table' and o._id == promiseId 73 | end 74 | 75 | ---must get `thenCall` field from `o` at one time, can't call repeatedly. 76 | ---@param o any 77 | ---@param typ? type 78 | ---@return function? 79 | local function getThenable(o, typ) 80 | local thenCall 81 | if (typ or type(o)) == 'table' then 82 | thenCall = o.thenCall 83 | if type(thenCall) ~= 'function' then 84 | thenCall = nil 85 | end 86 | end 87 | return thenCall 88 | end 89 | 90 | local resolvePromise, rejectPromise 91 | 92 | ---@param promise Promise 93 | local function handleQueue(promise) 94 | local queue = promise.queue 95 | if #queue == 0 then 96 | return 97 | end 98 | if promise.needHandleRejection and #queue > 0 then 99 | promise.needHandleRejection = nil 100 | end 101 | promise.queue = {} 102 | 103 | Promise.loop.nextTick(function() 104 | local state, result = promise.state, promise.result 105 | for _, q in ipairs(queue) do 106 | local newPromise, onFulfilled, onRejected = q[1], q[2], q[3] 107 | local func 108 | if state == FULFILLED then 109 | if utils.getCallable(onFulfilled) then 110 | func = onFulfilled 111 | else 112 | resolvePromise(newPromise, result) 113 | end 114 | elseif state == REJECTED then 115 | if utils.getCallable(onRejected) then 116 | func = onRejected 117 | else 118 | rejectPromise(newPromise, result) 119 | end 120 | end 121 | if func then 122 | local ok, res = xpcall(function() 123 | return func(result) 124 | end, function(errmsg) 125 | if type(errmsg) == 'string' then 126 | -- pop xpcall stack 127 | newPromise.err = errFactory:new(errmsg) 128 | :buildStack(3, shortSrc, true) 129 | return tostring(newPromise.err) 130 | end 131 | return errmsg 132 | end) 133 | if ok then 134 | resolvePromise(newPromise, res) 135 | else 136 | rejectPromise(newPromise, res) 137 | end 138 | end 139 | end 140 | end) 141 | end 142 | 143 | ---@param promise Promise 144 | ---@param result any 145 | ---@param state PromiseState 146 | local function transition(promise, result, state) 147 | if promise.state ~= PENDING then 148 | return 149 | end 150 | promise.result = result 151 | promise.state = state 152 | handleQueue(promise) 153 | end 154 | 155 | ---@param promise Promise 156 | ---@param executor PromiseExecutor 157 | ---@param self? table 158 | local function wrapExecutor(promise, executor, self) 159 | local called = false 160 | local resolve = function(value) 161 | if called then 162 | return 163 | end 164 | resolvePromise(promise, value) 165 | called = true 166 | end 167 | local reject = function(reason) 168 | if called then 169 | return 170 | end 171 | rejectPromise(promise, reason) 172 | called = true 173 | end 174 | 175 | local ok, res = xpcall(function() 176 | if self then 177 | ---@diagnostic disable-next-line: redundant-parameter, param-type-mismatch 178 | return executor(self, resolve, reject) 179 | else 180 | return executor(resolve, reject) 181 | end 182 | end, function(errmsg) 183 | if type(errmsg) == 'string' then 184 | -- pop xpcall stack 185 | promise.err = errFactory:new(errmsg) 186 | :buildStack(3, shortSrc, true) 187 | return tostring(promise.err) 188 | end 189 | return errmsg 190 | end) 191 | if not ok and not called then 192 | reject(res) 193 | end 194 | end 195 | 196 | ---@param promise Promise 197 | local function handleRejection(promise) 198 | promise.needHandleRejection = true 199 | 200 | Promise.loop.nextIdle(function() 201 | if promise.needHandleRejection then 202 | promise.needHandleRejection = nil 203 | local err = promise.err 204 | if not err then 205 | err = errFactory:new(promise.result) 206 | end 207 | err:unshift('UnhandledPromiseRejection with the reason:') 208 | error(err) 209 | end 210 | end) 211 | end 212 | 213 | ---@param promise Promise 214 | ---@param reason any 215 | rejectPromise = function(promise, reason) 216 | handleRejection(promise) 217 | transition(promise, reason, REJECTED) 218 | end 219 | 220 | ---@param promise Promise 221 | ---@param value any 222 | resolvePromise = function(promise, value) 223 | if promise == value then 224 | local reason = debug.traceback('TypeError: Chaining cycle detected for promise') 225 | rejectPromise(promise, reason) 226 | return 227 | end 228 | 229 | local valueType = type(value) 230 | if Promise.isInstance(value, valueType) then 231 | value:thenCall(function(val) 232 | resolvePromise(promise, val) 233 | end, function(reason) 234 | rejectPromise(promise, reason) 235 | end) 236 | else 237 | local thenCall = getThenable(value, valueType) 238 | if thenCall then 239 | wrapExecutor(promise, thenCall, value) 240 | else 241 | transition(promise, value, FULFILLED) 242 | end 243 | end 244 | end 245 | 246 | function Promise:new(executor) 247 | utils.assertType(executor, 'function') 248 | local o = self == Promise and setmetatable({}, self) or self 249 | o.state = PENDING 250 | o.result = nil 251 | o.queue = {} 252 | o.needHandleRejection = nil 253 | 254 | if executor ~= noop then 255 | wrapExecutor(o, executor) 256 | end 257 | return o 258 | end 259 | 260 | function Promise:thenCall(onFulfilled, onRejected) 261 | local o = self.new(Promise, noop) 262 | table.insert(self.queue, {o, onFulfilled, onRejected}) 263 | if self.state ~= PENDING then 264 | handleQueue(self) 265 | end 266 | return o 267 | end 268 | 269 | function Promise:catch(onRejected) 270 | return self:thenCall(nil, onRejected) 271 | end 272 | 273 | function Promise:finally(onFinally) 274 | local function wrapFinally() 275 | if utils.getCallable(onFinally) then 276 | ---@diagnostic disable-next-line: need-check-nil 277 | onFinally() 278 | end 279 | end 280 | 281 | return self:thenCall(function(value) 282 | wrapFinally() 283 | return value 284 | end, function(reason) 285 | wrapFinally() 286 | return Promise.reject(reason) 287 | end) 288 | end 289 | 290 | function Promise.resolve(value) 291 | local typ = type(value) 292 | if Promise.isInstance(value, typ) then 293 | return value 294 | else 295 | local o = Promise:new(noop) 296 | local thenCall = getThenable(value, typ) 297 | if thenCall then 298 | wrapExecutor(o, thenCall, value) 299 | else 300 | o.state = FULFILLED 301 | o.result = value 302 | end 303 | return o 304 | end 305 | end 306 | 307 | function Promise.reject(reason) 308 | local o = Promise:new(noop) 309 | o.state = REJECTED 310 | o.result = reason 311 | handleRejection(o) 312 | return o 313 | end 314 | 315 | function Promise.all(values) 316 | utils.assertType(values, 'table') 317 | return Promise:new(function(resolve, reject) 318 | local res = {} 319 | local cnt = 0 320 | for k, v in pairs(values) do 321 | cnt = cnt + 1 322 | Promise.resolve(v):thenCall(function(value) 323 | res[k] = value 324 | cnt = cnt - 1 325 | if cnt == 0 then 326 | resolve(res) 327 | end 328 | end, function(reason) 329 | reject(reason) 330 | end) 331 | end 332 | if cnt == 0 then 333 | resolve(res) 334 | end 335 | end) 336 | end 337 | 338 | function Promise.allSettled(values) 339 | utils.assertType(values, 'table') 340 | return Promise:new(function(resolve, reject) 341 | local res = {} 342 | local cnt = 0 343 | local _ = reject 344 | for k, v in pairs(values) do 345 | cnt = cnt + 1 346 | Promise.resolve(v):thenCall(function(value) 347 | res[k] = {status = 'fulfilled', value = value} 348 | end, function(reason) 349 | res[k] = {status = 'rejected', reason = reason} 350 | end):finally(function() 351 | cnt = cnt - 1 352 | if cnt == 0 then 353 | resolve(res) 354 | end 355 | end) 356 | end 357 | if cnt == 0 then 358 | resolve(res) 359 | end 360 | end) 361 | end 362 | 363 | function Promise.any(values) 364 | utils.assertType(values, 'table') 365 | return Promise:new(function(resolve, reject) 366 | local cnt = 0 367 | local function rejectAggregateError() 368 | if cnt == 0 then 369 | reject('AggregateError: All promises were rejected') 370 | end 371 | end 372 | 373 | for _, p in pairs(values) do 374 | cnt = cnt + 1 375 | Promise.resolve(p):thenCall(function(value) 376 | resolve(value) 377 | end, function() 378 | end):finally(function() 379 | cnt = cnt - 1 380 | rejectAggregateError() 381 | end) 382 | end 383 | rejectAggregateError() 384 | end) 385 | end 386 | 387 | function Promise.race(values) 388 | utils.assertType(values, 'table') 389 | return Promise:new(function(resolve, reject) 390 | for _, p in pairs(values) do 391 | Promise.resolve(p):thenCall(function(value) 392 | resolve(value) 393 | end, function(reason) 394 | reject(reason) 395 | end) 396 | end 397 | end) 398 | end 399 | 400 | return Promise 401 | -------------------------------------------------------------------------------- /rockspec/promise-async-1.0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = 'promise-async' 2 | version = '1.0-0' 3 | source = { 4 | url = 'git+https://github.com/kevinhwang91/promise-async.git', 5 | tag = 'v1.0.0' 6 | } 7 | description = { 8 | summary = 'Promise & Async in Lua', 9 | detailed = 'The goal of promise-async is to port Promise & Async from JavaScript to Lua.', 10 | homepage = 'https://github.com/kevinhwang91/promise-async', 11 | license = ' BSD-3-Clause' 12 | } 13 | 14 | dependencies = { 15 | 'lua >= 5.1, <= 5.4' 16 | } 17 | 18 | build = { 19 | type = 'builtin', 20 | modules = { 21 | async = 'lua/async.lua', 22 | promise = 'lua/promise.lua', 23 | ['promise-async.compat'] = 'lua/promise-async/compat.lua', 24 | ['promise-async.error'] = 'lua/promise-async/error.lua', 25 | ['promise-async.loop'] = 'lua/promise-async/loop.lua', 26 | ['promise-async.utils'] = 'lua/promise-async/utils.lua' 27 | }, 28 | copy_directories = { 29 | 'typings' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /rockspec/promise-async-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = 'promise-async' 2 | version = 'scm-0' 3 | source = { 4 | url = 'git+https://github.com/kevinhwang91/promise-async.git' 5 | } 6 | description = { 7 | summary = 'Promise & Async in Lua', 8 | detailed = 'The goal of promise-async is to port Promise & Async from JavaScript to Lua.', 9 | homepage = 'https://github.com/kevinhwang91/promise-async', 10 | license = ' BSD-3-Clause' 11 | } 12 | 13 | dependencies = { 14 | 'lua >= 5.1, <= 5.4' 15 | } 16 | 17 | build = { 18 | type = 'builtin', 19 | modules = { 20 | async = 'lua/async.lua', 21 | promise = 'lua/promise.lua', 22 | ['promise-async.compat'] = 'lua/promise-async/compat.lua', 23 | ['promise-async.error'] = 'lua/promise-async/error.lua', 24 | ['promise-async.loop'] = 'lua/promise-async/loop.lua', 25 | ['promise-async.utils'] = 'lua/promise-async/utils.lua' 26 | }, 27 | copy_directories = { 28 | 'typings' 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /spec/async_spec.lua: -------------------------------------------------------------------------------- 1 | local promise = require('promise') 2 | local async = require('async') 3 | local helpers = require('spec.helpers.init') 4 | local compat = require('promise-async.compat') 5 | local deferredPromise = helpers.deferredPromise 6 | local setTimeout = helpers.setTimeout 7 | local basics = require('spec.helpers.basics') 8 | local dummy = {dummy = 'dummy'} 9 | local sentinel = {sentinel = 'sentinel'} 10 | local sentinel2 = {sentinel = 'sentinel2'} 11 | local sentinel3 = {sentinel = 'sentinel3'} 12 | local other = {other = 'other'} 13 | 14 | describe('async await module.', function() 15 | describe('async return a Promise.', function() 16 | it('return value is a Promise', function() 17 | local f = async(function() end) 18 | assert.True(promise.isInstance(f)) 19 | end) 20 | 21 | it('async without return statement', function() 22 | async(function() end) 23 | :thenCall(function(value) 24 | assert.equal(nil, value) 25 | done() 26 | end) 27 | assert.True(wait()) 28 | end) 29 | 30 | it('async return a single', function() 31 | async(function() 32 | return dummy 33 | end):thenCall(function(value) 34 | assert.equal(dummy, value) 35 | done() 36 | end) 37 | assert.True(wait()) 38 | end) 39 | 40 | it('async return multiple values, which are packed into resolved result in Promise', function() 41 | async(function() 42 | return sentinel, sentinel2, sentinel3 43 | end):thenCall(function(value) 44 | assert.same({sentinel, sentinel2, sentinel3}, value) 45 | done() 46 | end) 47 | assert.True(wait()) 48 | end) 49 | 50 | it('async throw error', function() 51 | async(function() 52 | error(dummy) 53 | return other 54 | end):thenCall(nil, function(reason) 55 | assert.equal(dummy, reason) 56 | done() 57 | end) 58 | assert.True(wait()) 59 | end) 60 | end) 61 | 62 | describe('executor inside async.', function() 63 | describe('must be either a function or a callable table,', function() 64 | it('other values', function() 65 | local errorValues = {nil, 0, '0', true} 66 | for _, v in pairs(errorValues) do 67 | assert.error(function() 68 | async(v) 69 | end) 70 | end 71 | end) 72 | 73 | it('a function', function() 74 | async(function() 75 | setTimeout(function() 76 | done() 77 | end, 10) 78 | end) 79 | assert.True(wait()) 80 | end) 81 | 82 | it('a callable table', function() 83 | async(setmetatable({}, { 84 | __call = function() 85 | setTimeout(function() 86 | done() 87 | end, 10) 88 | end 89 | })) 90 | assert.True(wait()) 91 | end) 92 | end) 93 | 94 | it('should run immediately', function() 95 | local executor = spy.new(function() end) 96 | async(executor) 97 | assert.spy(executor).was_called() 98 | end) 99 | 100 | it('until the await is called, executor should run immediately even if in nested function', function() 101 | local value 102 | local executor = spy.new(function() end) 103 | async(function() 104 | value = async.wait(async(function() 105 | return async(function() 106 | executor() 107 | return dummy 108 | end) 109 | end)) 110 | done() 111 | end) 112 | assert.spy(executor).was_called() 113 | assert.True(wait()) 114 | assert.equal(dummy, value) 115 | end) 116 | end) 117 | 118 | describe([[await inside async's executor.]], function() 119 | local function testBasicAwait(expectedValue, stringRepresentation) 120 | it('should wait for the promise with resolved value: ' .. stringRepresentation, function() 121 | local value 122 | local p, resolve = deferredPromise() 123 | async(function() 124 | value = await(p) 125 | done() 126 | end) 127 | assert.False(wait(10)) 128 | resolve(expectedValue) 129 | assert.True(wait()) 130 | assert.equal(expectedValue, value) 131 | end) 132 | end 133 | 134 | for valueStr, basicFn in pairs(basics) do 135 | testBasicAwait(basicFn(), valueStr) 136 | end 137 | end) 138 | 139 | describe('`pcall` and `xpcall` surround statement or function.', function() 140 | it('call `pcall` to get the value from a single await', function() 141 | local ok, value 142 | local p, resolve = deferredPromise() 143 | async(function() 144 | ok, value = pcall(await, p) 145 | done() 146 | end) 147 | assert.False(wait(10)) 148 | resolve(dummy) 149 | assert.True(wait()) 150 | assert.True(ok) 151 | assert.equal(dummy, value) 152 | end) 153 | 154 | it('call `xpcall` to get the value from a single await', function() 155 | local ok, value 156 | local p, resolve = deferredPromise() 157 | async(function() 158 | ok, value = xpcall(await, function() end, p) 159 | done() 160 | end) 161 | assert.False(wait(10)) 162 | resolve(dummy) 163 | assert.True(wait()) 164 | assert.True(ok) 165 | assert.equal(dummy, value) 166 | end) 167 | 168 | it('call `pcall` to catch the reason from a single await', function() 169 | local ok, reason 170 | local p, _, reject = deferredPromise() 171 | async(function() 172 | ok, reason = pcall(await, p) 173 | done() 174 | end) 175 | assert.False(wait(10)) 176 | reject(dummy) 177 | assert.True(wait()) 178 | assert.False(ok) 179 | assert.equal(dummy, reason) 180 | end) 181 | 182 | it('call `xpcall` to catch the reason from a single await', function() 183 | local ok, reason 184 | local p, _, reject = deferredPromise() 185 | async(function() 186 | ok = xpcall(await, function(e) 187 | reason = e 188 | end, p) 189 | done() 190 | end) 191 | assert.False(wait(10)) 192 | reject(dummy) 193 | assert.True(wait()) 194 | assert.False(ok) 195 | assert.equal(dummy, reason) 196 | end) 197 | 198 | describe('call `pcall` to catch the reason from a function,', function() 199 | it('throw error after the result return by await', function() 200 | local ok, value, reason 201 | local p1, resolve = deferredPromise() 202 | local p2, _, reject = deferredPromise() 203 | async(function() 204 | ok, reason = pcall(function() 205 | value = await(p1) 206 | await(p2) 207 | end) 208 | done() 209 | end) 210 | p1:thenCall(function() 211 | reject(dummy) 212 | end) 213 | assert.False(wait(10)) 214 | setTimeout(function() 215 | resolve(other) 216 | end, 20) 217 | assert.True(wait()) 218 | assert.False(ok) 219 | assert.equal(other, value) 220 | assert.equal(dummy, reason) 221 | end) 222 | 223 | it('throw error before the result return by await', function() 224 | local ok, value, reason 225 | local p1, _, reject = deferredPromise() 226 | local p2, resolve = deferredPromise() 227 | async(function() 228 | ok, reason = pcall(function() 229 | await(p1) 230 | value = await(p2) 231 | end) 232 | done() 233 | end) 234 | resolve(other) 235 | assert.False(wait(10)) 236 | setTimeout(function() 237 | reject(dummy) 238 | end, 20) 239 | assert.True(wait()) 240 | assert.False(ok) 241 | assert.equal(nil, value) 242 | assert.equal(dummy, reason) 243 | end) 244 | end) 245 | 246 | describe('call `xpcall` to catch the reason from a function,', function() 247 | it('throw error after the result return by await', function() 248 | local ok, value, reason 249 | local p1, resolve = deferredPromise() 250 | local p2, _, reject = deferredPromise() 251 | async(function() 252 | ok = xpcall(function() 253 | value = await(p1) 254 | await(p2) 255 | end, function(e) 256 | reason = e 257 | end) 258 | done() 259 | end) 260 | p1:thenCall(function() 261 | reject(dummy) 262 | end) 263 | assert.False(wait(10)) 264 | setTimeout(function() 265 | resolve(other) 266 | end, 20) 267 | assert.True(wait()) 268 | assert.False(ok) 269 | assert.equal(other, value) 270 | assert.equal(dummy, reason) 271 | end) 272 | 273 | it('throw error before the result return by await', function() 274 | local ok, value, reason 275 | local p1, _, reject = deferredPromise() 276 | local p2, resolve = deferredPromise() 277 | async(function() 278 | ok = xpcall(function() 279 | await(p1) 280 | value = await(p2) 281 | end, function(e) 282 | reason = e 283 | end) 284 | done() 285 | end) 286 | resolve(other) 287 | assert.False(wait(10)) 288 | setTimeout(function() 289 | reject(dummy) 290 | end, 20) 291 | assert.True(wait()) 292 | assert.False(ok) 293 | assert.equal(nil, value) 294 | assert.equal(dummy, reason) 295 | end) 296 | end) 297 | end) 298 | 299 | describe('nested async functions.', function() 300 | it('simple call chain', function() 301 | local value 302 | async(function() 303 | value = await(async(function() 304 | return async(function() 305 | return dummy 306 | end) 307 | end)) 308 | done() 309 | end) 310 | assert.True(wait()) 311 | assert.equal(dummy, value) 312 | end) 313 | 314 | it('deferred call', function() 315 | local value 316 | setTimeout(function() 317 | async(function() 318 | value = await(async(function() 319 | return async(function() 320 | return sentinel 321 | end) 322 | end)) 323 | done() 324 | end) 325 | end, 10) 326 | assert.True(wait()) 327 | assert.equal(sentinel, value) 328 | end) 329 | 330 | it('return multiple values', function() 331 | local value1, value2, value3, value4 332 | async(function() 333 | value1, value2, value3, value4 = await(async(function() 334 | return sentinel, sentinel2, sentinel3 335 | end)) 336 | done() 337 | end) 338 | assert.True(wait()) 339 | assert.equal(sentinel, value1) 340 | assert.equal(sentinel2, value2) 341 | assert.equal(sentinel3, value3) 342 | assert.equal(nil, value4) 343 | end) 344 | 345 | it('should catch error from the deepest callee', function() 346 | local ok, value, reason 347 | async(function() 348 | ok = xpcall(function() 349 | value = await(async(function() 350 | return async(function() 351 | error(dummy) 352 | return other 353 | end) 354 | end)) 355 | end, function(e) 356 | reason = e 357 | end) 358 | done() 359 | end) 360 | assert.True(wait()) 361 | assert.False(ok) 362 | assert.equal(nil, value) 363 | assert.equal(dummy, reason) 364 | end) 365 | 366 | it('should wait for the deepest callee', function() 367 | local value 368 | local p, resolve = deferredPromise() 369 | async(function() 370 | value = await(async(function() 371 | return await(async(function() 372 | return await(p) 373 | end)) 374 | end)) 375 | done() 376 | end) 377 | resolve(dummy) 378 | assert.True(wait()) 379 | assert.equal(dummy, value) 380 | end) 381 | 382 | it('should wait for all callees', function() 383 | local value 384 | local p1, resolve1 = deferredPromise() 385 | local p2, resolve2 = deferredPromise() 386 | local p3, resolve3 = deferredPromise() 387 | 388 | async(function() 389 | value = compat.pack(await(async(function() 390 | return await(p1), await(async(function() 391 | return await(p2), await(p3) 392 | end)) 393 | end))) 394 | done() 395 | end) 396 | assert.False(wait(10)) 397 | resolve3(sentinel3) 398 | assert.False(wait(10)) 399 | resolve2(sentinel2) 400 | assert.False(wait(10)) 401 | resolve1(sentinel) 402 | assert.True(wait()) 403 | assert.same({sentinel, sentinel2, sentinel3, n = 3}, value) 404 | end) 405 | end) 406 | end) 407 | -------------------------------------------------------------------------------- /spec/error_spec.lua: -------------------------------------------------------------------------------- 1 | local e = require('promise-async.error') 2 | local basics = require('spec.helpers.basics') 3 | 4 | describe('Error for Promise and Async.', function() 5 | describe('Basic operations about error,', function() 6 | local msg = 'message test' 7 | it('create a error', function() 8 | local err = e:new(msg) 9 | assert.equal(msg, tostring(err)) 10 | end) 11 | 12 | it('append messages to a error', function() 13 | local err = e:new(msg) 14 | err:push('foo') 15 | err:push('bar') 16 | assert.equal('message test\nstack traceback:\nfoo\nbar', tostring(err)) 17 | end) 18 | 19 | it('do unshift for a error', function() 20 | local err = e:new(msg) 21 | err:push('foo') 22 | err:push('bar') 23 | err:unshift('UnhandledPromiseRejection baz:') 24 | assert.equal('UnhandledPromiseRejection baz:\nmessage test\nstack traceback:\nfoo\nbar', tostring(err)) 25 | end) 26 | end) 27 | 28 | describe('Build stacks.', function() 29 | describe('Build basic values for top of stack.', function() 30 | -- keep stack always from tail calls 31 | local function level3(v) 32 | local s = e:new(v):buildStack(2) 33 | return tostring(s) 34 | end 35 | local function level2(v) 36 | local o = level3(v) 37 | return o 38 | end 39 | local function level1(v) 40 | local o = level2(v) 41 | return o 42 | end 43 | local src = debug.getinfo(1).short_src 44 | local level1Line = debug.getinfo(level1, 'S').linedefined 45 | local level2Line = debug.getinfo(level2, 'S').linedefined 46 | local level3Line = debug.getinfo(level3, 'S').linedefined 47 | local stackStr = ([[stack traceback: 48 | %s:%d: in function 'level3' 49 | %s:%d: in function 'level2' 50 | %s:%d: in function 'level1']]):format(src, level3Line + 1, src, level2Line + 1, src, level1Line + 1) 51 | local function testBasicTopStack(expectedValue, stringRepresentation) 52 | it('The value is ' .. stringRepresentation .. '.', function() 53 | local s = level1(expectedValue) 54 | assert.truthy(s:find(tostring(expectedValue) .. '\n' .. stackStr, 1, true)) 55 | end) 56 | end 57 | 58 | for valueStr, basicFn in pairs(basics) do 59 | testBasicTopStack(basicFn(), valueStr) 60 | end 61 | end) 62 | end) 63 | end) 64 | -------------------------------------------------------------------------------- /spec/fixtures.lua: -------------------------------------------------------------------------------- 1 | local busted = require('busted') 2 | 3 | local _done 4 | local co = _G.co 5 | 6 | busted.subscribe({'test', 'start'}, function() 7 | _done = false 8 | end) 9 | 10 | local function getDone() 11 | return _done 12 | end 13 | 14 | function _G.done() 15 | _done = true 16 | return _done 17 | end 18 | 19 | --------------------------------------------------------------------- 20 | ---Need to implement _G.wait to pass tests 21 | 22 | local defaultTimeout = 1000 23 | 24 | ---Should override this function to customize EventLoop to pass tests 25 | ---@param ms? number 26 | ---@return boolean 27 | function _G.wait(ms) 28 | if getDone() then 29 | return true 30 | end 31 | local interval = 5 32 | local timer = require('luv').new_timer() 33 | local cnt = 0 34 | ms = ms or defaultTimeout 35 | timer:start(interval, interval, function() 36 | cnt = cnt + interval 37 | local d = getDone() 38 | if cnt >= ms or d then 39 | timer:stop() 40 | timer:close() 41 | local thread = coroutine.running() 42 | if thread ~= co then 43 | require('spec.helpers.init').setTimeout(function() 44 | coroutine.resume(co, d) 45 | end, 0) 46 | end 47 | end 48 | end) 49 | return coroutine.yield() 50 | end 51 | -------------------------------------------------------------------------------- /spec/helpers/basics.lua: -------------------------------------------------------------------------------- 1 | local Basic = {} 2 | 3 | local co = coroutine.create(function() end) 4 | coroutine.resume(co) 5 | 6 | Basic['`nil`'] = function() 7 | return nil 8 | end 9 | 10 | Basic['`false`'] = function() 11 | return false 12 | end 13 | 14 | Basic['`0`'] = function() 15 | return 0 16 | end 17 | 18 | Basic['`string`'] = function() 19 | return 'string' 20 | end 21 | 22 | Basic['a metatable'] = function() 23 | return setmetatable({}, { 24 | __tostring = function() 25 | return '{}' 26 | end 27 | }) 28 | end 29 | 30 | Basic['a thread'] = function() 31 | return co 32 | end 33 | 34 | return Basic 35 | -------------------------------------------------------------------------------- /spec/helpers/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local promise = require('promise') 3 | local reject = promise.reject 4 | 5 | promise.reject = function (reason) 6 | local p = reject(reason) 7 | p.needHandleRejection = nil 8 | return p 9 | end 10 | 11 | M.setTimeout = promise.loop.setTimeout 12 | 13 | function M.deferredPromise() 14 | local resolve, reject 15 | local p = promise(function(resolve0, reject0) 16 | resolve, reject = resolve0, reject0 17 | end) 18 | return p, resolve, reject 19 | end 20 | 21 | function M.testFulfilled(it, assert, value, test) 22 | it('already-fulfilled', function() 23 | test(promise.resolve(value)) 24 | assert.True(wait()) 25 | end) 26 | 27 | it('immediately-fulfilled', function() 28 | local p, resolve = M.deferredPromise() 29 | test(p) 30 | resolve(value) 31 | assert.True(wait()) 32 | end) 33 | 34 | it('eventually-fulfilled', function() 35 | local p, resolve = M.deferredPromise() 36 | test(p) 37 | wait(10) 38 | resolve(value) 39 | assert.True(wait()) 40 | end) 41 | end 42 | 43 | function M.testRejected(it, assert, reason, test) 44 | it('already-rejected', function() 45 | test(promise.reject(reason)) 46 | assert.True(wait()) 47 | end) 48 | 49 | it('immediately-rejected', function() 50 | local p, _, reject = M.deferredPromise() 51 | test(p) 52 | reject(reason) 53 | assert.True(wait()) 54 | end) 55 | 56 | it('eventually-fulfilled', function() 57 | local p, _, reject = M.deferredPromise() 58 | test(p) 59 | wait(10) 60 | reject(reason) 61 | assert.True(wait()) 62 | end) 63 | end 64 | 65 | return M 66 | -------------------------------------------------------------------------------- /spec/helpers/outputHandler.lua: -------------------------------------------------------------------------------- 1 | return function(options) 2 | local busted = require('busted') 3 | local handler = require('busted.outputHandlers.utfTerminal')(options) 4 | 5 | local promiseUnhandledError = {} 6 | 7 | busted.subscribe({'test', 'end'}, function(element, parent) 8 | while #promiseUnhandledError > 0 do 9 | local res = table.remove(promiseUnhandledError, 1) 10 | handler.successesCount = handler.successesCount - 1 11 | handler.failuresCount = handler.failuresCount + 1 12 | busted.publish({'failure', element.descriptor}, element, parent, tostring(res)) 13 | end 14 | end) 15 | 16 | require('promise').loop.callWrapper = function(callback) 17 | local ok, res = pcall(callback) 18 | if ok then 19 | return 20 | end 21 | table.insert(promiseUnhandledError, tostring(res)) 22 | end 23 | return handler 24 | end 25 | -------------------------------------------------------------------------------- /spec/helpers/reasons.lua: -------------------------------------------------------------------------------- 1 | local promise = require('promise') 2 | local Reasons = {} 3 | local dummy = {dummy = 'dummy'} 4 | 5 | Reasons['`nil`'] = function() 6 | return nil 7 | end 8 | 9 | Reasons['`false`'] = function() 10 | return false 11 | end 12 | 13 | -- Lua before 5.3 versions will transfer number to string after pcall. 14 | -- Pure string will carry some extra information after pcall, no need to test 15 | -- Reasons['`0`'] = function() 16 | -- return 0 17 | -- end 18 | 19 | Reasons['a metatable'] = function() 20 | return setmetatable({}, {}) 21 | end 22 | 23 | Reasons['an always-pending thenable'] = function() 24 | return { 25 | thenCall = function() end 26 | } 27 | end 28 | 29 | Reasons['a fulfilled promise'] = function() 30 | return promise.resolve(dummy) 31 | end 32 | 33 | Reasons['a rejected promise'] = function() 34 | return promise.reject(dummy) 35 | end 36 | 37 | return Reasons 38 | -------------------------------------------------------------------------------- /spec/helpers/thenables.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local setTimeout = helpers.setTimeout 3 | local deferredPromise = helpers.deferredPromise 4 | local promise = require('promise') 5 | local other = {other = 'other'} 6 | 7 | local Thenables = { 8 | fulfilled = { 9 | ['a synchronously-fulfilled custom thenable'] = function(value) 10 | return { 11 | thenCall = function(self, resolvePromise) 12 | local _ = self 13 | resolvePromise(value) 14 | end 15 | } 16 | end, 17 | ['an asynchronously-fulfilled custom thenable'] = function(value) 18 | return { 19 | thenCall = function(self, resolvePromise) 20 | local _ = self 21 | setTimeout(function() 22 | resolvePromise(value) 23 | end, 0) 24 | end 25 | } 26 | end, 27 | ['a synchronously-fulfilled one-time thenable'] = function(value) 28 | local numberOfTimesThenRetrieved = 0; 29 | return setmetatable({}, { 30 | __index = function(_, k) 31 | if numberOfTimesThenRetrieved == 0 and k == 'thenCall' then 32 | numberOfTimesThenRetrieved = numberOfTimesThenRetrieved + 1 33 | return function(self, resolvePromise) 34 | local _ = self 35 | resolvePromise(value) 36 | end 37 | end 38 | return nil 39 | end 40 | }) 41 | end, 42 | ['a thenable that tries to fulfill twice'] = function(value) 43 | return { 44 | thenCall = function(self, resolvePromise) 45 | local _ = self 46 | resolvePromise(value) 47 | resolvePromise(other) 48 | end 49 | } 50 | end, 51 | ['a thenable that fulfills but then throws'] = function(value) 52 | return { 53 | thenCall = function(self, resolvePromise) 54 | local _ = self 55 | resolvePromise(value) 56 | error(other) 57 | end 58 | } 59 | end, 60 | ['an already-fulfilled promise'] = function(value) 61 | return promise.resolve(value) 62 | end, 63 | ['an eventually-fulfilled promise'] = function(value) 64 | local p, resolve = deferredPromise() 65 | setTimeout(function() 66 | resolve(value) 67 | end, 10) 68 | return p 69 | end 70 | }, 71 | rejected = { 72 | ['a synchronously-rejected custom thenable'] = function(reason) 73 | return { 74 | thenCall = function(self, resolvePromise, rejectPromise) 75 | local _, _ = self, resolvePromise 76 | rejectPromise(reason) 77 | end 78 | } 79 | end, 80 | ['an asynchronously-rejected custom thenable'] = function(reason) 81 | return { 82 | thenCall = function(self, resolvePromise, rejectPromise) 83 | local _, _ = self, resolvePromise 84 | setTimeout(function() 85 | rejectPromise(reason) 86 | end, 0) 87 | end 88 | } 89 | end, 90 | ['a synchronously-rejected one-time thenable'] = function(reason) 91 | local numberOfTimesThenRetrieved = 0; 92 | return setmetatable({}, { 93 | __index = function(_, k) 94 | if numberOfTimesThenRetrieved == 0 and k == 'thenCall' then 95 | numberOfTimesThenRetrieved = numberOfTimesThenRetrieved + 1 96 | return function(self, resolvePromise, rejectPromise) 97 | local _, _ = self, resolvePromise 98 | rejectPromise(reason) 99 | end 100 | end 101 | return nil 102 | end 103 | }) 104 | end, 105 | ['a thenable that immediately throws in `thenCall`'] = function(reason) 106 | return { 107 | thenCall = function() 108 | error(reason) 109 | end 110 | } 111 | end, 112 | ['an table with a throwing `thenCall` metatable'] = function(reason) 113 | return setmetatable({}, { 114 | __index = function(_, k) 115 | if k == 'thenCall' then 116 | return function() 117 | error(reason) 118 | end 119 | end 120 | return nil 121 | end 122 | }) 123 | end, 124 | ['an already-rejected promise'] = function(reason) 125 | return promise.reject(reason) 126 | end, 127 | ['an eventually-rejected promise'] = function(reason) 128 | local p, _, reject = deferredPromise() 129 | setTimeout(function() 130 | reject(reason) 131 | end, 10) 132 | return p 133 | end 134 | } 135 | } 136 | 137 | return Thenables 138 | -------------------------------------------------------------------------------- /spec/init.lua: -------------------------------------------------------------------------------- 1 | package.path = os.getenv('PWD') .. '/lua/?.lua;' .. package.path 2 | local compat = require('promise-async.compat') 3 | 4 | if compat.is51() then 5 | _G.pcall = compat.pcall 6 | _G.xpcall = compat.xpcall 7 | end 8 | 9 | local uv = require('luv') 10 | 11 | local co = coroutine.create(function() 12 | require('busted.runner')({standalone = false, output = 'spec.helpers.outputHandler'}) 13 | -- no errors for nvim 14 | if vim then 15 | vim.schedule(function() 16 | vim.cmd('cq 0') 17 | end) 18 | end 19 | end) 20 | 21 | _G.co = co 22 | 23 | local compatibility = require('busted.compatibility') 24 | 25 | if vim then 26 | compatibility.exit = function(code) 27 | vim.schedule(function() 28 | vim.cmd(('cq %d'):format(code)) 29 | end) 30 | end 31 | _G.arg = vim.fn.argv() 32 | _G.print = function(...) 33 | local argv = {...} 34 | for i = 1, #argv do 35 | argv[i] = tostring(argv[i]) 36 | end 37 | table.insert(argv, '\n') 38 | io.write(unpack(argv)) 39 | end 40 | coroutine.resume(co) 41 | else 42 | local c = 0 43 | -- https://github.com/luvit/luv/issues/599 44 | compatibility.exit = function(code) 45 | c = code 46 | end 47 | local idle = uv.new_idle() 48 | idle:start(function() 49 | idle:stop() 50 | coroutine.resume(co) 51 | end) 52 | uv.run() 53 | os.exit(c) 54 | end 55 | -------------------------------------------------------------------------------- /spec/loop_spec.lua: -------------------------------------------------------------------------------- 1 | local loop = require('promise').loop 2 | 3 | local function testAsynchronousFunction(name) 4 | it(name .. 'is asynchronous', function() 5 | local called = spy.new(function() end) 6 | loop[name](function() 7 | called() 8 | done() 9 | end, 0) 10 | assert.spy(called).was_not_called() 11 | assert.True(wait()) 12 | assert.spy(called).was_called() 13 | end) 14 | end 15 | 16 | describe('EventLoop for Promise.', function() 17 | testAsynchronousFunction('setTimeout') 18 | testAsynchronousFunction('nextTick') 19 | testAsynchronousFunction('nextIdle') 20 | 21 | describe('fire `nextIdle` is later than `nextTick`,', function() 22 | it('call `nextTick` first', function() 23 | local queue = {} 24 | local tick = {'tick'} 25 | local idle = {'idle'} 26 | loop.nextTick(function() 27 | table.insert(queue, tick) 28 | end) 29 | loop.nextIdle(function() 30 | table.insert(queue, idle) 31 | end) 32 | loop.setTimeout(done, 50) 33 | assert.True(wait()) 34 | assert.same(tick, queue[1]) 35 | assert.same(idle, queue[2]) 36 | end) 37 | 38 | it('call `nextIdle` first', function() 39 | local queue = {} 40 | local tick = {'tick'} 41 | local idle = {'idle'} 42 | loop.nextIdle(function() 43 | table.insert(queue, idle) 44 | end) 45 | loop.nextTick(function() 46 | table.insert(queue, tick) 47 | end) 48 | loop.setTimeout(done, 50) 49 | assert.True(wait()) 50 | assert.same(tick, queue[1]) 51 | assert.same(idle, queue[2]) 52 | end) 53 | end) 54 | 55 | it('call `nextTick` in `nextTick` event', function() 56 | local onTick = spy.new(function() end) 57 | local onNextTick = spy.new(function() end) 58 | loop.nextTick(function() 59 | onTick() 60 | loop.nextTick(function() 61 | onNextTick() 62 | assert.spy(onNextTick).was_called() 63 | done() 64 | end) 65 | assert.spy(onTick).was_called() 66 | assert.spy(onNextTick).was_not_called() 67 | end) 68 | 69 | assert.True(wait()) 70 | end) 71 | 72 | it('call `nextIdle` in `nextIdle` event', function() 73 | local onIdle = spy.new(function() end) 74 | local onNextIdle = spy.new(function() end) 75 | loop.nextIdle(function() 76 | onIdle() 77 | loop.nextIdle(function() 78 | onNextIdle() 79 | assert.spy(onNextIdle).was_called() 80 | done() 81 | end) 82 | assert.spy(onIdle).was_called() 83 | assert.spy(onNextIdle).was_not_called() 84 | end) 85 | 86 | assert.True(wait()) 87 | end) 88 | 89 | it('override callWrapper method', function() 90 | local rawCallWrapper = loop.callWrapper 91 | local callback = function() end 92 | loop.callWrapper = function(fn) 93 | loop.callWrapper = rawCallWrapper 94 | assert.same(callback, fn) 95 | done() 96 | end 97 | loop.nextTick(callback) 98 | assert.True(wait()) 99 | loop.callWrapper = rawCallWrapper 100 | end) 101 | end) 102 | -------------------------------------------------------------------------------- /spec/promiseA+/2.1.2_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local setTimeout = helpers.setTimeout 3 | local testFulfilled = helpers.testFulfilled 4 | local deferredPromise = helpers.deferredPromise 5 | local dummy = {dummy = 'dummy'} 6 | 7 | describe('2.1.2.1: When fulfilled, a promise: must not transition to any other state.', function() 8 | local onFulfilled, onRejected = spy.new(function() end), spy.new(function() end) 9 | 10 | before_each(function() 11 | onFulfilled:clear() 12 | onRejected:clear() 13 | end) 14 | 15 | testFulfilled(it, assert, dummy, function(p) 16 | local onFulfilledCalled = false 17 | p:thenCall(function() 18 | onFulfilledCalled = true 19 | end, function() 20 | assert.False(onFulfilledCalled) 21 | done() 22 | end) 23 | 24 | setTimeout(function() 25 | done() 26 | end, 50) 27 | end) 28 | 29 | it('trying to fulfill then immediately reject', function() 30 | local p, resolve, reject = deferredPromise() 31 | p:thenCall(onFulfilled, onRejected) 32 | resolve(dummy) 33 | reject(dummy) 34 | 35 | setTimeout(function() 36 | done() 37 | end, 50) 38 | assert.True(wait()) 39 | assert.spy(onFulfilled).was_called() 40 | assert.spy(onRejected).was_not_called() 41 | end) 42 | 43 | it('trying to fulfill then reject, delayed', function() 44 | local p, resolve, reject = deferredPromise() 45 | p:thenCall(onFulfilled, onRejected) 46 | resolve(dummy) 47 | 48 | setTimeout(function() 49 | reject(dummy) 50 | done() 51 | end, 50) 52 | assert.True(wait()) 53 | assert.spy(onFulfilled).was_called() 54 | assert.spy(onRejected).was_not_called() 55 | end) 56 | 57 | it('trying to fulfill immediately then reject, delayed', function() 58 | local p, resolve, reject = deferredPromise() 59 | p:thenCall(onFulfilled, onRejected) 60 | 61 | setTimeout(function() 62 | resolve(dummy) 63 | reject(dummy) 64 | done() 65 | end, 50) 66 | assert.True(wait()) 67 | assert.spy(onFulfilled).was_called() 68 | assert.spy(onRejected).was_not_called() 69 | end) 70 | end) 71 | -------------------------------------------------------------------------------- /spec/promiseA+/2.1.3_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local testRejected = helpers.testRejected 3 | local deferredPromise = helpers.deferredPromise 4 | local setTimeout = helpers.setTimeout 5 | local dummy = {dummy = 'dummy'} 6 | 7 | describe('2.1.3.1: When rejected, a promise: must not transition to any other state.', function() 8 | local onFulfilled, onRejected = spy.new(function() end), spy.new(function() end) 9 | 10 | before_each(function() 11 | onFulfilled:clear() 12 | onRejected:clear() 13 | end) 14 | 15 | testRejected(it, assert, dummy, function(p) 16 | local onRejectedCalled = false 17 | p:thenCall(function() 18 | assert.False(onRejectedCalled) 19 | done() 20 | end, function() 21 | onRejectedCalled = true 22 | end) 23 | 24 | setTimeout(function() 25 | done() 26 | end, 50) 27 | end) 28 | 29 | it('trying to reject then immediately fulfill', function() 30 | local p, resolve, reject = deferredPromise() 31 | p:thenCall(onFulfilled, onRejected) 32 | reject(dummy) 33 | resolve(dummy) 34 | 35 | setTimeout(function() 36 | done() 37 | end, 50) 38 | assert.True(wait()) 39 | assert.spy(onFulfilled).was_not_called() 40 | assert.spy(onRejected).was_called() 41 | end) 42 | 43 | it('trying to reject then fulfill, delayed', function() 44 | local p, resolve, reject = deferredPromise() 45 | p:thenCall(onFulfilled, onRejected) 46 | reject(dummy) 47 | 48 | setTimeout(function() 49 | resolve(dummy) 50 | done() 51 | end, 50) 52 | assert.True(wait()) 53 | assert.spy(onFulfilled).was_not_called() 54 | assert.spy(onRejected).was_called() 55 | end) 56 | 57 | it('trying to reject immediately then fulfill, delayed', function() 58 | local p, resolve, reject = deferredPromise() 59 | p:thenCall(onFulfilled, onRejected) 60 | 61 | setTimeout(function() 62 | reject(dummy) 63 | resolve(dummy) 64 | done() 65 | end, 50) 66 | assert.True(wait()) 67 | assert.spy(onFulfilled).was_not_called() 68 | assert.spy(onRejected).was_called() 69 | end) 70 | end) 71 | -------------------------------------------------------------------------------- /spec/promiseA+/2.2.1_spec.lua: -------------------------------------------------------------------------------- 1 | local promise = require('promise') 2 | local dummy = {dummy = 'dummy'} 3 | 4 | describe('2.2.1: Both `onFulfilled` and `onRejected` are optional arguments.', function() 5 | describe('2.2.1.1: If `onFulfilled` is not a function, it must be ignored.', function() 6 | describe('applied to a directly-rejected promise', function() 7 | local function testNonFunction(nonFunction, stringRepresentation) 8 | it('`onFulfilled` is ' .. stringRepresentation, function() 9 | promise.reject(dummy) 10 | :thenCall(nonFunction, function() 11 | done() 12 | end) 13 | assert.True(wait()) 14 | end) 15 | end 16 | 17 | testNonFunction(nil, '`nil`') 18 | testNonFunction(false, '`false`') 19 | testNonFunction(5, '`5`') 20 | testNonFunction({}, '`a table`') 21 | end) 22 | 23 | describe('applied to a promise rejected and then chained off of', function() 24 | local function testNonFunction(nonFunction, stringRepresentation) 25 | it('`onFulfilled` is ' .. stringRepresentation, function() 26 | promise.reject(dummy) 27 | :thenCall(function() end, nil) 28 | :thenCall(nonFunction, function() 29 | done() 30 | end) 31 | assert.True(wait()) 32 | end) 33 | end 34 | 35 | testNonFunction(nil, '`nil`') 36 | testNonFunction(false, '`false`') 37 | testNonFunction(5, '`5`') 38 | testNonFunction({}, '`a table`') 39 | end) 40 | end) 41 | 42 | describe('2.2.1.2: If `onRejected` is not a function, it must be ignored.', function() 43 | describe('applied to a directly-fulfilled promise', function() 44 | local function testNonFunction(nonFunction, stringRepresentation) 45 | it('`onRejected` is ' .. stringRepresentation, function() 46 | promise.resolve(dummy) 47 | :thenCall(function() 48 | done() 49 | end, nonFunction) 50 | assert.True(wait()) 51 | end) 52 | end 53 | 54 | testNonFunction(nil, '`nil`') 55 | testNonFunction(false, '`false`') 56 | testNonFunction(5, '`5`') 57 | testNonFunction({}, '`a table`') 58 | end) 59 | 60 | describe('applied to a promise fulfilled and then chained off of', function() 61 | local function testNonFunction(nonFunction, stringRepresentation) 62 | it('`onRejected` is ' .. stringRepresentation, function() 63 | promise.resolve(dummy) 64 | :thenCall(nil, function() end) 65 | :thenCall(function() 66 | done() 67 | end, nonFunction) 68 | assert.True(wait()) 69 | end) 70 | end 71 | 72 | testNonFunction(nil, '`nil`') 73 | testNonFunction(false, '`false`') 74 | testNonFunction(5, '`5`') 75 | testNonFunction({}, '`a table`') 76 | end) 77 | end) 78 | end) 79 | -------------------------------------------------------------------------------- /spec/promiseA+/2.2.2_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local testFulfilled = helpers.testFulfilled 3 | local setTimeout = helpers.setTimeout 4 | local deferredPromise = helpers.deferredPromise 5 | local promise = require('promise') 6 | local dummy = {dummy = 'dummy'} 7 | local sentinel = {sentinel = 'sentinel'} 8 | 9 | describe('2.2.2: If `onFulfilled` is a function,', function() 10 | describe('2.2.2.1: it must be called after `promise` is fulfilled, ' .. 11 | 'with `promise`’s fulfillment value as its first argument.', function() 12 | testFulfilled(it, assert, sentinel, function(p) 13 | p:thenCall(function(value) 14 | assert.equal(sentinel, value) 15 | done() 16 | end) 17 | end) 18 | end) 19 | 20 | describe('2.2.2.2: it must not be called before `promise` is fulfilled', function() 21 | it('fulfilled after a delay', function() 22 | local onFulfilled = spy.new(done) 23 | local p, resolve = deferredPromise() 24 | p:thenCall(onFulfilled) 25 | 26 | setTimeout(function() 27 | resolve(dummy) 28 | end, 10) 29 | assert.True(wait()) 30 | assert.spy(onFulfilled).was_called(1) 31 | end) 32 | 33 | it('never fulfilled', function() 34 | local onFulfilled = spy.new(done) 35 | local p = deferredPromise() 36 | p:thenCall(onFulfilled) 37 | assert.False(wait(30)) 38 | assert.spy(onFulfilled).was_not_called() 39 | end) 40 | end) 41 | 42 | describe('2.2.2.3: it must not be called more than once.', function() 43 | it('already-fulfilled', function() 44 | local onFulfilled = spy.new(done) 45 | promise.resolve(dummy):thenCall(onFulfilled) 46 | assert.spy(onFulfilled).was_not_called() 47 | assert.True(wait()) 48 | assert.spy(onFulfilled).was_called(1) 49 | end) 50 | 51 | it('trying to fulfill a pending promise more than once, immediately', function() 52 | local onFulfilled = spy.new(done) 53 | local p, resolve = deferredPromise() 54 | p:thenCall(onFulfilled) 55 | resolve(dummy) 56 | resolve(dummy) 57 | assert.True(wait()) 58 | assert.spy(onFulfilled).was_called(1) 59 | end) 60 | 61 | it('trying to fulfill a pending promise more than once, delayed', function() 62 | local onFulfilled = spy.new(done) 63 | local p, resolve = deferredPromise() 64 | p:thenCall(onFulfilled) 65 | setTimeout(function() 66 | resolve(dummy) 67 | resolve(dummy) 68 | end, 10) 69 | assert.True(wait()) 70 | assert.spy(onFulfilled).was_called(1) 71 | end) 72 | 73 | it('trying to fulfill a pending promise more than once, immediately then delayed', function() 74 | local onFulfilled = spy.new(done) 75 | local p, resolve = deferredPromise() 76 | p:thenCall(onFulfilled) 77 | resolve(dummy) 78 | setTimeout(function() 79 | resolve(dummy) 80 | end, 10) 81 | assert.True(wait()) 82 | assert.spy(onFulfilled).was_called(1) 83 | end) 84 | 85 | it('when multiple `thenCall` calls are made, spaced apart in time', function() 86 | local onFulfilled1 = spy.new(function() end) 87 | local onFulfilled2 = spy.new(function() end) 88 | local onFulfilled3 = spy.new(function() end) 89 | local p, resolve = deferredPromise() 90 | p:thenCall(onFulfilled1) 91 | setTimeout(function() 92 | p:thenCall(onFulfilled2) 93 | end, 10) 94 | setTimeout(function() 95 | p:thenCall(onFulfilled3) 96 | end, 20) 97 | setTimeout(function() 98 | resolve(dummy) 99 | done() 100 | end, 30) 101 | assert.True(wait()) 102 | assert.spy(onFulfilled1).was_called(1) 103 | assert.spy(onFulfilled2).was_called(1) 104 | assert.spy(onFulfilled3).was_called(1) 105 | end) 106 | 107 | it('when `thenCall` is interleaved with fulfillment', function() 108 | local onFulfilled1 = spy.new(function() end) 109 | local onFulfilled2 = spy.new(function() end) 110 | local p, resolve = deferredPromise() 111 | p:thenCall(onFulfilled1) 112 | resolve(dummy) 113 | setTimeout(function() 114 | p:thenCall(onFulfilled2) 115 | done() 116 | end, 10) 117 | assert.True(wait()) 118 | assert.spy(onFulfilled1).was_called(1) 119 | assert.spy(onFulfilled2).was_called(1) 120 | end) 121 | end) 122 | end) 123 | -------------------------------------------------------------------------------- /spec/promiseA+/2.2.3_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local testRejected = helpers.testRejected 3 | local setTimeout = helpers.setTimeout 4 | local deferredPromise = helpers.deferredPromise 5 | local promise = require('promise') 6 | local dummy = {dummy = 'dummy'} 7 | local sentinel = {sentinel = 'sentinel'} 8 | 9 | describe('2.2.3: If `onRejected` is a function,', function() 10 | describe('2.2.3.1: it must be called after `promise` is rejected, ' .. 11 | 'with `promise`’s rejection reason as its first argument.', function() 12 | testRejected(it, assert, sentinel, function(p) 13 | p:thenCall(nil, function(reason) 14 | assert.equal(sentinel, reason) 15 | done() 16 | end) 17 | end) 18 | end) 19 | 20 | describe('2.2.3.2: it must not be called before `promise` is rejected', function() 21 | it('rejected after a delay', function() 22 | local onRejected = spy.new(done) 23 | local p, _, reject = deferredPromise() 24 | p:thenCall(nil, onRejected) 25 | 26 | setTimeout(function() 27 | reject(dummy) 28 | end, 10) 29 | assert.True(wait()) 30 | assert.spy(onRejected).was_called(1) 31 | end) 32 | 33 | it('never rejected', function() 34 | local onRejected = spy.new(done) 35 | local p = deferredPromise() 36 | p:thenCall(nil, onRejected) 37 | assert.False(wait(30)) 38 | assert.spy(onRejected).was_not_called() 39 | end) 40 | end) 41 | 42 | describe('2.2.3.3: it must not be called more than once.', function() 43 | it('already-rejected', function() 44 | local onRejected = spy.new(done) 45 | promise.reject(dummy):thenCall(nil, onRejected) 46 | assert.spy(onRejected).was_not_called() 47 | assert.True(wait()) 48 | assert.spy(onRejected).was_called(1) 49 | end) 50 | 51 | it('trying to reject a pending promise more than once, immediately', function() 52 | local onRejected = spy.new(done) 53 | local p, _, reject = deferredPromise() 54 | p:thenCall(nil, onRejected) 55 | reject(dummy) 56 | reject(dummy) 57 | assert.True(wait()) 58 | assert.spy(onRejected).was_called(1) 59 | end) 60 | 61 | it('trying to reject a pending promise more than once, delayed', function() 62 | local onRejected = spy.new(done) 63 | local p, _, reject = deferredPromise() 64 | p:thenCall(nil, onRejected) 65 | setTimeout(function() 66 | reject(dummy) 67 | reject(dummy) 68 | end, 10) 69 | assert.True(wait()) 70 | assert.spy(onRejected).was_called(1) 71 | end) 72 | 73 | it('trying to reject a pending promise more than once, immediately then delayed', function() 74 | local onRejected = spy.new(done) 75 | local p, _, reject = deferredPromise() 76 | p:thenCall(nil, onRejected) 77 | reject(dummy) 78 | setTimeout(function() 79 | reject(dummy) 80 | end, 10) 81 | assert.True(wait()) 82 | assert.spy(onRejected).was_called(1) 83 | end) 84 | 85 | it('when multiple `thenCall` calls are made, spaced apart in time', function() 86 | local onRejected1 = spy.new(function() end) 87 | local onRejected2 = spy.new(function() end) 88 | local onRejected3 = spy.new(function() end) 89 | local p, _, reject = deferredPromise() 90 | p:thenCall(nil, onRejected1) 91 | setTimeout(function() 92 | p:thenCall(nil, onRejected2) 93 | end, 15) 94 | setTimeout(function() 95 | p:thenCall(nil, onRejected3) 96 | end, 25) 97 | setTimeout(function() 98 | reject(dummy) 99 | done() 100 | end, 35) 101 | assert.True(wait()) 102 | assert.spy(onRejected1).was_called(1) 103 | assert.spy(onRejected2).was_called(1) 104 | assert.spy(onRejected3).was_called(1) 105 | end) 106 | 107 | it('when `thenCall` is interleaved with rejection', function() 108 | local onRejected1 = spy.new(function() end) 109 | local onRejected2 = spy.new(function() end) 110 | local p, _, reject = deferredPromise() 111 | p:thenCall(nil, onRejected1) 112 | reject(dummy) 113 | setTimeout(function() 114 | p:thenCall(nil, onRejected2) 115 | done() 116 | end, 10) 117 | assert.True(wait()) 118 | assert.spy(onRejected1).was_called(1) 119 | assert.spy(onRejected2).was_called(1) 120 | end) 121 | end) 122 | end) 123 | -------------------------------------------------------------------------------- /spec/promiseA+/2.2.4_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local testFulfilled = helpers.testFulfilled 3 | local testRejected = helpers.testRejected 4 | local setTimeout = helpers.setTimeout 5 | local deferredPromise = helpers.deferredPromise 6 | local promise = require('promise') 7 | local dummy = {dummy = 'dummy'} 8 | 9 | describe('2.2.4: `onFulfilled` or `onRejected` must not be called until ' .. 10 | 'the execution context stack contains only platform code.', function() 11 | describe('`thenCall` returns before the promise becomes fulfilled or rejected', function() 12 | testFulfilled(it, assert, dummy, function(p) 13 | local onFulfilled = spy.new(done) 14 | p:thenCall(onFulfilled) 15 | end) 16 | 17 | testRejected(it, assert, dummy, function(p) 18 | local onRejected = spy.new(done) 19 | p:thenCall(nil, onRejected) 20 | end) 21 | end) 22 | 23 | describe('Clean-stack execution ordering tests (fulfillment case)', function() 24 | local onFulfilled = spy.new(done) 25 | 26 | before_each(function() 27 | onFulfilled:clear() 28 | end) 29 | 30 | it('when `onFulfilled` is added immediately before the promise is fulfilled', function() 31 | local p, resolve = deferredPromise() 32 | p:thenCall(onFulfilled) 33 | resolve(dummy) 34 | assert.True(wait()) 35 | assert.spy(onFulfilled).was_called(1) 36 | end) 37 | 38 | it('when `onFulfilled` is added immediately after the promise is fulfilled', function() 39 | local p, resolve = deferredPromise() 40 | resolve(dummy) 41 | p:thenCall(onFulfilled) 42 | assert.True(wait()) 43 | assert.spy(onFulfilled).was_called(1) 44 | end) 45 | 46 | it('when one `onFulfilled` is added inside another `onFulfilled`', function() 47 | local p = promise.resolve() 48 | p:thenCall(function() 49 | p:thenCall(onFulfilled) 50 | end) 51 | assert.True(wait()) 52 | assert.spy(onFulfilled).was_called(1) 53 | end) 54 | 55 | it('when `onFulfilled` is added inside an `onRejected`', function() 56 | local p1 = promise.reject() 57 | local p2 = promise.resolve() 58 | p1:thenCall(nil, function() 59 | p2:thenCall(onFulfilled) 60 | end) 61 | assert.True(wait()) 62 | assert.spy(onFulfilled).was_called(1) 63 | end) 64 | 65 | it('when the promise is fulfilled asynchronously', function() 66 | local p, resolve = deferredPromise() 67 | setTimeout(function() 68 | resolve(dummy) 69 | end, 0) 70 | p:thenCall(onFulfilled) 71 | assert.True(wait()) 72 | assert.spy(onFulfilled).was_called(1) 73 | end) 74 | end) 75 | 76 | describe('Clean-stack execution ordering tests (rejection case)', function() 77 | local onRejected = spy.new(done) 78 | 79 | before_each(function() 80 | onRejected:clear() 81 | end) 82 | 83 | it('when `onRejected` is added immediately before the promise is rejected', function() 84 | local p, _, reject = deferredPromise() 85 | p:thenCall(nil, onRejected) 86 | reject(dummy) 87 | assert.True(wait()) 88 | assert.spy(onRejected).was_called(1) 89 | end) 90 | 91 | it('when `onRejected` is added immediately after the promise is rejected', function() 92 | local p, _, reject = deferredPromise() 93 | reject(dummy) 94 | p:thenCall(nil, onRejected) 95 | assert.True(wait()) 96 | assert.spy(onRejected).was_called(1) 97 | end) 98 | 99 | it('when `onRejected` is added inside an `onFulfilled`', function() 100 | local p1 = promise.resolve() 101 | local p2 = promise.reject() 102 | p1:thenCall(function() 103 | p2:thenCall(nil, onRejected) 104 | end) 105 | assert.True(wait()) 106 | assert.spy(onRejected).was_called(1) 107 | end) 108 | 109 | it('when one `onRejected` is added inside another `onRejected`', function() 110 | local p = promise.reject() 111 | p:thenCall(nil, function() 112 | p:thenCall(nil, onRejected) 113 | end) 114 | assert.True(wait()) 115 | assert.spy(onRejected).was_called(1) 116 | end) 117 | 118 | it('when the promise is rejected asynchronously', function() 119 | local p, _, reject = deferredPromise() 120 | setTimeout(function() 121 | reject(dummy) 122 | end, 0) 123 | p:thenCall(nil, onRejected) 124 | assert.True(wait()) 125 | assert.spy(onRejected).was_called(1) 126 | end) 127 | end) 128 | end) 129 | -------------------------------------------------------------------------------- /spec/promiseA+/2.2.6_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local testFulfilled = helpers.testFulfilled 3 | local testRejected = helpers.testRejected 4 | local setTimeout = helpers.setTimeout 5 | local dummy = {dummy = 'dummy'} 6 | local sentinel = {sentinel = 'sentinel'} 7 | local sentinel2 = {sentinel = 'sentinel2'} 8 | local sentinel3 = {sentinel = 'sentinel3'} 9 | 10 | describe('2.2.6: `thenCall` may be called multiple times on the same promise.', function() 11 | local function callbackAggregator(times, ultimateCallback) 12 | local soFar = 0 13 | return function() 14 | soFar = soFar + 1 15 | if soFar == times then 16 | ultimateCallback() 17 | end 18 | end 19 | end 20 | 21 | describe('2.2.6.1: If/when `promise` is fulfilled, all respective `onFulfilled` callbacks ' .. 22 | 'must execute in the order of their originating calls to `thenCall`.', function() 23 | describe('multiple boring fulfillment handlers', function() 24 | testFulfilled(it, assert, sentinel, function(p) 25 | local onFulfilled1 = spy.new(function() end) 26 | local onFulfilled2 = spy.new(function() end) 27 | local onFulfilled3 = spy.new(function() end) 28 | local onRejected = spy.new(function() end) 29 | p:thenCall(onFulfilled1, onRejected) 30 | p:thenCall(onFulfilled2, onRejected) 31 | p:thenCall(onFulfilled3, onRejected) 32 | p:thenCall(function(value) 33 | assert.equal(sentinel, value) 34 | 35 | assert.spy(onFulfilled1).was_called_with(sentinel) 36 | assert.spy(onFulfilled2).was_called_with(sentinel) 37 | assert.spy(onFulfilled3).was_called_with(sentinel) 38 | assert.spy(onRejected).was_not_called() 39 | done() 40 | end) 41 | end) 42 | end) 43 | 44 | describe('multiple fulfillment handlers, one of which throws', function() 45 | testFulfilled(it, assert, sentinel, function(p) 46 | local onFulfilled1 = spy.new(function() end) 47 | local onFulfilled2 = spy.new(function() 48 | error() 49 | end) 50 | local onFulfilled3 = spy.new(function() end) 51 | local onRejected = spy.new(function() end) 52 | p:thenCall(onFulfilled1, onRejected) 53 | p:thenCall(onFulfilled2, onRejected):catch(function() end) 54 | p:thenCall(onFulfilled3, onRejected) 55 | p:thenCall(function(value) 56 | assert.equal(sentinel, value) 57 | assert.spy(onFulfilled1).was_called_with(sentinel) 58 | assert.spy(onFulfilled2).was_called_with(sentinel) 59 | assert.spy(onFulfilled3).was_called_with(sentinel) 60 | assert.spy(onRejected).was_not_called() 61 | done() 62 | end) 63 | end) 64 | end) 65 | 66 | describe('results in multiple branching chains with their own fulfillment values', function() 67 | testFulfilled(it, assert, dummy, function(p) 68 | local semiDone = callbackAggregator(3, function() 69 | done() 70 | end) 71 | 72 | p:thenCall(function() 73 | return sentinel 74 | end):thenCall(function(value) 75 | assert.equal(sentinel, value) 76 | semiDone() 77 | end) 78 | 79 | p:thenCall(function() 80 | error(sentinel2) 81 | end):thenCall(nil, function(reason) 82 | assert.equal(sentinel2, reason) 83 | semiDone() 84 | end) 85 | 86 | p:thenCall(function() 87 | return sentinel3 88 | end):thenCall(function(value) 89 | assert.equal(sentinel3, value) 90 | semiDone() 91 | end) 92 | end) 93 | end) 94 | 95 | describe('`onFulfilled` handlers are called in the original order', function() 96 | local queue = {} 97 | local function enQueue(value) 98 | table.insert(queue, value) 99 | end 100 | 101 | before_each(function() 102 | queue = {} 103 | end) 104 | 105 | testFulfilled(it, assert, dummy, function(p) 106 | local function onFulfilled1() 107 | enQueue(1) 108 | end 109 | 110 | local function onFulfilled2() 111 | enQueue(2) 112 | end 113 | 114 | local function onFulfilled3() 115 | enQueue(3) 116 | end 117 | 118 | p:thenCall(onFulfilled1) 119 | p:thenCall(onFulfilled2) 120 | p:thenCall(onFulfilled3) 121 | 122 | p:thenCall(function() 123 | assert.same({1, 2, 3}, queue) 124 | done() 125 | end) 126 | end) 127 | 128 | describe('even when one handler is added inside another handler', function() 129 | testFulfilled(it, assert, dummy, function(p) 130 | local function onFulfilled1() 131 | enQueue(1) 132 | end 133 | 134 | local function onFulfilled2() 135 | enQueue(2) 136 | end 137 | 138 | local function onFulfilled3() 139 | enQueue(3) 140 | end 141 | 142 | p:thenCall(function() 143 | onFulfilled1() 144 | p:thenCall(onFulfilled3) 145 | end) 146 | p:thenCall(onFulfilled2) 147 | 148 | p:thenCall(function() 149 | setTimeout(function() 150 | assert.same({1, 2, 3}, queue) 151 | done() 152 | end, 10) 153 | end) 154 | end) 155 | end) 156 | end) 157 | end) 158 | 159 | describe('2.2.6.2: If/when `promise` is rejected, all respective `onRejected` callbacks ' .. 160 | 'must execute in the order of their originating calls to `thenCall`.', function() 161 | describe('multiple boring rejection handlers', function() 162 | testRejected(it, assert, sentinel, function(p) 163 | local onFulfilled = spy.new(function() end) 164 | local onRejected1 = spy.new(function() end) 165 | local onRejected2 = spy.new(function() end) 166 | local onRejected3 = spy.new(function() end) 167 | p:thenCall(onFulfilled, onRejected1) 168 | p:thenCall(onFulfilled, onRejected2) 169 | p:thenCall(onFulfilled, onRejected3) 170 | p:thenCall(nil, function(reason) 171 | assert.equal(sentinel, reason) 172 | 173 | assert.spy(onRejected1).was_called_with(sentinel) 174 | assert.spy(onRejected2).was_called_with(sentinel) 175 | assert.spy(onRejected3).was_called_with(sentinel) 176 | assert.spy(onFulfilled).was_not_called() 177 | done() 178 | end) 179 | end) 180 | end) 181 | 182 | describe('multiple rejection handlers, one of which throws', function() 183 | testRejected(it, assert, sentinel, function(p) 184 | local onFulfilled = spy.new(function() end) 185 | local onRejected1 = spy.new(function() end) 186 | local onRejected2 = spy.new(function() 187 | error() 188 | end) 189 | local onRejected3 = spy.new(function() end) 190 | p:thenCall(onFulfilled, onRejected1) 191 | p:thenCall(onFulfilled, onRejected2):catch(function() end) 192 | p:thenCall(onFulfilled, onRejected3) 193 | p:thenCall(nil, function(reason) 194 | assert.equal(sentinel, reason) 195 | 196 | assert.spy(onRejected1).was_called_with(sentinel) 197 | assert.spy(onRejected2).was_called_with(sentinel) 198 | assert.spy(onRejected3).was_called_with(sentinel) 199 | assert.spy(onFulfilled).was_not_called() 200 | done() 201 | end) 202 | end) 203 | end) 204 | 205 | describe('results in multiple branching chains with their own rejection values', function() 206 | testRejected(it, assert, dummy, function(p) 207 | local semiDone = callbackAggregator(3, function() 208 | done() 209 | end) 210 | 211 | p:thenCall(nil, function() 212 | return sentinel 213 | end):thenCall(function(value) 214 | assert.equal(sentinel, value) 215 | semiDone() 216 | end) 217 | 218 | p:thenCall(nil, function() 219 | error(sentinel2) 220 | end):thenCall(nil, function(reason) 221 | assert.equal(sentinel2, reason) 222 | semiDone() 223 | end) 224 | 225 | p:thenCall(nil, function() 226 | return sentinel3 227 | end):thenCall(function(value) 228 | assert.equal(sentinel3, value) 229 | semiDone() 230 | end) 231 | end) 232 | end) 233 | 234 | describe('`onRejected` handlers are called in the original order', function() 235 | local queue = {} 236 | local function enQueue(value) 237 | table.insert(queue, value) 238 | end 239 | 240 | before_each(function() 241 | queue = {} 242 | end) 243 | 244 | testRejected(it, assert, dummy, function(p) 245 | local function onRejected1() 246 | enQueue(1) 247 | end 248 | 249 | local function onRejected2() 250 | enQueue(2) 251 | end 252 | 253 | local function onRejected3() 254 | enQueue(3) 255 | end 256 | 257 | p:thenCall(nil, onRejected1) 258 | p:thenCall(nil, onRejected2) 259 | p:thenCall(nil, onRejected3) 260 | p:thenCall(nil, function() 261 | assert.same({1, 2, 3}, queue) 262 | done() 263 | end) 264 | end) 265 | 266 | describe('even when one handler is added inside another handler', function() 267 | testRejected(it, assert, dummy, function(p) 268 | local function onRejected1() 269 | enQueue(1) 270 | end 271 | 272 | local function onRejected2() 273 | enQueue(2) 274 | end 275 | 276 | local function onRejected3() 277 | enQueue(3) 278 | end 279 | 280 | p:thenCall(nil, function() 281 | onRejected1() 282 | p:thenCall(nil, onRejected3) 283 | end) 284 | p:thenCall(nil, onRejected2) 285 | p:thenCall(nil, function() 286 | setTimeout(function() 287 | assert.same({1, 2, 3}, queue) 288 | done() 289 | end, 15) 290 | end) 291 | end) 292 | end) 293 | end) 294 | end) 295 | end) 296 | -------------------------------------------------------------------------------- /spec/promiseA+/2.2.7_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local testFulfilled = helpers.testFulfilled 3 | local testRejected = helpers.testRejected 4 | local deferredPromise = helpers.deferredPromise 5 | local dummy = {dummy = 'dummy'} 6 | local sentinel = {sentinel = 'sentinel'} 7 | local other = {other = 'other'} 8 | local reasons = require('spec.helpers.reasons') 9 | 10 | describe('2.2.7: `thenCall` must return a promise: ' .. 11 | '`promise2 = promise1.thenCall(onFulfilled, onRejected)', function() 12 | it('is a promise', function() 13 | local p1 = deferredPromise() 14 | local p2 = p1:thenCall() 15 | 16 | assert.True(type(p2) == 'table' or type(p2) == 'function') 17 | assert.is.not_equal(p2, nil) 18 | assert.equal('function', type(p2.thenCall)) 19 | end) 20 | 21 | describe('2.2.7.1: If either `onFulfilled` or `onRejected` returns a value `x`, ' .. 22 | 'run the Promise Resolution Procedure `[[Resolve]](promise2, x)`', function() 23 | it('see separate 3.3 tests', function() 24 | end) 25 | end) 26 | 27 | describe('2.2.7.2: If either `onFulfilled` or `onRejected` throws an exception `e`, ' .. 28 | '`promise2` must be rejected with `e` as the reason.', function() 29 | local function testReason(expectedReason, stringRepresentation) 30 | describe('The reason is ' .. stringRepresentation, function() 31 | testFulfilled(it, assert, dummy, function(p1) 32 | local p2 = p1:thenCall(function() 33 | error(expectedReason) 34 | end) 35 | p2:thenCall(nil, function(actualReason) 36 | assert.equal(expectedReason, actualReason) 37 | done() 38 | end) 39 | end) 40 | testRejected(it, assert, dummy, function(p1) 41 | local p2 = p1:thenCall(nil, function() 42 | error(expectedReason) 43 | end) 44 | p2:thenCall(nil, function(actualReason) 45 | assert.equal(expectedReason, actualReason) 46 | done() 47 | end) 48 | end) 49 | end) 50 | end 51 | 52 | for reasonStr, reason in pairs(reasons) do 53 | testReason(reason(), reasonStr) 54 | end 55 | end) 56 | 57 | describe('2.2.7.3: If `onFulfilled` is not a function and `promise1` is fulfilled, ' .. 58 | '`promise2` must be fulfilled with the same value', function() 59 | local function testNonFunction(nonFunction, stringRepresentation) 60 | describe('`onFulfilled` is' .. stringRepresentation, function() 61 | testFulfilled(it, assert, sentinel, function(p1) 62 | local p2 = p1:thenCall(nonFunction) 63 | p2:thenCall(function(value) 64 | assert.equal(sentinel, value) 65 | done() 66 | end) 67 | end) 68 | end) 69 | end 70 | 71 | testNonFunction(nil, '`nil`') 72 | testNonFunction(false, '`false`') 73 | testNonFunction(5, '`5`') 74 | testNonFunction(setmetatable({}, {}), 'a metatable') 75 | testNonFunction({function() return other end}, 'an table containing a function') 76 | end) 77 | 78 | describe('2.2.7.4: If `onRejected` is not a function and `promise1` is rejected, ' .. 79 | '`promise2` must be rejected with the same reason', function() 80 | local function testNonFunction(nonFunction, stringRepresentation) 81 | describe('`onRejected` is' .. stringRepresentation, function() 82 | testRejected(it, assert, sentinel, function(p1) 83 | local p2 = p1:thenCall(nonFunction) 84 | p2:thenCall(nil, function(reason) 85 | assert.equal(sentinel, reason) 86 | done() 87 | end) 88 | end) 89 | end) 90 | end 91 | 92 | testNonFunction(nil, '`nil`') 93 | testNonFunction(false, '`false`') 94 | testNonFunction(5, '`5`') 95 | testNonFunction(setmetatable({}, {}), 'a metatable') 96 | testNonFunction({function() return other end}, 'an table containing a function') 97 | end) 98 | end) 99 | -------------------------------------------------------------------------------- /spec/promiseA+/2.3.1_spec.lua: -------------------------------------------------------------------------------- 1 | local promise = require('promise') 2 | local dummy = {dummy = 'dummy'} 3 | 4 | describe('2.3.1: If `promise` and `x` refer to the same object, reject `promise` with a ' .. 5 | '`TypeError` as the reason.', function() 6 | it('via return from a fulfilled promise', function() 7 | local p 8 | p = promise.resolve(dummy):thenCall(function() 9 | return p 10 | end) 11 | 12 | p:thenCall(nil, function(reason) 13 | assert.truthy(reason:match('^TypeError')) 14 | done() 15 | end) 16 | assert.True(wait()) 17 | end) 18 | 19 | it('via return from a rejected promise', function() 20 | local p 21 | p = promise.reject(dummy):thenCall(nil, function() 22 | return p 23 | end) 24 | 25 | p:thenCall(nil, function(reason) 26 | assert.truthy(reason:match('^TypeError')) 27 | done() 28 | end) 29 | assert.True(wait()) 30 | end) 31 | end) 32 | -------------------------------------------------------------------------------- /spec/promiseA+/2.3.2_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local setTimeout = helpers.setTimeout 3 | local deferredPromise = helpers.deferredPromise 4 | local promise = require('promise') 5 | local dummy = {dummy = 'dummy'} 6 | local sentinel = {sentinel = 'sentinel'} 7 | 8 | local function testPromiseResolution(xFactory, test) 9 | it('via return from a fulfilled promise', function() 10 | local p = promise.resolve(dummy):thenCall(function() 11 | return xFactory() 12 | end) 13 | test(p) 14 | assert.True(wait()) 15 | end) 16 | 17 | it('via return from a rejected promise', function() 18 | local p = promise.reject(dummy):thenCall(nil, function() 19 | return xFactory() 20 | end) 21 | test(p) 22 | assert.True(wait()) 23 | end) 24 | end 25 | 26 | describe('2.3.2: If `x` is a promise, adopt its state', function() 27 | describe('2.3.2.1: If `x` is pending, `promise` must remain pending until `x` is ' .. 28 | 'fulfilled or rejected.', function() 29 | testPromiseResolution(function() 30 | return deferredPromise() 31 | end, function(p) 32 | local onFulfilled = spy.new(function() end) 33 | local onRejected = spy.new(function() end) 34 | p:thenCall(onFulfilled, onRejected) 35 | 36 | assert.spy(onFulfilled).was_not_called() 37 | assert.spy(onRejected).was_not_called() 38 | done() 39 | end) 40 | end) 41 | 42 | describe('2.3.2.2: If/when `x` is fulfilled, fulfill `promise` with the same value.', function() 43 | describe('`x` is already-fulfilled', function() 44 | testPromiseResolution(function() 45 | return promise.resolve(sentinel) 46 | end, function(p) 47 | p:thenCall(function(value) 48 | assert.equal(sentinel, value) 49 | done() 50 | end) 51 | end) 52 | end) 53 | 54 | describe('`x` is eventually-fulfilled', function() 55 | testPromiseResolution(function() 56 | local p, resolve = deferredPromise() 57 | setTimeout(function() 58 | resolve(sentinel) 59 | end, 10) 60 | return p 61 | end, function(p) 62 | p:thenCall(function(value) 63 | assert.equal(sentinel, value) 64 | done() 65 | end) 66 | end) 67 | end) 68 | end) 69 | 70 | describe('2.3.2.3: If/when `x` is rejected, reject `promise` with the same reason.', function() 71 | describe('`x` is already-rejected', function() 72 | testPromiseResolution(function() 73 | return promise.reject(sentinel) 74 | end, function(p) 75 | p:thenCall(nil, function(reason) 76 | assert.equal(sentinel, reason) 77 | done() 78 | end) 79 | end) 80 | end) 81 | 82 | describe('`x` is eventually-rejected', function() 83 | testPromiseResolution(function() 84 | local p, _, reject = deferredPromise() 85 | setTimeout(function() 86 | reject(sentinel) 87 | end, 10) 88 | return p 89 | end, function(p) 90 | p:thenCall(nil, function(reason) 91 | assert.equal(sentinel, reason) 92 | done() 93 | end) 94 | end) 95 | end) 96 | end) 97 | end) 98 | -------------------------------------------------------------------------------- /spec/promiseA+/2.3.3_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local deferredPromise = helpers.deferredPromise 3 | local promise = require('promise') 4 | local setTimeout = helpers.setTimeout 5 | local reasons = require('spec.helpers.reasons') 6 | local dummy = {dummy = 'dummy'} 7 | local sentinel = {sentinel = 'sentinel'} 8 | local other = {other = 'other'} 9 | local thenables = require('spec.helpers.thenables') 10 | 11 | local function testPromiseResolution(xFactory, test) 12 | it('via return from a fulfilled promise', function() 13 | local p = promise.resolve(dummy):thenCall(function() 14 | return xFactory() 15 | end) 16 | test(p) 17 | assert.True(wait()) 18 | end) 19 | 20 | it('via return from a rejected promise', function() 21 | local p = promise.reject(dummy):thenCall(nil, function() 22 | return xFactory() 23 | end) 24 | test(p) 25 | assert.True(wait()) 26 | end) 27 | end 28 | 29 | describe('2.3.3: Otherwise, if `x` is a table or function,', function() 30 | describe('2.3.3.1: Let `thenCall` be `x.thenCall`', function() 31 | describe('`x` is a table', function() 32 | local thenCallRetrieved = spy.new(function() end) 33 | 34 | before_each(function() 35 | thenCallRetrieved:clear() 36 | end) 37 | 38 | testPromiseResolution(function() 39 | local x = {} 40 | setmetatable(x, { 41 | __index = function(_, k) 42 | if k == 'thenCall' then 43 | thenCallRetrieved() 44 | return function(_, resolvePromise) 45 | resolvePromise() 46 | end 47 | end 48 | end 49 | }) 50 | return x 51 | end, function(p) 52 | p:thenCall(function() 53 | assert.spy(thenCallRetrieved).was_called(1) 54 | done() 55 | end) 56 | end) 57 | end) 58 | 59 | describe('2.3.3.2: If retrieving the property `x.thenCall` results in a thrown exception ' .. 60 | '`e`, reject `promise` with `e` as the reason.', function() 61 | local function testRejectionViaThrowingGetter(e, stringRepresentation) 62 | describe('`e` is ' .. stringRepresentation, function() 63 | testPromiseResolution(function() 64 | return { 65 | thenCall = function() 66 | error(e) 67 | end 68 | } 69 | end, function(p) 70 | p:thenCall(nil, function(reason) 71 | assert.equal(e, reason) 72 | done() 73 | end) 74 | end) 75 | end) 76 | end 77 | 78 | for reasonStr, reason in pairs(reasons) do 79 | testRejectionViaThrowingGetter(reason(), reasonStr) 80 | end 81 | end) 82 | 83 | describe('2.3.3.3: If `thenCall` is a function, call it with `x` as `self`, first ' .. 84 | 'argument `resolvePromise`, and second argument `rejectPromise`', function() 85 | testPromiseResolution(function() 86 | local x 87 | x = { 88 | thenCall = function(self, resolvePromise, rejectPromise) 89 | assert.equal(x, self) 90 | assert.True(type(resolvePromise) == 'function') 91 | assert.True(type(rejectPromise) == 'function') 92 | resolvePromise() 93 | end 94 | } 95 | return x 96 | end, function(p) 97 | p:thenCall(function() 98 | done() 99 | end) 100 | end) 101 | end) 102 | 103 | describe('2.3.3.3.1: If/when `resolvePromise` is called with value `y`, ' .. 104 | 'run `[[Resolve]](promise, y)`', function() 105 | local function testCallingResolvePromise(yFactory, stringRepresentation, test) 106 | describe('`y` is ' .. stringRepresentation, function() 107 | describe('`thenCall` calls `resolvePromise` synchronously', function() 108 | testPromiseResolution(function() 109 | return { 110 | thenCall = function(self, resolvePromise) 111 | local _ = self 112 | resolvePromise(yFactory()) 113 | end 114 | } 115 | end, test) 116 | end) 117 | 118 | describe('`thenCall` calls `resolvePromise` asynchronously', function() 119 | testPromiseResolution(function() 120 | return { 121 | thenCall = function(self, resolvePromise) 122 | local _ = self 123 | setTimeout(function() 124 | resolvePromise(yFactory()) 125 | end, 0) 126 | end 127 | } 128 | end, test) 129 | end) 130 | end) 131 | end 132 | 133 | local function testCallingResolvePromiseFulfillsWith(yFactory, stringRepresentation, 134 | fulfillmentValue) 135 | testCallingResolvePromise(yFactory, stringRepresentation, function(p) 136 | p:thenCall(function(value) 137 | assert.equal(fulfillmentValue, value) 138 | done() 139 | end) 140 | end) 141 | end 142 | 143 | local function testCallingResolvePromiseRejectsWith(yFactory, stringRepresentation, 144 | rejectionReason) 145 | testCallingResolvePromise(yFactory, stringRepresentation, function(p) 146 | p:thenCall(nil, function(reason) 147 | assert.equal(rejectionReason, reason) 148 | done() 149 | end) 150 | end) 151 | end 152 | 153 | describe('`y` is not a thenable', function() 154 | testCallingResolvePromiseFulfillsWith(function() 155 | return nil 156 | end, '`null`', nil) 157 | testCallingResolvePromiseFulfillsWith(function() 158 | return false 159 | end, '`false`', false) 160 | testCallingResolvePromiseFulfillsWith(function() 161 | return 5 162 | end, '`5`', 5) 163 | testCallingResolvePromiseFulfillsWith(function() 164 | return sentinel 165 | end, '`an table`', sentinel) 166 | end) 167 | 168 | describe('`y` is a thenable', function() 169 | for stringRepresentation, factory in pairs(thenables.fulfilled) do 170 | testCallingResolvePromiseFulfillsWith(function() 171 | return factory(sentinel) 172 | end, stringRepresentation, sentinel) 173 | end 174 | for stringRepresentation, factory in pairs(thenables.rejected) do 175 | testCallingResolvePromiseRejectsWith(function() 176 | return factory(sentinel) 177 | end, stringRepresentation, sentinel) 178 | end 179 | end) 180 | 181 | describe('`y` is a thenable for a thenable', function() 182 | for outerString, outerFactory in pairs(thenables.fulfilled) do 183 | for innerString, factory in pairs(thenables.fulfilled) do 184 | local stringRepresentation = outerString .. ' for ' .. innerString 185 | testCallingResolvePromiseFulfillsWith(function() 186 | return outerFactory(factory(sentinel)) 187 | end, stringRepresentation, sentinel) 188 | end 189 | for innerString, factory in pairs(thenables.rejected) do 190 | local stringRepresentation = outerString .. ' for ' .. innerString 191 | testCallingResolvePromiseRejectsWith(function() 192 | return outerFactory(factory(sentinel)) 193 | end, stringRepresentation, sentinel) 194 | end 195 | end 196 | end) 197 | end) 198 | end) 199 | 200 | describe('2.3.3.3.2: If/when `rejectPromise` is called with reason `r`, reject `promise` with `r`', function() 201 | local function testCallingRejectPromise(r, stringRepresentation, test) 202 | describe('`r` is ' .. stringRepresentation, function() 203 | describe('`thenCall` calls `rejectPromise` synchronously', function() 204 | testPromiseResolution(function() 205 | return { 206 | thenCall = function(self, resolvePromise, rejectPromise) 207 | local _, _ = self, resolvePromise 208 | rejectPromise(r) 209 | end 210 | } 211 | end, test) 212 | end) 213 | 214 | describe('`thenCall` calls `rejectPromise` asynchronously', function() 215 | testPromiseResolution(function() 216 | return { 217 | thenCall = function(self, resolvePromise, rejectPromise) 218 | local _, _ = self, resolvePromise 219 | setTimeout(function() 220 | rejectPromise(r) 221 | end, 0) 222 | end 223 | } 224 | end, test) 225 | end) 226 | end) 227 | end 228 | 229 | local function testCallingRejectPromiseRejectsWith(rejectionReason, stringRepresentation) 230 | testCallingRejectPromise(rejectionReason, stringRepresentation, function(p) 231 | p:thenCall(nil, function(reason) 232 | assert.equal(rejectionReason, reason) 233 | done() 234 | end) 235 | end) 236 | end 237 | 238 | for reasonStr, reason in pairs(reasons) do 239 | testCallingRejectPromiseRejectsWith(reason(), reasonStr) 240 | end 241 | end) 242 | 243 | describe('2.3.3.3.3: If both `resolvePromise` and `rejectPromise` are called, or multiple ' .. 244 | 'calls to the same argument are made, the first call takes precedence, and any further ' .. 245 | 'calls are ignored.', function() 246 | describe('calling `resolvePromise` then `rejectPromise`, both synchronously', function() 247 | testPromiseResolution(function() 248 | return { 249 | thenCall = function(self, resolvePromise, rejectPromise) 250 | local _ = self 251 | resolvePromise(sentinel) 252 | rejectPromise(other) 253 | end 254 | } 255 | end, function(p) 256 | p:thenCall(function(value) 257 | assert.equal(sentinel, value) 258 | done() 259 | end) 260 | end) 261 | end) 262 | 263 | describe('calling `resolvePromise` synchronously then `rejectPromise` asynchronously', function() 264 | testPromiseResolution(function() 265 | return { 266 | thenCall = function(self, resolvePromise, rejectPromise) 267 | local _ = self 268 | resolvePromise(sentinel) 269 | setTimeout(function() 270 | rejectPromise(other) 271 | end, 0) 272 | end 273 | } 274 | end, function(p) 275 | p:thenCall(function(value) 276 | assert.equal(sentinel, value) 277 | done() 278 | end) 279 | end) 280 | end) 281 | 282 | describe('calling `resolvePromise` then `rejectPromise`, both asynchronously', function() 283 | testPromiseResolution(function() 284 | return { 285 | thenCall = function(self, resolvePromise, rejectPromise) 286 | local _ = self 287 | setTimeout(function() 288 | resolvePromise(sentinel) 289 | end, 0) 290 | setTimeout(function() 291 | rejectPromise(other) 292 | end, 0) 293 | end 294 | } 295 | end, function(p) 296 | p:thenCall(function(value) 297 | assert.equal(sentinel, value) 298 | done() 299 | end) 300 | end) 301 | end) 302 | 303 | describe('calling `resolvePromise` with an asynchronously-fulfilled promise, then calling ' .. 304 | '`rejectPromise`, both synchronously', function() 305 | testPromiseResolution(function() 306 | local p, resolve = deferredPromise() 307 | setTimeout(function() 308 | resolve(sentinel) 309 | end, 10) 310 | return { 311 | thenCall = function(self, resolvePromise, rejectPromise) 312 | local _ = self 313 | resolvePromise(p) 314 | rejectPromise(other) 315 | end 316 | } 317 | end, function(p) 318 | p:thenCall(function(value) 319 | assert.equal(sentinel, value) 320 | done() 321 | end) 322 | end) 323 | end) 324 | 325 | describe('calling `resolvePromise` with an asynchronously-rejected promise, then calling ' .. 326 | '`rejectPromise`, both synchronously', function() 327 | testPromiseResolution(function() 328 | local p, _, reject = deferredPromise() 329 | setTimeout(function() 330 | reject(sentinel) 331 | end, 10) 332 | return { 333 | thenCall = function(self, resolvePromise, rejectPromise) 334 | local _ = self 335 | resolvePromise(p) 336 | rejectPromise(other) 337 | end 338 | } 339 | end, function(p) 340 | p:thenCall(nil, function(reason) 341 | assert.equal(sentinel, reason) 342 | done() 343 | end) 344 | end) 345 | end) 346 | 347 | describe('calling `rejectPromise` then `resolvePromise`, both synchronously', function() 348 | testPromiseResolution(function() 349 | local p, resolve = deferredPromise() 350 | setTimeout(function() 351 | resolve(sentinel) 352 | end, 10) 353 | return { 354 | thenCall = function(self, resolvePromise, rejectPromise) 355 | local _ = self 356 | resolvePromise(p) 357 | rejectPromise(other) 358 | end 359 | } 360 | end, function(p) 361 | p:thenCall(function(value) 362 | assert.equal(sentinel, value) 363 | done() 364 | end) 365 | end) 366 | end) 367 | 368 | describe('calling `rejectPromise` synchronously then `resolvePromise` asynchronously', function() 369 | testPromiseResolution(function() 370 | return { 371 | thenCall = function(self, resolvePromise, rejectPromise) 372 | local _, _ = self, resolvePromise 373 | rejectPromise(sentinel) 374 | setTimeout(function() 375 | resolvePromise(other) 376 | end, 0) 377 | end 378 | } 379 | end, function(p) 380 | p:thenCall(nil, function(reason) 381 | assert.equal(sentinel, reason) 382 | done() 383 | end) 384 | end) 385 | end) 386 | 387 | describe('calling `rejectPromise` then `resolvePromise`, both asynchronously', function() 388 | testPromiseResolution(function() 389 | return { 390 | thenCall = function(self, resolvePromise, rejectPromise) 391 | local _, _ = self, resolvePromise 392 | setTimeout(function() 393 | rejectPromise(sentinel) 394 | end, 0) 395 | setTimeout(function() 396 | resolvePromise(other) 397 | end, 0) 398 | end 399 | } 400 | end, function(p) 401 | p:thenCall(nil, function(reason) 402 | assert.equal(sentinel, reason) 403 | done() 404 | end) 405 | end) 406 | end) 407 | 408 | describe('calling `resolvePromise` twice synchronously', function() 409 | testPromiseResolution(function() 410 | return { 411 | thenCall = function(self, resolvePromise) 412 | local _ = self 413 | resolvePromise(sentinel) 414 | resolvePromise(other) 415 | end 416 | } 417 | end, function(p) 418 | p:thenCall(function(value) 419 | assert.equal(sentinel, value) 420 | done() 421 | end) 422 | end) 423 | end) 424 | 425 | describe('calling `resolvePromise` twice, first synchronously then asynchronously', function() 426 | testPromiseResolution(function() 427 | return { 428 | thenCall = function(self, resolvePromise) 429 | local _ = self 430 | resolvePromise(sentinel) 431 | setTimeout(function() 432 | resolvePromise(other) 433 | end, 0) 434 | end 435 | } 436 | end, function(p) 437 | p:thenCall(function(value) 438 | assert.equal(sentinel, value) 439 | done() 440 | end) 441 | end) 442 | end) 443 | 444 | describe('calling `resolvePromise` twice, both times asynchronously', function() 445 | testPromiseResolution(function() 446 | return { 447 | thenCall = function(self, resolvePromise) 448 | local _ = self 449 | setTimeout(function() 450 | resolvePromise(sentinel) 451 | end, 0) 452 | setTimeout(function() 453 | resolvePromise(other) 454 | end, 0) 455 | end 456 | } 457 | end, function(p) 458 | p:thenCall(function(value) 459 | assert.equal(sentinel, value) 460 | done() 461 | end) 462 | end) 463 | end) 464 | 465 | describe('calling `resolvePromise` with an asynchronously-fulfilled promise, ' .. 466 | 'then calling it again, both times synchronously', function() 467 | testPromiseResolution(function() 468 | local p, resolve = deferredPromise() 469 | setTimeout(function() 470 | resolve(sentinel) 471 | end, 10) 472 | return { 473 | thenCall = function(self, resolvePromise) 474 | local _ = self 475 | resolvePromise(p) 476 | resolvePromise(other) 477 | end 478 | } 479 | end, function(p) 480 | p:thenCall(function(value) 481 | assert.equal(sentinel, value) 482 | done() 483 | end) 484 | end) 485 | end) 486 | 487 | describe('calling `resolvePromise` with an asynchronously-rejected promise, ' .. 488 | 'then calling it again, both times synchronously', function() 489 | testPromiseResolution(function() 490 | local p, _, reject = deferredPromise() 491 | setTimeout(function() 492 | reject(sentinel) 493 | end, 10) 494 | return { 495 | thenCall = function(self, resolvePromise) 496 | local _ = self 497 | resolvePromise(p) 498 | resolvePromise(other) 499 | end 500 | } 501 | end, function(p) 502 | p:thenCall(nil, function(reason) 503 | assert.equal(sentinel, reason) 504 | done() 505 | end) 506 | end) 507 | end) 508 | 509 | describe('calling `rejectPromise` twice synchronously', function() 510 | testPromiseResolution(function() 511 | return { 512 | thenCall = function(self, resolvePromise, rejectPromise) 513 | local _, _ = self, resolvePromise 514 | rejectPromise(sentinel) 515 | rejectPromise(other) 516 | end 517 | } 518 | end, function(p) 519 | p:thenCall(nil, function(reason) 520 | assert.equal(sentinel, reason) 521 | done() 522 | end) 523 | end) 524 | end) 525 | 526 | describe('calling `resolvePromise` twice, first synchronously then asynchronously', function() 527 | testPromiseResolution(function() 528 | return { 529 | thenCall = function(self, resolvePromise, rejectPromise) 530 | local _, _ = self, resolvePromise 531 | rejectPromise(sentinel) 532 | setTimeout(function() 533 | rejectPromise(other) 534 | end, 0) 535 | end 536 | } 537 | end, function(p) 538 | p:thenCall(nil, function(reason) 539 | assert.equal(sentinel, reason) 540 | done() 541 | end) 542 | end) 543 | end) 544 | 545 | describe('calling `rejectPromise` twice, both times asynchronously', function() 546 | testPromiseResolution(function() 547 | return { 548 | thenCall = function(self, resolvePromise, rejectPromise) 549 | local _, _ = self, resolvePromise 550 | setTimeout(function() 551 | rejectPromise(sentinel) 552 | end, 0) 553 | setTimeout(function() 554 | rejectPromise(other) 555 | end, 0) 556 | end 557 | } 558 | end, function(p) 559 | p:thenCall(nil, function(reason) 560 | assert.equal(sentinel, reason) 561 | done() 562 | end) 563 | end) 564 | end) 565 | 566 | describe('saving and abusing `resolvePromise` and `rejectPromise`', function() 567 | local savedResolvePromise, savedRejectPromise 568 | 569 | before_each(function() 570 | savedResolvePromise, savedRejectPromise = nil, nil 571 | end) 572 | 573 | testPromiseResolution(function() 574 | return { 575 | thenCall = function(self, resolvePromise, rejectPromise) 576 | local _ = self 577 | savedResolvePromise, savedRejectPromise = resolvePromise, rejectPromise 578 | end 579 | } 580 | end, function(p) 581 | local onFulfilled, onRejected = spy.new(function() end), spy.new(function() end) 582 | p:thenCall(onFulfilled, onRejected) 583 | if savedResolvePromise and savedRejectPromise then 584 | savedResolvePromise(dummy) 585 | savedResolvePromise(dummy) 586 | savedRejectPromise(dummy) 587 | savedRejectPromise(dummy) 588 | end 589 | 590 | setTimeout(function() 591 | savedResolvePromise(dummy) 592 | savedResolvePromise(dummy) 593 | savedRejectPromise(dummy) 594 | savedRejectPromise(dummy) 595 | end, 10) 596 | 597 | setTimeout(function() 598 | assert.spy(onFulfilled).was_called(1) 599 | assert.spy(onRejected).was_not_called() 600 | done() 601 | end, 50) 602 | end) 603 | end) 604 | 605 | describe('2.3.3.3.4: If calling `thenCall` throws an exception `e`,', function() 606 | describe('2.3.3.3.4.1: If `resolvePromise` or `rejectPromise` have been called, ignore it.', function() 607 | describe('`resolvePromise` was called with a non-thenable', function() 608 | testPromiseResolution(function() 609 | return { 610 | thenCall = function(self, resolvePromise) 611 | local _ = self 612 | resolvePromise(sentinel) 613 | error(other) 614 | end 615 | } 616 | end, function(p) 617 | p:thenCall(function(value) 618 | assert.equal(sentinel, value) 619 | done() 620 | end) 621 | end) 622 | end) 623 | 624 | describe('`resolvePromise` was called with an asynchronously-fulfilled promise', function() 625 | testPromiseResolution(function() 626 | local p, resolve = deferredPromise() 627 | setTimeout(function() 628 | resolve(sentinel) 629 | end, 10) 630 | return { 631 | thenCall = function(self, resolvePromise) 632 | local _ = self 633 | resolvePromise(p) 634 | error(other) 635 | end 636 | } 637 | end, function(p) 638 | p:thenCall(function(value) 639 | assert.equal(sentinel, value) 640 | done() 641 | end) 642 | end) 643 | end) 644 | 645 | describe('`resolvePromise` was called with an asynchronously-rejected promise', function() 646 | testPromiseResolution(function() 647 | local p, _, reject = deferredPromise() 648 | setTimeout(function() 649 | reject(sentinel) 650 | end, 10) 651 | return { 652 | thenCall = function(self, resolvePromise) 653 | local _ = self 654 | resolvePromise(p) 655 | error(other) 656 | end 657 | } 658 | end, function(p) 659 | p:thenCall(nil, function(reason) 660 | assert.equal(sentinel, reason) 661 | done() 662 | end) 663 | end) 664 | end) 665 | 666 | describe('`rejectPromise` was called', function() 667 | testPromiseResolution(function() 668 | return { 669 | thenCall = function(self, resolvePromise, rejectPromise) 670 | local _, _ = self, resolvePromise 671 | rejectPromise(sentinel) 672 | error(other) 673 | end 674 | } 675 | end, function(p) 676 | p:thenCall(nil, function(reason) 677 | assert.equal(sentinel, reason) 678 | done() 679 | end) 680 | end) 681 | end) 682 | 683 | describe('`resolvePromise` then `rejectPromise` were called', function() 684 | testPromiseResolution(function() 685 | return { 686 | thenCall = function(self, resolvePromise, rejectPromise) 687 | local _ = self 688 | resolvePromise(sentinel) 689 | rejectPromise(other) 690 | end 691 | } 692 | end, function(p) 693 | p:thenCall(function(value) 694 | assert.equal(sentinel, value) 695 | done() 696 | end) 697 | end) 698 | end) 699 | 700 | describe('`rejectPromise` then `resolvePromise` were called', function() 701 | testPromiseResolution(function() 702 | return { 703 | thenCall = function(self, resolvePromise, rejectPromise) 704 | local _ = self 705 | rejectPromise(sentinel) 706 | resolvePromise(other) 707 | end 708 | } 709 | end, function(p) 710 | p:thenCall(nil, function(reason) 711 | assert.equal(sentinel, reason) 712 | done() 713 | end) 714 | end) 715 | end) 716 | end) 717 | 718 | describe('2.3.3.3.4.2: Otherwise, reject `promise` with `e` as the reason.', function() 719 | describe('straightforward case', function() 720 | testPromiseResolution(function() 721 | return { 722 | thenCall = function() 723 | error(sentinel) 724 | end 725 | } 726 | end, function(p) 727 | p:thenCall(nil, function(reason) 728 | assert.equal(sentinel, reason) 729 | done() 730 | end) 731 | end) 732 | end) 733 | end) 734 | 735 | describe('`resolvePromise` is called asynchronously before the `throw`', function() 736 | testPromiseResolution(function() 737 | return { 738 | thenCall = function(self, resolvePromise) 739 | local _ = self 740 | setTimeout(function() 741 | resolvePromise(other) 742 | end, 0) 743 | error(sentinel) 744 | end 745 | } 746 | end, function(p) 747 | p:thenCall(nil, function(reason) 748 | assert.equal(sentinel, reason) 749 | done() 750 | end) 751 | end) 752 | end) 753 | 754 | describe('`rejectPromise` is called asynchronously before the `throw`', function() 755 | testPromiseResolution(function() 756 | return { 757 | thenCall = function(self, resolvePromise, rejectPromise) 758 | local _, _ = self, resolvePromise 759 | setTimeout(function() 760 | rejectPromise(other) 761 | end, 0) 762 | error(sentinel) 763 | end 764 | } 765 | end, function(p) 766 | p:thenCall(nil, function(reason) 767 | assert.equal(sentinel, reason) 768 | done() 769 | end) 770 | end) 771 | end) 772 | end) 773 | end) 774 | 775 | describe('2.3.3.4: If `thenCall` is not a function, fulfill promise with `x`', function() 776 | local function testFulfillViaNonFunction(thenCall, stringRepresentation) 777 | local x = nil 778 | 779 | before_each(function() 780 | x = {thenCall = thenCall} 781 | end) 782 | 783 | describe('thenCall is ' .. stringRepresentation, function() 784 | testPromiseResolution(function() 785 | return x 786 | end, function(p) 787 | p:thenCall(function(value) 788 | assert.equal(x, value) 789 | done() 790 | end) 791 | end) 792 | end) 793 | end 794 | 795 | testFulfillViaNonFunction(5, '`5`') 796 | testFulfillViaNonFunction({}, 'a table') 797 | testFulfillViaNonFunction({function() end}, 'a table containing a function') 798 | testFulfillViaNonFunction(setmetatable({}, {}), 'a metatable') 799 | end) 800 | end) 801 | -------------------------------------------------------------------------------- /spec/promiseA+/2.3.4_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('spec.helpers.init') 2 | local testFulfilled = helpers.testFulfilled 3 | local testRejected = helpers.testRejected 4 | local dummy = {dummy = 'dummy'} 5 | 6 | describe('2.3.4: If `x` is not an object or function, fulfill `promise` with `x`', function() 7 | local function testValue(expectedValue, stringRepresentation) 8 | describe('The value is ' .. stringRepresentation, function() 9 | testFulfilled(it, assert, dummy, function(p1) 10 | local p2 = p1:thenCall(function() 11 | return expectedValue 12 | end) 13 | p2:thenCall(function(actualValue) 14 | assert.equal(expectedValue, actualValue) 15 | done() 16 | end) 17 | end) 18 | 19 | testRejected(it, assert, dummy, function(p1) 20 | local p2 = p1:thenCall(nil, function() 21 | return expectedValue 22 | end) 23 | p2:thenCall(function(actualValue) 24 | assert.equal(expectedValue, actualValue) 25 | done() 26 | end) 27 | end) 28 | end) 29 | end 30 | 31 | testValue(nil, '`nil`') 32 | testValue(false, '`false`') 33 | testValue(true, '`true`') 34 | testValue(0, '`0`') 35 | end) 36 | -------------------------------------------------------------------------------- /spec/promise_spec.lua: -------------------------------------------------------------------------------- 1 | local promise = require('promise') 2 | local helpers = require('spec.helpers.init') 3 | local basics = require('spec.helpers.basics') 4 | local reasons = require('spec.helpers.reasons') 5 | local setTimeout = helpers.setTimeout 6 | local dummy = {dummy = 'dummy'} 7 | local sentinel = {sentinel = 'sentinel'} 8 | local sentinel2 = {sentinel = 'sentinel2'} 9 | local sentinel3 = {sentinel = 'sentinel3'} 10 | local other = {other = 'other'} 11 | 12 | describe('Extend Promise A+.', function() 13 | describe('Promise.resolve', function() 14 | describe('Resolving basic values.', function() 15 | local function testBasicResolve(expectedValue, stringRepresentation) 16 | it('The value is ' .. stringRepresentation .. 17 | ', and the state of Promise become fulfilled at once.', function() 18 | local p = promise.resolve(expectedValue) 19 | assert.truthy(tostring(p):match('')) 20 | p:thenCall(function(value) 21 | assert.equal(expectedValue, value) 22 | done() 23 | end) 24 | assert.True(wait()) 25 | end) 26 | end 27 | 28 | for valueStr, basicFn in pairs(basics) do 29 | testBasicResolve(basicFn(), valueStr) 30 | end 31 | end) 32 | 33 | it('resolve another resolved Promise', function() 34 | local p1 = promise.resolve(dummy) 35 | local p2 = promise.resolve(p1) 36 | p2:thenCall(function(value) 37 | assert.equal(dummy, value) 38 | done() 39 | end) 40 | assert.True(wait()) 41 | assert.equal(p1, p2) 42 | end) 43 | 44 | it('resolve another rejected Promise', function() 45 | local p1 = promise.reject(dummy) 46 | local p2 = promise.resolve(p1) 47 | p2:thenCall(nil, function(reason) 48 | assert.equal(dummy, reason) 49 | done() 50 | end) 51 | assert.True(wait()) 52 | assert.equal(p1, p2) 53 | end) 54 | 55 | it('resolve thenables and throwing Errors', function() 56 | local p1 = promise.resolve({ 57 | thenCall = function(self, resolvePromise) 58 | local _ = self 59 | resolvePromise(dummy) 60 | end 61 | }) 62 | assert.True(promise.isInstance(p1)) 63 | 64 | local onFulfilled1 = spy.new(function(value) 65 | assert.equal(dummy, value) 66 | end) 67 | p1:thenCall(onFulfilled1) 68 | 69 | local thenable = { 70 | thenCall = function(self, resolvePromise) 71 | local _ = self 72 | error(dummy) 73 | resolvePromise(other) 74 | end 75 | } 76 | local onRejected = spy.new(function(reason) 77 | assert.equal(dummy, reason) 78 | end) 79 | local p2 = promise.resolve(thenable) 80 | p2:thenCall(nil, onRejected) 81 | 82 | thenable = { 83 | thenCall = function(self, resolvePromise) 84 | local _ = self 85 | resolvePromise(dummy) 86 | error(other) 87 | end 88 | } 89 | local onFulfilled2 = spy.new(function(value) 90 | assert.equal(dummy, value) 91 | end) 92 | local p3 = promise.resolve(thenable) 93 | p3:thenCall(onFulfilled2) 94 | 95 | assert.False(wait(30)) 96 | assert.spy(onFulfilled1).was_called() 97 | assert.spy(onRejected).was_called() 98 | assert.spy(onFulfilled2).was_called() 99 | end) 100 | end) 101 | 102 | describe('Promise.rejected.', function() 103 | describe('Rejecting reasons', function() 104 | local function testBasicReject(expectedReason, stringRepresentation) 105 | it('The reason is ' .. stringRepresentation .. 106 | ', and the state of Promise become rejected at once.', function() 107 | local p = promise.reject(expectedReason) 108 | assert.truthy(tostring(p):match('')) 109 | p:thenCall(nil, function(value) 110 | assert.equal(expectedReason, value) 111 | done() 112 | end) 113 | assert.True(wait()) 114 | end) 115 | end 116 | 117 | for reasonStr, reason in pairs(reasons) do 118 | testBasicReject(reason(), reasonStr) 119 | end 120 | end) 121 | end) 122 | 123 | describe('Promise.catch method.', function() 124 | it('throw errors', function() 125 | local onRejected1 = spy.new(function(reason) 126 | assert.equal(dummy, reason) 127 | end) 128 | promise(function() 129 | error(dummy) 130 | end):catch(onRejected1) 131 | 132 | local onRejected2 = spy.new(function() end) 133 | promise(function(resolve) 134 | resolve() 135 | error(dummy) 136 | end):catch(onRejected2) 137 | 138 | assert.False(wait(30)) 139 | assert.spy(onRejected1).was_called() 140 | assert.spy(onRejected2).was_not_called() 141 | end) 142 | 143 | it('is resolved', function() 144 | local onRejected1 = spy.new(function() end) 145 | local onFulfilled = spy.new(function() end) 146 | local onRejected2 = spy.new(function() end) 147 | promise.resolve(dummy) 148 | :catch(onRejected1) 149 | :thenCall(onFulfilled) 150 | :catch(onRejected2) 151 | 152 | assert.False(wait(30)) 153 | assert.spy(onRejected1).was_not_called() 154 | assert.spy(onFulfilled).was_called() 155 | assert.spy(onRejected2).was_not_called() 156 | end) 157 | end) 158 | 159 | describe('Promise.finally method.', function() 160 | it('is pending', function() 161 | local onFinally = spy.new(done) 162 | promise(function() end):finally(onFinally) 163 | assert.False(wait(30)) 164 | assert.spy(onFinally).was_not_called() 165 | end) 166 | 167 | it('is fulfilled, next Promise is fulfilled with previous value', function() 168 | local onFinally = spy.new(function() end) 169 | local onFulfilled = spy.new(function(value) 170 | assert.equal(dummy, value) 171 | done() 172 | end) 173 | promise.resolve(dummy):finally(onFinally):thenCall(onFulfilled) 174 | assert.True(wait()) 175 | assert.spy(onFinally).was_called() 176 | end) 177 | 178 | it('is rejected, next Promise is rejected with previous reason', function() 179 | local onFinally = spy.new(function() end) 180 | local onRejected = spy.new(function(reason) 181 | assert.equal(dummy, reason) 182 | done() 183 | end) 184 | promise.reject(dummy):finally(onFinally):thenCall(nil, onRejected) 185 | assert.True(wait()) 186 | assert.spy(onFinally).was_called() 187 | end) 188 | 189 | it('throw error on finally', function() 190 | local onRejected = spy.new(function(reason) 191 | assert.equal(dummy, reason) 192 | done() 193 | end) 194 | promise.resolve():finally(function() 195 | error(dummy) 196 | end):thenCall(nil, onRejected) 197 | assert.True(wait()) 198 | assert.spy(onRejected).was_called() 199 | end) 200 | end) 201 | 202 | describe('Promise.all method.', function() 203 | it('should be fulfilled immediately if element is empty', function() 204 | promise.all({}):thenCall(function(value) 205 | assert.same({}, value) 206 | done() 207 | end) 208 | assert.True(wait()) 209 | end) 210 | 211 | describe('wait for fulfillments,', function() 212 | it('use index table as elements', function() 213 | local p1 = promise.resolve(sentinel) 214 | local p2 = sentinel2 215 | local p3 = promise(function(resolve) 216 | setTimeout(function() 217 | resolve(sentinel3) 218 | end, 10) 219 | end) 220 | 221 | promise.all({p1, p2, p3}):thenCall(function(value) 222 | assert.same({sentinel, sentinel2, sentinel3}, value) 223 | done() 224 | end) 225 | assert.True(wait()) 226 | end) 227 | 228 | it('use key-value table as elements, different from JavaScript', function() 229 | local p1 = promise.resolve(sentinel) 230 | local p2 = sentinel2 231 | local p3 = promise(function(resolve) 232 | setTimeout(function() 233 | resolve(sentinel3) 234 | end, 10) 235 | end) 236 | 237 | promise.all({p1 = p1, p2 = p2, p3 = p3}):thenCall(function(value) 238 | assert.same({p1 = sentinel, p2 = sentinel2, p3 = sentinel3}, value) 239 | done() 240 | end) 241 | assert.True(wait()) 242 | end) 243 | end) 244 | 245 | it('is rejected if any of the elements are rejected', function() 246 | local p1 = promise.resolve(sentinel) 247 | local p2 = sentinel2 248 | local p3 = promise(function(_, reject) 249 | setTimeout(function() 250 | reject(sentinel3) 251 | end, 10) 252 | end) 253 | promise.all({p1, p2, p3}):thenCall(nil, function(reason) 254 | assert.equal(sentinel3, reason) 255 | done() 256 | end) 257 | assert.True(wait()) 258 | end) 259 | end) 260 | 261 | describe('Promise.allSettled method.', function() 262 | it('should be fulfilled immediately if element is empty', function() 263 | promise.allSettled({}):thenCall(function(value) 264 | assert.same({}, value) 265 | done() 266 | end) 267 | assert.True(wait()) 268 | end) 269 | 270 | describe('wait for fulfillments,', function() 271 | it('use index table as elements', function() 272 | local p1 = promise.resolve(sentinel) 273 | local p2 = sentinel2 274 | local p3 = promise(function(resolve) 275 | setTimeout(function() 276 | resolve(sentinel3) 277 | end, 10) 278 | end) 279 | 280 | promise.allSettled({p1, p2, p3}):thenCall(function(value) 281 | assert.same({ 282 | {status = 'fulfilled', value = sentinel}, 283 | {status = 'fulfilled', value = sentinel2}, 284 | {status = 'fulfilled', value = sentinel3} 285 | }, value) 286 | done() 287 | end) 288 | assert.True(wait()) 289 | end) 290 | 291 | it('use key-value table as elements, different from JavaScript', function() 292 | local p1 = promise.resolve(sentinel) 293 | local p2 = sentinel2 294 | local p3 = promise(function(resolve) 295 | setTimeout(function() 296 | resolve(sentinel3) 297 | end, 10) 298 | end) 299 | 300 | promise.allSettled({p1 = p1, p2 = p2, p3 = p3}):thenCall(function(value) 301 | assert.same({ 302 | p1 = {status = 'fulfilled', value = sentinel}, 303 | p2 = {status = 'fulfilled', value = sentinel2}, 304 | p3 = {status = 'fulfilled', value = sentinel3} 305 | }, value) 306 | done() 307 | end) 308 | assert.True(wait()) 309 | end) 310 | end) 311 | 312 | 313 | it('is always resolved even if any of the elements are rejected', function() 314 | local p1 = promise.resolve(sentinel) 315 | local p2 = sentinel2 316 | local p3 = promise(function(_, reject) 317 | setTimeout(function() 318 | reject(sentinel3) 319 | end, 10) 320 | end) 321 | promise.allSettled({p1, p2, p3}):thenCall(function(value) 322 | assert.same({ 323 | {status = 'fulfilled', value = sentinel}, 324 | {status = 'fulfilled', value = sentinel2}, 325 | {status = 'rejected', reason = sentinel3} 326 | }, value) 327 | done() 328 | end) 329 | assert.True(wait()) 330 | end) 331 | end) 332 | 333 | describe('Promise.any method.', function() 334 | it('should be rejected immediately if element is empty', function() 335 | promise.any({}):thenCall(nil, function(reason) 336 | assert.truthy(reason:match('^AggregateError')) 337 | done() 338 | end) 339 | assert.True(wait()) 340 | end) 341 | 342 | it('resolve with the first promise to fulfill, even if a promise rejects first', function() 343 | local p1 = promise.reject(sentinel) 344 | local p2 = promise(function(resolve) 345 | setTimeout(function() 346 | resolve(sentinel2) 347 | end, 30) 348 | end) 349 | local p3 = promise(function(resolve) 350 | setTimeout(function() 351 | resolve(sentinel3) 352 | end, 10) 353 | end) 354 | promise.any({p1, p2, p3}):thenCall(function(value) 355 | assert.equal(sentinel3, value) 356 | done() 357 | end) 358 | assert.True(wait()) 359 | end) 360 | 361 | it('reject with `AggregateError` if no promise fulfills', function() 362 | promise.any({promise.reject(dummy)}):thenCall(nil, function(reason) 363 | assert.not_equal(dummy, reason) 364 | assert.truthy(reason:match('^AggregateError')) 365 | done() 366 | end) 367 | assert.True(wait()) 368 | end) 369 | end) 370 | 371 | describe('Promise.race method.', function() 372 | it('should be pending forever if element is empty', function() 373 | local onFinally = spy.new(done) 374 | promise.race({}):finally(onFinally) 375 | assert.spy(onFinally).was_not_called() 376 | assert.False(wait(30)) 377 | assert.spy(onFinally).was_not_called() 378 | end) 379 | 380 | describe('resolves or rejects with the first promise to settle,', function() 381 | it('resolve Promise is earlier than reject', function() 382 | local p1 = promise(function(resolve) 383 | setTimeout(function() 384 | resolve(sentinel) 385 | end, 10) 386 | end) 387 | local p2 = promise(function(_, reject) 388 | setTimeout(function() 389 | reject(sentinel2) 390 | end, 20) 391 | end) 392 | promise.race({p1, p2}):thenCall(function(value) 393 | assert.equal(sentinel, value) 394 | done() 395 | end) 396 | assert.True(wait()) 397 | end) 398 | 399 | it('reject Promise is earlier than resolve', function() 400 | local p1 = promise(function(_, reject) 401 | setTimeout(function() 402 | reject(sentinel) 403 | end, 10) 404 | end) 405 | local p2 = promise(function(resolve) 406 | setTimeout(function() 407 | resolve(sentinel2) 408 | end, 20) 409 | end) 410 | promise.race({p1, p2}):thenCall(nil, function(reason) 411 | assert.equal(sentinel, reason) 412 | done() 413 | end) 414 | assert.True(wait()) 415 | end) 416 | end) 417 | end) 418 | end) 419 | -------------------------------------------------------------------------------- /typings/README.md: -------------------------------------------------------------------------------- 1 | # Typings of promise-async only 2 | 3 | Development library for the completion and documentation of promise-async. 4 | 5 | ## Installation 6 | 7 | ### lua-language-server 8 | 9 | Append this directory path to `Lua.workspace.library` field in configuration file. 10 | 11 | Use `.luarc.json` as an example: 12 | 13 | ```json 14 | { 15 | "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", 16 | "workspace.library": ["your_path/typings"] 17 | } 18 | ``` 19 | 20 | ## Credits 21 | 22 | [lua-language-server/wiki/Setting](https://github.com/sumneko/lua-language-server/wiki/Setting) 23 | -------------------------------------------------------------------------------- /typings/async.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: unused-local, missing-return 2 | 3 | ---An async function is a function like the async keyword in JavaScript 4 | ---@class Async 5 | ---@overload fun(executor: table|fun(): ...): Promise 6 | local Async = {} 7 | 8 | ---Await expressions make promise returning functions behave as though they're synchronous by 9 | ---suspending execution until the returned promise is fulfilled or rejected. 10 | ---@param promise PromiseLike|Promise|any 11 | ---@return ... result The resolved value of the promise. 12 | function _G.await(promise) end 13 | 14 | return Async 15 | -------------------------------------------------------------------------------- /typings/loop.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: unused-local, missing-return 2 | 3 | ---Singleton table, can't be as metatable. Two ways to extend the event loop. 4 | ---1. Create a new table and implement all methods, assign the new one to `Promise.loop` . 5 | ---2. Assign the targeted method to the `Promise.loop` field to override method. 6 | ---@class PromiseAsyncEventLoop 7 | local EventLoop = {} 8 | 9 | ---Sets a timer which executes a function once the timer expires. 10 | ---@param callback fun() A callback function, to be executed after the timer expires. 11 | ---@param delay number The time, in milliseconds that the timer should wait. 12 | ---@return userdata timer The timer handle created by EventLoop. 13 | function EventLoop.setTimeout(callback, delay) end 14 | 15 | ---The callback function will be executed in the next tick to continue the event loop. 16 | ---@param callback fun() A callback function, will be wrapped by EventLoop.callWrapper. 17 | function EventLoop.nextTick(callback) end 18 | 19 | ---The callback function will be executed after all next tick events are handled. 20 | ---@param callback fun() A callback function, will be wrapped by EventLoop.callWrapper. 21 | function EventLoop.nextIdle(callback) end 22 | 23 | ---Wrap the callback function from `setTimeout`, `nextTick` and `nextIdle`. 24 | ---@param callback fun() A callback function, executed by asynchronous methods. 25 | function EventLoop.callWrapper(callback) end 26 | 27 | return EventLoop 28 | -------------------------------------------------------------------------------- /typings/promise.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: unused-local, missing-return 2 | 3 | ---@alias PromiseExecutor fun(resolve: fun(value: any), reject: fun(reason?: any)) 4 | 5 | ---@class Promise 6 | ---@field loop PromiseAsyncEventLoop 7 | ---@overload fun(executor: PromiseExecutor): Promise 8 | local Promise = {} 9 | 10 | ---Creates a new Promise. 11 | ---@param executor PromiseExecutor A callback used to initialize the promise. This callback is passed two arguments: 12 | ---a resolve callback used to resolve the promise with a value or the result of another promise, 13 | ---and a reject callback used to reject the promise with a provided reason or error. 14 | ---@return Promise promise A new Promise. 15 | function Promise:new(executor) end 16 | 17 | ---Attaches callbacks for the resolution and/or rejection of the Promise. 18 | ---@param onFulfilled? fun(value: any): any The callback to execute when the Promise is resolved. 19 | ---@param onRejected? fun(reason: any): any The callback to execute when the Promise is rejected. 20 | ---@return Promise promise A Promise for the completion of which ever callback is executed. 21 | function Promise:thenCall(onFulfilled, onRejected) end 22 | 23 | ---Attaches a callback for only the rejection of the Promise. 24 | ---@param onRejected? fun(reason: any): any The callback to execute when the Promise is rejected. 25 | ---@return Promise promise A Promise for the completion of the callback. 26 | function Promise:catch(onRejected) end 27 | 28 | ---Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). 29 | ---The resolved value cannot be modified from the callback. 30 | ---@param onFinally? fun() The callback to execute when the Promise is settled (fulfilled or rejected). 31 | ---@return Promise promise A new Promise. 32 | function Promise:finally(onFinally) end 33 | 34 | ---Creates a new resolved promise for the provided value. 35 | ---@param value? any A value, or the promise passed as value 36 | ---@return Promise promise A resolved promise. 37 | function Promise.resolve(value) end 38 | 39 | ---Creates a new rejected promise for the provided reason. 40 | ---@param reason? any The reason the Promise was rejected. 41 | ---@return Promise promise A new rejected Promise. 42 | function Promise.reject(reason) end 43 | 44 | ---Creates a Promise that is resolved with a table of results when all of the provided 45 | ---Promises resolve, or rejected when any Promise is rejected. 46 | ---@param values table A table of Promises. 47 | ---@return Promise promise A new Promise. 48 | function Promise.all(values) end 49 | 50 | ---Creates a Promise that is resolved with a table of results when all of the provided 51 | ---Promises resolve or reject. 52 | ---@param values table A table of Promises. 53 | ---@return Promise promise A new Promise. 54 | function Promise.allSettled(values) end 55 | 56 | ---The any function returns a Promise that is fulfilled by the first given Promise to be fulfilled, 57 | ---or rejected with an AggregateError containing an table of rejection reasons if all of the 58 | ---given Promises are rejected. It resolves all elements of the passed table to Promises as it runs this algorithm. 59 | ---@param values table 60 | ---@return Promise promise A new Promise 61 | function Promise.any(values) end 62 | 63 | ---Creates a Promise that is resolved or rejected when any of the provided Promises are resolved 64 | ---or rejected. 65 | ---@param values table A table of Promises. 66 | ---@return Promise promise A new Promise. 67 | function Promise.race(values) end 68 | 69 | return Promise 70 | -------------------------------------------------------------------------------- /typings/promiselike.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: unused-local, missing-return 2 | 3 | ---@class PromiseLike 4 | local PromiseLike = {} 5 | 6 | ---Attaches callbacks for the resolution and/or rejection of the Promise. 7 | ---@param onFulfilled? fun(value: any): any The callback to execute when the Promise is resolved. 8 | ---@param onRejected? fun(reason: any): any The callback to execute when the Promise is rejected. 9 | ---@return Promise promise A Promise for the completion of which ever callback is executed. 10 | function PromiseLike:thenCall(onFulfilled, onRejected) end 11 | 12 | return PromiseLike 13 | --------------------------------------------------------------------------------