{"id":17148441,"url":"https://github.com/aholstenson/transitory","last_synced_at":"2025-04-13T09:32:27.072Z","repository":{"id":48924331,"uuid":"93339984","full_name":"aholstenson/transitory","owner":"aholstenson","description":"In-memory cache with high hit rates via LFU eviction for Node and browsers. Supports time-based expiration, automatic loading and metrics.","archived":false,"fork":false,"pushed_at":"2023-06-21T20:14:13.000Z","size":242,"stargazers_count":34,"open_issues_count":4,"forks_count":6,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-04-24T22:21:40.750Z","etag":null,"topics":["cache","caching","lfu","lfu-cache"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/aholstenson.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2017-06-04T20:36:50.000Z","updated_at":"2023-12-20T05:24:11.000Z","dependencies_parsed_at":"2023-10-20T16:26:04.969Z","dependency_job_id":null,"html_url":"https://github.com/aholstenson/transitory","commit_stats":{"total_commits":144,"total_committers":3,"mean_commits":48.0,"dds":0.03472222222222221,"last_synced_commit":"3d108f18488322aa628e8dce188e244842352b5f"},"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aholstenson%2Ftransitory","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aholstenson%2Ftransitory/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aholstenson%2Ftransitory/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aholstenson%2Ftransitory/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aholstenson","download_url":"https://codeload.github.com/aholstenson/transitory/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248690900,"owners_count":21146229,"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","caching","lfu","lfu-cache"],"created_at":"2024-10-14T21:28:36.360Z","updated_at":"2025-04-13T09:32:26.465Z","avatar_url":"https://github.com/aholstenson.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Transitory\n\n[![npm version](https://badge.fury.io/js/transitory.svg)](https://badge.fury.io/js/transitory)\n[![Build Status](https://travis-ci.org/aholstenson/transitory.svg?branch=master)](https://travis-ci.org/aholstenson/transitory)\n[![Coverage Status](https://coveralls.io/repos/aholstenson/transitory/badge.svg)](https://coveralls.io/github/aholstenson/transitory)\n[![Dependencies](https://david-dm.org/aholstenson/transitory.svg)](https://david-dm.org/aholstenson/transitory)\n\nTransitory is an in-memory cache for Node and browsers, with high hit rates\nusing eviction based on frequency and recency. Additional cache layers support \ntime-based expiration, automatic loading and metrics.\n\n```javascript\nimport { newCache } from 'transitory';\n\nconst cache = newCache()\n  .maxSize(1000)\n  .expireAfterWrite(60000) // 60 seconds\n  .build();\n\ncache.set('key', { value: 10 });\ncache.set(1234, 'any value');\n\nconst value = cache.getIfPresent('key');\n```\n\nUsing TypeScript:\n\n```typescript\nimport { newCache, BoundlessCache } from 'transitory';\n\nconst cache: Cache\u003cstring, number\u003e = newCache()\n  .maxSize(1000)\n  .build();\n\nconst cacheWithoutBuilder = new BoundlessCache\u003cstring, number\u003e({});\n```\n\n## Supported features\n\n* Limiting cache size to a total number of items\n* Limiting cache size based on the weight of items\n* LFU (least-frequently used) eviction of items\n* Listener for evicted and removed items\n* Expiration of items a certain time after they were stored in the cache\n* Expiration of items based on if they haven't been read for a certain time\n* Automatic loading if a value is not cached\n* Collection of metrics about hit rates\n\n## Performance\n\nThe caches in this library are designed to have a high hit rate by evicting\nentries in the cache that are not frequently used. Transitory implements\n[W-TinyLFU](https://arxiv.org/abs/1512.00727) as its eviction policy which is\na LFU policy that provides good hit rates for many use cases.\n\nSee [Performance](https://github.com/aholstenson/transitory/wiki/Performance)\nin the wiki for comparisons of the hit rate of Transitory to other libraries.\n\n## Cache API\n\nThere are a few basic things that all caches support. All caches support\nstrings, numbers and booleans as their `KeyType`.\n\n* `cache.set(key: KeyType, value: ValueType): ValueType | null`\n\n    Store a value tied to the specified key. Returns the previous value or\n    `null` if no value currently exists for the given key.\n\n* `cache.getIfPresent(key: KeyType): ValueType | null`\n\n    Get the cached value for the specified key if it exists. Will return\n\t  the value or `null` if no cached value exist. Updates the usage of the\n\t  key. This is the main way to get cached items, unless the cache is a\n    loading cache.\n\n* `cache.get(key: KeyType, loader?): Promise\u003cValueType | null\u003e`\n\n    _For loading caches:_ Get a value loading it if it is not cached. Can\n    optionally take a `loader` function that loads the value.\n\n* `cache.peek(key: KeyType): ValueType | null`\n\n    Peek to see if a key is present without updating the usage of the\n\t  key. Returns the value associated with the key or `null`  if the key\n\t  is not present.\n\n* `cache.has(key: KeyType): boolean`\n\n    Check if the given key exists in the cache.\n\n* `cache.delete(key: KeyType): ValueType | null`\n\n    Delete a value in the cache. Returns the removed value or `null` if there\n    was no value associated with the key in the cache.\n\n* `cache.clear()`\n\n    Clear the cache removing all of the entries cached.\n\n* `cache.keys(): KeyType[]`\n\n    Get all of the keys in the cache as an `Array`. Can be used to iterate\n    over all of the values in the cache, but be sure to protect against values\n    being removed during iteration due to time-based expiration if used.\n\n* `cache.maxSize: number`\n\n   The maximum size of the cache or `-1` if boundless. This size represents the\n   weighted size of the cache.\n\n* `cache.size: number`\n\n   The number of entries stored in the cache. This is the actual number of entries\n   and not the weighted size of all of the entries in the cache.\n\n* `cache.weightedSize: number`\n\n    Get the weighted size of the cache. This is the weight of all entries that\n    are currently in the cache.\n\n* `cache.cleanUp()`\n\n    _Advanced:_ Request clean up of the cache by removing expired entries and\n    old data. Clean up is done automatically a short time after sets and\n    deletes, but if your cache uses time-based expiration and has very\n    sporadic updates it might be a good idea to call `cleanUp()` at times.\n    A good starting point would be to call `cleanUp()` in a `setInterval`\n    with a delay of at least a few minutes.\n\n* `cache.metrics: Metrics`\n\n    _For metric enabled caches:_ Get metrics for this cache. Returns an object\n    with the keys `hits`, `misses` and `hitRate`. For caches that do not have\n    metrics enabled trying to access metrics will throw an error.\n\n## Building a cache\n\nCaches are created via a builder that helps with adding on all requested\nfunctionality and returning a cache.\n\nA builder is created by calling the imported function:\n\n```javascript\nimport { newCache } from 'transitory';\nconst builder = newCache();\n```\n\nCalls on the builder can be chained:\n\n```javascript\nnewCache().maxSize(100).loading().build();\n```\n\nOr using caches directly for tree-shaking and better bundle sizes:\n\n```javascript\nimport { BoundedCache, ExpirationCache } from 'transitory';\n\nconst cache = new ExpirationCache({\n  maxWriteAge: 60000,\n  parent: new BoundedCache({\n    maxSize: 1000\n  })\n});\n```\n\n## Unlimited size cache\n\nIt's possible to create a cache without any limits, in which it acts like a\nstandard `Map`.\n\n```javascript\n// Using the builder\nconst cache = newCache()\n  .build();\n\n// Using caches directly\nimport { BoundlessCache } from 'transitory';\n\nconst cache = new BoundlessCache({});\n```\n\nThis is mostly useful if you have another layer of logic on top of it or if\nyou're creating caches without the builder.\n\n## Limiting the size of a cache\n\nCaches can be limited to a certain size. This type of cache will evict the\nleast frequently used items when it reaches its maximum size.\n\n```javascript\n// Using the builder\nconst cache = newCache()\n  .maxSize(100)\n  .build();\n\n// Using caches directly\nimport { BoundedCache } from 'transitory';\n\nconst cache = new BoundedCache({\n  maxSize: 100\n});\n```\n\nIt is also possible to change how the size of each entry in the cache is\ncalculated. This can be used to create a better cache if your entries vary in\ntheir size in memory.\n\n```javascript\n// Using the builder\nconst cache = newCache()\n  .maxSize(2000)\n  .withWeigher((key, value) =\u003e value.length)\n  .build();\n\n// Using caches directly\nimport { BoundedCache } from 'transitory';\n\nconst cache = new BoundedCache({\n  maxSize: 2000,\n  weigher: (key, value) =\u003e value.length\n});\n```\n\nThe size of an entry is evaluated when it is added to the cache so weighing\nworks best with immutable data. Transitory includes a weigher for estimated\nmemory:\n\n```javascript\nimport { memoryUsageWeigher } from 'transitory';\n\nconst cache = newCache()\n  .maxSize(50000000)\n  .withWeigher(memoryUsageWeigher)\n  .build();\n```\n\n## Automatic expiry\n\nLimiting the maximum amount of time an entry can exist in the cache can be done\nby using `expireAfterWrite(time)` or `expireAfterRead(time)`. Entries\nare lazy evaluated and will be removed when the values are set or deleted from\nthe cache.\n\n```javascript\n// Using the builder\nconst cache = newCache()\n  .expireAfterWrite(5000) // 5 seconds\n  .expireAfterRead(1000) // Values need to be read at least once a second\n  .build();\n```\n\nBoth methods can also take a function that should return the maximum age\nof the entry in milliseconds:\n\n```javascript\n// Using the builder\nconst cache = newCache()\n  .expireAfterWrite((key, value) =\u003e 5000)\n  .expireAfterRead((key, value) =\u003e 5000 / key.length)\n  .build();\n```\n\nUsing caches directly requires a parent cache and that functions are always\npassed:\n\n```javascript\nimport { BoundlessCache } from 'transitory';\n\nconst cache = new ExpirationCache({\n  maxWriteAge: () =\u003e 5000,\n  maxNoReadAge: () =\u003e 1000,\n\n  parent: new BoundlessCache({});\n});\n```\n\n## Loading caches\n\nCaches can be made to automatically load values if they are not in the cache.\nThis type of caches relies heavily on the use of promises.\n\nWith a global loader:\n\n```javascript\n// Using the builder\nconst cache = newCache()\n  .withLoader(key =\u003e loadSlowData(key))\n  .build();\n\n// Using caches directly\nimport { DefaultLoadingCache } from 'transitory';\n\nconst cache = new DefaultLoadingCache({\n  loader: key =\u003e loadSlowData(key),\n  parent: new BoundlessCache({}) // or any other cache\n});\n```\n\nUsing a global loader is done by calling `cache.get(key)`, which returns a\npromise:\n\n```javascript\ncache.get(781)\n  .then(data =\u003e handleLoadedData(data))\n  .catch(err =\u003e handleError(err));\n\ncache.get(1234, specialLoadingFunction)\n```\n\nWithout a global loader:\n\n```javascript\n// Using the builder\nconst cache = newCache()\n  .loading()\n  .build();\n\n// Using caches directly\nimport { DefaultLoadingCache } from 'transitory';\n\nconst cache = new DefaultLoadingCache({\n  parent: new BoundlessCache({}) // or any other cache\n});\n```\n\nUse via `cache.get(key, functionToLoadData)`:\n\n```javascript\ncache.get(781, key =\u003e loadSlowData(key))\n  .then(data =\u003e handleLoadedData(data))\n  .catch(err =\u003e handleError(err));\n```\n\nLoading caches can be combined with other things such as `maxSize`.\n\n## Metrics\n\nYou can track the hit rate of the cache by activating support for metrics:\n\n```javascript\n// Using the builder\nconst cache = newCache()\n  .metrics()\n  .build();\n\n// Using caches directly\nimport { MetricsCache } from 'transitory';\n\nconst cache = new MetricsCache({\n  parent: new BoundlessCache({})\n});\n```\n\nFetching metrics:\n\n```javascript\nconst metrics = cache.metrics;\n\nconsole.log('hitRate=', metrics.hitRate);\nconsole.log('hits=', metrics.hits);\nconsole.log('misses=', metrics.misses);\n```\n\n## Removal listener\n\nCaches support a single removal listener that will be notified when items in\nthe cache are removed.\n\n```javascript\nimport { RemovalReason } from 'transitory';\n\nconst cache = newCache()\n  .withRemovalListener((key, value, reason) =\u003e {\n    switch(reason) {\n      case RemovalReason.EXPLICIT:\n        // The user of the cache requested something to be removed\n        break;\n      case RemovalReason.REPLACED:\n        // A new value was loaded and this value was replaced\n        break;\n      case RemovalReason.SIZE:\n        // A value was evicted from the cache because the max size has been reached\n        break;\n      case RemovalReason.EXPIRED:\n        // A value was removed because it expired due to its max age\n        break;\n    }\n  })\n  .build();\n```\n\nWhen using caches directly the removal listener should go on the final cache:\n\n```javascript\nimport { LoadingCache, BoundlessCache } from 'transitory';\n\nconst cache = new LoadingCache({\n  removalListener: listenerFunction,\n\n  parent: new BoundlessCache({})\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faholstenson%2Ftransitory","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faholstenson%2Ftransitory","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faholstenson%2Ftransitory/lists"}