Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Promise #526

Closed
wants to merge 42 commits into from
Closed
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
80c41b7
Promise: first implementation based on es6-promise
rhysd Jun 10, 2017
edc3d79
impl: now neovim supports lambda function
rhysd Dec 16, 2017
b79035d
impl: fix missing a: and lambda's parameter
rhysd Dec 17, 2017
a721833
impl: move Promise to Async.Promise
rhysd Dec 17, 2017
fcfe330
Async.Promise: Add first tests to sanitize implementation
rhysd Dec 17, 2017
18175eb
test: prefer expression style
rhysd Dec 17, 2017
9946e20
impl: fix label in message of :throw
rhysd Dec 17, 2017
4cfd9e0
test: tests for .catch()
rhysd Dec 17, 2017
563629c
test: prefer command style
rhysd Dec 17, 2017
9adcd75
test: add tests for .all() and .race()
rhysd Dec 18, 2017
55d1aad
test: add tests for .resolve() and .reject()
rhysd Dec 18, 2017
50dd931
test: add tests for .is_available() and .is_promise()
rhysd Dec 18, 2017
e5f6d5c
test: avoid timing problem on macOS worker on Travis CI
rhysd Dec 18, 2017
c826376
impl: ignore unused parameter warning from vimlint for s:NOOP
rhysd Dec 18, 2017
fe38dcc
test: add tests for thenable object and resolving/rejecting more than…
rhysd Dec 19, 2017
a372aa7
test: add tests for P.resolve and P.reject where Promise is given
rhysd Dec 19, 2017
ffa7708
Async.Promise: Do not check self fulfillment
rhysd Dec 19, 2017
a7aba15
test: add tests to resolve/reject with various type of values (it sho…
rhysd Dec 19, 2017
7b100ad
test: v:true, v:false, v:null and v:none are not supported by themis.vim
rhysd Dec 19, 2017
cec1bc4
test: rejected in .then()/.catch()
rhysd Dec 20, 2017
afe55f7
test: add tests for resolving/rejecting a Promise more than once
rhysd Dec 20, 2017
6d999a1
test: add test for pending Promise value
rhysd Dec 20, 2017
34639ae
impl: define methods with :function! to avoid messy function names in…
rhysd Dec 20, 2017
5c4d387
test: .all() and .race() with empty array
rhysd Dec 20, 2017
67894f9
impl: fix .all() with empty array
rhysd Dec 20, 2017
e3aa141
test: tweak wait time to check Promise state in tests for .all()
rhysd Dec 20, 2017
8409c5f
test: omit parameter of Promise.resolve/Promise.reject
rhysd Dec 20, 2017
fdb173c
test: omit parameters of .then() and .catch()
rhysd Dec 20, 2017
2635fac
test: .then() with 2 parameters
rhysd Dec 20, 2017
809d4ef
test: funcref is passed to ctor
rhysd Dec 20, 2017
0b33756
test: assimilation on resolve() in constructor, resolved values by .a…
rhysd Dec 20, 2017
111cc50
test: resolve with no value on constructor
rhysd Dec 20, 2017
adcbfee
test: .race() and .all() with mix of fulfilled and pending promises
rhysd Dec 20, 2017
307a7e2
test: wait for nested fulfillment and ignore exception after settled
rhysd Dec 20, 2017
1708702
Async.Promise: Ensure fulfilled/rejected callbacks are asynchronously…
rhysd Dec 23, 2017
83916a4
Async.Promise: Fix tests for asynchronous fulfillment/rejection
rhysd Dec 23, 2017
b5b3f4a
test: sweep sleep() with fixed time
rhysd Dec 23, 2017
beaee61
impl: prefer partial to lambda
rhysd Dec 23, 2017
6faf65b
test: resolve/reject with funcref
rhysd Dec 23, 2017
63de1ea
test: P.reject() and P.resolve() with pending Promise
rhysd Dec 24, 2017
3300ee8
Async.Promise: Add a help document
rhysd Dec 24, 2017
7170440
doc: add a missing tag and tweak description of Promise object
rhysd Dec 24, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 252 additions & 0 deletions autoload/vital/__vital__/Async/Promise.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
" ECMAScript like Promise library for asynchronous operations.
" Spec: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
" This implementation is based upon es6-promise npm package.
" Repo: https://github.com/stefanpenner/es6-promise

" States of promise
let s:PENDING = 0
let s:FULFILLED = 1
let s:REJECTED = 2

let s:DICT_T = type({})
let s:NULL_T = type(v:null)

" @vimlint(EVL103, 1, a:resolve)
" @vimlint(EVL103, 1, a:reject)
function! s:noop(resolve, reject) abort

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL103: unused argument a:resolve

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL103: unused argument a:reject

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL103: unused argument a:resolve

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL103: unused argument a:reject

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL103: unused argument a:resolve

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL103: unused argument a:reject

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL103: unused argument a:resolve

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL103: unused argument a:reject

endfunction
" @vimlint(EVL103, 0, a:resolve)
" @vimlint(EVL103, 0, a:reject)
let s:NOOP = function('s:noop')

" Internal APIs

let s:PROMISE = {
\ '_state': s:PENDING,
\ '_children': [],
\ '_fulfillments': [],
\ '_rejections': [],
\ '_result': v:null,
\ }

let s:id = -1
function! s:_next_id() abort
let s:id += 1
return s:id
endfunction

" ... is added to use this function as a callback of timer_start()
function! s:_invoke_callback(settled, promise, callback, result, ...) abort
let has_callback = type(a:callback) != s:NULL_T
let success = 1
if has_callback
try
let Result = a:callback(a:result)
catch
let Err = v:exception
let success = 0
endtry
else
let Result = a:result
endif

if a:promise._state != s:PENDING
" Do nothing
elseif has_callback && success
call s:_resolve(a:promise, Result)
elseif !success
call s:_reject(a:promise, Err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

何回言うねん

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

elseif a:settled == s:FULFILLED
call s:_fulfill(a:promise, Result)
elseif a:settled == s:REJECTED
call s:_reject(a:promise, Result)
endif
endfunction

" ... is added to use this function as a callback of timer_start()
function! s:_publish(promise, ...) abort
let settled = a:promise._state
if empty(a:promise._children)
return
endif
for i in range(len(a:promise._children))
if settled == s:FULFILLED
let l:CB = a:promise._fulfillments[i]
elseif settled == s:REJECTED
let l:CB = a:promise._rejections[i]
else
throw 'vital: Async.Promise: Cannot publish a pending promise'
endif
let child = a:promise._children[i]
if type(child) != s:NULL_T
call s:_invoke_callback(settled, child, l:CB, a:promise._result)
else
call l:CB(a:promise._result)
endif
endfor
let a:promise._children = []
let a:promise._fulfillments = []
let a:promise._rejections = []
endfunction

function! s:_subscribe(parent, child, on_fulfilled, on_rejected) abort
let is_empty = empty(a:parent._children)
let a:parent._children += [ a:child ]
let a:parent._fulfillments += [ a:on_fulfilled ]
let a:parent._rejections += [ a:on_rejected ]
if is_empty && a:parent._state > s:PENDING
call timer_start(0, function('s:_publish', [a:parent]))
endif
endfunction

function! s:_handle_thenable(promise, thenable) abort
if a:thenable._state == s:FULFILLED
call s:_fulfill(a:promise, a:thenable._result)
elseif a:thenable._state == s:REJECTED
call s:_reject(a:promise, a:thenable._result)
else
call s:_subscribe(
\ a:thenable,
\ v:null,
\ function('s:_resolve', [a:promise]),
\ function('s:_reject', [a:promise]),
\ )
endif
endfunction

function! s:_resolve(promise, ...) abort
let Result = a:0 > 0 ? a:1 : v:null
if s:is_promise(Result)
call s:_handle_thenable(a:promise, Result)
else
call s:_fulfill(a:promise, Result)
endif
endfunction

function! s:_fulfill(promise, value) abort
if a:promise._state != s:PENDING
return
endif
let a:promise._result = a:value
let a:promise._state = s:FULFILLED
if !empty(a:promise._children)
call timer_start(0, function('s:_publish', [a:promise]))
endif
endfunction

function! s:_reject(promise, ...) abort
if a:promise._state != s:PENDING
return
endif
let a:promise._result = a:0 > 0 ? a:1 : v:null
let a:promise._state = s:REJECTED
call timer_start(0, function('s:_publish', [a:promise]))
endfunction

function! s:_resolve_one(index, value) dict abort
let self.done[a:index] = a:value
let self.resolved += 1
if self.resolved == self.total
call self.resolve(self.done)
endif
endfunction

function! s:_all(promises, resolve, reject) abort
let total = len(a:promises)
if total == 0
call a:resolve([])
return
endif

let wait_group = {
\ 'done': repeat([v:null], total),
\ 'resolve': a:resolve,
\ 'resolved': 0,
\ 'total': total,
\ 'notify_done': function('s:_resolve_one'),
\ }

" 'for' statement is not available here because iteration variable is captured into lambda
" expression by **reference**.
call map(
\ copy(a:promises),
\ {i, p -> p.then({v -> wait_group.notify_done(i, v)}, a:reject)},
\ )
endfunction

function! s:_race(promises, resolve, reject) abort
for p in a:promises
call p.then(a:resolve, a:reject)
endfor
endfunction

" Public APIs

function! s:new(resolver) abort
let promise = deepcopy(s:PROMISE)
let promise._vital_promise = s:_next_id()
try
if a:resolver != s:NOOP
call a:resolver(
\ function('s:_resolve', [promise]),
\ function('s:_reject', [promise]),
\ )
endif
catch
call s:_reject(promise, v:exception)
endtry
return promise
endfunction

function! s:all(promises) abort
return s:new(function('s:_all', [a:promises]))
endfunction

function! s:race(promises) abort
return s:new(function('s:_race', [a:promises]))
endfunction

function! s:resolve(...) abort
let promise = s:new(s:NOOP)
call s:_resolve(promise, a:0 > 0 ? a:1 : v:null)
return promise
endfunction

function! s:reject(...) abort
let promise = s:new(s:NOOP)
call s:_reject(promise, a:0 > 0 ? a:1 : v:null)
return promise
endfunction

function! s:is_available() abort
return has('nvim') || v:version >= 800
endfunction

function! s:is_promise(maybe_promise) abort
return type(a:maybe_promise) == s:DICT_T && has_key(a:maybe_promise, '_vital_promise')
endfunction

function! s:_promise_then(...) dict abort
let parent = self
let state = parent._state
let child = s:new(s:NOOP)
let Res = get(a:000, 0, v:null)
let Rej = get(a:000, 1, v:null)
if state == s:FULFILLED
call timer_start(0, function('s:_invoke_callback', [state, child, Res, parent._result]))
elseif state == s:REJECTED
call timer_start(0, function('s:_invoke_callback', [state, child, Rej, parent._result]))
else
call s:_subscribe(parent, child, Res, Rej)
endif
return child
endfunction
let s:PROMISE.then = function('s:_promise_then')

" .catch() is just a syntax sugar of .then()
function! s:_promise_catch(...) dict abort
return self.then(v:null, get(a:000, 0, v:null))
endfunction
let s:PROMISE.catch = function('s:_promise_catch')

" vim:set et ts=2 sts=2 sw=2 tw=0:
3 changes: 3 additions & 0 deletions test/.themisrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ set encoding=utf-8

call themis#option('recursive', 1)
call themis#option('exclude', ['test/_testdata/', 'test/README.md'])
if !has('nvim') && v:version < 800
call themis#option('exclude', ['test/Async/Promise.vimspec'])
endif

let g:Expect = themis#helper('expect')
call themis#helper('command').with(themis#helper('assert')).with({'Expect': g:Expect})
Expand Down
Loading