mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
Peça de vídeo publicada e com arquivo passa a mostrar um frame estático leve no card público antes do play, sem nenhum request do arquivo antes do gesto (`preload="none"` do S27 intacto). O frame é um derivado privado em `contentMedia` (nome determinístico), gerado por um dono só — `ensureContentPieceFrame` — chamado pelo job C211 (que já tem o arquivo em disco: a extração sai de graça) e pela rota `GET /conteudos/<slug>/frame` em miss, que cura o acervo anterior sem backfill, fila, collection nova ou migration. A porta reusa o gate de slug publicado e o proxy de mídia privada do `/midia`: o kill switch fecha as duas de uma vez. Sem frame possível, o slot mostra o estado neutro honesto com play e o marcador — nunca imagem quebrada, nunca retry. A projeção declara o frame por elegibilidade (vídeo com arquivo), não pela existência do derivado, então a cura é invisível para o cache da listagem e não há tag para bustar. O frame também aparece na página da peça e no card da home (variante compacta de 124px, cena 05). A passagem de design (tier primário) achou dois bugs que só o browser mostra: o glyph de imagem quebrada numa requisição que falha, e — ao esconder a imagem até decodificar — o frame sumindo para sempre no caminho mais rápido (uma imagem que carrega antes da hidratação não dispara `load` nem `error` de novo; o `ref` decide os dois desfechos). Também: o guard de falha de e2e aceita `RegExp` e confere os paths declarados antes do caso do favicon (todo 404 do browser traz a mesma mensagem), o manifest de e2e mapeia os módulos novos, e o job C211 passa o arquivo que já tem em disco em vez de rebaixar o vídeo. Design tier: central-conteudos-preview-frame-video Closes #1352
98 lines
3.9 KiB
TypeScript
98 lines
3.9 KiB
TypeScript
import config from '@payload-config'
|
|
import { after, NextResponse } from 'next/server'
|
|
import { getPayload } from 'payload'
|
|
|
|
import { CONTENT_MEDIA_SLUG } from '@/lib/contentPiece'
|
|
import { CONTENT_PIECE_FRAME_WAIT_MS } from '@/lib/contentPieceFrame'
|
|
import { PRIVATE_MEDIA_CACHE_CONTROL } from '@/lib/privateMedia'
|
|
import {
|
|
ensureContentPieceFrame,
|
|
findContentPieceFrameMedia,
|
|
} from '@/utilities/content/contentPieceFrameJob'
|
|
import { getPublishedContentPieceBySlug } from '@/utilities/content/contentPieceReads'
|
|
import {
|
|
buildPrivateMediaResponse,
|
|
resolvePrivateMediaStaticDir,
|
|
} from '@/utilities/privateMedia/privateMediaResponse'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
/**
|
|
* C226 — the still of a published video piece. Sibling of `/midia` with the very
|
|
* same gate: the cached public read (published + archived file), so a draft, an
|
|
* unpublished piece, an unknown slug, a link piece and a non-video piece all
|
|
* answer the same silent 404. The frame therefore stops being visible exactly
|
|
* when the video does — the kill switch survives by construction, because the
|
|
* derived file lives in the PRIVATE `contentMedia`, never in the public `media`.
|
|
*
|
|
* On a miss the still is generated and awaited for a short budget; the work
|
|
* keeps running after the response, so the archive published before this slice
|
|
* heals one piece per view without any backfill. Nothing here records a
|
|
* circulation event: seeing a frame is not opening or downloading the piece.
|
|
*/
|
|
|
|
const notFound = (): NextResponse =>
|
|
new NextResponse(null, {
|
|
status: 404,
|
|
headers: { 'Cache-Control': PRIVATE_MEDIA_CACHE_CONTROL },
|
|
})
|
|
|
|
/**
|
|
* Resolves the promise, or null when the budget runs out first. A deliberate
|
|
* twin of the C182 poster route's helper: the acervo route is behind the
|
|
* campaign gate and must not be edited by this slice, and 15 lines of promise
|
|
* racing is not worth a shared module for two call sites. Revisit if a third
|
|
* door ever needs the same race.
|
|
*/
|
|
const settledWithin = async <Value>(
|
|
promise: Promise<Value>,
|
|
budgetMs: number,
|
|
): Promise<Value | null> => {
|
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
try {
|
|
return await Promise.race([
|
|
promise,
|
|
new Promise<null>((resolve) => {
|
|
timer = setTimeout(() => resolve(null), budgetMs)
|
|
}),
|
|
])
|
|
} finally {
|
|
if (timer) clearTimeout(timer)
|
|
}
|
|
}
|
|
|
|
export const GET = async (
|
|
_request: Request,
|
|
{ params }: { params: Promise<{ slug: string }> },
|
|
): Promise<Response> => {
|
|
const { slug } = await params
|
|
const piece = await getPublishedContentPieceBySlug(slug)
|
|
// `framePath` is the eligibility decision (a video with an archived file),
|
|
// so a photo, an audio or a link piece never reaches the generation.
|
|
if (!piece?.framePath) return notFound()
|
|
|
|
const payload = await getPayload({ config })
|
|
const generation = ensureContentPieceFrame(payload, piece.id)
|
|
// The response below may answer the neutral slot before the still is ready;
|
|
// `after` keeps the process alive until the generation settles (never a 524).
|
|
after(async () => {
|
|
await generation
|
|
})
|
|
|
|
const generated = await settledWithin(generation, CONTENT_PIECE_FRAME_WAIT_MS)
|
|
// The probe after the wait closes the race with a generation that committed
|
|
// its row while this request was waiting (the `Promise.race` resolves the
|
|
// moment the budget is out, not the moment the row lands).
|
|
const media = generated ?? (await findContentPieceFrameMedia(payload, piece.id).catch(() => null))
|
|
if (!media?.filename) return notFound()
|
|
|
|
// Intentional admin bypass: the public gate is the cached published read
|
|
// above; the media row only supplies the stored object to stream.
|
|
const response = await buildPrivateMediaResponse({
|
|
media,
|
|
staticDir: resolvePrivateMediaStaticDir(payload, CONTENT_MEDIA_SLUG),
|
|
rangeHeader: null,
|
|
download: false,
|
|
})
|
|
return response.status === 200 ? response : notFound()
|
|
}
|