Initial commit

This commit is contained in:
kevinhwang91 2022-03-22 17:58:04 +08:00
commit fb97aa33dc
52 changed files with 5075 additions and 0 deletions

26
.editorconfig Normal file
View file

@ -0,0 +1,26 @@
[*]
indent_style = space
indent_size = 4
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
[*.lua]
quote_style = single
align_call_args = false
local_assign_continuation_align_to_first_expression = true
keep_one_space_between_table_and_bracket = false
align_table_field_to_first_field = true
remove_expression_list_finish_comma = true
remove_empty_header_and_footer_lines_in_function = true
[*.json]
indent_style = tab
[*.{yaml,yml}]
indent_style = space
indent_size = 2
[{Makefile,**.mk}]
indent_style = tab

43
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Bug Report
description: File a bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
attributes:
label: 'Version (lua -v) or (nvim -v | head -n1)'
placeholder: 'LuaJIT 2.1.0-beta3'
validations:
required: true
- type: input
attributes:
label: 'Operating system/version'
placeholder: 'ArchLinux'
validations:
required: true
- type: textarea
attributes:
label: 'How to reproduce the issue'
description: 'How do you trigger this bug? Please walk us through it step by step.'
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: 'Expected behavior'
description: 'Describe the behavior you expect. May include logs, images, or videos.'
validations:
required: true
- type: textarea
attributes:
label: 'Actual behavior'
validations:
required: true

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: false

View file

@ -0,0 +1,23 @@
name: Feature request
description: Request an enhancement for nvim-bqf
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Before requesting: search [existing issues](https://github.com/kevinhwang91/nvim-bqf/labels/enhancement).
- type: textarea
attributes:
label: "Feature description"
validations:
required: true
- type: textarea
attributes:
label: "Describe the solution you'd like"
validations:
required: true
- type: textarea
attributes:
label: "Additional context"
validations:
required: false

27
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Lint
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install lua-language-server
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cd
gh release download -R sumneko/lua-language-server -p '*-linux-x64.tar.gz' -D lua-language-server
tar xzf lua-language-server/* -C lua-language-server
echo "${PWD}/lua-language-server/bin" >> $GITHUB_PATH
- name: Run Lint
run: make lint

59
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,59 @@
name: Test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
strategy:
matrix:
include:
- { os: ubuntu-latest, target: nvim, version: v0.7.0 }
- { os: ubuntu-latest, target: nvim, version: nightly }
- { os: macos-latest, target: nvim, version: v0.7.0 }
- { os: macos-latest, target: nvim, version: nightly }
- { os: ubuntu-latest, target: lua, version: lua 5.1 }
- { os: ubuntu-latest, target: lua, version: lua 5.2 }
- { os: ubuntu-latest, target: lua, version: lua 5.3 }
- { os: ubuntu-latest, target: lua, version: lua 5.4 }
- { os: ubuntu-latest, target: lua, version: luajit 2.1.0-beta3 }
- { os: macos-latest, target: lua, version: lua 5.1 }
- { os: macos-latest, target: lua, version: lua 5.2 }
- { os: macos-latest, target: lua, version: lua 5.3 }
- { os: macos-latest, target: lua, version: lua 5.4 }
- { os: macos-latest, target: lua, version: luajit 2.1.0-beta3 }
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- if: matrix.target == 'nvim' && matrix.os == 'ubuntu-latest'
name: Install Neovim on Ubuntu
run: |
cd
curl -LO https://github.com/neovim/neovim/releases/download/${{ matrix.version }}/nvim-linux64.tar.gz
tar xzf nvim-linux64.tar.gz
echo "${PWD}/nvim-linux64/bin" >> $GITHUB_PATH
export PATH="${PWD}/nvim-linux64/bin:${PATH}"
nvim -v
- if: matrix.target == 'nvim' && matrix.os == 'macos-latest'
name: Install Neovim on Macos
run: |
cd
curl -LO https://github.com/neovim/neovim/releases/download/${{ matrix.version }}/nvim-macos.tar.gz
tar xzf nvim-macos.tar.gz
echo "${PWD}/nvim-osx64/bin" >> $GITHUB_PATH
export PATH="${PWD}/nvim-osx64/bin:${PATH}"
nvim -v
- name: Run Test
run: |
if [[ ${{ matrix.target }} == lua ]]; then
export LUA_VERSION="${{ matrix.version }}"
fi
make test_${{ matrix.target }}

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

17
.luarc.json Normal file
View file

@ -0,0 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"diagnostics.globals": [
"vim",
"jit",
"it",
"describe",
"before_each",
"after_each",
"spy",
"setup",
"teardown",
"done",
"wait"
],
"runtime.version": "LuaJIT"
}

29
LICENSE Normal file
View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2022, kevinhwang91
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

75
Makefile Normal file
View file

@ -0,0 +1,75 @@
SHELL := /bin/bash
DEPS ?= build
NVIM_BIN ?= $(shell command -v nvim)
ifdef NVIM_BIN
NVIM_LUA_VERSION := $(shell $(NVIM_BIN) -v | grep -E '^Lua(JIT)?' | tr A-Z a-z)
else
NVIM_LUA_VERSION := luajit 2.1.0-beta3
endif
LUA_VERSION ?= $(NVIM_LUA_VERSION)
LUA_NUMBER := $(word 2,$(LUA_VERSION))
TARGET_DIR := $(DEPS)/$(LUA_NUMBER)
HEREROCKS ?= $(DEPS)/hererocks.py
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
HEREROCKS_ENV ?= MACOSX_DEPLOYMENT_TARGET=10.15
endif
HEREROCKS_URL ?= https://raw.githubusercontent.com/luarocks/hererocks/master/hererocks.py
HEREROCKS_ACTIVE := source $(TARGET_DIR)/bin/activate
LUAROCKS ?= $(TARGET_DIR)/bin/luarocks
BUSTED ?= $(TARGET_DIR)/bin/busted
BUSTED_HELPER ?= $(PWD)/spec/fixtures.lua
LUV ?= $(TARGET_DIR)/lib/lua/$(LUA_NUMBER)/luv.so
LUA_LS ?= $(DEPS)/lua-language-server
LINT_LEVEL ?= Information
all: deps
deps: | $(HEREROCKS) $(BUSTED)
test: test_lua test_nvim
test_lua: $(BUSTED) $(LUV)
@echo Test with $(LUA_VERSION) ......
@$(HEREROCKS_ACTIVE) && eval $$(luarocks path) && \
lua spec/init.lua --helper=$(BUSTED_HELPER) $(BUSTED_ARGS)
test_nvim: $(BUSTED)
ifeq ($(LUA_VERSION),$(NVIM_LUA_VERSION))
@echo Test with Neovim ......
@$(HEREROCKS_ACTIVE) && eval $$(luarocks path) && \
$(NVIM_BIN) --clean -n --headless -u spec/init.lua -- \
--helper=$(BUSTED_HELPER) $(BUSTED_ARGS)
endif
$(HEREROCKS):
mkdir -p $(DEPS)
curl $(HEREROCKS_URL) -o $@
$(LUAROCKS): $(HEREROCKS)
$(HEREROCKS_ENV) python $< $(TARGET_DIR) --$(LUA_VERSION) -r latest
$(BUSTED): $(LUAROCKS)
$(HEREROCKS_ACTIVE) && luarocks install busted
$(LUV): $(LUAROCKS)
@$(HEREROCKS_ACTIVE) && [[ ! $$(luarocks which luv) ]] && \
luarocks install luv || true
lint:
@rm -rf $(LUA_LS)
@mkdir -p $(LUA_LS)
@lua-language-server --check $(PWD) --checklevel=$(LINT_LEVEL) --logpath=$(LUA_LS)
@[[ -f $(LUA_LS)/check.json ]] && { cat $(LUA_LS)/check.json 2>/dev/null; exit 1; } || true
clean:
rm -rf $(DEPS)
.PHONY: all deps clean lint test test_nvim test_lua

137
README.md Normal file
View file

@ -0,0 +1,137 @@
# promise-async
![GitHub Test](https://github.com/kevinhwang91/promise-async/workflows/Test/badge.svg)
![GitHub Lint](https://github.com/kevinhwang91/promise-async/workflows/Lint/badge.svg)
The goal of promise-async is to port [Promise][promise] & [Async][async] from JavaScript to Lua.
> A value returned by async function in JavaScript is actually a Promise Object. It's incomplete and
> inflexible for using an async function wrapped by bare coroutine without Promise.
## Features
- API is similar to JavaScript's
- Customize EventLoop in any platforms
- Support Lua 5.1-5.4 and LuaJIT with an EventLoop module
- Support Neovim platform
## Demonstrating
<https://user-images.githubusercontent.com/17562139/168878472-59cdea3e-16a1-4d2d-afaf-4b7a6d08df7a.mp4>
### Script
#### demo.lua
#### demo.js
## Quickstart
### Requirements
- Lua 5.1 or latter
- [Luv](https://github.com/luvit/luv)
> Luv is a default EventLoop for promise-async. It doesn't mean promise-async must require it. In
> fact, promise-async require a general EventLoop framework which Luv like.
### Installation
#### As a plugin for Neovim platform
Install with [Packer.nvim](https://github.com/wbthomason/packer.nvim):
- As a normal plugin
```lua
use {'kevinhwang91/promise-async'}
```
or
- As a Luarocks plugin
```lua
use_rocks {'promise-async'}
```
#### As a library from Luarocks
1. `luarocks install promise-async`
2. `luarocks install luv` or implement an EventLoop
[interface](https://github.com/kevinhwang91/promise-async/blob/main/typings/loop.lua) to adapt
your platform
## Documentation
promise-async's API is based on [MDN-Promise][promise] from JavaScript.
[typings/promise.lua](typings/promise.lua) is the typings with documentation of Promise class.
Summary up the API different from JavaScript.
<!-- markdownlint-disable MD013 -->
| JavaScript | Lua |
| --------------------------------------------------- | ----------------------------------------------- |
| `new Promise` | `Promise.new`/`Promise` |
| `Promise.then` | `Promise:thenCall`, `then` is language keyword |
| `Promise.catch` | `Promise:catch` |
| `Promise.finally`: return a new Promise | `Promise:finally`: return itself |
| `Promise.resolve` | `Promise.resolve` |
| `Promise.reject` | `Promise.reject` |
| `Promise.all`: `Symbol.iterator` as iterator | `Promise.all`: `pairs` as iterator |
| `Promise.allSettled`: `Symbol.iterator` as iterator | `Promise.allSettled`: `pairs` as iterator |
| `Promise.any`: `Symbol.iterator` as iterator | `Promise.any`: `pairs` as iterator |
| `Promise.race`: `Symbol.iterator` as iterator | `Promise.race`: `pairs` as iterator |
| `async`: as keyword at the start of a function | `Async`/`Async.sync`: as a surrounding function |
| `await`: as keyword | `async`/`Async.wait` as a function |
<!-- markdownlint-enable MD013 -->
The environment in `Async.sync` function have been injected some new functions for compatibility or
enhancement:
1. `await`: A reference of `Async.wait` function;
2. `pcall`: Be compatible with LuaJIT;
3. `xpcall`: Be compatible with LuaJIT;
## Development
### Neovim tips
- `Promise.resolve():thenCall(cb)` is almost equivalent to `vim.schedule(cb)`.
### Run tests
`make test`
### Improve completion experience
Following [typings/README.md](./typings/README.md)
### Customize EventLoop
TODO, refer to [loop.lua](./lua/promise-async/loop.lua)
## Credit
- [Promise][promise]
- [Async][async]
- [promises-tests](https://github.com/promises-aplus/promises-tests)
- [then/promise](https://github.com/then/promise)
- [promisejs.org](https://www.promisejs.org)
- [event-loop-timers-and-nexttick](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick)
[promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[async]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
## Feedback
- If you get an issue or come up with an awesome idea, don't hesitate to open an issue in github.
- If you think this plugin is useful or cool, consider rewarding it a star.
## License
The project is licensed under a BSD-3-clause license. See [LICENSE](./LICENSE) file for details.

6
examples/README.md Normal file
View file

@ -0,0 +1,6 @@
# Examples
Make sure that modules can be found under the `package.path` and `package.cpath`.
If can't find modules, please run `export LUA_PATH="$(dirname $PWD)/lua/?.lua;;"` under CWD in shell
or install the modules except for this project.

29
examples/coc_nvim.lua Normal file
View file

@ -0,0 +1,29 @@
local promise = require('promise')
local M = {}
local fn = vim.fn
function M.action(action, ...)
local args = {...}
return promise(function(resolve, reject)
table.insert(args, function(err, res)
if err ~= vim.NIL then
reject(err)
else
if res == vim.NIL then
res = nil
end
resolve(res)
end
end)
fn.CocActionAsync(action, unpack(args))
end)
end
function M.runCommand(name, ...)
return M.action('runCommand', name, ...)
end
--
-- M.action('showOutline', true)
return M

58
examples/demo.js Normal file
View file

@ -0,0 +1,58 @@
async function defuse(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(ms)
}, ms)
})
}
async function bomb(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(ms)
}, ms)
})
}
async function race() {
return await Promise.race([
defuse(500 + Math.ceil(Math.random() * 500)),
bomb(800 + Math.ceil(Math.random() * 200)),
])
}
async function play() {
console.info('Game start!')
let cnt = 0
try {
while (true) {
let ms = await race()
cnt = cnt + ms
console.info(`Defuse after ${ms}ms~`)
}
} catch (msErr) {
cnt = cnt + msErr
console.info(`Bomb after ${msErr}ms~`)
}
console.info(`Game end after ${cnt}ms!`)
await {
then: function(resolve, reject) {
setTimeout(() => {
reject(this.message)
}, 1000)
},
message: 'try to throw an error :)'
}
}
Promise.resolve().then((value) => {
console.info('In next tick')
})
console.info('In main')
play().finally(() => {
console.info('Before throwing UnhandledPromiseRejection on finally!')
})

87
examples/demo.lua Normal file
View file

@ -0,0 +1,87 @@
---@diagnostic disable: unused-local
local uv = require('luv')
local async = require('async')
local promise = require('promise')
math.randomseed(math.ceil(uv.uptime()))
local function setTimeout(callback, ms)
local timer = uv.new_timer()
timer:start(ms, 0, function()
timer:close()
callback()
end)
return timer
end
local function defuse(ms)
return promise.new(function(resolve, reject)
setTimeout(function()
resolve(ms)
end, ms)
end)
end
local function bomb(ms)
-- getmetatable(promise).__call = promise.new
return promise(function(resolve, reject)
setTimeout(function()
reject(ms)
end, ms)
end)
end
local function race()
return async(function()
return await(promise.race({
defuse(math.random(500, 1000)),
bomb(math.random(800, 1000))
}))
end)
end
local notify = vim and vim.notify or print
local function play()
return async(function()
-- We are not in the next tick until first `await` is called.
notify('Game start!')
local cnt = 0
xpcall(function()
while true do
local ms = await(race())
cnt = cnt + ms
notify(('Defuse after %dms~'):format(ms))
end
end, function(msErr)
cnt = cnt + msErr
notify(('Bomb after %dms~'):format(msErr))
end)
notify(('Game end after %dms!'):format(cnt))
await {
thenCall = function(self, resolve, reject)
setTimeout(function()
reject(self.message)
end, 1000)
end,
message = 'try to throw an error :)'
}
end)
end
promise.resolve():thenCall(function(value)
notify('In next tick')
end)
notify('In main')
play():finally(function()
print('Before throwing UnhandledPromiseRejection on finally!')
end)
-- uv.run will be called automatically under Neovim main loop
if not vim then
uv.run()
end

View file

@ -0,0 +1,47 @@
---@diagnostic disable: redefined-local
local uv = require('luv')
local promise = require('promise')
local mpack = require('mpack')
local asyncHandle
local thread
promise(function(resolve, reject)
asyncHandle = uv.new_async(function(err, data)
asyncHandle:close()
if err then
reject((mpack.unpack or mpack.decode)(err))
else
resolve((mpack.unpack or mpack.decode)(data))
end
end)
end):thenCall(function(value)
print(('Getting resolved value: %s from %s'):format(value[1], thread))
end, function(reason)
print(('Getting rejected reason: %s from %s'):format(reason[1], thread))
end)
thread = uv.new_thread(function(delay, asyn)
local uv = require('luv')
local mpack = require('mpack')
local promise = require('promise')
math.randomseed(math.ceil(uv.uptime()))
promise(function(resolve, reject)
print(tostring(uv.thread_self()) .. ' is running.')
promise.loop.setTimeout(function()
if math.random(1, 2) == 1 then
resolve({'succeeded'})
else
reject({'failed'})
end
end, delay)
end):thenCall(function(value)
uv.async_send(asyn, nil, (mpack.pack or mpack.encode)(value))
end):catch(function(reason)
uv.async_send(asyn, (mpack.pack or mpack.encode)(reason))
end)
uv.run()
end, 1000, asyncHandle)
if not vim then
uv.run()
end

23
examples/read_file.lua Normal file
View file

@ -0,0 +1,23 @@
local uv = require('luv')
local uva = require('uva')
local async = require('async')
local function readFile(path)
return async(function()
local fd = await(uva.open(path, 'r', 438))
local stat = await(uva.fstat(fd))
local data = await(uva.read(fd, stat.size, 0))
await(uva.close(fd))
return data
end)
end
local currentPath = debug.getinfo(1, 'S').source:sub(2)
print('Reading ' .. currentPath .. '......\n')
readFile(currentPath):thenCall(function(value)
print(value)
end)
if not vim then
uv.run()
end

74
examples/uva.lua Normal file
View file

@ -0,0 +1,74 @@
---@class UvFS
local M = {}
local uv = require('luv')
local promise = require('promise')
local compat = require('promise-async.compat')
local function wrap(name, argc)
return function(...)
local argv = {...}
return promise(function(resolve, reject)
argv[argc] = function(err, data)
if err then
reject(err)
else
resolve(data)
end
end
uv[name](compat.unpack(argv))
end)
end
end
M.close = wrap('fs_close', 2)
M.open = wrap('fs_open', 4)
M.read = wrap('fs_read', 4)
M.unlink = wrap('fs_unlink', 2)
M.write = wrap('fs_write', 4)
M.mkdir = wrap('fs_mkdir', 3)
M.mkdtemp = wrap('fs_mkdtemp', 2)
M.mkstemp = wrap('fs_mkstemp', 2)
M.rmdir = wrap('fs_rmdir', 2)
M.scandir = wrap('fs_scandir', 2)
M.stat = wrap('fs_stat', 2)
M.fstat = wrap('fs_fstat', 2)
M.lstat = wrap('fs_lstat', 2)
M.rename = wrap('fs_rename', 3)
M.fsync = wrap('fs_fsync', 2)
M.fdatasync = wrap('fs_fdatasync', 2)
M.ftruncate = wrap('fs_ftruncate', 3)
M.sendfile = wrap('fs_sendfile', 5)
M.access = wrap('fs_access', 3)
M.chmod = wrap('fs_chmod', 3)
M.fchmod = wrap('fs_fchmod', 3)
M.utime = wrap('fs_utime', 4)
M.futime = wrap('fs_futime', 4)
M.lutime = wrap('fs_lutime', 4)
M.link = wrap('fs_link', 3)
M.symlink = wrap('fs_symlink', 4)
M.readlink = wrap('fs_readlink', 2)
M.realpath = wrap('fs_realpath', 2)
M.chown = wrap('fs_chown', 4)
M.fchown = wrap('fs_fchown', 4)
M.lchown = wrap('fs_lchown', 4)
M.copyfile = wrap('fs_copyfile', 4)
-- TODO
M.opendir = function(path, entries)
return promise(function(resolve, reject)
uv.fs_opendir(path, function(err, data)
if err then
reject(err)
else
resolve(data)
end
end, entries)
end)
end
M.readdir = wrap('fs_readdir', 2)
M.closedir = wrap('fs_closedir', 2)
M.statfs = wrap('fs_statfs', 2)
return M

21
examples/write_file.lua Normal file
View file

@ -0,0 +1,21 @@
local uv = require('luv')
local uva = require('uva')
local async = require('async')
local function writeFile(path, data)
return async(function()
local path_ = path .. '_'
local fd = await(uva.open(path_, 'w', 438))
await(uva.write(fd, data, -1))
await(uva.close(fd))
pcall(await, uva.rename(path_, path))
end)
end
local path = debug.getinfo(1, 'S').source:sub(2) .. '__'
print('Writing ' .. path .. '......\n')
writeFile(path, 'write some texts :)\n')
if not vim then
uv.run()
end

131
lua/async.lua Normal file
View file

@ -0,0 +1,131 @@
local promise = require('promise')
local utils = require('promise-async.utils')
local compat = require('promise-async.compat')
local errFactory = require('promise-async.error')
local shortSrc = debug.getinfo(1, 'S').short_src
---@class Async
---@overload fun(executor: fun()): Promise
local Async = setmetatable({}, {
__call = function(self, executor)
return self.sync(executor)
end
})
local packedId = {}
local Packed = {_id = packedId}
Packed.__index = Packed
local function wrapPacked(packed)
return setmetatable(packed, Packed)
end
local function isPacked(o)
return type(o) == 'table' and o._id == packedId
end
local function wrapFenv()
local function result(ok, ...)
if ok then
return true, ...
end
local err = select(1, ...)
return false, errFactory.isInstance(err) and err:peek() or err
end
return {
await = Async.wait,
pcall = function(f, ...)
return result(compat.pcall(f, ...))
end,
xpcall = function(f, msgh, ...)
return compat.xpcall(f, function(err)
return msgh(errFactory.isInstance(err) and err:peek() or err)
end, ...)
end
}
end
local function getNewFenv(call)
return setmetatable(wrapFenv(), {
__index = compat.getfenv(call)
})
end
local function buildError(thread, level, err)
if not errFactory.isInstance(err) then
err = errFactory.new(err)
level = level + 1
end
local ok, value
repeat
ok, value = errFactory.format(thread, level, shortSrc)
level = level + 1
err:push(value)
until not ok
return err
end
---Export wait function to someone needs
---@param executor fun()
---@return Promise
function Async.sync(executor)
local typ = type(executor)
local isCallable, call = utils.getCallable(executor, typ)
assert(isCallable, 'a callable table or function expected, got ' .. typ)
compat.setfenv(call, getNewFenv(call))
return promise.new(function(resolve, reject)
local co = coroutine.create(typ == 'function' and executor or function()
return executor()
end)
local function pack(status, ...)
return status, {...}, select('#', ...)
end
local function next(err, res)
local ok, t, n = pack(coroutine.resume(co, err, res))
if not ok then
local reason = t[1]
reject(reason)
return
elseif coroutine.status(co) == 'dead' then
local value
if n == 1 then
value = t[1]
elseif n > 1 then
value = wrapPacked(t)
end
resolve(value)
return
end
local p = t[1]
p:thenCall(function(value)
next(false, value)
end, function(reason)
next(true, reason)
end)
end
next()
end)
end
---Export wait function to someone needs, wait function actually have been injected as `async`
---into the executor of async function
---@param p Promise|table
---@return ...
function Async.wait(p)
p = promise.resolve(p)
local err, res = coroutine.yield(p)
if err then
error(buildError(coroutine.running(), 2, res))
elseif isPacked(res) then
return compat.unpack(res)
else
return res
end
end
return Async

View file

@ -0,0 +1,133 @@
---Functions are compatible with LuaJIT's.
---@class PromiseAsyncCompat
local M = {}
---@return boolean
function M.is51()
return _G._VERSION:sub(-3) == '5.1' and not jit
end
if table.pack then
M.pack = table.pack
else
M.pack = function(...)
return {n = select('#', ...), ...}
end
end
if table.unpack then
M.unpack = table.unpack
else
M.unpack = unpack
end
if M.is51() then
local _pcall, _xpcall = pcall, xpcall
local utils = require('promise-async.utils')
local function yieldInCoroutine(thread, co, success, ...)
if coroutine.status(co) == 'suspended' then
return yieldInCoroutine(thread, co, coroutine.resume(co, coroutine.yield(...)))
end
return success, ...
end
local function doPcall(thread, f, ...)
local typ = type(f)
local ok, fn = utils.getCallable(f, typ)
if not ok then
return false, ('attempt to call a %s value'):format(typ)
end
local co = coroutine.create(function(...)
return fn(...)
end)
return yieldInCoroutine(thread, co, coroutine.resume(co, ...))
end
M.pcall = function(f, ...)
local thread = coroutine.running()
if not thread then
return _pcall(f, ...)
end
return doPcall(thread, f, ...)
end
local function xpcallCatch(msgh, success, ...)
if success then
return true, ...
end
local ok, result = _pcall(msgh, ...)
return false, ok and result or 'error in error handling'
end
M.xpcall = function(f, msgh, ...)
local thread = coroutine.running()
if not thread then
local args, n = {...}, select('#', ...)
return _xpcall(function()
return f(unpack(args, 1, n))
end, msgh)
end
return xpcallCatch(msgh, doPcall(thread, f, ...))
end
else
M.pcall = pcall
M.xpcall = xpcall
end
if setfenv then
M.setfenv = setfenv
M.getfenv = getfenv
else
local function findENV(f)
local name = ''
local value
local up = 1
while name do
name, value = debug.getupvalue(f, up)
if name == '_ENV' then
return up, value
end
up = up + 1;
end
return 0
end
local function envHelper(f, name)
if type(f) == 'number' then
if f < 0 then
error(([[bad argument #1 to '%s' (level must be non-negative)]]):format(name), 3)
end
local ok, dInfo = pcall(debug.getinfo, f + 2, 'f')
if not ok or not dInfo then
error(([[bad argument #1 to '%s' (invalid level)]]):format(name), 3)
end
f = dInfo.func
elseif type(f) ~= 'function' then
error(([[bad argument #1 to '%s' (number expected, got %s)]]):format(name, type(f)), 3)
end
return f
end
function M.setfenv(f, table)
f = envHelper(f, 'setfenv')
local up = findENV(f)
if up > 0 then
debug.upvaluejoin(f, up, function()
return table
end, 1)
end
return f
end
function M.getfenv(f)
if f == 0 or f == nil then
return _G
end
f = envHelper(f, 'getfenv')
local up, value = findENV(f)
return up > 0 and value or _G
end
end
return M

112
lua/promise-async/error.lua Normal file
View file

@ -0,0 +1,112 @@
local errorId = {}
---@class PromiseAsyncError
---@field err any
---@field queue string[]
---@field index number
local Error = {_id = errorId}
Error.__index = Error
local function dump(o, limit)
local s
local fmt = '%s [%s] =%s,'
if type(o) == 'table' then
if limit > 0 then
s = '{'
for k, v in pairs(o) do
if type(k) ~= 'number' then
k = '"' .. k .. '"'
end
s = fmt:format(s, k, dump(v, limit - 1))
end
s = s:sub(1, #s - 2) .. ' }'
else
s = '{...}'
end
else
s = tostring(o)
end
return ' ' .. s
end
function Error.isInstance(o)
return type(o) == 'table' and o._id == errorId
end
---@param thread? thread
---@param level number
---@param skipShortSrc? string
---@return boolean, string|nil
function Error.format(thread, level, skipShortSrc)
local ok = false
local res
local dInfo = thread and debug.getinfo(thread, level, 'nSl') or debug.getinfo(level, 'nSl')
if dInfo then
local name, shortSrc, currentline = dInfo.name, dInfo.short_src, dInfo.currentline
if skipShortSrc == shortSrc then
return ''
end
local detail
if not name or name == '' then
detail = ('in function <Anonymous:%d>'):format(dInfo.linedefined)
else
detail = ([[in function '%s']]):format(name)
end
ok = true
res = (' %s:%d: %s'):format(shortSrc, currentline, detail)
end
return ok, res
end
---@param err any
---@return PromiseAsyncError
function Error.new(err)
local o = setmetatable({}, Error)
o.err = err
o.queue = {}
o.index = 0
return o
end
function Error:__tostring()
local errMsg = dump(self.err, 1):match('%s*(.*)')
if #self.queue == 0 then
return errMsg
end
local t = {}
for i = 1, self.index do
table.insert(t, self.queue[i])
end
table.insert(t, errMsg)
if self.index < #self.queue then
table.insert(t, 'stack traceback:')
end
for i = self.index + 1, #self.queue do
table.insert(t, self.queue[i])
end
return table.concat(t, '\n')
end
---@param value string
function Error:unshift(value)
if value then
self.index = self.index + 1
table.insert(self.queue, 1, value)
end
return #self.queue
end
---@param value string
function Error:push(value)
if value then
table.insert(self.queue, value)
end
return #self.queue
end
---@return any
function Error:peek()
return self.err
end
return Error

View file

@ -0,0 +1,85 @@
local uv = require('luv')
---@class PromiseAsyncLoop
---@field tick userdata
---@field tickCallbacks function[]
---@field tickStarted boolean
---@field idle userdata
---@field idleCallbacks function[]
---@field idleStarted boolean
local EventLoop = {
tick = uv.new_timer(),
tickCallbacks = {},
tickStarted = false,
idle = uv.new_idle(),
idleCallbacks = {},
idleStarted = false
}
function EventLoop.setTimeout(callback, ms)
local timer = uv.new_timer()
timer:start(ms, 0, function()
timer:close()
EventLoop.callWrapper(callback)
end)
return timer
end
local function runTick()
EventLoop.tickStarted = true
local callbacks = EventLoop.tickCallbacks
EventLoop.tickCallbacks = {}
for _, cb in ipairs(callbacks) do
EventLoop.callWrapper(cb)
end
if #EventLoop.tickCallbacks > 0 then
EventLoop.tick:start(0, 0, runTick)
else
EventLoop.tickStarted = false
end
end
function EventLoop.nextTick(callback)
table.insert(EventLoop.tickCallbacks, callback)
if not EventLoop.tickStarted then
EventLoop.tick:start(0, 0, runTick)
end
end
local function runIdle()
EventLoop.idleStarted = true
local callbacks = EventLoop.idleCallbacks
EventLoop.idleCallbacks = {}
for _, cb in ipairs(callbacks) do
EventLoop.callWrapper(cb)
end
if #EventLoop.idleCallbacks > 0 then
EventLoop.idle:start(runIdle)
else
EventLoop.idle:stop()
EventLoop.idleStarted = false
end
end
function EventLoop.nextIdle(callback)
EventLoop.nextTick(function()
table.insert(EventLoop.idleCallbacks, callback)
if not EventLoop.idleStarted then
EventLoop.idle:start(runIdle)
end
end)
end
if vim and type(vim.schedule) == 'function' then
EventLoop.callWrapper = vim.schedule
else
function EventLoop.callWrapper(fn)
local ok, res = pcall(fn)
if not ok then
-- luv can't handle object from __tostring()
error(tostring(res))
end
end
end
return EventLoop

View file

@ -0,0 +1,33 @@
---@class PromiseAsyncUtils
local M = {}
---@param o any
---@param expectedType string
function M.assertType(o, expectedType)
local gotType = type(o)
local fmt = '%s expected, got %s'
return assert(gotType == expectedType, fmt:format(expectedType, gotType))
end
---@param o any
---@param typ? string
---@return boolean, function|table|any
function M.getCallable(o, typ)
local ok
local f
local t = typ or type(o)
if t == 'function' then
ok, f = true, o
elseif t ~= 'table' then
ok, f = false, o
else
local meta = getmetatable(o)
ok = meta and type(meta.__call) == 'function'
if ok then
f = meta.__call
end
end
return ok, f
end
return M

399
lua/promise.lua Normal file
View file

@ -0,0 +1,399 @@
local utils = require('promise-async.utils')
local compat = require('promise-async.compat')
local promiseId = {}
---@diagnostic disable: undefined-doc-name
---@alias PromiseState
---| PENDING # 1
---| FULFILLED # 2
---| REJECTED # 3
---@diagnostic enable: undefined-doc-name
local PENDING = 1
local FULFILLED = 2
local REJECTED = 3
--
---@class Promise
---@field state PromiseState
---@field result any
---@field queue table
---@field loop PromiseAsyncLoop
---@field finallyQueue function[]
---@field needHandleRejection? boolean
---@overload fun(executor: PromiseExecutor): Promise
local Promise = setmetatable({_id = promiseId}, {
__call = function(self, executor)
return self.new(executor)
end
})
local function loadEventLoop()
local success, res = pcall(require, 'promise-async.loop')
assert(success, 'Promise need a EventLoop, ' ..
'luv module or a customized EventLoop module is expected.')
return res
end
Promise.loop = setmetatable({}, {
__index = function(_, key)
Promise.loop = loadEventLoop()
return Promise.loop[key]
end,
__newindex = function(_, key, value)
Promise.loop = loadEventLoop()
Promise.loop[key] = value
end
})
function Promise:__tostring()
local state = self.state
if state == PENDING then
return 'Promise { <pending> }'
elseif state == REJECTED then
return ('Promise { <rejected> %s }'):format(tostring(self.result))
else
return ('Promise { <fulfilled> %s }'):format(tostring(self.result))
end
end
local function noop() end
---@param o any
---@param typ? string
---@return boolean
function Promise.isInstance(o, typ)
return (typ or type(o)) == 'table' and o._id == promiseId
end
---must one time get `thenCall` field from `o`, can't call repeatedly.
---@param o any
---@param typ? type
---@return function?
function Promise.getThenable(o, typ)
local thenCall
if (typ or type(o)) == 'table' then
thenCall = o.thenCall
if type(thenCall) ~= 'function' then
thenCall = nil
end
end
return thenCall
end
local resolvePromise, rejectPromise
---@param promise Promise
local function handleQueue(promise)
local queue = promise.queue
local finallyQueue = promise.finallyQueue
if #queue == 0 and #finallyQueue == 0 then
return
end
if promise.needHandleRejection and #queue > 0 then
promise.needHandleRejection = nil
end
promise.queue = {}
promise.finallyQueue = {}
Promise.loop.nextTick(function()
local state, result = promise.state, promise.result
for _, q in ipairs(queue) do
local newPromise, onFulfilled, onRejected = compat.unpack(q)
local func
if state == FULFILLED then
if utils.getCallable(onFulfilled) then
func = onFulfilled
else
resolvePromise(newPromise, result)
end
elseif state == REJECTED then
if utils.getCallable(onRejected) then
func = onRejected
else
rejectPromise(newPromise, result)
end
end
if func then
local ok, res = pcall(func, result)
if ok then
resolvePromise(newPromise, res)
else
rejectPromise(newPromise, res)
end
end
end
for _, onFinally in ipairs(finallyQueue) do
if utils.getCallable(onFinally) then
pcall(onFinally)
end
end
end)
end
---@param promise Promise
---@param result any
---@param state PromiseState
local function transition(promise, result, state)
if promise.state ~= PENDING then
return
end
promise.result = result
promise.state = state
handleQueue(promise)
end
---@param promise Promise
---@param executor PromiseExecutor
---@param self? table
local function wrapExecutor(promise, executor, self)
local called = false
local resolve = function(value)
if called then
return
end
resolvePromise(promise, value)
called = true
end
local reject = function(reason)
if called then
return
end
rejectPromise(promise, reason)
called = true
end
local ok, res
if self then
ok, res = pcall(executor, self, resolve, reject)
else
ok, res = pcall(executor, resolve, reject)
end
if not ok and not called then
reject(res)
end
end
---@param promise Promise
local function handleRejection(promise)
promise.needHandleRejection = true
Promise.loop.nextIdle(function()
if promise.needHandleRejection then
promise.needHandleRejection = nil
local errFactory = require('promise-async.error')
local reason = promise.result
if not errFactory.isInstance(reason) then
reason = errFactory.new(reason)
end
reason:unshift('UnhandledPromiseRejection with the reason:')
error(reason)
end
end)
end
---@param promise Promise
---@param reason any
rejectPromise = function(promise, reason)
handleRejection(promise)
transition(promise, reason, REJECTED)
end
---@param promise Promise
---@param value any
resolvePromise = function(promise, value)
if promise == value then
local reason = debug.traceback('TypeError: Chaining cycle detected for promise')
rejectPromise(promise, reason)
return
end
local valueType = type(value)
if Promise.isInstance(value, valueType) then
value:thenCall(function(val)
resolvePromise(promise, val)
end, function(reason)
rejectPromise(promise, reason)
end)
else
local thenCall = Promise.getThenable(value, valueType)
if thenCall then
wrapExecutor(promise, thenCall, value)
else
transition(promise, value, FULFILLED)
end
end
end
---@param executor PromiseExecutor
---@return Promise
function Promise.new(executor)
utils.assertType(executor, 'function')
---@type Promise
local o = setmetatable({}, Promise)
Promise.__index = Promise
o.state = PENDING
o.result = nil
o.queue = {}
o.finallyQueue = {}
o.needHandleRejection = nil
if executor ~= noop then
wrapExecutor(o, executor)
end
return o
end
---@param onFulfilled? fun(value: any)
---@param onRejected? fun(reason: any)
---@return Promise
function Promise:thenCall(onFulfilled, onRejected)
local o = Promise.new(noop)
table.insert(self.queue, {o, onFulfilled, onRejected})
if self.state ~= PENDING then
handleQueue(self)
end
return o
end
---@param onRejected? fun(reason: any)
---@return Promise
function Promise:catch(onRejected)
return self:thenCall(nil, onRejected)
end
---@param onFinally? fun()
---@return Promise
function Promise:finally(onFinally)
table.insert(self.finallyQueue, onFinally)
if self.state ~= PENDING then
handleQueue(self)
end
return self
end
---@param value? any
---@return Promise
function Promise.resolve(value)
local typ = type(value)
if Promise.isInstance(value, typ) then
return value
else
local o = Promise.new(noop)
local thenCall = Promise.getThenable(value, typ)
if thenCall then
wrapExecutor(o, thenCall, value)
else
o.state = FULFILLED
o.result = value
end
return o
end
end
---@param reason? any
---@return Promise
function Promise.reject(reason)
local o = Promise.new(noop)
o.state = REJECTED
o.result = reason
handleRejection(o)
return o
end
---@param values table
---@return Promise
function Promise.all(values)
utils.assertType(values, 'table')
return Promise.new(function(resolve, reject)
local res = {}
local cnt = 0
for k, v in pairs(values) do
cnt = cnt + 1
Promise.resolve(v):thenCall(function(value)
res[k] = value
cnt = cnt - 1
if cnt == 0 then
resolve(res)
end
end, function(reason)
reject(reason)
end)
end
if cnt == 0 then
resolve(res)
end
end)
end
---@param values table
---@return Promise
function Promise.allSettled(values)
utils.assertType(values, 'table')
return Promise.new(function(resolve, reject)
local res = {}
local cnt = 0
local _ = reject
for k, v in pairs(values) do
cnt = cnt + 1
Promise.resolve(v):thenCall(function(value)
res[k] = {status = 'fulfilled', value = value}
end, function(reason)
res[k] = {status = 'rejected', reason = reason}
end):finally(function()
cnt = cnt - 1
if cnt == 0 then
resolve(res)
end
end)
end
if cnt == 0 then
resolve(res)
end
end)
end
---@param values table
---@return Promise
function Promise.any(values)
utils.assertType(values, 'table')
return Promise.new(function(resolve, reject)
local cnt = 0
local function rejectAggregateError()
if cnt == 0 then
reject('AggregateError: All promises were rejected')
end
end
for _, p in pairs(values) do
cnt = cnt + 1
Promise.resolve(p):thenCall(function(value)
resolve(value)
end, function()
end):finally(function()
cnt = cnt - 1
rejectAggregateError()
end)
end
rejectAggregateError()
end)
end
---@param values table
---@return Promise
function Promise.race(values)
utils.assertType(values, 'table')
return Promise.new(function(resolve, reject)
for _, p in pairs(values) do
Promise.resolve(p):thenCall(function(value)
resolve(value)
end, function(reason)
reject(reason)
end)
end
end)
end
return Promise

View file

@ -0,0 +1,31 @@
---@diagnostic disable: lowercase-global
package = 'promise-async'
version = 'scm-1'
source = {
url = 'git://git@github.com/kevinhwang91/promise-async.git'
}
description = {
summary = 'Promise & Async in Lua',
detailed = 'The goal of promise-async is to port Promise & Async from JavaScript to Lua.',
homepage = 'https://github.com/kevinhwang91/promise-async',
license = ' BSD-3-Clause'
}
dependencies = {
'lua >= 5.1, < 5.4'
}
build = {
type = 'builtin',
modules = {
async = 'lua/async.lua',
promise = 'lua/promise.lua',
['promise-async.compat'] = 'lua/promise-async/compat.lua',
['promise-async.error'] = 'lua/promise-async/error.lua',
['promise-async.loop'] = 'lua/promise-async/loop.lua',
['promise-async.utils'] = 'lua/promise-async/utils.lua'
},
copy_directories = {
'typings'
}
}

405
spec/async_spec.lua Normal file
View file

@ -0,0 +1,405 @@
local promise = require('promise')
local async = require('async')
local helpers = require('spec.helpers.init')
local compat = require('promise-async.compat')
local deferredPromise = helpers.deferredPromise
local setTimeout = helpers.setTimeout
local basics = require('spec.helpers.basics')
local dummy = {dummy = 'dummy'}
local sentinel = {sentinel = 'sentinel'}
local sentinel2 = {sentinel = 'sentinel2'}
local sentinel3 = {sentinel = 'sentinel3'}
local other = {other = 'other'}
describe('async await module.', function()
describe('async return a Promise.', function()
it('return value is a Promise', function()
local f = async(function() end)
assert.True(promise.isInstance(f))
end)
it('async without return statement', function()
async(function() end)
:thenCall(function(value)
assert.equal(nil, value)
done()
end)
assert.True(wait())
end)
it('async return a single', function()
async(function()
return dummy
end):thenCall(function(value)
assert.equal(dummy, value)
done()
end)
assert.True(wait())
end)
it('async return multiple values, which is packed into value in Promise', function()
async(function()
return sentinel, sentinel2, sentinel3
end):thenCall(function(value)
assert.same({sentinel, sentinel2, sentinel3}, value)
done()
end)
assert.True(wait())
end)
it('async throw error', function()
async(function()
error(dummy)
return other
end):thenCall(nil, function(reason)
assert.equal(dummy, reason)
done()
end)
assert.True(wait())
end)
end)
describe('executor inside async.', function()
describe('must be either a function or a callable table,', function()
it('other values', function()
local errorValues = {nil, 0, '0', true}
for _, v in pairs(errorValues) do
assert.error(function()
async(v)
end)
end
end)
it('a function', function()
async(function()
setTimeout(function()
done()
end, 10)
end)
assert.True(wait())
end)
it('a callable table', function()
async(setmetatable({}, {
__call = function()
setTimeout(function()
done()
end, 10)
end
}))
assert.True(wait())
end)
end)
it('should run immediately', function()
local executor = spy.new(function() end)
async(executor)
assert.spy(executor).was_called()
end)
it('until the first await, executor should run immediately even if in nested function', function()
local value
local executor = spy.new(function() end)
async(function()
value = await(async(function()
return await(async(function()
executor()
return dummy
end))
end))
done()
end)
assert.spy(executor).was_called()
assert.True(wait())
assert.equal(dummy, value)
end)
end)
describe([[await inside async's executor.]], function()
local function testBasicAwait(expectedValue, stringRepresentation)
it('should wait for the promise with resolved value: ' .. stringRepresentation, function()
local value
local p, resolve = deferredPromise()
async(function()
value = await(p)
done()
end)
assert.False(wait(10))
resolve(expectedValue)
assert.True(wait())
assert.equal(expectedValue, value)
end)
end
for valueStr, basicFn in pairs(basics) do
testBasicAwait(basicFn(), valueStr)
end
end)
describe('`pcall` and `xpcall` surround statement or function.', function()
it('call `pcall` to get the value from a single await', function()
local ok, value
local p, resolve = deferredPromise()
async(function()
ok, value = pcall(await, p)
done()
end)
assert.False(wait(10))
resolve(dummy)
assert.True(wait())
assert.True(ok)
assert.equal(dummy, value)
end)
it('call `xpcall` to get the value from a single await', function()
local ok, value
local p, resolve = deferredPromise()
async(function()
ok, value = xpcall(await, function() end, p)
done()
end)
assert.False(wait(10))
resolve(dummy)
assert.True(wait())
assert.True(ok)
assert.equal(dummy, value)
end)
it('call `pcall` to catch the reason from a single await', function()
local ok, reason
local p, _, reject = deferredPromise()
async(function()
ok, reason = pcall(await, p)
done()
end)
assert.False(wait(10))
reject(dummy)
assert.True(wait())
assert.False(ok)
assert.equal(dummy, reason)
end)
it('call `xpcall` to catch the reason from a single await', function()
local ok, reason
local p, _, reject = deferredPromise()
async(function()
ok = xpcall(await, function(e)
reason = e
end, p)
done()
end)
assert.False(wait(10))
reject(dummy)
assert.True(wait())
assert.False(ok)
assert.equal(dummy, reason)
end)
describe('call `pcall` to catch the reason from a function,', function()
it('throw error after the result return by await', function()
local ok, value, reason
local p1, resolve = deferredPromise()
local p2, _, reject = deferredPromise()
async(function()
ok, reason = pcall(function()
value = await(p1)
await(p2)
end)
done()
end)
p1:thenCall(function()
reject(dummy)
end)
assert.False(wait(10))
setTimeout(function()
resolve(other)
end, 20)
assert.True(wait())
assert.False(ok)
assert.equal(other, value)
assert.equal(dummy, reason)
end)
it('throw error before the result return by await', function()
local ok, value, reason
local p1, _, reject = deferredPromise()
local p2, resolve = deferredPromise()
async(function()
ok, reason = pcall(function()
await(p1)
value = await(p2)
end)
done()
end)
resolve(other)
assert.False(wait(10))
setTimeout(function()
reject(dummy)
end, 20)
assert.True(wait())
assert.False(ok)
assert.equal(nil, value)
assert.equal(dummy, reason)
end)
end)
describe('call `xpcall` to catch the reason from a function,', function()
it('throw error after the result return by await', function()
local ok, value, reason
local p1, resolve = deferredPromise()
local p2, _, reject = deferredPromise()
async(function()
ok = xpcall(function()
value = await(p1)
await(p2)
end, function(e)
reason = e
end)
done()
end)
p1:thenCall(function()
reject(dummy)
end)
assert.False(wait(10))
setTimeout(function()
resolve(other)
end, 20)
assert.True(wait())
assert.False(ok)
assert.equal(other, value)
assert.equal(dummy, reason)
end)
it('throw error before the result return by await', function()
local ok, value, reason
local p1, _, reject = deferredPromise()
local p2, resolve = deferredPromise()
async(function()
ok = xpcall(function()
await(p1)
value = await(p2)
end, function(e)
reason = e
end)
done()
end)
resolve(other)
assert.False(wait(10))
setTimeout(function()
reject(dummy)
end, 20)
assert.True(wait())
assert.False(ok)
assert.equal(nil, value)
assert.equal(dummy, reason)
end)
end)
end)
describe('nested async functions.', function()
it('simple call chain', function()
local value
async(function()
value = await(async(function()
return await(async(function()
return dummy
end))
end))
done()
end)
assert.True(wait())
assert.equal(dummy, value)
end)
it('deferred call', function()
local value
setTimeout(function()
async(function()
value = await(async(function()
return await(async(function()
return sentinel
end))
end))
done()
end)
end, 10)
assert.True(wait())
assert.equal(sentinel, value)
end)
it('return multiple values', function()
local value
async(function()
value = compat.pack(await(async(function()
return await(async(function()
return sentinel, sentinel2, sentinel3
end))
end)))
done()
end)
assert.True(wait())
assert.same({sentinel, sentinel2, sentinel3, n = 3}, value)
end)
it('should catch error from the deepest callee', function()
local ok, value, reason
async(function()
ok = xpcall(function()
value = await(async(function()
return await(async(function()
error(dummy)
return other
end))
end))
end, function(e)
reason = e
end)
done()
end)
assert.True(wait())
assert.False(ok)
assert.equal(nil, value)
assert.equal(dummy, reason)
end)
it('should wait for the deepest callee', function()
local value
local p, resolve = deferredPromise()
async(function()
value = await(async(function()
return await(async(function()
return await(p)
end))
end))
done()
end)
resolve(dummy)
assert.True(wait())
assert.equal(dummy, value)
end)
it('should wait for all callees', function()
local value
local p1, resolve1 = deferredPromise()
local p2, resolve2 = deferredPromise()
local p3, resolve3 = deferredPromise()
async(function()
value = compat.pack(await(async(function()
return await(p1), await(async(function()
return await(p2), await(p3)
end))
end)))
done()
end)
assert.False(wait(10))
resolve3(sentinel3)
assert.False(wait(10))
resolve2(sentinel2)
assert.False(wait(10))
resolve1(sentinel)
assert.True(wait())
assert.same({sentinel, sentinel2, sentinel3, n = 3}, value)
end)
end)
end)

50
spec/fixtures.lua Normal file
View file

@ -0,0 +1,50 @@
local busted = require('busted')
local _done
local co = _G.co
busted.subscribe({'test', 'start'}, function()
_done = false
end)
local function getDone()
return _done
end
function _G.done()
_done = true
return _done
end
---------------------------------------------------------------------
---Need to implement _G.wait to pass tests
local defaultTimeout = 1000
---Should override this function to customize EventLoop to pass tests
---@param ms? number
---@return boolean
function _G.wait(ms)
if getDone() then
return true
end
local interval = 5
local timer = require('luv').new_timer()
local cnt = 0
ms = ms or defaultTimeout
timer:start(interval, interval, function()
cnt = cnt + interval
local d = getDone()
if cnt >= ms or d then
timer:stop()
timer:close()
local thread = coroutine.running()
if thread ~= co then
require('spec.helpers.init').setTimeout(function()
coroutine.resume(co, d)
end, 0)
end
end
end)
return coroutine.yield()
end

30
spec/helpers/basics.lua Normal file
View file

@ -0,0 +1,30 @@
local Basic = {}
local co = coroutine.create(function() end)
coroutine.resume(co)
Basic['`nil`'] = function()
return nil
end
Basic['`false`'] = function()
return false
end
Basic['`0`'] = function()
return 0
end
Basic['`string`'] = function()
return 'string'
end
Basic['a metatable'] = function()
return setmetatable({}, {})
end
Basic['a thread'] = function()
return co
end
return Basic

58
spec/helpers/init.lua Normal file
View file

@ -0,0 +1,58 @@
local M = {}
local promise = require('promise')
M.setTimeout = promise.loop.setTimeout
function M.deferredPromise()
local resolve, reject
local p = promise(function(resolve0, reject0)
resolve, reject = resolve0, reject0
end)
return p, resolve, reject
end
function M.testFulfilled(it, assert, value, test)
it('already-fulfilled', function()
test(promise.resolve(value))
assert.True(wait())
end)
it('immediately-fulfilled', function()
local p, resolve = M.deferredPromise()
test(p)
resolve(value)
assert.True(wait())
end)
it('eventually-fulfilled', function()
local p, resolve = M.deferredPromise()
test(p)
wait(10)
resolve(value)
assert.True(wait())
end)
end
function M.testRejected(it, assert, reason, test)
it('already-rejected', function()
test(promise.reject(reason))
assert.True(wait())
end)
it('immediately-rejected', function()
local p, _, reject = M.deferredPromise()
test(p)
reject(reason)
assert.True(wait())
end)
it('eventually-fulfilled', function()
local p, _, reject = M.deferredPromise()
test(p)
wait(10)
reject(reason)
assert.True(wait())
end)
end
return M

View file

@ -0,0 +1,29 @@
return function(options)
local busted = require('busted')
local handler = require('busted.outputHandlers.utfTerminal')(options)
local promiseUnhandledError = {}
busted.subscribe({'test', 'end'}, function(element, parent)
while #promiseUnhandledError > 0 do
local res = table.remove(promiseUnhandledError, 1)
handler.successesCount = handler.successesCount - 1
handler.failuresCount = handler.failuresCount + 1
busted.publish({'failure', element.descriptor}, element, parent, tostring(res))
end
end)
require('promise').loop.callWrapper = function(callback)
local ok, res = pcall(callback)
if ok then
return
end
-- Some tests never handle the rejected promises, We should ignore them.
local msg = tostring(res)
if msg:match('^UnhandledPromiseRejection') then
return
end
table.insert(promiseUnhandledError, msg)
end
return handler
end

37
spec/helpers/reasons.lua Normal file
View file

@ -0,0 +1,37 @@
local promise = require('promise')
local Reasons = {}
local dummy = {dummy = 'dummy'}
Reasons['`nil`'] = function()
return nil
end
Reasons['`false`'] = function()
return false
end
-- Lua before 5.3 versions will transfer number to string after pcall.
-- Pure string will carry some extra information after pcall, no need to test
-- Reasons['`0`'] = function()
-- return 0
-- end
Reasons['a metatable'] = function()
return setmetatable({}, {})
end
Reasons['an always-pending thenable'] = function()
return {
thenCall = function() end
}
end
Reasons['a fulfilled promise'] = function()
return promise.resolve(dummy)
end
Reasons['a rejected promise'] = function()
return promise.reject(dummy)
end
return Reasons

137
spec/helpers/thenables.lua Normal file
View file

@ -0,0 +1,137 @@
local helpers = require('spec.helpers.init')
local setTimeout = helpers.setTimeout
local deferredPromise = helpers.deferredPromise
local promise = require('promise')
local other = {other = 'other'}
local Thenables = {
fulfilled = {
['a synchronously-fulfilled custom thenable'] = function(value)
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(value)
end
}
end,
['an asynchronously-fulfilled custom thenable'] = function(value)
return {
thenCall = function(self, resolvePromise)
local _ = self
setTimeout(function()
resolvePromise(value)
end, 0)
end
}
end,
['a synchronously-fulfilled one-time thenable'] = function(value)
local numberOfTimesThenRetrieved = 0;
return setmetatable({}, {
__index = function(_, k)
if numberOfTimesThenRetrieved == 0 and k == 'thenCall' then
numberOfTimesThenRetrieved = numberOfTimesThenRetrieved + 1
return function(self, resolvePromise)
local _ = self
resolvePromise(value)
end
end
return nil
end
})
end,
['a thenable that tries to fulfill twice'] = function(value)
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(value)
resolvePromise(other)
end
}
end,
['a thenable that fulfills but then throws'] = function(value)
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(value)
error(other)
end
}
end,
['an already-fulfilled promise'] = function(value)
return promise.resolve(value)
end,
['an eventually-fulfilled promise'] = function(value)
local p, resolve = deferredPromise()
setTimeout(function()
resolve(value)
end, 10)
return p
end
},
rejected = {
['a synchronously-rejected custom thenable'] = function(reason)
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
rejectPromise(reason)
end
}
end,
['an asynchronously-rejected custom thenable'] = function(reason)
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
setTimeout(function()
rejectPromise(reason)
end, 0)
end
}
end,
['a synchronously-rejected one-time thenable'] = function(reason)
local numberOfTimesThenRetrieved = 0;
return setmetatable({}, {
__index = function(_, k)
if numberOfTimesThenRetrieved == 0 and k == 'thenCall' then
numberOfTimesThenRetrieved = numberOfTimesThenRetrieved + 1
return function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
rejectPromise(reason)
end
end
return nil
end
})
end,
['a thenable that immediately throws in `thenCall`'] = function(reason)
return {
thenCall = function()
error(reason)
end
}
end,
['an table with a throwing `thenCall` metatable'] = function(reason)
return setmetatable({}, {
__index = function(_, k)
if k == 'thenCall' then
return function()
error(reason)
end
end
return nil
end
})
end,
['an already-rejected promise'] = function(reason)
return promise.reject(reason)
end,
['an eventually-rejected promise'] = function(reason)
local p, _, reject = deferredPromise()
setTimeout(function()
reject(reason)
end, 10)
return p
end
}
}
return Thenables

54
spec/init.lua Normal file
View file

@ -0,0 +1,54 @@
package.path = os.getenv('PWD') .. '/lua/?.lua;' .. package.path
local compat = require('promise-async.compat')
if compat.is51() then
_G.pcall = compat.pcall
_G.xpcall = compat.xpcall
end
local uv = require('luv')
local co = coroutine.create(function()
require('busted.runner')({standalone = false, output = 'spec.helpers.outputHandler'})
-- no errors for nvim
if vim then
vim.schedule(function()
vim.cmd('cq 0')
end)
end
end)
_G.co = co
local compatibility = require('busted.compatibility')
if vim then
compatibility.exit = function(code)
vim.schedule(function()
vim.cmd(('cq %d'):format(code))
end)
end
_G.arg = vim.fn.argv()
_G.print = function(...)
local argv = {...}
for i = 1, #argv do
argv[i] = tostring(argv[i])
end
table.insert(argv, '\n')
io.write(unpack(argv))
end
coroutine.resume(co)
else
local c = 0
-- https://github.com/luvit/luv/issues/599
compatibility.exit = function(code)
c = code
end
local idle = uv.new_idle()
idle:start(function()
idle:stop()
coroutine.resume(co)
end)
uv.run()
os.exit(c)
end

84
spec/loop_spec.lua Normal file
View file

@ -0,0 +1,84 @@
local loop = require('promise').loop
local function testAsynchronousFunction(name)
it(name .. 'is asynchronous', function()
local called = spy.new(function() end)
loop[name](function()
called()
done()
end, 0)
assert.spy(called).was_not_called()
assert.True(wait())
assert.spy(called).was_called()
end)
end
describe('EventLoop for Promise.', function()
testAsynchronousFunction('setTimeout')
testAsynchronousFunction('nextTick')
testAsynchronousFunction('nextIdle')
describe('fire `nextIdle` is later than `nextTick`,', function()
it('call `nextTick` first', function()
local queue = {}
local tick = {'tick'}
local idle = {'idle'}
loop.nextTick(function()
table.insert(queue, tick)
end)
loop.nextIdle(function()
table.insert(queue, idle)
end)
loop.setTimeout(done, 50)
assert.True(wait())
assert.same(tick, queue[1])
assert.same(idle, queue[2])
end)
it('call `nextIdle` first', function()
local queue = {}
local tick = {'tick'}
local idle = {'idle'}
loop.nextIdle(function()
table.insert(queue, idle)
end)
loop.nextTick(function()
table.insert(queue, tick)
end)
loop.setTimeout(done, 50)
assert.True(wait())
assert.same(tick, queue[1])
assert.same(idle, queue[2])
end)
end)
it('call `nextTick` in `nextTick` event', function()
local onTick = spy.new(function() end)
local onNextTick = spy.new(function() end)
loop.nextTick(function()
onTick()
loop.nextTick(function()
onNextTick()
assert.spy(onNextTick).was_called()
done()
end)
assert.spy(onTick).was_called()
assert.spy(onNextTick).was_not_called()
end)
assert.True(wait())
end)
it('override callWrapper method', function()
local rawCallWrapper = loop.callWrapper
local callback = function() end
loop.callWrapper = function(fn)
loop.callWrapper = rawCallWrapper
assert.same(callback, fn)
done()
end
loop.nextTick(callback)
assert.True(wait())
loop.callWrapper = rawCallWrapper
end)
end)

View file

@ -0,0 +1,70 @@
local helpers = require('spec.helpers.init')
local setTimeout = helpers.setTimeout
local testFulfilled = helpers.testFulfilled
local deferredPromise = helpers.deferredPromise
local dummy = {dummy = 'dummy'}
describe('2.1.2.1: When fulfilled, a promise: must not transition to any other state.', function()
local onFulfilled, onRejected = spy.new(function() end), spy.new(function() end)
before_each(function()
onFulfilled:clear()
onRejected:clear()
end)
testFulfilled(it, assert, dummy, function(p)
local onFulfilledCalled = false
p:thenCall(function()
onFulfilledCalled = true
end, function()
assert.False(onFulfilledCalled)
done()
end)
setTimeout(function()
done()
end, 50)
end)
it('trying to fulfill then immediately reject', function()
local p, resolve, reject = deferredPromise()
p:thenCall(onFulfilled, onRejected)
resolve(dummy)
reject(dummy)
setTimeout(function()
done()
end, 50)
assert.True(wait())
assert.spy(onFulfilled).was_called()
assert.spy(onRejected).was_not_called()
end)
it('trying to fulfill then reject, delayed', function()
local p, resolve, reject = deferredPromise()
p:thenCall(onFulfilled, onRejected)
resolve(dummy)
setTimeout(function()
reject(dummy)
done()
end, 50)
assert.True(wait())
assert.spy(onFulfilled).was_called()
assert.spy(onRejected).was_not_called()
end)
it('trying to fulfill immediately then reject, delayed', function()
local p, resolve, reject = deferredPromise()
p:thenCall(onFulfilled, onRejected)
setTimeout(function()
resolve(dummy)
reject(dummy)
done()
end, 50)
assert.True(wait())
assert.spy(onFulfilled).was_called()
assert.spy(onRejected).was_not_called()
end)
end)

View file

@ -0,0 +1,70 @@
local helpers = require('spec.helpers.init')
local testRejected = helpers.testRejected
local deferredPromise = helpers.deferredPromise
local setTimeout = helpers.setTimeout
local dummy = {dummy = 'dummy'}
describe('2.1.3.1: When rejected, a promise: must not transition to any other state.', function()
local onFulfilled, onRejected = spy.new(function() end), spy.new(function() end)
before_each(function()
onFulfilled:clear()
onRejected:clear()
end)
testRejected(it, assert, dummy, function(p)
local onRejectedCalled = false
p:thenCall(function()
assert.False(onRejectedCalled)
done()
end, function()
onRejectedCalled = true
end)
setTimeout(function()
done()
end, 50)
end)
it('trying to reject then immediately fulfill', function()
local p, resolve, reject = deferredPromise()
p:thenCall(onFulfilled, onRejected)
reject(dummy)
resolve(dummy)
setTimeout(function()
done()
end, 50)
assert.True(wait())
assert.spy(onFulfilled).was_not_called()
assert.spy(onRejected).was_called()
end)
it('trying to reject then fulfill, delayed', function()
local p, resolve, reject = deferredPromise()
p:thenCall(onFulfilled, onRejected)
reject(dummy)
setTimeout(function()
resolve(dummy)
done()
end, 50)
assert.True(wait())
assert.spy(onFulfilled).was_not_called()
assert.spy(onRejected).was_called()
end)
it('trying to reject immediately then fulfill, delayed', function()
local p, resolve, reject = deferredPromise()
p:thenCall(onFulfilled, onRejected)
setTimeout(function()
reject(dummy)
resolve(dummy)
done()
end, 50)
assert.True(wait())
assert.spy(onFulfilled).was_not_called()
assert.spy(onRejected).was_called()
end)
end)

View file

@ -0,0 +1,78 @@
local promise = require('promise')
local dummy = {dummy = 'dummy'}
describe('2.2.1: Both `onFulfilled` and `onRejected` are optional arguments.', function()
describe('2.2.1.1: If `onFulfilled` is not a function, it must be ignored.', function()
describe('applied to a directly-rejected promise', function()
local function testNonFunction(nonFunction, stringRepresentation)
it('`onFulfilled` is ' .. stringRepresentation, function()
promise.reject(dummy)
:thenCall(nonFunction, function()
done()
end)
assert.True(wait())
end)
end
testNonFunction(nil, '`nil`')
testNonFunction(false, '`false`')
testNonFunction(5, '`5`')
testNonFunction({}, '`a table`')
end)
describe('applied to a promise rejected and then chained off of', function()
local function testNonFunction(nonFunction, stringRepresentation)
it('`onFulfilled` is ' .. stringRepresentation, function()
promise.reject(dummy)
:thenCall(function() end, nil)
:thenCall(nonFunction, function()
done()
end)
assert.True(wait())
end)
end
testNonFunction(nil, '`nil`')
testNonFunction(false, '`false`')
testNonFunction(5, '`5`')
testNonFunction({}, '`a table`')
end)
end)
describe('2.2.1.2: If `onRejected` is not a function, it must be ignored.', function()
describe('applied to a directly-fulfilled promise', function()
local function testNonFunction(nonFunction, stringRepresentation)
it('`onRejected` is ' .. stringRepresentation, function()
promise.resolve(dummy)
:thenCall(function()
done()
end, nonFunction)
assert.True(wait())
end)
end
testNonFunction(nil, '`nil`')
testNonFunction(false, '`false`')
testNonFunction(5, '`5`')
testNonFunction({}, '`a table`')
end)
describe('applied to a promise fulfilled and then chained off of', function()
local function testNonFunction(nonFunction, stringRepresentation)
it('`onRejected` is ' .. stringRepresentation, function()
promise.resolve(dummy)
:thenCall(nil, function() end)
:thenCall(function()
done()
end, nonFunction)
assert.True(wait())
end)
end
testNonFunction(nil, '`nil`')
testNonFunction(false, '`false`')
testNonFunction(5, '`5`')
testNonFunction({}, '`a table`')
end)
end)
end)

View file

@ -0,0 +1,122 @@
local helpers = require('spec.helpers.init')
local testFulfilled = helpers.testFulfilled
local setTimeout = helpers.setTimeout
local deferredPromise = helpers.deferredPromise
local promise = require('promise')
local dummy = {dummy = 'dummy'}
local sentinel = {sentinel = 'sentinel'}
describe('2.2.2: If `onFulfilled` is a function,', function()
describe('2.2.2.1: it must be called after `promise` is fulfilled, ' ..
'with `promise`s fulfillment value as its first argument.', function()
testFulfilled(it, assert, sentinel, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('2.2.2.2: it must not be called before `promise` is fulfilled', function()
it('fulfilled after a delay', function()
local onFulfilled = spy.new(done)
local p, resolve = deferredPromise()
p:thenCall(onFulfilled)
setTimeout(function()
resolve(dummy)
end, 10)
assert.True(wait())
assert.spy(onFulfilled).was_called(1)
end)
it('never fulfilled', function()
local onFulfilled = spy.new(done)
local p = deferredPromise()
p:thenCall(onFulfilled)
assert.False(wait(30))
assert.spy(onFulfilled).was_not_called()
end)
end)
describe('2.2.2.3: it must not be called more than once.', function()
it('already-fulfilled', function()
local onFulfilled = spy.new(done)
promise.resolve(dummy):thenCall(onFulfilled)
assert.spy(onFulfilled).was_not_called()
assert.True(wait())
assert.spy(onFulfilled).was_called(1)
end)
it('trying to fulfill a pending promise more than once, immediately', function()
local onFulfilled = spy.new(done)
local p, resolve = deferredPromise()
p:thenCall(onFulfilled)
resolve(dummy)
resolve(dummy)
assert.True(wait())
assert.spy(onFulfilled).was_called(1)
end)
it('trying to fulfill a pending promise more than once, delayed', function()
local onFulfilled = spy.new(done)
local p, resolve = deferredPromise()
p:thenCall(onFulfilled)
setTimeout(function()
resolve(dummy)
resolve(dummy)
end, 10)
assert.True(wait())
assert.spy(onFulfilled).was_called(1)
end)
it('trying to fulfill a pending promise more than once, immediately then delayed', function()
local onFulfilled = spy.new(done)
local p, resolve = deferredPromise()
p:thenCall(onFulfilled)
resolve(dummy)
setTimeout(function()
resolve(dummy)
end, 10)
assert.True(wait())
assert.spy(onFulfilled).was_called(1)
end)
it('when multiple `thenCall` calls are made, spaced apart in time', function()
local onFulfilled1 = spy.new(function() end)
local onFulfilled2 = spy.new(function() end)
local onFulfilled3 = spy.new(function() end)
local p, resolve = deferredPromise()
p:thenCall(onFulfilled1)
setTimeout(function()
p:thenCall(onFulfilled2)
end, 10)
setTimeout(function()
p:thenCall(onFulfilled3)
end, 20)
setTimeout(function()
resolve(dummy)
done()
end, 30)
assert.True(wait())
assert.spy(onFulfilled1).was_called(1)
assert.spy(onFulfilled2).was_called(1)
assert.spy(onFulfilled3).was_called(1)
end)
it('when `thenCall` is interleaved with fulfillment', function()
local onFulfilled1 = spy.new(function() end)
local onFulfilled2 = spy.new(function() end)
local p, resolve = deferredPromise()
p:thenCall(onFulfilled1)
resolve(dummy)
setTimeout(function()
p:thenCall(onFulfilled2)
done()
end, 10)
assert.True(wait())
assert.spy(onFulfilled1).was_called(1)
assert.spy(onFulfilled2).was_called(1)
end)
end)
end)

View file

@ -0,0 +1,122 @@
local helpers = require('spec.helpers.init')
local testRejected = helpers.testRejected
local setTimeout = helpers.setTimeout
local deferredPromise = helpers.deferredPromise
local promise = require('promise')
local dummy = {dummy = 'dummy'}
local sentinel = {sentinel = 'sentinel'}
describe('2.2.3: If `onRejected` is a function,', function()
describe('2.2.3.1: it must be called after `promise` is rejected, ' ..
'with `promise`s rejection reason as its first argument.', function()
testRejected(it, assert, sentinel, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('2.2.3.2: it must not be called before `promise` is rejected', function()
it('rejected after a delay', function()
local onRejected = spy.new(done)
local p, _, reject = deferredPromise()
p:thenCall(nil, onRejected)
setTimeout(function()
reject(dummy)
end, 10)
assert.True(wait())
assert.spy(onRejected).was_called(1)
end)
it('never rejected', function()
local onRejected = spy.new(done)
local p = deferredPromise()
p:thenCall(nil, onRejected)
assert.False(wait(30))
assert.spy(onRejected).was_not_called()
end)
end)
describe('2.2.3.3: it must not be called more than once.', function()
it('already-rejected', function()
local onRejected = spy.new(done)
promise.reject(dummy):thenCall(nil, onRejected)
assert.spy(onRejected).was_not_called()
assert.True(wait())
assert.spy(onRejected).was_called(1)
end)
it('trying to reject a pending promise more than once, immediately', function()
local onRejected = spy.new(done)
local p, _, reject = deferredPromise()
p:thenCall(nil, onRejected)
reject(dummy)
reject(dummy)
assert.True(wait())
assert.spy(onRejected).was_called(1)
end)
it('trying to reject a pending promise more than once, delayed', function()
local onRejected = spy.new(done)
local p, _, reject = deferredPromise()
p:thenCall(nil, onRejected)
setTimeout(function()
reject(dummy)
reject(dummy)
end, 10)
assert.True(wait())
assert.spy(onRejected).was_called(1)
end)
it('trying to reject a pending promise more than once, immediately then delayed', function()
local onRejected = spy.new(done)
local p, _, reject = deferredPromise()
p:thenCall(nil, onRejected)
reject(dummy)
setTimeout(function()
reject(dummy)
end, 10)
assert.True(wait())
assert.spy(onRejected).was_called(1)
end)
it('when multiple `thenCall` calls are made, spaced apart in time', function()
local onRejected1 = spy.new(function() end)
local onRejected2 = spy.new(function() end)
local onRejected3 = spy.new(function() end)
local p, _, reject = deferredPromise()
p:thenCall(nil, onRejected1)
setTimeout(function()
p:thenCall(nil, onRejected2)
end, 15)
setTimeout(function()
p:thenCall(nil, onRejected3)
end, 25)
setTimeout(function()
reject(dummy)
done()
end, 35)
assert.True(wait())
assert.spy(onRejected1).was_called(1)
assert.spy(onRejected2).was_called(1)
assert.spy(onRejected3).was_called(1)
end)
it('when `thenCall` is interleaved with rejection', function()
local onRejected1 = spy.new(function() end)
local onRejected2 = spy.new(function() end)
local p, _, reject = deferredPromise()
p:thenCall(nil, onRejected1)
reject(dummy)
setTimeout(function()
p:thenCall(nil, onRejected2)
done()
end, 10)
assert.True(wait())
assert.spy(onRejected1).was_called(1)
assert.spy(onRejected2).was_called(1)
end)
end)
end)

View file

@ -0,0 +1,128 @@
local helpers = require('spec.helpers.init')
local testFulfilled = helpers.testFulfilled
local testRejected = helpers.testRejected
local setTimeout = helpers.setTimeout
local deferredPromise = helpers.deferredPromise
local promise = require('promise')
local dummy = {dummy = 'dummy'}
describe('2.2.4: `onFulfilled` or `onRejected` must not be called until ' ..
'the execution context stack contains only platform code.', function()
describe('`thenCall` returns before the promise becomes fulfilled or rejected', function()
testFulfilled(it, assert, dummy, function(p)
local onFulfilled = spy.new(done)
p:thenCall(onFulfilled)
end)
testRejected(it, assert, dummy, function(p)
local onRejected = spy.new(done)
p:thenCall(nil, onRejected)
end)
end)
describe('Clean-stack execution ordering tests (fulfillment case)', function()
local onFulfilled = spy.new(done)
before_each(function()
onFulfilled:clear()
end)
it('when `onFulfilled` is added immediately before the promise is fulfilled', function()
local p, resolve = deferredPromise()
p:thenCall(onFulfilled)
resolve(dummy)
assert.True(wait())
assert.spy(onFulfilled).was_called(1)
end)
it('when `onFulfilled` is added immediately after the promise is fulfilled', function()
local p, resolve = deferredPromise()
resolve(dummy)
p:thenCall(onFulfilled)
assert.True(wait())
assert.spy(onFulfilled).was_called(1)
end)
it('when one `onFulfilled` is added inside another `onFulfilled`', function()
local p = promise.resolve()
p:thenCall(function()
p:thenCall(onFulfilled)
end)
assert.True(wait())
assert.spy(onFulfilled).was_called(1)
end)
it('when `onFulfilled` is added inside an `onRejected`', function()
local p1 = promise.reject()
local p2 = promise.resolve()
p1:thenCall(nil, function()
p2:thenCall(onFulfilled)
end)
assert.True(wait())
assert.spy(onFulfilled).was_called(1)
end)
it('when the promise is fulfilled asynchronously', function()
local p, resolve = deferredPromise()
setTimeout(function()
resolve(dummy)
end, 0)
p:thenCall(onFulfilled)
assert.True(wait())
assert.spy(onFulfilled).was_called(1)
end)
end)
describe('Clean-stack execution ordering tests (rejection case)', function()
local onRejected = spy.new(done)
before_each(function()
onRejected:clear()
end)
it('when `onRejected` is added immediately before the promise is rejected', function()
local p, _, reject = deferredPromise()
p:thenCall(nil, onRejected)
reject(dummy)
assert.True(wait())
assert.spy(onRejected).was_called(1)
end)
it('when `onRejected` is added immediately after the promise is rejected', function()
local p, _, reject = deferredPromise()
reject(dummy)
p:thenCall(nil, onRejected)
assert.True(wait())
assert.spy(onRejected).was_called(1)
end)
it('when `onRejected` is added inside an `onFulfilled`', function()
local p1 = promise.resolve()
local p2 = promise.reject()
p1:thenCall(function()
p2:thenCall(nil, onRejected)
end)
assert.True(wait())
assert.spy(onRejected).was_called(1)
end)
it('when one `onRejected` is added inside another `onRejected`', function()
local p = promise.reject()
p:thenCall(nil, function()
p:thenCall(nil, onRejected)
end)
assert.True(wait())
assert.spy(onRejected).was_called(1)
end)
it('when the promise is rejected asynchronously', function()
local p, _, reject = deferredPromise()
setTimeout(function()
reject(dummy)
end, 0)
p:thenCall(nil, onRejected)
assert.True(wait())
assert.spy(onRejected).was_called(1)
end)
end)
end)

View file

@ -0,0 +1,295 @@
local helpers = require('spec.helpers.init')
local testFulfilled = helpers.testFulfilled
local testRejected = helpers.testRejected
local setTimeout = helpers.setTimeout
local dummy = {dummy = 'dummy'}
local sentinel = {sentinel = 'sentinel'}
local sentinel2 = {sentinel = 'sentinel2'}
local sentinel3 = {sentinel = 'sentinel3'}
describe('2.2.6: `thenCall` may be called multiple times on the same promise.', function()
local function callbackAggregator(times, ultimateCallback)
local soFar = 0
return function()
soFar = soFar + 1
if soFar == times then
ultimateCallback()
end
end
end
describe('2.2.6.1: If/when `promise` is fulfilled, all respective `onFulfilled` callbacks ' ..
'must execute in the order of their originating calls to `thenCall`.', function()
describe('multiple boring fulfillment handlers', function()
testFulfilled(it, assert, sentinel, function(p)
local onFulfilled1 = spy.new(function() end)
local onFulfilled2 = spy.new(function() end)
local onFulfilled3 = spy.new(function() end)
local onRejected = spy.new(function() end)
p:thenCall(onFulfilled1, onRejected)
p:thenCall(onFulfilled2, onRejected)
p:thenCall(onFulfilled3, onRejected)
p:thenCall(function(value)
assert.equal(sentinel, value)
assert.spy(onFulfilled1).was_called_with(sentinel)
assert.spy(onFulfilled2).was_called_with(sentinel)
assert.spy(onFulfilled3).was_called_with(sentinel)
assert.spy(onRejected).was_not_called()
done()
end)
end)
end)
describe('multiple fulfillment handlers, one of which throws', function()
testFulfilled(it, assert, sentinel, function(p)
local onFulfilled1 = spy.new(function() end)
local onFulfilled2 = spy.new(function()
error()
end)
local onFulfilled3 = spy.new(function() end)
local onRejected = spy.new(function() end)
p:thenCall(onFulfilled1, onRejected)
p:thenCall(onFulfilled2, onRejected):catch(function() end)
p:thenCall(onFulfilled3, onRejected)
p:thenCall(function(value)
assert.equal(sentinel, value)
assert.spy(onFulfilled1).was_called_with(sentinel)
assert.spy(onFulfilled2).was_called_with(sentinel)
assert.spy(onFulfilled3).was_called_with(sentinel)
assert.spy(onRejected).was_not_called()
done()
end)
end)
end)
describe('results in multiple branching chains with their own fulfillment values', function()
testFulfilled(it, assert, dummy, function(p)
local semiDone = callbackAggregator(3, function()
done()
end)
p:thenCall(function()
return sentinel
end):thenCall(function(value)
assert.equal(sentinel, value)
semiDone()
end)
p:thenCall(function()
error(sentinel2)
end):thenCall(nil, function(reason)
assert.equal(sentinel2, reason)
semiDone()
end)
p:thenCall(function()
return sentinel3
end):thenCall(function(value)
assert.equal(sentinel3, value)
semiDone()
end)
end)
end)
describe('`onFulfilled` handlers are called in the original order', function()
local queue = {}
local function enQueue(value)
table.insert(queue, value)
end
before_each(function()
queue = {}
end)
testFulfilled(it, assert, dummy, function(p)
local function onFulfilled1()
enQueue(1)
end
local function onFulfilled2()
enQueue(2)
end
local function onFulfilled3()
enQueue(3)
end
p:thenCall(onFulfilled1)
p:thenCall(onFulfilled2)
p:thenCall(onFulfilled3)
p:thenCall(function()
assert.same({1, 2, 3}, queue)
done()
end)
end)
describe('even when one handler is added inside another handler', function()
testFulfilled(it, assert, dummy, function(p)
local function onFulfilled1()
enQueue(1)
end
local function onFulfilled2()
enQueue(2)
end
local function onFulfilled3()
enQueue(3)
end
p:thenCall(function()
onFulfilled1()
p:thenCall(onFulfilled3)
end)
p:thenCall(onFulfilled2)
p:thenCall(function()
setTimeout(function()
assert.same({1, 2, 3}, queue)
done()
end, 10)
end)
end)
end)
end)
end)
describe('2.2.6.2: If/when `promise` is rejected, all respective `onRejected` callbacks ' ..
'must execute in the order of their originating calls to `thenCall`.', function()
describe('multiple boring rejection handlers', function()
testRejected(it, assert, sentinel, function(p)
local onFulfilled = spy.new(function() end)
local onRejected1 = spy.new(function() end)
local onRejected2 = spy.new(function() end)
local onRejected3 = spy.new(function() end)
p:thenCall(onFulfilled, onRejected1)
p:thenCall(onFulfilled, onRejected2)
p:thenCall(onFulfilled, onRejected3)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
assert.spy(onRejected1).was_called_with(sentinel)
assert.spy(onRejected2).was_called_with(sentinel)
assert.spy(onRejected3).was_called_with(sentinel)
assert.spy(onFulfilled).was_not_called()
done()
end)
end)
end)
describe('multiple rejection handlers, one of which throws', function()
testRejected(it, assert, sentinel, function(p)
local onFulfilled = spy.new(function() end)
local onRejected1 = spy.new(function() end)
local onRejected2 = spy.new(function()
error()
end)
local onRejected3 = spy.new(function() end)
p:thenCall(onFulfilled, onRejected1)
p:thenCall(onFulfilled, onRejected2):catch(function() end)
p:thenCall(onFulfilled, onRejected3)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
assert.spy(onRejected1).was_called_with(sentinel)
assert.spy(onRejected2).was_called_with(sentinel)
assert.spy(onRejected3).was_called_with(sentinel)
assert.spy(onFulfilled).was_not_called()
done()
end)
end)
end)
describe('results in multiple branching chains with their own rejection values', function()
testRejected(it, assert, dummy, function(p)
local semiDone = callbackAggregator(3, function()
done()
end)
p:thenCall(nil, function()
return sentinel
end):thenCall(function(value)
assert.equal(sentinel, value)
semiDone()
end)
p:thenCall(nil, function()
error(sentinel2)
end):thenCall(nil, function(reason)
assert.equal(sentinel2, reason)
semiDone()
end)
p:thenCall(nil, function()
return sentinel3
end):thenCall(function(value)
assert.equal(sentinel3, value)
semiDone()
end)
end)
end)
describe('`onRejected` handlers are called in the original order', function()
local queue = {}
local function enQueue(value)
table.insert(queue, value)
end
before_each(function()
queue = {}
end)
testRejected(it, assert, dummy, function(p)
local function onRejected1()
enQueue(1)
end
local function onRejected2()
enQueue(2)
end
local function onRejected3()
enQueue(3)
end
p:thenCall(nil, onRejected1)
p:thenCall(nil, onRejected2)
p:thenCall(nil, onRejected3)
p:thenCall(nil, function()
assert.same({1, 2, 3}, queue)
done()
end)
end)
describe('even when one handler is added inside another handler', function()
testRejected(it, assert, dummy, function(p)
local function onRejected1()
enQueue(1)
end
local function onRejected2()
enQueue(2)
end
local function onRejected3()
enQueue(3)
end
p:thenCall(nil, function()
onRejected1()
p:thenCall(nil, onRejected3)
end)
p:thenCall(nil, onRejected2)
p:thenCall(nil, function()
setTimeout(function()
assert.same({1, 2, 3}, queue)
done()
end, 15)
end)
end)
end)
end)
end)
end)

View file

@ -0,0 +1,98 @@
local helpers = require('spec.helpers.init')
local testFulfilled = helpers.testFulfilled
local testRejected = helpers.testRejected
local deferredPromise = helpers.deferredPromise
local dummy = {dummy = 'dummy'}
local sentinel = {sentinel = 'sentinel'}
local other = {other = 'other'}
local reasons = require('spec.helpers.reasons')
describe('2.2.7: `thenCall` must return a promise: ' ..
'`promise2 = promise1.thenCall(onFulfilled, onRejected)', function()
it('is a promise', function()
local p1 = deferredPromise()
local p2 = p1:thenCall()
assert.True(type(p2) == 'table' or type(p2) == 'function')
assert.is.not_equal(p2, nil)
assert.equal('function', type(p2.thenCall))
end)
describe('2.2.7.1: If either `onFulfilled` or `onRejected` returns a value `x`, ' ..
'run the Promise Resolution Procedure `[[Resolve]](promise2, x)`', function()
it('see separate 3.3 tests', function()
end)
end)
describe('2.2.7.2: If either `onFulfilled` or `onRejected` throws an exception `e`, ' ..
'`promise2` must be rejected with `e` as the reason.', function()
local function testReason(expectedReason, stringRepresentation)
describe('The reason is ' .. stringRepresentation, function()
testFulfilled(it, assert, dummy, function(p1)
local p2 = p1:thenCall(function()
error(expectedReason)
end)
p2:thenCall(nil, function(actualReason)
assert.equal(expectedReason, actualReason)
done()
end)
end)
testRejected(it, assert, dummy, function(p1)
local p2 = p1:thenCall(nil, function()
error(expectedReason)
end)
p2:thenCall(nil, function(actualReason)
assert.equal(expectedReason, actualReason)
done()
end)
end)
end)
end
for reasonStr, reason in pairs(reasons) do
testReason(reason(), reasonStr)
end
end)
describe('2.2.7.3: If `onFulfilled` is not a function and `promise1` is fulfilled, ' ..
'`promise2` must be fulfilled with the same value', function()
local function testNonFunction(nonFunction, stringRepresentation)
describe('`onFulfilled` is' .. stringRepresentation, function()
testFulfilled(it, assert, sentinel, function(p1)
local p2 = p1:thenCall(nonFunction)
p2:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
end
testNonFunction(nil, '`nil`')
testNonFunction(false, '`false`')
testNonFunction(5, '`5`')
testNonFunction(setmetatable({}, {}), 'a metatable')
testNonFunction({function() return other end}, 'an table containing a function')
end)
describe('2.2.7.4: If `onRejected` is not a function and `promise1` is rejected, ' ..
'`promise2` must be rejected with the same reason', function()
local function testNonFunction(nonFunction, stringRepresentation)
describe('`onRejected` is' .. stringRepresentation, function()
testRejected(it, assert, sentinel, function(p1)
local p2 = p1:thenCall(nonFunction)
p2:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
end
testNonFunction(nil, '`nil`')
testNonFunction(false, '`false`')
testNonFunction(5, '`5`')
testNonFunction(setmetatable({}, {}), 'a metatable')
testNonFunction({function() return other end}, 'an table containing a function')
end)
end)

View file

@ -0,0 +1,31 @@
local promise = require('promise')
local dummy = {dummy = 'dummy'}
describe('2.3.1: If `promise` and `x` refer to the same object, reject `promise` with a ' ..
'`TypeError` as the reason.', function()
it('via return from a fulfilled promise', function()
local p
p = promise.resolve(dummy):thenCall(function()
return p
end)
p:thenCall(nil, function(reason)
assert.truthy(reason:match('^TypeError'))
done()
end)
assert.True(wait())
end)
it('via return from a rejected promise', function()
local p
p = promise.reject(dummy):thenCall(nil, function()
return p
end)
p:thenCall(nil, function(reason)
assert.truthy(reason:match('^TypeError'))
done()
end)
assert.True(wait())
end)
end)

View file

@ -0,0 +1,97 @@
local helpers = require('spec.helpers.init')
local setTimeout = helpers.setTimeout
local deferredPromise = helpers.deferredPromise
local promise = require('promise')
local dummy = {dummy = 'dummy'}
local sentinel = {sentinel = 'sentinel'}
local function testPromiseResolution(xFactory, test)
it('via return from a fulfilled promise', function()
local p = promise.resolve(dummy):thenCall(function()
return xFactory()
end)
test(p)
assert.True(wait())
end)
it('via return from a rejected promise', function()
local p = promise.reject(dummy):thenCall(nil, function()
return xFactory()
end)
test(p)
assert.True(wait())
end)
end
describe('2.3.2: If `x` is a promise, adopt its state', function()
describe('2.3.2.1: If `x` is pending, `promise` must remain pending until `x` is ' ..
'fulfilled or rejected.', function()
testPromiseResolution(function()
return deferredPromise()
end, function(p)
local onFulfilled = spy.new(function() end)
local onRejected = spy.new(function() end)
p:thenCall(onFulfilled, onRejected)
assert.spy(onFulfilled).was_not_called()
assert.spy(onRejected).was_not_called()
done()
end)
end)
describe('2.3.2.2: If/when `x` is fulfilled, fulfill `promise` with the same value.', function()
describe('`x` is already-fulfilled', function()
testPromiseResolution(function()
return promise.resolve(sentinel)
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('`x` is eventually-fulfilled', function()
testPromiseResolution(function()
local p, resolve = deferredPromise()
setTimeout(function()
resolve(sentinel)
end, 10)
return p
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
end)
describe('2.3.2.3: If/when `x` is rejected, reject `promise` with the same reason.', function()
describe('`x` is already-rejected', function()
testPromiseResolution(function()
return promise.reject(sentinel)
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('`x` is eventually-rejected', function()
testPromiseResolution(function()
local p, _, reject = deferredPromise()
setTimeout(function()
reject(sentinel)
end, 10)
return p
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
end)
end)

View file

@ -0,0 +1,800 @@
local helpers = require('spec.helpers.init')
local deferredPromise = helpers.deferredPromise
local promise = require('promise')
local setTimeout = helpers.setTimeout
local reasons = require('spec.helpers.reasons')
local dummy = {dummy = 'dummy'}
local sentinel = {sentinel = 'sentinel'}
local other = {other = 'other'}
local thenables = require('spec.helpers.thenables')
local function testPromiseResolution(xFactory, test)
it('via return from a fulfilled promise', function()
local p = promise.resolve(dummy):thenCall(function()
return xFactory()
end)
test(p)
assert.True(wait())
end)
it('via return from a rejected promise', function()
local p = promise.reject(dummy):thenCall(nil, function()
return xFactory()
end)
test(p)
assert.True(wait())
end)
end
describe('2.3.3: Otherwise, if `x` is a table or function,', function()
describe('2.3.3.1: Let `thenCall` be `x.thenCall`', function()
describe('`x` is a table', function()
local thenCallRetrieved = spy.new(function() end)
before_each(function()
thenCallRetrieved:clear()
end)
testPromiseResolution(function()
local x = {}
setmetatable(x, {
__index = function(_, k)
if k == 'thenCall' then
thenCallRetrieved()
return function(_, resolvePromise)
resolvePromise()
end
end
end
})
return x
end, function(p)
p:thenCall(function()
assert.spy(thenCallRetrieved).was_called(1)
done()
end)
end)
end)
describe('2.3.3.2: If retrieving the property `x.thenCall` results in a thrown exception ' ..
'`e`, reject `promise` with `e` as the reason.', function()
local function testRejectionViaThrowingGetter(e, stringRepresentation)
describe('`e` is ' .. stringRepresentation, function()
testPromiseResolution(function()
return {
thenCall = function()
error(e)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(e, reason)
done()
end)
end)
end)
end
for reasonStr, reason in pairs(reasons) do
testRejectionViaThrowingGetter(reason(), reasonStr)
end
end)
describe('2.3.3.3: If `thenCall` is a function, call it with `x` as `self`, first ' ..
'argument `resolvePromise`, and second argument `rejectPromise`', function()
testPromiseResolution(function()
local x
x = {
thenCall = function(self, resolvePromise, rejectPromise)
assert.equal(x, self)
assert.True(type(resolvePromise) == 'function')
assert.True(type(rejectPromise) == 'function')
resolvePromise()
end
}
return x
end, function(p)
p:thenCall(function()
done()
end)
end)
end)
describe('2.3.3.3.1: If/when `resolvePromise` is called with value `y`, ' ..
'run `[[Resolve]](promise, y)`', function()
local function testCallingResolvePromise(yFactory, stringRepresentation, test)
describe('`y` is ' .. stringRepresentation, function()
describe('`thenCall` calls `resolvePromise` synchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(yFactory())
end
}
end, test)
end)
describe('`thenCall` calls `resolvePromise` asynchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise)
local _ = self
setTimeout(function()
resolvePromise(yFactory())
end, 0)
end
}
end, test)
end)
end)
end
local function testCallingResolvePromiseFulfillsWith(yFactory, stringRepresentation,
fulfillmentValue)
testCallingResolvePromise(yFactory, stringRepresentation, function(p)
p:thenCall(function(value)
assert.equal(fulfillmentValue, value)
done()
end)
end)
end
local function testCallingResolvePromiseRejectsWith(yFactory, stringRepresentation,
rejectionReason)
testCallingResolvePromise(yFactory, stringRepresentation, function(p)
p:thenCall(nil, function(reason)
assert.equal(rejectionReason, reason)
done()
end)
end)
end
describe('`y` is not a thenable', function()
testCallingResolvePromiseFulfillsWith(function()
return nil
end, '`null`', nil)
testCallingResolvePromiseFulfillsWith(function()
return false
end, '`false`', false)
testCallingResolvePromiseFulfillsWith(function()
return 5
end, '`5`', 5)
testCallingResolvePromiseFulfillsWith(function()
return sentinel
end, '`an table`', sentinel)
end)
describe('`y` is a thenable', function()
for stringRepresentation, factory in pairs(thenables.fulfilled) do
testCallingResolvePromiseFulfillsWith(function()
return factory(sentinel)
end, stringRepresentation, sentinel)
end
for stringRepresentation, factory in pairs(thenables.rejected) do
testCallingResolvePromiseRejectsWith(function()
return factory(sentinel)
end, stringRepresentation, sentinel)
end
end)
describe('`y` is a thenable for a thenable', function()
for outerString, outerFactory in pairs(thenables.fulfilled) do
for innerString, factory in pairs(thenables.fulfilled) do
local stringRepresentation = outerString .. ' for ' .. innerString
testCallingResolvePromiseFulfillsWith(function()
return outerFactory(factory(sentinel))
end, stringRepresentation, sentinel)
end
for innerString, factory in pairs(thenables.rejected) do
local stringRepresentation = outerString .. ' for ' .. innerString
testCallingResolvePromiseRejectsWith(function()
return outerFactory(factory(sentinel))
end, stringRepresentation, sentinel)
end
end
end)
end)
end)
describe('2.3.3.3.2: If/when `rejectPromise` is called with reason `r`, reject `promise` with `r`', function()
local function testCallingRejectPromise(r, stringRepresentation, test)
describe('`r` is ' .. stringRepresentation, function()
describe('`thenCall` calls `rejectPromise` synchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
rejectPromise(r)
end
}
end, test)
end)
describe('`thenCall` calls `rejectPromise` asynchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
setTimeout(function()
rejectPromise(r)
end, 0)
end
}
end, test)
end)
end)
end
local function testCallingRejectPromiseRejectsWith(rejectionReason, stringRepresentation)
testCallingRejectPromise(rejectionReason, stringRepresentation, function(p)
p:thenCall(nil, function(reason)
assert.equal(rejectionReason, reason)
done()
end)
end)
end
for reasonStr, reason in pairs(reasons) do
testCallingRejectPromiseRejectsWith(reason(), reasonStr)
end
end)
describe('2.3.3.3.3: If both `resolvePromise` and `rejectPromise` are called, or multiple ' ..
'calls to the same argument are made, the first call takes precedence, and any further ' ..
'calls are ignored.', function()
describe('calling `resolvePromise` then `rejectPromise`, both synchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _ = self
resolvePromise(sentinel)
rejectPromise(other)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('calling `resolvePromise` synchronously then `rejectPromise` asynchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _ = self
resolvePromise(sentinel)
setTimeout(function()
rejectPromise(other)
end, 0)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('calling `resolvePromise` then `rejectPromise`, both asynchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _ = self
setTimeout(function()
resolvePromise(sentinel)
end, 0)
setTimeout(function()
rejectPromise(other)
end, 0)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('calling `resolvePromise` with an asynchronously-fulfilled promise, then calling ' ..
'`rejectPromise`, both synchronously', function()
testPromiseResolution(function()
local p, resolve = deferredPromise()
setTimeout(function()
resolve(sentinel)
end, 10)
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _ = self
resolvePromise(p)
rejectPromise(other)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('calling `resolvePromise` with an asynchronously-rejected promise, then calling ' ..
'`rejectPromise`, both synchronously', function()
testPromiseResolution(function()
local p, _, reject = deferredPromise()
setTimeout(function()
reject(sentinel)
end, 10)
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _ = self
resolvePromise(p)
rejectPromise(other)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('calling `rejectPromise` then `resolvePromise`, both synchronously', function()
testPromiseResolution(function()
local p, resolve = deferredPromise()
setTimeout(function()
resolve(sentinel)
end, 10)
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _ = self
resolvePromise(p)
rejectPromise(other)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('calling `rejectPromise` synchronously then `resolvePromise` asynchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
rejectPromise(sentinel)
setTimeout(function()
resolvePromise(other)
end, 0)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('calling `rejectPromise` then `resolvePromise`, both asynchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
setTimeout(function()
rejectPromise(sentinel)
end, 0)
setTimeout(function()
resolvePromise(other)
end, 0)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('calling `resolvePromise` twice synchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(sentinel)
resolvePromise(other)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('calling `resolvePromise` twice, first synchronously then asynchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(sentinel)
setTimeout(function()
resolvePromise(other)
end, 0)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('calling `resolvePromise` twice, both times asynchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise)
local _ = self
setTimeout(function()
resolvePromise(sentinel)
end, 0)
setTimeout(function()
resolvePromise(other)
end, 0)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('calling `resolvePromise` with an asynchronously-fulfilled promise, ' ..
'then calling it again, both times synchronously', function()
testPromiseResolution(function()
local p, resolve = deferredPromise()
setTimeout(function()
resolve(sentinel)
end, 10)
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(p)
resolvePromise(other)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('calling `resolvePromise` with an asynchronously-rejected promise, ' ..
'then calling it again, both times synchronously', function()
testPromiseResolution(function()
local p, _, reject = deferredPromise()
setTimeout(function()
reject(sentinel)
end, 10)
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(p)
resolvePromise(other)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('calling `rejectPromise` twice synchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
rejectPromise(sentinel)
rejectPromise(other)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('calling `resolvePromise` twice, first synchronously then asynchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
rejectPromise(sentinel)
setTimeout(function()
rejectPromise(other)
end, 0)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('calling `rejectPromise` twice, both times asynchronously', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
setTimeout(function()
rejectPromise(sentinel)
end, 0)
setTimeout(function()
rejectPromise(other)
end, 0)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('saving and abusing `resolvePromise` and `rejectPromise`', function()
local savedResolvePromise, savedRejectPromise
before_each(function()
savedResolvePromise, savedRejectPromise = nil, nil
end)
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _ = self
savedResolvePromise, savedRejectPromise = resolvePromise, rejectPromise
end
}
end, function(p)
local onFulfilled, onRejected = spy.new(function() end), spy.new(function() end)
p:thenCall(onFulfilled, onRejected)
if savedResolvePromise and savedRejectPromise then
savedResolvePromise(dummy)
savedResolvePromise(dummy)
savedRejectPromise(dummy)
savedRejectPromise(dummy)
end
setTimeout(function()
savedResolvePromise(dummy)
savedResolvePromise(dummy)
savedRejectPromise(dummy)
savedRejectPromise(dummy)
end, 10)
setTimeout(function()
assert.spy(onFulfilled).was_called(1)
assert.spy(onRejected).was_not_called()
done()
end, 50)
end)
end)
describe('2.3.3.3.4: If calling `thenCall` throws an exception `e`,', function()
describe('2.3.3.3.4.1: If `resolvePromise` or `rejectPromise` have been called, ignore it.', function()
describe('`resolvePromise` was called with a non-thenable', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(sentinel)
error(other)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('`resolvePromise` was called with an asynchronously-fulfilled promise', function()
testPromiseResolution(function()
local p, resolve = deferredPromise()
setTimeout(function()
resolve(sentinel)
end, 10)
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(p)
error(other)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('`resolvePromise` was called with an asynchronously-rejected promise', function()
testPromiseResolution(function()
local p, _, reject = deferredPromise()
setTimeout(function()
reject(sentinel)
end, 10)
return {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(p)
error(other)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('`rejectPromise` was called', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
rejectPromise(sentinel)
error(other)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('`resolvePromise` then `rejectPromise` were called', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _ = self
resolvePromise(sentinel)
rejectPromise(other)
end
}
end, function(p)
p:thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
end)
end)
describe('`rejectPromise` then `resolvePromise` were called', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _ = self
rejectPromise(sentinel)
resolvePromise(other)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
end)
describe('2.3.3.3.4.2: Otherwise, reject `promise` with `e` as the reason.', function()
describe('straightforward case', function()
testPromiseResolution(function()
return {
thenCall = function()
error(sentinel)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
end)
describe('`resolvePromise` is called asynchronously before the `throw`', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise)
local _ = self
setTimeout(function()
resolvePromise(other)
end, 0)
error(sentinel)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
describe('`rejectPromise` is called asynchronously before the `throw`', function()
testPromiseResolution(function()
return {
thenCall = function(self, resolvePromise, rejectPromise)
local _, _ = self, resolvePromise
setTimeout(function()
rejectPromise(other)
end, 0)
error(sentinel)
end
}
end, function(p)
p:thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
end)
end)
end)
end)
describe('2.3.3.4: If `thenCall` is not a function, fulfill promise with `x`', function()
local function testFulfillViaNonFunction(thenCall, stringRepresentation)
local x = nil
before_each(function()
x = {thenCall = thenCall}
end)
describe('thenCall is ' .. stringRepresentation, function()
testPromiseResolution(function()
return x
end, function(p)
p:thenCall(function(value)
assert.equal(x, value)
done()
end)
end)
end)
end
testFulfillViaNonFunction(5, '`5`')
testFulfillViaNonFunction({}, 'a table')
testFulfillViaNonFunction({function() end}, 'a table containing a function')
testFulfillViaNonFunction(setmetatable({}, {}), 'a metatable')
end)
end)

View file

@ -0,0 +1,35 @@
local helpers = require('spec.helpers.init')
local testFulfilled = helpers.testFulfilled
local testRejected = helpers.testRejected
local dummy = {dummy = 'dummy'}
describe('2.3.4: If `x` is not an object or function, fulfill `promise` with `x`', function()
local function testValue(expectedValue, stringRepresentation)
describe('The value is ' .. stringRepresentation, function()
testFulfilled(it, assert, dummy, function(p1)
local p2 = p1:thenCall(function()
return expectedValue
end)
p2:thenCall(function(actualValue)
assert.equal(expectedValue, actualValue)
done()
end)
end)
testRejected(it, assert, dummy, function(p1)
local p2 = p1:thenCall(nil, function()
return expectedValue
end)
p2:thenCall(function(actualValue)
assert.equal(expectedValue, actualValue)
done()
end)
end)
end)
end
testValue(nil, '`nil`')
testValue(false, '`false`')
testValue(true, '`true`')
testValue(0, '`0`')
end)

406
spec/promise_spec.lua Normal file
View file

@ -0,0 +1,406 @@
local promise = require('promise')
local helpers = require('spec.helpers.init')
local basics = require('spec.helpers.basics')
local reasons = require('spec.helpers.reasons')
local setTimeout = helpers.setTimeout
local dummy = {dummy = 'dummy'}
local sentinel = {sentinel = 'sentinel'}
local sentinel2 = {sentinel = 'sentinel2'}
local sentinel3 = {sentinel = 'sentinel3'}
local other = {other = 'other'}
describe('Extend Promise A+.', function()
describe('Promise.resolve', function()
describe('Resolving basic values.', function()
local function testBasicResolve(expectedValue, stringRepresentation)
it('The value is ' .. stringRepresentation ..
', and the state of Promise become fulfilled at once.', function()
local p = promise.resolve(expectedValue)
assert.truthy(tostring(p):match('<fulfilled>'))
p:thenCall(function(value)
assert.equal(expectedValue, value)
done()
end)
assert.True(wait())
end)
end
for valueStr, basicFn in pairs(basics) do
testBasicResolve(basicFn(), valueStr)
end
end)
it('resolve another resolved Promise', function()
local p1 = promise.resolve(dummy)
local p2 = promise.resolve(p1)
p2:thenCall(function(value)
assert.equal(dummy, value)
done()
end)
assert.True(wait())
assert.equal(p1, p2)
end)
it('resolve another rejected Promise', function()
local p1 = promise.reject(dummy)
local p2 = promise.resolve(p1)
p2:thenCall(nil, function(reason)
assert.equal(dummy, reason)
done()
end)
assert.True(wait())
assert.equal(p1, p2)
end)
it('resolve thenables and throwing Errors', function()
local p1 = promise.resolve({
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(dummy)
end
})
assert.True(promise.isInstance(p1))
local onFulfilled1 = spy.new(function(value)
assert.equal(dummy, value)
end)
p1:thenCall(onFulfilled1)
local thenable = {
thenCall = function(self, resolvePromise)
local _ = self
error(dummy)
resolvePromise(other)
end
}
local onRejected = spy.new(function(reason)
assert.equal(dummy, reason)
end)
local p2 = promise.resolve(thenable)
p2:thenCall(nil, onRejected)
thenable = {
thenCall = function(self, resolvePromise)
local _ = self
resolvePromise(dummy)
error(other)
end
}
local onFulfilled2 = spy.new(function(value)
assert.equal(dummy, value)
end)
local p3 = promise.resolve(thenable)
p3:thenCall(onFulfilled2)
assert.False(wait(30))
assert.spy(onFulfilled1).was_called()
assert.spy(onRejected).was_called()
assert.spy(onFulfilled2).was_called()
end)
end)
describe('Promise.rejected.', function()
describe('Rejecting reasons', function()
local function testBasicReject(expectedReason, stringRepresentation)
it('The reason is ' .. stringRepresentation ..
', and the state of Promise become rejected at once.', function()
local p = promise.reject(expectedReason)
assert.truthy(tostring(p):match('<rejected>'))
p:thenCall(nil, function(value)
assert.equal(expectedReason, value)
done()
end)
assert.True(wait())
end)
end
for reasonStr, reason in pairs(reasons) do
testBasicReject(reason(), reasonStr)
end
end)
end)
describe('Promise.catch method.', function()
it('throw errors', function()
local onRejected1 = spy.new(function(reason)
assert.equal(dummy, reason)
end)
promise(function()
error(dummy)
end):catch(onRejected1)
local onRejected2 = spy.new(function() end)
promise(function(resolve)
resolve()
error(dummy)
end):catch(onRejected2)
assert.False(wait(30))
assert.spy(onRejected1).was_called()
assert.spy(onRejected2).was_not_called()
end)
it('is resolved', function()
local onRejected1 = spy.new(function() end)
local onFulfilled = spy.new(function() end)
local onRejected2 = spy.new(function() end)
promise.resolve(dummy)
:catch(onRejected1)
:thenCall(onFulfilled)
:catch(onRejected2)
assert.False(wait(30))
assert.spy(onRejected1).was_not_called()
assert.spy(onFulfilled).was_called()
assert.spy(onRejected2).was_not_called()
end)
end)
describe('Promise.finally method.', function()
local onFinally = spy.new(done)
before_each(function()
onFinally:clear()
end)
it('always return itself, different from JavaScript', function()
local p1 = promise(function() end)
local p2 = p1:finally(onFinally)
assert.equal(p1, p2)
end)
it('is pending', function()
promise(function() end):finally(onFinally)
assert.False(wait(30))
assert.spy(onFinally).was_not_called()
end)
it('is fulfilled', function()
promise.resolve(dummy):finally(onFinally)
assert.True(wait())
assert.spy(onFinally).was_called()
end)
it('is rejected', function()
promise.reject(dummy):catch(function() end):finally(onFinally)
assert.True(wait())
assert.spy(onFinally).was_called()
end)
end)
describe('Promise.all method.', function()
it('should be fulfilled immediately if element is empty', function()
promise.all({}):thenCall(function(value)
assert.same({}, value)
done()
end)
assert.True(wait())
end)
describe('wait for fulfillments,', function()
it('use index table as elements', function()
local p1 = promise.resolve(sentinel)
local p2 = sentinel2
local p3 = promise(function(resolve)
setTimeout(function()
resolve(sentinel3)
end, 10)
end)
promise.all({p1, p2, p3}):thenCall(function(value)
assert.same({sentinel, sentinel2, sentinel3}, value)
done()
end)
assert.True(wait())
end)
it('use key-value table as elements, different from JavaScript', function()
local p1 = promise.resolve(sentinel)
local p2 = sentinel2
local p3 = promise(function(resolve)
setTimeout(function()
resolve(sentinel3)
end, 10)
end)
promise.all({p1 = p1, p2 = p2, p3 = p3}):thenCall(function(value)
assert.same({p1 = sentinel, p2 = sentinel2, p3 = sentinel3}, value)
done()
end)
assert.True(wait())
end)
end)
it('is rejected if any of the elements are rejected', function()
local p1 = promise.resolve(sentinel)
local p2 = sentinel2
local p3 = promise(function(_, reject)
setTimeout(function()
reject(sentinel3)
end, 10)
end)
promise.all({p1, p2, p3}):thenCall(nil, function(reason)
assert.equal(sentinel3, reason)
done()
end)
assert.True(wait())
end)
end)
describe('Promise.allSettled method.', function()
it('should be fulfilled immediately if element is empty', function()
promise.allSettled({}):thenCall(function(value)
assert.same({}, value)
done()
end)
assert.True(wait())
end)
describe('wait for fulfillments,', function()
it('use index table as elements', function()
local p1 = promise.resolve(sentinel)
local p2 = sentinel2
local p3 = promise(function(resolve)
setTimeout(function()
resolve(sentinel3)
end, 10)
end)
promise.allSettled({p1, p2, p3}):thenCall(function(value)
assert.same({
{status = 'fulfilled', value = sentinel},
{status = 'fulfilled', value = sentinel2},
{status = 'fulfilled', value = sentinel3}
}, value)
done()
end)
assert.True(wait())
end)
it('use key-value table as elements, different from JavaScript', function()
local p1 = promise.resolve(sentinel)
local p2 = sentinel2
local p3 = promise(function(resolve)
setTimeout(function()
resolve(sentinel3)
end, 10)
end)
promise.allSettled({p1 = p1, p2 = p2, p3 = p3}):thenCall(function(value)
assert.same({
p1 = {status = 'fulfilled', value = sentinel},
p2 = {status = 'fulfilled', value = sentinel2},
p3 = {status = 'fulfilled', value = sentinel3}
}, value)
done()
end)
assert.True(wait())
end)
end)
it('is always resolved even if any of the elements are rejected', function()
local p1 = promise.resolve(sentinel)
local p2 = sentinel2
local p3 = promise(function(_, reject)
setTimeout(function()
reject(sentinel3)
end, 10)
end)
promise.allSettled({p1, p2, p3}):thenCall(function(value)
assert.same({
{status = 'fulfilled', value = sentinel},
{status = 'fulfilled', value = sentinel2},
{status = 'rejected', reason = sentinel3}
}, value)
done()
end)
assert.True(wait())
end)
end)
describe('Promise.any method.', function()
it('should be rejected immediately if element is empty', function()
promise.any({}):thenCall(nil, function(reason)
assert.truthy(reason:match('^AggregateError'))
done()
end)
assert.True(wait())
end)
it('resolve with the first promise to fulfill, even if a promise rejects first', function()
local p1 = promise.reject(sentinel)
local p2 = promise(function(resolve)
setTimeout(function()
resolve(sentinel2)
end, 30)
end)
local p3 = promise(function(resolve)
setTimeout(function()
resolve(sentinel3)
end, 10)
end)
promise.any({p1, p2, p3}):thenCall(function(value)
assert.equal(sentinel3, value)
done()
end)
assert.True(wait())
end)
it('reject with `AggregateError` if no promise fulfills', function()
promise.any({promise.reject(dummy)}):thenCall(nil, function(reason)
assert.not_equal(dummy, reason)
assert.truthy(reason:match('^AggregateError'))
done()
end)
assert.True(wait())
end)
end)
describe('Promise.race method.', function()
it('should be pending forever if element is empty', function()
local onFinally = spy.new(done)
promise.race({}):finally(onFinally)
assert.spy(onFinally).was_not_called()
assert.False(wait(30))
assert.spy(onFinally).was_not_called()
end)
describe('resolves or rejects with the first promise to settle,', function()
it('resolve Promise is earlier than reject', function()
local p1 = promise(function(resolve)
setTimeout(function()
resolve(sentinel)
end, 10)
end)
local p2 = promise(function(_, reject)
setTimeout(function()
reject(sentinel2)
end, 20)
end)
promise.race({p1, p2}):thenCall(function(value)
assert.equal(sentinel, value)
done()
end)
assert.True(wait())
end)
it('reject Promise is earlier than resolve', function()
local p1 = promise(function(_, reject)
setTimeout(function()
reject(sentinel)
end, 10)
end)
local p2 = promise(function(resolve)
setTimeout(function()
resolve(sentinel2)
end, 20)
end)
promise.race({p1, p2}):thenCall(nil, function(reason)
assert.equal(sentinel, reason)
done()
end)
assert.True(wait())
end)
end)
end)
end)

22
typings/README.md Normal file
View file

@ -0,0 +1,22 @@
# Typings of promise-async only
Development library for the completion and documentation of promise-async.
## Installation
### lua-language-server
Append this directory path to `Lua.workspace.library` field in configuration file.
Use `.luarc.json` as an example:
```json
{
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"workspace.library": ["your_path/typings"]
}
```
## Credits
[lua-language-server/wiki/Setting](https://github.com/sumneko/lua-language-server/wiki/Setting)

14
typings/async.lua Normal file
View file

@ -0,0 +1,14 @@
---@diagnostic disable: unused-local
---An async function is a function like the async keyword in JavaScript
---@class Async
---@overload fun(executor: fun()): Promise
local Async = {}
---Await expressions make promise returning functions behave as though they're synchronous by
---suspending execution until the returned promise is fulfilled or rejected.
---@param promise Promise|table
---@return ... result The resolved value of the promise.
function _G.await(promise) end
return Async

27
typings/loop.lua Normal file
View file

@ -0,0 +1,27 @@
---@diagnostic disable: unused-local
---Singleton table, can't be as metatable. Two ways to extend the event loop.
---1. Create a new table and implement all methods, assign the new one to `Promise.loop` .
---2. Assign the targeted method to the `Promise.loop` field to override method.
---@class PromiseAsyncEventLoop
local EventLoop = {}
---Sets a timer which executes a function once the timer expires.
---@param callback fun() A callback function, to be executed after the timer expires.
---@param delay number The time, in milliseconds that the timer should wait.
---@return userdata timer The timer handle created by EventLoop.
function EventLoop.setTimeout(callback, delay) end
---The callback function will be executed in the next tick to continue the event loop.
---@param callback fun() A callback function, will be wrapped by EventLoop.callWrapper.
function EventLoop.nextTick(callback) end
---The callback function will be executed after all next tick events are handled.
---@param callback fun() A callback function, will be wrapped by EventLoop.callWrapper.
function EventLoop.nextIdle(callback) end
---Wrap the callback function from `setTimeout`, `nextTick` and `nextIdle`.
---@param callback fun() A callback function, executed by asynchronous methods.
function EventLoop.callWrapper(callback) end
return EventLoop

69
typings/promise.lua Normal file
View file

@ -0,0 +1,69 @@
---@diagnostic disable: unused-local
---@alias PromiseExecutor fun(resolve: fun(value: any), reject: fun(reason?: any))
---@class Promise
---@field loop PromiseAsyncLoop
---@overload fun(executor: PromiseExecutor): Promise
local Promise = {}
---Creates a new Promise.
---@param executor PromiseExecutor A callback used to initialize the promise. This callback is passed two arguments:
---a resolve callback used to resolve the promise with a value or the result of another promise,
---and a reject callback used to reject the promise with a provided reason or error.
---@return Promise promise A new Promise.
function Promise.new(executor) end
---Attaches callbacks for the resolution and/or rejection of the Promise.
---@param onFulfilled? fun(value: any) The callback to execute when the Promise is resolved.
---@param onRejected? fun(reason: any) The callback to execute when the Promise is rejected.
---@return Promise promise A Promise for the completion of which ever callback is executed.
function Promise:thenCall(onFulfilled, onRejected) end
---Attaches a callback for only the rejection of the Promise.
---@param onRejected? fun(reason: any) The callback to execute when the Promise is rejected.
---@return Promise promise A Promise for the completion of the callback.
function Promise:catch(onRejected) end
---Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected).
---The resolved value cannot be modified from the callback.
---@param onFinally? fun() The callback to execute when the Promise is settled (fulfilled or rejected).
---@return Promise promise The self promise.
function Promise:finally(onFinally) end
---Creates a new resolved promise for the provided value.
---@param value? any A value, or the promise passed as value
---@return Promise promise A resolved promise.
function Promise.resolve(value) end
---Creates a new rejected promise for the provided reason.
---@param reason? any The reason the Promise was rejected.
---@return Promise promise A new rejected Promise.
function Promise.reject(reason) end
---Creates a Promise that is resolved with a table of results when all of the provided
---Promises resolve, or rejected when any Promise is rejected.
---@param values table<any, Promise> A table of Promises.
---@return Promise promise A new Promise.
function Promise.all(values) end
---Creates a Promise that is resolved with a table of results when all of the provided
---Promises resolve or reject.
---@param values table<any, Promise> A table of Promises.
---@return Promise promise A new Promise.
function Promise.allSettled(values) end
---The any function returns a Promise that is fulfilled by the first given Promise to be fulfilled,
---or rejected with an AggregateError containing an table of rejection reasons if all of the
---given Promises are rejected. It resolves all elements of the passed table to Promises as it runs this algorithm.
---@param values table<any, Promise>
---@return Promise promise A new Promise
function Promise.any(values) end
---Creates a Promise that is resolved or rejected when any of the provided Promises are resolved
---or rejected.
---@param values table<any, Promise> A table of Promises.
---@return Promise promise A new Promise.
function Promise.race(values) end
return Promise