Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions src/promesa/impl/promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ goog.scope(function() {
this[QUEUE].push({
type: resolveType,
resolve: defaultResolveMapHandler,
reject: defaultResolveMapHandler,
reject: defaultRejectHandler,
complete: fn,
// metadata: {
// origin: goog.getUid(this),
Expand Down Expand Up @@ -284,9 +284,8 @@ goog.scope(function() {
function processNextTick(p) {
if (p[QUEUE].length === 0) return;

const value = p[VALUE];
let state = p[STATE];
let task, rvalue;
const pstate = p[STATE];
const pvalue = p[VALUE];

// console.log(":: process:",
// "uid:", goog.getUid(p),
Expand All @@ -295,12 +294,29 @@ goog.scope(function() {
// "value:", fmtValue(p[VALUE]));

while (p[QUEUE].length) {
task = p[QUEUE].shift();
// The outcome (state/rvalue) is task-local: each task is
// computed only from the source promise state/value and its own
// handler, never from the outcome of a previous task in the
// same queue (several independent chains may share the same
// source promise, e.g. the shared NULL_PROMISE).
const task = p[QUEUE].shift();
let state, rvalue;

try {
if (state === RESOLVED) {
rvalue = task.resolve(value)
} else if (state === REJECTED) {
rvalue = task.reject(value)
if (pstate === RESOLVED) {
rvalue = task.resolve(pvalue);
state = RESOLVED;
} else if (pstate === REJECTED) {
if (task.reject === defaultRejectHandler) {
// Same outcome as calling defaultRejectHandler (which
// rethrows the cause, see the catch block below), without
// the cost of throwing.
state = REJECTED;
rvalue = (task.type === RESOLVE_TYPE_BIND) ? self.rejected(pvalue) : pvalue;
} else {
rvalue = task.reject(pvalue);
state = RESOLVED;
}
} else {
rvalue = new TypeError("invalid state");
state = REJECTED;
Expand Down
33 changes: 33 additions & 0 deletions test/promesa/tests/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,39 @@

(p/reject! p1 (ex-info "boom" {}))))))

#?(:cljs
(t/deftest independent-chains-from-nil-do-not-share-outcome
;; (p/resolved nil) returns a shared instance, so both chains are
;; queued on the same promise; the exception of the first chain
;; should not leak into the second one.
(t/async done
(let [a (p/then (p/resolved nil) (fn [_] (throw (ex-info "A" {}))))
b (p/then (p/resolved nil) (fn [_] "b-ok"))
a' (.catch ^js a (fn [_] nil))]
(->> (p/wait-all a' b)
(p/fnly (fn [_ _]
(t/is (p/rejected? a))
(t/is (= "A" (ex-message (p/extract a))))
(t/is (p/resolved? b))
(t/is (= "b-ok" (p/extract b)))
(done))))))))

#?(:cljs
(t/deftest throw-in-then-recover-in-catch-and-continue
(t/async done
(let [c (-> ^js (p/resolved nil)
(.then (fn [_] (throw (ex-info "C" {}))))
(.catch (fn [e] (str "recovered:" (ex-message e))))
(.then (fn [v] (str v ":continued"))))
d (p/then (p/resolved nil) (fn [_] "d-ok"))]
(->> (p/wait-all c d)
(p/fnly (fn [_ _]
(t/is (p/resolved? c))
(t/is (= "recovered:C:continued" (p/extract c)))
(t/is (p/resolved? d))
(t/is (= "d-ok" (p/extract d)))
(done))))))))

(t/deftest rejected-and-wrap-on-catch
(let [p1 (p/rejected (ex-info "foobar" {}))
p2 (p/catch p1 (fn [cause]
Expand Down