Skip to content

Instantly share code, notes, and snippets.

@starwing
Last active November 5, 2021 06:26
Show Gist options
  • Save starwing/0a475c45671e760206f709012fe20549 to your computer and use it in GitHub Desktop.
Save starwing/0a475c45671e760206f709012fe20549 to your computer and use it in GitHub Desktop.
Promise for Lua
local assert = assert
local type = type
local getmetatable = getmetatable
local setmetatable = setmetatable
local xpcall = xpcall
local debug_traceback = debug.traceback
local Promise = {} do
local Fulfilled = "Fulfilled"
local Rejected = "Rejected"
local Thenable = {}
Thenable.__name = "Promise"
Thenable.__index = Thenable
local function execute_handlers(self)
assert(self.state)
local value, err = self.value
if self.state == Rejected then
value, err = nil, value
end
local i = 1
while i <= #self do
self[i](value, err)
i = i + 1
end
for i = 1, #self do
self[i] = nil
end
end
local function transmit(self, state, value)
if self.state or not state or value == self then
return
end
local subscribe = type(value) == "table" and value.subscribe
if not subscribe then
local mt = getmetatable(value)
local index = mt and mt.__index
subscribe = index and index.subscribe
end
if subscribe then
local function next_handler(value, err)
if err then
transmit(self, Rejected, err)
else
transmit(self, Fulfilled, value)
end
end
return subscribe(value, next_handler)
end
self.state = state
self.value = value
return execute_handlers(self)
end
local function new(f)
local self = setmetatable({state = nil, value = nil}, Thenable)
local ok, v = xpcall(f, debug_traceback, self)
if not ok then
transmit(self, Rejected, v)
elseif v then
transmit(self, Fulfilled, v)
end
return self
end
function Promise.new(f)
return new(f)
end
function Promise.resolve(value)
return new(function(p) return p:resolve(value) end)
end
function Promise.reject(err)
return new(function(p) return p:reject(err) end)
end
function Thenable:__tostring()
return ("Promise: %p { state = %s, value = %s }"):format(
self, self.state or "Pending", self.value)
end
function Thenable:subscribe(h)
if not self.state then
self[#self+1] = h
else
local value, err = self.value
if self.state == Rejected then
value, err = nil, value
end
h(value, err)
end
end
function Thenable:next(fulfilled, rejected)
return new(function(p)
local function next_handler(value, err)
if err then
if rejected then
local ok, v = xpcall(rejected, debug_traceback, err)
if ok and v then return p:resolve(v) end
err = v or err
end
return p:reject(err)
else
if fulfilled then
local ok, v = xpcall(fulfilled, debug_traceback, value)
if not ok then return p:reject(v) end
value = v or value
end
return p:resolve(value)
end
end
return self:subscribe(next_handler)
end)
end
function Thenable:catch(onRejected)
return self:next(nil, onRejected)
end
function Thenable:resolve(value)
return transmit(self, Fulfilled, value)
end
function Thenable:reject(err)
return transmit(self, Rejected, err)
end
end
local function test()
local cnt = 0
Promise.reject(1):
catch(function(err)
assert(err == 1)
cnt = cnt + 1
return 2
end):
next(function(v)
assert(v == 2)
cnt = cnt + 1
end)
assert(cnt == 2)
cnt = 0
Promise.resolve(1):
next(function(v)
assert(v == 1)
cnt = cnt + 1
return Promise.resolve(v + 1)
end):
next(function(v)
assert(v == 2)
cnt = cnt + 1
return Promise.resolve(v + 2)
end):
next(function(v)
assert(v == 4)
cnt = cnt + 1
end):
next(function(v)
assert(v == 4)
cnt = cnt + 1
end):
next(function(v)
assert(v == 4)
cnt = cnt + 1
error("foo")
end):
catch(function(err)
assert(err:match "foo")
cnt = cnt + 1
end)
assert(cnt == 6)
cnt = 0
Promise.resolve(1)
:next(nil)
:next(nil)
:next(nil)
:next(nil)
:next(function(v)
assert(v == 1)
cnt = cnt + 1
end)
assert(cnt == 1)
cnt = 0
local done
Promise.new(function(p)
cnt = cnt + 1
done = function()
p:resolve(1)
end
end):
next(function(v)
assert(v == 1)
cnt = cnt + 1
end)
assert(cnt == 1)
done()
assert(cnt == 2)
end
test()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment