{"_id":"@antoniovdlc/cache","name":"@antoniovdlc/cache","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@antoniovdlc/cache","version":"1.0.0","description":"A simple, yet over-engineered, cache","main":"dist/index.cjs.js","module":"dist/index.esm.js","types":"dist/index.d.ts","scripts":{"prepare":"husky install","type:check":"tsc --noEmit","lint":"eslint {src,test}/*","lint:fix":"eslint --fix {src,test}/*","format":"prettier --write --ignore-unknown {src,test}/*","format:check":"prettier --check {src,test}/*","test":"vitest run --coverage","pre-commit":"lint-staged","prebuild":"rimraf dist && mkdir dist","build":"npm run build:types && npm run build:lib","build:types":"tsc --declaration --emitDeclarationOnly --outDir dist","build:lib":"rollup -c","postversion":"git push && git push --tags"},"keywords":["cache","lru"],"author":{"name":"Antonio Villagra De La Cruz"},"repository":{"type":"git","url":"git+https://github.com/AntonioVdlC/cache.git"},"bugs":{"url":"https://github.com/AntonioVdlC/cache/issues"},"homepage":"https://github.com/AntonioVdlC/cache#readme","license":"MIT","devDependencies":{"@rollup/plugin-typescript":"^11.1.6","@typescript-eslint/eslint-plugin":"^6.21.0","@typescript-eslint/parser":"^6.21.0","@vitest/coverage-v8":"^1.2.0","eslint":"^8.56.0","eslint-config-prettier":"^9.1.0","husky":"^8.0.3","lint-staged":"^15.2.0","prettier":"^3.2.1","rimraf":"^5.0.5","rollup":"^2.79.1","rollup-plugin-terser":"^7.0.2","tslib":"^2.6.2","typescript":"^5.3.3","vite":"^5.0.11","vitest":"^1.2.0"},"_id":"@antoniovdlc/cache@1.0.0","gitHead":"69422e910175df416f2f413698568dfa2067accb","_nodeVersion":"20.11.1","_npmVersion":"10.2.4","dist":{"integrity":"sha512-uW0kXfiBP1EOtTVYVAl7XaN5DhbS1W8B3aKNzQkFvZ+6ZOjxPc3JaIFfMb4LFGavNuaX37KfyCFVoE+8+QKOrQ==","shasum":"f57f5126d4f7d4484305714ed73d8bc515ee8357","tarball":"https://registry.npmjs.org/@antoniovdlc/cache/-/cache-1.0.0.tgz","fileCount":6,"unpackedSize":25621,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDUI34kXdog1KJpzspXXxPtUWHiX3DFBON1KqzT0fk5OAIhAO86nGjaFapFsJiE+7aISTyYTLKiPmXtfLof/Z8fc90k"}]},"_npmUser":{"name":"antoniovdlc","email":"antonio.villagra.de.la.cruz@gmail.com"},"directories":{},"maintainers":[{"name":"antoniovdlc","email":"antonio.villagra.de.la.cruz@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/cache_1.0.0_1708761273109_0.528700709168276"},"_hasShrinkwrap":false}},"time":{"created":"2024-02-24T07:54:33.001Z","1.0.0":"2024-02-24T07:54:33.394Z","modified":"2024-02-24T07:54:34.109Z"},"maintainers":[{"name":"antoniovdlc","email":"antonio.villagra.de.la.cruz@gmail.com"}],"description":"A simple, yet over-engineered, cache","homepage":"https://github.com/AntonioVdlC/cache#readme","keywords":["cache","lru"],"repository":{"type":"git","url":"git+https://github.com/AntonioVdlC/cache.git"},"author":{"name":"Antonio Villagra De La Cruz"},"bugs":{"url":"https://github.com/AntonioVdlC/cache/issues"},"license":"MIT","readme":"# Cache\n\n[![version](https://img.shields.io/npm/v/@antoniovdlc/cache.svg)](http://npm.im/@antoniovdlc/cache)\n[![issues](https://img.shields.io/github/issues-raw/antoniovdlc/cache.svg)](https://github.com/AntonioVdlC/cache/issues)\n[![downloads](https://img.shields.io/npm/dt/@antoniovdlc/cache.svg)](http://npm.im/@antoniovdlc/cache)\n[![license](https://img.shields.io/npm/l/@antoniovdlc/cache.svg)](http://opensource.org/licenses/MIT)\n\nA simple, yet over-engineered, cache.\n\n## Installation\n\nThis package is distributed via npm:\n\n```\nnpm install @antoniovdlc/cache\n```\n\n## Motivation\n\nI was just writing a blog post on implementing an LRU cache, and I ended up implementing this monstruosity. Well, here it is!\n\n\n## TL;DR\n\nBy default, the cache behaves as a least-recently-used (LRU) cache.\n\n```ts\nconst cache = new Cache<string, number>(2);\ncache.put(\"a\", 1);\ncache.put(\"b\", 2);\ncache.get(\"a\"); // 1\ncache.put(\"c\", 3); // `b` is evicted\n```\n\n_You can check the `test` folder for even more examples!_\n\n## Methods\n\n### Instantiating a cache\n\nTo instantiate a cache, call `new` on the class, and pass it a `capacity` value.\n```ts\nconst cache = new Cache<string, number>(2);\n```\n\n#### options\n\nAn optional `options` object can be passed as a second argument.\n```ts\ntype CacheOptions<K, V> = {\n  persistence?: CachePersistence<K, V>;\n  autoPersist?: boolean;\n  ttl?: number;\n  ttlCleanupInterval?: number;\n  evictionPolicy?: CacheEvictionPolicy<K, V>;\n};\n```\nThe use of those options is explained further down this document.\n\n### Inserting data\n\nTo insert data, use the `put` method.\n\n#### put(key: K, value: V, ttl?: number): void\n```ts\nconst cache = new Cache<string, number>(2);\ncache.put(\"a\", 1);\n```\n\n### Retrieving data\n\nTo retrieve date, use either the `get` or the `peek` methods. To check that data is present in the cache, use the `has` method.\n\n#### get(key: K): V | undefined\n```ts\nconst cache = new Cache<string, number>(2);\ncache.put(\"a\", 1);\ncache.get(\"a\"); // 1\n```\n> Note that calling this method marks the retrieved item as most recently used.\n\n#### peek(key: K): V | undefined\n```ts\nconst cache = new Cache<string, number>(2);\ncache.put(\"a\", 1);\ncache.peek(\"a\"); // 1\n```\n> Note that calling this method does not mark the retrieved item as most recently used.\n\n#### has(key: K): boolean\n```ts\nconst cache = new Cache<string, number>(2);\ncache.put(\"a\", 1);\ncache.has(\"a\"); // true\n```\n\n### Removal of data\n\nData is automatically evicted from the cache when at capacity and inserting new data based on the eviction policy (by default, LRU). Some methods do allow for manual removal.\n\n#### remove(key: K): void\n```ts\nconst cache = new Cache<string, number>(2);\ncache.put(\"a\", 1);\ncache.has(\"a\"); // true\ncache.remove(\"a\");\ncache.has(\"a\"); // false\n```\n\n#### clear(): void\n```ts\nconst cache = new Cache<string, number>(2);\ncache.put(\"a\", 1);\ncache.put(\"b\", 2);\ncache.size; // 2\ncache.clear();\ncache.size; // 0\n```\n\n### Resizing\n\n#### resize(capacity: number): void\n\nIt is possible to resize the cache after initialization.\n\n```ts\nconst cache = new Cache<string, number>(2);\ncache.put(\"a\", 1);\ncache.put(\"b\", 2);\ncache.size; // 2\ncache.resize(1);\ncache.size; //1\n```\n\n> Note that if the new `capacity` is lesser than the current size of the cache, items will be evicted according to the eviction policy until the cache size is no longer greater than its capacity.\n\n## Statistics\n\nTo introspect the cache, the following statistics are calculated:\n\n```ts\ntype CacheStats = {\n  hitRate: number;\n  missRate: number;\n  evictionRate: number;\n  effectiveness: number;\n};\n```\n\n### .stats\n\nStats are accessible via the `stats` getter.\n```ts\nconst cache = new Cache<string, number>(2);\ncache.stats;\n```\n\n## Callbacks and Events\n\nCallbacks and events allow attaching custom logic to _interesting_ cache events.\n\n```ts\nenum CacheEvent {\n  Insertion = \"insertion\",\n  Eviction = \"eviction\",\n  Removal = \"removal\",\n  Full = \"full\",\n  Empty = \"empty\",\n}\n```\n\nEvent handlers may receive an item from the cache depending on the event.\n\n```ts\ntype CacheEventHandler<K, V> = (\n  event: CacheEvent,\n  item?: { key: K; value: V },\n) => void;\n```\n\n### on(event: CacheEvent, callback: CacheEventHandler<K, V>): CacheEventCallback\n\nTo attach a handler to an event, use the `on` method.\n```ts\nconst cache = new Cache<string, number>(1);\ncache.on(CacheEvent.Insertion, (item) => console.log(item));\ncache.put(\"a\", 1); // { key: \"a\", value: 1 }\n```\n\nThe `on` method returns an object with an `unregister` method, which can be called to remove the handler.\n```ts\nconst cache = new Cache<string, number>(2);\nconst { unregister } = cache.on(CacheEvent.Insertion, (item) => console.log(item));\ncache.put(\"a\", 1); // { key: \"a\", value: 1 }\nunregister();\ncache.put(\"b\", 2);\n```\n\n## Persistence\n\nPersistence can be added to the cache with an instance of `CachePersistence<K, V>`.\n\n```ts\nclass CachePersistence<K, V> {\n  constructor(\n      cacheKey: string = uuidv4(),\n      logic?: { persist: (cache: Map<K, V>) => void; restore: () => Map<K, V> },\n    ) { ... }\n}\n```\n> By default, the persistence will use `localStorage`.\n\nThen this instance can be passed to the cache either in the constructor, or later in a setter.\n\n```ts\nconst persistence = new CachePersistence<string, number>();\nconst cache = new Cache<string, number>(2, { persistence });\n```\nor\n```ts\nconst cache = new Cache<string, number>(2);\ncache.persistence = new CachePersistence<string, number>();\n```\n\nAuto-persistence can be turned on by passing an `autoPersist` option to the cache constructor. It is off by default.\n\n```ts\nconst persistence = new CachePersistence<string, number>();\nconst cache = new Cache<string, number>(2, { persistence, autoPersist: true });\n```\nor\n```ts\nconst persistence = new CachePersistence<string, number>();\nconst cache = new Cache<string, number>(2, { persistence });\ncache.autoPersist = true;\n```\n\n## TTL\n\nOptionally, the cache takes into account time-to-live (TTL) for its items.\n\nIt can be set as a default by providing a `ttl` option to the cache constructor or via a setter.\n\n```ts\nconst cache = new Cache<string, number>(2, { ttl: 1234 });\n```\nor\n```ts\nconst cache = new Cache<string, number>(2);\ncache.ttl = 1234;\n```\n\nThe cache implements a reactive cleanup, meaning that items are evicted from the cache on cache operations (`get`, `peek`, `has`).\n\nIt can also be provided on a per-item basis in the `put` method.\n```ts\nconst cache = new Cache<string, number>(2, { ttl: 1234 });\ncache.put(\"a\", 1, 4321);\n```\n\nOptionally, an internal clock can be set to wipe expired items from the cache at given intervals (proactive cleanup). This can complement the default reactive cleanup.\n\n```ts\nconst cache = new Cache<string, number>(1, {\n  ttl: 50,\n  ttlCleanupInterval: 100,\n});\ncache.put(\"a\", 1);\nawait _for(150);\ncache.has(\"a\"); // false\n```\n\n## Custom Eviction Policies\n\nFinally, the cache allows for custom eviction policies.\n\nThis can be used, for example, to implement an MRU (most recently used) cache:\n```ts\nconst cache = new Cache<string, number>(2, {\n  evictionPolicy: (cache) => cache.last!,\n});\n```\n\n> By default, if no `evictionPolicy` is passed, the cache implements an LRU (least recently used) policy.\n\n### FIFO, LIFO, RR\n\nIt is possible to implement other types of cache policies by inheriting and making small tweaks to the base `Cache` class.\n\nFor example, for FIFO (first in, first out), LIFO (last in, last out) and RR (random) cache policies, we can create a `CacheWithList` class and pass different eviction policies.\n\n```ts\nclass CacheWithList<K, V> extends Cache<K, V> {\n  list?: K[];\n\n  constructor(capacity: number, options?: CacheOptions<K, V>) {\n    super(capacity, options);\n    this.list = [];\n  }\n\n  put(key: K, value: V, ttl?: number) {\n    super.put(key, value, ttl);\n    this.list!.push(key);\n  }\n}\n```\n\n```ts\n// FIFO\nconst cache = new CacheWithList<string, number>(2, {\n  evictionPolicy: (cache: CacheWithList<string, number>) =>\n    cache.list!.shift()!,\n});\n\n// LIFO\nconst cache = new CacheWithList<string, number>(2, {\n  evictionPolicy: (cache: CacheWithList<string, number>) =>\n    cache.list!.pop()!,\n});\n\n// RR\nconst cache = new CacheWithList<string, number>(2, {\n  evictionPolicy: (cache: CacheWithList<string, number>) => {\n    const index = Math.floor(Math.random() * cache.list!.length);\n    const key = cache.list![index];\n\n    cache.list!.splice(index, 1);\n\n    return key;\n  },\n});\n```\n\n\n### LFU, MFU\n\nSimilarly, we can also implement frequency-based cache policies.\n\n```ts\nclass CacheWithFrequencies<K, V> extends Cache<K, V> {\n  frequencies?: Map<K, number>;\n\n  constructor(capacity: number, options?: CacheOptions<K, V>) {\n    super(capacity, options);\n    this.frequencies = new Map();\n  }\n\n  get(key: K): V | undefined {\n    const value = super.get(key);\n    if (value) {\n      this.frequencies!.set(key, (this.frequencies!.get(key) || 0) + 1);\n    } else {\n      this.frequencies!.delete(key);\n    }\n    return value;\n  }\n\n  put(key: K, value: V, ttl?: number) {\n    super.put(key, value, ttl);\n    this.frequencies!.set(key, 0);\n  }\n\n  remove(key: K) {\n    super.remove(key);\n    this.frequencies!.delete(key);\n  }\n}\n```\n\n```ts\n// LFU\nconst cache = new CacheWithFrequencies<string, number>(2, {\n  evictionPolicy: (cache: CacheWithFrequencies<string, number>) => {\n    let min = Infinity;\n    let lfu = \"\";\n    for (const [key, freq] of cache.frequencies!) {\n      if (freq < min) {\n        min = freq;\n        lfu = key;\n      }\n    }\n    return lfu;\n  },\n});\n\n// MFU\nconst cache = new CacheWithFrequencies<string, number>(2, {\n  evictionPolicy: (cache: CacheWithFrequencies<string, number>) => {\n    let max = -Infinity;\n    let mfu = \"\";\n    for (const [key, freq] of cache.frequencies!) {\n      if (freq > max) {\n        max = freq;\n        mfu = key;\n      }\n    }\n    return mfu;\n  },\n});\n```","readmeFilename":"README.md"}