mirror of
https://github.com/AmberGraceRblx/luau-promise.git
synced 2025-04-24 15:50:01 +00:00
Use xpcall, revamp error handling
This commit is contained in:
parent
344c9759aa
commit
1ca3fff6f5
6 changed files with 380 additions and 145 deletions
|
@ -1,3 +1,11 @@
|
|||
# Next
|
||||
- Runtime errors are now represented by objects. You must call tostring on rejection values before assuming they are strings (this was always good practice, but is required now).
|
||||
- Errors now have much better stack traces due to using xpcall internally instead of pcall.
|
||||
- Stack traces now be more direct and not include as many internal calls within the Promise library.
|
||||
- Chained promises from resolve() or returning from andThen now have improved rejection messages for debugging.
|
||||
- Yielding is now allowed in Promise.new and andThen executors.
|
||||
- Improve test coverage for asynchronous and time-driven functions
|
||||
|
||||
# 2.5.1
|
||||
|
||||
- Fix issue with rejecting with non-string not propagating correctly.
|
||||
|
|
|
@ -4,7 +4,7 @@ docs:
|
|||
desc: 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*).
|
||||
|
||||
types:
|
||||
- name: PromiseStatus
|
||||
- name: Status
|
||||
desc: An enum value used to represent the Promise's status.
|
||||
kind: enum
|
||||
type:
|
||||
|
@ -30,11 +30,9 @@ docs:
|
|||
desc: |
|
||||
Construct a new Promise that will be resolved or rejected with the given callbacks.
|
||||
|
||||
::: tip
|
||||
If your Promise executor needs to yield, it is recommended to use [[Promise.async]] instead. You cannot directly yield inside the `executor` function of [[Promise.new]].
|
||||
:::
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
@ -73,20 +71,16 @@ docs:
|
|||
- type: boolean
|
||||
desc: "Returns `true` if the Promise was already cancelled at the time of calling `onCancel`."
|
||||
returns: Promise
|
||||
- name: async
|
||||
- name: defer
|
||||
tags: [ 'constructor' ]
|
||||
desc: |
|
||||
The same as [[Promise.new]], except it allows yielding. Use this if you want to yield inside your Promise body.
|
||||
|
||||
If your Promise body does not need to yield, such as when attaching `resolve` to an event listener, you should use [[Promise.new]] instead.
|
||||
|
||||
::: tip
|
||||
Promises created with [[Promise.async]] don't begin executing until the next `RunService.Heartbeat` event, even if the executor function doesn't yield itself. This is to ensure that Promises produced from a function are either always synchronous or always asynchronous. <a href="/roblox-lua-promise/lib/Details.html#yielding-in-promise-executor">Learn more</a>
|
||||
:::
|
||||
The same as [[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/3db84579866c099cdd5bb2ff37947cec) as `spawn`.
|
||||
|
||||
```lua
|
||||
local function waitForChild(instance, childName, timeout)
|
||||
return Promise.async(function(resolve, reject)
|
||||
return Promise.defer(function(resolve, reject)
|
||||
local child = instance:WaitForChild(childName, timeout)
|
||||
|
||||
;(child and resolve or reject)(child)
|
||||
|
@ -96,7 +90,7 @@ docs:
|
|||
|
||||
static: true
|
||||
params:
|
||||
- name: asyncExecutor
|
||||
- name: deferExecutor
|
||||
type:
|
||||
kind: function
|
||||
params:
|
||||
|
@ -172,9 +166,9 @@ docs:
|
|||
returns: Promise<...any>
|
||||
- name: try
|
||||
desc: |
|
||||
Begins a Promise chain, calling a synchronous function and returning a Promise resolving with its return value. If the function errors, the returned Promise will be rejected with the error.
|
||||
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.
|
||||
|
||||
`Promise.try` is similar to [[Promise.promisify]], except the callback is invoked immediately instead of returning a new function, and unlike `promisify`, yielding is not allowed with `try`.
|
||||
`Promise.try` is similar to [[Promise.promisify]], except the callback is invoked immediately instead of returning a new function.
|
||||
|
||||
```lua
|
||||
Promise.try(function()
|
||||
|
|
266
lib/init.lua
266
lib/init.lua
|
@ -2,8 +2,6 @@
|
|||
An implementation of Promises similar to Promise/A+.
|
||||
]]
|
||||
|
||||
local ERROR_YIELD_NEW = "Yielding inside Promise.new is not allowed! Use Promise.async or create a new thread in the Promise executor!"
|
||||
local ERROR_YIELD_THEN = "Yielding inside andThen/catch is not allowed! Instead, return a new Promise from andThen/catch."
|
||||
local ERROR_NON_PROMISE_IN_LIST = "Non-promise value passed into %s at index %s"
|
||||
local ERROR_NON_LIST = "Please pass a list of promises to %s"
|
||||
local ERROR_NON_FUNCTION = "Please pass a handler function to %s!"
|
||||
|
@ -14,6 +12,36 @@ local MODE_KEY_METATABLE = {
|
|||
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
--[[
|
||||
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.
|
||||
]]
|
||||
local PromiseRuntimeError = {}
|
||||
PromiseRuntimeError.__index = PromiseRuntimeError
|
||||
|
||||
function PromiseRuntimeError.new(errorString)
|
||||
return setmetatable({
|
||||
errorString = tostring(errorString) or "[This error has no error text.]"
|
||||
}, PromiseRuntimeError)
|
||||
end
|
||||
|
||||
function PromiseRuntimeError.is(anything)
|
||||
if type(anything) == "table" then
|
||||
return rawget(anything, "errorString") ~= nil
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function PromiseRuntimeError:extend(errorString)
|
||||
return PromiseRuntimeError.new(("%s\n%s"):format(tostring(errorString), self.errorString))
|
||||
end
|
||||
|
||||
function PromiseRuntimeError:__tostring()
|
||||
return self.errorString
|
||||
end
|
||||
|
||||
--[[
|
||||
Packs a number of arguments into a table and returns its length.
|
||||
|
||||
|
@ -30,24 +58,18 @@ local function packResult(success, ...)
|
|||
return success, select("#", ...), { ... }
|
||||
end
|
||||
|
||||
--[[
|
||||
Calls a non-yielding function in a new coroutine.
|
||||
|
||||
Handles errors if they happen.
|
||||
]]
|
||||
local function runExecutor(yieldError, traceback, callback, ...)
|
||||
-- Wrapped because C functions can't be passed to coroutine.create!
|
||||
local co = coroutine.create(function(...)
|
||||
return callback(...)
|
||||
end)
|
||||
|
||||
local ok, len, result = packResult(coroutine.resume(co, ...))
|
||||
|
||||
if ok and coroutine.status(co) ~= "dead" then
|
||||
error(yieldError .. "\n" .. traceback, 2)
|
||||
local function makeErrorHandler(traceback)
|
||||
return function(err)
|
||||
return debug.traceback(err, 2) .. "\nPromise created at:\n\n" .. traceback
|
||||
end
|
||||
end
|
||||
|
||||
return ok, len, result
|
||||
--[[
|
||||
Calls a Promise executor with error handling.
|
||||
]]
|
||||
local function runExecutor(traceback, callback, ...)
|
||||
return packResult(xpcall(callback, makeErrorHandler(traceback), ...))
|
||||
end
|
||||
|
||||
--[[
|
||||
|
@ -56,12 +78,12 @@ end
|
|||
]]
|
||||
local function createAdvancer(traceback, callback, resolve, reject)
|
||||
return function(...)
|
||||
local ok, resultLength, result = runExecutor(ERROR_YIELD_THEN, traceback, callback, ...)
|
||||
local ok, resultLength, result = runExecutor(traceback, callback, ...)
|
||||
|
||||
if ok then
|
||||
resolve(unpack(result, 1, resultLength))
|
||||
else
|
||||
reject(result[1] .. "\n" .. traceback)
|
||||
reject(PromiseRuntimeError.new(result[1]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -70,7 +92,10 @@ local function isEmpty(t)
|
|||
return next(t) == nil
|
||||
end
|
||||
|
||||
local Promise = {}
|
||||
local Promise = {
|
||||
_timeEvent = RunService.Heartbeat,
|
||||
_getTime = tick
|
||||
}
|
||||
Promise.prototype = {}
|
||||
Promise.__index = Promise.prototype
|
||||
|
||||
|
@ -97,20 +122,17 @@ Promise.Status = setmetatable({
|
|||
Second parameter, parent, is used internally for tracking the "parent" in a
|
||||
promise chain. External code shouldn't need to worry about this.
|
||||
]]
|
||||
function Promise.new(callback, parent)
|
||||
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 = {
|
||||
-- Used to locate where a promise was created
|
||||
_source = debug.traceback(),
|
||||
_source = traceback,
|
||||
|
||||
_status = Promise.Status.Started,
|
||||
|
||||
-- Will be set to the Lua error string if it occurs while executing.
|
||||
_error = nil,
|
||||
|
||||
-- A table containing a list of all results, whether success or failure.
|
||||
-- Only valid if _status is set to something besides Started
|
||||
_values = nil,
|
||||
|
@ -131,9 +153,11 @@ function Promise.new(callback, parent)
|
|||
_cancellationHook = nil,
|
||||
|
||||
-- The "parent" of this promise in a promise chain. Required for
|
||||
-- cancellation propagation.
|
||||
-- cancellation propagation upstream.
|
||||
_parent = parent,
|
||||
|
||||
-- Consumers are Promises that have chained onto this one.
|
||||
-- We track them for cancellation propagation downstream.
|
||||
_consumers = setmetatable({}, MODE_KEY_METATABLE),
|
||||
}
|
||||
|
||||
|
@ -163,57 +187,45 @@ function Promise.new(callback, parent)
|
|||
return self._status == Promise.Status.Cancelled
|
||||
end
|
||||
|
||||
local ok, _, result = runExecutor(
|
||||
ERROR_YIELD_NEW,
|
||||
self._source,
|
||||
callback,
|
||||
resolve,
|
||||
reject,
|
||||
onCancel
|
||||
)
|
||||
coroutine.wrap(function()
|
||||
local ok, _, result = runExecutor(
|
||||
self._source,
|
||||
callback,
|
||||
resolve,
|
||||
reject,
|
||||
onCancel
|
||||
)
|
||||
|
||||
if not ok then
|
||||
self._error = result[1] or "error"
|
||||
reject((result[1] or "error") .. "\n" .. self._source)
|
||||
end
|
||||
if not ok then
|
||||
reject(PromiseRuntimeError.new(result[1]))
|
||||
end
|
||||
end)()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Promise._newWithSelf(executor, ...)
|
||||
local args
|
||||
local promise = Promise.new(function(...)
|
||||
args = { ... }
|
||||
end, ...)
|
||||
|
||||
-- we don't handle the length here since `args` will always be { resolve, reject, onCancelHook }
|
||||
executor(promise, unpack(args))
|
||||
|
||||
return promise
|
||||
function Promise.new(executor)
|
||||
return Promise._new(debug.traceback(nil, 2), executor)
|
||||
end
|
||||
|
||||
function Promise._new(traceback, executor, ...)
|
||||
return Promise._newWithSelf(function(self, ...)
|
||||
self._source = traceback
|
||||
executor(...)
|
||||
end, ...)
|
||||
function Promise:__tostring()
|
||||
return ("Promise(%s)"):format(self:getStatus())
|
||||
end
|
||||
|
||||
--[[
|
||||
Promise.new, except pcall on a new thread is automatic.
|
||||
]]
|
||||
function Promise.async(callback)
|
||||
local traceback = debug.traceback()
|
||||
function Promise.defer(callback)
|
||||
local traceback = debug.traceback(nil, 2)
|
||||
local promise
|
||||
promise = Promise._new(traceback, function(resolve, reject, onCancel)
|
||||
local connection
|
||||
connection = RunService.Heartbeat:Connect(function()
|
||||
connection = Promise._timeEvent:Connect(function()
|
||||
connection:Disconnect()
|
||||
local ok, err = pcall(callback, resolve, reject, onCancel)
|
||||
local ok, _, result = runExecutor(traceback, callback, resolve, reject, onCancel)
|
||||
|
||||
if not ok then
|
||||
promise._error = err or "error"
|
||||
reject(err .. "\n" .. traceback)
|
||||
reject(PromiseRuntimeError.new(result))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
@ -221,12 +233,15 @@ function Promise.async(callback)
|
|||
return promise
|
||||
end
|
||||
|
||||
-- Backwards compatibility
|
||||
Promise.async = Promise.defer
|
||||
|
||||
--[[
|
||||
Create a promise that represents the immediately resolved value.
|
||||
]]
|
||||
function Promise.resolve(...)
|
||||
local length, values = pack(...)
|
||||
return Promise._new(debug.traceback(), function(resolve)
|
||||
return Promise._new(debug.traceback(nil, 2), function(resolve)
|
||||
resolve(unpack(values, 1, length))
|
||||
end)
|
||||
end
|
||||
|
@ -236,16 +251,28 @@ end
|
|||
]]
|
||||
function Promise.reject(...)
|
||||
local length, values = pack(...)
|
||||
return Promise._new(debug.traceback(), function(_, reject)
|
||||
return Promise._new(debug.traceback(nil, 2), function(_, reject)
|
||||
reject(unpack(values, 1, length))
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
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
|
||||
|
||||
--[[
|
||||
Begins a Promise chain, turning synchronous errors into rejections.
|
||||
]]
|
||||
function Promise.try(...)
|
||||
return Promise.resolve():andThenCall(...)
|
||||
return Promise._try(debug.traceback(nil, 2), ...)
|
||||
end
|
||||
|
||||
--[[
|
||||
|
@ -271,9 +298,7 @@ function Promise._all(traceback, promises, amount)
|
|||
return Promise.resolve({})
|
||||
end
|
||||
|
||||
return Promise._newWithSelf(function(self, resolve, reject, onCancel)
|
||||
self._source = traceback
|
||||
|
||||
return Promise._new(traceback, function(resolve, reject, onCancel)
|
||||
-- An array to contain our resolved values from the given promises.
|
||||
local resolvedValues = {}
|
||||
local newPromises = {}
|
||||
|
@ -343,17 +368,17 @@ function Promise._all(traceback, promises, amount)
|
|||
end
|
||||
|
||||
function Promise.all(promises)
|
||||
return Promise._all(debug.traceback(), promises)
|
||||
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")
|
||||
|
||||
return Promise._all(debug.traceback(), promises, amount)
|
||||
return Promise._all(debug.traceback(nil, 2), promises, amount)
|
||||
end
|
||||
|
||||
function Promise.any(promises)
|
||||
return Promise._all(debug.traceback(), promises, 1):andThen(function(values)
|
||||
return Promise._all(debug.traceback(nil, 2), promises, 1):andThen(function(values)
|
||||
return values[1]
|
||||
end)
|
||||
end
|
||||
|
@ -376,7 +401,7 @@ function Promise.allSettled(promises)
|
|||
return Promise.resolve({})
|
||||
end
|
||||
|
||||
return Promise._new(debug.traceback(), function(resolve, _, onCancel)
|
||||
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 = {}
|
||||
|
@ -428,7 +453,7 @@ function Promise.race(promises)
|
|||
assert(Promise.is(promise), (ERROR_NON_PROMISE_IN_LIST):format("Promise.race", tostring(i)))
|
||||
end
|
||||
|
||||
return Promise._new(debug.traceback(), function(resolve, reject, onCancel)
|
||||
return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
|
||||
local newPromises = {}
|
||||
local finished = false
|
||||
|
||||
|
@ -471,7 +496,20 @@ function Promise.is(object)
|
|||
return false
|
||||
end
|
||||
|
||||
return type(object.andThen) == "function"
|
||||
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
|
||||
end
|
||||
|
||||
--[[
|
||||
|
@ -479,18 +517,7 @@ end
|
|||
]]
|
||||
function Promise.promisify(callback)
|
||||
return function(...)
|
||||
local traceback = debug.traceback()
|
||||
local length, values = pack(...)
|
||||
return Promise._new(traceback, function(resolve, reject)
|
||||
coroutine.wrap(function()
|
||||
local ok, resultLength, resultValues = packResult(pcall(callback, unpack(values, 1, length)))
|
||||
if ok then
|
||||
resolve(unpack(resultValues, 1, resultLength))
|
||||
else
|
||||
reject((resultValues[1] or "error") .. "\n" .. traceback)
|
||||
end
|
||||
end)()
|
||||
end)
|
||||
return Promise._try(debug.traceback(nil, 2), callback, ...)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -512,8 +539,8 @@ do
|
|||
seconds = 1 / 60
|
||||
end
|
||||
|
||||
return Promise._new(debug.traceback(), function(resolve, _, onCancel)
|
||||
local startTime = tick()
|
||||
return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
|
||||
local startTime = Promise._getTime()
|
||||
local endTime = startTime + seconds
|
||||
|
||||
local node = {
|
||||
|
@ -524,8 +551,8 @@ do
|
|||
|
||||
if connection == nil then -- first is nil when connection is nil
|
||||
first = node
|
||||
connection = RunService.Heartbeat:Connect(function()
|
||||
local currentTime = tick()
|
||||
connection = Promise._timeEvent:Connect(function()
|
||||
local currentTime = Promise._getTime()
|
||||
|
||||
while first.endTime <= currentTime do
|
||||
first.resolve(currentTime - first.startTime)
|
||||
|
@ -536,7 +563,7 @@ do
|
|||
break
|
||||
end
|
||||
first.previous = nil
|
||||
currentTime = tick()
|
||||
currentTime = Promise._getTime()
|
||||
end
|
||||
end)
|
||||
else -- first is non-nil
|
||||
|
@ -670,7 +697,7 @@ function Promise.prototype:andThen(successHandler, failureHandler)
|
|||
ERROR_NON_FUNCTION:format("Promise:andThen")
|
||||
)
|
||||
|
||||
return self:_andThen(debug.traceback(), successHandler, failureHandler)
|
||||
return self:_andThen(debug.traceback(nil, 2), successHandler, failureHandler)
|
||||
end
|
||||
|
||||
--[[
|
||||
|
@ -681,7 +708,7 @@ function Promise.prototype:catch(failureCallback)
|
|||
failureCallback == nil or type(failureCallback) == "function",
|
||||
ERROR_NON_FUNCTION:format("Promise:catch")
|
||||
)
|
||||
return self:_andThen(debug.traceback(), nil, failureCallback)
|
||||
return self:_andThen(debug.traceback(nil, 2), nil, failureCallback)
|
||||
end
|
||||
|
||||
--[[
|
||||
|
@ -690,7 +717,7 @@ end
|
|||
]]
|
||||
function Promise.prototype:tap(tapCallback)
|
||||
assert(type(tapCallback) == "function", ERROR_NON_FUNCTION:format("Promise:tap"))
|
||||
return self:_andThen(debug.traceback(), function(...)
|
||||
return self:_andThen(debug.traceback(nil, 2), function(...)
|
||||
local callbackReturn = tapCallback(...)
|
||||
|
||||
if Promise.is(callbackReturn) then
|
||||
|
@ -710,7 +737,7 @@ end
|
|||
function Promise.prototype:andThenCall(callback, ...)
|
||||
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:andThenCall"))
|
||||
local length, values = pack(...)
|
||||
return self:_andThen(debug.traceback(), function()
|
||||
return self:_andThen(debug.traceback(nil, 2), function()
|
||||
return callback(unpack(values, 1, length))
|
||||
end)
|
||||
end
|
||||
|
@ -720,7 +747,7 @@ end
|
|||
]]
|
||||
function Promise.prototype:andThenReturn(...)
|
||||
local length, values = pack(...)
|
||||
return self:_andThen(debug.traceback(), function()
|
||||
return self:_andThen(debug.traceback(nil, 2), function()
|
||||
return unpack(values, 1, length)
|
||||
end)
|
||||
end
|
||||
|
@ -814,7 +841,7 @@ function Promise.prototype:finally(finallyHandler)
|
|||
finallyHandler == nil or type(finallyHandler) == "function",
|
||||
ERROR_NON_FUNCTION:format("Promise:finally")
|
||||
)
|
||||
return self:_finally(debug.traceback(), finallyHandler)
|
||||
return self:_finally(debug.traceback(nil, 2), finallyHandler)
|
||||
end
|
||||
|
||||
--[[
|
||||
|
@ -823,7 +850,7 @@ end
|
|||
function Promise.prototype:finallyCall(callback, ...)
|
||||
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:finallyCall"))
|
||||
local length, values = pack(...)
|
||||
return self:_finally(debug.traceback(), function()
|
||||
return self:_finally(debug.traceback(nil, 2), function()
|
||||
return callback(unpack(values, 1, length))
|
||||
end)
|
||||
end
|
||||
|
@ -833,7 +860,7 @@ end
|
|||
]]
|
||||
function Promise.prototype:finallyReturn(...)
|
||||
local length, values = pack(...)
|
||||
return self:_finally(debug.traceback(), function()
|
||||
return self:_finally(debug.traceback(nil, 2), function()
|
||||
return unpack(values, 1, length)
|
||||
end)
|
||||
end
|
||||
|
@ -846,7 +873,7 @@ function Promise.prototype:done(finallyHandler)
|
|||
finallyHandler == nil or type(finallyHandler) == "function",
|
||||
ERROR_NON_FUNCTION:format("Promise:finallyO")
|
||||
)
|
||||
return self:_finally(debug.traceback(), finallyHandler, true)
|
||||
return self:_finally(debug.traceback(nil, 2), finallyHandler, true)
|
||||
end
|
||||
|
||||
--[[
|
||||
|
@ -855,7 +882,7 @@ end
|
|||
function Promise.prototype:doneCall(callback, ...)
|
||||
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:doneCall"))
|
||||
local length, values = pack(...)
|
||||
return self:_finally(debug.traceback(), function()
|
||||
return self:_finally(debug.traceback(nil, 2), function()
|
||||
return callback(unpack(values, 1, length))
|
||||
end, true)
|
||||
end
|
||||
|
@ -865,7 +892,7 @@ end
|
|||
]]
|
||||
function Promise.prototype:doneReturn(...)
|
||||
local length, values = pack(...)
|
||||
return self:_finally(debug.traceback(), function()
|
||||
return self:_finally(debug.traceback(nil, 2), function()
|
||||
return unpack(values, 1, length)
|
||||
end, true)
|
||||
end
|
||||
|
@ -925,6 +952,7 @@ function Promise.prototype:expect(...)
|
|||
return expectHelper(self:awaitStatus(...))
|
||||
end
|
||||
|
||||
-- Backwards compatibility
|
||||
Promise.prototype.awaitValue = Promise.prototype.expect
|
||||
|
||||
--[[
|
||||
|
@ -972,10 +1000,21 @@ function Promise.prototype:_resolve(...)
|
|||
self:_resolve(...)
|
||||
end,
|
||||
function(...)
|
||||
-- The handler errored. Replace the inner stack trace with our outer stack trace.
|
||||
local runtimeError = chainedPromise._values[1]
|
||||
|
||||
-- Backwards compatibility <v2
|
||||
if chainedPromise._error then
|
||||
return self:_reject((chainedPromise._error or "") .. "\n" .. self._source)
|
||||
runtimeError = PromiseRuntimeError.new(chainedPromise._error)
|
||||
end
|
||||
|
||||
if PromiseRuntimeError.is(runtimeError) then
|
||||
return self:_reject(runtimeError:extend(
|
||||
("The Promise at:\n\n%s\n...Rejected because it was chained to the following Promise, which encountered an error:\n"):format(
|
||||
self._source
|
||||
)
|
||||
))
|
||||
end
|
||||
|
||||
self:_reject(...)
|
||||
end
|
||||
)
|
||||
|
@ -1025,7 +1064,7 @@ function Promise.prototype:_reject(...)
|
|||
local err = tostring((...))
|
||||
|
||||
coroutine.wrap(function()
|
||||
RunService.Heartbeat:Wait()
|
||||
Promise._timeEvent:Wait()
|
||||
|
||||
-- Someone observed the error, hooray!
|
||||
if not self._unhandledRejection then
|
||||
|
@ -1033,15 +1072,16 @@ function Promise.prototype:_reject(...)
|
|||
end
|
||||
|
||||
-- Build a reasonable message
|
||||
local message
|
||||
if self._error then
|
||||
message = ("Unhandled promise rejection:\n\n%s"):format(err)
|
||||
else
|
||||
message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
|
||||
err,
|
||||
self._source
|
||||
)
|
||||
local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
|
||||
err,
|
||||
self._source
|
||||
)
|
||||
|
||||
if Promise.TEST then
|
||||
-- Don't spam output when we're running tests.
|
||||
return
|
||||
end
|
||||
|
||||
warn(message)
|
||||
end)()
|
||||
end
|
||||
|
@ -1062,11 +1102,7 @@ function Promise.prototype:_finalize()
|
|||
callback(self._status)
|
||||
end
|
||||
|
||||
if self._parent and self._error == nil then
|
||||
self._error = self._parent._error
|
||||
end
|
||||
|
||||
-- Allow family to be buried
|
||||
-- Clear references to other Promises to allow gc
|
||||
if not Promise.TEST then
|
||||
self._parent = nil
|
||||
self._consumers = nil
|
||||
|
|
|
@ -2,6 +2,24 @@ return function()
|
|||
local Promise = require(script.Parent)
|
||||
Promise.TEST = true
|
||||
|
||||
local timeEvent = Instance.new("BindableEvent")
|
||||
Promise._timeEvent = timeEvent.Event
|
||||
|
||||
local advanceTime do
|
||||
local injectedPromiseTime = 0
|
||||
|
||||
Promise._getTime = function()
|
||||
return injectedPromiseTime
|
||||
end
|
||||
|
||||
function advanceTime(delta)
|
||||
delta = delta or (1/60)
|
||||
|
||||
injectedPromiseTime = injectedPromiseTime + delta
|
||||
timeEvent:Fire(delta)
|
||||
end
|
||||
end
|
||||
|
||||
local function pack(...)
|
||||
local len = select("#", ...)
|
||||
|
||||
|
@ -79,11 +97,95 @@ return function()
|
|||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._values[1]:find("hahah")).to.be.ok()
|
||||
expect(tostring(promise._values[1]):find("hahah")).to.be.ok()
|
||||
|
||||
-- Loosely check for the pieces of the stack trace we expect
|
||||
expect(promise._values[1]:find("init.spec")).to.be.ok()
|
||||
expect(promise._values[1]:find("new")).to.be.ok()
|
||||
expect(tostring(promise._values[1]):find("init.spec")).to.be.ok()
|
||||
expect(tostring(promise._values[1]):find("runExecutor")).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should work with C functions", function()
|
||||
expect(function()
|
||||
Promise.new(tick):andThen(tick)
|
||||
end).to.never.throw()
|
||||
end)
|
||||
|
||||
it("should have a nice tostring", function()
|
||||
expect(tostring(Promise.resolve()):gmatch("Promise(Resolved)")).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should allow yielding", function()
|
||||
local bindable = Instance.new("BindableEvent")
|
||||
local promise = Promise.new(function(resolve)
|
||||
bindable.Event:Wait()
|
||||
resolve(5)
|
||||
end)
|
||||
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||
bindable:Fire()
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
expect(promise._values[1]).to.equal(5)
|
||||
end)
|
||||
|
||||
it("should preserve stack traces of resolve-chained promises", function()
|
||||
local function nestedCall(text)
|
||||
error(text)
|
||||
end
|
||||
|
||||
local promise = Promise.new(function(resolve)
|
||||
resolve(Promise.new(function()
|
||||
nestedCall("sample text")
|
||||
end))
|
||||
end)
|
||||
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||
|
||||
local trace = tostring(promise._values[1])
|
||||
expect(trace:find("sample text")).to.be.ok()
|
||||
expect(trace:find("nestedCall")).to.be.ok()
|
||||
expect(trace:find("runExecutor")).to.be.ok()
|
||||
expect(trace:find("runPlanNode")).to.be.ok()
|
||||
expect(trace:find("...Rejected because it was chained to the following Promise, which encountered an error:")).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.defer", function()
|
||||
it("should execute after the time event", function()
|
||||
local callCount = 0
|
||||
local promise = Promise.defer(function(resolve, reject, onCancel, nothing)
|
||||
expect(type(resolve)).to.equal("function")
|
||||
expect(type(reject)).to.equal("function")
|
||||
expect(type(onCancel)).to.equal("function")
|
||||
expect(type(nothing)).to.equal("nil")
|
||||
|
||||
callCount = callCount + 1
|
||||
|
||||
resolve("foo")
|
||||
end)
|
||||
|
||||
expect(callCount).to.equal(0)
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||
|
||||
advanceTime()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
|
||||
advanceTime()
|
||||
expect(callCount).to.equal(1)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.delay", function()
|
||||
it("Should schedule promise resolution", function()
|
||||
local promise = Promise.delay(1)
|
||||
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||
|
||||
advanceTime()
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||
|
||||
advanceTime(1)
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
@ -132,6 +234,19 @@ return function()
|
|||
end)
|
||||
|
||||
describe("Promise:andThen", function()
|
||||
it("should allow yielding", function()
|
||||
local bindable = Instance.new("BindableEvent")
|
||||
local promise = Promise.resolve():andThen(function()
|
||||
bindable.Event:Wait()
|
||||
return 5
|
||||
end)
|
||||
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||
bindable:Fire()
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
expect(promise._values[1]).to.equal(5)
|
||||
end)
|
||||
|
||||
it("should chain onto resolved promises", function()
|
||||
local args
|
||||
local argsLength
|
||||
|
@ -200,6 +315,24 @@ return function()
|
|||
expect(#chained._values).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should reject on error in callback", function()
|
||||
local callCount = 0
|
||||
|
||||
local promise = Promise.resolve(1):andThen(function()
|
||||
callCount = callCount + 1
|
||||
error("hahah")
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||
expect(tostring(promise._values[1]):find("hahah")).to.be.ok()
|
||||
|
||||
-- Loosely check for the pieces of the stack trace we expect
|
||||
expect(tostring(promise._values[1]):find("init.spec")).to.be.ok()
|
||||
expect(tostring(promise._values[1]):find("runExecutor")).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should chain onto asynchronously resolved promises", function()
|
||||
local args
|
||||
local argsLength
|
||||
|
@ -717,7 +850,7 @@ return function()
|
|||
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||
bindable:Fire()
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._values[1]:find("errortext")).to.be.ok()
|
||||
expect(tostring(promise._values[1]):find("errortext")).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
|
@ -770,11 +903,24 @@ return function()
|
|||
Promise.try(function()
|
||||
error('errortext')
|
||||
end):catch(function(e)
|
||||
errorText = e
|
||||
errorText = tostring(e)
|
||||
end)
|
||||
|
||||
expect(errorText:find("errortext")).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should catch asynchronous errors", function()
|
||||
local bindable = Instance.new("BindableEvent")
|
||||
local promise = Promise.try(function()
|
||||
bindable.Event:Wait()
|
||||
error('errortext')
|
||||
end)
|
||||
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||
bindable:Fire()
|
||||
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||
expect(tostring(promise._values[1]):find("errortext")).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise:andThenReturn", function()
|
||||
|
|
65
package-lock.json
generated
65
package-lock.json
generated
|
@ -7552,9 +7552,9 @@
|
|||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="
|
||||
},
|
||||
"slugify": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.5.tgz",
|
||||
"integrity": "sha512-5VCnH7aS13b0UqWOs7Ef3E5rkhFe8Od+cp7wybFv5mv/sYSRkucZlJX0bamAJky7b2TTtGvrJBWVdpdEicsSrA=="
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.4.0.tgz",
|
||||
"integrity": "sha512-FtLNsMGBSRB/0JOE2A0fxlqjI6fJsgHGS13iTuVT28kViI4JjUiNqp/vyis0ZXYcMnpR3fzGNkv+6vRlI2GwdQ=="
|
||||
},
|
||||
"snapdragon": {
|
||||
"version": "0.8.2",
|
||||
|
@ -8762,13 +8762,64 @@
|
|||
}
|
||||
},
|
||||
"vuepress-plugin-api-docs-generator": {
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vuepress-plugin-api-docs-generator/-/vuepress-plugin-api-docs-generator-1.0.16.tgz",
|
||||
"integrity": "sha512-6D1ZloMemPI3Iyx819ep/mE5+6y5zQ4zw2bM7y0PSUFs68HHbJbxAN9wOoxl9isTvmtTPM19L82E4MkSEzys/w==",
|
||||
"version": "1.0.18",
|
||||
"resolved": "https://registry.npmjs.org/vuepress-plugin-api-docs-generator/-/vuepress-plugin-api-docs-generator-1.0.18.tgz",
|
||||
"integrity": "sha512-fQauXyRcj5gAdQbAE3IIGHSZ9tsrLmykbjFgCq5Xd4LTZAsDOqWjWxfbFG5BChmePkCq+GjFlckEjGxAixAmBg==",
|
||||
"requires": {
|
||||
"@vuepress/plugin-register-components": "^1.0.0-rc.1",
|
||||
"@vuepress/plugin-register-components": "^1.4.1",
|
||||
"node-balanced": "0.0.14",
|
||||
"slugify": "^1.3.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vuepress/plugin-register-components": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@vuepress/plugin-register-components/-/plugin-register-components-1.4.1.tgz",
|
||||
"integrity": "sha512-6yI4J/tMhOASSLmlP+5p4ccljlWuNBRsyYSKiD5jWAV181oMmN32LtuoCggXBhSvQUgn2grxyjmYw+tcSV5KGQ==",
|
||||
"requires": {
|
||||
"@vuepress/shared-utils": "1.4.1"
|
||||
}
|
||||
},
|
||||
"@vuepress/shared-utils": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@vuepress/shared-utils/-/shared-utils-1.4.1.tgz",
|
||||
"integrity": "sha512-FBUHFhvR7vk6glQy/qUntBz8bVeWiNYZ2/G16EKaerKKn15xAiD7tUFCQ3L/KjtQJ8TV38GK47UEXh7UTcRwQg==",
|
||||
"requires": {
|
||||
"chalk": "^2.3.2",
|
||||
"diacritics": "^1.3.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"fs-extra": "^7.0.1",
|
||||
"globby": "^9.2.0",
|
||||
"gray-matter": "^4.0.1",
|
||||
"hash-sum": "^1.0.2",
|
||||
"semver": "^6.0.0",
|
||||
"upath": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz",
|
||||
"integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==",
|
||||
"requires": {
|
||||
"@types/glob": "^7.1.1",
|
||||
"array-union": "^1.0.2",
|
||||
"dir-glob": "^2.2.2",
|
||||
"fast-glob": "^2.2.6",
|
||||
"glob": "^7.1.3",
|
||||
"ignore": "^4.0.3",
|
||||
"pify": "^4.0.1",
|
||||
"slash": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"vuepress-plugin-container": {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"dependencies": {
|
||||
"gh-pages": "^2.1.1",
|
||||
"vuepress": "^1.0.3",
|
||||
"vuepress-plugin-api-docs-generator": "^1.0.16"
|
||||
"vuepress-plugin-api-docs-generator": "^1.0.18"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
|
|
Loading…
Reference in a new issue