From d21845d051cd68642793b215f668676eb82bfdce Mon Sep 17 00:00:00 2001 From: vinikjkkj Date: Thu, 19 Dec 2024 12:01:37 -0300 Subject: [PATCH] improve caches and tests --- package.json | 4 +- src/caches/cache.ts | 218 +++++++++++++++++++++--------------------- src/caches/pool.ts | 224 ++++++++++++++++++++++---------------------- src/caches/ttl.ts | 4 +- tests/pool.test.ts | 108 +++++++++++---------- tests/ttl.test.ts | 210 +++++++++++++++++++++-------------------- 6 files changed, 394 insertions(+), 374 deletions(-) diff --git a/package.json b/package.json index 57bf8b7..3d88e19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cachetools-js", - "version": "1.0.2", + "version": "1.0.3", "description": "Best, faster, ligther, all-in-one, fully-typed cache library for JS, based on cachetools from python, with events, pooling and decorators.", "author": "vinikjkkj", "license": "MIT", @@ -12,7 +12,7 @@ "prepare": "tsc", "lint": "eslint src", "lint:fix": "eslint src --fix", - "test": "jest", + "test": "jest --detectOpenHandles", "publish": "npm run build && npm publish" }, "devDependencies": { diff --git a/src/caches/cache.ts b/src/caches/cache.ts index c0aca57..387d235 100644 --- a/src/caches/cache.ts +++ b/src/caches/cache.ts @@ -1,107 +1,111 @@ -import { CacheEmitter, SizeError } from '../utils' -import { CacheParams, Keyable } from '../types' - -/** - * ### About - * Simple cache base class, based on `Proxy`, which allows getting and setting keys like a regular object. - * - * This class is useful if you want to create custom cache logic. - * - * It can also be used for personal purposes, although its specific application might not be obvious in all cases. - * - * ### Example - * ```typescript - * const cache = new Cache({maxsize: 2}) - * - * //store some keys - * cache['foo'] = 'bar' - * cache['bar'] = 'foo' - * - * //get 'bar' key - * cache['bar'] - * ``` -*/ -export class Cache extends CacheEmitter { - protected _cache: Map - protected _params: CacheParams - - [key: string | symbol]: unknown - - /** - * Creates a new Cache. - */ - constructor(params: CacheParams = {}){ - super() - this._cache = new Map() - this._params = params - - return new Proxy(this, { - get: (target, key) => { - if ((target as any)[key]) { - return (target as any)[key] - } else { - return target.get(key) - } - }, - set: (target, key, value) => { - if (typeof key === 'string' && key.startsWith('_')) { - (target as any)[key] = value - } else { - target.set(key, value) - } - - return true - } - }); - } - - get(key: Keyable){ - this.emit('get', key) - return this._cache.get(key) - } - - set(key: Keyable, value: unknown){ - this.emit('set', { key, value }) - if ( - this._params.maxsize && - this.length() === this._params.maxsize - ){ - throw new SizeError() - } - this._cache.set(key, this._params.useClones ? - structuredClone(value) : - value - ) - } - - take(key: Keyable){ - const value = this.get(key) - this.del(key) - return value - } - - del(key: Keyable){ - this.emit('del', key) - this._cache.delete(key) - } - - flushAll(){ - this.delAll() - } - - delAll(){ - this._cache.clear() - } - - keys(){ - return Array.from(this._cache.keys()) - } - - values(){ - return Array.from(this._cache.values()) - } - - length(){ - return this._cache.size - } -} +import { CacheEmitter, SizeError } from '../utils' +import { CacheParams, Keyable } from '../types' + +/** + * ### About + * Simple cache base class, based on `Proxy`, which allows getting and setting keys like a regular object. + * + * This class is useful if you want to create custom cache logic. + * + * It can also be used for personal purposes, although its specific application might not be obvious in all cases. + * + * ### Example + * ```typescript + * const cache = new Cache({maxsize: 2}) + * + * //store some keys + * cache['foo'] = 'bar' + * cache['bar'] = 'foo' + * + * //get 'bar' key + * cache['bar'] + * ``` +*/ +export class Cache extends CacheEmitter { + protected _cache: Map + protected _params: CacheParams + + [key: string | symbol]: unknown + + /** + * Creates a new Cache. + */ + constructor(params: CacheParams = {}){ + super() + this._cache = new Map() + this._params = params + + return new Proxy(this, { + get: (target, key) => { + if ((target as any)[key]) { + return (target as any)[key] + } else { + return target.get(key) + } + }, + set: (target, key, value) => { + if (typeof key === 'string' && key.startsWith('_')) { + (target as any)[key] = value + } else { + target.set(key, value) + } + + return true + } + }); + } + + get(key: Keyable){ + this.emit('get', key) + return this._cache.get(key) + } + + set(key: Keyable, value: unknown){ + this.emit('set', { key, value }) + if ( + this._params.maxsize && + this.length() === this._params.maxsize + ){ + throw new SizeError() + } + this._cache.set(key, this._params.useClones ? + structuredClone(value) : + value + ) + } + + take(key: Keyable){ + const value = this.get(key) + this.del(key) + return value + } + + del(key: Keyable){ + this.emit('del', key) + this._cache.delete(key) + } + + flushAll(){ + this.delAll() + } + + delAll(){ + this._cache.clear() + } + + keys(){ + return Array.from(this._cache.keys()) + } + + values(){ + return Array.from(this._cache.values()) + } + + length(){ + return this._cache.size + } + + destroy() { + this.delAll() + } +} diff --git a/src/caches/pool.ts b/src/caches/pool.ts index 3e7b832..3dc7c01 100644 --- a/src/caches/pool.ts +++ b/src/caches/pool.ts @@ -1,110 +1,114 @@ -import { AlreadyExists, CacheNotExists, CachePoolEmitter, CacheTypeNotExists, SizeError } from '../utils' -import { CacheLike, CachePoolParams, CachesObj, CacheTypes, Keyable, ParamsLike } from '../types' - -/** - * ### About - * Pool of caches to store all your caches in a single variable, making your code cleaner and easier to manage. - * - * You can create, get, and delete all types of caches, using standard cache methods more simply. - * - * ### Example - * ```typescript - * const caches = new CachePool({maxsize: 2}) - * - * //create some caches - * caches.createCache('AwesomeTTL', 'ttl', {ttl: 100}) - * caches.createCache('AwesomeLRU', 'lru', {maxsize: 4}) - * - * //store some keys in caches - * caches.set('AwesomeTTL', 'foo', 'bar') - * caches.set('AwesomeLRU', 'bar', 'foo') - * - * //get keys in caches - * cache['baz'] = 'foo' - * //throws SizeError, you need to delete some key to store another key - * - * ``` -*/ -export class CachePool extends CachePoolEmitter { - protected _caches: Map - protected _params: CachePoolParams - - [key: string | symbol]: unknown - - constructor(params: CachePoolParams = {}){ - super() - this._caches = new Map() - this._params = params - } - - protected _getCache(cacheName: Keyable){ - const cache = this._caches.get(cacheName) - - if (!cache) { - throw new CacheNotExists() - } - - return cache - } - - createCache(name: Keyable, type: CacheTypes, params?: ParamsLike){ - if (!CachesObj[type]) { - throw new CacheTypeNotExists() - } - - if (this._caches.has(name)) { - throw new AlreadyExists() - } - - if (this._params.maxsize && this.length() === this._params.maxsize) { - throw new SizeError() - } - - this.emit('create-cache', { name, type }) - const cache: CacheLike = new CachesObj[type](params || {}) - this._caches.set( - name, - cache - ) - - return cache - } - - getCache(name: Keyable){ - this.emit('get-cache', name) - return this._caches.get(name) - } - - delCache(name: Keyable){ - this.emit('del-cache', name) - this._caches.delete(name) - } - - delAllCaches(){ - this._caches.clear() - } - - get(cacheName: Keyable, key: Keyable){ - const cache = this._getCache(cacheName) - return cache.get(key) - } - - set(cacheName: Keyable, key: Keyable, value: unknown){ - const cache = this._getCache(cacheName) - cache.set(key, value) - } - - del(cacheName: Keyable, key: Keyable){ - const cache = this._getCache(cacheName) - cache.del(key) - } - - delAll(cacheName: Keyable){ - const cache = this._getCache(cacheName) - cache.delAll() - } - - length(){ - return this._caches.size - } -} +import { AlreadyExists, CacheNotExists, CachePoolEmitter, CacheTypeNotExists, SizeError } from '../utils' +import { CacheLike, CachePoolParams, CachesObj, CacheTypes, Keyable, ParamsLike } from '../types' + +/** + * ### About + * Pool of caches to store all your caches in a single variable, making your code cleaner and easier to manage. + * + * You can create, get, and delete all types of caches, using standard cache methods more simply. + * + * ### Example + * ```typescript + * const caches = new CachePool({maxsize: 2}) + * + * //create some caches + * caches.createCache('AwesomeTTL', 'ttl', {ttl: 100}) + * caches.createCache('AwesomeLRU', 'lru', {maxsize: 4}) + * + * //store some keys in caches + * caches.set('AwesomeTTL', 'foo', 'bar') + * caches.set('AwesomeLRU', 'bar', 'foo') + * + * //get keys in caches + * cache['baz'] = 'foo' + * //throws SizeError, you need to delete some key to store another key + * + * ``` +*/ +export class CachePool extends CachePoolEmitter { + protected _caches: Map + protected _params: CachePoolParams + + [key: string | symbol]: unknown + + constructor(params: CachePoolParams = {}){ + super() + this._caches = new Map() + this._params = params + } + + protected _getCache(cacheName: Keyable){ + const cache = this._caches.get(cacheName) + + if (!cache) { + throw new CacheNotExists() + } + + return cache + } + + createCache(name: Keyable, type: CacheTypes, params?: ParamsLike){ + if (!CachesObj[type]) { + throw new CacheTypeNotExists() + } + + if (this._caches.has(name)) { + throw new AlreadyExists() + } + + if (this._params.maxsize && this.length() === this._params.maxsize) { + throw new SizeError() + } + + this.emit('create-cache', { name, type }) + const cache: CacheLike = new CachesObj[type](params || {}) + this._caches.set( + name, + cache + ) + + return cache + } + + getCache(name: Keyable){ + this.emit('get-cache', name) + return this._caches.get(name) + } + + delCache(name: Keyable){ + this.emit('del-cache', name) + const cache = this._caches.get(name) + if (cache) cache.destroy() + + this._caches.delete(name) + } + + delAllCaches(){ + this._caches.forEach(v => v.destroy()) + this._caches.clear() + } + + get(cacheName: Keyable, key: Keyable){ + const cache = this._getCache(cacheName) + return cache.get(key) + } + + set(cacheName: Keyable, key: Keyable, value: unknown){ + const cache = this._getCache(cacheName) + cache.set(key, value) + } + + del(cacheName: Keyable, key: Keyable){ + const cache = this._getCache(cacheName) + cache.del(key) + } + + delAll(cacheName: Keyable){ + const cache = this._getCache(cacheName) + cache.delAll() + } + + length(){ + return this._caches.size + } +} diff --git a/src/caches/ttl.ts b/src/caches/ttl.ts index 7c34942..d7e8bdd 100644 --- a/src/caches/ttl.ts +++ b/src/caches/ttl.ts @@ -69,6 +69,7 @@ export class TTLCache extends Cache { del(key: Keyable){ super.del(key) + this.emit('expired', key) if (this._timeouts.has(key)) { this._timeouts.delete(key) } @@ -77,10 +78,12 @@ export class TTLCache extends Cache { private _clearInterval() { if (this._intervalId) { clearInterval(this._intervalId) + this._intervalId = undefined } } destroy() { + super.destroy() this._clearInterval() } @@ -89,7 +92,6 @@ export class TTLCache extends Cache { for (const [key, expirationTime] of this._timeouts.entries()) { if (expirationTime <= now) { this.del(key) - this.emit('expired', key) } } } diff --git a/tests/pool.test.ts b/tests/pool.test.ts index bd810c8..9529cae 100644 --- a/tests/pool.test.ts +++ b/tests/pool.test.ts @@ -1,53 +1,57 @@ -import { CachePool, FIFOCache, LFUCache, LRUCache, RRCache, TTLCache } from '../src' -import { AlreadyExists } from '../src/utils' - -describe('CachePool', () => { - let caches: CachePool - - beforeEach(() => { - caches = new CachePool({maxsize: 10}) - }) - - test('should create a cache and get it', () => { - const cache = caches.createCache('c1', 'ttl', {ttl: 10}) - - expect(caches.getCache('c1')).toBe(cache) - }) - - test('should create a cache with corretly instances', () => { - expect( - caches.createCache('c1', 'ttl', {ttl: 10}) - ).toBeInstanceOf(TTLCache) - - expect( - caches.createCache('c2', 'fifo', {maxsize: 10}) - ).toBeInstanceOf(FIFOCache) - - expect( - caches.createCache('c3', 'lru', {maxsize: 10}) - ).toBeInstanceOf(LRUCache) - - expect( - caches.createCache('c4', 'lfu', {maxsize: 10}) - ).toBeInstanceOf(LFUCache) - - expect( - caches.createCache('c5', 'rr', {maxsize: 10}) - ).toBeInstanceOf(RRCache) - }) - - test('should throw AlreadyExists error', () => { - caches.createCache('c1', 'ttl', {ttl: 10}) - expect( - () => caches.createCache('c1', 'ttl', {ttl: 10}) - ).toThrow(AlreadyExists) - }) - - test('should delete a cache and update length', () => { - caches.createCache('c1', 'ttl', {ttl: 10}) - caches.delCache('c1') - - expect(caches.length()).toBe(0) - expect(caches.getCache('c1')).toBeUndefined() - }) +import { CachePool, FIFOCache, LFUCache, LRUCache, RRCache, TTLCache } from '../src' +import { AlreadyExists } from '../src/utils' + +describe('CachePool', () => { + let caches: CachePool + + beforeEach(() => { + caches = new CachePool({maxsize: 10}) + }) + + afterEach(() => { + caches.delAllCaches() + }) + + test('should create a cache and get it', () => { + const cache = caches.createCache('c1', 'ttl', {ttl: 10}) + + expect(caches.getCache('c1')).toBe(cache) + }) + + test('should create a cache with corretly instances', () => { + expect( + caches.createCache('c1', 'ttl', {ttl: 10}) + ).toBeInstanceOf(TTLCache) + + expect( + caches.createCache('c2', 'fifo', {maxsize: 10}) + ).toBeInstanceOf(FIFOCache) + + expect( + caches.createCache('c3', 'lru', {maxsize: 10}) + ).toBeInstanceOf(LRUCache) + + expect( + caches.createCache('c4', 'lfu', {maxsize: 10}) + ).toBeInstanceOf(LFUCache) + + expect( + caches.createCache('c5', 'rr', {maxsize: 10}) + ).toBeInstanceOf(RRCache) + }) + + test('should throw AlreadyExists error', () => { + caches.createCache('c1', 'ttl', {ttl: 10}) + expect( + () => caches.createCache('c1', 'ttl', {ttl: 10}) + ).toThrow(AlreadyExists) + }) + + test('should delete a cache and update length', () => { + caches.createCache('c1', 'ttl', {ttl: 10}) + caches.delCache('c1') + + expect(caches.length()).toBe(0) + expect(caches.getCache('c1')).toBeUndefined() + }) }) \ No newline at end of file diff --git a/tests/ttl.test.ts b/tests/ttl.test.ts index 5aac6a6..9077790 100644 --- a/tests/ttl.test.ts +++ b/tests/ttl.test.ts @@ -1,103 +1,109 @@ -import { TTLCache, SizeError } from '../src' - -describe('TTLCache', () => { - let cache: TTLCache - - beforeEach(() => { - cache = new TTLCache({maxsize: 2}) - }) - - test('should set and get value correctly', () => { - cache.set('key', 'value') - expect(cache['key']).toBe('value') - }) - - test('should delete a key correctly', () => { - cache.set('key', 'value') - let key = cache['key'] - - cache.del('key') - expect(key).not.toBe(cache['key']) - }) - - test('should take a key correctly', () => { - cache.set('key', 'value') - let key = cache.take('key') - - expect(key).not.toBe(cache['key']) - }) - - test('should expire keys after TTL', (done) => { - cache.set('key', 'value', 100) - setTimeout(() => { - expect(cache.get('key')).toBeUndefined() - done() - }, 150) - }) - - test('should throw SizeError when exceeding maxsize', () => { - cache.set('key', 'value') - cache.set('key1', 'value') - - expect(() => { - cache.set('key2', 'value') - }).toThrow(SizeError) - }) - - test('should delete all keys', () => { - cache.set('key', 'value') - cache.set('key1', 'value') - - cache.delAll() - - expect(cache.length()).toBe(0) - }) - - test('should clear timeout on delete', (done) => { - cache.set('key', 'value', 100) - cache.del('key') - setTimeout(() => { - expect(cache.get('key')).toBeUndefined() - done() - }, 150) - }) - - test('should return keys and values', () => { - cache.set('key', 'value') - - expect(cache.keys()).toBeInstanceOf(Array) - expect(cache.keys().length).toBe(cache.length()) - - expect(cache.values()).toBeInstanceOf(Array) - expect(cache.values().length).toBe(cache.length()) - }) - - test('should emit events correctly', () => { - //set event - const setMock = jest.fn() - cache.on('set', setMock) - cache['key'] = 'value' - expect(setMock).toHaveBeenCalledTimes(1) - - //get event - const getMock = jest.fn() - cache.on('get', getMock) - cache['key'] - expect(getMock).toHaveBeenCalledTimes(1) - - //del event - const delMock = jest.fn() - cache.on('del', delMock) - cache.del('key') - expect(delMock).toHaveBeenCalledTimes(1) - - //expired event - const expireMock = jest.fn() - cache.on('expired', expireMock) - - cache.set('key1', 'value', 100) - setTimeout(() => { - expect(expireMock).toHaveBeenCalledTimes(1) - }, 200) - }) +import { TTLCache, SizeError } from '../src' + +describe('TTLCache', () => { + let cache: TTLCache + + beforeEach(() => { + cache = new TTLCache({maxsize: 2, checkPeriod: 15}) + }) + + afterEach(() => { + cache.destroy() + }, 10) + + test('should set and get value correctly', () => { + cache.set('key', 'value') + expect(cache['key']).toBe('value') + }) + + test('should delete a key correctly', () => { + cache.set('key', 'value') + let key = cache['key'] + + cache.del('key') + expect(key).not.toBe(cache['key']) + }) + + test('should take a key correctly', () => { + cache.set('key', 'value') + let key = cache.take('key') + + expect(key).not.toBe(cache['key']) + }) + + test('should expire keys after TTL', (done) => { + cache.set('key', 'value', 20) + setTimeout(() => { + expect(cache.get('key')).toBeUndefined() + done() + }, 20) + }) + + test('should throw SizeError when exceeding maxsize', () => { + cache.set('key', 'value') + cache.set('key1', 'value') + + expect(() => { + cache.set('key2', 'value') + }).toThrow(SizeError) + }) + + test('should delete all keys', () => { + cache.set('key', 'value') + cache.set('key1', 'value') + + cache.delAll() + + expect(cache.length()).toBe(0) + }) + + test('should clear timeout on delete', (done) => { + cache.set('key', 'value', 100) + cache.del('key') + setTimeout(() => { + expect(cache.get('key')).toBeUndefined() + done() + }, 150) + }) + + test('should return keys and values', () => { + cache.set('key', 'value') + + expect(cache.keys()).toBeInstanceOf(Array) + expect(cache.keys().length).toBe(cache.length()) + + expect(cache.values()).toBeInstanceOf(Array) + expect(cache.values().length).toBe(cache.length()) + }) + + test('should emit events correctly', (done) => { + //jest.useFakeTimers(); + //set event + const setMock = jest.fn() + cache.on('set', setMock) + cache['key'] = 'value' + expect(setMock).toHaveBeenCalledTimes(1) + + //get event + const getMock = jest.fn() + cache.on('get', getMock) + cache['key'] + expect(getMock).toHaveBeenCalledTimes(1) + + //del event + const delMock = jest.fn() + cache.on('del', delMock) + cache.del('key') + expect(delMock).toHaveBeenCalledTimes(1) + + //expired event + const expireMock = jest.fn() + cache.on('expired', expireMock) + cache.set('key1', 'value', 100) + + setTimeout(() => { + expect(expireMock).toHaveBeenCalledTimes(1) + done() + }, 150) + }, 10000) }) \ No newline at end of file