Files
shd.build/src/routes/builds/[slug]/preview.png/+server.ts
T
2026-08-02 12:00:30 -04:00

181 lines
5.7 KiB
TypeScript

import { createHash } from 'node:crypto';
import { error } from '@sveltejs/kit';
import {
renderBuildSocialCardPng,
type BuildSocialCardFact,
type BuildSocialCardInput,
} from '$lib/server/build-social-card';
import { publicOrigin } from '$lib/server/public-origin';
import { loadSharedBuildProjection } from '$lib/server/shared-build';
import type { RequestHandler } from './$types';
const number = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });
const MAX_CACHE_BYTES = 16 * 1024 * 1024;
const MAX_ACTIVE_REQUESTS = 4;
const MAX_QUEUED_REQUESTS = 32;
type CachedImage = { body: Buffer; etag: string };
const imageCache = new Map<string, CachedImage>();
const pendingImages = new Map<string, Promise<CachedImage>>();
const requestQueue: Array<(release: () => void) => void> = [];
let cacheBytes = 0;
let activeRequests = 0;
function unitValue(value: number, unit: string | null | undefined) {
const rendered = number.format(value);
return unit === 'Shots' ? `${rendered} ${unit}` : `${rendered}${unit ?? ''}`;
}
export function _socialCardInput(
projection: NonNullable<
Awaited<ReturnType<typeof loadSharedBuildProjection>>
>,
directUrl: string,
): BuildSocialCardInput {
const attribute = (id: string) =>
projection.attributes.find((item) => String(item.id) === id);
const statByName = (name: string) =>
projection.summary.stats.find(
(stat) => String(attribute(stat.id)?.name) === name,
);
const statValue = (name: string) => {
const stat = statByName(name);
return stat ? unitValue(Number(stat.value), attribute(stat.id)?.unit) : '0';
};
const weaponName = (index: number) => {
const selected = projection.loadout.weapons[index];
return (
projection.weapons.find((item) => String(item.id) === selected?.itemId)
?.name ?? 'Unassigned'
);
};
const coreNames = new Set(['Firepower', 'Toughness', 'Engineering']);
const supportingAttributes: BuildSocialCardFact[] = projection.summary.stats
.filter((stat) => !coreNames.has(String(attribute(stat.id)?.name)))
.slice(0, 3)
.map((stat) => ({
label: String(attribute(stat.id)?.name ?? 'Attribute'),
value: unitValue(Number(stat.value), attribute(stat.id)?.unit),
}));
return {
title: projection.build.title,
author: projection.build.author,
primaryWeapon: String(weaponName(0)),
secondaryWeapon: String(weaponName(1)),
firepower: statValue('Firepower'),
toughness: statValue('Toughness'),
engineering: statValue('Engineering'),
supportingAttributes,
visibility: projection.build.visibility as 'public' | 'unlisted',
directUrl,
};
}
function matchesEtag(request: Request, etag: string) {
return request.headers
.get('if-none-match')
?.split(',')
.some((value) => value.trim() === etag);
}
function releaseRequest() {
const next = requestQueue.shift();
if (next) {
next(releaseRequest);
return;
}
activeRequests -= 1;
}
function acquireRequest(): Promise<(() => void) | null> {
if (activeRequests < MAX_ACTIVE_REQUESTS) {
activeRequests += 1;
return Promise.resolve(releaseRequest);
}
if (requestQueue.length >= MAX_QUEUED_REQUESTS) return Promise.resolve(null);
return new Promise((resolve) => requestQueue.push(resolve));
}
function storeImage(key: string, image: CachedImage) {
imageCache.set(key, image);
cacheBytes += image.body.byteLength;
while (cacheBytes > MAX_CACHE_BYTES && imageCache.size > 1) {
const oldestKey = imageCache.keys().next().value;
if (!oldestKey) break;
const oldest = imageCache.get(oldestKey);
imageCache.delete(oldestKey);
cacheBytes -= oldest?.body.byteLength ?? 0;
}
}
async function loadImage(input: BuildSocialCardInput, inputKey: string) {
const cached = imageCache.get(inputKey);
if (cached) {
imageCache.delete(inputKey);
imageCache.set(inputKey, cached);
return cached;
}
const pending = pendingImages.get(inputKey);
if (pending) return pending;
const request = renderBuildSocialCardPng(input).then((body) => {
const image = {
body,
etag: `"${createHash('sha256').update(body).digest('base64url')}"`,
};
storeImage(inputKey, image);
return image;
});
pendingImages.set(inputKey, request);
try {
return await request;
} finally {
pendingImages.delete(inputKey);
}
}
export const GET: RequestHandler = async ({ params, request, url }) => {
const release = await acquireRequest();
if (!release) error(503, 'Build preview capacity exceeded');
try {
const projection = await loadSharedBuildProjection(params.slug, {
kind: 'public-preview',
});
if (!projection) error(404, 'Build preview not found');
const origin = publicOrigin(url.origin);
const directUrl = new URL(
`/builds/${encodeURIComponent(projection.build.slug)}`,
origin,
).toString();
const input = _socialCardInput(projection, directUrl);
const inputKey = createHash('sha256')
.update(JSON.stringify({ version: 1, input }))
.digest('base64url');
const image = await loadImage(input, inputKey);
const cacheControl = 'public, max-age=0, must-revalidate';
if (matchesEtag(request, image.etag)) {
return new Response(null, {
status: 304,
headers: { 'cache-control': cacheControl, etag: image.etag },
});
}
return new Response(new Uint8Array(image.body), {
headers: {
'cache-control': cacheControl,
'content-length': String(image.body.byteLength),
'content-type': 'image/png',
'cross-origin-resource-policy': 'same-origin',
etag: image.etag,
'x-content-type-options': 'nosniff',
...(projection.build.visibility === 'unlisted'
? { 'x-robots-tag': 'noindex' }
: {}),
},
});
} finally {
release();
}
};