Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More

@koopjs/cache-memory

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@koopjs/cache-memory - npm Package Compare versions

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"
}
}

@@ -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

@@ -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;