Update promise_tech.lua

This commit is contained in:
VorTechnix 2024-10-10 09:22:27 -07:00
parent 269a73be48
commit 30e154944d
No known key found for this signature in database
GPG Key ID: 091E91A69545D5BA

@ -1,26 +1,37 @@
-- ██████ ██████ ██████ ███ ███ ██ ███████ ███████
-- ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██
-- ██████ ██████ ██ ██ ██ ████ ██ ██ ███████ █████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██ ██████ ██ ██ ██ ███████ ███████
--- Javascript Promises, implemented in Lua
-- In other words, a wrapper to manage asynchronous operations.
-- Due to limitations of Lua, while this Promise API is similar it isn't exactly the same as in JS.
--
-- Also, .then_() does not return a thenable value but the SAME ORIGINAL promise, as we stack up all functions and then execute them in order once you call .run(). This has the subtle implication that Promise.state is not set to "fulfilled" until ALL functions in the chain have been called.
--
-- Additionally, every .then_(function(...) end) MAY return a promise themselves if they wish. These promises WILL be automatically executed when you call .run() on the PARENT promise, as they are considered required for the parent Promise function chain to run to completion.
-- @class worldeditadditions_core.Promise
--- @class worldeditadditions_core.Promise
local Promise = {}
setmetatable(Promise, {__tostring = function(self) return "Promise" end})
Promise.__index = Promise
Promise.__name = "Promise" -- A hack to allow identification in wea.inspect
Promise.__tostring = function(self) return "Promise: " .. self.state end
--- Creates a new Promise instance.
-- @param fn <function>: The function to wrap into a promise.
Promise.new = function(fn)
-- resolve must be a function
if type(fn) ~= "function" then
error("Error (Promise.new): First argument (fn) must be a function")
error("Error (Promise.new): Argument @position 1 (fn) must be a function")
end
local result = {
-- State can be "pending", "fulfilled", or "rejected"
-- Any state other than "pending" means the promise has been settled
-- and become "locked" enable to be acted on again
state = "pending",
-- The force_reject flag is to be used to facilitate non error rejections
-- If set to true this flag will be passed to child promises
force_reject = false,
-- The function to execute when the promise is settled
fn = fn
}
setmetatable(result, Promise)
@ -29,80 +40,174 @@ Promise.new = function(fn)
end
--[[
*************************
Instance methods
Local helpers
*************************
--]]
--- A dummy function
local f = function(val) end
-- Table tweaks (because this is for Minetest)
--- @class table
local table = table
if not table.unpack then table.unpack = unpack end
table.join = function(tbl, sep)
local function fn_iter(tbl,sep,i)
if i < #tbl then
return (tostring(tbl[i]) or "").. sep .. fn_iter(tbl,sep,i+1)
else return (tostring(tbl[i]) or "") end
end
return fn_iter(tbl,sep,1)
end
--- Warning wrapper
local warn = warn
if warn then warn("@on")
else warn = minetest and function(...)
minetest.log("warning", table.concat(arg,"\t"))
end or print
end
-- A handler for erors where a function is expected
-- @param called_from <string>: The function of Promise that called the error
-- @param position <string>: The position of the argument that caused the error (e.g. "First" or "Second")
-- @param arg_name <string>: The name of the argument that caused the error (e.g. "onFulfilled")
-- @param must_be <string>: The type of the argument that caused the error (e.g. "function" or "function or nil")
-- @param self_problem <bool>: Whether the error is a self-promblem or not (if called_from is supposed to be used with ":" instead of ".")
-- @return <string>: The error/warning message
local function_type_warn = function(called_from, position, arg_name, must_be, self_problem)
local cat = self_problem and ":" or "."
local err_str = string.format(
"Error (Promise%s%s): Argument @position %s (%s) must be a %s",
cat, called_from, position, arg_name, must_be)
-- local err_str = "Error (Promise" .. cat .. called_from .. "): " .. position .. " argument (" .. arg_name .. ") must be " .. must_be
if self_problem then
err_str = string.format(
"%s\nAre you using .%s() instead of :%s()?",
err_str, called_from, called_from)
end
return err_str
end
local type_enforce = function(called_from, args)
local err_str = nil
for i, arg in ipairs(args) do
local is_err = true
for _, should_be in ipairs(arg.should_be) do
-- First handle metatables
if type(arg.val) == "table" and type(should_be) == "table" and getmetatable(arg.val) == should_be then
is_err = false
elseif type(arg.val) == should_be then
is_err = false
end
end
if is_err then
err_str = function_type_warn(called_from, i, arg.name, table.join(arg.should_be, " or "), arg.name == "self" and true or false)
if arg.error then error(err_str)
else warn(err_str) end
end
end
end
--[[
*************************
Instance methods
*************************
--]]
--- Then function for promises
-- @param onFulfilled <function>: The function to call if the promise is fulfilled
-- @param onRejected <function>: The function to call if the promise is rejected
-- @param onFulfilled <function | nil>: The function to call if the promise is fulfilled
-- @param onRejected <function | nil>: The function to call if the promise is rejected
-- @return A promise object containing a table of results
Promise.then_ = function(self, onFulfilled, onRejected)
type_enforce("then_",{
{name = "self", val = self, should_be = {Promise}, error = true},
{name = "onFulfilled", val = onFulfilled, should_be = {"function", "nil"}, error = true},
{name = "onRejected", val = onRejected, should_be = {"function", "nil"}, error = true},
})
-- onFulfilled must be a function or nil
if onFulfilled == nil then onFulfilled = f
elseif type(onFulfilled) ~= "function" then
error("Error (Promise.then_): First argument (onFulfilled) must be a function or nil")
end
if onFulfilled == nil then onFulfilled = f end
-- onRejected must be a function or nil
if onRejected == nil then onRejected = f
elseif type(onRejected) ~= "function" then
error("Error (Promise.then_): Second argument (onRejected) must be a function or nil")
end
if onRejected == nil then onRejected = f end
-- If self.state is not "pending" then error
if self.state ~= "pending" then
return Promise.reject("Error (Promise.then_): Promise is already " .. self.state)
end
-- Make locals to collect the results of self.fn
local result, force_reject = {nil}, self.force_reject
-- Local resolve and reject functions
local _resolve = function(value) result[1] = value end
local _reject = function(value)
result[1] = value
force_reject = true
local result = {
val = nil,
force_reject = self.force_reject,
success = true,
err = nil
}
result.update = function(val, rej)
if result.val == nil then
result.val = val
if rej == true then result.force_reject = true end
end
end
-- Local resolve and reject functions
local _resolve = function(value) result.update(value) end
local _reject = function(value) result.update(value, true) end
-- Call self.fn
local success, err = pcall(self.fn, _resolve, _reject)
result.success, result.err = pcall(self.fn, _resolve, _reject)
-- Return a new promise with the results
if success and not force_reject then
onFulfilled(result[1])
if result.success and not result.force_reject then
onFulfilled(result.val)
self.state = "fulfilled"
return Promise.resolve(result[1])
return Promise.resolve(result.val)
else
onRejected(result[1])
self.state = "rejected"
return Promise.reject(success and result[1] or err)
return Promise.reject(result.success and result.val or result.err)
end
end
--[[
tmp = Promise.new(function(resolve, reject)
-- In 10 seconds call resolve(20)
setTimeout(10, resolve, 20)
end)
tmp:then_(function(value) print("Value", value) end, function(err) print("Error", err) end)
]]
--- Catch function for promises
-- @param onRejected <function>: The function to call if the promise is rejected
-- @return A promise object
Promise.catch = function(self, onRejected)
-- onRejected must be a function
if type(onRejected) ~= "function" then
error("Error (Promise.catch): First argument (onRejected) must be a function")
function_type_warn("catch", "First", "onRejected", "a function", true)
end
return Promise.then_(self, nil, onRejected)
end
--- Finally function for promises
-- Can be used to clone the current promise as it does not settle it
-- @param onFinally <function>: The function to call if the promise becomes settled
-- @return A promise object
-- @return A promise object containing the function of the current promise
Promise.finally = function(self, onFinally)
-- onFinally must be a function
if type(onFinally) ~= "function" then
function_type_warn("finally", "First", "onFinally", "a function", true)
end
onFinally()
return Promise.new(self.fn)
end
--[[
*************************
Static methods
@ -132,6 +237,45 @@ end
-- TODO: Implement static methods (all, any, race etc.)
--[[
*************************
Non JS methods
*************************
--]]
--- Poke a promise with a debug stick and see what happens
-- Also for those who want a table returned instead of a promise
-- @param promise <promise>: The promise to poke
-- @return boolean, table: true if no error, the settled state and value of the promise
Promise.poke = function(promise)
local result = {value=nil, state=nil}
-- Check that the argument is a promise
if not Promise.is_promise(promise) then
local _, err = pcall(function_type_warn, "poke", "First", "promise", "a Promise instance")
result.value = err
return false, result -- Stop execution and return the error
end
local set_result_value = function(value) result.value = value end
-- Operate on the promise based on its state and force_reject flag
if promise.state ~= "pending" then
promise:catch(f):catch(set_result_value)
result.state = promise.state
return false, result
elseif promise.force_reject then
promise:catch(set_result_value)
result.state = promise.state
else
promise:then_(set_result_value, set_result_value)
result.state = promise.state
end
return true, result
end
return Promise
--- TESTS
@ -172,4 +316,38 @@ Vx2 = 0
test():then_(function(value) Vx2 = value end, function(value) print("caught rejection, value", value) end):
then_(function(value) print("Sqrt is", math.sqrt(value)) end)
if Vx2 ~= 0 then print("Vx2", Vx2) end
]]
-- Security test
tmp = {val = nil, err = nil}
tmp_set = function(val)
tmp["val"] = val
print("DEBUG tmp_set val", val)
end
tmp_err = function(err)
tmp["err"] = err
print("DEBUG tmp_err err", err)
end
tmp1 = Promise.resolve(3)
tmp1:then_(tmp_set, tmp_err)
-- Prints "DEBUG tmp_set val 3"
print(tmp.val, tmp.err)
-- Prints "3 nil"
tmp1:then_(tmp_set, tmp_err)
-- Prints nothing
print(tmp.val, tmp.err)
-- Still returns "3 nil"
-- But there was no DEBUG print
tmp1:then_(tmp_set, tmp_err):catch(tmp_err)
-- Prints "DEBUG tmp_err err Error (Promise.then_): Promise is already fulfilled"
print(tmp.val, tmp.err)
-- Prints "3 Error (Promise.then_): Promise is already fulfilled"
Now we get our error: "Error (Promise.then_): Promise is already fulfilled"
This functionality is a safeguard against executing the function of a promise more than once
The promise will simply "short-circuit" the return a new promise with the error without evaluating anything
--]]