luau-promise/lib/init.lua

1227 lines
30 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_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
local MODE_KEY_METATABLE = {
__mode = "k";
}
2019-09-10 19:34:06 +00:00
local RunService = game:GetService("RunService")
2018-01-26 01:21:58 +00:00
2020-05-06 23:22:48 +00:00
--[[
Creates an enum dictionary with some metamethods to prevent common mistakes.
]]
local function makeEnum(enumName, members)
local enum = {}
for _, memberName in ipairs(members) do
enum[memberName] = memberName
end
return setmetatable(enum, {
__index = function(_, k)
error(("%s is not in %s!"):format(k, enumName), 2)
end,
__newindex = function()
error(("Creating new members in %s is not allowed!"):format(enumName), 2)
end
})
end
2020-05-05 03:56:49 +00:00
--[[
An object to represent runtime errors that occur during execution.
Promises that experience an error like this will be rejected with
an instance of this object.
]]
2020-05-06 23:22:48 +00:00
local Error do
Error = {
Kind = makeEnum("Promise.Error.Kind", {
"ExecutionError",
"AlreadyCancelled",
"NotResolvedInTime",
"TimedOut"
})
}
Error.__index = Error
function Error.new(options, parent)
options = options or {}
return setmetatable({
error = tostring(options.error) or "[This error has no error text.]",
trace = options.trace,
context = options.context,
kind = options.kind,
parent = parent,
createdTick = tick(),
createdTrace = debug.traceback()
}, Error)
end
2020-05-05 03:56:49 +00:00
2020-05-06 23:22:48 +00:00
function Error.is(anything)
if type(anything) == "table" then
local metatable = getmetatable(anything)
2020-05-06 22:02:10 +00:00
2020-05-06 23:22:48 +00:00
if type(metatable) == "table" then
return rawget(anything, "error") ~= nil and type(rawget(metatable, "extend")) == "function"
end
2020-05-06 22:02:10 +00:00
end
2020-05-06 23:22:48 +00:00
return false
2020-05-05 03:56:49 +00:00
end
2020-05-06 23:22:48 +00:00
function Error.isKind(anything, kind)
assert(kind ~= nil, "Argument #2 to Promise.Error.isKind must not be nil")
2020-05-05 03:56:49 +00:00
2020-05-06 23:22:48 +00:00
return Error.is(anything) and anything.kind == kind
end
2020-05-06 22:02:10 +00:00
2020-05-06 23:22:48 +00:00
function Error:extend(options)
options = options or {}
2020-05-06 22:02:10 +00:00
2020-05-06 23:22:48 +00:00
options.kind = options.kind or self.kind
return Error.new(options, self)
2020-05-06 22:02:10 +00:00
end
2020-05-06 23:22:48 +00:00
function Error:getErrorChain()
local runtimeErrors = { self }
2020-05-05 03:56:49 +00:00
2020-05-06 23:22:48 +00:00
while runtimeErrors[#runtimeErrors].parent do
table.insert(runtimeErrors, runtimeErrors[#runtimeErrors].parent)
end
2020-05-06 22:02:10 +00:00
2020-05-06 23:22:48 +00:00
return runtimeErrors
2020-05-06 22:02:10 +00:00
end
2020-05-06 23:22:48 +00:00
function Error:__tostring()
local errorStrings = {
("-- Promise.Error(%s) --"):format(self.kind or "?"),
}
for _, runtimeError in ipairs(self:getErrorChain()) do
table.insert(errorStrings, table.concat({
runtimeError.trace or runtimeError.error,
runtimeError.context
}, "\n"))
end
return table.concat(errorStrings, "\n")
end
2020-05-05 03:56:49 +00:00
end
--[[
Packs a number of arguments into a table and returns its length.
Used to cajole varargs without dropping sparse values.
]]
local function pack(...)
return select("#", ...), { ... }
end
2019-09-28 04:19:29 +00:00
--[[
Returns first value (success), and packs all following values.
]]
2020-02-14 21:08:53 +00:00
local function packResult(success, ...)
return success, select("#", ...), { ... }
2019-09-18 20:42:15 +00:00
end
2019-09-28 04:19:29 +00:00
2020-05-05 03:56:49 +00:00
local function makeErrorHandler(traceback)
2020-05-11 19:43:35 +00:00
assert(traceback ~= nil)
2020-05-05 03:56:49 +00:00
return function(err)
2020-05-11 19:43:35 +00:00
-- If the error object is already a table, forward it directly.
-- Should we extend the error here and add our own trace?
if type(err) == "table" then
return err
end
2020-05-06 23:22:48 +00:00
return Error.new({
2020-05-06 22:02:10 +00:00
error = err,
2020-05-06 23:22:48 +00:00
kind = Error.Kind.ExecutionError,
2020-05-11 19:43:35 +00:00
trace = debug.traceback(tostring(err), 2),
2020-05-06 22:02:10 +00:00
context = "Promise created at:\n\n" .. traceback
})
2018-01-26 01:21:58 +00:00
end
2020-05-05 03:56:49 +00:00
end
2020-05-05 03:56:49 +00:00
--[[
Calls a Promise executor with error handling.
]]
local function runExecutor(traceback, callback, ...)
return packResult(xpcall(callback, makeErrorHandler(traceback), ...))
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-05-05 03:56:49 +00:00
local ok, resultLength, result = runExecutor(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
2020-05-06 22:02:10 +00:00
reject(result[1])
2018-01-26 01:21:58 +00:00
end
end
end
local function isEmpty(t)
return next(t) == nil
end
2020-05-05 03:56:49 +00:00
local Promise = {
2020-05-06 23:22:48 +00:00
Error = Error,
Status = makeEnum("Promise.Status", {"Started", "Resolved", "Rejected", "Cancelled"}),
2020-05-05 03:56:49 +00:00
_timeEvent = RunService.Heartbeat,
2020-05-06 22:02:10 +00:00
_getTime = tick,
2020-05-05 03:56:49 +00:00
}
Promise.prototype = {}
Promise.__index = Promise.prototype
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
]]
2020-05-05 03:56:49 +00:00
function Promise._new(traceback, 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
2020-05-05 03:56:49 +00:00
_source = traceback,
2018-01-26 01:21:58 +00:00
_status = Promise.Status.Started,
-- 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
2020-05-05 03:56:49 +00:00
-- cancellation propagation upstream.
_parent = parent,
2020-05-05 03:56:49 +00:00
-- Consumers are Promises that have chained onto this one.
-- We track them for cancellation propagation downstream.
_consumers = setmetatable({}, MODE_KEY_METATABLE),
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-05-05 03:56:49 +00:00
coroutine.wrap(function()
local ok, _, result = runExecutor(
self._source,
callback,
resolve,
reject,
onCancel
)
2018-01-26 01:21:58 +00:00
2020-05-05 03:56:49 +00:00
if not ok then
2020-05-06 22:02:10 +00:00
reject(result[1])
2020-05-05 03:56:49 +00:00
end
end)()
2018-01-26 01:21:58 +00:00
return self
2018-01-26 01:21:58 +00:00
end
2020-05-05 03:56:49 +00:00
function Promise.new(executor)
return Promise._new(debug.traceback(nil, 2), executor)
2019-09-28 09:14:53 +00:00
end
2020-05-05 03:56:49 +00:00
function Promise:__tostring()
return ("Promise(%s)"):format(self:getStatus())
2019-09-28 09:14:53 +00:00
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
]]
2020-05-05 03:56:49 +00:00
function Promise.defer(callback)
local traceback = debug.traceback(nil, 2)
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
2020-05-05 03:56:49 +00:00
connection = Promise._timeEvent:Connect(function()
2019-09-18 20:42:15 +00:00
connection:Disconnect()
2020-05-05 03:56:49 +00:00
local ok, _, result = runExecutor(traceback, callback, resolve, reject, onCancel)
2019-09-10 21:12:00 +00:00
2019-09-18 20:42:15 +00:00
if not ok then
2020-05-06 22:02:10 +00:00
reject(result[1])
2019-09-18 20:42:15 +00:00
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
2020-05-05 03:56:49 +00:00
-- Backwards compatibility
Promise.async = Promise.defer
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-05-05 03:56:49 +00:00
return Promise._new(debug.traceback(nil, 2), 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-05-05 03:56:49 +00:00
return Promise._new(debug.traceback(nil, 2), function(_, reject)
reject(unpack(values, 1, length))
2018-01-26 01:21:58 +00:00
end)
end
2020-05-05 03:56:49 +00:00
--[[
Runs a non-promise-returning function as a Promise with the
given arguments.
]]
function Promise._try(traceback, callback, ...)
local valuesLength, values = pack(...)
return Promise._new(traceback, function(resolve, reject)
resolve(callback(unpack(values, 1, valuesLength)))
end)
end
2019-09-28 22:54:49 +00:00
--[[
Begins a Promise chain, turning synchronous errors into rejections.
]]
function Promise.try(...)
2020-05-05 03:56:49 +00:00
return Promise._try(debug.traceback(nil, 2), ...)
2019-09-28 22:54:49 +00:00
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.
2020-02-29 02:21:01 +00:00
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
2020-05-05 03:56:49 +00:00
return Promise._new(traceback, function(resolve, reject, onCancel)
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.
for i, promise in ipairs(promises) do
2019-09-28 04:13:30 +00:00
table.insert(
newPromises,
promise:andThen(
2019-09-28 04:13:30 +00:00
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)
2020-05-05 03:56:49 +00:00
return Promise._all(debug.traceback(nil, 2), promises)
end
function Promise.some(promises, amount)
assert(type(amount) == "number", "Bad argument #2 to Promise.some: must be a number")
2020-05-05 03:56:49 +00:00
return Promise._all(debug.traceback(nil, 2), promises, amount)
end
function Promise.any(promises)
2020-05-05 03:56:49 +00:00
return Promise._all(debug.traceback(nil, 2), 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.
2020-02-29 02:21:01 +00:00
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-05-05 03:56:49 +00:00
return Promise._new(debug.traceback(nil, 2), 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, promise in ipairs(promises) do
table.insert(
newPromises,
promise: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
2020-02-29 02:21:01 +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-05-05 03:56:49 +00:00
return Promise._new(debug.traceback(nil, 2), 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
2020-05-05 03:56:49 +00:00
local objectMetatable = getmetatable(object)
if objectMetatable == Promise then
-- The Promise came from this library.
return true
elseif objectMetatable == nil then
-- No metatable, but we should still chain onto tables with andThen methods
return type(object.andThen) == "function"
elseif type(objectMetatable) == "table" and type(rawget(objectMetatable, "andThen")) == "function" then
-- Maybe this came from a different or older Promise library.
return true
end
return false
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(...)
2020-05-05 03:56:49 +00:00
return Promise._try(debug.traceback(nil, 2), callback, ...)
2019-09-12 07:58:56 +00:00
end
end
--[[
Creates a Promise that resolves after given number of seconds.
]]
do
-- uses a sorted doubly linked list (queue) to achieve O(1) remove operations and O(n) for insert
-- the initial node in the linked list
local first
local connection
function Promise.delay(seconds)
assert(type(seconds) == "number", "Bad argument #1 to Promise.delay, must be a number.")
-- If seconds is -INF, INF, NaN, or less than 1 / 60, assume seconds is 1 / 60.
-- This mirrors the behavior of wait()
if not (seconds >= 1 / 60) or seconds == math.huge then
seconds = 1 / 60
end
2020-05-05 03:56:49 +00:00
return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
local startTime = Promise._getTime()
local endTime = startTime + seconds
local node = {
resolve = resolve,
startTime = startTime,
endTime = endTime
}
if connection == nil then -- first is nil when connection is nil
first = node
2020-05-05 03:56:49 +00:00
connection = Promise._timeEvent:Connect(function()
local currentTime = Promise._getTime()
while first.endTime <= currentTime do
2020-05-11 19:43:50 +00:00
-- Don't use currentTime here, as this is the time when we started resolving,
-- not necessarily the time *right now*.
first.resolve(Promise._getTime() - first.startTime)
first = first.next
if first == nil then
connection:Disconnect()
connection = nil
break
end
first.previous = nil
end
end)
else -- first is non-nil
if first.endTime < endTime then -- if `node` should be placed after `first`
-- we will insert `node` between `current` and `next`
-- (i.e. after `current` if `next` is nil)
local current = first
local next = current.next
while next ~= nil and next.endTime < endTime do
current = next
next = current.next
end
-- `current` must be non-nil, but `next` could be `nil` (i.e. last item in list)
current.next = node
node.previous = current
if next ~= nil then
node.next = next
next.previous = node
end
else
-- set `node` to `first`
node.next = first
first.previous = node
first = node
end
end
onCancel(function()
-- remove node from queue
local next = node.next
if first == node then
if next == nil then -- if `node` is the first and last
connection:Disconnect()
connection = nil
else -- if `node` is `first` and not the last
next.previous = nil
end
first = next
else
local previous = node.previous
-- since `node` is not `first`, then we know `previous` is non-nil
previous.next = next
if next ~= nil then
next.previous = previous
end
end
end)
end)
end
end
--[[
Rejects the promise after `seconds` seconds.
]]
2020-05-06 23:22:48 +00:00
function Promise.prototype:timeout(seconds, rejectionValue)
local traceback = debug.traceback(nil, 2)
return Promise.race({
Promise.delay(seconds):andThen(function()
2020-05-06 23:22:48 +00:00
return Promise.reject(rejectionValue == nil and Error.new({
kind = Error.Kind.TimedOut,
error = "Timed out",
context = ("Timeout of %d seconds exceeded.\n:timeout() called at:\n\n%s"):format(
seconds,
traceback
)
}) or rejectionValue)
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.
2020-05-06 23:22:48 +00:00
reject(Error.new({
error = "Promise is cancelled",
kind = Error.Kind.AlreadyCancelled,
context = "Promise created at\n\n" .. traceback
}))
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")
)
2020-05-05 03:56:49 +00:00
return self:_andThen(debug.traceback(nil, 2), successHandler, failureHandler)
2019-09-28 09:14:53 +00:00
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")
)
2020-05-05 03:56:49 +00:00
return self:_andThen(debug.traceback(nil, 2), 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"))
2020-05-05 03:56:49 +00:00
return self:_andThen(debug.traceback(nil, 2), 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(...)
2020-05-05 03:56:49 +00:00
return self:_andThen(debug.traceback(nil, 2), 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(...)
2020-05-05 03:56:49 +00:00
return self:_andThen(debug.traceback(nil, 2), function()
2019-09-29 02:03:06 +00:00
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")
)
2020-05-05 03:56:49 +00:00
return self:_finally(debug.traceback(nil, 2), finallyHandler)
2019-09-28 09:14:53 +00:00
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(...)
2020-05-05 03:56:49 +00:00
return self:_finally(debug.traceback(nil, 2), 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(...)
2020-05-05 03:56:49 +00:00
return self:_finally(debug.traceback(nil, 2), function()
2019-09-29 02:03:06 +00:00
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")
)
2020-05-05 03:56:49 +00:00
return self:_finally(debug.traceback(nil, 2), finallyHandler, true)
2019-09-29 02:03:06 +00:00
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(...)
2020-05-05 03:56:49 +00:00
return self:_finally(debug.traceback(nil, 2), function()
2019-09-29 02:03:06 +00:00
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(...)
2020-05-05 03:56:49 +00:00
return self:_finally(debug.traceback(nil, 2), function()
2019-09-29 02:03:06 +00:00
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
local function awaitHelper(status, ...)
return status == Promise.Status.Resolved, ...
end
2019-09-12 07:58:56 +00:00
--[[
Calls awaitStatus internally, returns (isResolved, values...)
]]
function Promise.prototype:await()
return awaitHelper(self:awaitStatus())
end
local function expectHelper(status, ...)
if status ~= Promise.Status.Resolved then
error((...) == nil and "Expected Promise rejected with no value." or (...), 3)
end
2019-09-12 07:58:56 +00:00
return ...
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:expect()
return expectHelper(self:awaitStatus())
2019-09-13 00:13:29 +00:00
end
2020-05-05 03:56:49 +00:00
-- Backwards compatibility
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(...)
2020-05-06 22:02:10 +00:00
local maybeRuntimeError = chainedPromise._values[1]
2020-05-05 03:56:49 +00:00
2020-05-06 22:02:10 +00:00
-- Backwards compatibility < v2
2019-09-28 09:14:53 +00:00
if chainedPromise._error then
2020-05-06 23:22:48 +00:00
maybeRuntimeError = Error.new({
2020-05-06 22:02:10 +00:00
error = chainedPromise._error,
2020-05-06 23:22:48 +00:00
kind = Error.Kind.ExecutionError,
2020-05-06 22:02:10 +00:00
context = "[No stack trace available as this Promise originated from an older version of the Promise library (< v2)]"
})
2020-05-05 03:56:49 +00:00
end
2020-05-06 23:22:48 +00:00
if Error.isKind(maybeRuntimeError, Error.Kind.ExecutionError) then
2020-05-06 22:02:10 +00:00
return self:_reject(maybeRuntimeError:extend({
error = "This Promise was chained to a Promise that errored.",
trace = "",
context = ("The Promise at:\n\n%s\n...Rejected because it was chained to the following Promise, which encountered an error:\n"):format(
self._source
)
}))
2019-09-28 09:14:53 +00:00
end
2020-05-05 03:56:49 +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
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()
2020-05-05 03:56:49 +00:00
Promise._timeEvent:Wait()
2019-09-18 21:58:07 +00:00
2018-01-26 01:21:58 +00:00
-- Someone observed the error, hooray!
if not self._unhandledRejection then
return
end
-- Build a reasonable message
2020-05-11 19:43:58 +00:00
local message = ("Unhandled Promise rejection:\n\n%s\n\n%s"):format(
2020-05-05 03:56:49 +00:00
err,
self._source
)
if Promise.TEST then
-- Don't spam output when we're running tests.
return
2019-09-28 09:14:53 +00:00
end
2020-05-05 03:56:49 +00:00
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
2020-05-05 03:56:49 +00:00
-- Clear references to other Promises to allow gc
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
2020-05-06 23:22:48 +00:00
--[[
Chains a Promise from this one that is resolved if this Promise is
resolved, and rejected if it is not resolved.
]]
function Promise.prototype:now(rejectionValue)
local traceback = debug.traceback(nil, 2)
if self:getStatus() == Promise.Status.Resolved then
return self:_andThen(traceback, function(...)
return ...
end)
else
return Promise.reject(rejectionValue == nil and Error.new({
kind = Error.Kind.NotResolvedInTime,
error = "This Promise was not resolved in time for :now()",
context = ":now() was called at:\n\n" .. traceback
}) or rejectionValue)
end
end
return Promise