mirror of
https://github.com/AmberGraceRblx/luau-promise.git
synced 2025-04-24 15:50:01 +00:00
parent
cef64024e6
commit
75d82b4909
4 changed files with 377 additions and 29 deletions
|
@ -11,6 +11,9 @@
|
|||
- Add `done`, `doneCall`, `doneReturn`
|
||||
- Add `andThenReturn`, `finallyReturn`
|
||||
- Add `Promise.delay`, `promise:timeout`
|
||||
- Add `Promise.some`, `Promise.any`
|
||||
- Add `Promise.allSettled`
|
||||
- `Promise.all` and `Promise.race` are now cancellable.
|
||||
|
||||
# 2.4.0
|
||||
|
||||
|
|
|
@ -174,7 +174,7 @@ docs:
|
|||
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.
|
||||
|
||||
`Promise.try` is similar to [[Promise.promisify]], except the callback is executed instantly, 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, and unlike `promisify`, yielding is not allowed with `try`.
|
||||
|
||||
```lua
|
||||
Promise.try(function()
|
||||
|
@ -212,6 +212,14 @@ docs:
|
|||
static: true
|
||||
params: "promises: array<Promise<T>>"
|
||||
returns: Promise<array<T>>
|
||||
|
||||
- name: allSettled
|
||||
desc: |
|
||||
Accepts an array of Promises and returns a new Promise that resolves with an array of in-place PromiseStatuses when all input Promises have settled. This is equivalent to mapping `promise:finally` over the array of Promises.
|
||||
static: true
|
||||
params: "promises: array<Promise<T>>"
|
||||
returns: Promise<array<PromiseStatus>>
|
||||
|
||||
- name: race
|
||||
desc: |
|
||||
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.
|
||||
|
@ -220,6 +228,26 @@ docs:
|
|||
static: true
|
||||
params: "promises: array<Promise<T>>"
|
||||
returns: Promise<T>
|
||||
|
||||
- name: some
|
||||
desc: |
|
||||
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.
|
||||
static: true
|
||||
params: "promises: array<Promise<T>>, count: number"
|
||||
returns: Promise<array<T>>
|
||||
|
||||
- name: any
|
||||
desc: |
|
||||
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.
|
||||
|
||||
static: true
|
||||
params: "promises: array<Promise<T>>"
|
||||
returns: Promise<T>
|
||||
|
||||
- name: delay
|
||||
desc: |
|
||||
Returns a Promise that resolves after `seconds` seconds have passed. The Promise resolves with the actual amount of time that was waited.
|
||||
|
|
164
lib/init.lua
164
lib/init.lua
|
@ -259,16 +259,121 @@ end
|
|||
* is resolved when all input promises resolve
|
||||
* is rejected if ANY input promises reject
|
||||
]]
|
||||
function Promise.all(promises)
|
||||
function Promise._all(traceback, promises, amount)
|
||||
if type(promises) ~= "table" then
|
||||
error(ERROR_NON_LIST:format("Promise.all"), 2)
|
||||
error(ERROR_NON_LIST:format("Promise.all"), 3)
|
||||
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.
|
||||
for i, promise in pairs(promises) do
|
||||
if not Promise.is(promise) then
|
||||
error((ERROR_NON_PROMISE_IN_LIST):format("Promise.all", tostring(i)), 2)
|
||||
error((ERROR_NON_PROMISE_IN_LIST):format("Promise.all", tostring(i)), 3)
|
||||
end
|
||||
end
|
||||
|
||||
-- If there are no values then return an already resolved promise.
|
||||
if #promises == 0 or amount == 0 then
|
||||
return Promise.resolve({})
|
||||
end
|
||||
|
||||
return Promise._newWithSelf(function(self, resolve, reject, onCancel)
|
||||
self._source = traceback
|
||||
|
||||
-- An array to contain our resolved values from the given promises.
|
||||
local resolvedValues = {}
|
||||
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 resolvedCount = 0
|
||||
local rejectedCount = 0
|
||||
local done = false
|
||||
|
||||
local function cancel()
|
||||
for _, promise in ipairs(newPromises) do
|
||||
promise:cancel()
|
||||
end
|
||||
end
|
||||
|
||||
-- Called when a single value is resolved and resolves if all are done.
|
||||
local function resolveOne(i, ...)
|
||||
if done then
|
||||
return
|
||||
end
|
||||
|
||||
resolvedCount = resolvedCount + 1
|
||||
|
||||
if amount == nil then
|
||||
resolvedValues[i] = ...
|
||||
else
|
||||
resolvedValues[resolvedCount] = ...
|
||||
end
|
||||
|
||||
if resolvedCount >= (amount or #promises) then
|
||||
done = true
|
||||
resolve(resolvedValues)
|
||||
cancel()
|
||||
end
|
||||
end
|
||||
|
||||
onCancel(cancel)
|
||||
|
||||
-- We can assume the values inside `promises` are all promises since we
|
||||
-- checked above.
|
||||
for i = 1, #promises do
|
||||
table.insert(
|
||||
newPromises,
|
||||
promises[i]:andThen(
|
||||
function(...)
|
||||
resolveOne(i, ...)
|
||||
end,
|
||||
function(...)
|
||||
rejectedCount = rejectedCount + 1
|
||||
|
||||
if amount == nil or #promises - rejectedCount < amount then
|
||||
cancel()
|
||||
done = true
|
||||
|
||||
reject(...)
|
||||
end
|
||||
end
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
if done then
|
||||
cancel()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Promise.all(promises)
|
||||
return Promise._all(debug.traceback(), 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)
|
||||
end
|
||||
|
||||
function Promise.any(promises)
|
||||
return Promise._all(debug.traceback(), promises, 1):andThen(function(values)
|
||||
return values[1]
|
||||
end)
|
||||
end
|
||||
|
||||
function Promise.allSettled(promises)
|
||||
if type(promises) ~= "table" then
|
||||
error(ERROR_NON_LIST:format("Promise.allSettled"), 2)
|
||||
end
|
||||
|
||||
-- We need to check that each value is a promise here so that we can produce
|
||||
-- a proper error rather than a rejected promise with our error.
|
||||
for i, promise in pairs(promises) do
|
||||
if not Promise.is(promise) then
|
||||
error((ERROR_NON_PROMISE_IN_LIST):format("Promise.allSettled", tostring(i)), 2)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -277,46 +382,40 @@ function Promise.all(promises)
|
|||
return Promise.resolve({})
|
||||
end
|
||||
|
||||
return Promise.new(function(resolve, reject)
|
||||
return Promise.new(function(resolve, _, onCancel)
|
||||
-- An array to contain our resolved values from the given promises.
|
||||
local resolvedValues = {}
|
||||
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 resolvedCount = 0
|
||||
local finalized = false
|
||||
local finishedCount = 0
|
||||
|
||||
-- Called when a single value is resolved and resolves if all are done.
|
||||
local function resolveOne(i, ...)
|
||||
resolvedValues[i] = ...
|
||||
resolvedCount = resolvedCount + 1
|
||||
finishedCount = finishedCount + 1
|
||||
|
||||
if resolvedCount == #promises then
|
||||
resolve(resolvedValues)
|
||||
fates[i] = ...
|
||||
|
||||
if finishedCount >= #promises then
|
||||
resolve(fates)
|
||||
end
|
||||
end
|
||||
|
||||
onCancel(function()
|
||||
for _, promise in ipairs(newPromises) do
|
||||
promise:cancel()
|
||||
end
|
||||
end)
|
||||
|
||||
-- We can assume the values inside `promises` are all promises since we
|
||||
-- checked above.
|
||||
for i = 1, #promises do
|
||||
if finalized then
|
||||
break
|
||||
end
|
||||
|
||||
table.insert(
|
||||
newPromises,
|
||||
promises[i]:andThen(
|
||||
promises[i]:finally(
|
||||
function(...)
|
||||
resolveOne(i, ...)
|
||||
end,
|
||||
function(...)
|
||||
for _, promise in ipairs(newPromises) do
|
||||
promise:cancel()
|
||||
end
|
||||
finalized = true
|
||||
|
||||
reject(...)
|
||||
end
|
||||
)
|
||||
)
|
||||
|
@ -337,13 +436,18 @@ function Promise.race(promises)
|
|||
|
||||
return Promise.new(function(resolve, reject, onCancel)
|
||||
local newPromises = {}
|
||||
local finished = false
|
||||
|
||||
local function cancel()
|
||||
for _, promise in ipairs(newPromises) do
|
||||
promise:cancel()
|
||||
end
|
||||
end
|
||||
|
||||
local function finalize(callback)
|
||||
return function (...)
|
||||
for _, promise in ipairs(newPromises) do
|
||||
promise:cancel()
|
||||
end
|
||||
|
||||
cancel()
|
||||
finished = true
|
||||
return callback(...)
|
||||
end
|
||||
end
|
||||
|
@ -358,6 +462,10 @@ function Promise.race(promises)
|
|||
promise:andThen(finalize(resolve), finalize(reject))
|
||||
)
|
||||
end
|
||||
|
||||
if finished then
|
||||
cancel()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
@ -582,6 +582,33 @@ return function()
|
|||
expect(ok).to.be.ok()
|
||||
expect(err:find("Non%-promise")).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should cancel pending promises if one rejects", function()
|
||||
local p = Promise.new(function() end)
|
||||
expect(Promise.all({
|
||||
Promise.resolve(),
|
||||
Promise.reject(),
|
||||
p
|
||||
}):getStatus()).to.equal(Promise.Status.Rejected)
|
||||
expect(p:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
end)
|
||||
|
||||
it("should cancel promises if it is cancelled", function()
|
||||
local p = Promise.new(function() end)
|
||||
p:andThen(function() end)
|
||||
|
||||
local promises = {
|
||||
Promise.new(function() end),
|
||||
Promise.new(function() end),
|
||||
p
|
||||
}
|
||||
|
||||
Promise.all(promises):cancel()
|
||||
|
||||
expect(promises[1]:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
expect(promises[2]:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
expect(promises[3]:getStatus()).to.equal(Promise.Status.Started)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.race", function()
|
||||
|
@ -614,6 +641,14 @@ return function()
|
|||
expect(promises[1]:getStatus()).to.equal(Promise.Status.Started)
|
||||
expect(promises[2]:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
expect(promises[3]:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
|
||||
local p = Promise.new(function() end)
|
||||
expect(Promise.race({
|
||||
Promise.reject(),
|
||||
Promise.resolve(),
|
||||
p
|
||||
}):getStatus()).to.equal(Promise.Status.Rejected)
|
||||
expect(p:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
end)
|
||||
|
||||
it("should error if a non-array table is passed in", function()
|
||||
|
@ -624,6 +659,23 @@ return function()
|
|||
expect(ok).to.be.ok()
|
||||
expect(err:find("Non%-promise")).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should cancel promises if it is cancelled", function()
|
||||
local p = Promise.new(function() end)
|
||||
p:andThen(function() end)
|
||||
|
||||
local promises = {
|
||||
Promise.new(function() end),
|
||||
Promise.new(function() end),
|
||||
p
|
||||
}
|
||||
|
||||
Promise.race(promises):cancel()
|
||||
|
||||
expect(promises[1]:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
expect(promises[2]:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
expect(promises[3]:getStatus()).to.equal(Promise.Status.Started)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.promisify", function()
|
||||
|
@ -790,4 +842,161 @@ return function()
|
|||
expect(always).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.some", function()
|
||||
it("should resolve once the goal is reached", function()
|
||||
local p = Promise.some({
|
||||
Promise.resolve(1),
|
||||
Promise.reject(),
|
||||
Promise.resolve(2)
|
||||
}, 2)
|
||||
expect(p:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
expect(p._values[1][1]).to.equal(1)
|
||||
expect(p._values[1][2]).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should error if the goal can't be reached", function()
|
||||
expect(Promise.some({
|
||||
Promise.resolve(),
|
||||
Promise.reject()
|
||||
}, 2):getStatus()).to.equal(Promise.Status.Rejected)
|
||||
|
||||
local reject
|
||||
local p = Promise.some({
|
||||
Promise.resolve(),
|
||||
Promise.new(function(_, r) reject = r end)
|
||||
}, 2)
|
||||
|
||||
expect(p:getStatus()).to.equal(Promise.Status.Started)
|
||||
reject("foo")
|
||||
expect(p:getStatus()).to.equal(Promise.Status.Rejected)
|
||||
expect(p._values[1]).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should cancel pending Promises once the goal is reached", function()
|
||||
local resolve
|
||||
local pending1 = Promise.new(function() end)
|
||||
local pending2 = Promise.new(function(r) resolve = r end)
|
||||
|
||||
local some = Promise.some({
|
||||
pending1,
|
||||
pending2,
|
||||
Promise.resolve()
|
||||
}, 2)
|
||||
|
||||
expect(some:getStatus()).to.equal(Promise.Status.Started)
|
||||
expect(pending1:getStatus()).to.equal(Promise.Status.Started)
|
||||
expect(pending2:getStatus()).to.equal(Promise.Status.Started)
|
||||
|
||||
resolve()
|
||||
|
||||
expect(some:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
expect(pending1:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
expect(pending2:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
end)
|
||||
|
||||
it("should error if passed a non-number", function()
|
||||
expect(function()
|
||||
Promise.some({}, "non-number")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return an empty array if amount is 0", function()
|
||||
local p = Promise.some({
|
||||
Promise.resolve(2)
|
||||
}, 0)
|
||||
|
||||
expect(p:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
expect(#p._values[1]).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should not return extra values", function()
|
||||
local p = Promise.some({
|
||||
Promise.resolve(1),
|
||||
Promise.resolve(2),
|
||||
Promise.resolve(3),
|
||||
Promise.resolve(4),
|
||||
}, 2)
|
||||
|
||||
expect(p:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
expect(#p._values[1]).to.equal(2)
|
||||
expect(p._values[1][1]).to.equal(1)
|
||||
expect(p._values[1][2]).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should cancel promises if it is cancelled", function()
|
||||
local p = Promise.new(function() end)
|
||||
p:andThen(function() end)
|
||||
|
||||
local promises = {
|
||||
Promise.new(function() end),
|
||||
Promise.new(function() end),
|
||||
p
|
||||
}
|
||||
|
||||
Promise.some(promises, 3):cancel()
|
||||
|
||||
expect(promises[1]:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
expect(promises[2]:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
expect(promises[3]:getStatus()).to.equal(Promise.Status.Started)
|
||||
end)
|
||||
|
||||
describe("Promise.any", function()
|
||||
it("should return the value directly", function()
|
||||
local p = Promise.any({
|
||||
Promise.reject(),
|
||||
Promise.reject(),
|
||||
Promise.resolve(1)
|
||||
})
|
||||
|
||||
expect(p:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
expect(p._values[1]).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should error if all are rejected", function()
|
||||
expect(Promise.any({
|
||||
Promise.reject(),
|
||||
Promise.reject(),
|
||||
Promise.reject(),
|
||||
}):getStatus()).to.equal(Promise.Status.Rejected)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.allSettled", function()
|
||||
it("should resolve with an array of PromiseStatuses", function()
|
||||
local reject
|
||||
local p = Promise.allSettled({
|
||||
Promise.resolve(),
|
||||
Promise.reject(),
|
||||
Promise.resolve(),
|
||||
Promise.new(function(_, r) reject = r end)
|
||||
})
|
||||
|
||||
expect(p:getStatus()).to.equal(Promise.Status.Started)
|
||||
reject()
|
||||
expect(p:getStatus()).to.equal(Promise.Status.Resolved)
|
||||
expect(p._values[1][1]).to.equal(Promise.Status.Resolved)
|
||||
expect(p._values[1][2]).to.equal(Promise.Status.Rejected)
|
||||
expect(p._values[1][3]).to.equal(Promise.Status.Resolved)
|
||||
expect(p._values[1][4]).to.equal(Promise.Status.Rejected)
|
||||
end)
|
||||
|
||||
it("should cancel promises if it is cancelled", function()
|
||||
local p = Promise.new(function() end)
|
||||
p:andThen(function() end)
|
||||
|
||||
local promises = {
|
||||
Promise.new(function() end),
|
||||
Promise.new(function() end),
|
||||
p
|
||||
}
|
||||
|
||||
Promise.allSettled(promises):cancel()
|
||||
|
||||
expect(promises[1]:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
expect(promises[2]:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||
expect(promises[3]:getStatus()).to.equal(Promise.Status.Started)
|
||||
end)
|
||||
end)
|
||||
end
|
Loading…
Reference in a new issue