@cloudflare/kv-asset-handler
Advanced tools
Comparing version 0.1.1 to 0.1.2
101
CHANGELOG.md
# Changelog | ||
## 0.1.2 | ||
- ### Features | ||
- **Support for `defaultDocument` configuration - [boemekeld], [pull/161]** | ||
This PR adds support for customizing the `defaultDocument` option in `getAssetFromKV`. In situations where a project does not use `index.html` as the default document for a path, this can now be customized to values like `index.shtm`: | ||
```js | ||
return getAssetFromKV(event, { | ||
defaultDocument: "index.shtm" | ||
}) | ||
``` | ||
[boemekeld]: https://github.com/boemekeld | ||
[pull/161]: https://github.com/cloudflare/kv-asset-handler/pull/161 | ||
- ### Fixes | ||
- **Fire `mapRequestToAsset` for all requests, if explicitly defined - [Cherry], [pull/159]** | ||
This PR fixes an issue where a custom `mapRequestToAsset` handler weren't fired if a matching asset path was found in `ASSET_MANIFEST` data. By correctly checking for this handler, we can conditionally handle any assets with this handler _even_ if they exist in the `ASSET_MANIFEST`. | ||
**Note that this is a breaking change**, as previously, the mapRequestToAsset function was ignored if you set it, and an exact match was found in the `ASSET_MANIFEST`. That being said, this behavior was a bug, and unexpected behavior, as documented in [issue/158]. | ||
[Cherry]: https://github.com/Cherry | ||
[issue/158]: https://github.com/kv-asset-handler/pull/158 | ||
[pull/159]: https://github.com/kv-asset-handler/pull/159 | ||
- **Etag logic refactor - [shagamemnon], [pull/133]** | ||
This PR refactors a great deal of the Etag functionality introduced in [0.0.11](https://github.com/cloudflare/kv-asset-handler/milestone/7?closed=1). `kv-asset-handler` will now correctly set [strong and weak Etags](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) both to the Cloudflare CDN and to client eyeballs, allowing for higher cache percentages with Workers Sites projects. | ||
[pull/133]: https://github.com/cloudflare/kv-asset-handler/pull/133 | ||
[shagamemnon]: https://github.com/shagamemnon | ||
- **Fix path decoding issue - [xiaolanglanglang], [pull/142]** | ||
This PR improves support for non-alphanumeric character paths in `kv-asset-handler`, for instance, if the path requested is in Chinese. | ||
[xiaolanglanglang]: https://github.com/xiaolanglanglang | ||
[pull/142]: https://github.com/cloudflare/kv-asset-handler/pull/142 | ||
- **Check HTTP method after mapRequestToAsset - [oliverpool], [pull/178]** | ||
This PR fixes an issue where the HTTP method for an asset is checked before the `mapRequestToAsset` handler is called. This has caused issues for users in the past, where they need to generate a `requestKey` based on an asset path, even if the request method is not `GET`. This fixes [issue/151]. | ||
[oliverpool]: https://github.com/oliverpool | ||
[pull/178]: https://github.com/cloudflare/kv-asset-handler/pull/178 | ||
[issue/151]: https://github.com/cloudflare/kv-asset-handler/issues/151 | ||
- ### Maintenance | ||
- **Add Markdown linting workflow to GitHub Actions - [jbampton], [pull/135]** | ||
Our GitHub Actions workflow now includes a linting workflow for Markdown in the project, including the README, this CHANGELOG, and any other `.md` files in the source code. | ||
[jbampton]: https://github.com/jbampton | ||
[pull/135]: https://github.com/cloudflare/kv-asset-handler/pull/135 | ||
- **Dependabot updates** | ||
A number of dependabot patch-level updates have been merged since our last release: | ||
- Bump @types/node from 15.30.0 to 15.30.1 ([pull/180]) | ||
- Bump hosted-git-info from 2.8.8 to 2.8.9 ([pull/176]) | ||
- Bump ini from 1.3.5 to 1.3.8 ([pull/160]) | ||
- Bump lodash from 4.17.19 to 4.17.21 ([pull/175]) | ||
- Bump urijs from 1.19.2 to 1.19.6 ([pull/168]) | ||
- Bump y18n from 4.0.0 to 4.0.1 ([pull/173]) | ||
[pull/160]: https://github.com/cloudflare/kv-asset-handler/pull/160 | ||
[pull/168]: https://github.com/cloudflare/kv-asset-handler/pull/168 | ||
[pull/173]: https://github.com/cloudflare/kv-asset-handler/pull/173 | ||
[pull/175]: https://github.com/cloudflare/kv-asset-handler/pull/175 | ||
[pull/176]: https://github.com/cloudflare/kv-asset-handler/pull/176 | ||
[pull/180]: https://github.com/cloudflare/kv-asset-handler/pull/180 | ||
- **Repository maintenance - [Cherry], [pull/179]** | ||
New project maintainer Cherry did a ton of maintenance in this release, improving workflows, code quality, and more. Check out the full list in [the PR][pull/179]. | ||
[Cherry]: https://github.com/Cherry | ||
[pull/179]: https://github.com/cloudflare/kv-asset-handler/pull/179 | ||
- ### Documentation | ||
- **Update README.md - [signalnerve], [pull/177]** | ||
This PR adds context to our README, with mentions about _what_ this project is, how to use it, and some new things since the last version of this package: namely, [Cloudflare Pages](https://pages.dev) and the new [Cloudflare Workers Discord server](https://discord.gg/cloudflaredev) | ||
[signalnerve]: https://github.com/signalnerve | ||
[pull/177]: https://github.com/cloudflare/kv-asset-handler/pull/177 | ||
- **Add instructions for updating version in related repos - [caass], [pull/171]** | ||
This PR adds instructions for updating the `kv-asset-handler` version in related repositories, such as our templates, that use `kv-asset-handler` and are exposed to end-users of Wrangler and Workers. | ||
[caass]: https://github.com/caass | ||
[pull/177]: https://github.com/cloudflare/kv-asset-handler/pull/171 | ||
## 0.1.1 | ||
@@ -4,0 +105,0 @@ |
@@ -12,3 +12,3 @@ import { Options, CacheControl, MethodNotAllowedError, NotFoundError, InternalError } from './types'; | ||
*/ | ||
declare const mapRequestToAsset: (request: Request) => Request; | ||
declare const mapRequestToAsset: (request: Request, options?: Partial<Options>) => Request; | ||
/** | ||
@@ -19,3 +19,3 @@ * maps the path of incoming request to /index.html if it evaluates to | ||
*/ | ||
declare function serveSinglePageApp(request: Request): Request; | ||
declare function serveSinglePageApp(request: Request, options?: Partial<Options>): Request; | ||
/** | ||
@@ -22,0 +22,0 @@ * takes the path of the incoming request, gathers the appropriate content from KV, and returns |
@@ -11,36 +11,25 @@ "use strict"; | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.InternalError = exports.NotFoundError = exports.MethodNotAllowedError = exports.serveSinglePageApp = exports.mapRequestToAsset = exports.getAssetFromKV = void 0; | ||
var mime = require("mime"); | ||
var types_1 = require("./types"); | ||
const mime = require("mime"); | ||
const types_1 = require("./types"); | ||
Object.defineProperty(exports, "MethodNotAllowedError", { enumerable: true, get: function () { return types_1.MethodNotAllowedError; } }); | ||
Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return types_1.NotFoundError; } }); | ||
Object.defineProperty(exports, "InternalError", { enumerable: true, get: function () { return types_1.InternalError; } }); | ||
const defaultCacheControl = { | ||
browserTTL: null, | ||
edgeTTL: 2 * 60 * 60 * 24, | ||
bypassCache: false, // do not bypass Cloudflare's cache | ||
}; | ||
function assignOptions(options) { | ||
// Assign any missing options passed in to the default | ||
// options.mapRequestToAsset is handled manually later | ||
return Object.assign({ | ||
ASSET_NAMESPACE: __STATIC_CONTENT, | ||
ASSET_MANIFEST: __STATIC_CONTENT_MANIFEST, | ||
cacheControl: defaultCacheControl, | ||
defaultMimeType: 'text/plain', | ||
defaultDocument: 'index.html', | ||
}, options); | ||
} | ||
/** | ||
@@ -53,9 +42,10 @@ * maps the path of incoming request to the request pathKey to look up | ||
*/ | ||
var mapRequestToAsset = function (request) { | ||
var parsedUrl = new URL(request.url); | ||
var pathname = parsedUrl.pathname; | ||
const mapRequestToAsset = (request, options) => { | ||
options = assignOptions(options); | ||
const parsedUrl = new URL(request.url); | ||
let pathname = parsedUrl.pathname; | ||
if (pathname.endsWith('/')) { | ||
// If path looks like a directory append index.html | ||
// If path looks like a directory append options.defaultDocument | ||
// e.g. If path is /about/ -> /about/index.html | ||
pathname = pathname.concat('index.html'); | ||
pathname = pathname.concat(options.defaultDocument); | ||
} | ||
@@ -65,3 +55,3 @@ else if (!mime.getType(pathname)) { | ||
// e.g. /about.me -> /about.me/index.html | ||
pathname = pathname.concat('/index.html'); | ||
pathname = pathname.concat('/' + options.defaultDocument); | ||
} | ||
@@ -77,12 +67,13 @@ parsedUrl.pathname = pathname; | ||
*/ | ||
function serveSinglePageApp(request) { | ||
function serveSinglePageApp(request, options) { | ||
options = assignOptions(options); | ||
// First apply the default handler, which already has logic to detect | ||
// paths that should map to HTML files. | ||
request = mapRequestToAsset(request); | ||
var parsedUrl = new URL(request.url); | ||
request = mapRequestToAsset(request, options); | ||
const parsedUrl = new URL(request.url); | ||
// Detect if the default handler decided to map to | ||
// a HTML file in some specific directory. | ||
if (parsedUrl.pathname.endsWith('.html')) { | ||
// If expected HTML file was missing, just return the root index.html | ||
return new Request(parsedUrl.origin + "/index.html", request); | ||
// If expected HTML file was missing, just return the root index.html (or options.defaultDocument) | ||
return new Request(`${parsedUrl.origin}/${options.defaultDocument}`, request); | ||
} | ||
@@ -96,7 +87,2 @@ else { | ||
exports.serveSinglePageApp = serveSinglePageApp; | ||
var defaultCacheControl = { | ||
browserTTL: null, | ||
edgeTTL: 2 * 60 * 60 * 24, | ||
bypassCache: false, | ||
}; | ||
/** | ||
@@ -113,171 +99,192 @@ * takes the path of the incoming request, gathers the appropriate content from KV, and returns | ||
* */ | ||
var getAssetFromKV = function (event, options) { return __awaiter(void 0, void 0, void 0, function () { | ||
var request, ASSET_NAMESPACE, ASSET_MANIFEST, SUPPORTED_METHODS, rawPathKey, pathIsEncoded, requestKey, parsedUrl, pathname, pathKey, cache, mimeType, shouldEdgeCache, cacheKey, evalCacheOpts, shouldSetBrowserCache, response, headers, shouldRevalidate, opts, body; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
// 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); | ||
request = event.request; | ||
ASSET_NAMESPACE = options.ASSET_NAMESPACE; | ||
ASSET_MANIFEST = typeof (options.ASSET_MANIFEST) === 'string' | ||
? JSON.parse(options.ASSET_MANIFEST) | ||
: options.ASSET_MANIFEST; | ||
if (typeof ASSET_NAMESPACE === 'undefined') { | ||
throw new types_1.InternalError("there is no KV namespace bound to the script"); | ||
const getAssetFromKV = (event, options) => __awaiter(void 0, void 0, void 0, function* () { | ||
options = assignOptions(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 types_1.InternalError(`there is no KV namespace bound to the script`); | ||
} | ||
const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, ''); // strip any preceding /'s | ||
let pathIsEncoded = false; | ||
let requestKey; | ||
// if options.mapRequestToAsset is explicitly passed in, always use it and assume user has own intentions | ||
// otherwise handle request as normal, with default mapRequestToAsset below | ||
if (options.mapRequestToAsset) { | ||
requestKey = options.mapRequestToAsset(request); | ||
} | ||
else if (ASSET_MANIFEST[rawPathKey]) { | ||
requestKey = request; | ||
} | ||
else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) { | ||
pathIsEncoded = true; | ||
requestKey = request; | ||
} | ||
else { | ||
const mappedRequest = mapRequestToAsset(request); | ||
const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(/^\/+/, ''); | ||
if (ASSET_MANIFEST[decodeURIComponent(mappedRawPathKey)]) { | ||
pathIsEncoded = true; | ||
requestKey = mappedRequest; | ||
} | ||
else { | ||
// use default mapRequestToAsset | ||
requestKey = mapRequestToAsset(request, options); | ||
} | ||
} | ||
const SUPPORTED_METHODS = ['GET', 'HEAD']; | ||
if (!SUPPORTED_METHODS.includes(requestKey.method)) { | ||
throw new types_1.MethodNotAllowedError(`${requestKey.method} is not a valid request method`); | ||
} | ||
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 === 'application/javascript') { | ||
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; | ||
} | ||
})(); | ||
// formats the etag depending on the response context. if the entityId | ||
// is invalid, returns an empty string (instead of null) to prevent the | ||
// the potentially disastrous scenario where the value of the Etag resp | ||
// header is "null". Could be modified in future to base64 encode etc | ||
const formatETag = (entityId = pathKey, validatorType = 'strong') => { | ||
if (!entityId) { | ||
return ''; | ||
} | ||
switch (validatorType) { | ||
case 'weak': | ||
if (!entityId.startsWith('W/')) { | ||
return `W/${entityId}`; | ||
} | ||
SUPPORTED_METHODS = ['GET', 'HEAD']; | ||
if (!SUPPORTED_METHODS.includes(request.method)) { | ||
throw new types_1.MethodNotAllowedError(request.method + " is not a valid request method"); | ||
return entityId; | ||
case 'strong': | ||
if (entityId.startsWith(`W/"`)) { | ||
entityId = entityId.replace('W/', ''); | ||
} | ||
rawPathKey = new URL(request.url).pathname.replace(/^\/+/, '') // strip any preceding /'s | ||
; | ||
pathIsEncoded = false; | ||
if (ASSET_MANIFEST[rawPathKey]) { | ||
requestKey = request; | ||
if (!entityId.endsWith(`"`)) { | ||
entityId = `"${entityId}"`; | ||
} | ||
else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) { | ||
pathIsEncoded = true; | ||
requestKey = request; | ||
} | ||
else { | ||
requestKey = options.mapRequestToAsset(request); | ||
} | ||
parsedUrl = new URL(requestKey.url); | ||
pathname = pathIsEncoded ? decodeURIComponent(parsedUrl.pathname) : parsedUrl.pathname // decode percentage encoded path only when necessary | ||
; | ||
pathKey = pathname.replace(/^\/+/, '') // remove prepended / | ||
; | ||
cache = caches.default; | ||
mimeType = mime.getType(pathKey) || options.defaultMimeType; | ||
if (mimeType.startsWith('text') || mimeType === 'application/javascript') { | ||
mimeType += '; charset=utf-8'; | ||
} | ||
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; | ||
} | ||
} | ||
cacheKey = new Request(parsedUrl.origin + "/" + pathKey, request); | ||
evalCacheOpts = (function () { | ||
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; | ||
} | ||
shouldSetBrowserCache = typeof options.cacheControl.browserTTL === 'number'; | ||
response = null; | ||
if (!shouldEdgeCache) return [3 /*break*/, 2]; | ||
return [4 /*yield*/, cache.match(cacheKey)]; | ||
case 1: | ||
response = _a.sent(); | ||
_a.label = 2; | ||
case 2: | ||
if (!response) return [3 /*break*/, 3]; | ||
headers = new Headers(response.headers); | ||
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: headers, | ||
statusText: 'Not Modified', | ||
}); | ||
} | ||
else { | ||
headers.set('CF-Cache-Status', 'HIT'); | ||
opts = { | ||
headers: headers, | ||
status: 0, | ||
statusText: '' | ||
}; | ||
if (response.status) { | ||
opts.status = response.status; | ||
opts.statusText = response.statusText; | ||
} | ||
else if (headers.has('Content-Range')) { | ||
opts.status = 206; | ||
opts.statusText = 'Partial Content'; | ||
} | ||
else { | ||
opts.status = 200; | ||
opts.statusText = 'OK'; | ||
} | ||
response = new Response(response.body, opts); | ||
} | ||
return [3 /*break*/, 5]; | ||
case 3: return [4 /*yield*/, ASSET_NAMESPACE.get(pathKey, 'arrayBuffer')]; | ||
case 4: | ||
body = _a.sent(); | ||
if (body === null) { | ||
throw new types_1.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'); | ||
} | ||
_a.label = 5; | ||
case 5: | ||
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 [2 /*return*/, response]; | ||
return entityId; | ||
default: | ||
return ''; | ||
} | ||
}); | ||
}); }; | ||
}; | ||
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 = yield cache.match(cacheKey); | ||
} | ||
if (response) { | ||
if (response.status > 300 && response.status < 400) { | ||
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'); | ||
} | ||
response = new Response(null, response); | ||
} | ||
else { | ||
// fixes #165 | ||
let opts = { | ||
headers: new Headers(response.headers), | ||
status: 0, | ||
statusText: '', | ||
}; | ||
opts.headers.set('cf-cache-status', 'HIT'); | ||
if (response.status) { | ||
opts.status = response.status; | ||
opts.statusText = response.statusText; | ||
} | ||
else if (opts.headers.has('Content-Range')) { | ||
opts.status = 206; | ||
opts.statusText = 'Partial Content'; | ||
} | ||
else { | ||
opts.status = 200; | ||
opts.statusText = 'OK'; | ||
} | ||
response = new Response(response.body, opts); | ||
} | ||
} | ||
else { | ||
const body = yield ASSET_NAMESPACE.get(pathKey, 'arrayBuffer'); | ||
if (body === null) { | ||
throw new types_1.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', formatETag(pathKey, 'strong')); | ||
} | ||
// 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 (response.status === 304) { | ||
let etag = formatETag(response.headers.get('etag'), 'strong'); | ||
let ifNoneMatch = cacheKey.headers.get('if-none-match'); | ||
let proxyCacheStatus = response.headers.get('CF-Cache-Status'); | ||
if (etag) { | ||
if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === 'MISS') { | ||
response.headers.set('CF-Cache-Status', 'EXPIRED'); | ||
} | ||
else { | ||
response.headers.set('CF-Cache-Status', 'REVALIDATED'); | ||
} | ||
response.headers.set('etag', formatETag(etag, 'weak')); | ||
} | ||
} | ||
if (shouldSetBrowserCache) { | ||
response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`); | ||
} | ||
else { | ||
response.headers.delete('Cache-Control'); | ||
} | ||
return response; | ||
}); | ||
exports.getAssetFromKV = getAssetFromKV; |
@@ -9,3 +9,3 @@ export declare const getEvent: (request: Request) => any; | ||
match(key: any): Promise<any>; | ||
put(key: any, val: Response): Promise<any>; | ||
put(key: any, val: Response): Promise<void>; | ||
}; | ||
@@ -12,0 +12,0 @@ }; |
@@ -11,50 +11,17 @@ "use strict"; | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.sleep = exports.mockGlobal = exports.mockCaches = exports.mockManifest = exports.mockKV = exports.getEvent = void 0; | ||
var makeServiceWorkerEnv = require('service-worker-mock'); | ||
var HASH = '123HASHBROWN'; | ||
exports.getEvent = function (request) { | ||
var waitUntil = function (callback) { return __awaiter(void 0, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, callback]; | ||
case 1: | ||
_a.sent(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }; | ||
const makeServiceWorkerEnv = require('service-worker-mock'); | ||
const HASH = '123HASHBROWN'; | ||
const getEvent = (request) => { | ||
const waitUntil = (callback) => __awaiter(void 0, void 0, void 0, function* () { | ||
yield callback; | ||
}); | ||
return { | ||
request: request, | ||
waitUntil: waitUntil, | ||
request, | ||
waitUntil, | ||
}; | ||
}; | ||
var store = { | ||
exports.getEvent = getEvent; | ||
const store = { | ||
'key1.123HASHBROWN.txt': 'val1', | ||
@@ -74,90 +41,94 @@ 'key1.123HASHBROWN.png': 'val1', | ||
'client.123HASHBROWN/index.html': 'Im here but serve my big bro above', | ||
'你好/index.123HASHBROWN.html': 'My path is non-ascii', | ||
}; | ||
exports.mockKV = function (store) { | ||
const mockKV = (store) => { | ||
return { | ||
get: function (path) { return store[path] || null; }, | ||
get: (path) => store[path] || null, | ||
}; | ||
}; | ||
exports.mockManifest = function () { | ||
exports.mockKV = mockKV; | ||
const mockManifest = () => { | ||
return JSON.stringify({ | ||
'key1.txt': "key1." + HASH + ".txt", | ||
'key1.png': "key1." + HASH + ".png", | ||
'cache.html': "cache." + HASH + ".html", | ||
'测试.html': "\u6D4B\u8BD5." + HASH + ".html", | ||
'你好.html': "\u4F60\u597D." + HASH + ".html", | ||
'%not-really-percent-encoded.html': "%not-really-percent-encoded." + HASH + ".html", | ||
'%2F.html': "%2F." + HASH + ".html", | ||
'%E4%BD%A0%E5%A5%BD.html': "%E4%BD%A0%E5%A5%BD." + HASH + ".html", | ||
'index.html': "index." + HASH + ".html", | ||
'sub/blah.png': "sub/blah." + HASH + ".png", | ||
'sub/index.html': "sub/index." + HASH + ".html", | ||
'client': "client." + HASH, | ||
'client/index.html': "client." + HASH, | ||
'key1.txt': `key1.${HASH}.txt`, | ||
'key1.png': `key1.${HASH}.png`, | ||
'cache.html': `cache.${HASH}.html`, | ||
'测试.html': `测试.${HASH}.html`, | ||
'你好.html': `你好.${HASH}.html`, | ||
'%not-really-percent-encoded.html': `%not-really-percent-encoded.${HASH}.html`, | ||
'%2F.html': `%2F.${HASH}.html`, | ||
'%E4%BD%A0%E5%A5%BD.html': `%E4%BD%A0%E5%A5%BD.${HASH}.html`, | ||
'index.html': `index.${HASH}.html`, | ||
'sub/blah.png': `sub/blah.${HASH}.png`, | ||
'sub/index.html': `sub/index.${HASH}.html`, | ||
client: `client.${HASH}`, | ||
'client/index.html': `client.${HASH}`, | ||
'你好/index.html': `你好/index.${HASH}.html`, | ||
}); | ||
}; | ||
var cacheStore = new Map(); | ||
exports.mockCaches = function () { | ||
exports.mockManifest = mockManifest; | ||
let cacheStore = new Map(); | ||
const mockCaches = () => { | ||
return { | ||
default: { | ||
match: function (key) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var cacheKey, response, activeCacheKeys, _i, activeCacheKeys_1, cacheStoreKey, range; | ||
return __generator(this, function (_a) { | ||
cacheKey = { | ||
url: key.url, | ||
headers: {} | ||
}; | ||
if (key.headers.has('if-none-match')) { | ||
cacheKey.headers = { | ||
'etag': key.headers.get('if-none-match') | ||
}; | ||
response = cacheStore.get(JSON.stringify(cacheKey)); | ||
match(key) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
let cacheKey = { | ||
url: key.url, | ||
headers: {}, | ||
}; | ||
let response; | ||
if (key.headers.has('if-none-match')) { | ||
let makeStrongEtag = key.headers.get('if-none-match').replace('W/', ''); | ||
Reflect.set(cacheKey.headers, 'etag', makeStrongEtag); | ||
response = cacheStore.get(JSON.stringify(cacheKey)); | ||
} | ||
else { | ||
// if client doesn't send if-none-match, we need to iterate through these keys | ||
// and just test the URL | ||
const activeCacheKeys = Array.from(cacheStore.keys()); | ||
for (const cacheStoreKey of activeCacheKeys) { | ||
if (JSON.parse(cacheStoreKey).url === key.url) { | ||
response = cacheStore.get(cacheStoreKey); | ||
} | ||
} | ||
} | ||
// TODO: write test to accomodate for rare scenarios with where range requests accomodate etags | ||
if (response && !key.headers.has('if-none-match')) { | ||
// this appears overly verbose, but is necessary to document edge cache behavior | ||
// The Range request header triggers the response header Content-Range ... | ||
const range = key.headers.get('range'); | ||
if (range) { | ||
response.headers.set('content-range', `bytes ${range.split('=').pop()}/${response.headers.get('content-length')}`); | ||
} | ||
// ... which we are using in this repository to set status 206 | ||
if (response.headers.has('content-range')) { | ||
response.status = 206; | ||
} | ||
else { | ||
activeCacheKeys = Array.from(cacheStore.keys()); | ||
for (_i = 0, activeCacheKeys_1 = activeCacheKeys; _i < activeCacheKeys_1.length; _i++) { | ||
cacheStoreKey = activeCacheKeys_1[_i]; | ||
if (JSON.parse(cacheStoreKey).url === key.url) { | ||
response = cacheStore.get(cacheStoreKey); | ||
} | ||
} | ||
response.status = 200; | ||
} | ||
if (response) { | ||
range = key.headers.get('range'); | ||
if (range) { | ||
response.headers.set('content-range', "bytes " + range.split('=').pop() + "/" + response.headers.get('content-length')); | ||
} | ||
// ... which we are using in this repository to set status 206 | ||
if (response.headers.has('content-range')) { | ||
response.status = 206; | ||
} | ||
else { | ||
response.status = 200; | ||
} | ||
let etag = response.headers.get('etag'); | ||
if (etag && !etag.includes('W/')) { | ||
response.headers.set('etag', `W/${etag}`); | ||
} | ||
return [2 /*return*/, response]; | ||
}); | ||
} | ||
return response; | ||
}); | ||
}, | ||
put: function (key, val) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var headers, body, resp, cacheKey; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
headers = new Headers(val.headers); | ||
return [4 /*yield*/, val.text()]; | ||
case 1: | ||
body = _a.sent(); | ||
resp = new Response(body, { headers: headers }); | ||
headers.set('content-length', (body.length).toString()); | ||
cacheKey = { | ||
url: key.url, | ||
headers: { | ||
'etag': val.headers.get('etag') | ||
} | ||
}; | ||
return [2 /*return*/, cacheStore.set(JSON.stringify(cacheKey), resp)]; | ||
} | ||
}); | ||
put(key, val) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
let headers = new Headers(val.headers); | ||
let url = new URL(key.url); | ||
let resWithBody = new Response(val.body, { headers, status: 200 }); | ||
let resNoBody = new Response(null, { headers, status: 304 }); | ||
let cacheKey = { | ||
url: key.url, | ||
headers: { | ||
etag: `"${url.pathname.replace('/', '')}"`, | ||
}, | ||
}; | ||
cacheStore.set(JSON.stringify(cacheKey), resNoBody); | ||
cacheKey.headers = {}; | ||
cacheStore.set(JSON.stringify(cacheKey), resWithBody); | ||
return; | ||
}); | ||
@@ -168,2 +139,3 @@ }, | ||
}; | ||
exports.mockCaches = mockCaches; | ||
function mockGlobal() { | ||
@@ -176,4 +148,5 @@ Object.assign(global, makeServiceWorkerEnv()); | ||
exports.mockGlobal = mockGlobal; | ||
exports.sleep = function (milliseconds) { | ||
return new Promise(function (resolve) { return setTimeout(resolve, milliseconds); }); | ||
const sleep = (milliseconds) => { | ||
return new Promise((resolve) => setTimeout(resolve, milliseconds)); | ||
}; | ||
exports.sleep = sleep; |
@@ -11,751 +11,427 @@ "use strict"; | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const ava_1 = require("ava"); | ||
const mocks_1 = require("../mocks"); | ||
const index_1 = require("../index"); | ||
ava_1.default('getAssetFromKV return correct val from KV and default caching', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/key1.txt')); | ||
const res = yield index_1.getAssetFromKV(event); | ||
if (res) { | ||
t.is(res.headers.get('cache-control'), null); | ||
t.is(res.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(yield res.text(), 'val1'); | ||
t.true(res.headers.get('content-type').includes('text')); | ||
} | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var ava_1 = require("ava"); | ||
var mocks_1 = require("../mocks"); | ||
var index_1 = require("../index"); | ||
ava_1.default('getAssetFromKV return correct val from KV and default caching', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/key1.txt')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
t.is(res.headers.get('cache-control'), null); | ||
t.is(res.headers.get('cf-cache-status'), 'MISS'); | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'val1']); | ||
t.true(res.headers.get('content-type').includes('text')); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV evaluated the file matching the extensionless path first /client/ -> client', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request(`https://foo.com/client/`)); | ||
const res = yield index_1.getAssetFromKV(event); | ||
t.is(yield res.text(), 'important file'); | ||
t.true(res.headers.get('content-type').includes('text')); | ||
})); | ||
ava_1.default('getAssetFromKV evaluated the file matching the extensionless path first /client -> client', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request(`https://foo.com/client`)); | ||
const res = yield index_1.getAssetFromKV(event); | ||
t.is(yield res.text(), 'important file'); | ||
t.true(res.headers.get('content-type').includes('text')); | ||
})); | ||
ava_1.default('getAssetFromKV if not in asset manifest still returns nohash.txt', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/nohash.txt')); | ||
const res = yield index_1.getAssetFromKV(event); | ||
if (res) { | ||
t.is(yield res.text(), 'no hash but still got some result'); | ||
t.true(res.headers.get('content-type').includes('text')); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV if no asset manifest /client -> client fails', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request(`https://foo.com/client`)); | ||
const error = yield t.throwsAsync(index_1.getAssetFromKV(event, { ASSET_MANIFEST: {} })); | ||
t.is(error.status, 404); | ||
})); | ||
ava_1.default('getAssetFromKV if sub/ -> sub/index.html served', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request(`https://foo.com/sub`)); | ||
const res = yield index_1.getAssetFromKV(event); | ||
if (res) { | ||
t.is(yield res.text(), 'picturedis'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV gets index.html by default for / requests', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
const res = yield index_1.getAssetFromKV(event); | ||
if (res) { | ||
t.is(yield res.text(), 'index.html'); | ||
t.true(res.headers.get('content-type').includes('html')); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV non ASCII path support', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/测试.html')); | ||
const res = yield index_1.getAssetFromKV(event); | ||
if (res) { | ||
t.is(yield res.text(), 'My filename is non-ascii'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV supports browser percent encoded URLs', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://example.com/%not-really-percent-encoded.html')); | ||
const res = yield index_1.getAssetFromKV(event); | ||
if (res) { | ||
t.is(yield res.text(), 'browser percent encoded'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV supports user percent encoded URLs', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/%2F.html')); | ||
const res = yield index_1.getAssetFromKV(event); | ||
if (res) { | ||
t.is(yield res.text(), 'user percent encoded'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV only decode URL when necessary', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event1 = mocks_1.getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD.html')); | ||
const event2 = mocks_1.getEvent(new Request('https://blah.com/你好.html')); | ||
const res1 = yield index_1.getAssetFromKV(event1); | ||
const res2 = yield index_1.getAssetFromKV(event2); | ||
if (res1 && res2) { | ||
t.is(yield res1.text(), 'Im important'); | ||
t.is(yield res2.text(), 'Im important'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV Support for user decode url path', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event1 = mocks_1.getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD/')); | ||
const event2 = mocks_1.getEvent(new Request('https://blah.com/你好/')); | ||
const res1 = yield index_1.getAssetFromKV(event1); | ||
const res2 = yield index_1.getAssetFromKV(event2); | ||
if (res1 && res2) { | ||
t.is(yield res1.text(), 'My path is non-ascii'); | ||
t.is(yield res2.text(), 'My path is non-ascii'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV custom key modifier', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/docs/sub/blah.png')); | ||
const customRequestMapper = (request) => { | ||
let defaultModifiedRequest = index_1.mapRequestToAsset(request); | ||
let url = new URL(defaultModifiedRequest.url); | ||
url.pathname = url.pathname.replace('/docs', ''); | ||
return new Request(url.toString(), request); | ||
}; | ||
const res = yield index_1.getAssetFromKV(event, { mapRequestToAsset: customRequestMapper }); | ||
if (res) { | ||
t.is(yield res.text(), 'picturedis'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV when setting browser caching', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
const res = yield index_1.getAssetFromKV(event, { cacheControl: { browserTTL: 22 } }); | ||
if (res) { | ||
t.is(res.headers.get('cache-control'), 'max-age=22'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV when setting custom cache setting', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event1 = mocks_1.getEvent(new Request('https://blah.com/')); | ||
const event2 = mocks_1.getEvent(new Request('https://blah.com/key1.png?blah=34')); | ||
const cacheOnlyPngs = (req) => { | ||
if (new URL(req.url).pathname.endsWith('.png')) | ||
return { | ||
browserTTL: 720, | ||
edgeTTL: 720, | ||
}; | ||
else | ||
return { | ||
bypassCache: true, | ||
}; | ||
}; | ||
const res1 = yield index_1.getAssetFromKV(event1, { cacheControl: cacheOnlyPngs }); | ||
const res2 = yield index_1.getAssetFromKV(event2, { cacheControl: cacheOnlyPngs }); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cache-control'), null); | ||
t.true(res2.headers.get('content-type').includes('png')); | ||
t.is(res2.headers.get('cache-control'), 'max-age=720'); | ||
t.is(res2.headers.get('cf-cache-status'), 'MISS'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV caches on two sequential requests', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const resourceKey = 'cache.html'; | ||
const resourceVersion = JSON.parse(mocks_1.mockManifest())[resourceKey]; | ||
const event1 = mocks_1.getEvent(new Request(`https://blah.com/${resourceKey}`)); | ||
const event2 = mocks_1.getEvent(new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': `"${resourceVersion}"`, | ||
}, | ||
})); | ||
const res1 = yield index_1.getAssetFromKV(event1, { cacheControl: { edgeTTL: 720, browserTTL: 720 } }); | ||
yield mocks_1.sleep(1); | ||
const res2 = yield index_1.getAssetFromKV(event2); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res1.headers.get('cache-control'), 'max-age=720'); | ||
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV does not store max-age on two sequential requests', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const resourceKey = 'cache.html'; | ||
const resourceVersion = JSON.parse(mocks_1.mockManifest())[resourceKey]; | ||
const event1 = mocks_1.getEvent(new Request(`https://blah.com/${resourceKey}`)); | ||
const event2 = mocks_1.getEvent(new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': `"${resourceVersion}"`, | ||
}, | ||
})); | ||
const res1 = yield index_1.getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }); | ||
yield mocks_1.sleep(100); | ||
const res2 = yield index_1.getAssetFromKV(event2); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res1.headers.get('cache-control'), null); | ||
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED'); | ||
t.is(res2.headers.get('cache-control'), null); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV does not cache on Cloudflare when bypass cache set', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
const res = yield index_1.getAssetFromKV(event, { cacheControl: { bypassCache: true } }); | ||
if (res) { | ||
t.is(res.headers.get('cache-control'), null); | ||
t.is(res.headers.get('cf-cache-status'), null); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV with no trailing slash on root', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com')); | ||
const res = yield index_1.getAssetFromKV(event); | ||
if (res) { | ||
t.is(yield res.text(), 'index.html'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV with no trailing slash on a subdirectory', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/sub/blah.png')); | ||
const res = yield index_1.getAssetFromKV(event); | ||
if (res) { | ||
t.is(yield res.text(), 'picturedis'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV no result throws an error', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/random')); | ||
const error = yield t.throwsAsync(index_1.getAssetFromKV(event)); | ||
t.is(error.status, 404); | ||
})); | ||
ava_1.default('getAssetFromKV TTls set to null should not cache on browser or edge', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
const res1 = yield index_1.getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } }); | ||
yield mocks_1.sleep(100); | ||
const res2 = yield index_1.getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } }); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), null); | ||
t.is(res1.headers.get('cache-control'), null); | ||
t.is(res2.headers.get('cf-cache-status'), null); | ||
t.is(res2.headers.get('cache-control'), null); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV passing in a custom NAMESPACE serves correct asset', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
let CUSTOM_NAMESPACE = mocks_1.mockKV({ | ||
'key1.123HASHBROWN.txt': 'val1', | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV evaluated the file matching the extensionless path first /client/ -> client', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request("https://foo.com/client/")); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'important file']); | ||
t.true(res.headers.get('content-type').includes('text')); | ||
return [2 /*return*/]; | ||
} | ||
Object.assign(global, { CUSTOM_NAMESPACE }); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
const res = yield index_1.getAssetFromKV(event); | ||
if (res) { | ||
t.is(yield res.text(), 'index.html'); | ||
t.true(res.headers.get('content-type').includes('html')); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV when custom namespace without the asset should fail', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
let CUSTOM_NAMESPACE = mocks_1.mockKV({ | ||
'key5.123HASHBROWN.txt': 'customvalu', | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV evaluated the file matching the extensionless path first /client -> client', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request("https://foo.com/client")); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'important file']); | ||
t.true(res.headers.get('content-type').includes('text')); | ||
return [2 /*return*/]; | ||
} | ||
const event = mocks_1.getEvent(new Request('https://blah.com')); | ||
const error = yield t.throwsAsync(index_1.getAssetFromKV(event, { ASSET_NAMESPACE: CUSTOM_NAMESPACE })); | ||
t.is(error.status, 404); | ||
})); | ||
ava_1.default('getAssetFromKV when namespace not bound fails', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
var MY_CUSTOM_NAMESPACE = undefined; | ||
Object.assign(global, { MY_CUSTOM_NAMESPACE }); | ||
const event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
const error = yield t.throwsAsync(index_1.getAssetFromKV(event, { ASSET_NAMESPACE: MY_CUSTOM_NAMESPACE })); | ||
t.is(error.status, 500); | ||
})); | ||
ava_1.default('getAssetFromKV when if-none-match === active resource version, should revalidate', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const resourceKey = 'key1.png'; | ||
const resourceVersion = JSON.parse(mocks_1.mockManifest())[resourceKey]; | ||
const event1 = mocks_1.getEvent(new Request(`https://blah.com/${resourceKey}`)); | ||
const event2 = mocks_1.getEvent(new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': `W/"${resourceVersion}"`, | ||
}, | ||
})); | ||
const res1 = yield index_1.getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }); | ||
yield mocks_1.sleep(100); | ||
const res2 = yield index_1.getAssetFromKV(event2); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV when if-none-match equals etag of stale resource then should bypass cache', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const resourceKey = 'key1.png'; | ||
const resourceVersion = JSON.parse(mocks_1.mockManifest())[resourceKey]; | ||
const req1 = new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': `"${resourceVersion}"`, | ||
}, | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV if not in asset manifest still returns nohash.txt', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/nohash.txt')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'no hash but still got some result']); | ||
t.true(res.headers.get('content-type').includes('text')); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
const req2 = new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': `"${resourceVersion}-another-version"`, | ||
}, | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV if no asset manifest /client -> client fails', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, error; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request("https://foo.com/client")); | ||
return [4 /*yield*/, t.throwsAsync(index_1.getAssetFromKV(event, { ASSET_MANIFEST: {} }))]; | ||
case 1: | ||
error = _a.sent(); | ||
t.is(error.status, 404); | ||
return [2 /*return*/]; | ||
} | ||
const event = mocks_1.getEvent(req1); | ||
const event2 = mocks_1.getEvent(req2); | ||
const res1 = yield index_1.getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }); | ||
const res2 = yield index_1.getAssetFromKV(event); | ||
const res3 = yield index_1.getAssetFromKV(event2); | ||
if (res1 && res2 && res3) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`); | ||
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED'); | ||
t.not(res3.headers.get('etag'), req2.headers.get('if-none-match')); | ||
t.is(res3.headers.get('cf-cache-status'), 'MISS'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV when resource in cache, etag should be weakened before returned to eyeball', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
const resourceKey = 'key1.png'; | ||
const resourceVersion = JSON.parse(mocks_1.mockManifest())[resourceKey]; | ||
const req1 = new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': `"${resourceVersion}"`, | ||
}, | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV if sub/ -> sub/index.html served', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request("https://foo.com/sub")); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'picturedis']); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV gets index.html by default for / requests', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'index.html']); | ||
t.true(res.headers.get('content-type').includes('html')); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV non ASCII path support', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/测试.html')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'My filename is non-ascii']); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV supports browser percent encoded URLs', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://example.com/%not-really-percent-encoded.html')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'browser percent encoded']); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV supports user percent encoded URLs', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/%2F.html')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'user percent encoded']); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV only decode URL when necessary', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event1, event2, res1, res2, _a, _b, _c, _d; | ||
return __generator(this, function (_e) { | ||
switch (_e.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event1 = mocks_1.getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD.html')); | ||
event2 = mocks_1.getEvent(new Request('https://blah.com/你好.html')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event1)]; | ||
case 1: | ||
res1 = _e.sent(); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event2)]; | ||
case 2: | ||
res2 = _e.sent(); | ||
if (!(res1 && res2)) return [3 /*break*/, 5]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res1.text()]; | ||
case 3: | ||
_b.apply(_a, [_e.sent(), 'Im important']); | ||
_d = (_c = t).is; | ||
return [4 /*yield*/, res2.text()]; | ||
case 4: | ||
_d.apply(_c, [_e.sent(), 'Im important']); | ||
return [3 /*break*/, 6]; | ||
case 5: | ||
t.fail('Response was undefined'); | ||
_e.label = 6; | ||
case 6: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV custom key modifier', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, customRequestMapper, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/docs/sub/blah.png')); | ||
customRequestMapper = function (request) { | ||
var defaultModifiedRequest = index_1.mapRequestToAsset(request); | ||
var url = new URL(defaultModifiedRequest.url); | ||
url.pathname = url.pathname.replace('/docs', ''); | ||
return new Request(url.toString(), request); | ||
}; | ||
return [4 /*yield*/, index_1.getAssetFromKV(event, { mapRequestToAsset: customRequestMapper })]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'picturedis']); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV when setting browser caching', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event, { cacheControl: { browserTTL: 22 } })]; | ||
case 1: | ||
res = _a.sent(); | ||
if (res) { | ||
t.is(res.headers.get('cache-control'), 'max-age=22'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV when setting custom cache setting', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event1, event2, cacheOnlyPngs, res1, res2; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event1 = mocks_1.getEvent(new Request('https://blah.com/')); | ||
event2 = mocks_1.getEvent(new Request('https://blah.com/key1.png?blah=34')); | ||
cacheOnlyPngs = function (req) { | ||
if (new URL(req.url).pathname.endsWith('.png')) | ||
return { | ||
browserTTL: 720, | ||
edgeTTL: 720, | ||
}; | ||
else | ||
return { | ||
bypassCache: true, | ||
}; | ||
}; | ||
return [4 /*yield*/, index_1.getAssetFromKV(event1, { cacheControl: cacheOnlyPngs })]; | ||
case 1: | ||
res1 = _a.sent(); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event2, { cacheControl: cacheOnlyPngs })]; | ||
case 2: | ||
res2 = _a.sent(); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cache-control'), null); | ||
t.true(res2.headers.get('content-type').includes('png')); | ||
t.is(res2.headers.get('cache-control'), 'max-age=720'); | ||
t.is(res2.headers.get('cf-cache-status'), 'MISS'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV caches on two sequential requests', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var resourceKey, resourceVersion, event1, event2, res1, res2; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
resourceKey = 'cache.html'; | ||
resourceVersion = JSON.parse(mocks_1.mockManifest())[resourceKey]; | ||
event1 = mocks_1.getEvent(new Request("https://blah.com/" + resourceKey)); | ||
event2 = mocks_1.getEvent(new Request("https://blah.com/" + resourceKey, { | ||
headers: { | ||
'if-none-match': resourceVersion | ||
} | ||
})); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event1, { cacheControl: { edgeTTL: 720, browserTTL: 720 } })]; | ||
case 1: | ||
res1 = _a.sent(); | ||
return [4 /*yield*/, mocks_1.sleep(1)]; | ||
case 2: | ||
_a.sent(); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event2)]; | ||
case 3: | ||
res2 = _a.sent(); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res1.headers.get('cache-control'), 'max-age=720'); | ||
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV does not store max-age on two sequential requests', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var resourceKey, resourceVersion, event1, event2, res1, res2; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
resourceKey = 'cache.html'; | ||
resourceVersion = JSON.parse(mocks_1.mockManifest())[resourceKey]; | ||
event1 = mocks_1.getEvent(new Request("https://blah.com/" + resourceKey)); | ||
event2 = mocks_1.getEvent(new Request("https://blah.com/" + resourceKey, { | ||
headers: { | ||
'if-none-match': resourceVersion | ||
} | ||
})); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } })]; | ||
case 1: | ||
res1 = _a.sent(); | ||
return [4 /*yield*/, mocks_1.sleep(100)]; | ||
case 2: | ||
_a.sent(); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event2)]; | ||
case 3: | ||
res2 = _a.sent(); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res1.headers.get('cache-control'), null); | ||
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED'); | ||
t.is(res2.headers.get('cache-control'), null); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV does not cache on Cloudflare when bypass cache set', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event, { cacheControl: { bypassCache: true } })]; | ||
case 1: | ||
res = _a.sent(); | ||
if (res) { | ||
t.is(res.headers.get('cache-control'), null); | ||
t.is(res.headers.get('cf-cache-status'), null); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV with no trailing slash on root', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'index.html']); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV with no trailing slash on a subdirectory', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/sub/blah.png')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'picturedis']); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV no result throws an error', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, error; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/random')); | ||
return [4 /*yield*/, t.throwsAsync(index_1.getAssetFromKV(event))]; | ||
case 1: | ||
error = _a.sent(); | ||
t.is(error.status, 404); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV TTls set to null should not cache on browser or edge', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var event, res1, res2; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } })]; | ||
case 1: | ||
res1 = _a.sent(); | ||
return [4 /*yield*/, mocks_1.sleep(100)]; | ||
case 2: | ||
_a.sent(); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } })]; | ||
case 3: | ||
res2 = _a.sent(); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), null); | ||
t.is(res1.headers.get('cache-control'), null); | ||
t.is(res2.headers.get('cf-cache-status'), null); | ||
t.is(res2.headers.get('cache-control'), null); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV passing in a custom NAMESPACE serves correct asset', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var CUSTOM_NAMESPACE, event, res, _a, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
CUSTOM_NAMESPACE = mocks_1.mockKV({ | ||
'key1.123HASHBROWN.txt': 'val1', | ||
}); | ||
Object.assign(global, { CUSTOM_NAMESPACE: CUSTOM_NAMESPACE }); | ||
event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 1: | ||
res = _c.sent(); | ||
if (!res) return [3 /*break*/, 3]; | ||
_b = (_a = t).is; | ||
return [4 /*yield*/, res.text()]; | ||
case 2: | ||
_b.apply(_a, [_c.sent(), 'index.html']); | ||
t.true(res.headers.get('content-type').includes('html')); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
t.fail('Response was undefined'); | ||
_c.label = 4; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV when custom namespace without the asset should fail', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var CUSTOM_NAMESPACE, event, error; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
CUSTOM_NAMESPACE = mocks_1.mockKV({ | ||
'key5.123HASHBROWN.txt': 'customvalu', | ||
}); | ||
event = mocks_1.getEvent(new Request('https://blah.com')); | ||
return [4 /*yield*/, t.throwsAsync(index_1.getAssetFromKV(event, { ASSET_NAMESPACE: CUSTOM_NAMESPACE }))]; | ||
case 1: | ||
error = _a.sent(); | ||
t.is(error.status, 404); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV when namespace not bound fails', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var MY_CUSTOM_NAMESPACE, event, error; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
MY_CUSTOM_NAMESPACE = undefined; | ||
Object.assign(global, { MY_CUSTOM_NAMESPACE: MY_CUSTOM_NAMESPACE }); | ||
event = mocks_1.getEvent(new Request('https://blah.com/')); | ||
return [4 /*yield*/, t.throwsAsync(index_1.getAssetFromKV(event, { ASSET_NAMESPACE: MY_CUSTOM_NAMESPACE }))]; | ||
case 1: | ||
error = _a.sent(); | ||
t.is(error.status, 500); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV when if-none-match === etag and etag === pathKey in manifest, should revalidate', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var resourceKey, resourceVersion, event1, event2, res1, res2; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
resourceKey = 'key1.png'; | ||
resourceVersion = JSON.parse(mocks_1.mockManifest())[resourceKey]; | ||
event1 = mocks_1.getEvent(new Request("https://blah.com/" + resourceKey)); | ||
event2 = mocks_1.getEvent(new Request("https://blah.com/" + resourceKey, { | ||
headers: { | ||
'if-none-match': resourceVersion | ||
} | ||
})); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } })]; | ||
case 1: | ||
res1 = _a.sent(); | ||
return [4 /*yield*/, mocks_1.sleep(100)]; | ||
case 2: | ||
_a.sent(); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event2)]; | ||
case 3: | ||
res2 = _a.sent(); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res2.headers.get('etag'), resourceVersion); | ||
t.is(res2.status, 304); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV when etag and if-none-match are present but if-none-match !== etag, should bypass cache', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var resourceKey, resourceVersion, req1, req2, event, event2, res1, res2, res3; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
mocks_1.mockGlobal(); | ||
resourceKey = 'key1.png'; | ||
resourceVersion = JSON.parse(mocks_1.mockManifest())[resourceKey]; | ||
req1 = new Request("https://blah.com/" + resourceKey, { | ||
headers: { | ||
'if-none-match': resourceVersion | ||
} | ||
}); | ||
req2 = new Request("https://blah.com/" + resourceKey, { | ||
headers: { | ||
'if-none-match': resourceVersion + "another-version" | ||
} | ||
}); | ||
event = mocks_1.getEvent(req1); | ||
event2 = mocks_1.getEvent(req2); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } })]; | ||
case 1: | ||
res1 = _a.sent(); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 2: | ||
res2 = _a.sent(); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event2)]; | ||
case 3: | ||
res3 = _a.sent(); | ||
if (res1 && res2 && res3) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res2.headers.get('etag'), req1.headers.get('if-none-match')); | ||
t.true(req2.headers.has('if-none-match')); | ||
t.not(res3.headers.get('etag'), req2.headers.get('if-none-match')); | ||
t.is(res3.headers.get('cf-cache-status'), 'MISS'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV if-none-match not sent but resource in cache, should return hit', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var resourceKey, event, res1, res2; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
resourceKey = 'cache.html'; | ||
event = mocks_1.getEvent(new Request("https://blah.com/" + resourceKey)); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } })]; | ||
case 1: | ||
res1 = _a.sent(); | ||
return [4 /*yield*/, mocks_1.sleep(1)]; | ||
case 2: | ||
_a.sent(); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event)]; | ||
case 3: | ||
res2 = _a.sent(); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res1.headers.get('cache-control'), null); | ||
t.is(res2.headers.get('cf-cache-status'), 'HIT'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
ava_1.default('getAssetFromKV if range request submitted and resource in cache, request fulfilled', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var resourceKey, event1, event2, res1, res2; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
resourceKey = 'cache.html'; | ||
event1 = mocks_1.getEvent(new Request("https://blah.com/" + resourceKey)); | ||
event2 = mocks_1.getEvent(new Request("https://blah.com/" + resourceKey, { headers: { 'range': 'bytes=0-10' } })); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } })]; | ||
case 1: | ||
res1 = _a.sent(); | ||
return [4 /*yield*/, res1]; | ||
case 2: | ||
_a.sent(); | ||
return [4 /*yield*/, mocks_1.sleep(2)]; | ||
case 3: | ||
_a.sent(); | ||
return [4 /*yield*/, index_1.getAssetFromKV(event2)]; | ||
case 4: | ||
res2 = _a.sent(); | ||
if (res2.headers.has('content-range')) { | ||
t.is(res2.status, 206); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); }); | ||
const event = mocks_1.getEvent(req1); | ||
const res1 = yield index_1.getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }); | ||
const res2 = yield index_1.getAssetFromKV(event); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV if-none-match not sent but resource in cache, should return cache hit 200 OK', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
const resourceKey = 'cache.html'; | ||
const event = mocks_1.getEvent(new Request(`https://blah.com/${resourceKey}`)); | ||
const res1 = yield index_1.getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }); | ||
yield mocks_1.sleep(1); | ||
const res2 = yield index_1.getAssetFromKV(event); | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS'); | ||
t.is(res1.headers.get('cache-control'), null); | ||
t.is(res2.status, 200); | ||
t.is(res2.headers.get('cf-cache-status'), 'HIT'); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default('getAssetFromKV if range request submitted and resource in cache, request fulfilled', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
const resourceKey = 'cache.html'; | ||
const event1 = mocks_1.getEvent(new Request(`https://blah.com/${resourceKey}`)); | ||
const event2 = mocks_1.getEvent(new Request(`https://blah.com/${resourceKey}`, { headers: { range: 'bytes=0-10' } })); | ||
const res1 = index_1.getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }); | ||
yield res1; | ||
yield mocks_1.sleep(2); | ||
const res2 = yield index_1.getAssetFromKV(event2); | ||
if (res2.headers.has('content-range')) { | ||
t.is(res2.status, 206); | ||
} | ||
else { | ||
t.fail('Response was undefined'); | ||
} | ||
})); | ||
ava_1.default.todo('getAssetFromKV when body not empty, should invoke .cancel()'); |
@@ -11,63 +11,31 @@ "use strict"; | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var ava_1 = require("ava"); | ||
var mocks_1 = require("../mocks"); | ||
var index_1 = require("../index"); | ||
ava_1.default('mapRequestToAsset() correctly changes /about -> /about/index.html', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var path, request, newRequest; | ||
return __generator(this, function (_a) { | ||
mocks_1.mockGlobal(); | ||
path = '/about'; | ||
request = new Request("https://foo.com" + path); | ||
newRequest = index_1.mapRequestToAsset(request); | ||
t.is(newRequest.url, request.url + '/index.html'); | ||
return [2 /*return*/]; | ||
}); | ||
}); }); | ||
ava_1.default('mapRequestToAsset() correctly changes /about/ -> /about/index.html', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var path, request, newRequest; | ||
return __generator(this, function (_a) { | ||
path = '/about/'; | ||
request = new Request("https://foo.com" + path); | ||
newRequest = index_1.mapRequestToAsset(request); | ||
t.is(newRequest.url, request.url + 'index.html'); | ||
return [2 /*return*/]; | ||
}); | ||
}); }); | ||
ava_1.default('mapRequestToAsset() correctly changes /about.me/ -> /about.me/index.html', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var path, request, newRequest; | ||
return __generator(this, function (_a) { | ||
path = '/about.me/'; | ||
request = new Request("https://foo.com" + path); | ||
newRequest = index_1.mapRequestToAsset(request); | ||
t.is(newRequest.url, request.url + 'index.html'); | ||
return [2 /*return*/]; | ||
}); | ||
}); }); | ||
const ava_1 = require("ava"); | ||
const mocks_1 = require("../mocks"); | ||
const index_1 = require("../index"); | ||
ava_1.default('mapRequestToAsset() correctly changes /about -> /about/index.html', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
let path = '/about'; | ||
let request = new Request(`https://foo.com${path}`); | ||
let newRequest = index_1.mapRequestToAsset(request); | ||
t.is(newRequest.url, request.url + '/index.html'); | ||
})); | ||
ava_1.default('mapRequestToAsset() correctly changes /about/ -> /about/index.html', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
let path = '/about/'; | ||
let request = new Request(`https://foo.com${path}`); | ||
let newRequest = index_1.mapRequestToAsset(request); | ||
t.is(newRequest.url, request.url + 'index.html'); | ||
})); | ||
ava_1.default('mapRequestToAsset() correctly changes /about.me/ -> /about.me/index.html', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
let path = '/about.me/'; | ||
let request = new Request(`https://foo.com${path}`); | ||
let newRequest = index_1.mapRequestToAsset(request); | ||
t.is(newRequest.url, request.url + 'index.html'); | ||
})); | ||
ava_1.default('mapRequestToAsset() correctly changes /about -> /about/default.html', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
mocks_1.mockGlobal(); | ||
let path = '/about'; | ||
let request = new Request(`https://foo.com${path}`); | ||
let newRequest = index_1.mapRequestToAsset(request, { defaultDocument: 'default.html' }); | ||
t.is(newRequest.url, request.url + '/default.html'); | ||
})); |
@@ -11,72 +11,33 @@ "use strict"; | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var ava_1 = require("ava"); | ||
var mocks_1 = require("../mocks"); | ||
var index_1 = require("../index"); | ||
const ava_1 = require("ava"); | ||
const mocks_1 = require("../mocks"); | ||
const index_1 = require("../index"); | ||
function testRequest(path) { | ||
mocks_1.mockGlobal(); | ||
var url = new URL('https://example.com'); | ||
let url = new URL('https://example.com'); | ||
url.pathname = path; | ||
var request = new Request(url.toString()); | ||
let request = new Request(url.toString()); | ||
return request; | ||
} | ||
ava_1.default('serveSinglePageApp returns root asset path when request path ends in .html', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var path, request, expected_request, actual_request; | ||
return __generator(this, function (_a) { | ||
path = '/foo/thing.html'; | ||
request = testRequest(path); | ||
expected_request = testRequest('/index.html'); | ||
actual_request = index_1.serveSinglePageApp(request); | ||
t.deepEqual(expected_request, actual_request); | ||
return [2 /*return*/]; | ||
}); | ||
}); }); | ||
ava_1.default('serveSinglePageApp returns root asset path when request path does not have extension', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var path, request, expected_request, actual_request; | ||
return __generator(this, function (_a) { | ||
path = '/foo/thing'; | ||
request = testRequest(path); | ||
expected_request = testRequest('/index.html'); | ||
actual_request = index_1.serveSinglePageApp(request); | ||
t.deepEqual(expected_request, actual_request); | ||
return [2 /*return*/]; | ||
}); | ||
}); }); | ||
ava_1.default('serveSinglePageApp returns requested asset when request path has non-html extension', function (t) { return __awaiter(void 0, void 0, void 0, function () { | ||
var path, request, expected_request, actual_request; | ||
return __generator(this, function (_a) { | ||
path = '/foo/thing.js'; | ||
request = testRequest(path); | ||
expected_request = request; | ||
actual_request = index_1.serveSinglePageApp(request); | ||
t.deepEqual(expected_request, actual_request); | ||
return [2 /*return*/]; | ||
}); | ||
}); }); | ||
ava_1.default('serveSinglePageApp returns root asset path when request path ends in .html', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
let path = '/foo/thing.html'; | ||
let request = testRequest(path); | ||
let expected_request = testRequest('/index.html'); | ||
let actual_request = index_1.serveSinglePageApp(request); | ||
t.deepEqual(expected_request, actual_request); | ||
})); | ||
ava_1.default('serveSinglePageApp returns root asset path when request path does not have extension', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
let path = '/foo/thing'; | ||
let request = testRequest(path); | ||
let expected_request = testRequest('/index.html'); | ||
let actual_request = index_1.serveSinglePageApp(request); | ||
t.deepEqual(expected_request, actual_request); | ||
})); | ||
ava_1.default('serveSinglePageApp returns requested asset when request path has non-html extension', (t) => __awaiter(void 0, void 0, void 0, function* () { | ||
let path = '/foo/thing.js'; | ||
let request = testRequest(path); | ||
let expected_request = request; | ||
let actual_request = index_1.serveSinglePageApp(request); | ||
t.deepEqual(expected_request, actual_request); | ||
})); |
@@ -10,4 +10,5 @@ export declare type CacheControl = { | ||
ASSET_MANIFEST: Object | string; | ||
mapRequestToAsset: (req: Request) => Request; | ||
mapRequestToAsset?: (req: Request, options?: Partial<Options>) => Request; | ||
defaultMimeType: string; | ||
defaultDocument: string; | ||
}; | ||
@@ -14,0 +15,0 @@ export declare class KVError extends Error { |
"use strict"; | ||
var __extends = (this && this.__extends) || (function () { | ||
var extendStatics = function (d, b) { | ||
extendStatics = Object.setPrototypeOf || | ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || | ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; | ||
return extendStatics(d, b); | ||
}; | ||
return function (d, b) { | ||
extendStatics(d, b); | ||
function __() { this.constructor = d; } | ||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); | ||
}; | ||
})(); | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.InternalError = exports.NotFoundError = exports.MethodNotAllowedError = exports.KVError = void 0; | ||
var KVError = /** @class */ (function (_super) { | ||
__extends(KVError, _super); | ||
function KVError(message, status) { | ||
var _newTarget = this.constructor; | ||
if (status === void 0) { status = 500; } | ||
var _this = _super.call(this, message) || this; | ||
class KVError extends Error { | ||
constructor(message, status = 500) { | ||
super(message); | ||
// see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html | ||
Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain | ||
_this.name = KVError.name; // stack traces display correctly now | ||
_this.status = status; | ||
return _this; | ||
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain | ||
this.name = KVError.name; // stack traces display correctly now | ||
this.status = status; | ||
} | ||
return KVError; | ||
}(Error)); | ||
} | ||
exports.KVError = KVError; | ||
var MethodNotAllowedError = /** @class */ (function (_super) { | ||
__extends(MethodNotAllowedError, _super); | ||
function MethodNotAllowedError(message, status) { | ||
if (message === void 0) { message = "Not a valid request method"; } | ||
if (status === void 0) { status = 405; } | ||
return _super.call(this, message, status) || this; | ||
class MethodNotAllowedError extends KVError { | ||
constructor(message = `Not a valid request method`, status = 405) { | ||
super(message, status); | ||
} | ||
return MethodNotAllowedError; | ||
}(KVError)); | ||
} | ||
exports.MethodNotAllowedError = MethodNotAllowedError; | ||
var NotFoundError = /** @class */ (function (_super) { | ||
__extends(NotFoundError, _super); | ||
function NotFoundError(message, status) { | ||
if (message === void 0) { message = "Not Found"; } | ||
if (status === void 0) { status = 404; } | ||
return _super.call(this, message, status) || this; | ||
class NotFoundError extends KVError { | ||
constructor(message = `Not Found`, status = 404) { | ||
super(message, status); | ||
} | ||
return NotFoundError; | ||
}(KVError)); | ||
} | ||
exports.NotFoundError = NotFoundError; | ||
var InternalError = /** @class */ (function (_super) { | ||
__extends(InternalError, _super); | ||
function InternalError(message, status) { | ||
if (message === void 0) { message = "Internal Error in KV Asset Handler"; } | ||
if (status === void 0) { status = 500; } | ||
return _super.call(this, message, status) || this; | ||
class InternalError extends KVError { | ||
constructor(message = `Internal Error in KV Asset Handler`, status = 500) { | ||
super(message, status); | ||
} | ||
return InternalError; | ||
}(KVError)); | ||
} | ||
exports.InternalError = InternalError; |
{ | ||
"name": "@cloudflare/kv-asset-handler", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"description": "Routes requests to KV assets", | ||
@@ -10,4 +10,6 @@ "main": "dist/index.js", | ||
"build": "tsc -d", | ||
"format": "prettier --write '**/*.js'", | ||
"format": "prettier --write \"**/*.{js,ts,json,md}\"", | ||
"pretest": "npm run build", | ||
"lint:code": "prettier --check \"**/*.{js,ts,json,md}\"", | ||
"lint:markdown": "markdownlint \"**/*.md\" --ignore node_modules", | ||
"test": "ava dist/test/*.js --verbose" | ||
@@ -17,3 +19,3 @@ }, | ||
"type": "git", | ||
"url": "git+https://github.com/cloudflare/workers-site-npm-pkg.git" | ||
"url": "git+https://github.com/cloudflare/kv-asset-handler.git" | ||
}, | ||
@@ -27,2 +29,8 @@ "keywords": [ | ||
], | ||
"files": [ | ||
"src", | ||
"dist", | ||
"LICENSE_APACHE", | ||
"LICENSE_MIT" | ||
], | ||
"author": "wrangler@cloudflare.com", | ||
@@ -35,11 +43,14 @@ "license": "MIT OR Apache-2.0", | ||
"dependencies": { | ||
"@cloudflare/workers-types": "^2.0.0", | ||
"@types/mime": "^2.0.2", | ||
"mime": "^2.4.6" | ||
"mime": "^2.5.2" | ||
}, | ||
"devDependencies": { | ||
"ava": "^3.9.0", | ||
"@ava/typescript": "^1.1.1", | ||
"@cloudflare/workers-types": "^2.2.2", | ||
"@types/mime": "^2.0.3", | ||
"@types/node": "^15.3.0", | ||
"ava": "^3.15.0", | ||
"prettier": "^2.3.0", | ||
"service-worker-mock": "^2.0.5", | ||
"typescript": "^3.9.5" | ||
"typescript": "^4.2.4" | ||
} | ||
} |
# @cloudflare/kv-asset-handler | ||
* [Installation](#installation) | ||
* [Usage](#usage) | ||
* [`getAssetFromKV`](#-getassetfromkv-) | ||
- [Example](#example) | ||
+ [Return](#return) | ||
+ [Optional Arguments](#optional-arguments) | ||
- [`mapRequestToAsset`](#-maprequesttoasset-) | ||
- [Example](#example-1) | ||
- [`cacheControl`](#-cachecontrol-) | ||
* [`browserTTL`](#-browserttl-) | ||
* [`edgeTTL`](#-edgettl-) | ||
* [`bypassCache`](#-bypasscache-) | ||
- [`ASSET_NAMESPACE`](#-asset-namespace-) | ||
- [`ASSET_MANIFEST` (optional)](#-asset-manifest---optional-) | ||
- [Helper functions](#helper-functions) | ||
* [`mapRequestToAsset`](#-maprequesttoasset--1) | ||
* [`serveSinglePageApp`](#-servesinglepageapp-) | ||
- [Cache revalidation and etags](#cache-revalidation-and-etags) | ||
[![npm](https://img.shields.io/npm/v/@cloudflare/kv-asset-handler.svg)](https://www.npmjs.com/package/@cloudflare/kv-asset-handler) | ||
[![Run npm tests](https://github.com/cloudflare/kv-asset-handler/actions/workflows/test.yml/badge.svg)](https://github.com/cloudflare/kv-asset-handler/actions/workflows/test.yml) | ||
[![Lint Markdown](https://github.com/cloudflare/kv-asset-handler/actions/workflows/lint.yml/badge.svg)](https://github.com/cloudflare/kv-asset-handler/actions/workflows/lint.yml) | ||
`kv-asset-handler` is an open-source library for managing the retrieval of static assets from [Workers KV](https://developers.cloudflare.com/workers/runtime-apis/kv) inside of a [Cloudflare Workers](https://workers.dev) function. `kv-asset-handler` is designed for use with [Workers Sites](https://developers.cloudflare.com/workers/platform/sites), a feature included in [Wrangler](https://github.com/cloudflare/wrangler), our command-line tool for deploying Workers projects. | ||
`kv-asset-handler` runs as part of a Cloudflare Workers function, so it allows you to customize _how_ and _when_ your static assets are loaded, as well as customize how those assets behave before they're sent to the client. | ||
Most users and sites will not need these customizations, and just want to serve their statically built applications. For that simple use-case, you can check out [Cloudflare Pages](https://pages.cloudflare.com), our batteries-included approach to deploying static sites on Cloudflare's edge network. Workers Sites was designed as a precursor to Cloudflare Pages, so check out Pages if you just want to deploy your static site without any special customizations! | ||
For users who _do_ want to customize their assets, and want to build complex experiences on top of their static assets, `kv-asset-handler` (and the default [Workers Sites template](https://github.com/cloudflare/worker-sites-template), which is provided for use with Wrangler + Workers Sites) allows you to customize caching behavior, headers, and anything else about how your Workers function loads the static assets for your site stored in Workers KV. | ||
The Cloudflare Workers Discord server is an active place where Workers users get help, share feedback, and collaborate on making our platform better. The `#workers-sites` channel in particular is a great place to chat about `kv-asset-handler`, and building cool experiences for your users using these tools! If you have questions, want to share what you're working on, or give feedback, [join us in Discord and say hello](https://discord.gg/cloudflaredev)! | ||
- [Installation](#installation) | ||
- [Usage](#usage) | ||
- [`getAssetFromKV`](#-getassetfromkv) | ||
- [Example](#example) | ||
* [Return](#return) | ||
* [Optional Arguments](#optional-arguments) | ||
- [`mapRequestToAsset`](#-maprequesttoasset) | ||
- [Example](#example-1) | ||
- [`cacheControl`](#-cachecontrol) | ||
- [`browserTTL`](#-browserttl) | ||
- [`edgeTTL`](#-edgettl) | ||
- [`bypassCache`](#-bypasscache) | ||
- [`ASSET_NAMESPACE`](#-asset-namespace) | ||
- [`ASSET_MANIFEST` (optional)](#-asset-manifest---optional) | ||
* [Helper functions](#helper-functions) | ||
- [`mapRequestToAsset`](#-maprequesttoasset-1) | ||
- [`serveSinglePageApp`](#-servesinglepageapp) | ||
* [Cache revalidation and etags](#cache-revalidation-and-etags) | ||
## Installation | ||
@@ -58,3 +73,3 @@ | ||
addEventListener('fetch', event => { | ||
addEventListener('fetch', (event) => { | ||
event.respondWith(handleEvent(event)) | ||
@@ -73,3 +88,3 @@ }) | ||
} else { | ||
return new Response("An unexpected error occurred", { status: 500 }) | ||
return new Response('An unexpected error occurred', { status: 500 }) | ||
} | ||
@@ -95,3 +110,3 @@ } | ||
By default, mapRequestToAsset is set to the exported function [`mapRequestToAsset`](#maprequesttoasset-1). This works for most static site generators, but you can customize this behavior by passing your own function as `mapRequestToAsset`. The function should take a `Request` object as its only argument, and return a new `Request` object with an updated path to be looked up in the asset manifest/KV. | ||
By default, mapRequestToAsset is set to the exported function [`mapRequestToAsset`](#maprequesttoasset-1). This works for most static site generators, but you can customize this behavior by passing your own function as `mapRequestToAsset`. The function should take a `Request` object as its only argument, and return a new `Request` object with an updated path to be looked up in the asset manifest/KV. | ||
@@ -183,2 +198,8 @@ For SPA mapping pass in the [`serveSinglePageApp`](#servesinglepageapp) function | ||
#### `defaultDocument` (optional) | ||
type: string | ||
This is the default document that will be concatenated for requests ends in `'/'` or without a valid mime type like `'/about'` or `'/about.me'`. The default value is `'index.html'`. | ||
# Helper functions | ||
@@ -212,4 +233,4 @@ | ||
* When a request's `if-none-match` value matches the `etag` of the resource in Cloudflare cache, Cloudflare will send a `304 Not Modified` response without a body, saving bandwidth. | ||
* Changes to a file on the server are immediately reflected in the browser - even when the cache control directive `max-age` is unexpired. | ||
- When a request's `if-none-match` value matches the `etag` of the resource in Cloudflare cache, Cloudflare will send a `304 Not Modified` response without a body, saving bandwidth. | ||
- Changes to a file on the server are immediately reflected in the browser - even when the cache control directive `max-age` is unexpired. | ||
@@ -223,3 +244,3 @@ #### Disable the `etag` | ||
let cacheControl = { | ||
bypassCache: true | ||
bypassCache: true, | ||
} | ||
@@ -226,0 +247,0 @@ ``` |
180
src/index.ts
@@ -7,2 +7,24 @@ import * as mime from 'mime' | ||
} | ||
const defaultCacheControl: CacheControl = { | ||
browserTTL: null, | ||
edgeTTL: 2 * 60 * 60 * 24, // 2 days | ||
bypassCache: false, // do not bypass Cloudflare's cache | ||
} | ||
function assignOptions(options?: Partial<Options>): Options { | ||
// Assign any missing options passed in to the default | ||
// options.mapRequestToAsset is handled manually later | ||
return Object.assign( | ||
{ | ||
ASSET_NAMESPACE: __STATIC_CONTENT, | ||
ASSET_MANIFEST: __STATIC_CONTENT_MANIFEST, | ||
cacheControl: defaultCacheControl, | ||
defaultMimeType: 'text/plain', | ||
defaultDocument: 'index.html', | ||
}, | ||
options, | ||
) | ||
} | ||
/** | ||
@@ -15,3 +37,5 @@ * maps the path of incoming request to the request pathKey to look up | ||
*/ | ||
const mapRequestToAsset = (request: Request) => { | ||
const mapRequestToAsset = (request: Request, options?: Partial<Options>) => { | ||
options = assignOptions(options) | ||
const parsedUrl = new URL(request.url) | ||
@@ -21,9 +45,9 @@ let pathname = parsedUrl.pathname | ||
if (pathname.endsWith('/')) { | ||
// If path looks like a directory append index.html | ||
// If path looks like a directory append options.defaultDocument | ||
// e.g. If path is /about/ -> /about/index.html | ||
pathname = pathname.concat('index.html') | ||
pathname = pathname.concat(options.defaultDocument) | ||
} else if (!mime.getType(pathname)) { | ||
// If path doesn't look like valid content | ||
// e.g. /about.me -> /about.me/index.html | ||
pathname = pathname.concat('/index.html') | ||
pathname = pathname.concat('/' + options.defaultDocument) | ||
} | ||
@@ -40,6 +64,8 @@ | ||
*/ | ||
function serveSinglePageApp(request: Request): Request { | ||
function serveSinglePageApp(request: Request, options?: Partial<Options>): Request { | ||
options = assignOptions(options) | ||
// First apply the default handler, which already has logic to detect | ||
// paths that should map to HTML files. | ||
request = mapRequestToAsset(request) | ||
request = mapRequestToAsset(request, options) | ||
@@ -51,4 +77,4 @@ const parsedUrl = new URL(request.url) | ||
if (parsedUrl.pathname.endsWith('.html')) { | ||
// If expected HTML file was missing, just return the root index.html | ||
return new Request(`${parsedUrl.origin}/index.html`, request) | ||
// If expected HTML file was missing, just return the root index.html (or options.defaultDocument) | ||
return new Request(`${parsedUrl.origin}/${options.defaultDocument}`, request) | ||
} else { | ||
@@ -61,8 +87,2 @@ // The default handler decided this is not an HTML page. It's probably | ||
const defaultCacheControl: CacheControl = { | ||
browserTTL: null, | ||
edgeTTL: 2 * 60 * 60 * 24, // 2 days | ||
bypassCache: false, // do not bypass Cloudflare's cache | ||
} | ||
/** | ||
@@ -80,19 +100,10 @@ * takes the path of the incoming request, gathers the appropriate content from KV, and returns | ||
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, | ||
) | ||
options = assignOptions(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 | ||
const ASSET_MANIFEST = | ||
typeof options.ASSET_MANIFEST === 'string' | ||
? JSON.parse(options.ASSET_MANIFEST) | ||
: options.ASSET_MANIFEST | ||
@@ -103,19 +114,31 @@ if (typeof ASSET_NAMESPACE === 'undefined') { | ||
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]) { | ||
// if options.mapRequestToAsset is explicitly passed in, always use it and assume user has own intentions | ||
// otherwise handle request as normal, with default mapRequestToAsset below | ||
if (options.mapRequestToAsset) { | ||
requestKey = options.mapRequestToAsset(request) | ||
} else if (ASSET_MANIFEST[rawPathKey]) { | ||
requestKey = request | ||
} else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) { | ||
pathIsEncoded = true; | ||
pathIsEncoded = true | ||
requestKey = request | ||
} else { | ||
requestKey = options.mapRequestToAsset(request) | ||
const mappedRequest = mapRequestToAsset(request) | ||
const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(/^\/+/, '') | ||
if (ASSET_MANIFEST[decodeURIComponent(mappedRawPathKey)]) { | ||
pathIsEncoded = true | ||
requestKey = mappedRequest | ||
} else { | ||
// use default mapRequestToAsset | ||
requestKey = mapRequestToAsset(request, options) | ||
} | ||
} | ||
const SUPPORTED_METHODS = ['GET', 'HEAD'] | ||
if (!SUPPORTED_METHODS.includes(requestKey.method)) { | ||
throw new MethodNotAllowedError(`${requestKey.method} is not a valid request method`) | ||
} | ||
const parsedUrl = new URL(requestKey.url) | ||
@@ -131,3 +154,3 @@ const pathname = pathIsEncoded ? decodeURIComponent(parsedUrl.pathname) : parsedUrl.pathname // decode percentage encoded path only when necessary | ||
if (mimeType.startsWith('text') || mimeType === 'application/javascript') { | ||
mimeType += '; charset=utf-8' | ||
mimeType += '; charset=utf-8' | ||
} | ||
@@ -162,2 +185,29 @@ | ||
// formats the etag depending on the response context. if the entityId | ||
// is invalid, returns an empty string (instead of null) to prevent the | ||
// the potentially disastrous scenario where the value of the Etag resp | ||
// header is "null". Could be modified in future to base64 encode etc | ||
const formatETag = (entityId: any = pathKey, validatorType: string = 'strong') => { | ||
if (!entityId) { | ||
return '' | ||
} | ||
switch (validatorType) { | ||
case 'weak': | ||
if (!entityId.startsWith('W/')) { | ||
return `W/${entityId}` | ||
} | ||
return entityId | ||
case 'strong': | ||
if (entityId.startsWith(`W/"`)) { | ||
entityId = entityId.replace('W/', '') | ||
} | ||
if (!entityId.endsWith(`"`)) { | ||
entityId = `"${entityId}"` | ||
} | ||
return entityId | ||
default: | ||
return '' | ||
} | ||
} | ||
options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts) | ||
@@ -182,23 +232,5 @@ | ||
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.status > 300 && response.status < 400) { | ||
if (response.body && 'cancel' in Object.getPrototypeOf(response.body)) { | ||
response.body.cancel(); | ||
response.body.cancel() | ||
console.log('Body exists and environment supports readable streams. Body cancelled') | ||
@@ -208,21 +240,17 @@ } else { | ||
} | ||
headers.set('cf-cache-status', 'REVALIDATED') | ||
response = new Response(null, { | ||
status: 304, | ||
headers, | ||
statusText: 'Not Modified', | ||
}) | ||
response = new Response(null, response) | ||
} else { | ||
headers.set('CF-Cache-Status', 'HIT') | ||
// fixes #165 | ||
let opts = { | ||
headers, | ||
headers: new Headers(response.headers), | ||
status: 0, | ||
statusText: '' | ||
statusText: '', | ||
} | ||
opts.headers.set('cf-cache-status', 'HIT') | ||
if (response.status) { | ||
opts.status = response.status | ||
opts.statusText = response.statusText | ||
} else if (headers.has('Content-Range')) { | ||
} else if (opts.headers.has('Content-Range')) { | ||
opts.status = 206 | ||
@@ -248,3 +276,3 @@ opts.statusText = 'Partial Content' | ||
if (!response.headers.has('etag')) { | ||
response.headers.set('etag', `${pathKey}`) | ||
response.headers.set('etag', formatETag(pathKey, 'strong')) | ||
} | ||
@@ -258,2 +286,16 @@ // determine Cloudflare cache behavior | ||
response.headers.set('Content-Type', mimeType) | ||
if (response.status === 304) { | ||
let etag = formatETag(response.headers.get('etag'), 'strong') | ||
let ifNoneMatch = cacheKey.headers.get('if-none-match') | ||
let proxyCacheStatus = response.headers.get('CF-Cache-Status') | ||
if (etag) { | ||
if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === 'MISS') { | ||
response.headers.set('CF-Cache-Status', 'EXPIRED') | ||
} else { | ||
response.headers.set('CF-Cache-Status', 'REVALIDATED') | ||
} | ||
response.headers.set('etag', formatETag(etag, 'weak')) | ||
} | ||
} | ||
if (shouldSetBrowserCache) { | ||
@@ -260,0 +302,0 @@ response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`) |
@@ -29,2 +29,3 @@ const makeServiceWorkerEnv = require('service-worker-mock') | ||
'client.123HASHBROWN/index.html': 'Im here but serve my big bro above', | ||
'你好/index.123HASHBROWN.html': 'My path is non-ascii', | ||
} | ||
@@ -50,4 +51,5 @@ export const mockKV = (store: any) => { | ||
'sub/index.html': `sub/index.${HASH}.html`, | ||
'client': `client.${HASH}`, | ||
client: `client.${HASH}`, | ||
'client/index.html': `client.${HASH}`, | ||
'你好/index.html': `你好/index.${HASH}.html`, | ||
}) | ||
@@ -58,4 +60,4 @@ } | ||
interface CacheKey { | ||
url:object; | ||
headers:object | ||
url: object | ||
headers: object | ||
} | ||
@@ -65,12 +67,11 @@ export const mockCaches = () => { | ||
default: { | ||
async match (key: any) { | ||
async match(key: any) { | ||
let cacheKey: CacheKey = { | ||
url: key.url, | ||
headers: {} | ||
headers: {}, | ||
} | ||
let response | ||
if (key.headers.has('if-none-match')) { | ||
cacheKey.headers = { | ||
'etag': key.headers.get('if-none-match') | ||
} | ||
let makeStrongEtag = key.headers.get('if-none-match').replace('W/', '') | ||
Reflect.set(cacheKey.headers, 'etag', makeStrongEtag) | ||
response = cacheStore.get(JSON.stringify(cacheKey)) | ||
@@ -87,3 +88,4 @@ } else { | ||
} | ||
if (response) { | ||
// TODO: write test to accomodate for rare scenarios with where range requests accomodate etags | ||
if (response && !key.headers.has('if-none-match')) { | ||
// this appears overly verbose, but is necessary to document edge cache behavior | ||
@@ -93,3 +95,6 @@ // The Range request header triggers the response header Content-Range ... | ||
if (range) { | ||
response.headers.set('content-range', `bytes ${range.split('=').pop()}/${response.headers.get('content-length')}`) | ||
response.headers.set( | ||
'content-range', | ||
`bytes ${range.split('=').pop()}/${response.headers.get('content-length')}`, | ||
) | ||
} | ||
@@ -102,17 +107,24 @@ // ... which we are using in this repository to set status 206 | ||
} | ||
let etag = response.headers.get('etag') | ||
if (etag && !etag.includes('W/')) { | ||
response.headers.set('etag', `W/${etag}`) | ||
} | ||
} | ||
return response | ||
}, | ||
async put (key: any, val: Response) { | ||
async put(key: any, val: Response) { | ||
let headers = new Headers(val.headers) | ||
let body = await val.text() | ||
let resp = new Response(body, { headers }) | ||
headers.set('content-length', (body.length).toString()) | ||
let url = new URL(key.url) | ||
let resWithBody = new Response(val.body, { headers, status: 200 }) | ||
let resNoBody = new Response(null, { headers, status: 304 }) | ||
let cacheKey: CacheKey = { | ||
url: key.url, | ||
headers: { | ||
'etag': val.headers.get('etag') | ||
} | ||
etag: `"${url.pathname.replace('/', '')}"`, | ||
}, | ||
} | ||
return cacheStore.set(JSON.stringify(cacheKey), resp) | ||
cacheStore.set(JSON.stringify(cacheKey), resNoBody) | ||
cacheKey.headers = {} | ||
cacheStore.set(JSON.stringify(cacheKey), resWithBody) | ||
return | ||
}, | ||
@@ -130,3 +142,3 @@ }, | ||
export const sleep = (milliseconds: number) => { | ||
return new Promise(resolve => setTimeout(resolve, milliseconds)) | ||
return new Promise((resolve) => setTimeout(resolve, milliseconds)) | ||
} |
@@ -6,3 +6,3 @@ import test from 'ava' | ||
test('getAssetFromKV return correct val from KV and default caching', async t => { | ||
test('getAssetFromKV return correct val from KV and default caching', async (t) => { | ||
mockGlobal() | ||
@@ -21,3 +21,3 @@ const event = getEvent(new Request('https://blah.com/key1.txt')) | ||
}) | ||
test('getAssetFromKV evaluated the file matching the extensionless path first /client/ -> client', async t => { | ||
test('getAssetFromKV evaluated the file matching the extensionless path first /client/ -> client', async (t) => { | ||
mockGlobal() | ||
@@ -29,3 +29,3 @@ const event = getEvent(new Request(`https://foo.com/client/`)) | ||
}) | ||
test('getAssetFromKV evaluated the file matching the extensionless path first /client -> client', async t => { | ||
test('getAssetFromKV evaluated the file matching the extensionless path first /client -> client', async (t) => { | ||
mockGlobal() | ||
@@ -38,3 +38,3 @@ const event = getEvent(new Request(`https://foo.com/client`)) | ||
test('getAssetFromKV if not in asset manifest still returns nohash.txt', async t => { | ||
test('getAssetFromKV if not in asset manifest still returns nohash.txt', async (t) => { | ||
mockGlobal() | ||
@@ -52,3 +52,3 @@ const event = getEvent(new Request('https://blah.com/nohash.txt')) | ||
test('getAssetFromKV if no asset manifest /client -> client fails', async t => { | ||
test('getAssetFromKV if no asset manifest /client -> client fails', async (t) => { | ||
mockGlobal() | ||
@@ -60,3 +60,3 @@ const event = getEvent(new Request(`https://foo.com/client`)) | ||
test('getAssetFromKV if sub/ -> sub/index.html served', async t => { | ||
test('getAssetFromKV if sub/ -> sub/index.html served', async (t) => { | ||
mockGlobal() | ||
@@ -72,3 +72,3 @@ const event = getEvent(new Request(`https://foo.com/sub`)) | ||
test('getAssetFromKV gets index.html by default for / requests', async t => { | ||
test('getAssetFromKV gets index.html by default for / requests', async (t) => { | ||
mockGlobal() | ||
@@ -86,3 +86,3 @@ const event = getEvent(new Request('https://blah.com/')) | ||
test('getAssetFromKV non ASCII path support', async t => { | ||
test('getAssetFromKV non ASCII path support', async (t) => { | ||
mockGlobal() | ||
@@ -99,3 +99,3 @@ const event = getEvent(new Request('https://blah.com/测试.html')) | ||
test('getAssetFromKV supports browser percent encoded URLs', async t => { | ||
test('getAssetFromKV supports browser percent encoded URLs', async (t) => { | ||
mockGlobal() | ||
@@ -112,3 +112,3 @@ const event = getEvent(new Request('https://example.com/%not-really-percent-encoded.html')) | ||
test('getAssetFromKV supports user percent encoded URLs', async t => { | ||
test('getAssetFromKV supports user percent encoded URLs', async (t) => { | ||
mockGlobal() | ||
@@ -125,3 +125,3 @@ const event = getEvent(new Request('https://blah.com/%2F.html')) | ||
test('getAssetFromKV only decode URL when necessary', async t => { | ||
test('getAssetFromKV only decode URL when necessary', async (t) => { | ||
mockGlobal() | ||
@@ -141,4 +141,19 @@ const event1 = getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD.html')) | ||
test('getAssetFromKV custom key modifier', async t => { | ||
test('getAssetFromKV Support for user decode url path', async (t) => { | ||
mockGlobal() | ||
const event1 = getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD/')) | ||
const event2 = getEvent(new Request('https://blah.com/你好/')) | ||
const res1 = await getAssetFromKV(event1) | ||
const res2 = await getAssetFromKV(event2) | ||
if (res1 && res2) { | ||
t.is(await res1.text(), 'My path is non-ascii') | ||
t.is(await res2.text(), 'My path is non-ascii') | ||
} else { | ||
t.fail('Response was undefined') | ||
} | ||
}) | ||
test('getAssetFromKV custom key modifier', async (t) => { | ||
mockGlobal() | ||
const event = getEvent(new Request('https://blah.com/docs/sub/blah.png')) | ||
@@ -163,3 +178,3 @@ | ||
test('getAssetFromKV when setting browser caching', async t => { | ||
test('getAssetFromKV when setting browser caching', async (t) => { | ||
mockGlobal() | ||
@@ -177,3 +192,3 @@ const event = getEvent(new Request('https://blah.com/')) | ||
test('getAssetFromKV when setting custom cache setting', async t => { | ||
test('getAssetFromKV when setting custom cache setting', async (t) => { | ||
mockGlobal() | ||
@@ -206,3 +221,3 @@ const event1 = getEvent(new Request('https://blah.com/')) | ||
}) | ||
test('getAssetFromKV caches on two sequential requests', async t => { | ||
test('getAssetFromKV caches on two sequential requests', async (t) => { | ||
mockGlobal() | ||
@@ -212,7 +227,9 @@ const resourceKey = 'cache.html' | ||
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) | ||
const event2 = getEvent(new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': resourceVersion | ||
} | ||
})) | ||
const event2 = getEvent( | ||
new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': `"${resourceVersion}"`, | ||
}, | ||
}), | ||
) | ||
@@ -231,3 +248,3 @@ const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720, browserTTL: 720 } }) | ||
}) | ||
test('getAssetFromKV does not store max-age on two sequential requests', async t => { | ||
test('getAssetFromKV does not store max-age on two sequential requests', async (t) => { | ||
mockGlobal() | ||
@@ -237,7 +254,9 @@ const resourceKey = 'cache.html' | ||
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) | ||
const event2 = getEvent(new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': resourceVersion | ||
} | ||
})) | ||
const event2 = getEvent( | ||
new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': `"${resourceVersion}"`, | ||
}, | ||
}), | ||
) | ||
@@ -258,3 +277,3 @@ const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) | ||
test('getAssetFromKV does not cache on Cloudflare when bypass cache set', async t => { | ||
test('getAssetFromKV does not cache on Cloudflare when bypass cache set', async (t) => { | ||
mockGlobal() | ||
@@ -273,3 +292,3 @@ const event = getEvent(new Request('https://blah.com/')) | ||
test('getAssetFromKV with no trailing slash on root', async t => { | ||
test('getAssetFromKV with no trailing slash on root', async (t) => { | ||
mockGlobal() | ||
@@ -285,3 +304,3 @@ const event = getEvent(new Request('https://blah.com')) | ||
test('getAssetFromKV with no trailing slash on a subdirectory', async t => { | ||
test('getAssetFromKV with no trailing slash on a subdirectory', async (t) => { | ||
mockGlobal() | ||
@@ -297,3 +316,3 @@ const event = getEvent(new Request('https://blah.com/sub/blah.png')) | ||
test('getAssetFromKV no result throws an error', async t => { | ||
test('getAssetFromKV no result throws an error', async (t) => { | ||
mockGlobal() | ||
@@ -304,3 +323,3 @@ const event = getEvent(new Request('https://blah.com/random')) | ||
}) | ||
test('getAssetFromKV TTls set to null should not cache on browser or edge', async t => { | ||
test('getAssetFromKV TTls set to null should not cache on browser or edge', async (t) => { | ||
mockGlobal() | ||
@@ -322,3 +341,3 @@ const event = getEvent(new Request('https://blah.com/')) | ||
}) | ||
test('getAssetFromKV passing in a custom NAMESPACE serves correct asset', async t => { | ||
test('getAssetFromKV passing in a custom NAMESPACE serves correct asset', async (t) => { | ||
mockGlobal() | ||
@@ -338,3 +357,3 @@ let CUSTOM_NAMESPACE = mockKV({ | ||
}) | ||
test('getAssetFromKV when custom namespace without the asset should fail', async t => { | ||
test('getAssetFromKV when custom namespace without the asset should fail', async (t) => { | ||
mockGlobal() | ||
@@ -351,3 +370,3 @@ let CUSTOM_NAMESPACE = mockKV({ | ||
}) | ||
test('getAssetFromKV when namespace not bound fails', async t => { | ||
test('getAssetFromKV when namespace not bound fails', async (t) => { | ||
mockGlobal() | ||
@@ -364,3 +383,3 @@ var MY_CUSTOM_NAMESPACE = undefined | ||
test('getAssetFromKV when if-none-match === etag and etag === pathKey in manifest, should revalidate', async t => { | ||
test('getAssetFromKV when if-none-match === active resource version, should revalidate', async (t) => { | ||
mockGlobal() | ||
@@ -370,7 +389,9 @@ const resourceKey = 'key1.png' | ||
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) | ||
const event2 = getEvent(new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': resourceVersion | ||
} | ||
})) | ||
const event2 = getEvent( | ||
new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': `W/"${resourceVersion}"`, | ||
}, | ||
}), | ||
) | ||
@@ -383,4 +404,3 @@ const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS') | ||
t.is(res2.headers.get('etag'), resourceVersion) | ||
t.is(res2.status, 304) | ||
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') | ||
} else { | ||
@@ -391,3 +411,3 @@ t.fail('Response was undefined') | ||
test('getAssetFromKV when etag and if-none-match are present but if-none-match !== etag, should bypass cache', async t => { | ||
test('getAssetFromKV when if-none-match equals etag of stale resource then should bypass cache', async (t) => { | ||
mockGlobal() | ||
@@ -398,9 +418,9 @@ const resourceKey = 'key1.png' | ||
headers: { | ||
'if-none-match': resourceVersion | ||
} | ||
'if-none-match': `"${resourceVersion}"`, | ||
}, | ||
}) | ||
const req2 = new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': resourceVersion + "another-version" | ||
} | ||
'if-none-match': `"${resourceVersion}-another-version"`, | ||
}, | ||
}) | ||
@@ -414,4 +434,4 @@ const event = getEvent(req1) | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS') | ||
t.is(res2.headers.get('etag'), req1.headers.get('if-none-match')) | ||
t.true(req2.headers.has('if-none-match')) | ||
t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`) | ||
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') | ||
t.not(res3.headers.get('etag'), req2.headers.get('if-none-match')) | ||
@@ -423,3 +443,23 @@ t.is(res3.headers.get('cf-cache-status'), 'MISS') | ||
}) | ||
test('getAssetFromKV if-none-match not sent but resource in cache, should return hit', async t => { | ||
test('getAssetFromKV when resource in cache, etag should be weakened before returned to eyeball', async (t) => { | ||
mockGlobal() | ||
const resourceKey = 'key1.png' | ||
const resourceVersion = JSON.parse(mockManifest())[resourceKey] | ||
const req1 = new Request(`https://blah.com/${resourceKey}`, { | ||
headers: { | ||
'if-none-match': `"${resourceVersion}"`, | ||
}, | ||
}) | ||
const event = getEvent(req1) | ||
const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }) | ||
const res2 = await getAssetFromKV(event) | ||
if (res1 && res2) { | ||
t.is(res1.headers.get('cf-cache-status'), 'MISS') | ||
t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`) | ||
} else { | ||
t.fail('Response was undefined') | ||
} | ||
}) | ||
test('getAssetFromKV if-none-match not sent but resource in cache, should return cache hit 200 OK', async (t) => { | ||
const resourceKey = 'cache.html' | ||
@@ -433,2 +473,3 @@ const event = getEvent(new Request(`https://blah.com/${resourceKey}`)) | ||
t.is(res1.headers.get('cache-control'), null) | ||
t.is(res2.status, 200) | ||
t.is(res2.headers.get('cf-cache-status'), 'HIT') | ||
@@ -440,7 +481,9 @@ } else { | ||
test('getAssetFromKV if range request submitted and resource in cache, request fulfilled', async t => { | ||
test('getAssetFromKV if range request submitted and resource in cache, request fulfilled', async (t) => { | ||
const resourceKey = 'cache.html' | ||
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) | ||
const event2 = getEvent(new Request(`https://blah.com/${resourceKey}`, { headers: { 'range': 'bytes=0-10'}})) | ||
const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) | ||
const event2 = getEvent( | ||
new Request(`https://blah.com/${resourceKey}`, { headers: { range: 'bytes=0-10' } }), | ||
) | ||
const res1 = getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) | ||
await res1 | ||
@@ -456,2 +499,2 @@ await sleep(2) | ||
test.todo('getAssetFromKV when body not empty, should invoke .cancel()') | ||
test.todo('getAssetFromKV when body not empty, should invoke .cancel()') |
@@ -5,3 +5,3 @@ import test from 'ava' | ||
test('mapRequestToAsset() correctly changes /about -> /about/index.html', async t => { | ||
test('mapRequestToAsset() correctly changes /about -> /about/index.html', async (t) => { | ||
mockGlobal() | ||
@@ -14,3 +14,3 @@ let path = '/about' | ||
test('mapRequestToAsset() correctly changes /about/ -> /about/index.html', async t => { | ||
test('mapRequestToAsset() correctly changes /about/ -> /about/index.html', async (t) => { | ||
let path = '/about/' | ||
@@ -22,3 +22,3 @@ let request = new Request(`https://foo.com${path}`) | ||
test('mapRequestToAsset() correctly changes /about.me/ -> /about.me/index.html', async t => { | ||
test('mapRequestToAsset() correctly changes /about.me/ -> /about.me/index.html', async (t) => { | ||
let path = '/about.me/' | ||
@@ -29,1 +29,9 @@ let request = new Request(`https://foo.com${path}`) | ||
}) | ||
test('mapRequestToAsset() correctly changes /about -> /about/default.html', async (t) => { | ||
mockGlobal() | ||
let path = '/about' | ||
let request = new Request(`https://foo.com${path}`) | ||
let newRequest = mapRequestToAsset(request, { defaultDocument: 'default.html' }) | ||
t.is(newRequest.url, request.url + '/default.html') | ||
}) |
@@ -14,3 +14,3 @@ import test from 'ava' | ||
test('serveSinglePageApp returns root asset path when request path ends in .html', async t => { | ||
test('serveSinglePageApp returns root asset path when request path ends in .html', async (t) => { | ||
let path = '/foo/thing.html' | ||
@@ -25,3 +25,3 @@ let request = testRequest(path) | ||
test('serveSinglePageApp returns root asset path when request path does not have extension', async t => { | ||
test('serveSinglePageApp returns root asset path when request path does not have extension', async (t) => { | ||
let path = '/foo/thing' | ||
@@ -36,3 +36,3 @@ let request = testRequest(path) | ||
test('serveSinglePageApp returns requested asset when request path has non-html extension', async t => { | ||
test('serveSinglePageApp returns requested asset when request path has non-html extension', async (t) => { | ||
let path = '/foo/thing.js' | ||
@@ -39,0 +39,0 @@ let request = testRequest(path) |
@@ -10,4 +10,5 @@ export type CacheControl = { | ||
ASSET_MANIFEST: Object | string | ||
mapRequestToAsset: (req: Request) => Request, | ||
mapRequestToAsset?: (req: Request, options?: Partial<Options>) => Request | ||
defaultMimeType: string | ||
defaultDocument: string | ||
} | ||
@@ -14,0 +15,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
1
246
0
124349
8
23
1962
- Removed@cloudflare/workers-types@^2.0.0
- Removed@types/mime@^2.0.2
- Removed@cloudflare/workers-types@2.2.2(transitive)
- Removed@types/mime@2.0.3(transitive)
Updatedmime@^2.5.2