Skip to content

Commit

Permalink
feat: add custom ttl and revalidate
Browse files Browse the repository at this point in the history
  • Loading branch information
Kikobeats committed Feb 22, 2019
1 parent 25711c3 commit b8b4cff
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 15 deletions.
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,38 @@ curl https://myserver.dev/user?force=true # MISS (forcing invalidation)
##### cache

Type: `boolean`<br>
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.

The library delegates in [keyv](https://github.com/lukechilds/keyv), a tiny key value store with [multi adapter support](https://github.com/lukechilds/keyv#official-storage-adapters).

If you don't specify it, a memory cache will be used.

##### ttl

Type: `number`<br>
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`<br>
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_<br>
Expand All @@ -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`.
Expand Down
42 changes: 29 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,47 @@ 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)
const key = getKey(url)
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 })

Expand Down
47 changes: 47 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down

0 comments on commit b8b4cff

Please sign in to comment.