Skip to content

Commit c4d7a93

Browse files
BnayaJulien-R44
andauthored
fix: lock timeout overrides timeout 0 (#104)
Co-authored-by: Julien Ripouteau <julien@ripouteau.com>
1 parent 2525d2e commit c4d7a93

3 files changed

Lines changed: 107 additions & 0 deletions

File tree

.changeset/fix-locktimeout-swr.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'bentocache': patch
3+
---
4+
5+
Fix lockTimeout overriding timeout:0 SWR behavior. When timeout is set to 0 (stale-while-revalidate mode), subsequent requests made during background revalidation now return stale data immediately as expected, regardless of lockTimeout setting. Fixes #103
6+

packages/bentocache/src/cache/cache_entry/cache_entry_options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ export function createCacheEntryOptions(
176176
* lock to be acquired
177177
*/
178178
getApplicableLockTimeout(hasFallbackValue: boolean) {
179+
/**
180+
* If we're in SWR mode (timeout: 0 with fallback), we should
181+
* return 0 regardless of lockTimeout to ensure immediate return
182+
*/
183+
if (self.shouldSwr(hasFallbackValue)) return 0
184+
179185
if (lockTimeout) return lockTimeout
180186

181187
/**
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { test } from '@japa/runner'
2+
import { sleep } from '@julr/utils/misc'
3+
4+
import { CacheFactory } from '../../factories/cache_factory.js'
5+
6+
test.group('SWR with background revalidation', () => {
7+
test('should return stale immediately when factory is running in background', async ({
8+
assert,
9+
}) => {
10+
const { cache } = new CacheFactory()
11+
.merge({
12+
ttl: 100,
13+
grace: '6h',
14+
timeout: 0,
15+
})
16+
.withL1L2Config()
17+
.create()
18+
19+
await cache.set({ key: 'key', value: 'stale value' })
20+
await sleep(150)
21+
22+
let factoryCallCount = 0
23+
const slowFactory = async () => {
24+
factoryCallCount++
25+
await sleep(1000)
26+
return `fresh value ${factoryCallCount}`
27+
}
28+
29+
const start1 = Date.now()
30+
const result1 = await cache.getOrSet({ key: 'key', factory: slowFactory })
31+
const elapsed1 = Date.now() - start1
32+
33+
await sleep(100)
34+
35+
const start2 = Date.now()
36+
const result2 = await cache.getOrSet({ key: 'key', factory: slowFactory })
37+
const elapsed2 = Date.now() - start2
38+
39+
assert.equal(result1, 'stale value')
40+
assert.equal(result2, 'stale value')
41+
assert.isBelow(elapsed1, 100)
42+
assert.isBelow(elapsed2, 100)
43+
assert.equal(factoryCallCount, 1)
44+
45+
await sleep(1100)
46+
47+
const result3 = await cache.get({ key: 'key' })
48+
assert.equal(result3, 'fresh value 1')
49+
})
50+
51+
test('lockTimeout should not prevent immediate return when timeout is 0', async ({ assert }) => {
52+
const { cache } = new CacheFactory()
53+
.merge({
54+
ttl: 100,
55+
grace: '6h',
56+
timeout: 0,
57+
lockTimeout: 5000,
58+
})
59+
.withL1L2Config()
60+
.create()
61+
62+
await cache.set({ key: 'key', value: 'stale value' })
63+
await sleep(150)
64+
65+
let factoryCallCount = 0
66+
67+
const start1 = Date.now()
68+
const promise1 = cache.getOrSet({
69+
key: 'key',
70+
factory: async () => {
71+
factoryCallCount++
72+
await sleep(2000)
73+
return `factory-call-${factoryCallCount}`
74+
},
75+
})
76+
const result1 = await promise1
77+
const elapsed1 = Date.now() - start1
78+
79+
const start2 = Date.now()
80+
const result2 = await cache.getOrSet({
81+
key: 'key',
82+
factory: async () => {
83+
factoryCallCount++
84+
return `factory-call-${factoryCallCount}`
85+
},
86+
})
87+
const elapsed2 = Date.now() - start2
88+
89+
assert.equal(result1, 'stale value')
90+
assert.equal(result2, 'stale value')
91+
assert.isBelow(elapsed1, 100)
92+
assert.isBelow(elapsed2, 100)
93+
assert.equal(factoryCallCount, 1)
94+
}).timeout(10_000)
95+
})

0 commit comments

Comments
 (0)