diff --git a/CHANGELOG.md b/CHANGELOG.md index e05b371..4e0ab4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Improve stack traces - Promise.promisify will now turn errors into rejections even if they occur after a yield. - Add Promise.try +- Add `done`, `doneCall`, `doneReturn` +- Add `andThenReturn`, `finallyReturn` # 2.4.0 diff --git a/lib/README.md b/lib/README.md index e0b3031..1087283 100644 --- a/lib/README.md +++ b/lib/README.md @@ -322,6 +322,31 @@ docs: returns: Promise returns: Promise + - name: done + desc: | + 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. + + ::: warning + If this 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. + params: + - name: doneHandler + type: + kind: function + params: "status: PromiseStatus" + returns: ...any? + returns: Promise<...any?> + overloads: + - params: + - name: doneHandler + type: + kind: function + params: "status: PromiseStatus" + returns: Promise + returns: Promise + - name: andThenCall desc: | Attaches an `andThen` handler to this Promise that calls the given callback with the predefined arguments. The resolved value is discarded. @@ -364,6 +389,87 @@ docs: desc: Arguments which will be passed to the callback. returns: Promise + - name: doneCall + desc: | + Same as `andThenCall`, except for `done`. + + Attaches a `done` handler to this Promise that calls the given callback with the predefined arguments. + params: + - name: callback + type: + kind: function + params: "...: ...any?" + returns: "any" + - name: "..." + type: "...any?" + desc: Arguments which will be passed to the callback. + returns: Promise + + - name: andThenReturn + desc: | + Attaches an `andThen` handler to this Promise that discards the resolved value and returns the given value from it. + + ```lua + promise:andThenReturn("some", "values") + ``` + + This is sugar for + + ```lua + promise:andThen(function() + return "some", "values" + end) + ``` + params: + - name: "..." + type: "...any?" + desc: Values to return from the function. + returns: Promise<...any?> + + - name: finallyReturn + desc: | + Attaches a `finally` handler to this Promise that discards the resolved value and returns the given value from it. + + ```lua + promise:finallyReturn("some", "values") + ``` + + This is sugar for + + ```lua + promise:finally(function() + return "some", "values" + end) + ``` + params: + - name: "..." + type: "...any?" + desc: Values to return from the function. + returns: Promise<...any?> + + - name: doneReturn + desc: | + 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) + ``` + + params: + - name: "..." + type: "...any?" + desc: Values to return from the function. + returns: Promise<...any?> + + - name: cancel desc: | Cancels this promise, preventing the promise from resolving or rejecting. Does not do anything if the promise is already settled. diff --git a/lib/init.lua b/lib/init.lua index a658704..0be1852 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -502,6 +502,16 @@ function Promise.prototype:andThenCall(callback, ...) end) end +--[[ + Shorthand for an andThen handler that returns the given value. +]] +function Promise.prototype:andThenReturn(...) + local length, values = pack(...) + return self:_andThen(debug.traceback(), function() + return unpack(values, 1, length) + end) +end + --[[ Cancels the promise, disallowing it from rejecting or resolving, and calls the cancellation hook if provided. @@ -548,8 +558,10 @@ end Used to set a handler for when the promise resolves, rejects, or is cancelled. Returns a new promise chained from this promise. ]] -function Promise.prototype:_finally(traceback, finallyHandler) - self._unhandledRejection = false +function Promise.prototype:_finally(traceback, finallyHandler, onlyOk) + if not onlyOk then + self._unhandledRejection = false + end -- Return a promise chained off of this promise return Promise._new(traceback, function(resolve, reject) @@ -563,6 +575,17 @@ function Promise.prototype:_finally(traceback, finallyHandler) ) end + if onlyOk then + local callback = finallyCallback + finallyCallback = function(...) + if self._status == Promise.Status.Rejected then + return resolve(self) + end + + return callback(...) + end + end + if self._status == Promise.Status.Started then -- The promise is not settled, so queue this. table.insert(self._queuedFinally, finallyCallback) @@ -592,6 +615,48 @@ function Promise.prototype:finallyCall(callback, ...) end) end +--[[ + Shorthand for a finally handler that returns the given value. +]] +function Promise.prototype:finallyReturn(...) + local length, values = pack(...) + return self:_finally(debug.traceback(), function() + return unpack(values, 1, length) + end) +end + +--[[ + Similar to finally, except rejections are propagated through it. +]] +function Promise.prototype:done(finallyHandler) + assert( + finallyHandler == nil or type(finallyHandler) == "function", + ERROR_NON_FUNCTION:format("Promise:finallyO") + ) + return self:_finally(debug.traceback(), finallyHandler, true) +end + +--[[ + Calls a callback on `done` with specific arguments. +]] +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 callback(unpack(values, 1, length)) + end, true) +end + +--[[ + Shorthand for a done handler that returns the given value. +]] +function Promise.prototype:doneReturn(...) + local length, values = pack(...) + return self:_finally(debug.traceback(), function() + return unpack(values, 1, length) + end, true) +end + --[[ Yield until the promise is completed. diff --git a/lib/init.spec.lua b/lib/init.spec.lua index fdd613f..296c203 100644 --- a/lib/init.spec.lua +++ b/lib/init.spec.lua @@ -416,6 +416,18 @@ return function() expect(p2._parent).to.equal(p1) expect(p1._consumers[p2]).to.equal(true) end) + + it("should forward return values", function() + local value + + Promise.resolve():finally(function() + return 1 + end):andThen(function(v) + value = v + end) + + expect(value).to.equal(1) + end) end) describe("Promise.all", function() @@ -698,4 +710,84 @@ return function() expect(errorText:find("errortext")).to.be.ok() end) end) + + describe("Promise:andThenReturn", function() + it("should return the given values", function() + local value1, value2 + + Promise.resolve():andThenReturn(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: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() + it("should call the given function with arguments", function() + local value1, value2 + Promise.resolve():andThenCall(function(a, b) + value1 = a + value2 = b + end, 3, 4) + + expect(value1).to.equal(3) + expect(value2).to.equal(4) + 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) end \ No newline at end of file