-
Notifications
You must be signed in to change notification settings - Fork 7
Description
Package(s)
- router
Describe the bug
The documentation states:
For async with generator middleware, yield a promise or use an async generator.
The following are equivalent…function* middleware(ctx): IterableIterator<Promise<void>> { yield yield Promise.resolve() } async function* middleware(ctx): AsyncIterableIterator<void> { yield await Promise.resolve() yield }
Unfortunately, this is not the case. AsyncGenerator middleware steps will not be awaited before passing control to the next middleware.
This breaks AsyncGenerator middleware. A kludge/workaround is to explicitly yield
a separate Promise, but this defeats the purpose of using an async generator in the first place.
To Reproduce
import ko from 'knockout';
import { Router, Route } from '@profiscience/knockout-contrib-router';
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* asyncGeneratorMiddleware() {
console.log('asyncGeneratorMiddleware beforeRender...');
await delay(1000);
console.log('asyncGeneratorMiddleware beforeRender done');
yield;
console.log('asyncGeneratorMiddleware afterRender...');
await delay(1000);
console.log('asyncGeneratorMiddleware afterRender done');
}
function* generatorMiddleware() {
console.log('generatorMiddleware beforeRender');
yield;
console.log('generatorMiddleware afterRender...');
}
function monolithMiddleware() {
console.log('monolithMiddleware');
}
Router.useRoutes([
new Route(
'/',
asyncGeneratorMiddleware,
generatorMiddleware,
monolithMiddleware,
'some-component',
)
]);
Router.initialized.then((r) => console.log('Router initialized'));
ko.applyBindings();
Expected behavior
asyncGeneratorMiddleware beforeRender...
asyncGeneratorMiddleware beforeRender done
generatorMiddleware beforeRender
monolithMiddleware
some-component viewmodel constructor
asyncGeneratorMiddleware afterRender...
Router initialized
asyncGeneratorMiddleware afterRender done
generatorMiddleware afterRender
The middlewares wait for each other, and the viewmodel constructor is invoked after the async work in all the beforeRender
lifecycle steps is done.
Actual behavior
asyncGeneratorMiddleware beforeRender...
generatorMiddleware beforeRender
monolithMiddleware
some-component viewmodel constructor
generatorMiddleware afterRender
Router initialized
asyncGeneratorMiddleware beforeRender done
asyncGeneratorMiddleware afterRender...
asyncGeneratorMiddleware afterRender done
The middlewares start at the same time, and the viewmodel constructor is invoked before the the async work in all the beforeRender
lifecycle steps is done.
Additional info
It looks like the culprit is in router/src/context.ts, line 243 ff..
lifecycle = {
beforeRender: () => iterator.next().value,
afterRender: () => iterator.next().value,
beforeDispose: () => iterator.next().value,
afterDispose: () => iterator.next().value,
}
This assumes that there always is a .value
property on the return value of iterator.next()
. Async generators directly yield a promise, the .value
only becomes available after promise resolution. Even though beforeRender()
is awaited, it always awaits undefined
for async generators.
I think this would fix it without changing behavior for synchronous generator middleware:
lifecycle = {
beforeRender: async () => (await iterator.next()).value,
afterRender: async () => (await iterator.next()).value,
beforeDispose: async () => (await iterator.next()).value,
afterDispose: async () => (await iterator.next()).value,
}