astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
92 lines (91 loc) • 4.02 kB
JavaScript
import CachePolicy from "http-cache-semantics";
import { fetchWithRedirects } from "../utils/redirectValidation.js";
export async function loadRemoteImage(src, fetchFn = globalThis.fetch, imageConfig = {
remotePatterns: [],
domains: []
}) {
const res = await fetchWithRedirects({
url: src,
fetchFn,
imageConfig
});
if (!res.ok) {
throw new Error(`Failed to load remote image ${src}. The request did not return a 200 OK response. (received ${res.status}))`);
}
// calculate an expiration date based on the response's TTL
const req = new Request(src);
const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res));
const expires = policy.storable() ? policy.timeToLive() : 0;
return {
data: Buffer.from(await res.arrayBuffer()),
expires: Date.now() + expires,
etag: res.headers.get("Etag") ?? undefined,
lastModified: res.headers.get("Last-Modified") ?? undefined
};
}
/**
* Revalidate a cached remote asset using its entity-tag or modified date.
* Uses the [If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) and [If-Modified-Since](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since)
* headers to check with the remote server if the cached version of a remote asset is still up to date.
* The remote server may respond that the cached asset is still up-to-date if the entity-tag or modification time matches (304 Not Modified), or respond with an updated asset (200 OK)
* @param src - url to remote asset
* @param revalidationData - an object containing the stored Entity-Tag of the cached asset and/or the Last Modified time
* @returns An object containing the refreshed expiry time and cache headers. `data` will be a `Buffer` of the new image if the asset was modified (200 OK), or `null` if the cached version is still valid (304 Not Modified).
*/
export async function revalidateRemoteImage(src, revalidationData, fetchFn = globalThis.fetch, imageConfig = {
remotePatterns: [],
domains: []
}) {
const headers = {
...revalidationData.etag && { "If-None-Match": revalidationData.etag },
...revalidationData.lastModified && { "If-Modified-Since": revalidationData.lastModified }
};
const req = new Request(src, {
headers,
cache: "no-cache"
});
const res = await fetchWithRedirects({
url: src,
headers: new Headers(headers),
imageConfig,
fetchFn
});
// Allow 304 Not Modified: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304
if (!res.ok && res.status !== 304) {
if (res.status >= 300 && res.status < 400) {
throw new Error(`Failed to revalidate cached remote image ${src}. The request was redirected.`);
}
throw new Error(`Failed to revalidate cached remote image ${src}. The request did not return a 200 OK / 304 NOT MODIFIED response. (received ${res.status} ${res.statusText})`);
}
const data = Buffer.from(await res.arrayBuffer());
if (res.ok && !data.length) {
// Server did not include body but indicated cache was stale
return await loadRemoteImage(src, fetchFn, imageConfig);
}
// calculate an expiration date based on the response's TTL
const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res.ok ? res : new Response(null, {
status: 200,
headers: res.headers
})));
const expires = policy.storable() ? policy.timeToLive() : 0;
return {
data: res.ok ? data : null,
expires: Date.now() + expires,
// While servers should respond with the same headers as a 200 response, if they don't we should reuse the stored value
etag: res.headers.get("Etag") ?? (res.ok ? undefined : revalidationData.etag),
lastModified: res.headers.get("Last-Modified") ?? (res.ok ? undefined : revalidationData.lastModified)
};
}
function webToCachePolicyRequest({ url, method, headers }) {
return {
method,
url,
headers: Object.fromEntries(headers.entries())
};
}
function webToCachePolicyResponse({ status, headers }) {
return {
status,
headers: Object.fromEntries(headers.entries())
};
}