Created
November 23, 2020 02:29
-
-
Save iamspark1e/92da71de55f2be2e8a13b97305a36b78 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const getAssetFromKV = async (event: FetchEvent, options?: Partial<Options>): Promise<Response> => { | |
// Assign any missing options passed in to the default | |
options = Object.assign( | |
{ | |
ASSET_NAMESPACE: __STATIC_CONTENT, | |
ASSET_MANIFEST: __STATIC_CONTENT_MANIFEST, | |
mapRequestToAsset: mapRequestToAsset, | |
cacheControl: defaultCacheControl, | |
defaultMimeType: 'text/plain', | |
}, | |
options, | |
) | |
const request = event.request | |
const ASSET_NAMESPACE = options.ASSET_NAMESPACE | |
const ASSET_MANIFEST = typeof (options.ASSET_MANIFEST) === 'string' | |
? JSON.parse(options.ASSET_MANIFEST) | |
: options.ASSET_MANIFEST | |
if (typeof ASSET_NAMESPACE === 'undefined') { | |
throw new InternalError(`there is no KV namespace bound to the script`) | |
} | |
const SUPPORTED_METHODS = ['GET', 'HEAD'] | |
if (!SUPPORTED_METHODS.includes(request.method)) { | |
throw new MethodNotAllowedError(`${request.method} is not a valid request method`) | |
} | |
const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, '') // strip any preceding /'s | |
let pathIsEncoded = false | |
let requestKey | |
if (ASSET_MANIFEST[rawPathKey]) { | |
requestKey = request | |
} else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) { | |
pathIsEncoded = true; | |
requestKey = request | |
} else { | |
requestKey = options.mapRequestToAsset(request) | |
} | |
const parsedUrl = new URL(requestKey.url) | |
const pathname = pathIsEncoded ? decodeURIComponent(parsedUrl.pathname) : parsedUrl.pathname // decode percentage encoded path only when necessary | |
// pathKey is the file path to look up in the manifest | |
let pathKey = pathname.replace(/^\/+/, '') // remove prepended / | |
// @ts-ignore | |
const cache = caches.default | |
let mimeType = mime.getType(pathKey) || options.defaultMimeType | |
if (mimeType.startsWith('text')) { | |
mimeType += '; charset=utf-8' | |
} | |
let shouldEdgeCache = false // false if storing in KV by raw file path i.e. no hash | |
// check manifest for map from file path to hash | |
if (typeof ASSET_MANIFEST !== 'undefined') { | |
if (ASSET_MANIFEST[pathKey]) { | |
pathKey = ASSET_MANIFEST[pathKey] | |
// if path key is in asset manifest, we can assume it contains a content hash and can be cached | |
shouldEdgeCache = true | |
} | |
} | |
// TODO this excludes search params from cache, investigate ideal behavior | |
let cacheKey = new Request(`${parsedUrl.origin}/${pathKey}`, request) | |
// if argument passed in for cacheControl is a function then | |
// evaluate that function. otherwise return the Object passed in | |
// or default Object | |
const evalCacheOpts = (() => { | |
switch (typeof options.cacheControl) { | |
case 'function': | |
return options.cacheControl(request) | |
case 'object': | |
return options.cacheControl | |
default: | |
return defaultCacheControl | |
} | |
})() | |
options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts) | |
// override shouldEdgeCache if options say to bypassCache | |
if ( | |
options.cacheControl.bypassCache || | |
options.cacheControl.edgeTTL === null || | |
request.method == 'HEAD' | |
) { | |
shouldEdgeCache = false | |
} | |
// only set max-age if explicitly passed in a number as an arg | |
const shouldSetBrowserCache = typeof options.cacheControl.browserTTL === 'number' | |
let response = null | |
if (shouldEdgeCache) { | |
response = await cache.match(cacheKey) | |
} | |
if (response) { | |
let headers = new Headers(response.headers) | |
let shouldRevalidate = false | |
// Four preconditions must be met for a 304 Not Modified: | |
// - the request cannot be a range request | |
// - client sends if-none-match | |
// - resource has etag | |
// - test if-none-match against the pathKey so that we test against KV, rather than against | |
// CF cache, which may modify the etag with a weak validator (e.g. W/"...") | |
shouldRevalidate = [ | |
request.headers.has('range') !== true, | |
request.headers.has('if-none-match'), | |
response.headers.has('etag'), | |
request.headers.get('if-none-match') === `${pathKey}`, | |
].every(Boolean) | |
if (shouldRevalidate) { | |
// fixes issue #118 | |
if (response.body && 'cancel' in Object.getPrototypeOf(response.body)) { | |
response.body.cancel(); | |
console.log('Body exists and environment supports readable streams. Body cancelled') | |
} else { | |
console.log('Environment doesnt support readable streams') | |
} | |
headers.set('cf-cache-status', 'REVALIDATED') | |
response = new Response(null, { | |
status: 304, | |
headers, | |
statusText: 'Not Modified', | |
}) | |
} else { | |
headers.set('CF-Cache-Status', 'HIT') | |
response = new Response(response.body, { headers }) | |
} | |
} else { | |
const body = await ASSET_NAMESPACE.get(pathKey, 'arrayBuffer') | |
if (body === null) { | |
throw new NotFoundError(`could not find ${pathKey} in your content namespace`) | |
} | |
response = new Response(body) | |
if (shouldEdgeCache) { | |
response.headers.set('Accept-Ranges', 'bytes') | |
response.headers.set('Content-Length', body.length) | |
// set etag before cache insertion | |
if (!response.headers.has('etag')) { | |
response.headers.set('etag', `${pathKey}`) | |
} | |
// determine Cloudflare cache behavior | |
response.headers.set('Cache-Control', `max-age=${options.cacheControl.edgeTTL}`) | |
event.waitUntil(cache.put(cacheKey, response.clone())) | |
response.headers.set('CF-Cache-Status', 'MISS') | |
} | |
} | |
response.headers.set('Content-Type', mimeType) | |
if (shouldSetBrowserCache) { | |
response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`) | |
} else { | |
response.headers.delete('Cache-Control') | |
} | |
return response | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment