Closed atticoos closed 7 years ago
Hi @ajwhite -- thanks for the PR.
Did you try running the unit test before adding the changes?
It passes with no code changes. Are you using TDD? (Write the test first).
I had hand written some examples when experimenting with this library, which had lead to the reproducible behavior. However, I see where things went wrong in my test, and I had not written it first (good call). cancel
doesn't fire until 100ms
, yet the test completes at 50ms
, so onCancel
would not have been called by the time of assertion. Just need to block the final then
where the assertion takes place for another 51ms
.
Change incoming.
The new test can now be seen failing against master
in https://github.com/ajwhite/speculation/tree/unit-test-without-cancellation-patch
This test gets the job done:
test('speculation promise resolved before cancel', assert => {
const msg = 'onCancel should not run';
const testWait = (
time,
cancel = Promise.resolve()
) => speculation((resolve, reject, onCancel) => {
onCancel(() => {
assert.fail(msg);
})
.catch(() => wait(40))
.then(() => {
assert.end();
});
}, cancel);
testWait(20, wait(50)).then(() => {
assert.pass(msg);
}).then(wait(40)).then(() => {
assert.end();
});
});
Failing right inside the cancellation callback -- fantastic. No need for an additional dep 👍
Okay, this looks good to me. Only issue in your feedback testWait
was missing a resolve
call
test('speculation promise resolved before cancel', assert => {
const msg = 'onCancel should not run';
const testWait = (
time,
cancel = Promise.resolve()
) => speculation((resolve, reject, onCancel) => {
onCancel(() => {
assert.fail(msg);
})
.catch(() => wait(40))
.then(() => {
assert.end();
});
+ setTimeout(resolve, time);
}, cancel);
testWait(20, wait(50)).then(() => {
assert.pass(msg);
}).then(wait(40)).then(() => {
assert.end();
});
});
With the resolve in place, both end()
will become called, and tape
will yell at you
not ok 8
.end()
called twice
I've moved this to only end
in the completion block to the end of testWait
-- I think that should be enough since that will be the last execution in the test.
However, I wonder if we should add a .catch
before the end of testWait
to ensure assert.end()
is always called
testWait(20, wait(50)).then(() => {
assert.pass(msg);
})
+.catch(() => {})
.then(wait(40))
.then(() => {
assert.end();
});
Should this work?
test('speculation promise resolved before cancel', assert => {
const msg = 'onCancel should not run';
const testWait = (
time,
cancel = Promise.resolve()
) => speculation((resolve, reject, onCancel) => {
onCancel(() => {
assert.fail(msg);
});
}, cancel);
testWait(20, wait(50)).then(() => {
assert.pass('success callback should be called');
});
wait(60).then(() => {
assert.end();
});
});
Breaking it out like that greatly simplifies this and is a bit more coherent 👍
Cool, but it fails. 👎
You're missing a setTimeout(resolve, time)
in the speculation block -- works fine once added
I added some style tweaks to your PR. Thoughts?
Looks good, I agree with these on a number of levels:
handleCancelIfCompleted
was a leaky abstraction in the namingAlthough isFulfilled
-- "fulfilled" is the terminology as described in the spec for resolved promises. This represents both rejections or resolved. A "pending promise" becomes a "completed promise" in either fulfilled or rejected states.
But overall great improvements 👍
Oops, you're right. That should be isSettled
.
Published in 1.0.5
The goal of this PR is to avoid
onCancel
become called when a Promise does not actually become cancelled.In Speculation's current form,
onCancel
will become invoked regardless of the outcome of the observed promise. This callback is useful to to perform cleanup tasks, but it might be used to perform other duties that should only be performed when a promise is indeed cancelled early.This solution will avoid triggering
onCancel
unless the cancellation is actually performed before the pending promise becomes completed.The solution works by creating a higher order function of
resolve
that letsspeculation
know if the Promise becomes resolved or rejected downstream in where the promise is managed by the dependent's business logic. The higher order function will maintain aboolean
state ofcompleted
. Unfortunately this is a bit of an impure approach, as we manage state withinspeculation
on the "progress" of the promise -- but I wasn't quite sure off the top of my head of a better way. Happy to revise if requested.Example:
Example output:
With this new patch: