2018-01-26 01:21:58 +00:00
|
|
|
--[[
|
|
|
|
An implementation of Promises similar to Promise/A+.
|
|
|
|
]]
|
|
|
|
|
|
|
|
local PROMISE_DEBUG = false
|
|
|
|
|
2018-07-05 22:49:25 +00:00
|
|
|
--[[
|
|
|
|
Packs a number of arguments into a table and returns its length.
|
|
|
|
|
|
|
|
Used to cajole varargs without dropping sparse values.
|
|
|
|
]]
|
|
|
|
local function pack(...)
|
|
|
|
local len = select("#", ...)
|
|
|
|
|
|
|
|
return len, { ... }
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
wpcallPacked is a version of xpcall that:
|
|
|
|
* Returns the length of the result first
|
|
|
|
* Returns the result packed into a table
|
2018-09-14 20:47:10 +00:00
|
|
|
* Passes extra arguments through to the passed function; xpcall doesn't
|
2018-07-05 22:49:25 +00:00
|
|
|
* Issues a warning if PROMISE_DEBUG is enabled
|
|
|
|
]]
|
|
|
|
local function wpcallPacked(f, ...)
|
|
|
|
local argsLength, args = pack(...)
|
|
|
|
|
|
|
|
local body = function()
|
|
|
|
return f(unpack(args, 1, argsLength))
|
|
|
|
end
|
|
|
|
|
|
|
|
local resultLength, result = pack(xpcall(body, debug.traceback))
|
2018-05-21 21:10:38 +00:00
|
|
|
|
|
|
|
-- If promise debugging is on, warn whenever a pcall fails.
|
|
|
|
-- This is useful for debugging issues within the Promise implementation
|
|
|
|
-- itself.
|
|
|
|
if PROMISE_DEBUG and not result[1] then
|
|
|
|
warn(result[2])
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
2018-05-21 21:10:38 +00:00
|
|
|
|
2018-07-05 22:49:25 +00:00
|
|
|
return resultLength, result
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Creates a function that invokes a callback with correct error handling and
|
|
|
|
resolution mechanisms.
|
|
|
|
]]
|
|
|
|
local function createAdvancer(callback, resolve, reject)
|
|
|
|
return function(...)
|
2018-07-05 22:49:25 +00:00
|
|
|
local resultLength, result = wpcallPacked(callback, ...)
|
|
|
|
local ok = result[1]
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
if ok then
|
2018-07-05 22:49:25 +00:00
|
|
|
resolve(unpack(result, 2, resultLength))
|
2018-01-26 01:21:58 +00:00
|
|
|
else
|
2018-07-05 22:49:25 +00:00
|
|
|
reject(unpack(result, 2, resultLength))
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function isEmpty(t)
|
|
|
|
return next(t) == nil
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:45:27 +00:00
|
|
|
local function createSymbol(name)
|
|
|
|
assert(type(name) == "string", "createSymbol requires `name` to be a string.")
|
|
|
|
|
|
|
|
local symbol = newproxy(true)
|
|
|
|
|
|
|
|
getmetatable(symbol).__tostring = function()
|
|
|
|
return ("Symbol(%s)"):format(name)
|
|
|
|
end
|
|
|
|
|
|
|
|
return symbol
|
|
|
|
end
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
local Promise = {}
|
2018-09-14 20:43:22 +00:00
|
|
|
Promise.prototype = {}
|
|
|
|
Promise.__index = Promise.prototype
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
Promise.Status = {
|
2018-09-14 20:45:27 +00:00
|
|
|
Started = createSymbol("Started"),
|
|
|
|
Resolved = createSymbol("Resolved"),
|
|
|
|
Rejected = createSymbol("Rejected"),
|
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.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
local function get(url)
|
|
|
|
return Promise.new(function(resolve, reject)
|
|
|
|
spawn(function()
|
|
|
|
resolve(HttpService:GetAsync(url))
|
|
|
|
end)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
get("https://google.com")
|
2018-04-11 22:26:54 +00:00
|
|
|
:andThen(function(stuff)
|
|
|
|
print("Got some stuff!", stuff)
|
2018-01-26 01:21:58 +00:00
|
|
|
end)
|
|
|
|
]]
|
|
|
|
function Promise.new(callback)
|
|
|
|
local promise = {
|
|
|
|
-- Used to locate where a promise was created
|
|
|
|
_source = debug.traceback(),
|
|
|
|
|
|
|
|
-- A tag to identify us as a promise
|
|
|
|
_type = "Promise",
|
|
|
|
|
|
|
|
_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,
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
-- If an error occurs with no observers, this will be set.
|
|
|
|
_unhandledRejection = false,
|
|
|
|
|
|
|
|
-- Queues representing functions we should invoke when we update!
|
|
|
|
_queuedResolve = {},
|
|
|
|
_queuedReject = {},
|
|
|
|
}
|
|
|
|
|
|
|
|
setmetatable(promise, Promise)
|
|
|
|
|
|
|
|
local function resolve(...)
|
|
|
|
promise:_resolve(...)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function reject(...)
|
|
|
|
promise:_reject(...)
|
|
|
|
end
|
|
|
|
|
2018-07-05 22:49:25 +00:00
|
|
|
local _, result = wpcallPacked(callback, resolve, reject)
|
|
|
|
local ok = result[1]
|
|
|
|
local err = result[2]
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
if not ok and promise._status == Promise.Status.Started then
|
|
|
|
reject(err)
|
|
|
|
end
|
|
|
|
|
|
|
|
return promise
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Create a promise that represents the immediately resolved value.
|
|
|
|
]]
|
|
|
|
function Promise.resolve(value)
|
|
|
|
return Promise.new(function(resolve)
|
|
|
|
resolve(value)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Create a promise that represents the immediately rejected value.
|
|
|
|
]]
|
|
|
|
function Promise.reject(value)
|
|
|
|
return Promise.new(function(_, reject)
|
|
|
|
reject(value)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Returns a new promise that:
|
|
|
|
* is resolved when all input promises resolve
|
|
|
|
* is rejected if ANY input promises reject
|
|
|
|
]]
|
2018-09-14 18:17:06 +00:00
|
|
|
function Promise.all(promises)
|
|
|
|
if type(promises) ~= "table" then
|
2018-09-14 18:31:24 +00:00
|
|
|
error("Please pass a list of promises to Promise.all", 2)
|
2018-09-14 18:17:06 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- If there are no values then return an already resolved promise.
|
|
|
|
if #promises == 0 then
|
|
|
|
return Promise.resolve({})
|
|
|
|
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.
|
2018-09-14 18:17:06 +00:00
|
|
|
for i = 1, #promises do
|
|
|
|
if not Promise.is(promises[i]) then
|
|
|
|
error(("Non-promise value passed into Promise.all at index #%d"):format(i), 2)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return Promise.new(function(resolve, reject)
|
|
|
|
-- An array to contain our resolved values from the given promises.
|
|
|
|
local resolvedValues = {}
|
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 rejected = false
|
|
|
|
|
|
|
|
-- Called when a single value is resolved and resolves if all are done.
|
|
|
|
local function resolveOne(i, ...)
|
|
|
|
if rejected then
|
|
|
|
-- Bail out if this promise has already been rejected.
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
resolvedValues[i] = ...
|
|
|
|
resolvedCount = resolvedCount + 1
|
|
|
|
|
|
|
|
if resolvedCount == #promises then
|
|
|
|
resolve(resolvedValues)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:47:10 +00:00
|
|
|
-- We can assume the values inside `promises` are all promises since we
|
|
|
|
-- checked above.
|
2018-09-14 18:17:06 +00:00
|
|
|
for i = 1, #promises do
|
2018-09-14 18:31:24 +00:00
|
|
|
promises[i]:andThen(
|
|
|
|
function(...)
|
|
|
|
resolveOne(i, ...)
|
|
|
|
end,
|
|
|
|
function(...)
|
|
|
|
if not rejected then
|
|
|
|
reject(...)
|
|
|
|
end
|
2018-09-14 18:17:06 +00:00
|
|
|
end
|
2018-09-14 18:31:24 +00:00
|
|
|
)
|
2018-09-14 18:17:06 +00:00
|
|
|
end
|
|
|
|
end)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Is the given object a Promise instance?
|
|
|
|
]]
|
|
|
|
function Promise.is(object)
|
|
|
|
if type(object) ~= "table" then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
return object._type == "Promise"
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:getStatus()
|
2018-06-17 02:40:57 +00:00
|
|
|
return self._status
|
|
|
|
end
|
|
|
|
|
2018-01-26 01:21:58 +00:00
|
|
|
--[[
|
|
|
|
Creates a new promise that receives the result of this promise.
|
|
|
|
|
|
|
|
The given callbacks are invoked depending on that result.
|
|
|
|
]]
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:andThen(successHandler, failureHandler)
|
2018-01-26 01:21:58 +00:00
|
|
|
self._unhandledRejection = false
|
|
|
|
|
|
|
|
-- Create a new promise to follow this part of the chain
|
|
|
|
return Promise.new(function(resolve, reject)
|
|
|
|
-- Our default callbacks just pass values onto the next promise.
|
|
|
|
-- This lets success and failure cascade correctly!
|
|
|
|
|
|
|
|
local successCallback = resolve
|
|
|
|
if successHandler then
|
|
|
|
successCallback = createAdvancer(successHandler, resolve, reject)
|
|
|
|
end
|
|
|
|
|
|
|
|
local failureCallback = reject
|
|
|
|
if failureHandler then
|
|
|
|
failureCallback = createAdvancer(failureHandler, resolve, reject)
|
|
|
|
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-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Used to catch any errors that may have occurred in the promise.
|
|
|
|
]]
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:catch(failureCallback)
|
2018-01-26 01:21:58 +00:00
|
|
|
return self:andThen(nil, failureCallback)
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Yield until the promise is completed.
|
|
|
|
|
|
|
|
This matches the execution model of normal Roblox functions.
|
|
|
|
]]
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:await()
|
2018-01-26 01:21:58 +00:00
|
|
|
self._unhandledRejection = false
|
|
|
|
|
|
|
|
if self._status == Promise.Status.Started then
|
|
|
|
local result
|
2018-06-17 02:47:21 +00:00
|
|
|
local resultLength
|
2018-01-26 01:21:58 +00:00
|
|
|
local bindable = Instance.new("BindableEvent")
|
|
|
|
|
2018-09-14 20:39:27 +00:00
|
|
|
self:andThen(
|
|
|
|
function(...)
|
|
|
|
resultLength, result = pack(...)
|
|
|
|
bindable:Fire(true)
|
|
|
|
end,
|
|
|
|
function(...)
|
|
|
|
resultLength, result = pack(...)
|
|
|
|
bindable:Fire(false)
|
|
|
|
end
|
|
|
|
)
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
local ok = bindable.Event:Wait()
|
|
|
|
bindable:Destroy()
|
|
|
|
|
2018-06-17 02:47:21 +00:00
|
|
|
return ok, unpack(result, 1, resultLength)
|
2018-01-26 01:21:58 +00:00
|
|
|
elseif self._status == Promise.Status.Resolved then
|
2018-06-17 02:47:21 +00:00
|
|
|
return true, unpack(self._values, 1, self._valuesLength)
|
2018-01-26 01:21:58 +00:00
|
|
|
elseif self._status == Promise.Status.Rejected then
|
2018-06-17 02:47:21 +00:00
|
|
|
return false, unpack(self._values, 1, self._valuesLength)
|
2018-01-26 01:21:58 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-09-14 18:31:24 +00:00
|
|
|
--[[
|
|
|
|
Intended for use in tests.
|
|
|
|
|
|
|
|
Similar to await(), but instead of yielding if the promise is unresolved,
|
|
|
|
_unwrap will throw. This indicates an assumption that a promise has
|
|
|
|
resolved.
|
|
|
|
]]
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:_unwrap()
|
2018-09-14 18:31:24 +00:00
|
|
|
if self._status == Promise.Status.Started then
|
|
|
|
error("Promise has not resolved or rejected.", 2)
|
|
|
|
end
|
|
|
|
|
|
|
|
local success = self._status == Promise.Status.Resolved
|
|
|
|
|
|
|
|
return success, unpack(self._values, 1, self._valuesLength)
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:_resolve(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
if self._status ~= Promise.Status.Started then
|
|
|
|
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
|
|
|
|
|
2018-09-14 20:38:52 +00:00
|
|
|
(...):andThen(
|
|
|
|
function(...)
|
|
|
|
self:_resolve(...)
|
|
|
|
end,
|
|
|
|
function(...)
|
|
|
|
self:_reject(...)
|
|
|
|
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
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:43:22 +00:00
|
|
|
function Promise.prototype:_reject(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
if self._status ~= Promise.Status.Started then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
self._status = Promise.Status.Rejected
|
2018-09-14 20:38:52 +00:00
|
|
|
self._valuesLength, self._values = pack(...)
|
2018-01-26 01:21:58 +00:00
|
|
|
|
|
|
|
-- If there are any rejection handlers, call those!
|
|
|
|
if not isEmpty(self._queuedReject) then
|
|
|
|
-- We assume that these callbacks will not throw errors.
|
|
|
|
for _, callback in ipairs(self._queuedReject) do
|
|
|
|
callback(...)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
-- At this point, no one was able to observe the error.
|
|
|
|
-- An error handler might still be attached if the error occurred
|
|
|
|
-- synchronously. We'll wait one tick, and if there are still no
|
|
|
|
-- observers, then we should put a message in the console.
|
|
|
|
|
|
|
|
self._unhandledRejection = true
|
|
|
|
local err = tostring((...))
|
|
|
|
|
|
|
|
spawn(function()
|
|
|
|
-- Someone observed the error, hooray!
|
|
|
|
if not self._unhandledRejection then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Build a reasonable message
|
|
|
|
local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
|
|
|
|
err,
|
|
|
|
self._source
|
|
|
|
)
|
|
|
|
warn(message)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-09-14 20:47:10 +00:00
|
|
|
return Promise
|