@koopjs/cache-memory
Advanced tools
Comparing version 4.0.1 to 5.0.0
{ | ||
"name": "@koopjs/cache-memory", | ||
"version": "4.0.1", | ||
"version": "5.0.0", | ||
"description": "An in-memory cache for KOop", | ||
@@ -41,7 +41,9 @@ "main": "src/index.js", | ||
"tap-spec": "^5.0.0", | ||
"tape": "^5.0.0" | ||
"tape": "^5.0.0", | ||
"uuid": "^9.0.0" | ||
}, | ||
"dependencies": { | ||
"@alloc/quick-lru": "^5.2.0", | ||
"lodash": "^4.17.4" | ||
} | ||
} |
163
README.md
@@ -5,4 +5,17 @@ # Koop Memory Cache | ||
This is a LRU cache with ttl (time to live) expiry. It expects cache items to be GeoJSON feature collections. It is based on [quick-lru](https://github.com/sindresorhus/quick-lru). | ||
Cache eviction occurs in two ways: | ||
1. if the cache grows to a number of items greater than the cache's defined size, the least recently inserted or accessed item is evicted | ||
2. if a item is accessed and it has gone past its ttl period, it is evicted | ||
## Usage | ||
### Stand-alone instantiation: | ||
```js | ||
const Cache = require('@koopjs/cache-memory'); | ||
const cache = new Cache({ size: 1000 }); | ||
``` | ||
### As a Koop cache plugin | ||
This is the default cache for [Koop](https://github.com/koopjs/koop) so you won't need to instantiate it yourself. If you really wanted to, it would look like this: | ||
@@ -14,4 +27,6 @@ | ||
const cache = require('@koopjs/cache-memory') | ||
koop.register(cache) | ||
koop.register(cache, { size: 1000 }) | ||
``` | ||
#### Options | ||
`size`: the maximum number of items to store in the queue before evicting the least recently used item. | ||
@@ -22,5 +37,4 @@ ## Cache API | ||
### `insert` | ||
Insert geojson into the cache | ||
Insert geojson into the cache. | ||
Note: A metadata entry will be created automatically. It can be populated from an object on the inserted geojson. | ||
@@ -31,3 +45,3 @@ ```js | ||
features: [], | ||
metadata: { // Metadata is an arbitrary object that will be stored in the catalog under the same key as the geojson | ||
metadata: { | ||
name: 'Example GeoJSON', | ||
@@ -39,68 +53,21 @@ description: 'This is geojson that will be stored in the cache' | ||
const options = { | ||
ttl: 1000 // The TTL option is measured in seconds, it will be used to set the `expires` field in the catalog entry | ||
ttl: 1000 // The TTL option is measured in seconds, it will be used to set the `maxAge` property in the LRU cache | ||
} | ||
cache.insert('key', geojson, options, err => { | ||
// This function will call back with an error if there is already data in the cache using the same key | ||
// This function will call back with an error if one occurs | ||
}) | ||
``` | ||
### `append` | ||
Add features to an existing geojson feature collection in the cache | ||
Note: | ||
### `retrieve` | ||
Retrieve a cached feature collection. | ||
```js | ||
const geojson = { | ||
type: 'FeatureCollection', | ||
features: [] | ||
} | ||
cache.append('key', geojson, err => { | ||
// This function will call back with an error if the cache key does not exist | ||
}) | ||
``` | ||
### `update` | ||
Update a cached feature collection with new features. | ||
This will completely replace the features that are in the cache, but the metadata doc will remain in the catalog. | ||
```js | ||
const geojson = { | ||
type: 'FeatureCollection', | ||
features: [] | ||
} | ||
const options = { | ||
ttl: 1000 | ||
} | ||
cache.update('key', geojson, options, err => { | ||
// This function will call back with an error if the cache key does not exist | ||
}) | ||
``` | ||
pick: [] // an array of keys used to return a subset of the feature collections root level properties | ||
omit | ||
} | ||
### `upsert` | ||
Update a cached feature collection with new features or insert if the features are not there. | ||
```js | ||
const geojson = { | ||
type: 'FeatureCollection', | ||
features: [] | ||
} | ||
const options = { | ||
ttl: 1000 | ||
} | ||
cache.upsert('key', geojson, options, err => { | ||
// This function will call back with an error if the cache key does not exist | ||
}) | ||
``` | ||
### `retrieve` | ||
Retrieve a cached feature collection | ||
```js | ||
const options = {} // For now there are no options on retrieve. This is meant for compatibility with the general cache API | ||
cache.retrieve('key', options, (err, geojson) => { | ||
/* This function will call back with an error if there is no geojson in the cache | ||
The geojson returned will contain the metadata document from the catalog | ||
/* This function will callback with an error or the data cached with the passed key. It will return undefined if not found or expired. | ||
{ | ||
@@ -120,83 +87,7 @@ type: 'FeatureCollection', | ||
cache.delete('key', err => { | ||
// This function will call back with an error if there was nothing to delete | ||
// This function will call back with an error if one occurs | ||
}) | ||
``` | ||
### `createStream` | ||
Create a stream of features from the cache | ||
```js | ||
cache.createStream('key', options) | ||
.pipe(/* do something with the stream of geojson features emitted one at a time */) | ||
``` | ||
## Catalog API | ||
The catalog stores metadata about items that are in the cache. | ||
### `catalog.insert` | ||
Add an arbitrary metadata document to the cache. | ||
Note: This is called automatically by `insert` | ||
```js | ||
const metadata = { | ||
name: 'Standalone metadata', | ||
status: 'Processing', | ||
description: 'Metadata that is not attached to any other data in the cache' | ||
} | ||
cache.catalog.insert('key', metadata, err => { | ||
// this function will call back with an error if there is already a metadata document using the same key | ||
}) | ||
``` | ||
### `catalog.update` | ||
Update a metadata entry | ||
```js | ||
const original = { | ||
name: 'Doc', | ||
status: 'Processing' | ||
} | ||
cache.catalog.insert('key', original) | ||
const update = { | ||
status: 'Cached' | ||
} | ||
cache.catalog.update('key', update, err => { | ||
// this function will call back with an error if there is no metadata in the catalog using that key | ||
}) | ||
cache.catalog.retrieve('key', (err, metadata) => { | ||
/* | ||
Updates are merged into the existing metadata document | ||
Return value will be: | ||
{ | ||
name: 'Doc', | ||
status: 'Cached' | ||
} | ||
*/ | ||
}) | ||
``` | ||
### `catalog.retrieve` | ||
Retrieve a metadata entry from the catalog | ||
```js | ||
cache.catalog.retrieve('key', (err, metadata) => { | ||
// This function will call back with an error if there is no metadata stored under the given key | ||
// Or else it will call back with the stored metadata doc | ||
}) | ||
``` | ||
### `catalog.delete` | ||
Remove a catalog entry from the catalog | ||
Note: This cannot be called if data is in the cache under the same key | ||
```js | ||
cache.catalog.delete('key', err => { | ||
// This function will call back with an error if there is nothing to delete or if there is still data in the cache using the same key | ||
}) | ||
``` | ||
[npm-img]: https://img.shields.io/npm/v/@koopjs/cache-memory.svg?style=flat-square | ||
[npm-url]: https://www.npmjs.com/package/@koopjs/cache-memory |
171
src/index.js
@@ -1,10 +0,5 @@ | ||
const EventEmitter = require('events'); | ||
const _ = require('lodash'); | ||
const { asCachableGeojson } = require('./helper'); | ||
const Readable = require('stream').Readable; | ||
const LRUCache = require('@alloc/quick-lru'); | ||
// Convenience to make callbacks optional in most functions | ||
function noop() {} | ||
class Cache extends EventEmitter { | ||
class Cache { | ||
static pluginName = 'Memory Cache'; | ||
@@ -14,148 +9,62 @@ static type = 'cache'; | ||
constructor() { | ||
super(); | ||
this.featuresStore = new Map(); | ||
this.catalogStore = new Map(); | ||
} | ||
#cache; | ||
insert(key, geojson, options = {}, callback = noop) { | ||
if (this.featuresStore.has(key)) { | ||
return callback(new Error('Cache key is already in use')); | ||
} | ||
// Store features separately from rest of geojson | ||
const { features, ...rest } = asCachableGeojson(geojson); | ||
this.featuresStore.set(key, features); | ||
this.catalogInsert(key, rest, options, callback); | ||
constructor(options) { | ||
this.#cache = new LRUCache({ maxSize: options?.size || 500 }); | ||
} | ||
catalogInsert(key, catalogEntry, options = {}, callback = noop) { | ||
if (this.catalogStore.has(key)) { | ||
return callback(new Error('Catalog key is already in use')); | ||
} | ||
const clonedEntry = _.cloneDeep(catalogEntry); | ||
_.set(clonedEntry, '_cache.updated', Date.now()); | ||
if (options.ttl) { | ||
_.set(clonedEntry, '_cache.expires', Date.now() + options.ttl * 1000); | ||
} | ||
this.catalogStore.set(key, clonedEntry); | ||
callback(); | ||
insert(key, geojson, options, callback) { | ||
this.#cache.set(); | ||
this.#cache.set(key, normalizeGeojson(geojson), { | ||
maxAge: calculateMaxAge(options?.ttl), | ||
}); | ||
callback(null); | ||
} | ||
update(key, geojson, options = {}, callback = noop) { // eslint-disable-line | ||
if (!this.featuresStore.has(key)) { | ||
return callback(new Error('Resource not found')); | ||
} | ||
const { features, ...rest } = asCachableGeojson(geojson); | ||
retrieve(key, options, callback) { | ||
const cacheEntry = this.#cache.get(key); | ||
this.featuresStore.set(key, features); | ||
const existingCatalogEntry = this.catalogStore.get(key); | ||
const catalogEntry = rest || existingCatalogEntry; | ||
this.catalogUpdate(key, catalogEntry, options, callback); | ||
} | ||
upsert(key, geojson, options = {}, callback = noop) { | ||
if (this.featuresStore.has(key)) { | ||
this.update(key, geojson, options, callback); | ||
} else { | ||
this.insert(key, geojson, options, callback); | ||
if (!cacheEntry) { | ||
return callback(null); | ||
} | ||
} | ||
append(key, geojson, options = {}, callback = noop) { // eslint-disable-line | ||
const { features } = asCachableGeojson(geojson); | ||
const existingFeatures = this.featuresStore.get(key); | ||
const appendedFeatureArray = existingFeatures.concat(features); | ||
this.featuresStore.set(key, appendedFeatureArray); | ||
this.catalogUpdate(key, { | ||
cache: { | ||
updated: Date.now(), | ||
}, | ||
}); | ||
callback(); | ||
} | ||
let data = cacheEntry; | ||
retrieve(key, options, callback = noop) { | ||
if (!this.featuresStore.has(key)) { | ||
return callback(new Error('Resource not found')); | ||
if (options?.pick) { | ||
data = _.pick(data, options.pick); | ||
} else if (options?.omit) { | ||
data = _.omit(data, options.omit); | ||
} | ||
const features = this.featuresStore.get(key); | ||
const geojsonWrapper = this.catalogStore.get(key); | ||
const geojson = { ...geojsonWrapper, features }; | ||
callback(null, geojson); | ||
return geojson; | ||
callback(null, data); | ||
} | ||
createStream(key, options = {}) { // eslint-disable-line | ||
const features = this.featuresStore.get(key); | ||
return Readable.from(features); | ||
delete(key, callback) { | ||
this.#cache.delete(key); | ||
callback(null); | ||
} | ||
} | ||
delete(key, callback = noop) { | ||
if (!this.featuresStore.has(key)) { | ||
return callback(new Error('Resource not found')); | ||
} | ||
this.featuresStore.delete(key); | ||
const catalogEntry = this.catalogStore.get(key); | ||
this.catalogStore.set(key, { | ||
...catalogEntry, | ||
_cache: { | ||
status: 'deleted', | ||
updated: Date.now(), | ||
}, | ||
}); | ||
callback(); | ||
function normalizeGeojson(geojson) { | ||
if (geojson === undefined || geojson === null || Array.isArray(geojson)) { | ||
return { | ||
type: 'FeatureCollection', | ||
features: geojson || [], | ||
metadata: {}, | ||
}; | ||
} | ||
catalogUpdate = function (key, update, options = {}, callback = noop) { // eslint-disable-line | ||
if (!this.catalogStore.has(key)) { | ||
return callback(new Error('Resource not found')); | ||
} | ||
const existingCatalogEntry = this.catalogStore.get(key); | ||
const catalogEntry = { | ||
...existingCatalogEntry, | ||
..._.cloneDeep(update), | ||
}; | ||
catalogEntry._cache.updated = Date.now(); | ||
geojson.type = geojson.type || 'FeatureCollection'; | ||
geojson.features = geojson.features || []; | ||
return _.cloneDeep(geojson); | ||
} | ||
if (options.ttl) { | ||
catalogEntry._cache.expires = Date.now() + options.ttl * 1000; | ||
} | ||
this.catalogStore.set(key, catalogEntry); | ||
callback(); | ||
}; | ||
catalogRetrieve(key, callback = noop) { | ||
if (!this.catalogStore.has(key)) { | ||
return callback(new Error('Resource not found')); | ||
} | ||
const catalogEntry = this.catalogStore.get(key); | ||
callback(null, catalogEntry); | ||
return catalogEntry; | ||
function calculateMaxAge(ttl) { | ||
if (!ttl) { | ||
return; | ||
} | ||
catalogDelete(key, callback = noop) { | ||
if (this.featuresStore.has(key)) { | ||
return callback( | ||
new Error('Cannot delete catalog entry while data is still in cache') | ||
); | ||
} | ||
this.catalogStore.delete(key); | ||
callback(); | ||
} | ||
return Date.now() + ttl * 1000; | ||
} | ||
module.exports = Cache; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
5515
2
3
4
54
87
+ Added@alloc/quick-lru@^5.2.0
+ Added@alloc/quick-lru@5.2.0(transitive)