mcollina/async-cache-dedupe
mcollina/async-cache-dedupe — 715★ on GitHub (JavaScript). Async cache with dedupe support
Snapshot summary built from the project's own GitHub metadata — there's no written TopGit review yet. The page will update automatically when a full review is published.
TopGit writes full reviews for the most-starred, most-requested repositories. This page is a snapshot until then — see the READ ME tab for the original README in full.
Snapshot
Top contributors
Show top contributors
async-cache-dedupe
async-cache-dedupe is a cache for asynchronous fetching of resources
with full deduplication, i.e. the same resource is only asked once at any given time.
Install
npm i async-cache-dedupe
Example
import { createCache } from 'async-cache-dedupe'
const cache = createCache({
ttl: 5, // seconds
stale: 5, // number of seconds to return data after ttl has expired
storage: { type: 'memory' },
})
cache.define('fetchSomething', async (k) => {
console.log('query', k)
// query 42
// query 24
return { k }
})
const p1 = cache.fetchSomething(42)
const p2 = cache.fetchSomething(24)
const p3 = cache.fetchSomething(42)
const res = await Promise.all([p1, p2, p3])
console.log(res)
// [
// { k: 42 },
// { k: 24 }
// { k: 42 }
// ]
Commonjs/require is also supported.
API
createCache(opts)
Creates a new cache.
Options:
ttl: the maximum time a cache entry can live, default0; if0, an element is removed from the cache as soon as the promise resolves.stale: the time after which the value is served from the cache after the ttl has expired. This can be a number in seconds or a function that accepts the data and returns the stale value.onDedupe: a function that is called every time it is defined is deduped.onError: a function that is called every time there is a cache error.onHit: a function that is called every time there is a hit in the cache.onMiss: a function that is called every time the result is not in the cache.storage: the storage options; default is{ type: "memory" }Storage options are:type:memory(default) orredisoptions: by storage type-
for
memorytypesize: maximum number of items to store in the cache per resolver. Default is1024.invalidation: enable invalidation, see invalidation. Default is disabled.log: logger instancepinocompatible, default is disabled.
Example
createCache({ storage: { type: 'memory', options: { size: 2048 } } }) -
for
redistypeclient: a redis client instance, mandatory. Should be anioredisclient or compatible.invalidation: enable invalidation, see invalidation. Default is disabled.invalidation.referencesTTL: references TTL in seconds, it means how long the references are alive; it should be set at the maximum of all the caches ttl.log: logger instancepinocompatible, default is disabled.
Example
createCache({ storage: { type: 'redis', options: { client: new Redis(), invalidation: { referencesTTL: 60 } } } })
-
transformer: the transformer to used to serialize and deserialize the cache entries. It must be an object with the following methods:-
serialize: a function that receives the result of the original function and returns a serializable object. -
deserialize: a function that receives the serialized object and returns the original result. -
Default is
undefined, so the default transformer is used.Example
import superjson from 'superjson'; const cache = createCache({ transformer: { serialize: (result) => superjson.serialize(result), deserialize: (serialized) => superjson.deserialize(serialized), } })
-
cache.define(name[, opts], original(arg, cacheKey))
Define a new function to cache of the given name.
The define method adds a cache[name] function that will call the original function if the result is not present
in the cache. The cache key for arg is computed using safe-stable-stringify and it is passed as the cacheKey argument to the original function.
Options:
-
ttl: a number or a function that returns a number of the maximum time a cache entry can live, default as defined in the cache; default is zero, so cache is disabled, the function will be only the deduped. The first argument of the function is the result of the original function. -
stale: the time after which the value is served from the cache after the ttl has expired. This can be a number in seconds or a function that accepts the data and returns the stale value. -
serialize: a function to convert the given argument into a serializable object (or string). -
onDedupe: a function that is called every time there is defined is deduped. -
onError: a function that is called every time there is a cache error. -
onHit: a function that is called every time there is a hit in the cache. -
onMiss: a function that is called every time the result is not in the cache. -
storage: the storage to use, same as above. It's possible to specify different storages for each defined function for fine-tuning. -
transformer: the transformer to used to serialize and deserialize the cache entries. It's possible to specify different transformers for each defined function for fine-tuning. -
references: sync or async function to generate references, it receives(args, key, result)from the defined function call and must return an array of strings or falsy; see invalidation to know how to use them.Example 1
const cache = createCache({ ttl: 60 }) cache.define('fetchUser', { references: (args, key, result) => result ? [`user~${result.id}`] : null }, (id) => database.find({ table: 'users', where: { id }})) await cache.fetchUser(1)Example 2 - dynamically set
ttlbased on result.const cache = createCache() cache.define('fetchAccessToken', { ttl: (result) => result.expiresInSeconds }, async () => { const response = await fetch("https://example.com/token"); const result = await response.json(); // => { "token": "abc", "expiresInSeconds": 60 } return result; }) await cache.fetchAccessToken()Example 3 - dynamically set
stalevalue based on result.const cache = createCache() cache.define('fetchUserProfile', { ttl: 60, stale: (result) => result.staleWhileRevalidateInSeconds }, async () => { const response = await fetch("https://example.com/token"); const result = await response.json(); // => { "username": "MrTest", "staleWhileRevalidateInSeconds": 5 } return result; }) await cache.fetchUserProfile()
cache.clear([name], [arg])
Clear the cache. If name is specified, all the cache entries from the function defined with that name are cleared.
If arg is specified, only the elements cached with the given name and arg are cleared.
cache.invalidateAll(references, [storage])
cache.invalidateAll perform invalidation over the whole storage; if storage is not specified - using the same name as the defined function, invalidation is made over the default storage.
references can be:
- a single reference
- an array of references (without wildcard)
- a matching reference with wildcard, same logic for
memoryandredis
Example
const cache = createCache({ ttl: 60 })
cache.define('fetchUser', {
references: (args, key, result) => result ? [`user:${result.id}`] : null
}, (id) => database.find({ table: 'users', where: { id }}))
cache.define('fetchCountries', {
storage: { type: 'memory', size: 256 },
references: (args, key, result) => [`countries`]
}, (id) => database.find({ table: 'countries' }))
// ...
// invalidate all users from default storage
cache.invalidateAll('user:*')
// invalidate user 1 from default storage
cache.invalidateAll('user:1')
// invalidate user 1 and user 2 from default storage
cache.invalidateAll(['user:1', 'user:2'])
// note "fetchCountries" uses a different storage
cache.invalidateAll('countries', 'fetchCountries')
See below how invalidation and references work.
Invalidation
Along with time to live invalidation of the cache entries, we can use invalidation by keys.
The concept behind invalidation by keys is that entries have an auxiliary key set that explicitly links requests along with their own result. These auxiliary keys are called here references.
A scenario. Let's say we have an entry user {id: 1, name: "Alice"}, it may change often or rarely, the ttl system is not accurate:
- it can be updated before
ttlexpiration, in this case the old value is shown until expiration byttl. - it's not been updated during
ttlexpiration, so in this case, we don't need to reload the value, because it's not changed
To solve this common problem, we can use references.
We can say that the result of defined function getUser(id: 1) has reference user~1, and the result of defined function findUsers, containing {id: 1, name: "Alice"},{id: 2, name: "Bob"} has references [user~1,user~2].
So we can find the results in the cache by their references, independently of the request that generated them, and we can invalidate by references.
So, when a writing event involving user {id: 1} happens (usually an update), we can remove all the entries in the cache that have references to user~1, so the result of getUser(id: 1) and findUsers, and they will be reloaded at the next request with the new data - but not the result of getUser(id: 2).
Explicit invalidation is disabled by default, you have to enable it in storage settings.
See mercurius-cache-example for a complete example.
Redis
Using a redis storage is the best choice for a shared and/or large cache.
All the references entries in redis have referencesTTL, so they are all cleaned at some time.
referencesTTL value should be set at the maximum of all the ttls, to let them be available for every cache entry, but at the same time, they expire, avoiding data leaking.
Anyway, we should keep references up-to-date to be more efficient on writes and invalidation, using the garbage collector function, that prunes the expired references: while expired references do not compromise the cache integrity, they slow down the I/O operations.
Storage memory doesn't have gc.
Redis garbage collector
As said, While the garbage collector is optional, is highly recommended to keep references up to date and improve performances on setting cache entries and invalidation of them.
storage.gc([mode], [options])
mode:lazy(default) orstrict. Inlazymode, only a chunk of thereferencesare randomly checked, and probably freed; runninglazyjobs tend to eventually clear all the expiredreferences. Instrictmode, all thereferencesare checked and freed, and after that,referencesand entries are perfectly clean.lazymode is the light heuristic way to ensure cached entries andreferencesare cleared without stressing too muchredis,strictmode at the opposite stress moreredisto get a perfect result. The best strategy is to combine them both, running oftenlazyjobs along with somestrictones, depending on the size of the cache.
Options:
chunk: the chunk size of references analyzed per loops, default64lazy~chunk: the chunk size of references analyzed per loops inlazymode, default64; if bothchunkandlazy.chunkis set, the maximum one is takenlazy~cursor: the cursor offset, default zero; cursor should be set atreport.cursorto continue scanning from the previous operation
Return report of the gc job, as follows
"report":{
"references":{
"scanned":["r:user:8", "r:group:11", "r:group:16"],
"removed":["r:user:8", "r:group:16"]
},
"keys":{
"scanned":["users~1"],
"removed":["users~1"]
},
"loops":4,
"cursor":0,
"error":null
}
Example
import { createCache, createStorage } from 'async-cache-dedupe'
const cache = createCache({
ttl: 5,
storage: { type: 'redis', options: { client: redisClient, invalidation: true } },
})
// ... cache.define('fetchSomething'
const storage = createStorage('redis', { client: redisClient, invalidation: true })
let cursor
setInterval(() => {
const report = await storage.gc('lazy', { lazy: { cursor } })
if(report.error) {
console.error('error on redis gc', error)
return
}
console.log('gc report (lazy)', report)
cursor = report.cursor
}, 60e3).unref()
setInterval(() => {
const report = await storage.gc('strict', { chunk: 128 })
if(report.error) {
console.error('error on redis gc', error)
return
}
console.log('gc report (strict)', report)
}, 10 * 60e3).unref()
Custom storage
Allow users to provide their own storage implementation that conforms to the expected async interface.
const { StorageInterface } = require('async-cache-dedupe')
class CustomStorage extends StorageInterface {
async get (key) { } // Retrieve value from your storage
async set (key, value, ttl, references) { } // Store value in your storage with TTL and references
async remove (key) { } // Remove value by key
async invalidate (references) { } // Invalidate all entries with given references
async clear () { } // Clear entire cache
async refresh () { } // Used internally for TTL refresh logic
async getTTL (key) { } // Return TTL for a key
async exists (key) { } // Check if a key exists
}
const { createStorage, Cache } = require('async-cache-dedupe')
const storage = createStorage('custom', {
storage: new CustomStorage({
// You can set the optiions required for the custom storage
})
})
const cache = createCache({
ttl: 5,
storage: { type: 'custom', options: { storage } },
})
- When using the
customstorage type, you must provide a valid storage instance via the options. - You can refer to the Redis or Memory storage implementation for guidance.
- Ensure your storage implementation is compatible with your runtime environment (server or client).
TypeScript
This module provides a basic type definition for TypeScript.
As the library does some meta-programming and magic stuff behind the scenes, your compiler could yell at you when defining functions using the define property.
To avoid this, chain all defined functions in a single invocation:
import { createCache, Cache } from "async-cache-dedupe";
const fetchSomething = async (k: any) => {
console.log("query", k);
return { k };
};
const cache = createCache({
ttl: 5, // seconds
storage: { type: "memory" },
});
const cacheInstance = cache
.define("fetchSomething", fetchSomething)
.define("fetchSomethingElse", fetchSomething);
const p1 = cacheInstance.fetchSomething(42); // <--- TypeScript doesn't argue anymore here!
const p2 = cacheInstance.fetchSomethingElse(42); // <--- TypeScript doesn't argue anymore here!
Browser
All the major browser are supported; only memory storage type is supported, redis storage can't be used in a browser env.
This is a very simple example of how to use this module in a browser environment:
<script src="https://unpkg.com/async-cache-dedupe"></script>
<script>
const cache = asyncCacheDedupe.createCache({
ttl: 5, // seconds
storage: { type: 'memory' },
})
cache.define('fetchSomething', async (k) => {
console.log('query', k)
return { k }
})
const p1 = cache.fetchSomething(42)
const p2 = cache.fetchSomething(42)
const p3 = cache.fetchSomething(42)
Promise.all([p1, p2, p3]).then((values) => {
console.log(values)
})
</script>
You can also use the module with a bundler. The supported bundlers are webpack, rollup, esbuild and browserify.
Contributing
Contributions are welcome! To run the project locally for testing:
Local Development Setup
-
Start Redis for testing: The project includes a Docker Compose configuration for running Redis locally:
cd utils docker compose up -dThis will start a Redis instance on port 6379, which is used by the test suite.
-
Install dependencies:
npm install -
Run tests:
npm test -
Stop Redis when done:
cd utils docker compose down
Tests require 100% coverage to pass.
Maintainers
- Matteo Collina, https://twitter.com/matteocollina, https://www.npmjs.com/~matteo.collina
- Simone Sanfratello, https://twitter.com/simonesanfradev, https://www.npmjs.com/~simone.sanfra
Breaking Changes
- version
0.5.0->0.6.0options.cacheSizeis dropped in favor ofstorage
License
MIT
Related repositories
Master programming by recreating your favorite technologies from scratch.
A curated meta-list of curated lists organized by technology domain. The repository acts as a directory pointing to hundreds of specialized awesome lists covering programming languages, platforms, frameworks, and tooling. All content is community-contributed under the CC0 public domain dedication.
Public APIs is a community-curated GitHub repository listing free, publicly accessible APIs across a wide range of categories, with auth type, HTTPS, and CORS noted for each entry. It's a browsable reference, not a library to install.
freeCodeCamp is a free, self-paced curriculum for learning to code, published as open source at freeCodeCamp/freeCodeCamp. It's a 501(c)(3) nonprofit funded by donor support, structured around six certifications in its Full-Stack Developer Curriculum, each gated by required projects instead of open-book quizzes. The repository also carries beta language certifications for developers, interview-prep resources, and the code that runs the live freecodecamp.org platform.
Quick answers
Does mcollina/async-cache-dedupe have a project website?
No homepage URL was recorded for mcollina/async-cache-dedupe in TopGit's last sync. The README tab above frequently contains screenshots and demo links, or check the repository description on GitHub.
Does mcollina/async-cache-dedupe have any tags?
TopGit's last sync did not record any GitHub topics for mcollina/async-cache-dedupe. GitHub topics appear in the right sidebar of a repository page; that's the authoritative place to check.
How active is development on mcollina/async-cache-dedupe?
The most recent commit recorded on mcollina/async-cache-dedupe was 1 month ago, based on the GitHub push timestamp. The repository has 51 forks — one of the better signals of community interest.
Is mcollina/async-cache-dedupe open source?
Yes — mcollina/async-cache-dedupe ships under the MIT license, which makes its source code freely readable (and, depending on license terms, forkable and reusable). Source: github.com/mcollina/async-cache-dedupe.
What license does mcollina/async-cache-dedupe use?
mcollina/async-cache-dedupe is released under the MIT license. Always verify the LICENSE file directly on GitHub for the authoritative terms — license strings can be edited out of sync with a project's actual stance.
Where do I read more about mcollina/async-cache-dedupe?
This TopGit page is a snapshot — the READ ME tab shows the project's own README content (links stripped, images preserved). The GitHub repository at github.com/mcollina/async-cache-dedupe is the definitive source.
Read full README in the tab above.
Is async-cache-dedupe worth your time?
ChatGPT, Claude and Perplexity can all read this page. Ask one of them what it makes of async-cache-dedupe.