Skip to content

Commit

Permalink
feat(AsyncMiddleware): Add module (#6)
Browse files Browse the repository at this point in the history
Co-authored-by: Ryan Cumming <etaoins@gmail.com>
  • Loading branch information
72636c and etaoins authored Jun 11, 2020
1 parent 5fb0235 commit 4da048f
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 80 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ Refer to the [Koala manifesto](CONTRIBUTING.md) for philosophy behind Koala.

## Included Add-Ons

1. **[MetricsMiddleware](./src/metricsMiddleware/README.md)** uses [hot-shots](https://github.com/brightcove/hot-shots) to record Datadog metrics about requests and their response codes.
- **[AsyncMiddleware](./src/asyncMiddleware/README.md)** facilitates lazy loading of an asynchronously-initialised middleware.

2. **[RequestLogging](./src/requestLogging/README.md)** facilitates logging information about requests and responses.
- **[MetricsMiddleware](./src/metricsMiddleware/README.md)** uses [hot-shots](https://github.com/brightcove/hot-shots) to record Datadog metrics about requests and their response codes.

3. **[SecureHeaders](./src/secureHeaders/README.md)** attaches response headers that opt-in to stricter browser security policies.
- **[RequestLogging](./src/requestLogging/README.md)** facilitates logging information about requests and responses.

4. **[TracingHeaders](./src/tracingHeaders/README.md)** deals with [RFC002 request tracing](https://github.com/SEEK-Jobs/rfc/blob/master/RFC002-RequestIds.md) and `User-Agent` headers.
- **[SecureHeaders](./src/secureHeaders/README.md)** attaches response headers that opt-in to stricter browser security policies.

5. **[VersionMiddleware](./src/versionMiddleware/README.md)** attaches app version information to outgoing responses.
- **[TracingHeaders](./src/tracingHeaders/README.md)** deals with [RFC002 request tracing](https://github.com/SEEK-Jobs/rfc/blob/master/RFC002-RequestIds.md) and `User-Agent` headers.

- **[VersionMiddleware](./src/versionMiddleware/README.md)** attaches app version information to outgoing responses.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"hot-shots": "7.5.0",
"koa": "2.12.0",
"semantic-release": "17.0.8",
"skuba": "3.5.0-beta.2"
"skuba": "3.6.0"
},
"engines": {
"node": ">=10.7"
Expand Down
26 changes: 26 additions & 0 deletions src/asyncMiddleware/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 🐨 Async Middleware 🐨

## Introduction

This add-on wraps an asynchronously-initialised middleware to allow it to be synchronously attached to a Koa application.
This is useful if you want to keep the initialisation of your Koa application synchronous for simplicity and interoperability with tooling like [Koa Cluster],
but you need to perform asynchronous work like [GraphQL schema introspection] in order to build one of the middlewares in your chain.

[koa cluster]: https://github.com/koajs/cluster
[graphql schema introspection]: https://graphql.org/learn/introspection/

## Usage

```typescript
import { AsyncMiddleware } from 'seek-koala';

const initGraphMiddleware: = async () => {
const schema = await introspectSchema();

return new GraphServer(schema).getMiddleware();
};

const graphMiddleware = AsyncMiddleware.lazyLoad(initGraphMiddleware);

app.use(graphMiddleware);
```
49 changes: 49 additions & 0 deletions src/asyncMiddleware/asyncMiddleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Koa from 'koa';

import { lazyLoad } from './asyncMiddleware';

describe('asyncMiddleware', () => {
const makeCtx = (fields: Record<string, unknown> = {}): Koa.Context =>
(({
state: {},
method: 'GET',
...fields,
} as unknown) as Koa.Context);

const ctx: Koa.Context = makeCtx();
const next = jest.fn().mockRejectedValue(new Error('why are you here'));

it('should cache a successfully initialised inner middleware', async () => {
const innerMiddleware = jest.fn(() => (ctx.status = 201));
const createInnerMiddleware = jest.fn().mockResolvedValue(innerMiddleware);
const middleware = lazyLoad(createInnerMiddleware);

await expect(middleware(ctx, next)).resolves.toBe(201);

expect(createInnerMiddleware).toHaveBeenCalledTimes(1);
expect(innerMiddleware).toHaveBeenCalledTimes(1);

await expect(middleware(ctx, next)).resolves.toBe(201);

expect(createInnerMiddleware).toHaveBeenCalledTimes(1);
expect(innerMiddleware).toHaveBeenCalledTimes(2);
});

it('should retry inner middleware initialisation on failure', async () => {
const err = new Error('middleware initialisation failed!');
const innerMiddleware = jest.fn(() => (ctx.status = 201));
const createInnerMiddleware = jest
.fn()
.mockRejectedValueOnce(err)
.mockResolvedValue(innerMiddleware);
const middleware = lazyLoad(createInnerMiddleware);

await expect(middleware(ctx, next)).rejects.toThrow(err);

expect(createInnerMiddleware).toHaveBeenCalledTimes(1);

await expect(middleware(ctx, next)).resolves.toBe(201);

expect(createInnerMiddleware).toHaveBeenCalledTimes(2);
});
});
34 changes: 34 additions & 0 deletions src/asyncMiddleware/asyncMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Middleware } from 'koa';

/**
* Wraps an asynchronously-initialised middleware to allow it to be
* synchronously attached to a Koa application.
*
* This lazy loads the function supplied through the `init` parameter at time of
* request. If initialisation fails, the error is thrown up the chain for
* in-flight requests, and initialisation is retried on the next request.
*
* @param init - Function to asynchronously initialise a middleware
*/
export const lazyLoad = <State, Context>(
init: () => Promise<Middleware<State, Context>>,
): Middleware<State, Context> => {
let cache: Promise<Middleware<State, Context>> | undefined;

const initOrInvalidate = async () => {
try {
return await init();
} catch (err) {
cache = undefined;

/* eslint-disable-next-line no-throw-literal */
throw err;
}
};

return async (ctx, next) => {
const middleware = await (cache ?? (cache = initOrInvalidate()));

return middleware(ctx, next) as unknown;
};
};
18 changes: 6 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import * as MetricsMiddleware from './metricsMiddleware/metricsMiddleware';
import * as RequestLogging from './requestLogging/requestLogging';
import * as SecureHeaders from './secureHeaders/secureHeaders';
import * as TracingHeaders from './tracingHeaders/tracingHeaders';
import * as VersionMiddleware from './versionMiddleware/versionMiddleware';
export * as AsyncMiddleware from './asyncMiddleware/asyncMiddleware';
export * as MetricsMiddleware from './metricsMiddleware/metricsMiddleware';
export * as RequestLogging from './requestLogging/requestLogging';
export * as SecureHeaders from './secureHeaders/secureHeaders';
export * as TracingHeaders from './tracingHeaders/tracingHeaders';
export * as VersionMiddleware from './versionMiddleware/versionMiddleware';

export { AppIdentifier } from './types';
export {
MetricsMiddleware,
RequestLogging,
SecureHeaders,
TracingHeaders,
VersionMiddleware,
};
Loading

0 comments on commit 4da048f

Please sign in to comment.