Skip to content

Commit 55265aa

Browse files
committed
fix(rpc): cache falsy RPC results (presence check, not truthiness)
1 parent cba158f commit 55265aa

3 files changed

Lines changed: 51 additions & 7 deletions

File tree

packages/devframe/src/client/rpc.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -316,14 +316,11 @@ export async function getDevframeRpcClient(
316316
async onRequest(req, next, resolve) {
317317
await rpcOptions.onRequest?.call(this, req, next, resolve)
318318
if (cacheOptions && cacheManager?.validate(req.m)) {
319-
const cached = cacheManager.cached(req.m, req.a)
320-
if (cached) {
321-
return resolve(cached)
322-
}
323-
else {
324-
const res = await next(req)
325-
cacheManager?.apply(req, res)
319+
if (cacheManager.has(req.m, req.a)) {
320+
return resolve(cacheManager.cached(req.m, req.a))
326321
}
322+
const res = await next(req)
323+
cacheManager.apply(req, res)
327324
}
328325
else {
329326
await next(req)

packages/devframe/src/rpc/cache.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,46 @@ it('cache', async () => {
1919
cache.clear()
2020
expect(cache.cached<number>('fn2', [3, 4])).toBeUndefined()
2121
})
22+
23+
it('serves falsy cached values via `has` (presence, not truthiness)', () => {
24+
const cache = new RpcCacheManager({ functions: ['fn'] })
25+
26+
// absent key: `has` reports false, distinct from a stored falsy value
27+
expect(cache.has('fn', ['zero'])).toBe(false)
28+
29+
cache.apply({ m: 'fn', a: ['zero'] }, 0)
30+
cache.apply({ m: 'fn', a: ['false'] }, false)
31+
cache.apply({ m: 'fn', a: ['empty'] }, '')
32+
cache.apply({ m: 'fn', a: ['null'] }, null)
33+
34+
expect(cache.has('fn', ['zero'])).toBe(true)
35+
expect(cache.has('fn', ['false'])).toBe(true)
36+
expect(cache.has('fn', ['empty'])).toBe(true)
37+
expect(cache.has('fn', ['null'])).toBe(true)
38+
39+
expect(cache.cached('fn', ['zero'])).toBe(0)
40+
expect(cache.cached('fn', ['false'])).toBe(false)
41+
expect(cache.cached('fn', ['empty'])).toBe('')
42+
expect(cache.cached('fn', ['null'])).toBe(null)
43+
})
44+
45+
it('the client cache path (has-then-cached) serves a falsy value without a second fetch', async () => {
46+
const cache = new RpcCacheManager({ functions: ['fn'] })
47+
let nextCalls = 0
48+
const next = async (req: { m: string, a: unknown[] }) => {
49+
nextCalls++
50+
cache.apply(req, 0)
51+
return 0
52+
}
53+
54+
// mirrors the onRequest cache path in client/rpc.ts
55+
async function callThroughCache(req: { m: string, a: unknown[] }) {
56+
if (cache.has(req.m, req.a))
57+
return cache.cached(req.m, req.a)
58+
return next(req)
59+
}
60+
61+
expect(await callThroughCache({ m: 'fn', a: [] })).toBe(0)
62+
expect(await callThroughCache({ m: 'fn', a: [] })).toBe(0)
63+
expect(nextCalls).toBe(1) // second call served from cache, not re-fetched
64+
})

packages/devframe/src/rpc/cache.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export class RpcCacheManager {
3333
return undefined
3434
}
3535

36+
has(m: string, a: unknown[]): boolean {
37+
return this.cacheMap.get(m)?.has(this.keySerializer(a)) ?? false
38+
}
39+
3640
apply(req: { m: string, a: unknown[] }, res: unknown): void {
3741
const methodCache = this.cacheMap.get(req.m) || new Map<string, unknown>()
3842
methodCache.set(this.keySerializer(req.a), res)

0 commit comments

Comments
 (0)