luau-promise/lib/init.lua

1039 lines
25 KiB
Lua
Raw Normal View History

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
--[[
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 = (...)
2019-09-18 20:42:15 +00:00
return result, pack(select(2, ...))
end
2019-09-28 04:19:29 +00:00
--[[
Calls a non-yielding function in a new coroutine.
Handles errors if they happen.
]]
2020-02-13 00:59:04 +00:00
local function runExecutor(yieldError, traceback, 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)
2019-09-18 20:42:15 +00:00
local ok, len, result = packResult(coroutine.resume(co, ...))
if ok and coroutine.status(co) ~= "dead" then
2020-02-13 00:59:04 +00:00
error(yieldError .. "\n" .. traceback, 2)
2018-01-26 01:21:58 +00:00
end
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(...)
2020-02-13 00:59:04 +00:00
local ok, resultLength, result = runExecutor(ERROR_YIELD_THEN, traceback, 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-11-13 04:07:43 +00:00
reject(result[1] .. "\n" .. traceback)
2018-01-26 01:21:58 +00:00
end
end
end
local function isEmpty(t)
return next(t) == nil
end
local Promise = {}
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.
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
]]
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
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
_values = nil,
2018-01-26 01:21:58 +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 = {},
_queuedFinally = {},
2018-10-23 23:12:05 +00:00
-- The function to run when/if this promise is cancelled.
_cancellationHook = nil,
-- 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
setmetatable(self, Promise)
2018-01-26 01:21:58 +00:00
local function resolve(...)
self:_resolve(...)
2018-01-26 01:21:58 +00:00
end
local function reject(...)
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
end
2019-09-12 07:58:56 +00:00
return self._status == Promise.Status.Cancelled
2018-10-23 23:12:05 +00:00
end
2020-02-13 00:59:04 +00:00
local ok, _, result = runExecutor(
2019-09-28 09:14:53 +00:00
ERROR_YIELD_NEW,
2020-02-13 00:59:04 +00:00
self._source,
2019-09-28 09:14:53 +00:00
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
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, ...)
2019-11-13 04:07:43 +00:00
return Promise._newWithSelf(function(self, ...)
2019-09-28 09:14:53 +00:00
self._source = traceback
2019-11-13 04:07:43 +00:00
executor(...)
2019-09-28 09:14:53 +00:00
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
2020-02-12 23:55:29 +00:00
promise = Promise._new(traceback, 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.
]]
function Promise.resolve(...)
local length, values = pack(...)
2020-02-12 23:55:29 +00:00
return Promise._new(debug.traceback(), function(resolve)
resolve(unpack(values, 1, length))
2018-01-26 01:21:58 +00:00
end)
end
--[[
Create a promise that represents the immediately rejected value.
]]
function Promise.reject(...)
local length, values = pack(...)
2020-02-12 23:55:29 +00:00
return Promise._new(debug.traceback(), function(_, reject)
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
]]
function Promise._all(traceback, promises, amount)
2018-09-14 18:17:06 +00:00
if type(promises) ~= "table" then
error(ERROR_NON_LIST:format("Promise.all"), 3)
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.
for i, promise in pairs(promises) do
if not Promise.is(promise) then
error((ERROR_NON_PROMISE_IN_LIST):format("Promise.all", tostring(i)), 3)
2018-09-14 18:17:06 +00:00
end
end
-- If there are no values then return an already resolved promise.
if #promises == 0 or amount == 0 then
return Promise.resolve({})
end
return Promise._newWithSelf(function(self, resolve, reject, onCancel)
self._source = traceback
2018-09-14 18:17:06 +00:00
-- 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
local rejectedCount = 0
local done = false
local function cancel()
for _, promise in ipairs(newPromises) do
promise:cancel()
end
end
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, ...)
if done then
return
end
2018-09-14 18:17:06 +00:00
resolvedCount = resolvedCount + 1
if amount == nil then
resolvedValues[i] = ...
else
resolvedValues[resolvedCount] = ...
end
if resolvedCount >= (amount or #promises) then
done = true
2018-09-14 18:17:06 +00:00
resolve(resolvedValues)
cancel()
2018-09-14 18:17:06 +00:00
end
end
onCancel(cancel)
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
table.insert(
newPromises,
promises[i]:andThen(
function(...)
resolveOne(i, ...)
end,
function(...)
rejectedCount = rejectedCount + 1
if amount == nil or #promises - rejectedCount < amount then
cancel()
done = true
reject(...)
2019-09-28 04:13:30 +00:00
end
end
)
)
end
if done then
cancel()
end
end)
end
function Promise.all(promises)
return Promise._all(debug.traceback(), promises)
end
function Promise.some(promises, amount)
assert(type(amount) == "number", "Bad argument #2 to Promise.some: must be a number")
return Promise._all(debug.traceback(), promises, amount)
end
function Promise.any(promises)
return Promise._all(debug.traceback(), promises, 1):andThen(function(values)
return values[1]
end)
end
function Promise.allSettled(promises)
if type(promises) ~= "table" then
error(ERROR_NON_LIST:format("Promise.allSettled"), 2)
end
-- 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.
for i, promise in pairs(promises) do
if not Promise.is(promise) then
error((ERROR_NON_PROMISE_IN_LIST):format("Promise.allSettled", tostring(i)), 2)
end
end
-- If there are no values then return an already resolved promise.
if #promises == 0 then
return Promise.resolve({})
end
2020-02-12 23:55:29 +00:00
return Promise._new(debug.traceback(), function(resolve, _, onCancel)
-- An array to contain our resolved values from the given promises.
local fates = {}
local newPromises = {}
-- Keep a count of resolved promises because just checking the resolved
-- values length wouldn't account for promises that resolve with nil.
local finishedCount = 0
-- Called when a single value is resolved and resolves if all are done.
local function resolveOne(i, ...)
finishedCount = finishedCount + 1
fates[i] = ...
2019-09-28 04:13:30 +00:00
if finishedCount >= #promises then
resolve(fates)
end
end
onCancel(function()
for _, promise in ipairs(newPromises) do
promise:cancel()
end
end)
-- We can assume the values inside `promises` are all promises since we
-- checked above.
for i = 1, #promises do
table.insert(
newPromises,
promises[i]:finally(
function(...)
resolveOne(i, ...)
2019-09-28 04:13:30 +00:00
end
)
)
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
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
2020-02-12 23:55:29 +00:00
return Promise._new(debug.traceback(), function(resolve, reject, onCancel)
2019-09-28 04:13:30 +00:00
local newPromises = {}
local finished = false
local function cancel()
for _, promise in ipairs(newPromises) do
promise:cancel()
end
end
2019-09-28 04:13:30 +00:00
2019-09-10 19:34:06 +00:00
local function finalize(callback)
return function (...)
cancel()
finished = true
2019-09-10 19:34:06 +00:00
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
if finished then
cancel()
end
2019-09-10 19:34:06 +00:00
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
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.
]]
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(...)
2020-02-12 23:55:29 +00:00
return Promise._new(traceback, 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
--[[
Creates a Promise that resolves after given number of seconds.
]]
do
local connection
local queue = {}
local function enqueue(callback, seconds)
table.insert(queue, {
callback = callback,
startTime = tick(),
endTime = tick() + math.max(seconds, 1/60)
})
table.sort(queue, function(a, b)
return a.endTime < b.endTime
end)
if not connection then
connection = RunService.Heartbeat:Connect(function()
while #queue > 0 and queue[1].endTime <= tick() do
local item = table.remove(queue, 1)
item.callback(tick() - item.startTime)
end
if #queue == 0 then
connection:Disconnect()
connection = nil
end
end)
end
end
local function dequeue(callback)
for i, item in ipairs(queue) do
if item.callback == callback then
table.remove(queue, i)
break
end
end
end
function Promise.delay(seconds)
assert(type(seconds) == "number", "Bad argument #1 to Promise.delay, must be a number.")
-- If seconds is -INF, INF, or NaN, assume seconds is 0.
-- This mirrors the behavior of wait()
if seconds < 0 or seconds == math.huge or seconds ~= seconds then
seconds = 0
end
2020-02-12 23:55:29 +00:00
return Promise._new(debug.traceback(), function(resolve, _, onCancel)
enqueue(resolve, seconds)
onCancel(function()
dequeue(resolve)
end)
end)
end
end
--[[
Rejects the promise after `seconds` seconds.
]]
function Promise.prototype:timeout(seconds, timeoutValue)
return Promise.race({
Promise.delay(seconds):andThen(function()
return Promise.reject(timeoutValue == nil and "Timed out" or timeoutValue)
end),
self
})
end
function Promise.prototype:getStatus()
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.
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.
failureCallback(unpack(self._values, 1, self._valuesLength))
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
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.
]]
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
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()
end
self:_finalize()
2018-10-23 23:12:05 +00:00
end
2018-10-23 23:14:29 +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
2019-09-12 07:58:56 +00:00
self._consumers[consumer] = nil
if next(self._consumers) == nil then
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
-- Return a promise chained off of this promise
2019-09-28 09:14:53 +00:00
return Promise._new(traceback, function(resolve, reject)
local finallyCallback = resolve
if finallyHandler then
2019-09-28 09:14:53 +00:00
finallyCallback = createAdvancer(
traceback,
finallyHandler,
resolve,
reject
)
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
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)
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
local bindable = Instance.new("BindableEvent")
self:finally(function()
2019-09-12 07:58:56 +00:00
bindable:Fire()
end)
2019-09-12 07:58:56 +00:00
bindable.Event:Wait()
bindable:Destroy()
2019-09-12 07:58:56 +00:00
end
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
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.
]]
2019-11-13 04:14:18 +00:00
function Promise.prototype:expect(...)
2019-09-13 00:13:29 +00:00
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
2019-11-13 04:14:18 +00:00
Promise.prototype.awaitValue = Promise.prototype.expect
--[[
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.
]]
function Promise.prototype:_unwrap()
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
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(
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
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
self:_finalize()
2018-01-26 01:21:58 +00:00
end
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
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
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)
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