@griddo/cx-render-mock
v1.0.0
Published
Test-double del render de griddo-cx: núcleo de decisiones puro + motor de protocolo con transporte HTTP inyectable. Cero dependencias de runtime.
Readme
@griddo/cx-render-mock
Test-double del render de griddo-cx: resuelve los estados transitorios de las páginas
(2 pending-publishing / 4 pending-unpublishing) a sus estados finales (3 live / 1 offline) llamando al
protocolo build/start → build/end de griddo-api, sin construir Gatsby ni subir a S3.
Cero dependencias de runtime: el HTTP se inyecta, así que sirve tanto en un script de Node como dentro de un runner de tests.
Publicación
Se publica en npm como @griddo/cx-render-mock con la action publish-cx-render-mock.yml, igual que
api-types:
- Por tag:
git tag cx-render-mock-v0.1.0 && git push --tags. Sufijos-alpha/-beta/-rcpublican con su dist-tag de prerelease. - A mano: pestaña Actions → "Release CX Render Mock" → Run workflow, indicando la versión.
El tarball lleva solo dist/, README.md, CHANGELOG.md y package.json (whitelist files; comprobable con
npm pack --dry-run). La suite E2E (griddo-qa) mantiene una copia espejo hasta que consuma el paquete
publicado. Racional completo de la extracción: griddo-qa/docs/cx-render-packaging-feasibility.md.
Contenido
| Módulo | Qué es |
| ------------------ | ------------------------------------------------------------------------------------------------------------------- |
| src/facade.ts | CxRenderMock: motor + publicador con una sola configuración, y el flujo completo. Empieza por aquí. |
| src/engine.ts | CxRenderEngine: el protocolo de render, si solo necesitas esa parte. |
| src/publish.ts | CmsPublisher: el disparo previo (marcar páginas, publicar sites). No es render, es lo que hace el CMS. |
| src/core.ts | Funciones puras: clasificar por hash, diff por título, gate de build/start, body de build/end. Sin estado ni I/O. |
| src/transport.ts | El contrato del HTTP inyectable y los decoradores withRetry / withTimeout. |
| src/errors.ts | RenderError con code, para distinguir qué falló sin leer el mensaje. |
| src/logger.ts | Contrato de log opcional (cuatro métodos, cero dependencias). |
Por qué esa división: el núcleo no tiene estado ni dependencias, así que son funciones puras testeables sin mocks; el motor sí tiene estado y una dependencia (el HTTP) que hay que invertir, así que es una clase con inyección por constructor.
Qué capa te toca usar
| Dónde lo consumes | Usa | | ------------------------------------------------------------ | ------------- | | Script, CLI o proceso de Node; test runner sobre Promises | el motor | | Runner con cola de comandos propia (Cypress y similares) | el núcleo |
Desde un script de Node: el motor
El transporte es la única pieza que escribes tú. Dos reglas: devuelve { status, body } y no lanza en
4xx/5xx, porque el motor necesita ver el status para decidir.
import { CxRenderMock, type RequestOptions, type Transport } from "@griddo/cx-render-mock";
const request = async (method: string, url: string, opts: RequestOptions = {}) => {
const res = await fetch(url, {
method,
headers: opts.headers,
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
});
const text = await res.text();
let body: unknown;
try {
body = text ? JSON.parse(text) : null;
} catch {
body = text; // un 502 con HTML, por ejemplo: el status ya informa
}
return { status: res.status, body };
};
const transport: Transport = {
get: (url, opts) => request("GET", url, opts),
post: (url, opts) => request("POST", url, opts),
put: (url, opts) => request("PUT", url, opts),
};
const cx = new CxRenderMock({ transport, apiUrl, authToken });
// Publica, renderiza, confirma, y suelta el bloqueo de render si algo se rompe a medias.
await cx.publishAndRender([siteId], [pageId], ["Mi página"]);Hay una versión ejecutable de esto en examples/render-a-page.mjs.
Si necesitas las piezas por separado, cx.engine y cx.publisher están ahí, o instáncialas tú
(new CxRenderEngine({...}), new CmsPublisher({...})).
Si no sabes qué sites tienen trabajo pendiente, pregúntaselo al backend en lugar de llevar la cuenta:
await engine.renderPendingSites(); // GET sites/all → renderiza los que tengan cambios sin renderizar
await engine.renderPendingSites([siteId]); // acotado a los tuyos, si compartes base de datos con otros procesosAjustes
Todas las esperas y topes son tuyos; lo que no indiques toma el default (DEFAULT_TIMINGS):
new CxRenderEngine({
transport,
apiUrl,
authToken,
timings: { settleMs: 5_000, maxRounds: 5 }, // el resto sigue por defecto
pageList: { itemsPerPage: 1_000 }, // sube el listado si tus sites tienen muchas páginas
sleep: () => Promise.resolve(), // en tests, para no esperar de verdad
});El núcleo acepta lo mismo por argumento: decideBuildStartStep(status, waits, timings) y
listPageUrl(apiUrl, siteId, pageList).
Reintentos y timeouts
El transporte se puede envolver, así que esas políticas no viven en el motor:
import { withRetry, withTimeout } from "@griddo/cx-render-mock";
const transport = withRetry(withTimeout(myTransport, { ms: 30_000 }), { attempts: 4, logger });withRetry solo reintenta lecturas por defecto, y solo ante 5xx o un fallo del cliente. No es timidez:
build/start toma un bloqueo del site y build/end lo libera, así que repetir a ciegas una escritura puede
dejar el protocolo a medias. Amplíalo con methods si tu caso lo tolera.
withTimeout deja de esperar, pero no cancela la request: el paquete no conoce tu cliente. Para cancelación
real, usa un AbortController dentro de tu transporte.
Trazas
Pásale un logger y te cuenta qué hace (qué sites tienen trabajo, cuántas páginas publica, cuándo reintenta y
por qué). Sin logger no escribe nada.
import { createConsoleLogger } from "@griddo/cx-render-mock";
const cx = new CxRenderMock({ transport, apiUrl, authToken, logger: createConsoleLogger("debug") });Son cuatro métodos (debug, info, warn, error), así que encaja con console, con pino o con el log de tu
runner de tests envolviéndolo.
Errores
Todo lo que lanza el paquete es un RenderError con un code, para no tener que leer el mensaje:
import { RENDER_ERROR_CODE, RenderError } from "@griddo/cx-render-mock";
try {
await cx.publishAndRender([siteId], [pageId], titles);
} catch (error) {
if (error instanceof RenderError && error.code === RENDER_ERROR_CODE.TITLES_NOT_FINAL) {
console.warn("no llegaron a su estado final:", error.missingTitles);
}
}Códigos: BUILD_START_NOT_READY, BUILD_END_FAILED, TITLES_NOT_FINAL, SITES_LOOKUP_FAILED,
PAGE_LIST_FAILED, MARK_PENDING_FAILED, PUBLISH_FAILED. Los errores traen además status, body y la
cause original cuando aplica.
⚠️ Solo se envuelven las respuestas con error. Si tu transporte revienta antes de haber respuesta (red caída,
DNS, puerto cerrado), ese error se propaga tal cual, sin convertirse en RenderError: es tu cliente el que
falló, no el protocolo. Maneja los dos casos.
Desde Cypress (o cualquier runner con cola): el núcleo
Orquesta tú las requests con el cliente del runner y delega solo las decisiones:
import { buildEndBody, classifyPage, decideBuildStartStep, listPageUrl } from "@griddo/cx-render-mock";
cy.request({ url: `${apiUrl}site/${siteId}/build/start`, failOnStatusCode: false }).then((bs) => {
const step = decideBuildStartStep(bs.status, waits); // proceed | retry | fail
// ...para cada id de bs.body.publishIds, pide GET page/{id} y decide con classifyPage(hash, status)
cy.request({
method: "POST",
url: `${apiUrl}site/${siteId}/build/end`,
body: buildEndBody(publishHashes, bs.body.siteHash, bs.body.unpublishHashes, publishPagesIds),
});
});Se puede meter el motor dentro de Cypress envolviéndolo en cy.then(() => engine.renderUntilLive(...)), pero
no conviene, por dos motivos:
- Sus Promises no viven en la cola de comandos: pierdes el command log paso a paso y el reporte de fallo del runner queda menos preciso.
- Si el runner envuelve su propio cliente HTTP para inyectar cabeceras (Cypress lo hace sobreescribiendo
cy.request), un transporte sobrefetchse las salta y ese tráfico deja de llevarlas.
Ambos caminos están probados: el motor con el paquete instalado desde el tarball en un consumidor de Node (ESM y
CJS), y el núcleo con un spec real de Cypress consumiendo @griddo/cx-render-mock desde node_modules.
Dos avisos que importan
- Lock
rendering:build/startbloquea el site ybuild/endlo libera. Si el proceso muere entre ambos, el site queda bloqueado ~4h. Llama areleaseRenderLock(siteId)en tufinally. - No es seguro con varios procesos a la vez:
build/enddespublica copias globales de forma incondicional, sin acotar al site que lo llama. Dos procesos contra la misma base de datos se pisan; en griddo-api eso lo acota la featureqa-render-segregation, así que confirma que esté activa en tu entorno.
Limitaciones heredadas (conocidas, no arregladas)
- Si la API responde con un body que no es un array ni
{ items }(por ejemplo, el HTML de un 5xx sin status de error), el recorrido puede romper conTypeErroren lugar de un error con contexto. - El motor recorre las páginas de un site de una en una. Con muchas páginas es lento, pero es deliberado: ir en paralelo satura la API, y ese backend ya se ha caído por carga.
Referencia rápida
| Exporta | Qué es |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| CxRenderMock | Fachada: engine, publisher y publishAndRender(siteIds, pageIds, titles, expectedStatus?) |
| CxRenderEngine | renderUntilLive, renderPendingSites, renderSiteOnce, confirmPagesByTitle, releaseRenderLock |
| CmsPublisher | markPagesPending, publishSites, publishPages |
| Transport, TransportResponse, RequestOptions | El contrato del HTTP que inyectas |
| withRetry, withTimeout | Decoradores del transporte |
| RenderError, RENDER_ERROR_CODE | El error del paquete y sus códigos |
| Logger, NOOP_LOGGER, createConsoleLogger | El contrato de log opcional |
| RenderTimings, DEFAULT_TIMINGS, PageListQuery, DEFAULT_PAGE_LIST_QUERY | Esperas, topes y paginación |
| LIVE_STATUS, PageInfo, SiteBuildInfo, BuildEndBody | Tipos del contrato de la API |
| classifyPage, diffMissingByTitle, decideBuildStartStep, selectPendingSites, collectRepublishIds, buildEndBody, listPageUrl | Las decisiones puras, si orquestas tú |
Comandos
yarn install
yarn build # dist/ (ESM + CJS + tipos) con tsup
yarn typecheck # tsc sin emitir
yarn test # tests unitarios (vitest)
yarn test:coverage # los mismos, con informe de cobertura
yarn lint # biome (formato + reglas)
yarn verify # lint + typecheck + test, lo que corre antes de publicarCobertura actual: 98,75% de líneas y 90% de ramas (70 tests). Lo que queda sin cubrir son guardas defensivas para respuestas malformadas de la API.
Historial
Cambios por versión en CHANGELOG.md.
Antes de publicar
- Decidir registry y scope (npm privado / GitHub Packages) y quitar
"private": true. - Fijar quién mantiene el contrato de
endBuilden griddo-api: si su semántica cambia, este paquete se rompe sin ninguna señal. - Confirmar que
qa-render-segregationestá integrada en el entorno donde vaya a usarse. - Probar el paquete empaquetado de verdad (
yarn packe instalarlo en un consumidor) antes del primer publish. - Cuando esté publicado: cambiar el import de
griddo-qa/cypress/support/cx-render.tspor el nombre del paquete y borrargriddo-qa/packages/cx-render.
