Skip to content

Commit

Permalink
Feature/cache middleware (#80)
Browse files Browse the repository at this point in the history
* first draft implementation of cache middleware

* Added improved tests

* Added documentation

* Fixes after review

* Version bump
  • Loading branch information
lmammino committed Jan 13, 2018
1 parent e02a9e6 commit 09cf110
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 6 deletions.
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "middy",
"version": "0.6.3",
"version": "0.6.4",
"description": "The simple (but cool 😎) middleware engine for AWS lambda in Node.js",
"main": "src/index.js",
"types": "./typescript/middy.d.ts",
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

0 comments on commit 09cf110

Please sign in to comment.