├── .gitignore ├── .npmrc ├── .travis.yml ├── LICENSE ├── README.md ├── lib └── index.js ├── package-lock.json ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | { 2 | language: "node_js", 3 | node_js: ["8"], 4 | os: "osx" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Federico Miras 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micro-cacheable 2 | [![NPM version](https://img.shields.io/npm/v/micro-cacheable.svg)](https://www.npmjs.com/package/micro-cacheable) 3 | [![Build Status](https://travis-ci.org/fmiras/micro-cacheable.svg?branch=master)](https://travis-ci.org/fmiras/micro-cacheable) 4 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 5 | [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/xojs/xo) 6 | [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/micro) 7 | [![Greenkeeper badge](https://badges.greenkeeper.io/fmiras/micro-cacheable.svg)](https://greenkeeper.io/) 8 | 9 | micro-cacheable is an utility for data caching focused on [micro framework](https://github.com/zeit/micro). The problem this package solves is to save the already requested data in-memory to have it available for a configurated time without processing it again. 10 | 11 | ## Usage 12 | 13 | ```bash 14 | cd my-micro-project/ 15 | npm install --save micro-cacheable 16 | ``` 17 | 18 | and add use the package like this: 19 | 20 | ```javascript 21 | // index.js 22 | const cache = require('micro-cacheable') 23 | 24 | const microFn = (req, res) => { 25 | return new Date() 26 | } 27 | 28 | module.exports = cache(60 * 60 * 1000, microFn) 29 | ``` 30 | 31 | then just run your microservice normally and it will return the same result for an hour (first param as miliseconds) unless that you change the request url. 32 | 33 | #### A more useful example: 34 | 35 | Let's say that we need a microservice that receives a name (string) and search data of a person on 3 or 4 APIs: 36 | ```javascript 37 | const { parse } = require('url') 38 | const fetch = require('node-fetch') 39 | 40 | module.exports = async (req, res) => { 41 | const { searchName } = parse(req.url, true).query 42 | const facebookData = await fetch('https://someapi1.com') 43 | const githubData = await fetch('https://someapi2.com') 44 | const financialData = await fetch('https://someapi3.com') 45 | return { facebookData, githubData, financialData } 46 | } 47 | ``` 48 | 49 | This microservice would fetch 3 APIs every time it receives a request. Probably, in some cases, if the microservice receive the same name it will return the same data, at least for the same day, so you can just add micro-cacheable like this: 50 | ```javascript 51 | const { parse } = require('url') 52 | const fetch = require('node-fetch') 53 | const cache = require('micro-cacheable') 54 | 55 | const microFn = async (req, res) => { 56 | const { searchName } = parse(req.url, true).query 57 | const facebookData = await fetch('https://someapi1.com') 58 | const githubData = await fetch('https://someapi2.com') 59 | const financialData = await fetch('https://someapi3.com') 60 | return { facebookData, githubData, financialData } 61 | } 62 | 63 | module.exports = cache(24 * 60 * 60 * 1000, microFn) // One day data caching 64 | ``` 65 | ### Mongo Support 66 | micro-cacheable supports Mongo (and hopefully Redis in the future) because in-memory data cache can't scale horizontally and if you work with microservices you will loose that advantage. So you can avoid that problem setting the `MONGO_URL` and `MONGO_DB` enviroment variables so all your microservice's instances use the same cache. 67 | 68 | ## Why? 69 | I worked on a project with micro using it for making web-scrapping workers that take too long the first time to get the data, and users requested often the same data so with this I can save a lot of requests, processing and time making requests of +5000ms only take 50ms. 70 | 71 | ## Contributing 72 | 73 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device 74 | 2. Link the package to the global module directory: `npm link` 75 | 3. Within the module you want to test your local development instance of micro-cacheable, just link it to the dependencies: `npm link micro-cacheable`. Instead of the default one from npm, node will now use your clone of micro-cacheable! 76 | 77 | ## Credits 78 | 79 | Thanks to [ZEIT](https://zeit.co) Team for giving us [micro](https://github.com/zeit/micro) to make our life easier! 80 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { MongoClient } = require('mongodb') 2 | 3 | const responses = [] 4 | const { MONGO_URL, MONGO_DB } = process.env 5 | 6 | const retrieveDataMongo = async url => { 7 | const connection = await MongoClient.connect(MONGO_URL) 8 | const db = connection.db(MONGO_DB) 9 | const collection = db.collection('responses') 10 | const lastCall = await collection.findOne({ url }) 11 | connection.close() 12 | return lastCall 13 | } 14 | 15 | const retrieveData = async url => { 16 | if (MONGO_URL && MONGO_DB) { 17 | return retrieveDataMongo(url) 18 | } 19 | 20 | return responses[url] 21 | } 22 | 23 | const saveDataMongo = async (url, { data, date }) => { 24 | const connection = await MongoClient.connect(MONGO_URL) 25 | const db = connection.db(MONGO_DB) 26 | const collection = await db.collection('responses') 27 | await collection.remove({ url }) 28 | const lastCall = await collection.insert({ url, data, date }) 29 | connection.close() 30 | return lastCall 31 | } 32 | 33 | const saveData = async (url, data) => { 34 | if (MONGO_URL && MONGO_DB) { 35 | return saveDataMongo(url, data) 36 | } 37 | 38 | responses[url] = data 39 | return data 40 | } 41 | 42 | module.exports = (ms, microFunction) => async (req, res) => { 43 | const lastCall = await retrieveData(req.url) 44 | if (lastCall && lastCall.date > new Date()) { 45 | return lastCall.data 46 | } 47 | const data = await microFunction(req, res) 48 | const date = new Date(new Date().getTime() + ms) 49 | saveData(req.url, { data, date }) 50 | return data 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micro-cacheable", 3 | "version": "2.0.1", 4 | "description": "A micro utility for data caching", 5 | "author": "fmiras", 6 | "license": "MIT", 7 | "repository": "fmiras/micro-cacheable", 8 | "bugs": "https://github.com/fmiras/micro-cacheable/issues", 9 | "homepage": "https://github.com/fmiras/micro-cacheable", 10 | "main": "lib/index.js", 11 | "files": [ 12 | "lib" 13 | ], 14 | "scripts": { 15 | "test": "xo && nyc ava", 16 | "precommit": "lint-staged" 17 | }, 18 | "lint-staged": { 19 | "*.js": [ 20 | "xo", 21 | "prettier --single-quote --no-semi --write", 22 | "git add" 23 | ] 24 | }, 25 | "xo": { 26 | "extends": "prettier" 27 | }, 28 | "keywords": [ 29 | "micro", 30 | "cache", 31 | "microservices", 32 | "micro-cache", 33 | "micro-cacheable" 34 | ], 35 | "devDependencies": { 36 | "ava": "1.2.1", 37 | "husky": "1.3.0", 38 | "nyc": "13.3.0", 39 | "lint-staged": "8.1.0", 40 | "prettier": "1.16.4", 41 | "sinon": "7.2.4", 42 | "sleep": "6.0.0", 43 | "xo": "0.22.0" 44 | }, 45 | "dependencies": { 46 | "mongodb": "3.2.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const sinon = require('sinon') 3 | const { sleep } = require('sleep') 4 | 5 | const cache = require('../lib') 6 | 7 | test('cache(...args) execute the handler twice with 3 requests', async t => { 8 | const handler = sinon.stub() 9 | handler.onCall(0).returns(1) 10 | handler.onCall(1).returns(2) 11 | 12 | const microFn = await cache(1000, handler) 13 | const req = { url: 'https://someapi.com' } 14 | 15 | t.is(1, await microFn(req)) 16 | t.is(1, await microFn(req)) 17 | await sleep(1) 18 | t.is(2, await microFn(req)) 19 | t.is(2, handler.callCount) 20 | }) 21 | 22 | test('cache(...args) execute the handler if different url', async t => { 23 | const handler = sinon.stub() 24 | handler.onCall(0).returns(1) 25 | handler.onCall(1).returns(2) 26 | 27 | const microFn = cache(1000, handler) 28 | 29 | t.is(1, await microFn({ url: 'https://someapi.com/resource/1' })) 30 | t.is(2, await microFn({ url: 'https://someapi.com/resource/2' })) 31 | }) 32 | --------------------------------------------------------------------------------