import { env, waitUntil } from "cloudflare:workers";
import { contentTypeForAsset, parseVideoPath, videoR2Key } from "~/lib/video";
const IMMUTABLE = "public, max-age=31536000, immutable";
const RANGE = /^bytes=(\d*)-(\d*)$/;
function text(request: Request, status: number, message: string, extra: Record<string, string> = {}): Response {
return new Response(request.method === "HEAD" ? null : message, {
status,
headers: { "content-type": "text/plain", "cache-control": "no-store", ...extra },
});
}
function parseRange(header: string | null): R2Range | undefined | null {
if (!header) return undefined;
const match = RANGE.exec(header.trim());
if (!match || (!match[1] && !match[2])) return null;
if (!match[1]) return { suffix: Number(match[2]) };
const offset = Number(match[1]);
if (!match[2]) return { offset };
const end = Number(match[2]);
return end < offset ? null : { offset, length: end - offset + 1 };
}
function assetHeaders(asset: string, object: R2Object): Headers {
return new Headers({
"content-type": contentTypeForAsset(asset),
"cache-control": IMMUTABLE,
etag: object.httpEtag,
"accept-ranges": "bytes",
"x-content-type-options": "nosniff",
});
}
/**
* Prepared HLS assets live in R2 under `videos/<id>/<version>/`. Each version is immutable, so the colo cache
* holds full objects forever; Range and If-None-Match are answered by the cache on a hit. A ranged miss reads
* just that range from R2 and is not cached, the next full fetch fills the cache.
*/
export async function handleVideoRoutes(request: Request, url: URL): Promise<Response | null> {