Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/cache middleware #80

Merged
merged 6 commits into from Jan 13, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Expand Up @@ -494,6 +494,7 @@ on how to write a middleware.

Currently available middlewares:

- [`cache`](/docs/middlewares.md#cache): a simple but flexible caching layer
- [`cors`](/docs/middlewares.md#cors): sets CORS headers on response
- [`httpErrorHandler`](/docs/middlewares.md#httperrorhandler): creates a proper HTTP response for errors that are created with the [http-errors](https://www.npmjs.com/package/http-errors) module and represents proper HTTP errors.
- [`jsonBodyParser`](/docs/middlewares.md#jsonbodyparser): automatically parses HTTP requests with JSON body and converts the body into an object. Also handles gracefully broken JSON if used in combination of
Expand All @@ -504,8 +505,6 @@ Currently available middlewares:
- [`doNotWaitForEmptyEventLoop`](/docs/middlewares.md#donotwaitforemptyeventloop): sets callbackWaitsForEmptyEventLoop property to false




For a dedicated documentation on those middlewares check out the [Middlewares
documentation](/docs/middlewares.md)

Expand Down
3 changes: 1 addition & 2 deletions README.md.hb
Expand Up @@ -494,6 +494,7 @@ on how to write a middleware.

Currently available middlewares:

- [`cache`](/docs/middlewares.md#cache): a simple but flexible caching layer
- [`cors`](/docs/middlewares.md#cors): sets CORS headers on response
- [`httpErrorHandler`](/docs/middlewares.md#httperrorhandler): creates a proper HTTP response for errors that are created with the [http-errors](https://www.npmjs.com/package/http-errors) module and represents proper HTTP errors.
- [`jsonBodyParser`](/docs/middlewares.md#jsonbodyparser): automatically parses HTTP requests with JSON body and converts the body into an object. Also handles gracefully broken JSON if used in combination of
Expand All @@ -504,8 +505,6 @@ Currently available middlewares:
- [`doNotWaitForEmptyEventLoop`](/docs/middlewares.md#donotwaitforemptyeventloop): sets callbackWaitsForEmptyEventLoop property to false




For a dedicated documentation on those middlewares check out the [Middlewares
documentation](/docs/middlewares.md)

Expand Down
56 changes: 56 additions & 0 deletions docs/middlewares.md
Expand Up @@ -2,6 +2,7 @@

## Available middlewares

- [cache](#cache)
- [cors](#cors)
- [doNotWaitForEmptyEventLoop](#donotwaitforemptyeventloop)
- [httpErrorHandler](#httperrorhandler)
Expand All @@ -11,6 +12,61 @@
- [urlEncodeBodyParser](#urlencodebodyparser)


## [cache](/src/middlewares/cache.js)

Offers a simple but flexible caching layer that allows to cache the response associated
to a given event and return it directly (without running the handler) if such event is received again
in a successive execution.

By default, the middleware stores the cache in memory, so the persistence is guaranteed only for
a short amount of time (the duration of the container), but you can use the configuration
layer to provide your own caching implementation.

### Options

- `calculateCacheId`: a function that accepts the `event` object as a parameter
and returns a promise that resolves to a string which is the cache id for the
give request. By default the cache id is calculated as `md5(JSON.stringify(event))`.
- `getValue`: a function that defines how to retrieve a the value associated to a given
cache id from the cache storage. it accepts `key` (a string) and returns a promise
that resolves to the cached response (if any) or to `undefined` (if the given key
does not exists in the cache)
- `setValue`: a function that defines how to set a value in the cache. It accepts
a `key` (string) and a `value` (response object). It must return a promise that
resolves when the value has been stored.

### Sample usage

```javascript
// assumes the event contains a unique event identifier
const calculateCacheId = (event) => Promise.resolve(event.id)
// use an in memory storage as example
const myStorage = {}
// simulates a delay in retrieving the value from the caching storage
const getValue = (key) => new Promise((resolve, reject) => {
setTimeout(() => resolve(myStorage[key]), 100)
})
// simulates a delay in writing the value in the caching storage
const setValue = (key, value) => new Promise((resolve, reject) => {
setTimeout(() => {
myStorage[key] = value
return resolve()
}, 100)
})

const originalHandler = (event, context, cb) => {
/* ... */
}

const handler = middy(originalHandler)
.use(cache({
calculateCacheId,
getValue,
setValue
}))
```


## [cors](/src/middlewares/cors.js)

Sets CORS headers (`Access-Control-Allow-Origin`), necessary for making cross-origin requests, to response object.
Expand Down
58 changes: 58 additions & 0 deletions src/middlewares/__tests__/cache.js
@@ -0,0 +1,58 @@
const middy = require('../../middy')
const cache = require('../cache')

describe('💽 Cache stuff', () => {
test('It should cache things using the default settings', (endTest) => {
const originalHandler = jest.fn((event, context, cb) => {
cb(null, event.a + event.b)
})

const handler = middy(originalHandler)
.use(cache())

const event = {a: 2, b: 3}
const context = {}
handler(event, context, (_, response) => {
handler(event, context, (_, response2) => {
expect(response).toEqual(response2)
expect(originalHandler.mock.calls.length).toBe(1)
endTest()
})
})
})

test('It should cache things using custom cache settings', (endTest) => {
const calculateCacheId = (event) => Promise.resolve(event.id)
const myStorage = {}
const getValue = (key) => new Promise((resolve, reject) => {
setTimeout(() => resolve(myStorage[key]), 50)
})
const setValue = (key, value) => new Promise((resolve, reject) => {
setTimeout(() => {
myStorage[key] = value
return resolve()
}, 50)
})

const originalHandler = jest.fn((event, context, cb) => {
cb(null, event.a + event.b)
})

const handler = middy(originalHandler)
.use(cache({
calculateCacheId,
getValue,
setValue
}))

const event = {id: 'some_unique_id', a: 2, b: 3}
const context = {}
handler(event, context, (_, response) => {
handler(event, context, (_, response2) => {
expect(response).toEqual(response2)
expect(originalHandler.mock.calls.length).toBe(1)
endTest()
})
})
})
})
38 changes: 38 additions & 0 deletions src/middlewares/cache.js
@@ -0,0 +1,38 @@
const { createHash } = require('crypto')

module.exports = (opts) => {
const defaultStorage = {}
const defaults = {
calculateCacheId: (event) => Promise.resolve(createHash('md5').update(JSON.stringify(event)).digest('hex')),
getValue: (key) => Promise.resolve(defaultStorage[key]),
setValue: (key, value) => {
defaultStorage[key] = value
return Promise.resolve()
}
}

const options = Object.assign({}, defaults, opts)
let currentCacheKey

return ({
before: (handler, next) => {
options.calculateCacheId(handler.event)
.then((cacheKey) => {
currentCacheKey = cacheKey
return options.getValue(cacheKey)
})
.then((cachedResponse) => {
if (typeof cachedResponse !== 'undefined') {
return handler.callback(null, cachedResponse)
}

return next()
})
},
after: (handler, next) => {
// stores the calculated response in the cache
options.setValue(currentCacheKey, handler.response)
.then(next)
}
})
}
1 change: 1 addition & 0 deletions src/middlewares/index.js
@@ -1,4 +1,5 @@
module.exports = {
cache: require('./cache'),
cors: require('./cors'),
doNotWaitForEmptyEventLoop: require('./doNotWaitForEmptyEventLoop'),
httpErrorHandler: require('./httpErrorHandler'),
Expand Down