npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@frontacles/cachemap

v1.0.0-0

Published

A cache done by extending the `Map` object.

Downloads

5

Readme

CacheMap

The CacheMap class extends the Map object to use it as a key-value cache.

It shines in situations when you want to cache values that are derived state or that are the result of an async operation (e.g. fetch).

Node.js CI

The package is lightweight (~ 0.5 KB compressed, not tree-shakeable (it’s a class!), typed and tested.

It’s mostly inspired by how Laravel Cache::remember works.

Installation

Install the package:

npm install @frontacles/cachemap

Import CacheMap in your script:

import CacheMap from '@frontacles/cachemap'

Not using a package manager? Download the package files in your project and take the files in /src.

The CacheMap class

CacheMap brings some methods that can all cache values.

The methods specific to CacheMap are all designed to create a new item in the cache: if the key already exists, the cache item won’t be touched.

If you want to touch a cached item, you can use the regular Map methods, all available in CacheMap, all inherited from Map:

Overview

Create a cache (or many caches):

import CacheMap from '@frontacles/cachemap'

const cache = new CacheMap() // no parameter creates an empty cache

const SuperMarioBros3Cache = new CacheMap([ // init with array of ['key', value]
  ['key', 'value'],
  ['country', 'Mushroom Kingdom'],
  ['hierarchy', {
    boss: 'Bowser',
    chiefs: ['Lemmy', 'Iggy', 'Morton', 'Larry', 'Ludwig', 'Wendy', 'Roy'],
    randos: ['Goomba', 'Koopa Troopa', 'Cheep cheep', 'Pirhana Plant']
  }],
])

Add new items in the cache using CacheMap.add:

const cache = SuperMarioBros3Cache // rename our SMB3 cache, for convenience

cache
  .add('plumbers', ['Mario', 'Luigi']) // returns `cache`, allowing chaining
  .add('tiny assistant', 'Toad')
  // .clear() // uncomment this line to kill everyone

Cache and return using CacheMap.remember:

cache.remember('last visited level', '1-3') // 1-3
cache.remember('last visited level', '8-2') // still returns '1-3', it was cached!

Cache and return the computed value of a function using CacheMap.remember:

cache.remember('bonus', () => randomFrom(['Mushroom', 'Fire flower', 'Star']))

Asynchronously cache and return (as a Promise) the result of an async function using CacheMap.rememberAsync:

const tinyHouse = await cache.rememberAsync('tiny house', prepareTinyHouse)

async function prepareTinyHouse() {
  return computeChestContent().then(chest => chest.toByteBuffer())
}

CacheMap.add

CacheMap.add updates the cache if the key is new, and returns its CacheMap instance, allowing fluent usage (methods chaining).

import CacheMap from '@frontacles/cachemap'
const cache = new CacheMap()

const nextFullMoonInBrussels = new Date(Date.parse('2023-08-31T03:35:00+02:00'))

cache
  .add('next full moon', nextFullMoonInBrussels)
  .add('cloud conditions', 'hopefully decent')
  .add('next full moon', 'yesterday') // won’t be changed, key already exists!

// CacheMap(2) [Map] {
//   'next full moon' => 2023-08-13T14:04:51.876Z,
//   'cloud conditions' => 'hopefully decent'
// }

CacheMap.remember

CacheMap.remember adds caches a value to the cache, and returns it. It takes a primitive value or a callback function returning the value that is then stored in the cache.

Like CacheMap.add, it only updates the cache if the key is new. The returned value is always the cached one.

const bills = [13.52, 17, 4.20, 21.6]

cache.remember('money you owe me', () => sum(bills))

// CacheMap(1) [Map] { 'money you owe me' => 56.32 }

bills.push(25.63)

cache.remember('money you owe me', () => sum(bills))

// CacheMap(1) [Map] { 'money you owe me' => 56.32 }

On the second usage of cache.remember in the previous example, the function doesn’t run at all: as the key already exists in the cache, its value is immediatly returned.

CacheMap.rememberAsync

CacheMap.rememberAsync is excatly the same as CacheMap.remember, except that:

  • it also accepts an async function (on top of a sync one or a primitive value);
  • it returns a Promise resolving into the cached value.

This makes it handy for network operations like fetch.

const todayCelsiusInParis = () => fetch('https://wttr.in/Paris?format=j1')
  .then(response => response.json())
  .then(({ weather }) => `${weather[0].mintempC}-${[0].maxtempC}`)

const parisCelsius = await cache.rememberAsync('temperature', todayCelsiusInParis) // 17-26

// CacheMap(1) [Map] { 'temperature' => '17-26' }

cache.rememberAsync('rainy or not', 'you can hide').then(console.log) // 'you can hide'

// CacheMap(2) [Map] {
//   'temperature' => '17-26'
//   'rain' => 'you can hide'
// }

Better derived states with remember

Getters are very convenient features available in objects and classes, allowing to compute a derived value from another simply by calling a property, instead of having to manually update it with a function:

const obj = {
  log: ['a', 'b', 'c'],

  get latest() {
    return this.log[this.log.length - 1]
  },
}

console.log(obj.latest) // 'c'

Without getters, we would have need a manual operation:

const obj = {
  log: ['a', 'b', 'c'],

  latest: null,

  updateLatest: () => {
    this.latest = this.log.length - 1
  }
}

console.log(obj.latest) // null

obj.updateLatest()
console.log(obj.latest) // 'c'

obj.log.push('d') // `obj.latest` is still 'c'

obj.updateLatest()
console.log(obj.latest) // 'd'

(Or, alternatively, work around this by having a obj.latest() function doing the computation on the fly, exactly like get latest(), but it means you then have to write obj.latest() instead of obj.latest.)

Enters CacheMap.remember (and CacheMap.rememberAsync) to avoid running the get/latest() computation each time we need this data.

In the following example, the Ranking class constructor receives an array of scores, and, from there, getters are used to compute once the podium 🏆 and the average. New computations of the derived values are only needed after a new entry is pushed into the list of scores in Ranking.add.

class Ranking {

  #scores
  #cache = new CacheMap()

  constructor(scores) {
    this.#scores = scores
  }

  // Extract the podium.

  get podium() {
    return this.#cache.remember('podium', () => {
      console.log('Extracting the podium…')
      const scores = structuredClone(this.#scores)
      scores.sort((a, b) => b - a)
      return scores.slice(0, 3)
    })
  }

  // Compute the average.

  get average() {
    return this.#cache.remember('average', () => {
      console.log('Computing the average score…')

      const sum = numbers => numbers.reduce((acc, val) => acc + val, 0)
      return sum(this.#scores) / this.#scores.length
    })
  }

  // Push a new score.

  add(score) {
    this.#scores.push(score)
    this.#cache.clear() // invalidate the cache, so it gets recomputed next time we access podium or average
  }
}

const ranking = new Ranking([17, 9, 651, 4, 19.8, 231])

console.log(ranking.podium)
// Extracting the podium…
// [ 651, 231, 19.8 ]

console.log(ranking.podium) // does not print “Extracting the podium” a second time, because the cached value is returned!
// [ 651, 231, 19.8 ]

ranking.add(91)
console.log(ranking.podium) // the cache has been invalidated, so the function runs again
// Extracting the podium…
// [ 651, 231, 91 ]

As you can see, computation is only done when needed. Other example of this behaviour.

Clear the cache

You can clear the whole cache with CacheMap.clear, or only forget 1 key with CacheMap.delete.

import CacheMap from '@frontacles/cachemap'

const scores = new CacheMap()
scores.add('Elvira', '68')
scores.add('Loulou', '54')
scores.add('Mehdi', '74')

// forget 1 cache key
scores.delete('Mehdi') // [Map Iterator] { 'Elvira', 'Loulou' }

// forget all keys
scores.clear() // [Map Iterator] {  }

Ideas

(@todo: move this to issues)

  • Cache with expiration.
  • Cache until a condition is met (could be merged with previous: expiration).
  • IndexedDB save/load (IndexedDB is the only reliable browser storage that [can store Map objects](because it’s compatible with Map objects: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#javascript_types)).
  • LRU (last recently used) to delete oldest created or oldest accessed items when the cache size reaches a given limit.
  • Evaluate the need/benefits to use WeakMap.
  • Enrich map with convenient functions like deleteMany. It could be part of another class extending the base CacheMap. We could name it SuperCacheMap or RichCacheMap or something like this.

Changelog

See CHANGELOG.md or the releases.

Browser and tooling support

@frontacles/cachemap is provided as module for modern browsers usage with standard JavaScript syntax:

  • it is up to you to transpile it for legacy browsers;
  • you can’t import it using require('@frontacles/cachemap'); @todo: CHECK FOR cachemap - if you don’t transpile it, DateTime requires support for class fields (Safari 14.0) starting v1.32.0.

Read more about ESModules.

Security

See the security policy.

Contributing

See the contributing guidelines.

License

The @frontacles/cachemap package is open-sourced software licensed under the DWTFYWTPL.