2018-01-26 01:21:58 +00:00
|
|
|
--[[
|
|
|
|
An implementation of Promises similar to Promise/A+.
|
|
|
|
]]
|
|
|
|
|
2019-09-28 04:44:18 +00:00
|
|
|
local ERROR_YIELD_NEW = "Yielding inside Promise.new is not allowed! Use Promise.async or create a new thread in the Promise executor!"
|
|
|
|
local ERROR_YIELD_THEN = "Yielding inside andThen/catch is not allowed! Instead, return a new Promise from andThen/catch."
|
|
|
|
local ERROR_NON_PROMISE_IN_LIST = "Non-promise value passed into %s at index %s"
|
|
|
|
local ERROR_NON_LIST = "Please pass a list of promises to %s"
|
2019-09-28 09:14:53 +00:00
|
|
|
local ERROR_NON_FUNCTION = "Please pass a handler function to %s!"
|
2019-09-28 04:44:18 +00:00
|
|
|
|
2019-09-10 19:34:06 +00:00
|
|
|
local RunService = game:GetService("RunService")
|
2018-01-26 01:21:58 +00:00
|
|
|
|
2018-07-05 22:49:25 +00:00
|
|
|
--[[
|
|
|
|
Packs a number of arguments into a table and returns its length.
|
|
|
|
|
|
|
|
Used to cajole varargs without dropping sparse values.
|
|
|
|
]]
|
|
|
|
local function pack(...)
|
|
|
|
local len = select("#", ...)
|
|
|
|
|
|
|
|
return len, { ... }
|
|
|
|
end
|
|
|
|
|
2019-09-28 04:19:29 +00:00
|
|
|
--[[
|
|
|
|
Returns first value (success), and packs all following values.
|
|
|
|
]]
|
2019-09-18 20:42:15 +00:00
|
|
|
local function packResult(...)
|
|
|
|
local result = (...)
|
2018-07-05 22:49:25 +00:00
|
|
|
|
2019-09-18 20:42:15 +00:00
|
|
|
return result, pack(select(2, ...))
|
|
|
|
end
|
2018-07-05 22:49:25 +00:00
|
|
|
|
2019-09-28 04:19:29 +00:00
|
|
|
--[[
|
|
|
|
Calls a non-yielding function in a new coroutine.
|
|
|
|
|
|
|
|
Handles errors if they happen.
|
|
|
|
]]
|
2019-09-28 04:44:18 +00:00
|
|
|
local function ppcall(yieldError, callback, ...)
|
2019-09-28 05:06:29 +00:00
|
|
|
-- Wrapped because C functions can't be passed to coroutine.create!
|
|
|
|
local co = coroutine.create(function(...)
|
|
|
|
return callback(...)
|
|
|
|
end)
|
2018-05-21 21:10:38 +00:00
|
|
|
|
2019-09-18 20:42:15 +00:00
|
|
|
local ok, len, result = packResult(coroutine.resume(co, ...))
|
|
|
|
|
|
|
|
if ok and coroutine.status(co) ~= "dead" then
|
2019-09-28 04:44:18 +00:00
|
|
|
error(yieldError, 2)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
2018-05-21 21:10:38 +00:00
|
|
|
|
2019-09-18 20:42:15 +00:00
|
|
|
return ok, len, result
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Creates a function that invokes a callback with correct error handling and
|
|
|
|
resolution mechanisms.
|
|
|
|
]]
|
2019-09-28 09:14:53 +00:00
|
|
|
local function createAdvancer(traceback, callback, resolve, reject)
|
2018-01-26 01:21:58 +00:00
|
|
|
return function(...)
|
2019-09-28 04:44:18 +00:00
|
|
|
local ok, resultLength, result = ppcall(ERROR_YIELD_THEN, callback, ...)
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
if ok then
|
2019-09-18 20:42:15 +00:00
|
|
|
resolve(unpack(result, 1, resultLength))
|
2018-01-26 01:21:58 +00:00
|
|
|
else
|
2019-09-28 09:14:53 +00:00
|
|
|
reject(result[1], traceback)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function isEmpty(t)
|
|
|
|
return next(t) == nil
|
|
|
|
end
|
|
|
|
|
|
|
|
local Promise = {}
|
2018-09-14 20:43:22 +00:00
|
|
|
Promise.prototype = {}
|
|
|
|
Promise.__index = Promise.prototype
|
2018-01-26 01:21:58 +00:00
|
|
|
|
2019-09-28 05:56:58 +00:00
|
|
|
Promise.Status = setmetatable({
|
|
|
|
Started = "Started",
|
|
|
|
Resolved = "Resolved",
|
|
|
|
Rejected = "Rejected",
|
|
|
|
Cancelled = "Cancelled",
|
|
|
|
}, {
|
|
|
|
__index = function(_, k)
|
|
|
|
error(("%s is not in Promise.Status!"):format(k), 2)
|
|
|
|
end
|
|
|
|
})
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
--[[
|
|
|
|
Constructs a new Promise with the given initializing callback.
|
|
|
|
|
|
|
|
This is generally only called when directly wrapping a non-promise API into
|
|
|
|
a promise-based version.
|
|
|
|
|
|
|
|
The callback will receive 'resolve' and 'reject' methods, used to start
|
|
|
|
invoking the promise chain.
|
|
|
|
|
2018-10-24 06:33:30 +00:00
|
|
|
Second parameter, parent, is used internally for tracking the "parent" in a
|
|
|
|
promise chain. External code shouldn't need to worry about this.
|
2018-01-26 01:21:58 +00:00
|
|
|
]]
|
2018-10-24 06:33:30 +00:00
|
|
|
function Promise.new(callback, parent)
|
|
|
|
if parent ~= nil and not Promise.is(parent) then
|
|
|
|
error("Argument #2 to Promise.new must be a promise or nil", 2)
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:47:51 +00:00
|
|
|
local self = {
|
2018-01-26 01:21:58 +00:00
|
|
|
-- Used to locate where a promise was created
|
|
|
|
_source = debug.traceback(),
|
|
|
|
|
|
|
|
_status = Promise.Status.Started,
|
|
|
|
|
2019-09-28 09:14:53 +00:00
|
|
|
-- Will be set to the Lua error string if it occurs while executing.
|
|
|
|
_error = nil,
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
-- A table containing a list of all results, whether success or failure.
|
|
|
|
-- Only valid if _status is set to something besides Started
|
2018-06-17 02:42:14 +00:00
|
|
|
_values = nil,
|
2018-01-26 01:21:58 +00:00
|
|
|
|
2018-06-17 02:47:21 +00:00
|
|
|
-- Lua doesn't like sparse arrays very much, so we explicitly store the
|
|
|
|
-- length of _values to handle middle nils.
|
|
|
|
_valuesLength = -1,
|
|
|
|
|
2019-09-12 07:58:56 +00:00
|
|
|
-- Tracks if this Promise has no error observers..
|
|
|
|
_unhandledRejection = true,
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
-- Queues representing functions we should invoke when we update!
|
|
|
|
_queuedResolve = {},
|
|
|
|
_queuedReject = {},
|
2018-10-24 06:33:30 +00:00
|
|
|
_queuedFinally = {},
|
2018-10-23 23:12:05 +00:00
|
|
|
|
|
|
|
-- The function to run when/if this promise is cancelled.
|
|
|
|
_cancellationHook = nil,
|
2018-10-24 06:33:30 +00:00
|
|
|
|
|
|
|
-- The "parent" of this promise in a promise chain. Required for
|
|
|
|
-- cancellation propagation.
|
|
|
|
_parent = parent,
|
|
|
|
|
2019-09-12 07:58:56 +00:00
|
|
|
_consumers = setmetatable({}, {
|
|
|
|
__mode = "k";
|
|
|
|
}),
|
2018-01-26 01:21:58 +00:00
|
|
|
}
|
|
|
|
|
2019-09-12 07:58:56 +00:00
|
|
|
if parent and parent._status == Promise.Status.Started then
|
|
|
|
parent._consumers[self] = true
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:47:51 +00:00
|
|
|
setmetatable(self, Promise)
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
local function resolve(...)
|
2018-09-14 20:47:51 +00:00
|
|
|
self:_resolve(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
local function reject(...)
|
2018-09-14 20:47:51 +00:00
|
|
|
self:_reject(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
2018-10-23 23:12:05 +00:00
|
|
|
local function onCancel(cancellationHook)
|
2019-09-12 07:58:56 +00:00
|
|
|
if cancellationHook then
|
|
|
|
if self._status == Promise.Status.Cancelled then
|
|
|
|
cancellationHook()
|
|
|
|
else
|
|
|
|
self._cancellationHook = cancellationHook
|
|
|
|
end
|
2018-11-09 05:00:40 +00:00
|
|
|
end
|
2019-09-12 07:58:56 +00:00
|
|
|
|
|
|
|
return self._status == Promise.Status.Cancelled
|
2018-10-23 23:12:05 +00:00
|
|
|
end
|
|
|
|
|
2019-09-28 09:14:53 +00:00
|
|
|
local ok, _, result = ppcall(
|
|
|
|
ERROR_YIELD_NEW,
|
|
|
|
callback,
|
|
|
|
resolve,
|
|
|
|
reject,
|
|
|
|
onCancel
|
|
|
|
)
|
2018-01-26 01:21:58 +00:00
|
|
|
|
2019-09-28 09:14:53 +00:00
|
|
|
if not ok then
|
|
|
|
self._error = result[1] or "error"
|
|
|
|
reject((result[1] or "error") .. "\n" .. self._source)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
2018-09-14 20:47:51 +00:00
|
|
|
return self
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
2019-09-28 09:14:53 +00:00
|
|
|
function Promise._newWithSelf(executor, ...)
|
|
|
|
local args
|
|
|
|
local promise = Promise.new(function(...)
|
|
|
|
args = {...}
|
|
|
|
end, ...)
|
|
|
|
|
|
|
|
executor(promise, unpack(args))
|
|
|
|
|
|
|
|
return promise
|
|
|
|
end
|
|
|
|
|
|
|
|
function Promise._new(traceback, executor, ...)
|
|
|
|
return Promise._newWithSelf(function(self, resolve, reject)
|
|
|
|
self._source = traceback
|
|
|
|
|
|
|
|
executor(resolve, function(err, traceback)
|
|
|
|
err = err or "error"
|
|
|
|
traceback = traceback or ""
|
|
|
|
self._error = err
|
|
|
|
reject(err .. "\n" .. traceback)
|
|
|
|
end)
|
|
|
|
end, ...)
|
|
|
|
end
|
|
|
|
|
2019-09-10 21:12:00 +00:00
|
|
|
--[[
|
2019-09-18 20:42:15 +00:00
|
|
|
Promise.new, except pcall on a new thread is automatic.
|
2019-09-10 21:12:00 +00:00
|
|
|
]]
|
|
|
|
function Promise.async(callback)
|
2019-09-18 20:42:15 +00:00
|
|
|
local traceback = debug.traceback()
|
2019-09-28 09:14:53 +00:00
|
|
|
local promise
|
|
|
|
promise = Promise.new(function(resolve, reject, onCancel)
|
2019-09-18 20:42:15 +00:00
|
|
|
local connection
|
|
|
|
connection = RunService.Heartbeat:Connect(function()
|
|
|
|
connection:Disconnect()
|
|
|
|
local ok, err = pcall(callback, resolve, reject, onCancel)
|
2019-09-10 21:12:00 +00:00
|
|
|
|
2019-09-18 20:42:15 +00:00
|
|
|
if not ok then
|
2019-09-28 09:14:53 +00:00
|
|
|
promise._error = err or "error"
|
2019-09-18 20:42:15 +00:00
|
|
|
reject(err .. "\n" .. traceback)
|
|
|
|
end
|
|
|
|
end)
|
2019-09-10 19:34:06 +00:00
|
|
|
end)
|
2019-09-28 09:14:53 +00:00
|
|
|
|
|
|
|
return promise
|
2019-09-10 19:34:06 +00:00
|
|
|
end
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
--[[
|
|
|
|
Create a promise that represents the immediately resolved value.
|
|
|
|
]]
|
2019-09-28 21:48:55 +00:00
|
|
|
function Promise.resolve(...)
|
|
|
|
local length, values = pack(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
return Promise.new(function(resolve)
|
2019-09-28 21:48:55 +00:00
|
|
|
resolve(unpack(values, 1, length))
|
2018-01-26 01:21:58 +00:00
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Create a promise that represents the immediately rejected value.
|
|
|
|
]]
|
2019-09-28 21:48:55 +00:00
|
|
|
function Promise.reject(...)
|
|
|
|
local length, values = pack(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
return Promise.new(function(_, reject)
|
2019-09-28 21:48:55 +00:00
|
|
|
reject(unpack(values, 1, length))
|
2018-01-26 01:21:58 +00:00
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2019-09-28 22:54:49 +00:00
|
|
|
--[[
|
|
|
|
Begins a Promise chain, turning synchronous errors into rejections.
|
|
|
|
]]
|
|
|
|
function Promise.try(...)
|
|
|
|
return Promise.resolve():andThenCall(...)
|
|
|
|
end
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
--[[
|
|
|
|
Returns a new promise that:
|
|
|
|
* is resolved when all input promises resolve
|
|
|
|
* is rejected if ANY input promises reject
|
|
|
|
]]
|
2018-09-14 18:17:06 +00:00
|
|
|
function Promise.all(promises)
|
|
|
|
if type(promises) ~= "table" then
|
2019-09-28 04:44:18 +00:00
|
|
|
error(ERROR_NON_LIST:format("Promise.all"), 2)
|
2018-09-14 18:17:06 +00:00
|
|
|
end
|
|
|
|
|
2018-09-14 20:47:10 +00:00
|
|
|
-- We need to check that each value is a promise here so that we can produce
|
|
|
|
-- a proper error rather than a rejected promise with our error.
|
2019-09-27 22:46:10 +00:00
|
|
|
for i, promise in pairs(promises) do
|
|
|
|
if not Promise.is(promise) then
|
2019-09-28 04:44:18 +00:00
|
|
|
error((ERROR_NON_PROMISE_IN_LIST):format("Promise.all", tostring(i)), 2)
|
2018-09-14 18:17:06 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-09-27 22:46:10 +00:00
|
|
|
-- If there are no values then return an already resolved promise.
|
|
|
|
if #promises == 0 then
|
|
|
|
return Promise.resolve({})
|
|
|
|
end
|
|
|
|
|
2018-09-14 18:17:06 +00:00
|
|
|
return Promise.new(function(resolve, reject)
|
|
|
|
-- An array to contain our resolved values from the given promises.
|
|
|
|
local resolvedValues = {}
|
2019-09-28 04:13:30 +00:00
|
|
|
local newPromises = {}
|
2018-09-14 20:47:10 +00:00
|
|
|
|
|
|
|
-- Keep a count of resolved promises because just checking the resolved
|
|
|
|
-- values length wouldn't account for promises that resolve with nil.
|
2018-09-14 18:17:06 +00:00
|
|
|
local resolvedCount = 0
|
2019-09-28 04:13:30 +00:00
|
|
|
local finalized = false
|
2018-09-14 18:17:06 +00:00
|
|
|
|
|
|
|
-- Called when a single value is resolved and resolves if all are done.
|
|
|
|
local function resolveOne(i, ...)
|
|
|
|
resolvedValues[i] = ...
|
|
|
|
resolvedCount = resolvedCount + 1
|
|
|
|
|
|
|
|
if resolvedCount == #promises then
|
|
|
|
resolve(resolvedValues)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:47:10 +00:00
|
|
|
-- We can assume the values inside `promises` are all promises since we
|
|
|
|
-- checked above.
|
2018-09-14 18:17:06 +00:00
|
|
|
for i = 1, #promises do
|
2019-09-28 04:13:30 +00:00
|
|
|
if finalized then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
|
|
|
|
table.insert(
|
|
|
|
newPromises,
|
|
|
|
promises[i]:andThen(
|
|
|
|
function(...)
|
|
|
|
resolveOne(i, ...)
|
|
|
|
end,
|
|
|
|
function(...)
|
|
|
|
for _, promise in ipairs(newPromises) do
|
|
|
|
promise:cancel()
|
|
|
|
end
|
|
|
|
finalized = true
|
|
|
|
|
|
|
|
reject(...)
|
|
|
|
end
|
|
|
|
)
|
2018-09-14 18:31:24 +00:00
|
|
|
)
|
2018-09-14 18:17:06 +00:00
|
|
|
end
|
|
|
|
end)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
2019-09-10 19:34:06 +00:00
|
|
|
--[[
|
|
|
|
Races a set of Promises and returns the first one that resolves,
|
|
|
|
cancelling the others.
|
|
|
|
]]
|
|
|
|
function Promise.race(promises)
|
2019-09-28 04:44:18 +00:00
|
|
|
assert(type(promises) == "table", ERROR_NON_LIST:format("Promise.race"))
|
2019-09-10 19:34:06 +00:00
|
|
|
|
2019-09-27 22:46:10 +00:00
|
|
|
for i, promise in pairs(promises) do
|
2019-09-28 04:44:18 +00:00
|
|
|
assert(Promise.is(promise), (ERROR_NON_PROMISE_IN_LIST):format("Promise.race", tostring(i)))
|
2019-09-10 19:34:06 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return Promise.new(function(resolve, reject, onCancel)
|
2019-09-28 04:13:30 +00:00
|
|
|
local newPromises = {}
|
|
|
|
|
2019-09-10 19:34:06 +00:00
|
|
|
local function finalize(callback)
|
|
|
|
return function (...)
|
2019-09-28 04:13:30 +00:00
|
|
|
for _, promise in ipairs(newPromises) do
|
2019-09-10 19:34:06 +00:00
|
|
|
promise:cancel()
|
|
|
|
end
|
|
|
|
|
|
|
|
return callback(...)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-09-28 04:13:30 +00:00
|
|
|
if onCancel(finalize(reject)) then
|
|
|
|
return
|
|
|
|
end
|
2019-09-10 19:34:06 +00:00
|
|
|
|
|
|
|
for _, promise in ipairs(promises) do
|
2019-09-28 04:13:30 +00:00
|
|
|
table.insert(
|
|
|
|
newPromises,
|
|
|
|
promise:andThen(finalize(resolve), finalize(reject))
|
|
|
|
)
|
2019-09-10 19:34:06 +00:00
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
--[[
|
|
|
|
Is the given object a Promise instance?
|
|
|
|
]]
|
|
|
|
function Promise.is(object)
|
|
|
|
if type(object) ~= "table" then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2019-09-23 02:58:23 +00:00
|
|
|
return type(object.andThen) == "function"
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
2019-09-12 07:58:56 +00:00
|
|
|
--[[
|
|
|
|
Converts a yielding function into a Promise-returning one.
|
|
|
|
]]
|
2019-09-14 02:58:32 +00:00
|
|
|
function Promise.promisify(callback)
|
2019-09-12 07:58:56 +00:00
|
|
|
return function(...)
|
2019-09-28 21:38:35 +00:00
|
|
|
local traceback = debug.traceback()
|
2019-09-12 07:58:56 +00:00
|
|
|
local length, values = pack(...)
|
2019-09-28 21:38:35 +00:00
|
|
|
return Promise.new(function(resolve, reject)
|
2019-09-15 06:50:53 +00:00
|
|
|
coroutine.wrap(function()
|
2019-09-28 21:38:35 +00:00
|
|
|
local ok, resultLength, resultValues = packResult(pcall(callback, unpack(values, 1, length)))
|
|
|
|
if ok then
|
|
|
|
resolve(unpack(resultValues, 1, resultLength))
|
|
|
|
else
|
|
|
|
reject((resultValues[1] or "error") .. "\n" .. traceback)
|
|
|
|
end
|
2019-09-15 06:50:53 +00:00
|
|
|
end)()
|
2019-09-12 07:58:56 +00:00
|
|
|
end)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:getStatus()
|
2018-06-17 02:40:57 +00:00
|
|
|
return self._status
|
|
|
|
end
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
--[[
|
|
|
|
Creates a new promise that receives the result of this promise.
|
|
|
|
|
|
|
|
The given callbacks are invoked depending on that result.
|
|
|
|
]]
|
2019-09-28 09:14:53 +00:00
|
|
|
function Promise.prototype:_andThen(traceback, successHandler, failureHandler)
|
2018-01-26 01:21:58 +00:00
|
|
|
self._unhandledRejection = false
|
|
|
|
|
|
|
|
-- Create a new promise to follow this part of the chain
|
2019-09-28 09:14:53 +00:00
|
|
|
return Promise._new(traceback, function(resolve, reject)
|
2018-01-26 01:21:58 +00:00
|
|
|
-- Our default callbacks just pass values onto the next promise.
|
|
|
|
-- This lets success and failure cascade correctly!
|
|
|
|
|
|
|
|
local successCallback = resolve
|
|
|
|
if successHandler then
|
2019-09-28 09:14:53 +00:00
|
|
|
successCallback = createAdvancer(
|
|
|
|
traceback,
|
|
|
|
successHandler,
|
|
|
|
resolve,
|
|
|
|
reject
|
|
|
|
)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
local failureCallback = reject
|
|
|
|
if failureHandler then
|
2019-09-28 09:14:53 +00:00
|
|
|
failureCallback = createAdvancer(
|
|
|
|
traceback,
|
|
|
|
failureHandler,
|
|
|
|
resolve,
|
|
|
|
reject
|
|
|
|
)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
if self._status == Promise.Status.Started then
|
2018-04-11 22:26:54 +00:00
|
|
|
-- If we haven't resolved yet, put ourselves into the queue
|
2018-01-26 01:21:58 +00:00
|
|
|
table.insert(self._queuedResolve, successCallback)
|
|
|
|
table.insert(self._queuedReject, failureCallback)
|
|
|
|
elseif self._status == Promise.Status.Resolved then
|
|
|
|
-- This promise has already resolved! Trigger success immediately.
|
2018-06-17 03:04:01 +00:00
|
|
|
successCallback(unpack(self._values, 1, self._valuesLength))
|
2018-01-26 01:21:58 +00:00
|
|
|
elseif self._status == Promise.Status.Rejected then
|
|
|
|
-- This promise died a terrible death! Trigger failure immediately.
|
2018-06-17 03:04:01 +00:00
|
|
|
failureCallback(unpack(self._values, 1, self._valuesLength))
|
2018-10-24 06:33:30 +00:00
|
|
|
elseif self._status == Promise.Status.Cancelled then
|
|
|
|
-- We don't want to call the success handler or the failure handler,
|
|
|
|
-- we just reject this promise outright.
|
|
|
|
reject("Promise is cancelled")
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
2018-10-24 06:33:30 +00:00
|
|
|
end, self)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
2019-09-28 09:14:53 +00:00
|
|
|
function Promise.prototype:andThen(successHandler, failureHandler)
|
|
|
|
assert(
|
|
|
|
successHandler == nil or type(successHandler) == "function",
|
|
|
|
ERROR_NON_FUNCTION:format("Promise:andThen")
|
|
|
|
)
|
|
|
|
assert(
|
|
|
|
failureHandler == nil or type(failureHandler) == "function",
|
|
|
|
ERROR_NON_FUNCTION:format("Promise:andThen")
|
|
|
|
)
|
|
|
|
|
|
|
|
return self:_andThen(debug.traceback(), successHandler, failureHandler)
|
|
|
|
end
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
--[[
|
|
|
|
Used to catch any errors that may have occurred in the promise.
|
|
|
|
]]
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:catch(failureCallback)
|
2019-09-28 09:14:53 +00:00
|
|
|
assert(
|
|
|
|
failureCallback == nil or type(failureCallback) == "function",
|
|
|
|
ERROR_NON_FUNCTION:format("Promise:catch")
|
|
|
|
)
|
|
|
|
return self:_andThen(debug.traceback(), nil, failureCallback)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
2019-09-28 05:33:06 +00:00
|
|
|
--[[
|
|
|
|
Like andThen, but the value passed into the handler is also the
|
|
|
|
value returned from the handler.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:tap(tapCallback)
|
2019-09-28 09:14:53 +00:00
|
|
|
assert(type(tapCallback) == "function", ERROR_NON_FUNCTION:format("Promise:tap"))
|
|
|
|
return self:_andThen(debug.traceback(), function(...)
|
2019-09-28 05:33:06 +00:00
|
|
|
local callbackReturn = tapCallback(...)
|
|
|
|
|
|
|
|
if Promise.is(callbackReturn) then
|
|
|
|
local length, values = pack(...)
|
|
|
|
return callbackReturn:andThen(function()
|
|
|
|
return unpack(values, 1, length)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
return ...
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2019-09-13 00:13:29 +00:00
|
|
|
--[[
|
|
|
|
Calls a callback on `andThen` with specific arguments.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:andThenCall(callback, ...)
|
2019-09-28 09:14:53 +00:00
|
|
|
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:andThenCall"))
|
2019-09-13 00:13:29 +00:00
|
|
|
local length, values = pack(...)
|
2019-09-28 09:14:53 +00:00
|
|
|
return self:_andThen(debug.traceback(), function()
|
2019-09-13 00:13:29 +00:00
|
|
|
return callback(unpack(values, 1, length))
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2019-09-29 02:03:06 +00:00
|
|
|
--[[
|
|
|
|
Shorthand for an andThen handler that returns the given value.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:andThenReturn(...)
|
|
|
|
local length, values = pack(...)
|
|
|
|
return self:_andThen(debug.traceback(), function()
|
|
|
|
return unpack(values, 1, length)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2018-10-23 23:12:05 +00:00
|
|
|
--[[
|
|
|
|
Cancels the promise, disallowing it from rejecting or resolving, and calls
|
|
|
|
the cancellation hook if provided.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:cancel()
|
|
|
|
if self._status ~= Promise.Status.Started then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
self._status = Promise.Status.Cancelled
|
|
|
|
|
|
|
|
if self._cancellationHook then
|
|
|
|
self._cancellationHook()
|
|
|
|
end
|
2018-10-24 06:33:30 +00:00
|
|
|
|
|
|
|
if self._parent then
|
2019-09-12 07:58:56 +00:00
|
|
|
self._parent:_consumerCancelled(self)
|
|
|
|
end
|
|
|
|
|
|
|
|
for child in pairs(self._consumers) do
|
|
|
|
child:cancel()
|
2018-10-24 06:33:30 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
self:_finalize()
|
2018-10-23 23:12:05 +00:00
|
|
|
end
|
|
|
|
|
2018-10-23 23:14:29 +00:00
|
|
|
--[[
|
2018-10-24 06:33:30 +00:00
|
|
|
Used to decrease the number of consumers by 1, and if there are no more,
|
|
|
|
cancel this promise.
|
2018-10-23 23:14:29 +00:00
|
|
|
]]
|
2019-09-12 07:58:56 +00:00
|
|
|
function Promise.prototype:_consumerCancelled(consumer)
|
|
|
|
if self._status ~= Promise.Status.Started then
|
|
|
|
return
|
|
|
|
end
|
2018-10-24 06:33:30 +00:00
|
|
|
|
2019-09-12 07:58:56 +00:00
|
|
|
self._consumers[consumer] = nil
|
|
|
|
|
|
|
|
if next(self._consumers) == nil then
|
2018-10-24 06:33:30 +00:00
|
|
|
self:cancel()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Used to set a handler for when the promise resolves, rejects, or is
|
|
|
|
cancelled. Returns a new promise chained from this promise.
|
|
|
|
]]
|
2019-09-29 02:03:06 +00:00
|
|
|
function Promise.prototype:_finally(traceback, finallyHandler, onlyOk)
|
|
|
|
if not onlyOk then
|
|
|
|
self._unhandledRejection = false
|
|
|
|
end
|
2018-10-24 06:33:30 +00:00
|
|
|
|
|
|
|
-- Return a promise chained off of this promise
|
2019-09-28 09:14:53 +00:00
|
|
|
return Promise._new(traceback, function(resolve, reject)
|
2018-10-24 06:33:30 +00:00
|
|
|
local finallyCallback = resolve
|
|
|
|
if finallyHandler then
|
2019-09-28 09:14:53 +00:00
|
|
|
finallyCallback = createAdvancer(
|
|
|
|
traceback,
|
|
|
|
finallyHandler,
|
|
|
|
resolve,
|
|
|
|
reject
|
|
|
|
)
|
2018-10-24 06:33:30 +00:00
|
|
|
end
|
|
|
|
|
2019-09-29 02:03:06 +00:00
|
|
|
if onlyOk then
|
|
|
|
local callback = finallyCallback
|
|
|
|
finallyCallback = function(...)
|
|
|
|
if self._status == Promise.Status.Rejected then
|
|
|
|
return resolve(self)
|
|
|
|
end
|
|
|
|
|
|
|
|
return callback(...)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-10-24 06:33:30 +00:00
|
|
|
if self._status == Promise.Status.Started then
|
|
|
|
-- The promise is not settled, so queue this.
|
|
|
|
table.insert(self._queuedFinally, finallyCallback)
|
|
|
|
else
|
|
|
|
-- The promise already settled or was cancelled, run the callback now.
|
2019-09-12 07:58:56 +00:00
|
|
|
finallyCallback(self._status)
|
2018-10-24 06:33:30 +00:00
|
|
|
end
|
|
|
|
end, self)
|
2018-10-23 23:14:29 +00:00
|
|
|
end
|
|
|
|
|
2019-09-28 09:14:53 +00:00
|
|
|
function Promise.prototype:finally(finallyHandler)
|
|
|
|
assert(
|
|
|
|
finallyHandler == nil or type(finallyHandler) == "function",
|
|
|
|
ERROR_NON_FUNCTION:format("Promise:finally")
|
|
|
|
)
|
|
|
|
return self:_finally(debug.traceback(), finallyHandler)
|
|
|
|
end
|
|
|
|
|
2019-09-13 00:13:29 +00:00
|
|
|
--[[
|
|
|
|
Calls a callback on `finally` with specific arguments.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:finallyCall(callback, ...)
|
2019-09-28 09:14:53 +00:00
|
|
|
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:finallyCall"))
|
2019-09-13 00:13:29 +00:00
|
|
|
local length, values = pack(...)
|
2019-09-28 09:14:53 +00:00
|
|
|
return self:_finally(debug.traceback(), function()
|
2019-09-13 00:13:29 +00:00
|
|
|
return callback(unpack(values, 1, length))
|
|
|
|
end)
|
|
|
|
end
|
2019-09-29 02:03:06 +00:00
|
|
|
|
|
|
|
--[[
|
|
|
|
Shorthand for a finally handler that returns the given value.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:finallyReturn(...)
|
|
|
|
local length, values = pack(...)
|
|
|
|
return self:_finally(debug.traceback(), function()
|
|
|
|
return unpack(values, 1, length)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Similar to finally, except rejections are propagated through it.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:done(finallyHandler)
|
|
|
|
assert(
|
|
|
|
finallyHandler == nil or type(finallyHandler) == "function",
|
|
|
|
ERROR_NON_FUNCTION:format("Promise:finallyO")
|
|
|
|
)
|
|
|
|
return self:_finally(debug.traceback(), finallyHandler, true)
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Calls a callback on `done` with specific arguments.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:doneCall(callback, ...)
|
|
|
|
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:doneCall"))
|
|
|
|
local length, values = pack(...)
|
|
|
|
return self:_finally(debug.traceback(), function()
|
|
|
|
return callback(unpack(values, 1, length))
|
|
|
|
end, true)
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Shorthand for a done handler that returns the given value.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:doneReturn(...)
|
|
|
|
local length, values = pack(...)
|
|
|
|
return self:_finally(debug.traceback(), function()
|
|
|
|
return unpack(values, 1, length)
|
|
|
|
end, true)
|
|
|
|
end
|
2019-09-13 00:13:29 +00:00
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
--[[
|
|
|
|
Yield until the promise is completed.
|
|
|
|
|
|
|
|
This matches the execution model of normal Roblox functions.
|
|
|
|
]]
|
2019-09-12 07:58:56 +00:00
|
|
|
function Promise.prototype:awaitStatus()
|
2018-01-26 01:21:58 +00:00
|
|
|
self._unhandledRejection = false
|
|
|
|
|
|
|
|
if self._status == Promise.Status.Started then
|
2018-11-09 08:40:40 +00:00
|
|
|
local bindable = Instance.new("BindableEvent")
|
|
|
|
|
|
|
|
self:finally(function()
|
2019-09-12 07:58:56 +00:00
|
|
|
bindable:Fire()
|
2018-11-09 04:50:07 +00:00
|
|
|
end)
|
|
|
|
|
2019-09-12 07:58:56 +00:00
|
|
|
bindable.Event:Wait()
|
2018-11-09 08:40:40 +00:00
|
|
|
bindable:Destroy()
|
2019-09-12 07:58:56 +00:00
|
|
|
end
|
2018-11-09 04:50:07 +00:00
|
|
|
|
2019-09-12 07:58:56 +00:00
|
|
|
if self._status == Promise.Status.Resolved then
|
|
|
|
return self._status, unpack(self._values, 1, self._valuesLength)
|
2018-01-26 01:21:58 +00:00
|
|
|
elseif self._status == Promise.Status.Rejected then
|
2019-09-12 07:58:56 +00:00
|
|
|
return self._status, unpack(self._values, 1, self._valuesLength)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
2018-11-09 04:50:07 +00:00
|
|
|
|
2019-09-12 07:58:56 +00:00
|
|
|
return self._status
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Calls awaitStatus internally, returns (isResolved, values...)
|
|
|
|
]]
|
|
|
|
function Promise.prototype:await(...)
|
|
|
|
local length, result = pack(self:awaitStatus(...))
|
|
|
|
local status = table.remove(result, 1)
|
|
|
|
|
|
|
|
return status == Promise.Status.Resolved, unpack(result, 1, length - 1)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
2019-09-13 00:13:29 +00:00
|
|
|
--[[
|
|
|
|
Calls await and only returns if the Promise resolves.
|
|
|
|
Throws if the Promise rejects or gets cancelled.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:awaitValue(...)
|
|
|
|
local length, result = pack(self:awaitStatus(...))
|
|
|
|
local status = table.remove(result, 1)
|
|
|
|
|
|
|
|
assert(
|
|
|
|
status == Promise.Status.Resolved,
|
|
|
|
tostring(result[1] == nil and "" or result[1])
|
|
|
|
)
|
|
|
|
|
|
|
|
return unpack(result, 1, length - 1)
|
|
|
|
end
|
|
|
|
|
2018-09-14 18:31:24 +00:00
|
|
|
--[[
|
|
|
|
Intended for use in tests.
|
|
|
|
|
|
|
|
Similar to await(), but instead of yielding if the promise is unresolved,
|
|
|
|
_unwrap will throw. This indicates an assumption that a promise has
|
|
|
|
resolved.
|
|
|
|
]]
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:_unwrap()
|
2018-09-14 18:31:24 +00:00
|
|
|
if self._status == Promise.Status.Started then
|
|
|
|
error("Promise has not resolved or rejected.", 2)
|
|
|
|
end
|
|
|
|
|
|
|
|
local success = self._status == Promise.Status.Resolved
|
|
|
|
|
|
|
|
return success, unpack(self._values, 1, self._valuesLength)
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:_resolve(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
if self._status ~= Promise.Status.Started then
|
2019-09-12 07:58:56 +00:00
|
|
|
if Promise.is((...)) then
|
|
|
|
(...):_consumerCancelled(self)
|
|
|
|
end
|
2018-01-26 01:21:58 +00:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
-- If the resolved value was a Promise, we chain onto it!
|
|
|
|
if Promise.is((...)) then
|
|
|
|
-- Without this warning, arguments sometimes mysteriously disappear
|
2018-09-14 20:41:32 +00:00
|
|
|
if select("#", ...) > 1 then
|
2018-01-26 01:21:58 +00:00
|
|
|
local message = (
|
|
|
|
"When returning a Promise from andThen, extra arguments are " ..
|
|
|
|
"discarded! See:\n\n%s"
|
|
|
|
):format(
|
|
|
|
self._source
|
|
|
|
)
|
|
|
|
warn(message)
|
|
|
|
end
|
|
|
|
|
2019-09-28 09:14:53 +00:00
|
|
|
local chainedPromise = ...
|
|
|
|
|
|
|
|
local promise = chainedPromise:andThen(
|
2018-09-14 20:38:52 +00:00
|
|
|
function(...)
|
|
|
|
self:_resolve(...)
|
|
|
|
end,
|
|
|
|
function(...)
|
2019-09-28 09:14:53 +00:00
|
|
|
-- The handler errored. Replace the inner stack trace with our outer stack trace.
|
|
|
|
if chainedPromise._error then
|
|
|
|
return self:_reject((chainedPromise._error or "") .. "\n" .. self._source)
|
|
|
|
end
|
2018-09-14 20:38:52 +00:00
|
|
|
self:_reject(...)
|
|
|
|
end
|
|
|
|
)
|
2018-01-26 01:21:58 +00:00
|
|
|
|
2019-09-12 07:58:56 +00:00
|
|
|
if promise._status == Promise.Status.Cancelled then
|
|
|
|
self:cancel()
|
|
|
|
elseif promise._status == Promise.Status.Started then
|
|
|
|
-- Adopt ourselves into promise for cancellation propagation.
|
|
|
|
self._parent = promise
|
|
|
|
promise._consumers[self] = true
|
|
|
|
end
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
self._status = Promise.Status.Resolved
|
2018-09-14 20:41:32 +00:00
|
|
|
self._valuesLength, self._values = pack(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
-- We assume that these callbacks will not throw errors.
|
|
|
|
for _, callback in ipairs(self._queuedResolve) do
|
|
|
|
callback(...)
|
|
|
|
end
|
2018-10-24 06:33:30 +00:00
|
|
|
|
|
|
|
self:_finalize()
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:_reject(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
if self._status ~= Promise.Status.Started then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
self._status = Promise.Status.Rejected
|
2018-09-14 20:38:52 +00:00
|
|
|
self._valuesLength, self._values = pack(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
-- If there are any rejection handlers, call those!
|
|
|
|
if not isEmpty(self._queuedReject) then
|
|
|
|
-- We assume that these callbacks will not throw errors.
|
|
|
|
for _, callback in ipairs(self._queuedReject) do
|
|
|
|
callback(...)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
-- At this point, no one was able to observe the error.
|
|
|
|
-- An error handler might still be attached if the error occurred
|
|
|
|
-- synchronously. We'll wait one tick, and if there are still no
|
|
|
|
-- observers, then we should put a message in the console.
|
|
|
|
|
|
|
|
local err = tostring((...))
|
|
|
|
|
2019-09-18 21:58:07 +00:00
|
|
|
coroutine.wrap(function()
|
|
|
|
RunService.Heartbeat:Wait()
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
-- Someone observed the error, hooray!
|
|
|
|
if not self._unhandledRejection then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Build a reasonable message
|
2019-09-28 09:14:53 +00:00
|
|
|
local message
|
|
|
|
if self._error then
|
|
|
|
message = ("Unhandled promise rejection:\n\n%s"):format(err)
|
|
|
|
else
|
|
|
|
message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
|
|
|
|
err,
|
|
|
|
self._source
|
|
|
|
)
|
|
|
|
end
|
2018-01-26 01:21:58 +00:00
|
|
|
warn(message)
|
2019-09-18 21:58:07 +00:00
|
|
|
end)()
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
2018-10-24 06:33:30 +00:00
|
|
|
|
|
|
|
self:_finalize()
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Calls any :finally handlers. We need this to be a separate method and
|
|
|
|
queue because we must call all of the finally callbacks upon a success,
|
|
|
|
failure, *and* cancellation.
|
|
|
|
]]
|
|
|
|
function Promise.prototype:_finalize()
|
|
|
|
for _, callback in ipairs(self._queuedFinally) do
|
|
|
|
-- Purposefully not passing values to callbacks here, as it could be the
|
|
|
|
-- resolved values, or rejected errors. If the developer needs the values,
|
|
|
|
-- they should use :andThen or :catch explicitly.
|
2019-09-12 07:58:56 +00:00
|
|
|
callback(self._status)
|
2018-10-24 06:33:30 +00:00
|
|
|
end
|
2019-09-12 07:58:56 +00:00
|
|
|
|
2019-09-28 09:14:53 +00:00
|
|
|
if self._parent and self._error == nil then
|
|
|
|
self._error = self._parent._error
|
|
|
|
end
|
|
|
|
|
2019-09-12 07:58:56 +00:00
|
|
|
-- Allow family to be buried
|
2019-09-18 20:42:15 +00:00
|
|
|
if not Promise.TEST then
|
|
|
|
self._parent = nil
|
|
|
|
self._consumers = nil
|
|
|
|
end
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
2018-09-14 20:47:10 +00:00
|
|
|
return Promise
|