@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
).
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
:
- clear the cache with
CacheMap.clear
; - delete an item from the cache with
CacheMap.delete
; - update the value of a cached item with
CacheMap.set
.
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 withMap
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 baseCacheMap
. We could name itSuperCacheMap
orRichCacheMap
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 forclass
fields (Safari 14.0) startingv1.32.0
.
Security
See the security policy.
Contributing
See the contributing guidelines.
License
The @frontacles/cachemap
package is open-sourced software licensed under the DWTFYWTPL.