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

-- 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