mirror of
https://github.com/AmberGraceRblx/luau-promise.git
synced 2025-04-24 15:50:01 +00:00
parent
2cb2eec152
commit
afc245c4f1
2 changed files with 162 additions and 158 deletions
124
lib/init.lua
124
lib/init.lua
|
@ -1205,6 +1205,14 @@ end
|
||||||
function Promise.prototype:_andThen(traceback, successHandler, failureHandler)
|
function Promise.prototype:_andThen(traceback, successHandler, failureHandler)
|
||||||
self._unhandledRejection = false
|
self._unhandledRejection = false
|
||||||
|
|
||||||
|
-- If we are already cancelled, we return a cancelled Promise
|
||||||
|
if self._status == Promise.Status.Cancelled then
|
||||||
|
local promise = Promise.new(function() end)
|
||||||
|
promise:cancel()
|
||||||
|
|
||||||
|
return promise
|
||||||
|
end
|
||||||
|
|
||||||
-- Create a new promise to follow this part of the chain
|
-- Create a new promise to follow this part of the chain
|
||||||
return Promise._new(traceback, function(resolve, reject, onCancel)
|
return Promise._new(traceback, function(resolve, reject, onCancel)
|
||||||
-- Our default callbacks just pass values onto the next promise.
|
-- Our default callbacks just pass values onto the next promise.
|
||||||
|
@ -1239,14 +1247,6 @@ function Promise.prototype:_andThen(traceback, successHandler, failureHandler)
|
||||||
elseif self._status == Promise.Status.Rejected then
|
elseif self._status == Promise.Status.Rejected then
|
||||||
-- This promise died a terrible death! Trigger failure immediately.
|
-- This promise died a terrible death! Trigger failure immediately.
|
||||||
failureCallback(unpack(self._values, 1, self._valuesLength))
|
failureCallback(unpack(self._values, 1, self._valuesLength))
|
||||||
elseif self._status == Promise.Status.Cancelled then
|
|
||||||
-- We don't want to call the success handler or the failure handler,
|
|
||||||
-- we just reject this promise outright.
|
|
||||||
reject(Error.new({
|
|
||||||
error = "Promise is cancelled",
|
|
||||||
kind = Error.Kind.AlreadyCancelled,
|
|
||||||
context = "Promise created at\n\n" .. traceback,
|
|
||||||
}))
|
|
||||||
end
|
end
|
||||||
end, self)
|
end, self)
|
||||||
end
|
end
|
||||||
|
@ -1430,28 +1430,37 @@ end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Used to set a handler for when the promise resolves, rejects, or is
|
Used to set a handler for when the promise resolves, rejects, or is
|
||||||
cancelled. Returns a new promise chained from this promise.
|
cancelled.
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:_finally(traceback, finallyHandler, onlyOk)
|
function Promise.prototype:_finally(traceback, finallyHandler)
|
||||||
if not onlyOk then
|
self._unhandledRejection = false
|
||||||
self._unhandledRejection = false
|
|
||||||
end
|
local promise = Promise._new(traceback, function(resolve, reject, onCancel)
|
||||||
|
onCancel(function()
|
||||||
|
-- The finally Promise is not a proper consumer of self. We don't care about the resolved value.
|
||||||
|
-- All we care about is running at the end. Therefore, if self has no other consumers, it's safe to
|
||||||
|
-- cancel. We don't need to hold out cancelling just because there's a finally handler.
|
||||||
|
self:_consumerCancelled(self)
|
||||||
|
end)
|
||||||
|
|
||||||
-- Return a promise chained off of this promise
|
|
||||||
return Promise._new(traceback, function(resolve, reject)
|
|
||||||
local finallyCallback = resolve
|
local finallyCallback = resolve
|
||||||
if finallyHandler then
|
if finallyHandler then
|
||||||
finallyCallback = createAdvancer(traceback, finallyHandler, resolve, reject)
|
|
||||||
end
|
|
||||||
|
|
||||||
if onlyOk then
|
|
||||||
local callback = finallyCallback
|
|
||||||
finallyCallback = function(...)
|
finallyCallback = function(...)
|
||||||
if self._status == Promise.Status.Rejected then
|
local callbackReturn = finallyHandler(...)
|
||||||
return resolve(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
return callback(...)
|
if Promise.is(callbackReturn) then
|
||||||
|
callbackReturn
|
||||||
|
:finally(function(status)
|
||||||
|
if status ~= Promise.Status.Rejected then
|
||||||
|
resolve(self)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
:catch(function(...)
|
||||||
|
reject(...)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
resolve(self)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1462,7 +1471,9 @@ function Promise.prototype:_finally(traceback, finallyHandler, onlyOk)
|
||||||
-- The promise already settled or was cancelled, run the callback now.
|
-- The promise already settled or was cancelled, run the callback now.
|
||||||
finallyCallback(self._status)
|
finallyCallback(self._status)
|
||||||
end
|
end
|
||||||
end, self)
|
end)
|
||||||
|
|
||||||
|
return promise
|
||||||
end
|
end
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
|
@ -1543,69 +1554,6 @@ function Promise.prototype:finallyReturn(...)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[=[
|
|
||||||
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>
|
|
||||||
]=]
|
|
||||||
function Promise.prototype:done(doneHandler)
|
|
||||||
assert(doneHandler == nil or isCallable(doneHandler), string.format(ERROR_NON_FUNCTION, "Promise:done"))
|
|
||||||
return self:_finally(debug.traceback(nil, 2), doneHandler, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
|
||||||
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
|
|
||||||
]=]
|
|
||||||
function Promise.prototype:doneCall(callback, ...)
|
|
||||||
assert(isCallable(callback), string.format(ERROR_NON_FUNCTION, "Promise:doneCall"))
|
|
||||||
local length, values = pack(...)
|
|
||||||
return self:_finally(debug.traceback(nil, 2), function()
|
|
||||||
return callback(unpack(values, 1, length))
|
|
||||||
end, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
|
||||||
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
|
|
||||||
]=]
|
|
||||||
function Promise.prototype:doneReturn(...)
|
|
||||||
local length, values = pack(...)
|
|
||||||
return self:_finally(debug.traceback(nil, 2), function()
|
|
||||||
return unpack(values, 1, length)
|
|
||||||
end, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -556,9 +556,11 @@ return function()
|
||||||
count += 1
|
count += 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
root:andThen(function()
|
root
|
||||||
count += 1
|
:andThen(function()
|
||||||
end):cancel()
|
count += 1
|
||||||
|
end)
|
||||||
|
:cancel()
|
||||||
|
|
||||||
resolve("foo")
|
resolve("foo")
|
||||||
|
|
||||||
|
@ -636,7 +638,7 @@ return function()
|
||||||
it("should track consumers", function()
|
it("should track consumers", function()
|
||||||
local pending = Promise.new(function() end)
|
local pending = Promise.new(function() end)
|
||||||
local p0 = Promise.resolve()
|
local p0 = Promise.resolve()
|
||||||
local p1 = p0:finally(function()
|
local p1 = p0:andThen(function()
|
||||||
return pending
|
return pending
|
||||||
end)
|
end)
|
||||||
local p2 = Promise.new(function(resolve)
|
local p2 = Promise.new(function(resolve)
|
||||||
|
@ -703,18 +705,10 @@ return function()
|
||||||
expect(callCount).to.equal(5)
|
expect(callCount).to.equal(5)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should be a child of the parent Promise", function()
|
it("should not forward return values", function()
|
||||||
local p1 = Promise.new(function() end)
|
|
||||||
local p2 = p1:finally(function() end)
|
|
||||||
|
|
||||||
expect(p2._parent).to.equal(p1)
|
|
||||||
expect(p1._consumers[p2]).to.equal(true)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should forward return values", function()
|
|
||||||
local value
|
local value
|
||||||
|
|
||||||
Promise.resolve()
|
Promise.resolve(2)
|
||||||
:finally(function()
|
:finally(function()
|
||||||
return 1
|
return 1
|
||||||
end)
|
end)
|
||||||
|
@ -722,7 +716,124 @@ return function()
|
||||||
value = v
|
value = v
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(value).to.equal(1)
|
expect(value).to.equal(2)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should not consume rejections", function()
|
||||||
|
local catchRan = false
|
||||||
|
local thenRan = false
|
||||||
|
Promise.reject(5)
|
||||||
|
:finally(function()
|
||||||
|
return 42
|
||||||
|
end)
|
||||||
|
:andThen(function()
|
||||||
|
thenRan = true
|
||||||
|
end)
|
||||||
|
:catch(function(value)
|
||||||
|
catchRan = true
|
||||||
|
expect(value).to.equal(5)
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(catchRan).to.equal(true)
|
||||||
|
expect(thenRan).to.equal(false)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should wait for returned promises", function()
|
||||||
|
local resolve
|
||||||
|
local promise = Promise.reject("foo"):finally(function()
|
||||||
|
return Promise.new(function(r)
|
||||||
|
resolve = r
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
|
local _, value = promise:_unwrap()
|
||||||
|
expect(value).to.equal("foo")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should reject with a returned rejected promise's value", function()
|
||||||
|
local reject
|
||||||
|
local promise = Promise.reject("foo"):finally(function()
|
||||||
|
return Promise.new(function(_, r)
|
||||||
|
reject = r
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
|
||||||
|
reject("bar")
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
|
local _, value = promise:_unwrap()
|
||||||
|
expect(value).to.equal("bar")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should reject when handler errors", function()
|
||||||
|
local errorValue = {}
|
||||||
|
local promise = Promise.reject("bar"):finally(function()
|
||||||
|
error(errorValue)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local ok, value = promise:_unwrap()
|
||||||
|
|
||||||
|
expect(ok).to.equal(false)
|
||||||
|
expect(value).to.equal(errorValue)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should not prevent cancellation", function()
|
||||||
|
local promise = Promise.new(function() end)
|
||||||
|
|
||||||
|
local finallyRan = false
|
||||||
|
promise:finally(function()
|
||||||
|
finallyRan = true
|
||||||
|
end)
|
||||||
|
|
||||||
|
local consumer = promise:andThen(function() end)
|
||||||
|
|
||||||
|
consumer:cancel()
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||||
|
expect(finallyRan).to.equal(true)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should propagate cancellation downwards", function()
|
||||||
|
local finallyRan = false
|
||||||
|
local andThenRan = false
|
||||||
|
local root = Promise.new(function() end)
|
||||||
|
|
||||||
|
local consumer = root:finally(function()
|
||||||
|
finallyRan = true
|
||||||
|
end)
|
||||||
|
|
||||||
|
root:cancel()
|
||||||
|
|
||||||
|
expect(root:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||||
|
expect(consumer:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||||
|
|
||||||
|
expect(finallyRan).to.equal(true)
|
||||||
|
expect(andThenRan).to.equal(false)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should propagate cancellation upwards", function()
|
||||||
|
local finallyRan = false
|
||||||
|
local andThenRan = false
|
||||||
|
local root = Promise.new(function() end)
|
||||||
|
|
||||||
|
local consumer = root:finally(function()
|
||||||
|
finallyRan = true
|
||||||
|
end)
|
||||||
|
|
||||||
|
consumer:cancel()
|
||||||
|
|
||||||
|
expect(root:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||||
|
expect(consumer:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||||
|
|
||||||
|
expect(finallyRan).to.equal(true)
|
||||||
|
expect(andThenRan).to.equal(false)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -1168,20 +1279,6 @@ return function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("Promise:doneReturn", function()
|
|
||||||
it("should return the given values", function()
|
|
||||||
local value1, value2
|
|
||||||
|
|
||||||
Promise.resolve():doneReturn(1, 2):andThen(function(one, two)
|
|
||||||
value1 = one
|
|
||||||
value2 = two
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(value1).to.equal(1)
|
|
||||||
expect(value2).to.equal(2)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("Promise:andThenCall", function()
|
describe("Promise:andThenCall", function()
|
||||||
it("should call the given function with arguments", function()
|
it("should call the given function with arguments", function()
|
||||||
local value1, value2
|
local value1, value2
|
||||||
|
@ -1195,47 +1292,6 @@ return function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("Promise:doneCall", function()
|
|
||||||
it("should call the given function with arguments", function()
|
|
||||||
local value1, value2
|
|
||||||
Promise.resolve():doneCall(function(a, b)
|
|
||||||
value1 = a
|
|
||||||
value2 = b
|
|
||||||
end, 3, 4)
|
|
||||||
|
|
||||||
expect(value1).to.equal(3)
|
|
||||||
expect(value2).to.equal(4)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("Promise:done", function()
|
|
||||||
it("should trigger on resolve or cancel", function()
|
|
||||||
local promise = Promise.new(function() end)
|
|
||||||
local value
|
|
||||||
|
|
||||||
local p = promise:done(function()
|
|
||||||
value = true
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(value).to.never.be.ok()
|
|
||||||
promise:cancel()
|
|
||||||
expect(p:getStatus()).to.equal(Promise.Status.Cancelled)
|
|
||||||
expect(value).to.equal(true)
|
|
||||||
|
|
||||||
local never, always
|
|
||||||
Promise.reject()
|
|
||||||
:done(function()
|
|
||||||
never = true
|
|
||||||
end)
|
|
||||||
:finally(function()
|
|
||||||
always = true
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(never).to.never.be.ok()
|
|
||||||
expect(always).to.be.ok()
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("Promise.some", function()
|
describe("Promise.some", function()
|
||||||
it("should resolve once the goal is reached", function()
|
it("should resolve once the goal is reached", function()
|
||||||
local p = Promise.some({
|
local p = Promise.some({
|
||||||
|
|
Loading…
Reference in a new issue