Add ~live outfit image URLs
Gonna have the /outfit-urls page start returning these instead, for feature parity with before I might change the strategy on this at some point, like have it get `updatedAt` and redirect instead of generating the image. But this is simpler for now (and the Vercel cache doesn't seem to be as aggressive as I want anyway), and I can change it later!
This commit is contained in:
parent
3603d6fd85
commit
2543f89255
2 changed files with 39 additions and 28 deletions
|
@ -7,20 +7,22 @@
|
||||||
* - layerUrls: A comma-separated list of URLs to render, in order from
|
* - layerUrls: A comma-separated list of URLs to render, in order from
|
||||||
* bottom to top. This is a sorta "independent" render mode,
|
* bottom to top. This is a sorta "independent" render mode,
|
||||||
* not bound to any saved outfit. The URLs must match a known
|
* not bound to any saved outfit. The URLs must match a known
|
||||||
* layer URL format.
|
* layer URL format. This mode will return a long-term cache
|
||||||
|
* header, so the client and our CDN cache can cache the
|
||||||
|
* requested URL forever. (NOTE: The Vercel cache seems pretty
|
||||||
|
* quick to eject them, though...)
|
||||||
* - id: Instead of `layerUrls`, you can instead provide an outfit ID, which
|
* - id: Instead of `layerUrls`, you can instead provide an outfit ID, which
|
||||||
* will load the outfit data and render it directly.
|
* will load the outfit data and render it directly. By default, this
|
||||||
* - updatedAt: If you provide an `id`, you must also provide `updatedAt`:
|
* will return a 10-minute cache header, to keep individual users from
|
||||||
|
* re-loading the image from scratch too often, while still keeping it
|
||||||
|
* relatively fresh. (If you provide `updatedAt` too, we cache it for
|
||||||
|
* longer!)
|
||||||
|
* - updatedAt: If you provide an `id`, you may also provide `updatedAt`:
|
||||||
* the UNIX timestamp for when the outfit was last updated. This
|
* the UNIX timestamp for when the outfit was last updated. This
|
||||||
* has no effect on output, but is very important for caching:
|
* has no effect on image output, but it enables us to return a
|
||||||
* we always return a long-term cache header, so our CDN cache
|
* long-term cache header, so the client and our CDN cache can
|
||||||
* will likely cache the requested URL forever. That way, outfit
|
* cache the requested URL forever. (NOTE: The Vercel cache
|
||||||
* images will cache long-term, unless they're updated and the
|
* seems pretty quick to eject them, though...)
|
||||||
* user requests a new URL. (This _does_ mean this API can no
|
|
||||||
* longer be used for simple embeds in e.g. petpages that
|
|
||||||
* auto-update to the latest version of the image… but I don't
|
|
||||||
* actually know if anyone does that? If we need a
|
|
||||||
* latest-version API, we can build that as a separate case.)
|
|
||||||
*/
|
*/
|
||||||
const beeline = require("honeycomb-beeline")({
|
const beeline = require("honeycomb-beeline")({
|
||||||
writeKey: process.env["HONEYCOMB_WRITE_KEY"],
|
writeKey: process.env["HONEYCOMB_WRITE_KEY"],
|
||||||
|
@ -54,16 +56,16 @@ async function handle(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let layerUrls;
|
let layerUrls;
|
||||||
|
let isSafeToCacheLongTerm;
|
||||||
if (req.query.layerUrls) {
|
if (req.query.layerUrls) {
|
||||||
layerUrls = req.query.layerUrls.split(",");
|
layerUrls = req.query.layerUrls.split(",");
|
||||||
} else if (req.query.id) {
|
|
||||||
if (!req.query.updatedAt) {
|
|
||||||
return reject(
|
|
||||||
res,
|
|
||||||
`updatedAt parameter is required, when id parameter is provided`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// When layerUrls are provided, it's always safe to cache long-term. We
|
||||||
|
// assume layer assets are immutable, and that TNT generally creates new
|
||||||
|
// IDs when they're not. (Or, if TNT's conversion strategy or our rendering
|
||||||
|
// strategy dramatically changes, we might add a cache-buster to the URL.)
|
||||||
|
isSafeToCacheLongTerm = true;
|
||||||
|
} else if (req.query.id) {
|
||||||
const outfitId = req.query.id;
|
const outfitId = req.query.id;
|
||||||
try {
|
try {
|
||||||
layerUrls = await loadLayerUrlsForSavedOutfit(outfitId, size);
|
layerUrls = await loadLayerUrlsForSavedOutfit(outfitId, size);
|
||||||
|
@ -75,6 +77,10 @@ async function handle(req, res) {
|
||||||
500
|
500
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When an outfit ID is provided, it's only safe to cache long-term if
|
||||||
|
// `updatedAt` is also provided.
|
||||||
|
isSafeToCacheLongTerm = Boolean(req.query.updatedAt);
|
||||||
} else {
|
} else {
|
||||||
return reject(res, `Missing required parameter: layerUrls`);
|
return reject(res, `Missing required parameter: layerUrls`);
|
||||||
}
|
}
|
||||||
|
@ -95,18 +101,19 @@ async function handle(req, res) {
|
||||||
|
|
||||||
const { image, status } = imageResult;
|
const { image, status } = imageResult;
|
||||||
|
|
||||||
if (status === "success") {
|
if (status === "success" && isSafeToCacheLongTerm) {
|
||||||
// On success, we use very aggressive caching, on the assumption that
|
// This image is safe to cache long-term, so send a long-term cache header!
|
||||||
// layers are ~immutable too, and that our rendering algorithm will almost
|
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
||||||
// never change in a way that requires pushing changes. If it does, we
|
res.status(200);
|
||||||
// should add a cache-buster to the URL!
|
} else if (status === "success") {
|
||||||
//
|
// This image rendered successfully, but isn't safe to cache long-term. We
|
||||||
// TODO: Maybe verify that there's a timestamp param in the ?id case?
|
// cache for a short period of time, instead, to avoid thrashing too hard
|
||||||
res.setHeader("Cache-Control", "public, max-age=604800, immutable");
|
// on individual users, while still keeping it relatively fresh.
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=600, immutable");
|
||||||
res.status(200);
|
res.status(200);
|
||||||
} else {
|
} else {
|
||||||
// On partial failure, we still send the image, but with a 500 status. We
|
// On partial failure, we still send the image, but with a 500 status. We
|
||||||
// send a long-lived cache header, but in such a way that the user can
|
// send a one-week cache header, but in such a way that the user can
|
||||||
// refresh the page to try again. (`private` means the CDN won't cache it,
|
// refresh the page to try again. (`private` means the CDN won't cache it,
|
||||||
// and we don't send `immutable`, which would save it even across reloads.)
|
// and we don't send `immutable`, which would save it even across reloads.)
|
||||||
// The 500 won't really affect the client, which will still show the image
|
// The 500 won't really affect the client, which will still show the image
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
{
|
{
|
||||||
"routes": [
|
"routes": [
|
||||||
|
{
|
||||||
|
"src": "/outfits/(?<id>[^/]+)/(?<size>150|300|600).png",
|
||||||
|
"dest": "/api/outfitImage.js?size=$size&id=$id&updatedAt=$updatedAt"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"src": "/outfits/(?<id>[^/]+)/v/(?<updatedAt>[^/]+)/(?<size>150|300|600).png",
|
"src": "/outfits/(?<id>[^/]+)/v/(?<updatedAt>[^/]+)/(?<size>150|300|600).png",
|
||||||
"dest": "/api/outfitImage.js?size=$size&id=$id&updatedAt=$updatedAt"
|
"dest": "/api/outfitImage.js?size=$size&id=$id&updatedAt=$updatedAt"
|
||||||
|
|
Loading…
Reference in a new issue