{"id":28951063,"url":"https://github.com/nrkno/nodecache-as-promised","last_synced_at":"2025-06-23T14:08:02.047Z","repository":{"id":57132210,"uuid":"114610947","full_name":"nrkno/nodecache-as-promised","owner":"nrkno","description":"In-memory cache supporting promise based workers and middleware hooks (distributed expiry and persistence provided)","archived":false,"fork":false,"pushed_at":"2023-08-22T12:42:46.000Z","size":1009,"stargazers_count":20,"open_issues_count":2,"forks_count":5,"subscribers_count":16,"default_branch":"master","last_synced_at":"2025-03-05T22:07:48.122Z","etag":null,"topics":["cache","cache-busting","cache-storage","javascript","lru-cache","middlewares","nodejs","nrkno","promise-cache","redis-cache","redis-pubsub","stale-if-error","stale-while-revalidate","ttl-cache"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nrkno.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-12-18T07:46:19.000Z","updated_at":"2024-01-13T23:57:40.000Z","dependencies_parsed_at":"2022-08-24T22:50:47.275Z","dependency_job_id":null,"html_url":"https://github.com/nrkno/nodecache-as-promised","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/nrkno/nodecache-as-promised","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrkno%2Fnodecache-as-promised","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrkno%2Fnodecache-as-promised/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrkno%2Fnodecache-as-promised/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrkno%2Fnodecache-as-promised/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nrkno","download_url":"https://codeload.github.com/nrkno/nodecache-as-promised/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrkno%2Fnodecache-as-promised/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261491821,"owners_count":23166678,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","cache-busting","cache-storage","javascript","lru-cache","middlewares","nodejs","nrkno","promise-cache","redis-cache","redis-pubsub","stale-if-error","stale-while-revalidate","ttl-cache"],"created_at":"2025-06-23T14:08:01.279Z","updated_at":"2025-06-23T14:08:02.035Z","avatar_url":"https://github.com/nrkno.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @nrk/nodecache-as-promised\n\n\u003e Fast and resilient cache for NodeJs targeting high-volume sites\n\n- [Installing](#installing)\n- [Publish](#publish)\n- [Features](#features)\n- [APIs](#apis)\n- [Examples](#examples)\n- [Middlewares](#middlewares)\n- [Creating your own middlewares](#creating-your-own-middleware)\n- [Local development](#local-development)\n- [Building and committing](#building-and-committing)\n\n## Installing\n\n```\nnpm install @nrk/nodecache-as-promised --save\n```\n\n\n## Publish\n\n```\nnpm install \nnpm login \n\n# one of\nnpm version patch -m 'Release patch %s'\nnpm version minor -m 'Release minor %s'\nnpm version major -m 'Release major %s'\n\nnpm run build\ngit push\nnpm publish\n\n```\n\n## Motivation\nSometimes Node.js needs to do some heavy lifting, performing CPU or network intensive tasks and yet respond quickly on incoming requests. For repetitive tasks like Server side rendering of markup or parsing big JSON responses caching can give the application a great performance boost. Since many requests may hit the server concurrently, you do not want more than **one** worker to run for a given resource at the same time. In addition - serving stale content when a backend resource is down may save your day! The intention of `nodecache-as-promised` is to give you a fairly simple interface, yet powerful application cache, with fine-grained control over caching behaviour.\n\n`nodecache-as-promised` is inspired by how [Varnish](https://varnish-cache.org/) works. It is not intended to replace Varnish (but works great in combination). Whereas Varnish is a high-performant edge/burst/failover cache, working as a reverse proxy and loadbalancer, it depends on a fast backend when configured with short a cache window (ie. TTL ~1s). It uses URLs in combination with cookies as keys for its cached content. Since there are no restrictions on conformant URLs/cookies for clients requesting content, it is quite easy to bust it's cache without any security measures. `nodecache-as-promised` on the other hand is running at application level for more strict handling of cache keys, and may use many different caches and policies on how the web page is built.\n\n### Features\n- __In-memory cache__ is used as primary storage since it will always be faster than parsing and fetching data from disk or via network. An [LRU-cache](https://www.npmjs.com/package/lru-cache) is enabled to constrain the amount of memory used.\n- __Caches are filled using worker promises__ since cached objects often are depending on async operations. [RxJs](https://www.npmjs.com/package/rxjs) is used to queue concurrent requests for the same key; thus ensuring that only __one__ worker is performed when cached content is missing/stale.\n- __Caching of custom class instances, functions and native objects__ such as Date, RegExp and Redux stores are supported through in-memory caching. Non-serializable (using JSON.stringify) objects are filtered out in persistent caches though.\n- __Grace mode__ is used if a worker fails (eg. caused by failing backends), ie.  stale cache is returned instead.\n- __Avoidance of spamming backend resources__ using a configurable deltaWait parameter, serving either a stale object or a rejection.\n- __Middleware support__ so you may create your own custom extensions. Provided middlewares:\n  - __Persistent cache__ is used as secondary storage to avoid high back-pressure when inMemoryCaches are cleared after server restarts. This is achieved storing cache-misses and deletions on cache evictions using a [ioredis](https://www.npmjs.com/package/ioredis)-factory connecting to a redis instance.\n  - __Distributed on demand expiry__ so that new content may be published across servers/instances before cache-TTL is reached. This is achieved using Redis pub/sub depending on a [ioredis](https://www.npmjs.com/package/ioredis)-factory\n\n### Performance testing\n\nParsing a json-file at around 47kb (file contents are cached at startup). Using a Macbook pro, mid 2015, 16gb ram, i7 CPU.\n\n\u003cp align=\"left\"\u003e\n  \u003cimg src=\"./test/linear-perftest-nocache.jpeg?raw=true\" width=\"50%\"/\u003e\n\u003c/p\u003e\n\nThe image shows a graph from running the test script `npm run perf:nocache-cache-file -- --type=linear`. At around 1300 iterations the event loop starts lagging, and at around 1500 iterations the process stops responding. It displays that even natively optimized JSON.parse could be a bottleneck when fetching remote API-data for rendring. (`React.render` would be even slower)\n\n\u003cp align=\"left\"\u003e\n  \u003cimg src=\"./test/linear-perftest-cache.jpeg?raw=true\" width=\"50%\"/\u003e\n\u003c/p\u003e\n\nThe second image is a graph from running test script `npm run perf:cache -- --type=linear`. At around 3.1 million iterations the event loop starts lagging, and at around 3.4 million iterations the process runs out of memory and crashes. The graph has no relation to how fast JSON.parse is, but what speed is achievable by skipping it altogether (ie. `Promise`-processing)\n\n## APIs\nCreate a new `inMemoryCache` instance using a factory method. This instance may be extended by the `distCache` and/or `persistentCache` middlewares (`.use(..)`).\n\n### inMemoryCache factory\nCreating a new instance\n\n```js\nimport inMemoryCache from '@nrk/nodecache-as-promised'\nconst cache = inMemoryCache(options)\n```\n\n#### options\nAn object containing configuration\n- initial - `Object`. Initial key/value set to prefill cache. Default: `{}`\n- maxLength - `Number`. Max key count before LRU-cache evicts object. Default: `1000`\n- maxAge - `Number`. Max time before a (stale) key is evicted by LRU-cache (in ms). Default: `172800000` (48h)\n- log - `Object with log4j-facade`. Used to log internal work. Default: `console`\n\n### Instance methods\nWhen the factory is created (with or without middlewares), the following methods may be used.\n\n#### .get(key, [options])\nGet an item from the cache.\n```js\nconst {value} = cache.get('myKey')\nconsole.log(value)\n```\n\nUsing parameter `options` - the function either fetches a value from cache or executes provided worker if the cache is stale or cold. The worker will set the cache key if ran and thus returns a Promise\n\n```js\ncache.get('myKey', options)\n  .then(({value}) =\u003e {\n    console.log(value)\n  })\n```\n#### options\nConfiguration for the newly created object\n- worker - `function`. A function that returns a promise which resolves new value to be set in cache.\n- ttl - `Number`. Ttl (in ms) before cached object becomes stale. Default: `86400000` (24h)\n- workerTimeout - `Number`. max time allowed to run promise. Default: `5000`\n- deltaWait - `Number`. delta wait (in ms) before retrying promise, when stale. Default: `10000`\n\n#### returned object\n- value - `any` - value set in cache\n- created - `Number` - UX timestamp (ms) when the value was created\n- cache - `Enum(hit|miss|stale)` - status of cached content\n- TTL - `Number` - Amount of ms until until value becomes stale since creation\n\n**NOTE:** It might seem a bit strange to set cache values using `.get` - but it is to avoid a series of operations using `.get()` to check if a value exists, then call `.set()`, and finally running `.get()` once more (making queing difficult). In summary: `.get()` returns a value from cache or a provided worker.\n\n#### .set(key, value, [ttl])\nSet a new cache value.\n```js\n// set a cache value that becomes stale after 1 minute\ncache.set('myKey', 'someData', 60 * 1000)\n```\n\nIf `ttl`-parameter is omitted, a default will be used: `86400000` (24h)\n\n\n#### .has(key)\nCheck if a key is in the cache, without updating the recent-ness or deleting it for being stale.\n\n#### .del(key)\nDeletes a key out of the cache.\n\n#### .expire(keys)\nMark keys as stale (ie. set TTL = 0)\n```js\ncache.expire(['myKey*', 'anotherKey'])\n```\n\nAsterisk `*` is used for wildcards\n\n#### .keys()\nGet all keys as an array of strings stored in cache\n\n#### .values()\nGet all values as an array of all values in cache\n\n#### .entries()\nGet all entries as a Map of all keys and values in cache\n\n#### .clear()\nClear the cache entirely, throwing away all values.\n\n#### .addDisposer(callback)\nAdd callback to be called when an item is evicted by LRU-cache. Used to do cleanup\n```js\nconst cb = (key, value) =\u003e cleanup(key, value)\ncache.addDisposer(cb)\n```\n\n#### .removeDisposer(callback)\nRemove callback attached to LRU-cache\n```js\ncache.removeDisposer(cb)\n```\n\n#### .debug([extraData])\nPrints debug information about current cache (ie. hot keys, stale keys, keys in waiting state etc). Use `extraData` to add custom properties to the debug info, eg. hostname.\n```js\ncache.debug({hostname: os.hostname()})\n```\n\n#### .log.[trace|debug|info|warn|error] (data)\nLogger instance exposed to be used by middlewares\n```js\ncache.log.info('hello world!')\n```\n\n## Examples\n*Note! These examples are written using ES2015 syntax. The lib is exported using Babel as CJS modules*\n\n### Basic usage\n```js\nimport inMemoryCache from '@nrk/nodecache-as-promised'\nconst cache = inMemoryCache({ /* options */})\n\n// implicit set cache on miss, or use cached value\ncache.get('key', { worker: () =\u003e Promise.resolve({hello: 'world'}) })\n  .then((data) =\u003e {\n    console.log(data)\n    // {\n    //   value: {\n    //     hello: 'world'\n    //   },\n    //   created: 123456789,\n    //   cache: 'miss',\n    //   TTL: 86400000\n    // }\n  })\n```\n\n### Basic usage with options\n```js\nimport inMemoryCache from '@nrk/nodecache-as-promised';\n\nconst cache = inMemoryCache({\n  initial: {                    // initial state\n    foo: 'bar'\n  },                            \n  maxLength: 1000,              // LRU max object count\n  maxAge: 24 * 60 * 60 * 1000   // LRU max age in ms\n})\n// set/overwrite cache key\ncache.set('key', {hello: 'world'})\n// imiplicit set cache on miss, or use cached value\ncache.get('anotherkey', {\n  worker: () =\u003e Promise.resolve({hello: 'world'}),\n  ttl: 60 * 1000,               // TTL for cached object, in ms\n  workerTimeout: 5 * 1000,      // worker timeout, in ms\n  deltaWait: 5 * 1000,          // wait time, if worker fails\n}).then((data) =\u003e {\n    console.log(data)\n    // {\n    //   value: {\n    //     hello: 'world'\n    //   },\n    //   created: 123456789,\n    //   cache: 'miss',\n    //   TTL: 86400000\n    // }\n  })\n```\n\n## Middlewares\n\n### distCache middleware\nCreating a new distCache middleware instance. The distCache middleware is extending the inMemoryCache instance by making a publish call to Redis using the provided `namespace` when the `.expire`-method is called. A subscription to the `namespace` ensures calls to `.expire` is distributed to all instances of the inMemoryCache using the same distCache middleware with the same `namespace`. It adds a couple of parameters to the `.debug`-method.\n\n```js\nimport cache, {distCache} from '@nrk/nodecache-as-promised'\nconst cache = inMemoryCache()\ncache.use(distCache(redisFactory, namespace))\n```\n\n#### Parameters\nParameters that must be provided upon creation:\n- redisFactory - `Function`. A function that returns an ioredis compatible redisClient.\n- namespace - `String`. Pub/sub-namespace used for distributed expiries\n\n#### Example\n```js\nimport inMemoryCache, {distCache} from '@nrk/nodecache-as-promised'\nimport Redis from 'ioredis'\n\n// a factory function that returns a redisClient\nconst redisFactory = () =\u003e new Redis(/* options */)\nconst cache = inMemoryCache({initial: {fooKey: 'bar'}})\ncache.use(distCache(redisFactory, 'namespace'))\n// publish to redis (using wildcard)\ncache.expire(['foo*'])\nsetTimeout(() =\u003e {\n  cache.get('fooKey').then(console.log)\n  // expired in server # 1 + 2\n  // {value: {fooKey: 'bar'}, created: 123456789, cache: 'stale', TTL: 86400000}\n}, 1000)\n```\n\n### persistentCache middleware\nCreating a new persistentCache middleware instance. The persistentCache middleware is extending the inMemoryCache instance by serializing and storing any new values recieved via workers in `.get` or in `.set`-calls to Redis. In addition it deletes values from Redis when the `.del` and `.clear`-methods are called. Cache values evicted by the LRU-cache are also deleted. On creation it will load and set initial cache values by doing a search for stored keys on the provided `keySpace` (may be disabled using the option `bootLoad: false` - so that loading may be done afterwards using the provided `.load`-method). It adds a couple of parameters to the `.debug`-method.\n\n\n```js\nimport cache, {persistentCache} from '@nrk/nodecache-as-promised'\nconst cache = inMemoryCache()\ncache.use(persistentCache(redisFactory, options))\n```\n\n#### Parameters\nParameters that must be provided upon creation:\n- redisFactory - `Function`. A function that returns an ioredis compatible redisClient.\n\n#### options\n- doNotPersist - `RegExp`. Keys matching this regexp is not persisted to cache. Default `null`\n- keySpace - `String`. Prefix used when storing keys in redis.\n- grace - `Number`. Used to calculate TTL in redis (before auto removal), ie. object.TTL + grace. Default `86400000` (24h)\n- bootload - `Boolean`. Flag to choose if persisted cache is loaded from redis on middleware creation. Default `true`\n\n#### Example\n```js\nimport inMemoryCache, {persistentCache} from '@nrk/nodecache-as-promised'\nimport Redis from 'ioredis'\n\nconst redisFactory = () =\u003e new Redis(/* options */)\nconst cache = inMemoryCache({/* options */})\ncache.use(persistentCache(\n  redisFactory,\n  {\n    keySpace: 'myCache',   // key prefix used when storing in redis\n    grace: 60 * 60         // auto expire unused keys in Redis after TTL + grace seconds\n  }\n))\n\ncache.get('key', { worker: () =\u003e Promise.resolve('hello') })\n// will store a key in redis, using key: myCache-\u003ckey\u003e\n// {value: 'hello', created: 123456789, cache: 'hit', TTL: 60000}\n```\n\n#### Combining middlewares\n\nExample in combining persistentCache __and__ distCache\n\n```js\nimport inMemoryCache, {distCache, persistentCache} from '@nrk/nodecache-as-promised'\nimport Redis from 'ioredis'\n\nconst redisFactory = () =\u003e new Redis(/* options */)\nconst cache = inMemoryCache({/* options */})\ncache.use(distCache(redisFactory, 'namespace'))\ncache.use(persistentCache(\n  redisFactory,\n  {\n    keySpace: 'myCache',   // key prefix used when storing in redis\n    grace: 60 * 60         // auto expire unused keys in Redis after TTL + grace seconds\n  }\n))\n\ncache.expire(['foo*'])  // distributed expire of all keys starting with foo\ncache.get('key', {\n  worker: () =\u003e Promise.resolve('hello'),\n  ttl: 60000,                       // in ms\n  workerTimeout: 5000,\n  deltaWait: 5000\n}).then(console.log)\n// will store a key in redis, using key: myCache-\u003ckey\u003e\n// {value: 'hello', created: 123456789, cache: 'miss', TTL: 60000}\n```\n\n## Creating your own middleware\nA middleware consists of three parts:\n1) an exported factory function\n2) constructor arguments to be used within the middleware\n3) an exported facade that corresponds with the overriden functions (appending a `next` parameter that runs the next function in the middleware chain)\n\nLets say you want to build a middleware that notifies some other part of your application that a new value has been set (eg. using RxJs streams).\n\nHere's an example on how to achieve this:\n```js\n// export namespace to be applied in inMemoryCache.use().\nexport const streamingMiddleware = (onSet, onDispose) =\u003e (cacheInstance) =\u003e {\n  // create a function that runs before the others in the middleware chain\n  const set = (key, value, next) =\u003e {\n    onSet(key, value)\n    next(key, value)\n  }\n\n  // use functionality exposed by the inMemoryCache instance\n  cacheInstance.addDisposer(onDispose)\n\n  // export facade\n  return {\n    set\n  }\n}\n```\n\n\n---\n\n## Local development\nFirst clone the repo and install its dependencies:\n\n```bash\ngit clone git@github.com:nrkno/nodecache-as-promised.git\ngit checkout -b feature/my-changes\ncd nodecache-as-promised\nnpm install \u0026\u0026 npm run build \u0026\u0026 npm run test\n```\n\n## Building and committing\nAfter having applied changes, remember to build and run/fix tests before pushing the changes upstream.\n\n```bash\n# run the tests, generate code coverage report\nnpm run test\n# inspect code coverage\nopen ./coverage/lcov-report/index.html\n# update the code\nnpm run build\ngit commit -am \"Add my changes\"\ngit push origin feature/my-changes\n# then make a PR to the master branch,\n# and assign one of the maintainers to review your code\n```\n\n\u003e NOTE! Please make sure to keep commits small and clean (that the commit message actually refers to the updated files). Stylistically, make sure the commit message is **Capitalized** and **starts with a verb in the present tense** (eg. `Add minification support`).\n\n## License\n\nMIT © [NRK](https://www.nrk.no)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnrkno%2Fnodecache-as-promised","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnrkno%2Fnodecache-as-promised","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnrkno%2Fnodecache-as-promised/lists"}