diff --git a/src/asyncMiddleware/README.md b/src/asyncMiddleware/README.md index 3e150e3..af7313f 100644 --- a/src/asyncMiddleware/README.md +++ b/src/asyncMiddleware/README.md @@ -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/ @@ -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); ``` diff --git a/src/asyncMiddleware/asyncMiddleware.test.ts b/src/asyncMiddleware/asyncMiddleware.test.ts index 59080e6..d93f46c 100644 --- a/src/asyncMiddleware/asyncMiddleware.test.ts +++ b/src/asyncMiddleware/asyncMiddleware.test.ts @@ -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); + }); }); diff --git a/src/asyncMiddleware/asyncMiddleware.ts b/src/asyncMiddleware/asyncMiddleware.ts index cc3a448..f8487d9 100644 --- a/src/asyncMiddleware/asyncMiddleware.ts +++ b/src/asyncMiddleware/asyncMiddleware.ts @@ -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 = ( init: () => Promise>, + ttl?: number, ): Middleware => { let cache: Promise> | undefined; + let cacheTimestamp: number; const initOrInvalidate = async () => { try { - return await init(); + const cacheInit = await init(); + cacheTimestamp = Date.now(); + return cacheInit; } catch (err) { cache = undefined; @@ -26,7 +32,15 @@ export const lazyLoad = ( } }; + 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;