Skip to content

Commit

Permalink
feat(AsyncMiddleware): Add TTL for cache validation (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
lumishrestha authored Jul 16, 2020
1 parent 6829c14 commit 63e58a2
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 3 deletions.
4 changes: 2 additions & 2 deletions src/asyncMiddleware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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 middleware in your chain.
but you need to perform asynchronous work like [GraphQL schema introspection] in order to build one of the middleware in your chain. You can optionally set a TTL so the inner middleware can be reinitialised when the cache has expired.

[koa cluster]: https://github.com/koajs/cluster
[graphql schema introspection]: https://graphql.org/learn/introspection/
Expand All @@ -20,7 +20,7 @@ const initGraphMiddleware: = async () => {
return new GraphServer(schema).getMiddleware();
};

const graphMiddleware = AsyncMiddleware.lazyLoad(initGraphMiddleware);
const graphMiddleware = AsyncMiddleware.lazyLoad(initGraphMiddleware, 120000);

app.use(graphMiddleware);
```
38 changes: 38 additions & 0 deletions src/asyncMiddleware/asyncMiddleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,42 @@ describe('asyncMiddleware', () => {

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

it('should cache a new inner middleware on expired TTL', async () => {
const ctx: Koa.Context = makeCtx();
const ttl = 60_000;

const currentTime = jest.spyOn(Date, 'now');

const innerMiddleware = jest
.fn()
.mockImplementationOnce(() => (ctx.status = 201))
.mockImplementationOnce(() => (ctx.status = 202))
.mockImplementationOnce(() => (ctx.status = 203));
const init = jest.fn().mockResolvedValue(innerMiddleware);
const middleware = lazyLoad(init, ttl);

currentTime.mockReturnValue(0);

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

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

currentTime.mockReturnValue(1000);

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

expect(init).toHaveBeenCalledTimes(1);
expect(innerMiddleware).toHaveBeenCalledTimes(2);

currentTime.mockReturnValue(80_000);
await expect(middleware(ctx, next)).resolves.toBe(203);
expect(ctx.status).toBe(203);

expect(init).toHaveBeenCalledTimes(2);
expect(innerMiddleware).toHaveBeenCalledTimes(3);
});
});
16 changes: 15 additions & 1 deletion src/asyncMiddleware/asyncMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,23 @@ import { Middleware } from 'koa';
* 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.
* If `ttl` is set, the middleware is re-initialised when cache expires.
*
* @param init - Function to asynchronously initialise a middleware
* @param ttl - Time in ms
*/
export const lazyLoad = <State, Context>(
init: () => Promise<Middleware<State, Context>>,
ttl?: number,
): Middleware<State, Context> => {
let cache: Promise<Middleware<State, Context>> | undefined;
let cacheTimestamp: number;

const initOrInvalidate = async () => {
try {
return await init();
const cacheInit = await init();
cacheTimestamp = Date.now();
return cacheInit;
} catch (err) {
cache = undefined;

Expand All @@ -26,7 +32,15 @@ export const lazyLoad = <State, Context>(
}
};

const validateCache = () =>
typeof cache === 'undefined' ||
typeof cacheTimestamp === 'undefined' ||
typeof ttl === 'undefined' ||
Date.now() - cacheTimestamp < ttl ||
(cache = undefined);

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

return middleware(ctx, next) as unknown;
Expand Down

0 comments on commit 63e58a2

Please sign in to comment.