Skip to content

Commit

Permalink
Dedupe only with no ttl (mcollina#9)
Browse files Browse the repository at this point in the history
* Do not cache with no TTL

* Updated README
  • Loading branch information
mcollina authored Nov 16, 2021
1 parent 3a62439 commit 287b5b3
Showing 4 changed files with 199 additions and 160 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ Creates a new cache.

Options:

* `tll`: the maximum time a cache entry can live, default `0`
* `tll`: the maximum time a cache entry can live, default `0`; if `0`, an element is removed from the cache as soon as as the promise resolves.
* `cacheSize`: the maximum amount of entries to fit in the cache for each defined method, default `1024`.

### `cache.define(name[, opts], original(arg, cacheKey))`
5 changes: 4 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -90,9 +90,12 @@ class Wrapper {
buildPromise (query, args, key) {
query.promise = this.func(args, key)
// we fork the promise chain on purpose
query.promise.catch(() => this.ids.set(key, undefined))
const p = query.promise.catch(() => this.ids.set(key, undefined))
if (this.ttl > 0) {
query.cachedOn = currentSecond()
} else {
// clear the cache if there is no TTL
p.then(() => this.ids.set(key, undefined))
}
}

190 changes: 32 additions & 158 deletions test/base.test.js
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@

const { test } = require('tap')
const { Cache } = require('..')
const { AsyncLocalStorage } = require('async_hooks')
const stringify = require('safe-stable-stringify')

const kValues = require('../symbol')
@@ -226,180 +225,55 @@ test('cacheSize on constructor', async (t) => {
])
})

test('AsyncLocalStoreage', (t) => {
t.plan(5)
const als = new AsyncLocalStorage()
test('throws for methods in the property chain', async function (t) {
const cache = new Cache()

cache.define('fetchSomething', async (query) => {
t.equal(query, 42)
return { k: query }
})

als.run({ foo: 'bar' }, function () {
setImmediate(function () {
cache.fetchSomething(42).then((res) => {
t.same(res, { k: 42 })
t.same(als.getStore(), { foo: 'bar' })
})
})
})
const keys = [
'toString',
'hasOwnProperty',
'define',
'clear'
]

als.run({ bar: 'foo' }, function () {
setImmediate(function () {
cache.fetchSomething(42).then((res) => {
t.same(res, { k: 42 })
t.same(als.getStore(), { bar: 'foo' })
})
for (const key of keys) {
t.throws(() => {
cache.define(key, () => {})
})
})
}
})

test('do not cache failures', async (t) => {
t.plan(4)

const cache = new Cache()
test('automatically expires with no TTL', async (t) => {
// plan verifies that fetchSomething is called only once
t.plan(10)

let called = false
cache.define('fetchSomething', async (query) => {
t.pass('called')
if (!called) {
called = true
throw new Error('kaboom')
let hits = 0
const cache = new Cache({
onHit () {
hits++
}
return { k: query }
})

await t.rejects(cache.fetchSomething(42))
t.same(await cache.fetchSomething(42), { k: 42 })
})

test('clear the full cache', async (t) => {
t.plan(7)

const cache = new Cache()

cache.define('fetchA', async (query) => {
t.pass('a called')
return { k: query }
})

cache.define('fetchB', async (query) => {
t.pass('b called')
return { j: query }
})

t.same(await Promise.all([
cache.fetchA(42),
cache.fetchB(24)
]), [
{ k: 42 },
{ j: 24 }
])

t.same(await Promise.all([
cache.fetchA(42),
cache.fetchB(24)
]), [
{ k: 42 },
{ j: 24 }
])

cache.clear()

t.same(await Promise.all([
cache.fetchA(42),
cache.fetchB(24)
]), [
{ k: 42 },
{ j: 24 }
])
})

test('clears only one method', async (t) => {
t.plan(6)

const cache = new Cache()

cache.define('fetchA', async (query) => {
t.pass('a called')
return { k: query }
})

cache.define('fetchB', async (query) => {
t.pass('b called')
return { j: query }
})

t.same(await Promise.all([
cache.fetchA(42),
cache.fetchB(24)
]), [
{ k: 42 },
{ j: 24 }
])

t.same(await Promise.all([
cache.fetchA(42),
cache.fetchB(24)
]), [
{ k: 42 },
{ j: 24 }
])

cache.clear('fetchA')

t.same(await Promise.all([
cache.fetchA(42),
cache.fetchB(24)
]), [
{ k: 42 },
{ j: 24 }
])
})

test('clears only one method with one value', async (t) => {
t.plan(5)

const cache = new Cache()
const expected = [42, 24, 42]

cache.define('fetchA', async (query) => {
t.pass('a called')
cache.define('fetchSomething', async (query, cacheKey) => {
t.equal(query, expected.shift())
t.equal(stringify(query), cacheKey)
return { k: query }
})

t.same(await Promise.all([
cache.fetchA(42),
cache.fetchA(24)
]), [
{ k: 42 },
{ k: 24 }
])
const p1 = cache.fetchSomething(42)
const p2 = cache.fetchSomething(24)
const p3 = cache.fetchSomething(42)

cache.clear('fetchA', 42)
const res = await Promise.all([p1, p2, p3])

t.same(await Promise.all([
cache.fetchA(42),
cache.fetchA(24)
]), [
t.same(res, [
{ k: 42 },
{ k: 24 }
{ k: 24 },
{ k: 42 }
])
})

test('throws for methods in the property chain', async function (t) {
const cache = new Cache()

const keys = [
'toString',
'hasOwnProperty',
'define',
'clear'
]
t.equal(hits, 1)

for (const key of keys) {
t.throws(() => {
cache.define(key, () => {})
})
}
t.same(await cache.fetchSomething(42), { k: 42 })
t.equal(hits, 1)
})
Loading

0 comments on commit 287b5b3

Please sign in to comment.