sequelize-simple-cache
Advanced tools
Comparing version 1.0.0-beta.11 to 1.0.0-beta.12
{ | ||
"name": "sequelize-simple-cache", | ||
"version": "1.0.0-beta.11", | ||
"version": "1.0.0-beta.12", | ||
"description": "A simple, transparent, client-side, in-memory cache for Sequelize", | ||
@@ -5,0 +5,0 @@ "main": "src/SequelizeSimpleCache.js", |
@@ -105,2 +105,13 @@ # sequelize-simple-cache | ||
### Limit | ||
This cache meant as a simple in-memory read cache for a very limited amount of data. | ||
So, you should be able to control the size of the cache. | ||
```javascript | ||
const cache = new SequelizeSimpleCache({ | ||
User: { }, // default limit is 50 | ||
Page: { limit: 30 }, | ||
}); | ||
``` | ||
### Logging | ||
@@ -110,3 +121,3 @@ | ||
Logging goes to `console.debug()` unless you set `delegate` to log somewhere else. | ||
`event` is one of: `init`, `hit`, `miss`, `load` or `ops`. | ||
`event` is one of: `init`, `hit`, `miss`, `load`, `purge` or `ops`. | ||
```javascript | ||
@@ -113,0 +124,0 @@ const cache = new SequelizeSimpleCache({ |
@@ -10,7 +10,12 @@ const md5 = require('md5'); | ||
methods: ['findById', 'findOne', 'findAll', 'findAndCountAll', 'count', 'min', 'max', 'sum'], | ||
limit: 50, | ||
}; | ||
this.config = Object.entries(config) | ||
.reduce((acc, [type, { ttl = defaults.ttl, methods = defaults.methods }]) => ({ | ||
.reduce((acc, [type, { | ||
ttl = defaults.ttl, | ||
methods = defaults.methods, | ||
limit = defaults.limit, | ||
}]) => ({ | ||
...acc, | ||
[type]: { ttl, methods }, | ||
[type]: { ttl, methods, limit }, | ||
}), {}); | ||
@@ -25,4 +30,6 @@ const { | ||
this.delegate = delegate; | ||
this.cache = new Map(); | ||
this.stats = { hit: 0, miss: 0, load: 0 }; | ||
this.cache = {}; | ||
this.stats = { | ||
hit: 0, miss: 0, load: 0, purge: 0, | ||
}; | ||
if (this.ops > 0) { | ||
@@ -53,4 +60,9 @@ this.heart = setInterval(() => { | ||
if (!config) return model; // no caching for this model | ||
const { ttl, methods } = config; | ||
this.log('init', { type, ttl, methods }); | ||
const { ttl, methods, limit } = config; | ||
// create map for model | ||
const cache = new Map(); | ||
this.cache[type] = cache; | ||
this.log('init', { | ||
type, ttl, methods, limit, | ||
}); | ||
// proxy for intercepting Sequelize methods | ||
@@ -65,3 +77,3 @@ return new Proxy(model, { | ||
const hash = md5(key); | ||
const item = this.cache.get(hash); | ||
const item = cache.get(hash); | ||
if (item) { // hit | ||
@@ -80,4 +92,12 @@ const { data, expires } = item; | ||
const expires = Date.now() + ttl * 1000; | ||
this.cache.set(hash, { data, expires, type }); | ||
cache.set(hash, { data, expires }); | ||
this.log('load', { key, hash, expires }); | ||
if (cache.size > limit) { // check cache limit | ||
let oldest = {}; | ||
cache.forEach(({ expires: e }, h) => { | ||
if (!oldest.h || e < oldest.e) oldest = { h, e }; | ||
}); | ||
cache.delete(oldest.h); | ||
this.log('purge', { hash: oldest.h, expires: oldest.e }); | ||
} | ||
} | ||
@@ -100,20 +120,24 @@ return data; // resolve from database | ||
clear(...modelnames) { | ||
if (!modelnames.length) { | ||
this.cache.clear(); | ||
return; | ||
} | ||
this.cache.forEach(({ type }, key) => { | ||
if (modelnames.includes(type)) { | ||
this.cache.delete(key); | ||
} | ||
const types = modelnames.length ? modelnames : Object.keys(this.cache); | ||
types.forEach((type) => { | ||
const cache = this.cache[type]; | ||
if (!cache) return; | ||
cache.clear(); | ||
}); | ||
} | ||
size(...modelnames) { | ||
const types = modelnames.length ? modelnames : Object.keys(this.cache); | ||
return types | ||
.filter(type => this.cache[type]) | ||
.reduce((acc, type) => acc + this.cache[type].size, 0); | ||
} | ||
log(event, details = {}) { | ||
// stats | ||
if (['hit', 'miss', 'load'].includes(event)) { | ||
if (['hit', 'miss', 'load', 'purge'].includes(event)) { | ||
this.stats[event] += 1; | ||
} | ||
// logging | ||
if (!this.debug && ['init', 'hit', 'miss', 'load'].includes(event)) return; | ||
if (!this.debug && ['init', 'hit', 'miss', 'load', 'purge'].includes(event)) return; | ||
this.delegate(event, { | ||
@@ -123,3 +147,4 @@ ...details, | ||
ratio: this.stats.hit / (this.stats.hit + this.stats.miss), | ||
size: this.cache.size, | ||
size: Object.entries(this.cache) | ||
.reduce((acc, [type, map]) => ({ ...acc, [type]: map.size }), {}), | ||
}); | ||
@@ -126,0 +151,0 @@ } |
@@ -129,7 +129,7 @@ const chai = require('chai'); | ||
const result2 = await Page.findOne({ where: { foo: true } }); | ||
expect(cache.cache.size).to.be.equal(2); | ||
expect(cache.size()).to.be.equal(2); | ||
cache.clear('User'); | ||
expect(result1).to.be.deep.equal({ username: 'fred' }); | ||
expect(result2).to.be.deep.equal({ foo: true }); | ||
expect(cache.cache.size).to.be.equal(1); | ||
expect(cache.size()).to.be.equal(1); | ||
}); | ||
@@ -386,2 +386,20 @@ | ||
}); | ||
it('should ensure limit is not exceeded', async () => { | ||
const model = { | ||
name: 'User', | ||
findOne: async () => ({ username: 'fred' }), | ||
}; | ||
const cache = new SequelizeSimpleCache({ User: { limit: 3 } }, { ops: false }); | ||
const User = cache.init(model); | ||
await User.findOne({ where: { username: 'john' } }); | ||
await new Promise(resolve => setTimeout(() => resolve(), 16)); | ||
await User.findOne({ where: { username: 'jim' } }); | ||
await new Promise(resolve => setTimeout(() => resolve(), 16)); | ||
await User.findOne({ where: { username: 'bob' } }); | ||
await new Promise(resolve => setTimeout(() => resolve(), 16)); | ||
expect(cache.size()).to.be.equal(3); | ||
await User.findOne({ where: { username: 'ron' } }); | ||
expect(cache.size()).to.be.equal(3); | ||
}); | ||
}); |
28726
512
158