You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
263 lines
5.9 KiB
Lua
263 lines
5.9 KiB
Lua
-- 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) or value == nil 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
|