Skip to content

Instantly share code, notes, and snippets.

@iamspark1e
Created November 23, 2020 02:29
Show Gist options
  • Save iamspark1e/92da71de55f2be2e8a13b97305a36b78 to your computer and use it in GitHub Desktop.
Save iamspark1e/92da71de55f2be2e8a13b97305a36b78 to your computer and use it in GitHub Desktop.
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