Skip to content

Commit

Permalink
LastModified
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Feb 15, 2023
1 parent 897b2b1 commit 74dd041
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 39 deletions.
15 changes: 13 additions & 2 deletions packages/plugins/response-cache/__tests__/etag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,35 @@ describe('Response Caching via ETag', () => {
'http://localhost:4000/graphql?query=' + query,
)
expect(response.headers.get('ETag')).toEqual(query)
const lastModified = response.headers.get('Last-Modified')
expect(lastModified).toBeTruthy()
const lastModifiedDate = new Date(
lastModified || 'Expected Last-Modified to be a valid date',
)
expect(lastModifiedDate).toBeInstanceOf(Date)
expect(lastModifiedDate.toString()).not.toEqual('Invalid Date')
expect(lastModifiedDate.getDate()).toEqual(new Date().getDate())
})
it('should respond 304 when the ETag matches', async () => {
it('should respond 304 when the ETag and Last-Modified matches', async () => {
const tomorrow = new Date()
const query = '{me{id,name}}'
const response = await yoga.fetch(
'http://localhost:4000/graphql?query=' + query,
{
headers: {
'If-None-Match': query,
'If-Modified-Since': tomorrow.toString(),
},
},
)
expect(response.status).toEqual(304)
expect(cnt).toEqual(0)
})
it('should not send ETag if the result is not cached', async () => {
it.only('should not send ETag or Last-Modified if the result is not cached', async () => {
const response = await yoga.fetch(
'http://localhost:4000/graphql?query={me(throwError:true){id,name}}',
)
expect(response.headers.get('ETag')).toBeFalsy()
expect(response.headers.get('Last-Modified')).toBeFalsy()
})
})
10 changes: 5 additions & 5 deletions packages/plugins/response-cache/__tests__/response-cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ it('should not hit GraphQL pipeline if cached', async () => {
body: JSON.stringify({ query: '{ _ }' }),
})
const body2 = await response2.json()
expect(body2).toEqual({
expect(body2).toMatchObject({
data: {
_: 'DUMMY',
},
Expand Down Expand Up @@ -111,7 +111,7 @@ it('cache a query operation', async () => {
response = await fetch()
expect(response.status).toEqual(200)
body = await response.json()
expect(body).toEqual({
expect(body).toMatchObject({
data: {
__typename: 'Query',
},
Expand Down Expand Up @@ -148,7 +148,7 @@ it('cache a query operation per session', async () => {

expect(response.status).toEqual(200)
let body = await response.json()
expect(body).toEqual({
expect(body).toMatchObject({
data: {
__typename: 'Query',
},
Expand All @@ -164,7 +164,7 @@ it('cache a query operation per session', async () => {
response = await fetch('1')
expect(response.status).toEqual(200)
body = await response.json()
expect(body).toEqual({
expect(body).toMatchObject({
data: {
__typename: 'Query',
},
Expand All @@ -179,7 +179,7 @@ it('cache a query operation per session', async () => {

expect(response.status).toEqual(200)
body = await response.json()
expect(body).toEqual({
expect(body).toMatchObject({
data: {
__typename: 'Query',
},
Expand Down
83 changes: 58 additions & 25 deletions packages/plugins/response-cache/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import {
useResponseCache as useEnvelopResponseCache,
UseResponseCacheParameter as UseEnvelopResponseCacheParameter,
} from '@envelop/response-cache'
import { Maybe, Plugin, PromiseOrValue, YogaInitialContext } from 'graphql-yoga'
import { ExecutionResult } from 'graphql'
import {
Maybe,
Plugin,
PromiseOrValue,
YogaInitialContext,
YogaLogger,
} from 'graphql-yoga'

export type UseResponseCacheParameter = Omit<
UseEnvelopResponseCacheParameter,
Expand Down Expand Up @@ -50,8 +57,12 @@ export function useResponseCache(options: UseResponseCacheParameter): Plugin {
options?.buildResponseCacheKey || defaultBuildResponseCacheKey
const cache = options.cache ?? createInMemoryCache()
const enabled = options.enabled ?? (() => true)
const cachedByRequest = new WeakMap<Request, boolean>()
const cachedLastModifiedByRequest = new WeakMap<Request, string>()
let logger: YogaLogger
return {
onYogaInit({ yoga }) {
logger = yoga.logger
},
onPluginInit({ addPlugin }) {
addPlugin(
useEnvelopResponseCache({
Expand All @@ -60,6 +71,20 @@ export function useResponseCache(options: UseResponseCacheParameter): Plugin {
getDocumentString: getDocumentStringForEnvelop,
session: sessionFactoryForEnvelop,
buildResponseCacheKey: cacheKeyFactoryForEnvelop,
shouldCacheResult({ result }) {
const shouldCached = options.shouldCacheResult
? options.shouldCacheResult({ result })
: !result.errors?.length
if (shouldCached) {
result.extensions ||= {}
result.extensions.responseCache ||= {}
result.extensions.responseCache!['lastModified'] =
new Date().toString()
} else {
logger.warn('[useResponseCache] Failed to cache due to errors')
}
return shouldCached
},
includeExtensionMetadata: true,
}),
)
Expand All @@ -70,13 +95,22 @@ export function useResponseCache(options: UseResponseCacheParameter): Plugin {
if (operationId) {
const cachedResponse = await cache.get(operationId)
if (cachedResponse) {
const okResponse = new fetchAPI.Response(null, {
status: 304,
headers: {
ETag: operationId,
},
})
endResponse(okResponse)
const lastModifiedFromClient =
request.headers.get('If-Modified-Since')
const lastModifiedFromCache =
cachedResponse.extensions?.responseCache?.['lastModified']
if (
new Date(lastModifiedFromClient!).getTime() >=
new Date(lastModifiedFromCache).getTime()
) {
const okResponse = new fetchAPI.Response(null, {
status: 304,
headers: {
ETag: operationId,
},
})
endResponse(okResponse)
}
}
}
}
Expand All @@ -92,35 +126,34 @@ export function useResponseCache(options: UseResponseCacheParameter): Plugin {
operationIdByRequest.set(request, operationId)
const cachedResponse = await cache.get(operationId)
if (cachedResponse) {
if (options.includeExtensionMetadata) {
setResult({
...cachedResponse,
extensions: {
responseCache: {
hit: true,
},
},
})
} else {
setResult(cachedResponse)
}
cachedResponse.extensions ||= {}
cachedResponse.extensions.responseCache ||= {}
;(cachedResponse.extensions.responseCache as { hit: boolean }).hit =
true
setResult(cachedResponse)
return
}
}
},
onResultProcess({ request, result }) {
if ('extensions' in result && result.extensions?.responseCache != null) {
cachedByRequest.set(request, true)
const executionResult = result as ExecutionResult
if (executionResult.extensions?.responseCache != null) {
cachedLastModifiedByRequest.set(
request,
executionResult.extensions.responseCache['lastModified'],
)
if (!options.includeExtensionMetadata) {
result.extensions.responseCache = undefined
executionResult.extensions.responseCache = undefined
}
}
},
onResponse({ response, request }) {
if (cachedByRequest.get(request)) {
const lastModified = cachedLastModifiedByRequest.get(request)
if (lastModified) {
const operationId = operationIdByRequest.get(request)
if (operationId) {
response.headers.set('ETag', operationId)
response.headers.set('Last-Modified', lastModified)
}
}
},
Expand Down
Loading

0 comments on commit 74dd041

Please sign in to comment.