Skip to content

Commit 3da404d

Browse files
author
dustin deus
committed
return caching and ttl informations, short circuit with memoized cache
1 parent 3155ae0 commit 3da404d

File tree

2 files changed

+89
-23
lines changed

2 files changed

+89
-23
lines changed

src/http-data-source.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export type Request = UndiciRequestOptions &
4242

4343
export type Response<TResult> = {
4444
body: TResult
45+
memoized: boolean
46+
isFromCache: boolean
47+
maxTtl?: number
4548
} & Omit<ResponseData, 'body'>
4649

4750
export interface LRUOptions {
@@ -257,14 +260,16 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
257260
}
258261

259262
const response: Response<TResult> = {
263+
isFromCache: false,
264+
memoized: false,
260265
...responseData,
261266
body: json,
262267
}
263268

264269
this.onResponse<TResult>(options, response)
265270

271+
// let's see if we can fill the shared cache
266272
if (options.requestCache && this.isResponseCacheable<TResult>(options, response)) {
267-
// TODO log errors with external logger
268273
this.storageAdapter
269274
.set(cacheKey, response, options.requestCache?.maxTtl)
270275
.catch((err) => this.logger?.error(err))
@@ -282,6 +287,8 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
282287
`staleIfError:${cacheKey}`,
283288
)
284289
if (hasFallback) {
290+
hasFallback.isFromCache = true
291+
hasFallback.maxTtl = options.requestCache.maxTtlIfError
285292
return hasFallback
286293
}
287294
}
@@ -296,29 +303,37 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
296303
}
297304

298305
const cacheKey = this.onCacheKeyCalculation(request)
299-
const ttlCacheEnabled = request.requestCache
300306

301307
// check if we have any GET call in the cache and respond immediatly
302-
if (request.method === 'GET' && ttlCacheEnabled) {
303-
const cachedResponse = await this.storageAdapter.get(cacheKey)
308+
if (request.method === 'GET') {
309+
// Memoize GET calls for the same data source instance
310+
// a single instance of the data sources is scoped to one graphql request
311+
const cachedResponse = this.memoizedResults.get(cacheKey)
304312
if (cachedResponse) {
313+
cachedResponse.memoized = true
314+
cachedResponse.isFromCache = false
305315
return cachedResponse
306316
}
317+
318+
// try to fetch from shared cache
319+
if (request.requestCache) {
320+
const cachedResponse: Response<TResult> = await this.storageAdapter.get(cacheKey)
321+
if (cachedResponse) {
322+
cachedResponse.memoized = false
323+
cachedResponse.isFromCache = true
324+
cachedResponse.maxTtl = request.requestCache.maxTtl
325+
return cachedResponse
326+
}
327+
}
307328
}
308329

309330
const options = {
310331
...this.globalRequestOptions,
311332
...request,
312333
}
313334

314-
// Memoize GET calls for the same data source instance
315-
// a single instance of the data sources is scoped to one graphql request
335+
// let's see if we can fill the memoized cache
316336
if (options.method === 'GET') {
317-
const cachedResponse = this.memoizedResults.get(cacheKey)
318-
if (cachedResponse) {
319-
return cachedResponse
320-
}
321-
322337
const response = await this.performRequest<TResult>(options, cacheKey)
323338

324339
if (this.isResponseCacheable<TResult>(options, response)) {

test/rest-data-source.test.ts

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ setGlobalDispatcher(agent)
1616
const test = anyTest as TestInterface<{ path: string }>
1717

1818
test('Should be able to make a simple GET call', async (t) => {
19-
t.plan(2)
19+
t.plan(5)
2020

2121
const path = '/'
2222

@@ -46,6 +46,9 @@ test('Should be able to make a simple GET call', async (t) => {
4646

4747
const response = await dataSource.getFoo()
4848

49+
t.false(response.isFromCache)
50+
t.false(response.memoized)
51+
t.falsy(response.maxTtl)
4952
t.deepEqual(response.body, { name: 'foo' })
5053
})
5154

@@ -181,8 +184,8 @@ test('Should be able to pass query params', async (t) => {
181184
query: {
182185
a: 1,
183186
b: '2',
184-
c: undefined
185-
}
187+
c: undefined,
188+
},
186189
})
187190
}
188191
})()
@@ -230,7 +233,7 @@ test('Should error', async (t) => {
230233
})
231234

232235
test('Should memoize subsequent GET calls to the same endpoint', async (t) => {
233-
t.plan(5)
236+
t.plan(17)
234237

235238
const path = '/'
236239

@@ -260,15 +263,27 @@ test('Should memoize subsequent GET calls to the same endpoint', async (t) => {
260263

261264
let response = await dataSource.getFoo()
262265
t.deepEqual(response.body, { name: 'foo' })
266+
t.false(response.isFromCache)
267+
t.false(response.memoized)
268+
t.falsy(response.maxTtl)
263269

264270
response = await dataSource.getFoo()
265271
t.deepEqual(response.body, { name: 'foo' })
272+
t.false(response.isFromCache)
273+
t.true(response.memoized)
274+
t.falsy(response.maxTtl)
266275

267276
response = await dataSource.getFoo()
268277
t.deepEqual(response.body, { name: 'foo' })
278+
t.false(response.isFromCache)
279+
t.true(response.memoized)
280+
t.falsy(response.maxTtl)
269281

270282
response = await dataSource.getFoo()
271283
t.deepEqual(response.body, { name: 'foo' })
284+
t.false(response.isFromCache)
285+
t.true(response.memoized)
286+
t.falsy(response.maxTtl)
272287
})
273288

274289
test('Should be able to define a custom cache key for request memoization', async (t) => {
@@ -570,7 +585,7 @@ test('Initialize data source with cache and context', async (t) => {
570585
})
571586

572587
test('Response is cached', async (t) => {
573-
t.plan(6)
588+
t.plan(16)
574589

575590
const path = '/'
576591

@@ -589,7 +604,7 @@ test('Response is cached', async (t) => {
589604

590605
const baseURL = `http://localhost:${(server.address() as AddressInfo)?.port}`
591606

592-
const dataSource = new (class extends HTTPDataSource {
607+
let dataSource = new (class extends HTTPDataSource {
593608
constructor() {
594609
super(baseURL)
595610
}
@@ -604,8 +619,7 @@ test('Response is cached', async (t) => {
604619
})()
605620

606621
const map = new Map<string, string>()
607-
608-
dataSource.initialize({
622+
const datasSourceConfig = {
609623
context: {
610624
a: 1,
611625
},
@@ -620,14 +634,42 @@ test('Response is cached', async (t) => {
620634
map.set(key, value)
621635
},
622636
},
623-
})
637+
}
624638

625-
let response = await dataSource.getFoo()
639+
dataSource.initialize(datasSourceConfig)
626640

641+
let response = await dataSource.getFoo()
642+
t.false(response.isFromCache)
643+
t.false(response.memoized)
644+
t.falsy(response.maxTtl)
627645
t.deepEqual(response.body, { name: 'foo' })
628646

629647
response = await dataSource.getFoo()
648+
t.false(response.isFromCache)
649+
t.true(response.memoized)
650+
t.falsy(response.maxTtl)
651+
t.deepEqual(response.body, { name: 'foo' })
652+
653+
dataSource = new (class extends HTTPDataSource {
654+
constructor() {
655+
super(baseURL)
656+
}
657+
getFoo() {
658+
return this.get(path, {
659+
requestCache: {
660+
maxTtl: 100,
661+
maxTtlIfError: 200,
662+
},
663+
})
664+
}
665+
})()
666+
667+
dataSource.initialize(datasSourceConfig)
630668

669+
response = await dataSource.getFoo()
670+
t.true(response.isFromCache)
671+
t.false(response.memoized)
672+
t.is(response.maxTtl, 100)
631673
t.deepEqual(response.body, { name: 'foo' })
632674

633675
const cached = JSON.parse(map.get('keyv:' + baseURL + path)!)
@@ -650,7 +692,7 @@ test('Response is cached', async (t) => {
650692
})
651693

652694
test('Fallback from cache on origin error', async (t) => {
653-
t.plan(6)
695+
t.plan(12)
654696

655697
const path = '/'
656698

@@ -711,6 +753,9 @@ test('Fallback from cache on origin error', async (t) => {
711753
dataSource.initialize(datasSourceConfig)
712754

713755
let response = await dataSource.getFoo()
756+
t.false(response.isFromCache)
757+
t.false(response.memoized)
758+
t.falsy(response.maxTtl)
714759

715760
t.deepEqual(response.body, { name: 'foo' })
716761

@@ -735,14 +780,17 @@ test('Fallback from cache on origin error', async (t) => {
735780
dataSource.initialize(datasSourceConfig)
736781

737782
response = await dataSource.getFoo()
783+
t.true(response.isFromCache)
784+
t.false(response.memoized)
785+
t.is(response.maxTtl, 200)
738786

739787
t.deepEqual(response.body, { name: 'foo' })
740788

741789
t.is(map.size, 1)
742790
})
743791

744792
test('Should not cache POST requests', async (t) => {
745-
t.plan(3)
793+
t.plan(6)
746794

747795
const path = '/'
748796

@@ -796,6 +844,9 @@ test('Should not cache POST requests', async (t) => {
796844
})
797845

798846
const response = await dataSource.postFoo()
847+
t.false(response.isFromCache)
848+
t.false(response.memoized)
849+
t.falsy(response.maxTtl)
799850

800851
t.deepEqual(response.body, { name: 'foo' })
801852

0 commit comments

Comments
 (0)