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! "
2021-11-24 22:28:49 +00:00
local MODE_KEY_METATABLE = { __mode = " k " }
2020-03-29 10:35:15 +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 )
2020-06-09 18:53:06 +00:00
error ( string.format ( " %s is not in %s! " , k , enumName ) , 2 )
2020-05-06 23:22:48 +00:00
end ,
__newindex = function ( )
2020-06-09 18:53:06 +00:00
error ( string.format ( " Creating new members in %s is not allowed! " , enumName ) , 2 )
end ,
2020-05-06 23:22:48 +00:00
} )
end
2021-09-23 23:08:38 +00:00
--[=[
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 .
2021-09-23 23:08:38 +00:00
@ class Error
] = ]
2021-11-24 22:28:49 +00:00
local Error
do
2020-05-06 23:22:48 +00:00
Error = {
Kind = makeEnum ( " Promise.Error.Kind " , {
" ExecutionError " ,
" AlreadyCancelled " ,
" NotResolvedInTime " ,
2020-06-09 18:53:06 +00:00
" TimedOut " ,
} ) ,
2020-05-06 23:22:48 +00:00
}
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 ,
2020-08-07 05:52:41 +00:00
createdTick = os.clock ( ) ,
2020-06-09 18:53:06 +00:00
createdTrace = debug.traceback ( ) ,
2020-05-06 23:22:48 +00:00
} , 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 = {
2020-06-09 18:53:06 +00:00
string.format ( " -- Promise.Error(%s) -- " , self.kind or " ? " ) ,
2020-05-06 23:22:48 +00:00
}
for _ , runtimeError in ipairs ( self : getErrorChain ( ) ) do
2021-11-24 22:28:49 +00:00
table.insert (
errorStrings ,
table.concat ( {
runtimeError.trace or runtimeError.error ,
runtimeError.context ,
} , " \n " )
)
2020-05-06 23:22:48 +00:00
end
return table.concat ( errorStrings , " \n " )
end
2020-05-05 03:56:49 +00:00
end
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 ( ... )
2020-02-14 12:02:48 +00:00
return select ( " # " , ... ) , { ... }
2018-07-05 22:49:25 +00:00
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
2018-07-05 22:49:25 +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-06-09 18:53:06 +00:00
context = " Promise created at: \n \n " .. traceback ,
2020-05-06 22:02:10 +00:00
} )
2018-01-26 01:21:58 +00:00
end
2020-05-05 03:56:49 +00:00
end
2018-05-21 21:10:38 +00:00
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
2021-09-23 23:08:38 +00:00
--[=[
An enum value used to represent the Promise ' s status.
@ interface Status
@ tag enum
@ within Promise
. Started " Started " -- The Promise is executing, and not settled yet.
. Resolved " Resolved " -- The Promise finished successfully.
. Rejected " Rejected " -- The Promise was rejected.
. Cancelled " Cancelled " -- The Promise was cancelled before it finished.
] = ]
--[=[
@ prop Status Status
@ within Promise
@ readonly
@ tag enums
A table containing all members of the ` Status ` enum , e.g . , ` Promise.Status . Resolved ` .
] = ]
--[=[
A Promise is an object that represents a value that will exist in the future , but doesn ' t right now.
Promises allow you to then attach callbacks that can run once the value becomes available ( known as * resolving * ) ,
or if an error has occurred ( known as * rejecting * ) .
@ class Promise
@ __index prototype
] = ]
2020-05-05 03:56:49 +00:00
local Promise = {
2020-05-06 23:22:48 +00:00
Error = Error ,
2021-11-24 22:28:49 +00:00
Status = makeEnum ( " Promise.Status " , { " Started " , " Resolved " , " Rejected " , " Cancelled " } ) ,
2020-08-07 05:52:41 +00:00
_getTime = os.clock ,
_timeEvent = game : GetService ( " RunService " ) . Heartbeat ,
2020-05-05 03:56:49 +00:00
}
2018-09-14 20:43:22 +00:00
Promise.prototype = { }
Promise.__index = Promise.prototype
2018-01-26 01:21:58 +00:00
2020-05-05 03:56:49 +00:00
function Promise . _new ( traceback , callback , parent )
2018-10-24 06:33:30 +00:00
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
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
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
2020-05-05 03:56:49 +00:00
-- cancellation propagation upstream.
2018-10-24 06:33:30 +00:00
_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.
2020-03-29 10:35:15 +00:00
_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
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
2020-05-05 03:56:49 +00:00
coroutine.wrap ( function ( )
2021-11-24 22:28:49 +00:00
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
2018-09-14 20:47:51 +00:00
return self
2018-01-26 01:21:58 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
Construct a new Promise that will be resolved or rejected with the given callbacks .
If you ` resolve ` with a Promise , it will be chained onto .
You can safely yield within the executor function and it will not block the creating thread .
` ` ` lua
local myFunction ( )
return Promise.new ( function ( resolve , reject , onCancel )
wait ( 1 )
resolve ( " Hello world! " )
end )
end
myFunction ( ) : andThen ( print )
` ` `
You do not need to use ` pcall ` within a Promise . Errors that occur during execution will be caught and turned into a rejection automatically . If ` error ( ) ` is called with a table , that table will be the rejection value . Otherwise , string errors will be converted into ` Promise.Error ( Promise.Error . Kind.ExecutionError ) ` objects for tracking debug information .
You may register an optional cancellation hook by using the ` onCancel ` argument :
* This should be used to abort any ongoing operations leading up to the promise being settled .
* Call the ` onCancel ` function with a function callback as its only argument to set a hook which will in turn be called when / if the promise is cancelled .
* ` onCancel ` returns ` true ` if the Promise was already cancelled when you called ` onCancel ` .
* Calling ` onCancel ` with no argument will not override a previously set cancellation hook , but it will still return ` true ` if the Promise is currently cancelled .
* You can set the cancellation hook at any time before resolving .
* When a promise is cancelled , calls to ` resolve ` or ` reject ` will be ignored , regardless of if you set a cancellation hook or not .
2021-10-23 19:52:36 +00:00
@ param executor ( resolve : ( ... : any ) -> ( ) , reject : ( ... : any ) -> ( ) , onCancel : ( abortHandler ? : ( ) -> ( ) ) -> boolean ) -> ( )
2021-09-23 23:08:38 +00:00
@ return Promise
] = ]
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 ( )
2021-03-19 00:43:49 +00:00
return string.format ( " Promise(%s) " , self._status )
2019-09-28 09:14:53 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
The same as [ Promise.new ] ( / api / Promise # new ) , except execution begins after the next ` Heartbeat ` event .
This is a spiritual replacement for ` spawn ` , but it does not suffer from the same [ issues ] ( https : // eryn.io / gist / 3 db84579866c099cdd5bb2ff37947cec ) as ` spawn ` .
` ` ` lua
local function waitForChild ( instance , childName , timeout )
return Promise.defer ( function ( resolve , reject )
local child = instance : WaitForChild ( childName , timeout )
; ( child and resolve or reject ) ( child )
end )
end
` ` `
2021-10-23 19:52:36 +00:00
@ param executor ( resolve : ( ... : any ) -> ( ) , reject : ( ... : any ) -> ( ) , onCancel : ( abortHandler ? : ( ) -> ( ) ) -> boolean ) -> ( )
2021-09-23 23:08:38 +00:00
@ return Promise
] = ]
2021-12-28 02:28:28 +00:00
function Promise . defer ( executor )
2020-05-05 03:56:49 +00:00
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-08-07 05:52:41 +00:00
connection = Promise._timeEvent : Connect ( function ( )
2019-09-18 20:42:15 +00:00
connection : Disconnect ( )
2021-12-28 02:28:28 +00:00
local ok , _ , result = runExecutor ( traceback , executor , 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
2021-09-23 23:08:38 +00:00
--[=[
Creates an immediately resolved Promise with the given value .
2021-11-24 22:29:16 +00:00
` ` ` lua
-- Example using Promise.resolve to deliver cached values:
function getSomething ( name )
if cache [ name ] then
return Promise.resolve ( cache [ name ] )
else
return Promise.new ( function ( resolve , reject )
local thing = getTheThing ( )
cache [ name ] = thing
resolve ( thing )
end )
end
end
` ` `
2021-09-23 23:08:38 +00:00
@ param ... any
@ return Promise < ... any >
] = ]
2019-09-28 21:48:55 +00:00
function Promise . resolve ( ... )
local length , values = pack ( ... )
2020-05-05 03:56:49 +00:00
return Promise._new ( debug.traceback ( nil , 2 ) , 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
2021-09-23 23:08:38 +00:00
--[=[
Creates an immediately rejected Promise with the given value .
:: : caution
Something needs to consume this rejection ( i.e . ` : catch ( ) ` it ) , otherwise it will emit an unhandled Promise rejection warning on the next frame . Thus , you should not create and store rejected Promises for later use . Only create them on - demand as needed .
:: :
@ param ... any
@ return Promise < ... any >
] = ]
2019-09-28 21:48:55 +00:00
function Promise . reject ( ... )
local length , values = pack ( ... )
2020-05-05 03:56:49 +00:00
return Promise._new ( debug.traceback ( nil , 2 ) , 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
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 ( ... )
2020-06-09 18:53:06 +00:00
return Promise._new ( traceback , function ( resolve )
2020-05-05 03:56:49 +00:00
resolve ( callback ( unpack ( values , 1 , valuesLength ) ) )
end )
end
2021-09-23 23:08:38 +00:00
--[=[
Begins a Promise chain , calling a function and returning a Promise resolving with its return value . If the function errors , the returned Promise will be rejected with the error . You can safely yield within the Promise.try callback .
:: : info
` Promise.try ` is similar to [ Promise.promisify ] ( # promisify ) , except the callback is invoked immediately instead of returning a new function .
:: :
` ` ` lua
Promise.try ( function ( )
return math.random ( 1 , 2 ) == 1 and " ok " or error ( " Oh an error! " )
end )
: andThen ( function ( text )
print ( text )
end )
: catch ( function ( err )
warn ( " Something went wrong " )
end )
` ` `
2021-12-28 02:28:28 +00:00
@ param callback ( ... : T ... ) -> ... any
@ param ... T ... -- Additional arguments passed to `callback`
2021-09-23 23:08:38 +00:00
@ return Promise
] = ]
2021-12-28 02:28:28 +00:00
function Promise . try ( callback , ... )
return Promise._try ( debug.traceback ( nil , 2 ) , callback , ... )
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
] ]
2019-09-29 06:03:22 +00:00
function Promise . _all ( traceback , promises , amount )
2018-09-14 18:17:06 +00:00
if type ( promises ) ~= " table " then
2020-06-09 18:53:06 +00:00
error ( string.format ( ERROR_NON_LIST , " 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
2019-09-27 22:46:10 +00:00
if not Promise.is ( promise ) then
2020-06-09 18:53:06 +00:00
error ( string.format ( ERROR_NON_PROMISE_IN_LIST , " Promise.all " , tostring ( i ) ) , 3 )
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.
2019-09-29 06:03:22 +00:00
if # promises == 0 or amount == 0 then
2019-09-27 22:46:10 +00:00
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
2019-09-29 06:03:22 +00:00
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 , ... )
2019-09-29 06:03:22 +00:00
if done then
return
end
2018-09-14 18:17:06 +00:00
resolvedCount = resolvedCount + 1
2019-09-29 06:03:22 +00:00
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 )
2019-09-29 06:03:22 +00:00
cancel ( )
2018-09-14 18:17:06 +00:00
end
end
2019-09-29 06:03:22 +00:00
onCancel ( cancel )
2018-09-14 20:47:10 +00:00
-- We can assume the values inside `promises` are all promises since we
-- checked above.
2020-02-14 12:02:48 +00:00
for i , promise in ipairs ( promises ) do
2021-11-24 22:28:49 +00:00
newPromises [ i ] = promise : andThen ( function ( ... )
resolveOne ( i , ... )
end , function ( ... )
rejectedCount = rejectedCount + 1
if amount == nil or # promises - rejectedCount < amount then
cancel ( )
done = true
reject ( ... )
2020-06-09 18:53:06 +00:00
end
2021-11-24 22:28:49 +00:00
end )
2019-09-29 06:03:22 +00:00
end
if done then
cancel ( )
end
end )
end
2021-09-23 23:08:38 +00:00
--[=[
Accepts an array of Promises and returns a new promise that :
* is resolved after all input promises resolve .
* is rejected if * any * input promises reject .
:: : info
Only the first return value from each promise will be present in the resulting array .
:: :
After any input Promise rejects , all other input Promises that are still pending will be cancelled if they have no other consumers .
` ` ` lua
local promises = {
returnsAPromise ( " example 1 " ) ,
returnsAPromise ( " example 2 " ) ,
returnsAPromise ( " example 3 " ) ,
}
return Promise.all ( promises )
` ` `
@ param promises { Promise < T > }
@ return Promise < { T } >
] = ]
2019-09-29 06:03:22 +00:00
function Promise . all ( promises )
2020-05-05 03:56:49 +00:00
return Promise._all ( debug.traceback ( nil , 2 ) , promises )
2019-09-29 06:03:22 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
Folds an array of values or promises into a single value . The array is traversed sequentially .
The reducer function can return a promise or value directly . Each iteration receives the resolved value from the previous , and the first receives your defined initial value .
The folding will stop at the first rejection encountered .
` ` ` lua
local basket = { " blueberry " , " melon " , " pear " , " melon " }
Promise.fold ( basket , function ( cost , fruit )
if fruit == " blueberry " then
return cost -- blueberries are free!
else
-- call a function that returns a promise with the fruit price
return fetchPrice ( fruit ) : andThen ( function ( fruitCost )
return cost + fruitCost
end )
end
end , 0 )
` ` `
@ since v3 .1 .0
@ param list { T | Promise < T > }
@ param reducer ( accumulator : U , value : T , index : number ) -> U | Promise < U >
@ param initialValue U
] = ]
2021-12-28 02:28:28 +00:00
function Promise . fold ( list , reducer , initialValue )
2020-11-20 22:14:27 +00:00
assert ( type ( list ) == " table " , " Bad argument #1 to Promise.fold: must be a table " )
2021-12-28 02:28:28 +00:00
assert ( type ( reducer ) == " function " , " Bad argument #2 to Promise.fold: must be a function " )
2020-11-20 22:14:27 +00:00
2020-12-01 00:42:21 +00:00
local accumulator = Promise.resolve ( initialValue )
2020-12-01 00:34:13 +00:00
return Promise.each ( list , function ( resolvedElement , i )
2020-12-01 00:42:21 +00:00
accumulator = accumulator : andThen ( function ( previousValueResolved )
2021-12-28 02:28:28 +00:00
return reducer ( previousValueResolved , resolvedElement , i )
2020-12-01 00:42:21 +00:00
end )
2020-12-01 00:34:13 +00:00
end ) : andThenReturn ( accumulator )
2020-11-20 22:14:27 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
Accepts an array of Promises and returns a Promise that is resolved as soon as ` count ` Promises are resolved from the input array . The resolved array values are in the order that the Promises resolved in . When this Promise resolves , all other pending Promises are cancelled if they have no other consumers .
` count ` 0 results in an empty array . The resultant array will never have more than ` count ` elements .
` ` ` lua
local promises = {
returnsAPromise ( " example 1 " ) ,
returnsAPromise ( " example 2 " ) ,
returnsAPromise ( " example 3 " ) ,
}
return Promise.some ( promises , 2 ) -- Only resolves with first 2 promises to resolve
` ` `
@ param promises { Promise < T > }
@ param count number
@ return Promise < { T } >
] = ]
2021-12-28 02:28:28 +00:00
function Promise . some ( promises , count )
assert ( type ( count ) == " number " , " Bad argument #2 to Promise.some: must be a number " )
2019-09-29 06:03:22 +00:00
2021-12-28 02:28:28 +00:00
return Promise._all ( debug.traceback ( nil , 2 ) , promises , count )
2019-09-29 06:03:22 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
Accepts an array of Promises and returns a Promise that is resolved as soon as * any * of the input Promises resolves . It will reject only if * all * input Promises reject . As soon as one Promises resolves , all other pending Promises are cancelled if they have no other consumers .
Resolves directly with the value of the first resolved Promise . This is essentially [[Promise.some]] with ` 1 ` count , except the Promise resolves with the value directly instead of an array with one element .
` ` ` lua
local promises = {
returnsAPromise ( " example 1 " ) ,
returnsAPromise ( " example 2 " ) ,
returnsAPromise ( " example 3 " ) ,
}
return Promise.any ( promises ) -- Resolves with first value to resolve (only rejects if all 3 rejected)
` ` `
@ param promises { Promise < T > }
@ return Promise < T >
] = ]
2019-09-29 06:03:22 +00:00
function Promise . any ( promises )
2020-05-05 03:56:49 +00:00
return Promise._all ( debug.traceback ( nil , 2 ) , promises , 1 ) : andThen ( function ( values )
2019-09-29 06:03:22 +00:00
return values [ 1 ]
end )
end
2021-09-23 23:08:38 +00:00
--[=[
Accepts an array of Promises and returns a new Promise that resolves with an array of in - place Statuses when all input Promises have settled . This is equivalent to mapping ` promise : finally ` over the array of Promises .
` ` ` lua
local promises = {
returnsAPromise ( " example 1 " ) ,
returnsAPromise ( " example 2 " ) ,
returnsAPromise ( " example 3 " ) ,
}
return Promise.allSettled ( promises )
` ` `
@ param promises { Promise < T > }
@ return Promise < { Status } >
] = ]
2019-09-29 06:03:22 +00:00
function Promise . allSettled ( promises )
if type ( promises ) ~= " table " then
2020-06-09 18:53:06 +00:00
error ( string.format ( ERROR_NON_LIST , " Promise.allSettled " ) , 2 )
2019-09-29 06:03:22 +00:00
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
2019-09-29 06:03:22 +00:00
if not Promise.is ( promise ) then
2020-06-09 18:53:06 +00:00
error ( string.format ( ERROR_NON_PROMISE_IN_LIST , " Promise.allSettled " , tostring ( i ) ) , 2 )
2019-09-29 06:03:22 +00:00
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 )
2019-09-29 06:03:22 +00:00
-- 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
2019-09-29 06:03:22 +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.
2020-02-14 12:02:48 +00:00
for i , promise in ipairs ( promises ) do
2021-11-24 22:28:49 +00:00
newPromises [ i ] = promise : finally ( function ( ... )
resolveOne ( i , ... )
end )
2018-09-14 18:17:06 +00:00
end
end )
2018-01-26 01:21:58 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
Accepts an array of Promises and returns a new promise that is resolved or rejected as soon as any Promise in the array resolves or rejects .
:: : warning
If the first Promise to settle from the array settles with a rejection , the resulting Promise from ` race ` will reject .
If you instead want to tolerate rejections , and only care about at least one Promise resolving , you should use [ Promise.any ] ( # any ) or [ Promise.some ] ( # some ) instead .
:: :
All other Promises that don ' t win the race will be cancelled if they have no other consumers.
` ` ` lua
local promises = {
returnsAPromise ( " example 1 " ) ,
returnsAPromise ( " example 2 " ) ,
returnsAPromise ( " example 3 " ) ,
}
return Promise.race ( promises ) -- Only returns 1st value to resolve or reject
` ` `
2021-12-28 02:28:28 +00:00
@ param promises { Promise < T > }
2021-09-23 23:08:38 +00:00
@ return Promise < T >
] = ]
2019-09-10 19:34:06 +00:00
function Promise . race ( promises )
2020-06-09 18:53:06 +00:00
assert ( type ( promises ) == " table " , string.format ( ERROR_NON_LIST , " 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
2020-06-09 18:53:06 +00:00
assert ( Promise.is ( promise ) , string.format ( ERROR_NON_PROMISE_IN_LIST , " 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 = { }
2019-09-29 06:03:22 +00:00
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 )
2021-11-24 22:28:49 +00:00
return function ( ... )
2019-09-29 06:03:22 +00:00
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
2020-06-09 18:53:06 +00:00
for i , promise in ipairs ( promises ) do
newPromises [ i ] = promise : andThen ( finalize ( resolve ) , finalize ( reject ) )
2019-09-10 19:34:06 +00:00
end
2019-09-29 06:03:22 +00:00
if finished then
cancel ( )
end
2019-09-10 19:34:06 +00:00
end )
end
2021-09-23 23:08:38 +00:00
--[=[
Iterates serially over the given an array of values , calling the predicate callback on each value before continuing .
2020-05-13 23:48:45 +00:00
2021-09-23 23:08:38 +00:00
If the predicate returns a Promise , we wait for that Promise to resolve before moving on to the next item
in the array .
:: : info
` Promise.each ` is similar to ` Promise.all ` , except the Promises are ran in order instead of all at once .
But because Promises are eager , by the time they are created , they ' re already running. Thus, we need a way to defer creation of each Promise until a later time.
The predicate function exists as a way for us to operate on our data instead of creating a new closure for each Promise . If you would prefer , you can pass in an array of functions , and in the predicate , call the function and return its return value .
:: :
` ` ` lua
Promise.each ( {
" foo " ,
" bar " ,
" baz " ,
" qux "
} , function ( value , index )
return Promise.delay ( 1 ) : andThen ( function ( )
print ( ( " %d) Got %s! " ) : format ( index , value ) )
end )
end )
--[[
( 1 second passes )
> 1 ) Got foo !
( 1 second passes )
> 2 ) Got bar !
( 1 second passes )
> 3 ) Got baz !
( 1 second passes )
> 4 ) Got qux !
] ]
` ` `
If the Promise a predicate returns rejects , the Promise from ` Promise.each ` is also rejected with the same value .
If the array of values contains a Promise , when we get to that point in the list , we wait for the Promise to resolve before calling the predicate with the value .
If a Promise in the array of values is already Rejected when ` Promise.each ` is called , ` Promise.each ` rejects with that value immediately ( the predicate callback will never be called even once ) . If a Promise in the list is already Cancelled when ` Promise.each ` is called , ` Promise.each ` rejects with ` Promise.Error ( Promise.Error . Kind.AlreadyCancelled ` ) . If a Promise in the array of values is Started at first , but later rejects , ` Promise.each ` will reject with that value and iteration will not continue once iteration encounters that value .
Returns a Promise containing an array of the returned / resolved values from the predicate for each item in the array of values .
If this Promise returned from ` Promise.each ` rejects or is cancelled for any reason , the following are true :
- Iteration will not continue .
- Any Promises within the array of values will now be cancelled if they have no other consumers .
- The Promise returned from the currently active predicate will be cancelled if it hasn ' t resolved yet.
@ since 3.0 .0
@ param list { T | Promise < T > }
@ param predicate ( value : T , index : number ) -> U | Promise < U >
@ return Promise < { U } >
] = ]
2020-05-13 23:48:45 +00:00
function Promise . each ( list , predicate )
2020-06-09 18:53:06 +00:00
assert ( type ( list ) == " table " , string.format ( ERROR_NON_LIST , " Promise.each " ) )
assert ( type ( predicate ) == " function " , string.format ( ERROR_NON_FUNCTION , " Promise.each " ) )
2020-05-13 23:48:45 +00:00
return Promise._new ( debug.traceback ( nil , 2 ) , function ( resolve , reject , onCancel )
local results = { }
local promisesToCancel = { }
local cancelled = false
local function cancel ( )
for _ , promiseToCancel in ipairs ( promisesToCancel ) do
promiseToCancel : cancel ( )
end
end
onCancel ( function ( )
cancelled = true
cancel ( )
end )
-- We need to preprocess the list of values and look for Promises.
-- If we find some, we must register our andThen calls now, so that those Promises have a consumer
-- from us registered. If we don't do this, those Promises might get cancelled by something else
-- before we get to them in the series because it's not possible to tell that we plan to use it
-- unless we indicate it here.
local preprocessedList = { }
for index , value in ipairs ( list ) do
if Promise.is ( value ) then
if value : getStatus ( ) == Promise.Status . Cancelled then
cancel ( )
return reject ( Error.new ( {
error = " Promise is cancelled " ,
kind = Error.Kind . AlreadyCancelled ,
2020-06-09 18:53:06 +00:00
context = string.format (
" The Promise that was part of the array at index %d passed into Promise.each was already cancelled when Promise.each began. \n \n That Promise was created at: \n \n %s " ,
2020-05-13 23:48:45 +00:00
index ,
value._source
2020-06-09 18:53:06 +00:00
) ,
2020-05-13 23:48:45 +00:00
} ) )
elseif value : getStatus ( ) == Promise.Status . Rejected then
cancel ( )
return reject ( select ( 2 , value : await ( ) ) )
end
-- Chain a new Promise from this one so we only cancel ours
local ourPromise = value : andThen ( function ( ... )
return ...
end )
table.insert ( promisesToCancel , ourPromise )
preprocessedList [ index ] = ourPromise
else
preprocessedList [ index ] = value
end
end
for index , value in ipairs ( preprocessedList ) do
if Promise.is ( value ) then
local success
success , value = value : await ( )
if not success then
cancel ( )
return reject ( value )
end
end
if cancelled then
return
end
local predicatePromise = Promise.resolve ( predicate ( value , index ) )
table.insert ( promisesToCancel , predicatePromise )
local success , result = predicatePromise : await ( )
if not success then
cancel ( )
return reject ( result )
end
results [ index ] = result
end
resolve ( results )
end )
end
2021-09-23 23:08:38 +00:00
--[=[
Checks whether the given object is a Promise via duck typing . This only checks if the object is a table and has an ` andThen ` method .
@ param object any
@ return boolean -- `true` if the given `object` is a Promise.
] = ]
2018-01-26 01:21:58 +00:00
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 "
2020-08-24 17:20:47 +00:00
elseif
type ( objectMetatable ) == " table "
and type ( rawget ( objectMetatable , " __index " ) ) == " table "
and type ( rawget ( rawget ( objectMetatable , " __index " ) , " andThen " ) ) == " function "
then
2020-05-05 03:56:49 +00:00
-- Maybe this came from a different or older Promise library.
return true
end
return false
2018-01-26 01:21:58 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
Wraps a function that yields into one that returns a Promise .
Any errors that occur while executing the function will be turned into rejections .
:: : info
` Promise.promisify ` is similar to [ Promise.try ] ( # try ) , except the callback is returned as a callable function instead of being invoked immediately .
:: :
` ` ` lua
local sleep = Promise.promisify ( wait )
sleep ( 1 ) : andThen ( print )
` ` `
` ` ` lua
local isPlayerInGroup = Promise.promisify ( function ( player , groupId )
return player : IsInGroup ( groupId )
end )
` ` `
@ param callback ( ... : any ) -> ... any
@ return ( ... : any ) -> Promise
] = ]
2019-09-14 02:58:32 +00:00
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
2021-09-23 23:08:38 +00:00
--[=[
Returns a Promise that resolves after ` seconds ` seconds have passed . The Promise resolves with the actual amount of time that was waited .
This function is ** not ** a wrapper around ` wait ` . ` Promise.delay ` uses a custom scheduler which provides more accurate timing . As an optimization , cancelling this Promise instantly removes the task from the scheduler .
:: : warning
Passing ` NaN ` , infinity , or a number less than 1 / 60 is equivalent to passing 1 / 60.
:: :
` ` ` lua
Promise.delay ( 5 ) : andThenCall ( print , " This prints after 5 seconds " )
` ` `
@ function delay
@ within Promise
@ param seconds number
@ return Promise < number >
] = ]
2019-09-29 04:07:32 +00:00
do
2020-02-14 12:02:48 +00:00
-- uses a sorted doubly linked list (queue) to achieve O(1) remove operations and O(n) for insert
2019-09-29 04:07:32 +00:00
2020-02-14 12:02:48 +00:00
-- the initial node in the linked list
local first
local connection
2019-09-29 04:07:32 +00:00
function Promise . delay ( seconds )
assert ( type ( seconds ) == " number " , " Bad argument #1 to Promise.delay, must be a number. " )
2020-02-14 12:02:48 +00:00
-- If seconds is -INF, INF, NaN, or less than 1 / 60, assume seconds is 1 / 60.
2019-09-29 04:07:32 +00:00
-- This mirrors the behavior of wait()
2020-02-14 12:02:48 +00:00
if not ( seconds >= 1 / 60 ) or seconds == math.huge then
seconds = 1 / 60
2019-09-29 04:07:32 +00:00
end
2020-05-05 03:56:49 +00:00
return Promise._new ( debug.traceback ( nil , 2 ) , function ( resolve , _ , onCancel )
2020-08-07 05:52:41 +00:00
local startTime = Promise._getTime ( )
2020-02-14 12:02:48 +00:00
local endTime = startTime + seconds
local node = {
resolve = resolve ,
startTime = startTime ,
2020-06-09 18:53:06 +00:00
endTime = endTime ,
2020-02-14 12:02:48 +00:00
}
if connection == nil then -- first is nil when connection is nil
first = node
2020-08-07 05:52:41 +00:00
connection = Promise._timeEvent : Connect ( function ( )
2020-08-22 08:49:59 +00:00
local threadStart = Promise._getTime ( )
while first ~= nil and first.endTime < threadStart do
2020-07-16 10:51:54 +00:00
local current = first
first = current.next
2020-02-14 12:02:48 +00:00
if first == nil then
connection : Disconnect ( )
connection = nil
2020-07-16 10:51:54 +00:00
else
first.previous = nil
2020-02-14 12:02:48 +00:00
end
2020-07-16 10:51:54 +00:00
2020-08-07 05:52:41 +00:00
current.resolve ( Promise._getTime ( ) - current.startTime )
2020-02-14 12:02:48 +00:00
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
2019-09-29 04:07:32 +00:00
onCancel ( function ( )
2020-02-14 12:02:48 +00:00
-- 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
2019-09-29 04:07:32 +00:00
end )
end )
end
end
2021-09-23 23:08:38 +00:00
--[=[
Returns a new Promise that resolves if the chained Promise resolves within ` seconds ` seconds , or rejects if execution time exceeds ` seconds ` . The chained Promise will be cancelled if the timeout is reached .
Rejects with ` rejectionValue ` if it is non - nil . If a ` rejectionValue ` is not given , it will reject with a ` Promise.Error ( Promise.Error . Kind.TimedOut ) ` . This can be checked with [[Error.isKind]] .
` ` ` lua
getSomething ( ) : timeout ( 5 ) : andThen ( function ( something )
-- got something and it only took at max 5 seconds
end ) : catch ( function ( e )
-- Either getting something failed or the time was exceeded.
if Promise.Error . isKind ( e , Promise.Error . Kind.TimedOut ) then
warn ( " Operation timed out! " )
else
warn ( " Operation encountered an error! " )
end
end )
` ` `
Sugar for :
` ` ` lua
Promise.race ( {
Promise.delay ( seconds ) : andThen ( function ( )
return Promise.reject (
rejectionValue == nil
and Promise.Error . new ( { kind = Promise.Error . Kind.TimedOut } )
or rejectionValue
)
end ) ,
promise
} )
` ` `
@ param seconds number
@ param rejectionValue ? any -- The value to reject with if the timeout is reached
@ return Promise
] = ]
2020-05-06 23:22:48 +00:00
function Promise . prototype : timeout ( seconds , rejectionValue )
local traceback = debug.traceback ( nil , 2 )
2019-09-29 04:07:32 +00:00
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 " ,
2020-06-09 18:53:06 +00:00
context = string.format (
" Timeout of %d seconds exceeded. \n :timeout() called at: \n \n %s " ,
2020-05-06 23:22:48 +00:00
seconds ,
traceback
2020-06-09 18:53:06 +00:00
) ,
2020-05-06 23:22:48 +00:00
} ) or rejectionValue )
2019-09-29 04:07:32 +00:00
end ) ,
2020-06-09 18:53:06 +00:00
self ,
2019-09-29 04:07:32 +00:00
} )
end
2021-09-23 23:08:38 +00:00
--[=[
Returns the current Promise status .
@ return Status
] = ]
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
2021-11-24 22:28:49 +00:00
successCallback = createAdvancer ( traceback , successHandler , resolve , reject )
2018-01-26 01:21:58 +00:00
end
local failureCallback = reject
if failureHandler then
2021-11-24 22:28:49 +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.
2020-05-06 23:22:48 +00:00
reject ( Error.new ( {
error = " Promise is cancelled " ,
kind = Error.Kind . AlreadyCancelled ,
2020-06-09 18:53:06 +00:00
context = " Promise created at \n \n " .. traceback ,
2020-05-06 23:22:48 +00:00
} ) )
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
2021-09-23 23:08:38 +00:00
--[=[
Chains onto an existing Promise and returns a new Promise .
:: : warning
Within the failure handler , you should never assume that the rejection value is a string . Some rejections within the Promise library are represented by [[Error]] objects . If you want to treat it as a string for debugging , you should call ` tostring ` on it first .
:: :
Return a Promise from the success or failure handler and it will be chained onto .
@ param successHandler ( ... : any ) -> ... any
@ param failureHandler ? ( ... : any ) -> ... any
@ return Promise < ... any >
] = ]
2019-09-28 09:14:53 +00:00
function Promise . prototype : andThen ( successHandler , failureHandler )
assert (
successHandler == nil or type ( successHandler ) == " function " ,
2020-06-09 18:53:06 +00:00
string.format ( ERROR_NON_FUNCTION , " Promise:andThen " )
2019-09-28 09:14:53 +00:00
)
assert (
failureHandler == nil or type ( failureHandler ) == " function " ,
2020-06-09 18:53:06 +00:00
string.format ( ERROR_NON_FUNCTION , " Promise:andThen " )
2019-09-28 09:14:53 +00:00
)
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
2021-09-23 23:08:38 +00:00
--[=[
Shorthand for ` Promise : andThen ( nil , failureHandler ) ` .
Returns a Promise that resolves if the ` failureHandler ` worked without encountering an additional error .
:: : warning
Within the failure handler , you should never assume that the rejection value is a string . Some rejections within the Promise library are represented by [[Error]] objects . If you want to treat it as a string for debugging , you should call ` tostring ` on it first .
:: :
@ param failureHandler ( ... : any ) -> ... any
@ return Promise < ... any >
] = ]
2021-12-28 02:28:28 +00:00
function Promise . prototype : catch ( failureHandler )
2019-09-28 09:14:53 +00:00
assert (
2021-12-28 02:28:28 +00:00
failureHandler == nil or type ( failureHandler ) == " function " ,
2020-06-09 18:53:06 +00:00
string.format ( ERROR_NON_FUNCTION , " Promise:catch " )
2019-09-28 09:14:53 +00:00
)
2021-12-28 02:28:28 +00:00
return self : _andThen ( debug.traceback ( nil , 2 ) , nil , failureHandler )
2018-01-26 01:21:58 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
Similar to [ Promise.andThen ] ( # andThen ) , except the return value is the same as the value passed to the handler . In other words , you can insert a ` : tap ` into a Promise chain without affecting the value that downstream Promises receive .
` ` ` lua
getTheValue ( )
: tap ( print )
: andThen ( function ( theValue )
print ( " Got " , theValue , " even though print returns nil! " )
end )
` ` `
If you return a Promise from the tap handler callback , its value will be discarded but ` tap ` will still wait until it resolves before passing the original value through .
@ param tapHandler ( ... : any ) -> ... any
@ return Promise < ... any >
] = ]
2021-12-28 02:28:28 +00:00
function Promise . prototype : tap ( tapHandler )
assert ( type ( tapHandler ) == " function " , string.format ( ERROR_NON_FUNCTION , " Promise:tap " ) )
2020-05-05 03:56:49 +00:00
return self : _andThen ( debug.traceback ( nil , 2 ) , function ( ... )
2021-12-28 02:28:28 +00:00
local callbackReturn = tapHandler ( ... )
2019-09-28 05:33:06 +00:00
if Promise.is ( callbackReturn ) then
local length , values = pack ( ... )
return callbackReturn : andThen ( function ( )
return unpack ( values , 1 , length )
end )
end
return ...
end )
end
2021-09-23 23:08:38 +00:00
--[=[
Attaches an ` andThen ` handler to this Promise that calls the given callback with the predefined arguments . The resolved value is discarded .
` ` ` lua
promise : andThenCall ( someFunction , " some " , " arguments " )
` ` `
This is sugar for
` ` ` lua
promise : andThen ( function ( )
return someFunction ( " some " , " arguments " )
end )
` ` `
@ param callback ( ... : any ) -> any
@ param ... ? any -- Additional arguments which will be passed to `callback`
@ return Promise
] = ]
2019-09-13 00:13:29 +00:00
function Promise . prototype : andThenCall ( callback , ... )
2020-06-09 18:53:06 +00:00
assert ( type ( callback ) == " function " , string.format ( ERROR_NON_FUNCTION , " 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
2021-09-23 23:08:38 +00:00
--[=[
Attaches an ` andThen ` handler to this Promise that discards the resolved value and returns the given value from it .
` ` ` lua
promise : andThenReturn ( " some " , " values " )
` ` `
This is sugar for
` ` ` lua
promise : andThen ( function ( )
return " some " , " values "
end )
` ` `
:: : caution
Promises are eager , so if you pass a Promise to ` andThenReturn ` , it will begin executing before ` andThenReturn ` is reached in the chain . Likewise , if you pass a Promise created from [[Promise.reject]] into ` andThenReturn ` , it ' s possible that this will trigger the unhandled rejection warning. If you need to return a Promise, it ' s usually best practice to use [[Promise.andThen]] .
:: :
@ param ... any -- Values to return from the function
@ return Promise
] = ]
2019-09-29 02:03:06 +00:00
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
2021-09-23 23:08:38 +00:00
--[=[
Cancels this promise , preventing the promise from resolving or rejecting . Does not do anything if the promise is already settled .
Cancellations will propagate upwards and downwards through chained promises .
Promises will only be cancelled if all of their consumers are also cancelled . This is to say that if you call ` andThen ` twice on the same promise , and you cancel only one of the child promises , it will not cancel the parent promise until the other child promise is also cancelled .
` ` ` lua
promise : cancel ( )
` ` `
] = ]
2018-10-23 23:12:05 +00:00
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
2021-11-24 22:28:49 +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
2021-09-23 23:08:38 +00:00
--[=[
Set a handler that will be called regardless of the promise ' s fate. The handler is called when the promise is resolved, rejected, *or* cancelled.
Returns a new promise chained from this promise .
:: : caution
If the Promise is cancelled , any Promises chained off of it with ` andThen ` won ' t run. Only Promises chained with `finally` or `done` will run in the case of cancellation.
:: :
` ` ` lua
local thing = createSomething ( )
doSomethingWith ( thing )
: andThen ( function ( )
print ( " It worked! " )
-- do something..
end )
: catch ( function ( )
warn ( " Oh no it failed! " )
end )
: finally ( function ( )
-- either way, destroy thing
thing : Destroy ( )
end )
` ` `
@ param finallyHandler ( status : Status ) -> ... any
@ return Promise < ... any >
] = ]
2019-09-28 09:14:53 +00:00
function Promise . prototype : finally ( finallyHandler )
assert (
finallyHandler == nil or type ( finallyHandler ) == " function " ,
2020-06-09 18:53:06 +00:00
string.format ( ERROR_NON_FUNCTION , " Promise:finally " )
2019-09-28 09:14:53 +00:00
)
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
2021-09-23 23:08:38 +00:00
--[=[
Same as ` andThenCall ` , except for ` finally ` .
Attaches a ` finally ` handler to this Promise that calls the given callback with the predefined arguments .
@ param callback ( ... : any ) -> any
@ param ... ? any -- Additional arguments which will be passed to `callback`
@ return Promise
] = ]
2019-09-13 00:13:29 +00:00
function Promise . prototype : finallyCall ( callback , ... )
2020-06-09 18:53:06 +00:00
assert ( type ( callback ) == " function " , string.format ( ERROR_NON_FUNCTION , " 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
2021-09-23 23:08:38 +00:00
--[=[
Attaches a ` finally ` handler to this Promise that discards the resolved value and returns the given value from it .
` ` ` lua
promise : finallyReturn ( " some " , " values " )
` ` `
This is sugar for
` ` ` lua
promise : finally ( function ( )
return " some " , " values "
end )
` ` `
@ param ... any -- Values to return from the function
@ return Promise
] = ]
2019-09-29 02:03:06 +00:00
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
2021-09-23 23:08:38 +00:00
--[=[
Set a handler that will be called only if the Promise resolves or is cancelled . This method is similar to ` finally ` , except it doesn ' t catch rejections.
:: : caution
` done ` should be reserved specifically when you want to perform some operation after the Promise is finished ( like ` finally ` ) , but you don ' t want to consume rejections (like in <a href="/roblox-lua-promise/lib/Examples.html#cancellable-animation-sequence">this example</a>). You should use `andThen` instead if you only care about the Resolved case.
:: :
:: : warning
Like ` finally ` , if the Promise is cancelled , any Promises chained off of it with ` andThen ` won ' t run. Only Promises chained with `done` and `finally` will run in the case of cancellation.
:: :
Returns a new promise chained from this promise .
@ param doneHandler ( status : Status ) -> ... any
@ return Promise < ... any >
] = ]
2021-12-28 02:28:28 +00:00
function Promise . prototype : done ( doneHandler )
assert ( doneHandler == nil or type ( doneHandler ) == " function " , string.format ( ERROR_NON_FUNCTION , " Promise:done " ) )
return self : _finally ( debug.traceback ( nil , 2 ) , doneHandler , true )
2019-09-29 02:03:06 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
Same as ` andThenCall ` , except for ` done ` .
Attaches a ` done ` handler to this Promise that calls the given callback with the predefined arguments .
@ param callback ( ... : any ) -> any
@ param ... ? any -- Additional arguments which will be passed to `callback`
@ return Promise
] = ]
2019-09-29 02:03:06 +00:00
function Promise . prototype : doneCall ( callback , ... )
2020-06-09 18:53:06 +00:00
assert ( type ( callback ) == " function " , string.format ( ERROR_NON_FUNCTION , " Promise:doneCall " ) )
2019-09-29 02:03:06 +00:00
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
2021-09-23 23:08:38 +00:00
--[=[
Attaches a ` done ` handler to this Promise that discards the resolved value and returns the given value from it .
` ` ` lua
promise : doneReturn ( " some " , " values " )
` ` `
This is sugar for
` ` ` lua
promise : done ( function ( )
return " some " , " values "
end )
` ` `
@ param ... any -- Values to return from the function
@ return Promise
] = ]
2019-09-29 02:03:06 +00:00
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
2021-09-23 23:08:38 +00:00
--[=[
Yields the current thread until the given Promise completes . Returns the Promise ' s status, followed by the values that the promise resolved or rejected with.
2018-01-26 01:21:58 +00:00
2021-09-23 23:08:38 +00:00
@ yields
@ return Status -- The Status representing the fate of the Promise
@ return ... any -- The values the Promise resolved or rejected with.
] = ]
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
2020-02-14 12:02:48 +00:00
local function awaitHelper ( status , ... )
return status == Promise.Status . Resolved , ...
end
2021-09-23 23:08:38 +00:00
--[=[
Yields the current thread until the given Promise completes . Returns true if the Promise resolved , followed by the values that the promise resolved or rejected with .
:: : caution
If the Promise gets cancelled , this function will return ` false ` , which is indistinguishable from a rejection . If you need to differentiate , you should use [[Promise.awaitStatus]] instead .
:: :
` ` ` lua
local worked , value = getTheValue ( ) : await ( )
if worked then
print ( " got " , value )
else
warn ( " it failed " )
end
` ` `
@ yields
@ return boolean -- `true` if the Promise successfully resolved
@ return ... any -- The values the Promise resolved or rejected with.
] = ]
2020-05-06 18:20:34 +00:00
function Promise . prototype : await ( )
return awaitHelper ( self : awaitStatus ( ) )
2020-02-14 12:02:48 +00:00
end
local function expectHelper ( status , ... )
if status ~= Promise.Status . Resolved then
2020-05-06 18:20:34 +00:00
error ( ( ... ) == nil and " Expected Promise rejected with no value. " or ( ... ) , 3 )
2020-02-14 12:02:48 +00:00
end
2019-09-12 07:58:56 +00:00
2020-02-14 12:02:48 +00:00
return ...
2018-01-26 01:21:58 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
Yields the current thread until the given Promise completes . Returns the the values that the promise resolved with .
` ` ` lua
local worked = pcall ( function ( )
print ( " got " , getTheValue ( ) : expect ( ) )
end )
if not worked then
warn ( " it failed " )
end
` ` `
This is essentially sugar for :
` ` ` lua
select ( 2 , assert ( promise : await ( ) ) )
` ` `
** Errors ** if the Promise rejects or gets cancelled .
@ error any -- Errors with the rejection value if this Promise rejects or gets cancelled.
@ yields
@ return ... any -- The values the Promise resolved with.
] = ]
2020-05-06 18:20:34 +00:00
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
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
2020-06-09 18:53:06 +00:00
local message = string.format (
2021-11-24 22:28:49 +00:00
" When returning a Promise from andThen, extra arguments are " .. " discarded! See: \n \n %s " ,
2018-01-26 01:21:58 +00:00
self._source
)
warn ( message )
end
2019-09-28 09:14:53 +00:00
local chainedPromise = ...
2021-11-24 22:28:49 +00:00
local promise = chainedPromise : andThen ( function ( ... )
self : _resolve ( ... )
end , function ( ... )
local maybeRuntimeError = chainedPromise._values [ 1 ]
-- Backwards compatibility < v2
if chainedPromise._error then
maybeRuntimeError = Error.new ( {
error = chainedPromise._error ,
kind = Error.Kind . ExecutionError ,
context = " [No stack trace available as this Promise originated from an older version of the Promise library (< v2)] " ,
} )
end
2020-05-05 03:56:49 +00:00
2021-11-24 22:28:49 +00:00
if Error.isKind ( maybeRuntimeError , Error.Kind . ExecutionError ) then
return self : _reject ( maybeRuntimeError : extend ( {
error = " This Promise was chained to a Promise that errored. " ,
trace = " " ,
context = string.format (
" The Promise at: \n \n %s \n ...Rejected because it was chained to the following Promise, which encountered an error: \n " ,
self._source
) ,
} ) )
2018-09-14 20:38:52 +00:00
end
2021-11-24 22:28: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
2020-07-11 03:45:47 +00:00
coroutine.wrap ( callback ) ( ... )
2018-01-26 01:21:58 +00:00
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
2020-07-11 03:45:47 +00:00
coroutine.wrap ( callback ) ( ... )
2018-01-26 01:21:58 +00:00
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-08-07 05:52:41 +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
2021-11-24 22:28:49 +00:00
local message = string.format ( " Unhandled Promise rejection: \n \n %s \n \n %s " , err , self._source )
2020-05-05 03:56:49 +00:00
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
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.
2020-07-11 03:45:47 +00:00
coroutine.wrap ( callback ) ( self._status )
2018-10-24 06:33:30 +00:00
end
2019-09-12 07:58:56 +00:00
2020-08-24 17:34:36 +00:00
self._queuedFinally = nil
self._queuedReject = nil
self._queuedResolve = nil
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
2021-09-23 23:08:38 +00:00
--[=[
Chains a Promise from this one that is resolved if this Promise is already resolved , and rejected if it is not resolved at the time of calling ` : now ( ) ` . This can be used to ensure your ` andThen ` handler occurs on the same frame as the root Promise execution .
` ` ` lua
doSomething ( )
: now ( )
: andThen ( function ( value )
print ( " Got " , value , " synchronously. " )
end )
` ` `
If this Promise is still running , Rejected , or Cancelled , the Promise returned from ` : now ( ) ` will reject with the ` rejectionValue ` if passed , otherwise with a ` Promise.Error ( Promise.Error . Kind.NotResolvedInTime ) ` . This can be checked with [[Error.isKind]] .
@ param rejectionValue ? any -- The value to reject with if the Promise isn't resolved
@ return Promise
] = ]
2020-05-06 23:22:48 +00:00
function Promise . prototype : now ( rejectionValue )
local traceback = debug.traceback ( nil , 2 )
2021-03-19 00:43:49 +00:00
if self._status == Promise.Status . Resolved then
2020-05-06 23:22:48 +00:00
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() " ,
2020-06-09 18:53:06 +00:00
context = " :now() was called at: \n \n " .. traceback ,
2020-05-06 23:22:48 +00:00
} ) or rejectionValue )
end
end
2021-09-23 23:08:38 +00:00
--[=[
Repeatedly calls a Promise - returning function up to ` times ` number of times , until the returned Promise resolves .
If the amount of retries is exceeded , the function will return the latest rejected Promise .
` ` ` lua
local function canFail ( a , b , c )
return Promise.new ( function ( resolve , reject )
-- do something that can fail
local failed , thing = doSomethingThatCanFail ( a , b , c )
if failed then
reject ( " it failed " )
else
resolve ( thing )
end
end )
end
local MAX_RETRIES = 10
local value = Promise.retry ( canFail , MAX_RETRIES , " foo " , " bar " , " baz " ) -- args to send to canFail
` ` `
@ since 3.0 .0
@ param callback ( ... : P ) -> Promise < T >
@ param times number
@ param ... ? P
] = ]
2020-05-29 06:10:29 +00:00
function Promise . retry ( callback , times , ... )
assert ( type ( callback ) == " function " , " Parameter #1 to Promise.retry must be a function " )
assert ( type ( times ) == " number " , " Parameter #2 to Promise.retry must be a number " )
2021-11-24 22:28:49 +00:00
local args , length = { ... } , select ( " # " , ... )
2020-05-29 06:10:29 +00:00
2020-06-09 18:53:06 +00:00
return Promise.resolve ( callback ( ... ) ) : catch ( function ( ... )
if times > 0 then
return Promise.retry ( callback , times - 1 , unpack ( args , 1 , length ) )
else
return Promise.reject ( ... )
end
end )
2020-05-29 06:10:29 +00:00
end
2021-09-23 23:08:38 +00:00
--[=[
Converts an event into a Promise which resolves the next time the event fires .
The optional ` predicate ` callback , if passed , will receive the event arguments and should return ` true ` or ` false ` , based on if this fired event should resolve the Promise or not . If ` true ` , the Promise resolves . If ` false ` , nothing happens and the predicate will be rerun the next time the event fires .
The Promise will resolve with the event arguments .
:: : tip
This function will work given any object with a ` Connect ` method . This includes all Roblox events .
:: :
` ` ` lua
-- Creates a Promise which only resolves when `somePart` is touched
-- by a part named `"Something specific"`.
return Promise.fromEvent ( somePart.Touched , function ( part )
return part.Name == " Something specific "
end )
` ` `
@ since 3.0 .0
@ param event Event -- Any object with a `Connect` method. This includes all Roblox events.
@ param predicate ? ( ... : P ) -> boolean -- A function which determines if the Promise should resolve with the given value, or wait for the next event to check again.
@ return Promise < P >
] = ]
2020-05-29 06:10:45 +00:00
function Promise . fromEvent ( event , predicate )
predicate = predicate or function ( )
return true
end
2021-03-19 00:43:49 +00:00
return Promise._new ( debug.traceback ( nil , 2 ) , function ( resolve , _ , onCancel )
2020-05-29 06:10:45 +00:00
local connection
local shouldDisconnect = false
local function disconnect ( )
connection : Disconnect ( )
connection = nil
end
-- We use shouldDisconnect because if the callback given to Connect is called before
-- Connect returns, connection will still be nil. This happens with events that queue up
-- events when there's nothing connected, such as RemoteEvents
connection = event : Connect ( function ( ... )
local callbackValue = predicate ( ... )
if callbackValue == true then
resolve ( ... )
if connection then
disconnect ( )
else
shouldDisconnect = true
end
elseif type ( callbackValue ) ~= " boolean " then
error ( " Promise.fromEvent predicate should always return a boolean " )
end
end )
if shouldDisconnect and connection then
return disconnect ( )
end
2021-03-19 00:43:49 +00:00
onCancel ( disconnect )
2020-05-29 06:10:45 +00:00
end )
end
2020-02-14 12:02:48 +00:00
return Promise