Skip to content

Instantly share code, notes, and snippets.

@bmwalters
Last active February 14, 2017 14:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmwalters/f44adad176bb1101292278e4f624f811 to your computer and use it in GitHub Desktop.
Save bmwalters/f44adad176bb1101292278e4f624f811 to your computer and use it in GitHub Desktop.
ES2015 Promises implemented in Lua 5.3
local function check_callable(o)
return type(o) == "function" or type(getmetatable(o).__call) == "function"
end
local PromiseState = {
pending = "pending",
fulfilled = "fulfilled",
rejected = "rejected"
}
local Promise
local PROMISE = {}
PROMISE.__index = PROMISE
function PROMISE:__tostring()
if self._state == PromiseState.pending then
return "Promise (pending)"
elseif self._state == PromiseState.fulfilled then
return ("Promise (fulfilled, value=%s)"):format(tostring(self._value))
elseif self._state == PromiseState.rejected then
return ("Promise (rejected, reason=%s)"):format(tostring(self._reason))
end
end
local function TriggerPromiseReactions(reactions, argument)
for _, reaction in ipairs(reactions) do
local status, val = pcall(reaction.handler, argument)
if status then
reaction.capabilities.resolve(val)
else
reaction.capabilities.reject(val)
end
end
end
function PROMISE:then_(on_fulfilled, on_rejected)
local new_promise_resolve, new_promise_reject
local new_promise = Promise(function(res, rej)
new_promise_resolve, new_promise_reject = res, rej
end)
local result_capability = {
promise = new_promise,
resolve = new_promise_resolve,
reject = new_promise_reject
}
on_fulfilled = on_fulfilled or function(x) return x end
on_rejected = on_rejected or function(x) error(x) end
local fulfill_reaction = {
capabilities = result_capability,
handler = on_fulfilled
}
local reject_reaction = {
capabilities = result_capability,
handler = on_rejected
}
if self._state == PromiseState.pending then
self._fulfill_reactions[#self._fulfill_reactions + 1] = fulfill_reaction
self._reject_reactions[#self._reject_reactions + 1] = reject_reaction
elseif self._state == PromiseState.fulfilled then
TriggerPromiseReactions({ fulfill_reaction }, self._value)
elseif self._state == PromiseState.rejected then
TriggerPromiseReactions({ reject_reaction }, self._reason)
end
return result_capability.promise
end
function PROMISE:catch(on_rejected)
self:then_(nil, on_rejected)
end
function PROMISE:_fulfill(val)
if self._state ~= PromiseState.pending then return end
self._value = val
TriggerPromiseReactions(self._fulfill_reactions, self._reason)
self._fulfill_reactions = nil
self._reject_reactions = nil
self._state = PromiseState.fulfilled
end
function PROMISE:_reject(val)
if self._state ~= PromiseState.pending then return end
self._reason = val
TriggerPromiseReactions(self._reject_reactions, self._reason)
self._fulfill_reactions = nil
self._reject_reactions = nil
self._state = PromiseState.rejected
end
local function CreateResolvingFunctions(promise)
local already_resolved = false
local resolve, reject
resolve = function(resolution)
if already_resolved then return end
already_resolved = true
if resolution == promise then
return promise:_reject("selfResolutionError")
end
if getmetatable(resolution) == PROMISE then
local resolve2, reject2 = CreateResolvingFunctions(promise)
local success, val = pcall(resolution.then_, resolution, resolve2, reject2)
if not success then
return reject2(val)
end
else
return promise:_fulfill(resolution)
end
end
reject = function(reason)
if already_resolved then return end
already_resolved = true
return promise:_reject(reason)
end
return resolve, reject
end
Promise = setmetatable({}, {
__call = function(_, executor)
if executor == nil then
error("bad argument #1 to 'Promise' (callable expected, got nil)")
end
if not check_callable(executor) then
error("bad argument #1 to 'Promise' (callable expected, got non-callable)")
end
local promise = setmetatable({
_state = PromiseState.pending,
_fulfill_reactions = {},
_reject_reactions = {}
}, PROMISE)
local resolve, reject = CreateResolvingFunctions(promise)
local success, val = pcall(executor, resolve, reject)
if not success then
reject(val)
end
return promise
end
})
function Promise.reject(reason)
return Promise(function(resolve, reject)
reject(reason)
end)
end
function Promise.resolve(val)
if getmetatable(val) == PROMISE then
return val
end
return Promise(function(resolve, reject)
resolve(val)
end)
end
function Promise.all(promises)
local new_promise_resolve, new_promise_reject
local new_promise = Promise(function(res, rej)
new_promise_resolve, new_promise_reject = res, rej
end)
local result_capability = {
promise = new_promise,
resolve = new_promise_resolve,
reject = new_promise_reject
}
local values = {}
local remaining_elements_count = #promises
for i = 1, #promises do
local next_promise = Promise.resolve(promises[i])
local already_called = false
local resolve_element = function(val)
if already_called then return end
already_called = true
values[i] = val
remaining_elements_count = remaining_elements_count - 1
if remaining_elements_count == 0 then
return result_capability.resolve(values)
end
end
next_promise:then_(resolve_element, result_capability.reject)
end
remaining_elements_count = remaining_elements_count - 1
if remaining_elements_count == 0 then
result_capability.resolve(values)
end
return result_capability.promise
end
function Promise.race(promises)
local new_promise_resolve, new_promise_reject
local new_promise = Promise(function(res, rej)
new_promise_resolve, new_promise_reject = res, rej
end)
local result_capability = {
promise = new_promise,
resolve = new_promise_resolve,
reject = new_promise_reject
}
for i = 1, #promises do
local next_promise = Promise.resolve(promises[i])
next_promise:then_(result_capability.resolve, result_capability.reject)
end
return result_capability.promise
end
return Promise
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment