diff --git a/README.md b/README.md index d692438..ac172b0 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,11 @@ This Promise implementation attempts to satisfy those traits. * Creates an immediately rejected Promise with the given value. * `Promise.is(object) -> bool` * Returns whether the given object is a Promise. +* `Promise.all(array) -> array` + * 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. + * Note: Only the first return value from each promise will be present in the resulting array. ### Instance Methods * `Promise:andThen(successHandler, [failureHandler]) -> Promise` @@ -69,8 +74,6 @@ httpGet("https://google.com") ``` ## Future Additions -* `Promise.all` - * Currently stubbed out, throws an error. * `Promise.wrapAsync` * Intended to wrap an existing Roblox API that yields, exposing a new one that returns a Promise. diff --git a/lib/init.lua b/lib/init.lua index 902b90b..1054415 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -165,8 +165,60 @@ end * is resolved when all input promises resolve * is rejected if ANY input promises reject ]] -function Promise.all(...) - error("unimplemented", 2) +function Promise.all(promises) + if type(promises) ~= "table" then + error("Please pass an array of promises or values to Promise.all", 2) + end + + -- If there are no values then return an already resolved promise. + if #promises == 0 then + return Promise.resolve({}) + 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 = 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 = {} + -- 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 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 + + -- We can assume the values inside `promises` are all promises since we checked above. + for i = 1, #promises do + promises[i]:andThen(function(...) + resolveOne(i, ...) + end) + :catch(function(...) + -- Only reject if this promise is unrejected. + if not rejected then + reject(...) + end + end) + end + end) end --[[ diff --git a/lib/init.spec.lua b/lib/init.spec.lua index 6600a43..c153375 100644 --- a/lib/init.spec.lua +++ b/lib/init.spec.lua @@ -264,4 +264,56 @@ return function() expect(#chained._values).to.equal(0) end) end) + + describe("Promise.all", function() + it("should error if given something other than a table", function() + expect(function() + Promise.all(1) + end).to.throw() + end) + + it("should resolve instantly with an empty table if given no values", function() + local promise = Promise.all({}) + local success, value = promise:await() + + expect(success).to.equal(true) + expect(promise:getStatus()).to.equal(Promise.Status.Resolved) + expect(value).to.be.a("table") + expect(#value).to.equal(0) + end) + + it("should error if given non-promise values", function() + expect(function() + Promise.all({{}, {}, {}}) + end).to.throw() + end) + + it("should wait for all promises to be resolved and return their values", function() + local promises = { + Promise.new(function(resolve) + resolve(1) + end), + Promise.new(function(resolve) + resolve("A string") + end), + Promise.new(function(resolve) + resolve(nil) + end), + Promise.new(function(resolve) + resolve(false) + end) + } + + local promise = Promise.all(promises) + local success, resolved = promise:await() + + expect(success).to.equal(true) + expect(resolved).to.be.a("table") + expect(#resolved).to.equal(4) + expect(resolved[1]).to.equal(1) + expect(resolved[2]).to.equal("A string") + expect(resolved[3]).to.equal(nil) + expect(resolved[4]).to.equal(false) + end) + end) end