@griddo/cx-render-mock 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — sin publicar
4
+
5
+ Primera versión del paquete, extraída de la suite E2E de Griddo, donde este código lleva tiempo en uso.
6
+
7
+ - `CxRenderMock`: fachada con motor y publicador, y el flujo completo (`publishAndRender`).
8
+ - `CxRenderEngine`: `build/start` → clasificar por hash → `build/end`, con reintento por rondas,
9
+ descubrimiento de sites pendientes y liberación del bloqueo de render.
10
+ - `CmsPublisher`: marcar páginas como pendientes y publicar sites.
11
+ - Núcleo de decisiones puras, reutilizable si prefieres orquestar las requests tú.
12
+ - `Transport` inyectable con decoradores `withRetry` y `withTimeout`.
13
+ - `RenderError` con código, status, body y causa.
14
+ - Logger opcional de cuatro métodos.
15
+ - Esperas, topes y paginación configurables.
16
+ - Settle adaptativo: antes de `build/start` se sondea el listado hasta que dos firmas consecutivas coinciden
17
+ (`pageListSignature` + `decideSettleStep`), con `settleMs` como techo en lugar de como espera fija.
18
+
19
+ Notas de estructura de esta primera versión: las piezas que hablan con la API lo hacen a través de un cliente
20
+ interno (`api-client`), que es el único sitio donde se decide qué respuesta cuenta como fallo. El índice exporta
21
+ símbolo por símbolo, así que lo interno no se publica por accidente. Y dos métodos llevan el nombre de lo que
22
+ hacen hoy: `renderSiteOnce` (una sola pasada, ya no busca quiescencia) y `confirmPagesByTitle` (confirma
23
+ cualquier estado final, no solo `live`).
24
+
25
+ Pendiente antes de publicar: registry y scope, dueño del contrato de `endBuild` en griddo-api, y confirmar
26
+ `qa-render-segregation` en el entorno de uso. Ver el README.
package/README.md ADDED
@@ -0,0 +1,253 @@
1
+ # @griddo/cx-render-mock
2
+
3
+ Test-double del render de **griddo-cx**: resuelve los estados transitorios de las páginas
4
+ (`2` pending-publishing / `4` pending-unpublishing) a sus estados finales (`3` live / `1` offline) llamando al
5
+ protocolo `build/start` → `build/end` de griddo-api, **sin** construir Gatsby ni subir a S3.
6
+
7
+ **Cero dependencias de runtime**: el HTTP se inyecta, así que sirve tanto en un script de Node como dentro de un
8
+ runner de tests.
9
+
10
+ ## Publicación
11
+
12
+ Se publica en npm como `@griddo/cx-render-mock` con la action **`publish-cx-render-mock.yml`**, igual que
13
+ `api-types`:
14
+
15
+ - **Por tag**: `git tag cx-render-mock-v0.1.0 && git push --tags`. Sufijos `-alpha`/`-beta`/`-rc` publican con
16
+ su dist-tag de prerelease.
17
+ - **A mano**: pestaña Actions → "Release CX Render Mock" → Run workflow, indicando la versión.
18
+
19
+ El tarball lleva solo `dist/`, `README.md`, `CHANGELOG.md` y `package.json` (whitelist `files`; comprobable con
20
+ `npm pack --dry-run`). La suite E2E (griddo-qa) mantiene una copia espejo hasta que consuma el paquete
21
+ publicado. Racional completo de la extracción: `griddo-qa/docs/cx-render-packaging-feasibility.md`.
22
+
23
+ ## Contenido
24
+
25
+ | Módulo | Qué es |
26
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------- |
27
+ | `src/facade.ts` | `CxRenderMock`: motor + publicador con una sola configuración, y el flujo completo. **Empieza por aquí.** |
28
+ | `src/engine.ts` | `CxRenderEngine`: el protocolo de render, si solo necesitas esa parte. |
29
+ | `src/publish.ts` | `CmsPublisher`: el disparo previo (marcar páginas, publicar sites). **No es render**, es lo que hace el CMS. |
30
+ | `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. |
31
+ | `src/transport.ts` | El contrato del HTTP inyectable y los decoradores `withRetry` / `withTimeout`. |
32
+ | `src/errors.ts` | `RenderError` con `code`, para distinguir qué falló sin leer el mensaje. |
33
+ | `src/logger.ts` | Contrato de log opcional (cuatro métodos, cero dependencias). |
34
+
35
+ Por qué esa división: el núcleo no tiene estado ni dependencias, así que son funciones puras testeables sin
36
+ mocks; el motor sí tiene estado y una dependencia (el HTTP) que hay que invertir, así que es una clase con
37
+ inyección por constructor.
38
+
39
+ ## Qué capa te toca usar
40
+
41
+ | Dónde lo consumes | Usa |
42
+ | ------------------------------------------------------------ | ------------- |
43
+ | Script, CLI o proceso de Node; test runner sobre Promises | **el motor** |
44
+ | Runner con cola de comandos propia (Cypress y similares) | **el núcleo** |
45
+
46
+ ### Desde un script de Node: el motor
47
+
48
+ El transporte es la única pieza que escribes tú. Dos reglas: devuelve `{ status, body }` y **no lanza** en
49
+ 4xx/5xx, porque el motor necesita ver el status para decidir.
50
+
51
+ ```ts
52
+ import { CxRenderMock, type RequestOptions, type Transport } from "@griddo/cx-render-mock";
53
+
54
+ const request = async (method: string, url: string, opts: RequestOptions = {}) => {
55
+ const res = await fetch(url, {
56
+ method,
57
+ headers: opts.headers,
58
+ body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
59
+ });
60
+ const text = await res.text();
61
+ let body: unknown;
62
+ try {
63
+ body = text ? JSON.parse(text) : null;
64
+ } catch {
65
+ body = text; // un 502 con HTML, por ejemplo: el status ya informa
66
+ }
67
+ return { status: res.status, body };
68
+ };
69
+
70
+ const transport: Transport = {
71
+ get: (url, opts) => request("GET", url, opts),
72
+ post: (url, opts) => request("POST", url, opts),
73
+ put: (url, opts) => request("PUT", url, opts),
74
+ };
75
+
76
+ const cx = new CxRenderMock({ transport, apiUrl, authToken });
77
+
78
+ // Publica, renderiza, confirma, y suelta el bloqueo de render si algo se rompe a medias.
79
+ await cx.publishAndRender([siteId], [pageId], ["Mi página"]);
80
+ ```
81
+
82
+ Hay una versión ejecutable de esto en [`examples/render-a-page.mjs`](examples/render-a-page.mjs).
83
+
84
+ Si necesitas las piezas por separado, `cx.engine` y `cx.publisher` están ahí, o instáncialas tú
85
+ (`new CxRenderEngine({...})`, `new CmsPublisher({...})`).
86
+
87
+ Si no sabes qué sites tienen trabajo pendiente, pregúntaselo al backend en lugar de llevar la cuenta:
88
+
89
+ ```ts
90
+ await engine.renderPendingSites(); // GET sites/all → renderiza los que tengan cambios sin renderizar
91
+ await engine.renderPendingSites([siteId]); // acotado a los tuyos, si compartes base de datos con otros procesos
92
+ ```
93
+
94
+ ### Ajustes
95
+
96
+ Todas las esperas y topes son tuyos; lo que no indiques toma el default (`DEFAULT_TIMINGS`):
97
+
98
+ ```ts
99
+ new CxRenderEngine({
100
+ transport,
101
+ apiUrl,
102
+ authToken,
103
+ timings: { settleMs: 5_000, maxRounds: 5 }, // el resto sigue por defecto
104
+ pageList: { itemsPerPage: 1_000 }, // sube el listado si tus sites tienen muchas páginas
105
+ sleep: () => Promise.resolve(), // en tests, para no esperar de verdad
106
+ });
107
+ ```
108
+
109
+ El núcleo acepta lo mismo por argumento: `decideBuildStartStep(status, waits, timings)` y
110
+ `listPageUrl(apiUrl, siteId, pageList)`.
111
+
112
+ ### Reintentos y timeouts
113
+
114
+ El transporte se puede envolver, así que esas políticas no viven en el motor:
115
+
116
+ ```ts
117
+ import { withRetry, withTimeout } from "@griddo/cx-render-mock";
118
+
119
+ const transport = withRetry(withTimeout(myTransport, { ms: 30_000 }), { attempts: 4, logger });
120
+ ```
121
+
122
+ `withRetry` solo reintenta **lecturas** por defecto, y solo ante 5xx o un fallo del cliente. No es timidez:
123
+ `build/start` toma un bloqueo del site y `build/end` lo libera, así que repetir a ciegas una escritura puede
124
+ dejar el protocolo a medias. Amplíalo con `methods` si tu caso lo tolera.
125
+
126
+ `withTimeout` deja de esperar, pero **no cancela** la request: el paquete no conoce tu cliente. Para cancelación
127
+ real, usa un `AbortController` dentro de tu transporte.
128
+
129
+ ### Trazas
130
+
131
+ Pásale un `logger` y te cuenta qué hace (qué sites tienen trabajo, cuántas páginas publica, cuándo reintenta y
132
+ por qué). Sin logger no escribe nada.
133
+
134
+ ```ts
135
+ import { createConsoleLogger } from "@griddo/cx-render-mock";
136
+
137
+ const cx = new CxRenderMock({ transport, apiUrl, authToken, logger: createConsoleLogger("debug") });
138
+ ```
139
+
140
+ Son cuatro métodos (`debug`, `info`, `warn`, `error`), así que encaja con `console`, con pino o con el log de tu
141
+ runner de tests envolviéndolo.
142
+
143
+ ### Errores
144
+
145
+ Todo lo que lanza el paquete es un `RenderError` con un `code`, para no tener que leer el mensaje:
146
+
147
+ ```ts
148
+ import { RENDER_ERROR_CODE, RenderError } from "@griddo/cx-render-mock";
149
+
150
+ try {
151
+ await cx.publishAndRender([siteId], [pageId], titles);
152
+ } catch (error) {
153
+ if (error instanceof RenderError && error.code === RENDER_ERROR_CODE.TITLES_NOT_FINAL) {
154
+ console.warn("no llegaron a su estado final:", error.missingTitles);
155
+ }
156
+ }
157
+ ```
158
+
159
+ Códigos: `BUILD_START_NOT_READY`, `BUILD_END_FAILED`, `TITLES_NOT_FINAL`, `SITES_LOOKUP_FAILED`,
160
+ `PAGE_LIST_FAILED`, `MARK_PENDING_FAILED`, `PUBLISH_FAILED`. Los errores traen además `status`, `body` y la
161
+ `cause` original cuando aplica.
162
+
163
+ ⚠️ Solo se envuelven las **respuestas** con error. Si tu transporte revienta antes de haber respuesta (red caída,
164
+ DNS, puerto cerrado), ese error se propaga tal cual, sin convertirse en `RenderError`: es tu cliente el que
165
+ falló, no el protocolo. Maneja los dos casos.
166
+
167
+ ### Desde Cypress (o cualquier runner con cola): el núcleo
168
+
169
+ Orquesta tú las requests con el cliente del runner y delega solo las decisiones:
170
+
171
+ ```ts
172
+ import { buildEndBody, classifyPage, decideBuildStartStep, listPageUrl } from "@griddo/cx-render-mock";
173
+
174
+ cy.request({ url: `${apiUrl}site/${siteId}/build/start`, failOnStatusCode: false }).then((bs) => {
175
+ const step = decideBuildStartStep(bs.status, waits); // proceed | retry | fail
176
+ // ...para cada id de bs.body.publishIds, pide GET page/{id} y decide con classifyPage(hash, status)
177
+ cy.request({
178
+ method: "POST",
179
+ url: `${apiUrl}site/${siteId}/build/end`,
180
+ body: buildEndBody(publishHashes, bs.body.siteHash, bs.body.unpublishHashes, publishPagesIds),
181
+ });
182
+ });
183
+ ```
184
+
185
+ Se puede meter el motor dentro de Cypress envolviéndolo en `cy.then(() => engine.renderUntilLive(...))`, pero
186
+ **no conviene**, por dos motivos:
187
+
188
+ 1. Sus Promises no viven en la cola de comandos: pierdes el command log paso a paso y el reporte de fallo del
189
+ runner queda menos preciso.
190
+ 2. Si el runner envuelve su propio cliente HTTP para inyectar cabeceras (Cypress lo hace sobreescribiendo
191
+ `cy.request`), un transporte sobre `fetch` se las salta y ese tráfico deja de llevarlas.
192
+
193
+ Ambos caminos están probados: el motor con el paquete instalado desde el tarball en un consumidor de Node (ESM y
194
+ CJS), y el núcleo con un spec real de Cypress consumiendo `@griddo/cx-render-mock` desde `node_modules`.
195
+
196
+ ## Dos avisos que importan
197
+
198
+ 1. **Lock `rendering`**: `build/start` bloquea el site y `build/end` lo libera. Si el proceso muere entre ambos,
199
+ el site queda bloqueado ~4h. Llama a `releaseRenderLock(siteId)` en tu `finally`.
200
+ 2. **No es seguro con varios procesos a la vez**: `build/end` despublica copias globales de forma
201
+ incondicional, sin acotar al site que lo llama. Dos procesos contra la misma base de datos se pisan; en
202
+ griddo-api eso lo acota la feature `qa-render-segregation`, así que confirma que esté activa en tu entorno.
203
+
204
+ ## Limitaciones heredadas (conocidas, no arregladas)
205
+
206
+ - Si la API responde con un body que no es un array ni `{ items }` (por ejemplo, el HTML de un 5xx sin status de
207
+ error), el recorrido puede romper con `TypeError` en lugar de un error con contexto.
208
+ - El motor recorre las páginas de un site de una en una. Con muchas páginas es lento, pero es deliberado: ir en
209
+ paralelo satura la API, y ese backend ya se ha caído por carga.
210
+
211
+ ## Referencia rápida
212
+
213
+ | Exporta | Qué es |
214
+ | -------------------------------------------------------------------- | --------------------------------------------------------------------------- |
215
+ | `CxRenderMock` | Fachada: `engine`, `publisher` y `publishAndRender(siteIds, pageIds, titles, expectedStatus?)` |
216
+ | `CxRenderEngine` | `renderUntilLive`, `renderPendingSites`, `renderSiteOnce`, `confirmPagesByTitle`, `releaseRenderLock` |
217
+ | `CmsPublisher` | `markPagesPending`, `publishSites`, `publishPages` |
218
+ | `Transport`, `TransportResponse`, `RequestOptions` | El contrato del HTTP que inyectas |
219
+ | `withRetry`, `withTimeout` | Decoradores del transporte |
220
+ | `RenderError`, `RENDER_ERROR_CODE` | El error del paquete y sus códigos |
221
+ | `Logger`, `NOOP_LOGGER`, `createConsoleLogger` | El contrato de log opcional |
222
+ | `RenderTimings`, `DEFAULT_TIMINGS`, `PageListQuery`, `DEFAULT_PAGE_LIST_QUERY` | Esperas, topes y paginación |
223
+ | `LIVE_STATUS`, `PageInfo`, `SiteBuildInfo`, `BuildEndBody` | Tipos del contrato de la API |
224
+ | `classifyPage`, `diffMissingByTitle`, `decideBuildStartStep`, `selectPendingSites`, `collectRepublishIds`, `buildEndBody`, `listPageUrl` | Las decisiones puras, si orquestas tú |
225
+
226
+ ## Comandos
227
+
228
+ ```bash
229
+ yarn install
230
+ yarn build # dist/ (ESM + CJS + tipos) con tsup
231
+ yarn typecheck # tsc sin emitir
232
+ yarn test # tests unitarios (vitest)
233
+ yarn test:coverage # los mismos, con informe de cobertura
234
+ yarn lint # biome (formato + reglas)
235
+ yarn verify # lint + typecheck + test, lo que corre antes de publicar
236
+ ```
237
+
238
+ Cobertura actual: **98,75% de líneas y 90% de ramas** (70 tests). Lo que queda sin cubrir son guardas defensivas
239
+ para respuestas malformadas de la API.
240
+
241
+ ## Historial
242
+
243
+ Cambios por versión en [CHANGELOG.md](CHANGELOG.md).
244
+
245
+ ## Antes de publicar
246
+
247
+ 1. Decidir **registry y scope** (npm privado / GitHub Packages) y quitar `"private": true`.
248
+ 2. Fijar **quién mantiene el contrato de `endBuild`** en griddo-api: si su semántica cambia, este paquete se
249
+ rompe sin ninguna señal.
250
+ 3. Confirmar que **`qa-render-segregation`** está integrada en el entorno donde vaya a usarse.
251
+ 4. Probar el paquete empaquetado de verdad (`yarn pack` e instalarlo en un consumidor) antes del primer publish.
252
+ 5. Cuando esté publicado: cambiar el import de `griddo-qa/cypress/support/cx-render.ts` por el nombre del
253
+ paquete y borrar `griddo-qa/packages/cx-render`.