diff --git a/src/plugin/task-instance.js b/src/plugin/task-instance.js index a91ccb1..619d975 100644 --- a/src/plugin/task-instance.js +++ b/src/plugin/task-instance.js @@ -66,8 +66,8 @@ export default function createTaskInstance(data, subscriber) { }, /** - * To finalize an instance that was called with the `keepRunning` binding, - * we call the resulting handle method returned by the stepper. + * To finalize an instance called with the `keepRunning` binding, + * we call the resulting handle method returned by the stepper. */ destroy() { if (!this.isFinished) this.cancel() diff --git a/src/plugin/task-property.js b/src/plugin/task-property.js index 129673b..38ba0d0 100644 --- a/src/plugin/task-property.js +++ b/src/plugin/task-property.js @@ -48,7 +48,8 @@ export default function createTaskProperty(host, operation, autorun = true) { run(...params) { if (!scheduler) scheduler = createTaskScheduler(this, policy) this.isAborted = false - let instanceData = { params, operation: operation.bind(host, ...params) }, + let boundOperation = reflectBind(host, operation, params), + instanceData = { params, operation: boundOperation }, ti = createTaskInstance(instanceData, subscriber) if (autorun) scheduler.schedule(ti) return ti @@ -75,3 +76,15 @@ export default function createTaskProperty(host, operation, autorun = true) { ...subscriptions } } + +/** + * Function neutral bind operation. + * + * If `bind` is used on a gen function, it changes its prototype to `Function`. + * So we make sure to set the prototype of each operation back to its original. + */ +function reflectBind(ctx, operation, args) { + let boundOperation = operation.bind(ctx, ...args) + Reflect.setPrototypeOf(boundOperation, operation) + return boundOperation +} diff --git a/src/plugin/task-stepper.js b/src/plugin/task-stepper.js index c3c7de9..2f3c06e 100644 --- a/src/plugin/task-stepper.js +++ b/src/plugin/task-stepper.js @@ -1,4 +1,4 @@ -import { isObj } from '../util/assert' +import { isObj, isGen } from '../util/assert' /** A {Stepper} is responsible for iterating through the generator function. @@ -14,9 +14,11 @@ import { isObj } from '../util/assert' * @constructs Task Stepper */ export default function createTaskStepper(ti, callbacks) { - let iter = ti._operation(), - keepRunning = ti.options.keepRunning, - cancelablePromise + let iter, + cancelablePromise, + keepRunning = ti.options.keepRunning + + if (isGen(ti._operation)) iter = ti._operation() return { async handleStart() { @@ -75,39 +77,58 @@ export default function createTaskStepper(ti, callbacks) { }, /** - * Recursively iterates through generator function until the operation is - * either canceled, rejected, or resolved. + * Wrapper for task's operation. * - * When the operation is finished, we return the resulting handle method - * so that it can be executed or returned, in the cases where the final - * callback needs to be deferred for later handling. + * Runs operation, updates state, and either executes resulting callback + * or, if operation is kept running, defers it for later handling. */ async stepThrough() { let stepper = this - async function takeAStep(prev = undefined) { - let value, done + if (ti.isCanceled) return stepper.handleEnd(stepper.handleCancel) // CANCELED / PRE-START + + if (!ti.hasStarted) await stepper.handleStart() // STARTED - try { - ({ value, done } = await stepper.handleYield(prev)) - } catch (err) { // REJECTED - return stepper.handleError.bind(stepper, err) - } + const resultCallback = isGen(ti._operation) // RESOLVED / REJECTED / CANCELED + ? await stepper._iterThrough() + : await stepper._syncThrough() + + if (keepRunning) return stepper.handleEnd.bind(stepper, resultCallback) + else return stepper.handleEnd(resultCallback) + }, - if (isObj(value) && value._cancel_) cancelablePromise = value - if (ti.isCanceled) return stepper.handleCancel // CANCELED / POST-YIELD + /** + * Recursively iterates through generator function until the operation is + * either resolved, rejected, or canceled. + * @return resulting handle callback + */ + async _iterThrough(prev = undefined) { + let value, done, stepper = this - value = await value - if (done) return stepper.handleSuccess.bind(stepper, value) // RESOLVED - else return await takeAStep(value) + try { + ({ value, done } = await stepper.handleYield(prev)) + } catch (err) { // REJECTED + return stepper.handleError.bind(stepper, err) } - if (ti.isCanceled) return stepper.handleEnd(stepper.handleCancel) // CANCELED / PRE-START - if (!ti.hasStarted) await stepper.handleStart() // STARTED - const resultCallback = await takeAStep() // RESOLVED / REJECTED / CANCELED + if (isObj(value) && value._cancel_) cancelablePromise = value + if (ti.isCanceled) return stepper.handleCancel // CANCELED / POST-YIELD - if (keepRunning) return stepper.handleEnd.bind(stepper, resultCallback) - else return stepper.handleEnd(resultCallback) + value = await value + if (done) return stepper.handleSuccess.bind(stepper, value) // RESOLVED + else return await this._iterThrough(value) + }, + + /** + * Awaits the async function until the operation is either resolved + * or rejected. (Cancelation is not an option with async functions.) + * @return resulting handle callback + */ + async _syncThrough() { + let stepper = this + return ti._operation() + .then(val => stepper.handleSuccess.bind(stepper, val)) + .catch(err => stepper.handleError.bind(stepper, err)) } } } diff --git a/src/util/assert.js b/src/util/assert.js index c9478d0..549b324 100644 --- a/src/util/assert.js +++ b/src/util/assert.js @@ -10,7 +10,7 @@ export function isNamedFn(fn) { return isFn(fn) && fn.name !== 'undefined' && fn.name !== '' } -export function isGenFn(fn) { +export function isGen(fn) { return fn.constructor.name === 'GeneratorFunction' } diff --git a/test/unit/coverage/lcov-report/index.html b/test/unit/coverage/lcov-report/index.html index 137c5d5..54575af 100644 --- a/test/unit/coverage/lcov-report/index.html +++ b/test/unit/coverage/lcov-report/index.html @@ -64,7 +64,7 @@

diff --git a/test/unit/specs/task-stepper.spec.js b/test/unit/specs/task-stepper.spec.js index b1690d1..8c1137d 100644 --- a/test/unit/specs/task-stepper.spec.js +++ b/test/unit/specs/task-stepper.spec.js @@ -13,7 +13,7 @@ function * exTask() { return 'success' } -describe('Task Stepper', function() { +describe('Task Stepper - Generator Functions', function() { it('solves empty function', async () => { let operation = function * () {}, ti = createTaskInstance({ operation }), @@ -78,7 +78,6 @@ describe('Task Stepper', function() { expect(ti.isRejected).to.be.true expect(ti.isResolved).to.be.false expect(ti.isCanceled).to.be.false - // TODO should rejected task still attempt to return value? expect(ti.value).to.be.null expect(ti.error).to.not.be.null }) @@ -224,3 +223,31 @@ describe('Task Stepper', function() { expect(ti.value).to.equal('Success') }) }) + +describe('Task Stepper - Async Functions', function() { + it('resolves operation', async () => { + let operation = async function() { + return 'success' + }, + ti = createTaskInstance({ operation }), + stepper = createTaskStepper(ti, subscriber) + await stepper.stepThrough() + expect(ti.isResolved).to.be.true + expect(ti.isRejected).to.be.false + expect(ti.value).to.equal('success') + expect(ti.error).to.be.null + }) + + it('rejects the task instance', async () => { + let operation = async function() { + return await sinon.stub().returns('failed').throws()() + }, + ti = createTaskInstance({ operation }), + stepper = createTaskStepper(ti, subscriber) + await stepper.stepThrough() + expect(ti.isRejected).to.be.true + expect(ti.isResolved).to.be.false + expect(ti.value).to.be.null + expect(ti.error).to.not.be.null + }) +})