mirror of
https://github.com/kevinhwang91/promise-async
synced 2024-09-16 13:24:04 +02:00
Initial commit
This commit is contained in:
commit
fb97aa33dc
52 changed files with 5075 additions and 0 deletions
26
.editorconfig
Normal file
26
.editorconfig
Normal 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
43
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: false
|
23
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
23
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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
27
.github/workflows/lint.yml
vendored
Normal 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
59
.github/workflows/test.yml
vendored
Normal 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
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
17
.luarc.json
Normal file
17
.luarc.json
Normal 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
29
LICENSE
Normal 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
75
Makefile
Normal 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
137
README.md
Normal 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
6
examples/README.md
Normal 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
29
examples/coc_nvim.lua
Normal 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
58
examples/demo.js
Normal 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
87
examples/demo.lua
Normal 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
|
47
examples/multiple_threads.lua
Normal file
47
examples/multiple_threads.lua
Normal 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
23
examples/read_file.lua
Normal 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
74
examples/uva.lua
Normal 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
21
examples/write_file.lua
Normal 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
131
lua/async.lua
Normal 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
|
133
lua/promise-async/compat.lua
Normal file
133
lua/promise-async/compat.lua
Normal 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
112
lua/promise-async/error.lua
Normal 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
|
85
lua/promise-async/loop.lua
Normal file
85
lua/promise-async/loop.lua
Normal 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
|
33
lua/promise-async/utils.lua
Normal file
33
lua/promise-async/utils.lua
Normal 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
399
lua/promise.lua
Normal 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
|
31
rockspec/promise-async-scm-1.rockspec
Normal file
31
rockspec/promise-async-scm-1.rockspec
Normal 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
405
spec/async_spec.lua
Normal 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
50
spec/fixtures.lua
Normal 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
30
spec/helpers/basics.lua
Normal 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
58
spec/helpers/init.lua
Normal 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
|
29
spec/helpers/outputHandler.lua
Normal file
29
spec/helpers/outputHandler.lua
Normal 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
37
spec/helpers/reasons.lua
Normal 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
137
spec/helpers/thenables.lua
Normal 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
54
spec/init.lua
Normal 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
84
spec/loop_spec.lua
Normal 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)
|
70
spec/promiseA+/2.1.2_spec.lua
Normal file
70
spec/promiseA+/2.1.2_spec.lua
Normal 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)
|
70
spec/promiseA+/2.1.3_spec.lua
Normal file
70
spec/promiseA+/2.1.3_spec.lua
Normal 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)
|
78
spec/promiseA+/2.2.1_spec.lua
Normal file
78
spec/promiseA+/2.2.1_spec.lua
Normal 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)
|
122
spec/promiseA+/2.2.2_spec.lua
Normal file
122
spec/promiseA+/2.2.2_spec.lua
Normal 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)
|
122
spec/promiseA+/2.2.3_spec.lua
Normal file
122
spec/promiseA+/2.2.3_spec.lua
Normal 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)
|
128
spec/promiseA+/2.2.4_spec.lua
Normal file
128
spec/promiseA+/2.2.4_spec.lua
Normal 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)
|
295
spec/promiseA+/2.2.6_spec.lua
Normal file
295
spec/promiseA+/2.2.6_spec.lua
Normal 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)
|
98
spec/promiseA+/2.2.7_spec.lua
Normal file
98
spec/promiseA+/2.2.7_spec.lua
Normal 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)
|
31
spec/promiseA+/2.3.1_spec.lua
Normal file
31
spec/promiseA+/2.3.1_spec.lua
Normal 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)
|
97
spec/promiseA+/2.3.2_spec.lua
Normal file
97
spec/promiseA+/2.3.2_spec.lua
Normal 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)
|
800
spec/promiseA+/2.3.3_spec.lua
Normal file
800
spec/promiseA+/2.3.3_spec.lua
Normal 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)
|
35
spec/promiseA+/2.3.4_spec.lua
Normal file
35
spec/promiseA+/2.3.4_spec.lua
Normal 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
406
spec/promise_spec.lua
Normal 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
22
typings/README.md
Normal 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
14
typings/async.lua
Normal 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
27
typings/loop.lua
Normal 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
69
typings/promise.lua
Normal 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
|
Loading…
Reference in a new issue