Replace the ad-hoc `worker.js` with a self-contained `linearvoid-media` worker under `infrastructure/cloudflare/` that owns `media.linearvoid.tv`. It serves HLS objects from the R2 bucket, enforcing immutable Cache-Control on segments, a short TTL on live manifests, plus HLS CORS, OPTIONS preflight, Range support, and a `/health` probe. Includes wrapper config with production domain, Vitest unit tests, Miniflare R2 integration tests, automated deploy workflow, and docs updates.
78 lines
2.8 KiB
JavaScript
78 lines
2.8 KiB
JavaScript
import { Miniflare } from "miniflare";
|
|
import { beforeAll, afterAll, describe, it, expect } from "vitest";
|
|
|
|
const IMMUTABLE = "public, max-age=31536000, immutable";
|
|
const MANIFEST = "public, max-age=1, s-maxage=2, must-revalidate";
|
|
|
|
// Real-workerd integration test: boots the worker with an in-memory R2 bucket and
|
|
// dispatches requests through the fetch handler.
|
|
let mf;
|
|
|
|
beforeAll(async () => {
|
|
mf = new Miniflare({
|
|
modules: true,
|
|
scriptPath: "src/worker.js",
|
|
r2Buckets: { MEDIA_BUCKET: "linearvoid-media" },
|
|
});
|
|
const bucket = await mf.getR2Bucket("MEDIA_BUCKET");
|
|
await bucket.put("hls/playlist.m3u8", "#EXTM3U\n#EXT-X-ENDLIST\n", {
|
|
httpMetadata: { contentType: "application/vnd.apple.mpegurl" },
|
|
});
|
|
await bucket.put("hls/seg-000001.m4s", new Uint8Array([0, 1, 2, 3, 4]));
|
|
await bucket.put("hls/init.mp4", new Uint8Array([9, 9, 9]));
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await mf?.dispose();
|
|
});
|
|
|
|
const base = "http://media.linearvoid.tv";
|
|
|
|
describe("linearvoid-media worker (R2)", () => {
|
|
it("serves a media segment with immutable cache headers", async () => {
|
|
const res = await mf.dispatchFetch(`${base}/hls/seg-000001.m4s`);
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get("cache-control")).toBe(IMMUTABLE);
|
|
expect(res.headers.get("content-type")).toBe("video/mp4");
|
|
expect(res.headers.get("access-control-allow-origin")).toBe("*");
|
|
expect(await res.arrayBuffer()).toEqual(new Uint8Array([0, 1, 2, 3, 4]).buffer);
|
|
});
|
|
|
|
it("serves a live manifest with a short TTL", async () => {
|
|
const res = await mf.dispatchFetch(`${base}/hls/playlist.m3u8`);
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get("cache-control")).toBe(MANIFEST);
|
|
expect(res.headers.get("content-type")).toBe("application/vnd.apple.mpegurl");
|
|
});
|
|
|
|
it("honours Range requests with 206 + Content-Range", async () => {
|
|
const res = await mf.dispatchFetch(`${base}/hls/seg-000001.m4s`, {
|
|
headers: { Range: "bytes=0-2" },
|
|
});
|
|
expect(res.status).toBe(206);
|
|
expect(res.headers.get("content-range")).toBe("bytes 0-2/5");
|
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
expect([...bytes]).toEqual([0, 1, 2]);
|
|
});
|
|
|
|
it("returns 404 for a missing object", async () => {
|
|
const res = await mf.dispatchFetch(`${base}/hls/does-not-exist.m4s`);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("answers OPTIONS preflight with CORS headers", async () => {
|
|
const res = await mf.dispatchFetch(`${base}/hls/seg-000001.m4s`, {
|
|
method: "OPTIONS",
|
|
});
|
|
expect(res.status).toBe(204);
|
|
expect(res.headers.get("access-control-allow-origin")).toBe("*");
|
|
});
|
|
|
|
it("rejects non-GET/HEAD methods", async () => {
|
|
const res = await mf.dispatchFetch(`${base}/hls/seg-000001.m4s`, {
|
|
method: "POST",
|
|
});
|
|
expect(res.status).toBe(405);
|
|
});
|
|
});
|