• Stars
    star
    370
  • Rank 115,405 (Top 3 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 3 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Async cache with dedupe support

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, default 0; if 0, 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) or redis
    • options: by storage type
      • for memory type

        • size: maximum number of items to store in the cache per resolver. Default is 1024.
        • invalidation: enable invalidation, see invalidation. Default is disabled.
        • log: logger instance pino compatible, default is disabled.

        Example

        createCache({ storage: { type: 'memory', options: { size: 2048 } } })
      • for redis type

        • client: a redis client instance, mandatory. Should be an ioredis client 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 instance pino compatible, 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 ttl based 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 stale value based on result.

    const cache = createCache()
    
    cache.define('fetchUserProfile', {
      ttl: 60,
      state: (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.fetchAccessToken()

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 memory and redis

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 ttl expiration, in this case the old value is shown until expiration by ttl.
  • it's not been updated during ttl expiration, 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) or strict. In lazy mode, only a chunk of the references are randomly checked, and probably freed; running lazy jobs tend to eventually clear all the expired references. In strict mode, all the references are checked and freed, and after that, references and entries are perfectly clean. lazy mode is the light heuristic way to ensure cached entries and references are cleared without stressing too much redis, strict mode at the opposite stress more redis to get a perfect result. The best strategy is to combine them both, running often lazy jobs along with some strict ones, depending on the size of the cache.

Options:

  • chunk: the chunk size of references analyzed per loops, default 64
  • lazy~chunk: the chunk size of references analyzed per loops in lazy mode, default 64; if both chunk and lazy.chunk is set, the maximum one is taken
  • lazy~cursor: the cursor offset, default zero; cursor should be set at report.cursor to 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()

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.


Maintainers


Breaking Changes

  • version 0.5.0 -> 0.6.0
    • options.cacheSize is dropped in favor of storage

License

MIT

More Repositories

1

autocannon

fast HTTP/1.1 benchmarking tool written in Node.js
JavaScript
7,779
star
2

fastq

Fast, in memory work queue
JavaScript
771
star
3

make-promises-safe

A node.js module to make the use of promises safe
JavaScript
669
star
4

hyperid

Uber-fast unique id generation, for Node.js and the browser
JavaScript
659
star
5

msgpack5

A msgpack v5 implementation for node.js, with extension points / msgpack.org[Node]
JavaScript
484
star
6

split2

Split Streams3 style
JavaScript
269
star
7

reusify

Reuse objects and functions with style
JavaScript
156
star
8

steed

horsepower for your modules
JavaScript
155
star
9

fastparallel

Zero-overhead parallel function call for node.js. Also supports each and map!
JavaScript
153
star
10

qest

The Internet of Things broker that loves devices and web developers.
JavaScript
144
star
11

close-with-grace

Exit your process, gracefully (if possible) - for Node.js
JavaScript
127
star
12

bloomrun

A js pattern matcher
JavaScript
120
star
13

mqemitter

An Opinionated Message Queue with an emitter-style API
JavaScript
118
star
14

on-exit-leak-free

Execute a function on exit without leaking memory, allowing all objects to be garbage collected
JavaScript
113
star
15

cloneable-readable

Clone a Readable stream, safely
JavaScript
106
star
16

loopbench

Benchmark your event loop
JavaScript
101
star
17

commist

Build your commands on minimist!
JavaScript
99
star
18

fast-json-parse

The fastest way to parse JSON safely
JavaScript
85
star
19

climem

Monitor the memory consumption of your node process via CLI
JavaScript
85
star
20

desm

get the file directory from import.meta.url
JavaScript
83
star
21

fastbench

the simplest benchmark you can run on node
JavaScript
83
star
22

syncthrough

Transform your data as it pass by, synchronously.
JavaScript
77
star
23

mows

Using MQTT.js in the browser over WebSocket -- Built with browserify!
JavaScript
73
star
24

hyperemitter

Horizontally Scalable EventEmitter powered by a Merkle DAG
JavaScript
71
star
25

heroku-buildpack-graphicsmagick

Shell
67
star
26

fastseries

Zero-overhead asynchronous series/each/map function calls
JavaScript
65
star
27

docker-loghose

Collect all the logs from all docker containers
JavaScript
63
star
28

hwp

JavaScript
60
star
29

tinysonic

a quick syntax for JSON object
JavaScript
56
star
30

fastify-sandbox

load a plugin via a synchronous worker
JavaScript
54
star
31

infinicache

JavaScript
53
star
32

ponte

The M2M/IoT Bridge for REST developers
51
star
33

public-speaking

Matteo Collina's portfolio of public speaking engagements
CSS
48
star
34

h2url

experimental http2 client for node and the CLI
JavaScript
46
star
35

autocannon-ci

run your benchmarks as part of your dev flow, for Node.js
JavaScript
46
star
36

heroku-buildpack-imagemagick

An heroku buildpack with the latest version of ImageMagick
Shell
46
star
37

multines

Multi-process nes backend, turn nes into a fully scalable solution
JavaScript
45
star
38

mercurius-auto-schema

JavaScript
43
star
39

native-hdr-histogram

node.js bindings for hdr histogram C implementation
C
42
star
40

mqemitter-redis

Redis-powered MQEmitter
JavaScript
41
star
41

tentacoli

All the ways for doing requests/streams multiplexing over a single stream
JavaScript
39
star
42

take-your-http-server-to-ludicrous-speed

Take Your HTTP server to Ludicrous Speed
HTML
38
star
43

manifetch

A manifest-based fetch() API client builder.
JavaScript
37
star
44

pino-roll

A Pino transport that automatically rolls your log files
JavaScript
36
star
45

openapi-graphql

Create a GraphQL from an OpenAPI schema
TypeScript
36
star
46

levelgraph-talk-nodejsconfit

My Talk at nodejsconf.it 2014! "How to Cook a Graph Database in a Night"
CSS
36
star
47

single-user-cache

JavaScript
35
star
48

retimer

reschedulable setTimeout for you node needs
JavaScript
35
star
49

generify

A reusable project generator
JavaScript
32
star
50

we-are-not-object-oriented-anymore-demo

The demo for my "we are not object-oriented anymore" talk
JavaScript
31
star
51

stream-iterators-utils

Utility belt for using async iterators with streams
JavaScript
30
star
52

node-errormailer

Sending email for each error in your node app was never easier! It fully support connect and express.
JavaScript
29
star
53

autocow

Display cows every two seconds, because you can
JavaScript
29
star
54

ioredis-auto-pipeline

Automatic redis pipeline support
JavaScript
27
star
55

typescript-async-await-target-cost

Shell
27
star
56

worker

Running Node within Node (a fork of synchronous-worker)
C++
25
star
57

undici-thread-interceptor

An Undici interceptor that routes requests over a worker thread
JavaScript
25
star
58

type-safe-fastify

An example on how to set up Fastify routes with full type safety
TypeScript
24
star
59

pbkdf2-password

Easy salt/password creation for Node.js, extracted from Mosca
JavaScript
24
star
60

one-two-three-fastify

JavaScript
23
star
61

mqtt-level-store

Store your in-flight MQTT message on Level, for Node
JavaScript
23
star
62

fastfall

call your callbacks in a waterfall, at speed
JavaScript
22
star
63

fastify-astro

Let's wrap Astro in a Fastify plugin
22
star
64

the-cost-of-logging

My talk "The Cost of Logging" about our uber-fast Pino logger
HTML
20
star
65

localswarm

Like airswarm, but using tcp ports and unix sockets - node.js style
JavaScript
20
star
66

unix-socket-leader

Elect a leader using unix sockets, for node
JavaScript
20
star
67

rake-minify

A rake task to minify javascripts and coffeescripts
Ruby
19
star
68

fastify-auth-mongo-jwt

Sample user-management (signup, login) with Fastify and JWT
JavaScript
19
star
69

docker-allcontainers

Get notified when a new container is started or stopped
JavaScript
19
star
70

mqemitter-mongodb

MongoDB based MQEmitter
JavaScript
18
star
71

fast-write-atomic

Fast way to write a file atomically, for Node.js
JavaScript
18
star
72

fastify-api

A radically simple API routing and method injection plugin for Fastify.
JavaScript
18
star
73

minimist

A fork of minimist, published as @matteo.collina/minimist
JavaScript
17
star
74

never-ending-stream

Automatically restarts your stream for you when it ends
JavaScript
16
star
75

throughv

stream.Transform with parallel chunk processing
JavaScript
16
star
76

fastify-massive

Massive.js plugin for Fastify
JavaScript
15
star
77

baseswim

A base swim node
JavaScript
15
star
78

modular_monolith

Example of the "Building a Modular Monolith with Fastify" talk
JavaScript
15
star
79

autocannon-compare

Compare two autocannon runs
JavaScript
15
star
80

dateformat

A CJS version of dateformat, forked from node-dateformat
JavaScript
14
star
81

kanban

Kanban is a node.js control-flow library. As the Japanese methodology, it is pull-based.
JavaScript
14
star
82

bhdr

benchmark utility powered by hdr histograms, for node
JavaScript
14
star
83

streampecker

Peek a stream!
JavaScript
13
star
84

blueslider

Turn your slides using you TI SensorTag
JavaScript
13
star
85

mcdo

JavaScript
13
star
86

reduplexer

reduplexer(writable, readable, options)
JavaScript
13
star
87

levelgraph-recursive

Breadth-first and Deep-first for your LevelGraph
JavaScript
13
star
88

object-router

Route your functions with pattern matching
JavaScript
12
star
89

help-me

Help command for node, partner of minimist and commist
JavaScript
12
star
90

mongo-clean

Clean all the collections in a mongo database
JavaScript
12
star
91

mqstreams

MQ pub/sub as streams - based on mqemitter
JavaScript
12
star
92

we-are-not-object-oriented-anymore

We are not Object Oriented anymore
HTML
11
star
93

levelup-talk-cloudconf

My talk for CloudConf on LevelUp
CSS
11
star
94

fastify-undici-dispatcher

An undici dispatcher to in-process Fastify servers
JavaScript
11
star
95

reaching-ludicrous-speed

My Node.js Interactive 2015 presentation
HTML
11
star
96

hello-fastify

A Fastify "hello world" template, with tests
JavaScript
11
star
97

net-object-stream

Turn any binary stream into an object stream
JavaScript
11
star
98

conf-app

JavaScript
10
star
99

capistrano-remote-cache-with-project-root

Ruby
10
star
100

nrts

node:test runner wrapper with TypeScript support
JavaScript
10
star