298 lines
6.4 KiB
Lua
298 lines
6.4 KiB
Lua
|
--[[
|
||
|
The MIT License (MIT)
|
||
|
=====================
|
||
|
|
||
|
Copyright © `2015` `Colin Fein`
|
||
|
|
||
|
Permission is hereby granted, free of charge, to any person
|
||
|
obtaining a copy of this software and associated documentation
|
||
|
files (the “Software”), to deal in the Software without
|
||
|
restriction, including without limitation the rights to use,
|
||
|
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
copies of the Software, and to permit persons to whom the
|
||
|
Software is furnished to do so, subject to the following
|
||
|
conditions:
|
||
|
|
||
|
The above copyright notice and this permission notice shall be
|
||
|
included in all copies or substantial portions of the Software.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||
|
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||
|
]]
|
||
|
|
||
|
-- Port of https://github.com/rhysbrettbowen/promise_impl/blob/master/promise.js
|
||
|
-- and https://github.com/rhysbrettbowen/Aplus
|
||
|
--
|
||
|
local queue = {}
|
||
|
|
||
|
local State = {
|
||
|
PENDING = 'pending',
|
||
|
FULFILLED = 'fulfilled',
|
||
|
REJECTED = 'rejected',
|
||
|
}
|
||
|
|
||
|
local passthrough = function(x) return x end
|
||
|
local errorthrough = function(x) error(x) end
|
||
|
|
||
|
local function callable_table(callback)
|
||
|
local mt = getmetatable(callback)
|
||
|
return type(mt) == 'table' and type(mt.__call) == 'function'
|
||
|
end
|
||
|
|
||
|
local function is_callable(value)
|
||
|
local t = type(value)
|
||
|
return t == 'function' or (t == 'table' and callable_table(value))
|
||
|
end
|
||
|
|
||
|
local transition, resolve, run
|
||
|
|
||
|
local Promise = {
|
||
|
is_promise = true,
|
||
|
state = State.PENDING
|
||
|
}
|
||
|
Promise.mt = { __index = Promise }
|
||
|
|
||
|
local do_async = function(callback)
|
||
|
if Promise.async then
|
||
|
Promise.async(callback)
|
||
|
else
|
||
|
table.insert(queue, callback)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local reject = function(promise, reason)
|
||
|
transition(promise, State.REJECTED, reason)
|
||
|
end
|
||
|
|
||
|
local fulfill = function(promise, value)
|
||
|
transition(promise, State.FULFILLED, value)
|
||
|
end
|
||
|
|
||
|
transition = function(promise, state, value)
|
||
|
if promise.state == state
|
||
|
or promise.state ~= State.PENDING
|
||
|
or ( state ~= State.FULFILLED and state ~= State.REJECTED )
|
||
|
then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
promise.state = state
|
||
|
promise.value = value
|
||
|
run(promise)
|
||
|
end
|
||
|
|
||
|
function Promise:next(on_fulfilled, on_rejected)
|
||
|
local promise = Promise.new()
|
||
|
|
||
|
table.insert(self.queue, {
|
||
|
fulfill = is_callable(on_fulfilled) and on_fulfilled or nil,
|
||
|
reject = is_callable(on_rejected) and on_rejected or nil,
|
||
|
promise = promise
|
||
|
})
|
||
|
|
||
|
run(self)
|
||
|
|
||
|
return promise
|
||
|
end
|
||
|
|
||
|
resolve = function(promise, x)
|
||
|
if promise == x then
|
||
|
reject(promise, 'TypeError: cannot resolve a promise with itself')
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local x_type = type(x)
|
||
|
|
||
|
if x_type ~= 'table' then
|
||
|
fulfill(promise, x)
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- x is a promise in the current implementation
|
||
|
if x.is_promise then
|
||
|
-- 2.3.2.1 if x is pending, resolve or reject this promise after completion
|
||
|
if x.state == State.PENDING then
|
||
|
x:next(
|
||
|
function(value)
|
||
|
resolve(promise, value)
|
||
|
end,
|
||
|
function(reason)
|
||
|
reject(promise, reason)
|
||
|
end
|
||
|
)
|
||
|
return
|
||
|
end
|
||
|
-- if x is not pending, transition promise to x's state and value
|
||
|
transition(promise, x.state, x.value)
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local called = false
|
||
|
-- 2.3.3.1. Catches errors thrown by __index metatable
|
||
|
local success, reason = pcall(function()
|
||
|
local next = x.next
|
||
|
if is_callable(next) then
|
||
|
next(
|
||
|
x,
|
||
|
function(y)
|
||
|
if not called then
|
||
|
resolve(promise, y)
|
||
|
called = true
|
||
|
end
|
||
|
end,
|
||
|
function(r)
|
||
|
if not called then
|
||
|
reject(promise, r)
|
||
|
called = true
|
||
|
end
|
||
|
end
|
||
|
)
|
||
|
else
|
||
|
fulfill(promise, x)
|
||
|
end
|
||
|
end)
|
||
|
|
||
|
if not success then
|
||
|
if not called then
|
||
|
reject(promise, reason)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
run = function(promise)
|
||
|
if promise.state == State.PENDING then return end
|
||
|
|
||
|
do_async(function()
|
||
|
-- drain promise.queue while allowing pushes from within callbacks
|
||
|
local q = promise.queue
|
||
|
local i = 0
|
||
|
while i < #q do
|
||
|
i = i + 1
|
||
|
local obj = q[i]
|
||
|
local success, result = pcall(function()
|
||
|
local success = obj.fulfill or passthrough
|
||
|
local failure = obj.reject or errorthrough
|
||
|
local callback = promise.state == State.FULFILLED and success or failure
|
||
|
return callback(promise.value)
|
||
|
end)
|
||
|
|
||
|
if not success then
|
||
|
reject(obj.promise, result)
|
||
|
else
|
||
|
resolve(obj.promise, result)
|
||
|
end
|
||
|
end
|
||
|
for j = 1, i do
|
||
|
q[j] = nil
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
function Promise.new(callback)
|
||
|
local instance = {
|
||
|
queue = {}
|
||
|
}
|
||
|
setmetatable(instance, Promise.mt)
|
||
|
|
||
|
if callback then
|
||
|
callback(
|
||
|
function(value)
|
||
|
resolve(instance, value)
|
||
|
end,
|
||
|
function(reason)
|
||
|
reject(instance, reason)
|
||
|
end
|
||
|
)
|
||
|
end
|
||
|
|
||
|
return instance
|
||
|
end
|
||
|
|
||
|
function Promise:catch(callback)
|
||
|
return self:next(nil, callback)
|
||
|
end
|
||
|
|
||
|
function Promise:resolve(value)
|
||
|
fulfill(self, value)
|
||
|
end
|
||
|
|
||
|
function Promise:reject(reason)
|
||
|
reject(self, reason)
|
||
|
end
|
||
|
|
||
|
function Promise.update()
|
||
|
while true do
|
||
|
local async = table.remove(queue, 1)
|
||
|
|
||
|
if not async then
|
||
|
break
|
||
|
end
|
||
|
|
||
|
async()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- resolve when all promises complete
|
||
|
function Promise.all(...)
|
||
|
local promises = {...}
|
||
|
local results = {}
|
||
|
local state = State.FULFILLED
|
||
|
local remaining = #promises
|
||
|
|
||
|
local promise = Promise.new()
|
||
|
|
||
|
local check_finished = function()
|
||
|
if remaining > 0 then
|
||
|
return
|
||
|
end
|
||
|
transition(promise, state, results)
|
||
|
end
|
||
|
|
||
|
for i,p in ipairs(promises) do
|
||
|
p:next(
|
||
|
function(value)
|
||
|
results[i] = value
|
||
|
remaining = remaining - 1
|
||
|
check_finished()
|
||
|
end,
|
||
|
function(value)
|
||
|
results[i] = value
|
||
|
remaining = remaining - 1
|
||
|
state = State.REJECTED
|
||
|
check_finished()
|
||
|
end
|
||
|
)
|
||
|
end
|
||
|
|
||
|
check_finished()
|
||
|
|
||
|
return promise
|
||
|
end
|
||
|
|
||
|
-- resolve with first promise to complete
|
||
|
function Promise.race(...)
|
||
|
local promises = {...}
|
||
|
local promise = Promise.new()
|
||
|
|
||
|
Promise.all(...):next(nil, function(value)
|
||
|
reject(promise, value)
|
||
|
end)
|
||
|
|
||
|
local success = function(value)
|
||
|
fulfill(promise, value)
|
||
|
end
|
||
|
|
||
|
for _,p in ipairs(promises) do
|
||
|
p:next(success)
|
||
|
end
|
||
|
|
||
|
return promise
|
||
|
end
|
||
|
|
||
|
return Promise
|