Last active
February 14, 2017 14:29
-
-
Save bmwalters/f44adad176bb1101292278e4f624f811 to your computer and use it in GitHub Desktop.
ES2015 Promises implemented in Lua 5.3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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