Skip to content

Commit

Permalink
node-cache - NodeCacheStore (#788)
Browse files Browse the repository at this point in the history
* node-cache - NodeCacheStore

* adding in NodeCacheStore

* version bump to v1.0.0
  • Loading branch information
jaredwray authored Sep 17, 2024
1 parent 0880eca commit 7abf431
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 6 deletions.
41 changes: 41 additions & 0 deletions packages/node-cache/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Note: `NodeCache` is ready and available for use. `NodeCacheStore` is in progres
* [Getting Started](#getting-started)
* [Basic Usage](#basic-usage)
* [Advanced Usage](#advanced-usage)
* [NodeCacheStore](#nodecachestore)
* [API](#api)
* [How to Contribute](#how-to-contribute)
* [License and Copyright](#license-and-copyright)
Expand Down Expand Up @@ -60,6 +61,46 @@ await cache.get('foo'); // 'bar'
cache.getStats(); // {hits: 1, misses: 1, keys: 1, ksize: 2, vsize: 3}
```

## NodeCacheStore

The `NodeCacheStore` is a class that extends the `NodeCache` and adds the ability to use storage adapters. This is based on the `cacheable` engine and allows you to do layer 1 and layer 2 caching. The storage adapters are based on the [Keyv](https://keyv.org) package. This allows you to use any of the storage adapters that are available.

```javascript
import {NodeCacheStore} from '@cacheable/node-cache';

const cache = new NodeCacheStore();
cache.set('foo', 'bar');
cache.get('foo'); // 'bar'
```

### NodeCacheStoreOptions

When initializing the cache you can pass in the options below:

```javascript
export type NodeCacheStoreOptions = {
ttl?: number; // The standard ttl as number in seconds for every generated cache element. 0 = unlimited
primary?: Keyv; // The primary storage adapter
secondary?: Keyv; // The secondary storage adapter
maxKeys?: number; // Default is 0 (unlimited). If this is set it will throw and error if you try to set more keys than the max.
};
```

### Node Cache Store API

* `set(key: string | number, value: any, ttl?: number): Promise<boolean>` - Set a key value pair with an optional ttl (in milliseconds). Will return true on success. If the ttl is not set it will default to 0 (no ttl)
* `mset(data: Array<NodeCacheItem>): Promise<boolean>` - Set multiple key value pairs at once
* `get(key: string | number): Promise<any>` - Get a value from the cache by key
* `mget(keys: Array<string | number>): Promise<Record<string, unknown>>` - Get multiple values from the cache by keys
* `del(key: string | number): Promise<boolean>` - Delete a key
* `mdel(keys: Array<string | number>): Promise<boolean>` - Delete multiple keys
* `clear(): Promise<void>` - Clear the cache
* `stats`: `NodeCacheStats` - Get the stats of the cache
* `ttl`: `number` - The standard ttl as number in seconds for every generated cache element. 0 = unlimited
* `primary`: `Keyv` - The primary storage adapter
* `secondary`: `Keyv` - The secondary storage adapter
* `maxKeys`: `number` - If this is set it will throw and error if you try to set more keys than the max

## API

### `constructor(options?: NodeCacheOptions)`
Expand Down
4 changes: 2 additions & 2 deletions packages/node-cache/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cacheable/node-cache",
"version": "0.8.0",
"version": "1.0.0",
"description": "Simple and Maintained fast NodeJS internal caching",
"type": "module",
"main": "./dist/index.cjs",
Expand Down Expand Up @@ -43,7 +43,7 @@
"xo": "^0.59.3"
},
"dependencies": {
"cacheable": "^1.1.0",
"cacheable": "^1.2.0",
"eventemitter3": "^5.0.1",
"keyv": "^5.0.1"
},
Expand Down
1 change: 1 addition & 0 deletions packages/node-cache/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,4 @@ export default class NodeCache extends eventemitter {
}
}

export {NodeCacheStore, type NodeCacheStoreOptions} from './store.js';
91 changes: 88 additions & 3 deletions packages/node-cache/src/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Cacheable, CacheableMemory} from 'cacheable';
import {Cacheable, CacheableMemory, type CacheableItem} from 'cacheable';
import {Keyv} from 'keyv';
import {type NodeCacheItem} from 'index.js';

export type NodeCacheStoreOptions = {
ttl?: number;
Expand All @@ -14,23 +15,107 @@ export class NodeCacheStore {
constructor(options?: NodeCacheStoreOptions) {
if (options) {
const cacheOptions = {
ttl: options.ttl,
primary: options.primary,
secondary: options.secondary,
};

this._cache = new Cacheable(cacheOptions);

if (options.maxKeys) {
this._maxKeys = options.maxKeys;
if (this._maxKeys > 0) {
this._cache.stats.enabled = true;
}
}

this._cache = new Cacheable(cacheOptions);
}
}

public get cache(): Cacheable {
return this._cache;
}

public get ttl(): number | undefined {
return this._cache.ttl;
}

public set ttl(ttl: number | undefined) {
this._cache.ttl = ttl;
}

public get primary(): Keyv {
return this._cache.primary;
}

public set primary(primary: Keyv) {
this._cache.primary = primary;
}

public get secondary(): Keyv | undefined {
return this._cache.secondary;
}

public set secondary(secondary: Keyv | undefined) {
this._cache.secondary = secondary;
}

public get maxKeys(): number {
return this._maxKeys;
}

public set maxKeys(maxKeys: number) {
this._maxKeys = maxKeys;
if (this._maxKeys > 0) {
this._cache.stats.enabled = true;
}
}

public async set(key: string | number, value: any, ttl?: number): Promise<boolean> {
if (this._maxKeys > 0) {
console.log(this._cache.stats.count, this._maxKeys);
if (this._cache.stats.count >= this._maxKeys) {
return false;
}
}

const finalTtl = ttl ?? this._cache.ttl;

await this._cache.set(key.toString(), value, finalTtl);
return true;
}

public async mset(list: NodeCacheItem[]): Promise<void> {
const items = new Array<CacheableItem>();
for (const item of list) {
items.push({key: item.key.toString(), value: item.value, ttl: item.ttl});
}

await this._cache.setMany(items);
}

public async get<T>(key: string | number): Promise<T | undefined> {
return this._cache.get(key.toString());
}

public async mget<T>(keys: Array<string | number>): Promise<Record<string, T | undefined>> {
const result: Record<string, T | undefined> = {};
for (const key of keys) {
// eslint-disable-next-line no-await-in-loop
result[key.toString()] = await this._cache.get(key.toString());
}

return result;
}

public async del(key: string | number): Promise<boolean> {
return this._cache.delete(key.toString());
}

public async mdel(keys: Array<string | number>): Promise<boolean> {
return this._cache.deleteMany(keys.map(key => key.toString()));
}

public async clear(): Promise<void> {
return this._cache.clear();
}
}
1 change: 0 additions & 1 deletion packages/node-cache/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,6 @@ describe('NodeCache', () => {
cache.set('foo', 'bar', 100);
cache.set('baz', 'qux', 0);
cache.set('moo', 'moo', 0.5);
console.log(cache.store.get('foo'));
expect(cache.get('foo')).toBe('bar');
expect(cache.get('baz')).toBe('qux');
await sleep(600);
Expand Down
115 changes: 115 additions & 0 deletions packages/node-cache/test/store.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {describe, test, expect} from 'vitest';
import {Keyv} from 'keyv';
import {NodeCacheStore} from '../src/store.js';

// eslint-disable-next-line no-promise-executor-return
const sleep = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

describe('NodeCacheStore', () => {
test('should create a new instance', () => {
const store = new NodeCacheStore();
Expand All @@ -12,4 +16,115 @@ describe('NodeCacheStore', () => {
store.maxKeys = 200;
expect(store.maxKeys).toBe(200);
});
test('should set a ttl', () => {
const store = new NodeCacheStore({ttl: 100});
expect(store.ttl).toBe(100);
store.ttl = 200;
expect(store.ttl).toBe(200);
});
test('should set a primary keyv store', () => {
const store = new NodeCacheStore();
expect(store.primary).toBeDefined();
const keyv = new Keyv();
store.primary = keyv;
expect(store.primary).toBe(keyv);
});
test('should set a secondary keyv store', () => {
const store = new NodeCacheStore();
expect(store.secondary).toBeUndefined();
const keyv = new Keyv();
store.secondary = keyv;
expect(store.secondary).toBe(keyv);
});
test('should be able to get and set primary and secondary keyv stores', async () => {
const store = new NodeCacheStore();
expect(store.primary).toBeDefined();
expect(store.secondary).toBeUndefined();
const primary = new Keyv();
const secondary = new Keyv();
store.primary = primary;
store.secondary = secondary;
expect(store.primary).toBe(primary);
expect(store.secondary).toBe(secondary);
await store.set('test', 'value');
const restult1 = await store.get('test');
expect(restult1).toBe('value');
await store.set('test', 'value', 100);
const restult2 = await store.get('test');
expect(restult2).toBe('value');
await sleep(200);
const restult3 = await store.get('test');
expect(restult3).toBeUndefined();
});
test('should set a maxKeys limit', async () => {
const store = new NodeCacheStore({maxKeys: 3});
expect(store.maxKeys).toBe(3);
expect(store.cache.stats.enabled).toBe(true);
await store.set('test1', 'value1');
await store.set('test2', 'value2');
await store.set('test3', 'value3');
await store.set('test4', 'value4');
const result1 = await store.get('test4');
expect(result1).toBeUndefined();
});
test('should clear the cache', async () => {
const store = new NodeCacheStore();
await store.set('test', 'value');
await store.clear();
const result1 = await store.get('test');
expect(result1).toBeUndefined();
});
test('should delete a key', async () => {
const store = new NodeCacheStore();
await store.set('test', 'value');
await store.del('test');
const result1 = await store.get('test');
expect(result1).toBeUndefined();
});
test('should be able to get and set an object', async () => {
const store = new NodeCacheStore();
await store.set('test', {foo: 'bar'});
const result1 = await store.get('test');
expect(result1).toEqual({foo: 'bar'});
});
test('should be able to get and set an array', async () => {
const store = new NodeCacheStore();
await store.set('test', ['foo', 'bar']);
const result1 = await store.get('test');
expect(result1).toEqual(['foo', 'bar']);
});
test('should be able to get and set a number', async () => {
const store = new NodeCacheStore();
await store.set('test', 123);
const result1 = await store.get('test');
expect(result1).toBe(123);
});
test('should be able to set multiple keys', async () => {
const store = new NodeCacheStore();
await store.mset([
{key: 'test1', value: 'value1'},
{key: 'test2', value: 'value2'},
]);
const result1 = await store.get('test1');
const result2 = await store.get('test2');
expect(result1).toBe('value1');
expect(result2).toBe('value2');
});
test('should be able to get multiple keys', async () => {
const store = new NodeCacheStore();
await store.set('test1', 'value1');
await store.set('test2', 'value2');
const result1 = await store.mget(['test1', 'test2']);
expect(result1).toEqual({test1: 'value1', test2: 'value2'});
});
test('should be able to delete multiple keys', async () => {
const store = new NodeCacheStore();
await store.set('test1', 'value1');
await store.set('test2', 'value2');
await store.mdel(['test1', 'test2']);
const result1 = await store.get('test1');
const result2 = await store.get('test2');
expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
});
});

0 comments on commit 7abf431

Please sign in to comment.