UNPKG

koa-redis-cache

Version:

a middleware for koa to cache response with redis.

190 lines (162 loc) 4.39 kB
'use strict' const { pathToRegexp } = require('path-to-regexp') const wrapper = require('co-redis') const readall = require('readall') const crypto = require('crypto') const Redis = require('redis') module.exports = function(options = {}) { let redisAvailable = false const { prefix = 'koa-redis-cache:', expire = 30 * 60, // 30 min routes = ['(.*)'], exclude = [], passParam = '', maxLength = Infinity, onerror = function() {} } = options const { host:redisHost = 'localhost', port:redisPort = 6379, url:redisUrl = `redis://${redisHost}:${redisPort}/`, options:redisOptions = {} } = options.redis || {} /** * redisClient */ const redisClient = wrapper(Redis.createClient(redisUrl, redisOptions)) redisClient.on('error', (error)=> { redisAvailable = false onerror(error) }) redisClient.on('end', () => { redisAvailable = false }) redisClient.on('connect', () => { redisAvailable = true }) return async function cache(ctx, next) { const { url, path } = ctx.request const resolvedPrefix = typeof prefix === 'function' ? prefix.call(ctx, ctx) : prefix; const key = resolvedPrefix + md5(url) const tkey = key + ':type' let match = false let routeExpire = false for (let i = 0; i < routes.length; i++) { let route = routes[i] if (typeof routes[i] === 'object') { route = routes[i].path routeExpire = routes[i].expire } if (paired(route, path)) { match = true break } } for (let j = 0; j < exclude.length; j++) { if (paired(exclude[j], path)) { match = false break } } if (!redisAvailable || !match || (passParam && ctx.request.query[passParam]) || (ctx.request.method !== 'GET')) { return await next() } let ok = false try { ok = await getCache(ctx, key, tkey) } catch (e) { ok = false } if (ok) { return } await next() try { let trueExpire = routeExpire || expire await setCache(ctx, key, tkey, trueExpire) } catch (e) {} routeExpire = false } /** * getCache */ async function getCache(ctx, key, tkey) { let value = await redisClient.get(key) let type let ok = false if (value) { ctx.response.status = 200 type = (await redisClient.get(tkey)) || 'text/html' // can happen if user specified return_buffers: true in redis options if (Buffer.isBuffer(type)) type = type.toString() ctx.response.set('X-Koa-Redis-Cache', 'true') ctx.response.type = type ctx.response.body = value ok = true } return ok } /** * setCache */ async function setCache(ctx, key, tkey, expire) { let body = ctx.response.body if ((ctx.request.method !== 'GET') || (ctx.response.status !== 200) || !body) { return } if (typeof body === 'string') { // string if (Buffer.byteLength(body) > maxLength) return await redisClient.setex(key, expire, body) } else if (Buffer.isBuffer(body)) { // buffer if (body.length > maxLength) return await redisClient.setex(key, expire, body) } else if (typeof body === 'object' && ctx.response.type === 'application/json') { // json body = JSON.stringify(body) if (Buffer.byteLength(body) > maxLength) return await redisClient.setex(key, expire, body) } else if (typeof body.pipe === 'function') { // stream body = await read(body) ctx.response.body = body if (Buffer.byteLength(body) > maxLength) return await redisClient.setex(key, expire, body) } else { return } await cacheType(ctx, tkey, expire) } /** * cacheType */ async function cacheType(ctx, tkey, expire) { let type = ctx.response.type if (type) { await redisClient.setex(tkey, expire, type) } } } function paired(route, path) { let options = { sensitive: true, strict: true, } return pathToRegexp(route, [], options).exec(path) } function read(stream) { return new Promise((resolve, reject) => { readall(stream, (err, data) => { if (err) { reject(err) } else { resolve(data) } }) }) } function md5(str) { return crypto.createHash('md5').update(str).digest('hex') }