From b8b4cff160fb3c154901d385f77d09a2661ff8e0 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Fri, 22 Feb 2019 11:05:21 +0100 Subject: [PATCH] feat: add custom ttl and revalidate --- README.md | 28 ++++++++++++++++++++++++++-- index.js | 42 +++++++++++++++++++++++++++++------------- test/index.js | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 05d8435..501d81d 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ curl https://myserver.dev/user?force=true # MISS (forcing invalidation) ##### cache Type: `boolean`
-Default: `[keyv](https://github.com/lukechilds/keyv)` +Default: [`keyv`](https://github.com/lukechilds/keyv) The cache instance used for backed your pre-calculated server side response copies. @@ -126,6 +126,30 @@ The library delegates in [keyv](https://github.com/lukechilds/keyv), a tiny key If you don't specify it, a memory cache will be used. +##### ttl + +Type: `number`
+Default: `7200000` + +Number of milliseconds a cache response is considered fresh. + +After this period of time, the cache response should be refreshed. + +This value can be specified as well providing it as part of [`.get`](#get) output. + +If you don't provide one, this be used as fallback for avoid keep things into cache forever. + +##### revalidate + +Type: `function`|`number`
+Default: `ttl => ttl / 24` + +Number of milliseconds that indicates grace period after response cache expiration for refreshing it in the background. the latency of the refresh is hidden from the user. + +You can provide a function, it will receive [`ttl`](#ttl) as first parameter or a fixed value. + +The value will be associated with [`stale-while-revalidate`](https://www.mnot.net/blog/2014/06/01/chrome_and_stale-while-revalidate) directive. + ##### get _Required_
@@ -144,7 +168,7 @@ function get ({ req, res }) { The method will received `({ req, res })` and it should be returns: - **data** `string`: The content to be saved on the cache. -- **ttl** `number`: The quantity of time in milliseconds the content is considered valid on the cache. Don't specify it means store it forever. +- **ttl** `number`: The quantity of time in milliseconds the content is considered valid on the cache. Don't specify it means use default [`ttl`](#ttl). - **createdAt** `date`: The timestamp associated with the content (`Date.now()` by default). Any other property can be specified and will passed to `.send`. diff --git a/index.js b/index.js index bdc3dd2..d5c3455 100644 --- a/index.js +++ b/index.js @@ -15,23 +15,39 @@ const getKey = url => { return baseKey.replace(origin, '').replace('/?', '') } -const setCacheControl = ({ res, createdAt, isHit, ttl, force }) => { - // Specifies the maximum amount of time a resource - // will be considered fresh in seconds - const diff = force ? 0 : createdAt + ttl - Date.now() - const maxAge = Math.floor(diff / 1000) - res.setHeader( - 'Cache-Control', - `public, max-age=${maxAge}, s-maxage=${maxAge}, stale-while-revalidate=30` - ) - res.setHeader('X-Cache-Status', isHit ? 'HIT' : 'MISS') - res.setHeader('X-Cache-Expired-At', prettyMs(diff)) +const toSeconds = ms => Math.floor(ms / 1000) + +const createSetCacheControl = ({ revalidate }) => { + return ({ res, createdAt, isHit, ttl, force }) => { + // Specifies the maximum amount of time a resource + // will be considered fresh in seconds + const diff = force ? 0 : createdAt + ttl - Date.now() + const maxAge = toSeconds(diff) + res.setHeader( + 'Cache-Control', + `public, max-age=${maxAge}, s-maxage=${maxAge}, stale-while-revalidate=${toSeconds( + revalidate(ttl) + )}` + ) + res.setHeader('X-Cache-Status', isHit ? 'HIT' : 'MISS') + res.setHeader('X-Cache-Expired-At', prettyMs(diff)) + } } -module.exports = ({ cache = new Keyv(), get, send } = {}) => { +module.exports = ({ + cache = new Keyv(), + get, + send, + revalidate = ttl => ttl / 24, + ttl: defaultTtl = 7200000 +} = {}) => { assert(get, 'get required') assert(send, 'send required') + const setCacheControl = createSetCacheControl({ + revalidate: typeof revalidate === 'function' ? revalidate : () => revalidate + }) + return async ({ req, res, ...opts }) => { const hasForce = Boolean(req.query ? req.query.force : parse(req.url).force) const url = urlResolve('http://localhost', req.url) @@ -39,7 +55,7 @@ module.exports = ({ cache = new Keyv(), get, send } = {}) => { const cachedResult = await cache.get(key) const isHit = cachedResult && !hasForce - const { ttl, createdAt = Date.now(), data, ...props } = isHit + const { ttl = defaultTtl, createdAt = Date.now(), data, ...props } = isHit ? cachedResult : await get({ req, res, ...opts }) diff --git a/test/index.js b/test/index.js index 98f7da0..0e73f6b 100644 --- a/test/index.js +++ b/test/index.js @@ -19,6 +19,53 @@ test('required props', t => { t.throws(() => cacheableResponse({ get: true }), AssertionError, 'send is required') }) +test('default ttl and revalidate', async t => { + const url = await createServer({ + get: ({ req, res }) => ({ data: { foo: 'bar' } }), + send: ({ data, headers, res, req, ...props }) => res.end('Welcome to Micro') + }) + const { headers } = await got(`${url}/kikobeats`) + t.is(headers['cache-control'], 'public, max-age=7200, s-maxage=7200, stale-while-revalidate=300') +}) + +test('custom ttl', async t => { + const url = await createServer({ + get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }), + send: ({ data, headers, res, req, ...props }) => res.end('Welcome to Micro') + }) + const { headers } = await got(`${url}/kikobeats`) + t.is( + headers['cache-control'], + 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=3600' + ) +}) + +test('custom revalidate', async t => { + const url = await createServer({ + revalidate: ttl => ttl * 0.8, + get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }), + send: ({ data, headers, res, req, ...props }) => res.end('Welcome to Micro') + }) + const { headers } = await got(`${url}/kikobeats`) + t.is( + headers['cache-control'], + 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=69120' + ) +}) + +test('custom fixed revalidate', async t => { + const url = await createServer({ + revalidate: 300000, + get: ({ req, res }) => ({ data: { foo: 'bar' }, ttl: 86400000 }), + send: ({ data, headers, res, req, ...props }) => res.end('Welcome to Micro') + }) + const { headers } = await got(`${url}/kikobeats`) + t.is( + headers['cache-control'], + 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=300' + ) +}) + test('MISS for first access', async t => { const url = await createServer({ get: ({ req, res }) => {