Skip to content

AsyncGenerator middleware does not await steps #1191

@Tomalak

Description

@Tomalak

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,
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions