@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 +26 -0
- package/README.md +253 -0
- package/dist/index.d.mts +550 -0
- package/dist/index.d.ts +550 -0
- package/dist/index.js +591 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +544 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Núcleo PURO del test-double de render de CX — agnóstico de Cypress en VALOR y en TIPO.
|
|
3
|
+
*
|
|
4
|
+
* Contiene las decisiones difíciles del protocolo (clasificación por hash, diff por título, gate de
|
|
5
|
+
* build/start, shaping del body de build/end, colección de re-pending de la mitigación cross-job, y los knobs
|
|
6
|
+
* por defecto del render de un solo pase) SIN acoplarse a la cola de comandos de Cypress: cero `cy`/`Cypress`/`expect`/`window`,
|
|
7
|
+
* cero `import ... 'cypress'`, y ninguna firma devuelve `Cypress.Chainable` (solo tipos neutrales). Es el
|
|
8
|
+
* artefacto compartible: el shell Cypress de griddo-qa (`cypress/support/cx-render.ts`) y el motor de Promises
|
|
9
|
+
* del paquete (`./engine`) consumen ESTE módulo, de modo que el racional que causa los bugs vive en un único
|
|
10
|
+
* sitio (functional-core / imperative-shell). Ver `docs/cx-render-packaging-feasibility.md` §5/§6.
|
|
11
|
+
*
|
|
12
|
+
* Behavior-preserving: este código se EXTRAJO de `cx-render.ts` sin cambiar su comportamiento observable;
|
|
13
|
+
* las constantes, umbrales y predicados son idénticos a los del shell original.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Modelo de live_status: data-driven en la BD pero ESTABLE por orden de seed (contrato code-grounded e2e-202 →
|
|
17
|
+
* griddo-api CONFIG/LiveStatusSeeder). El double no puede leer los flags de CONFIG en runtime → usa estos ids
|
|
18
|
+
* verificados.
|
|
19
|
+
*/
|
|
20
|
+
declare const LIVE_STATUS: {
|
|
21
|
+
readonly UNPUBLISHED: 1;
|
|
22
|
+
readonly PENDING_PUBLISHING: 2;
|
|
23
|
+
readonly LIVE: 3;
|
|
24
|
+
readonly PENDING_UNPUBLISHING: 4;
|
|
25
|
+
};
|
|
26
|
+
/** Los cuatro ids de live_status del contrato (1/2/3/4). */
|
|
27
|
+
type LiveStatus = (typeof LIVE_STATUS)[keyof typeof LIVE_STATUS];
|
|
28
|
+
/**
|
|
29
|
+
* Forma mínima de una página tal como la exponen los endpoints que consume el double: `GET page/{id}` (trae
|
|
30
|
+
* `hash` + `liveStatus`) y `site/{id}/pages?format=list` (trae `id` + `title` + `liveStatus`). Todos los
|
|
31
|
+
* campos son opcionales porque el body real de la API puede venir incompleto y el core no debe asumir.
|
|
32
|
+
*/
|
|
33
|
+
interface PageInfo {
|
|
34
|
+
id?: number;
|
|
35
|
+
title?: string;
|
|
36
|
+
hash?: string | null;
|
|
37
|
+
liveStatus?: {
|
|
38
|
+
id?: number;
|
|
39
|
+
} | null;
|
|
40
|
+
}
|
|
41
|
+
/** Cuerpo de `POST site/{id}/build/end` (lo que el CX real envía para resolver los transitorios). */
|
|
42
|
+
interface BuildEndBody {
|
|
43
|
+
publishHashes: string[];
|
|
44
|
+
siteHash: string | null;
|
|
45
|
+
unpublishHashes: string[];
|
|
46
|
+
publishPagesIds: number[];
|
|
47
|
+
}
|
|
48
|
+
/** Esperas y topes del render. Todos sustituibles; los defaults están en `DEFAULT_TIMINGS`. */
|
|
49
|
+
interface RenderTimings {
|
|
50
|
+
/**
|
|
51
|
+
* TECHO (ms) del settle ADAPTATIVO previo al ÚNICO build/start del render de un solo pase. El settle espera
|
|
52
|
+
* al spread ASÍNCRONO de las copias GLOBAL, pero ya NO es un sleep fijo: se sondea el listado del site hasta
|
|
53
|
+
* que dos sondas consecutivas devuelven la misma firma (`pageListSignature` → `decideSettleStep`), con este
|
|
54
|
+
* valor como tope. Racional: una constante fija no cubre una latencia variable — a 15s el pase único perdía
|
|
55
|
+
* rezagados bajo carga de CI (renderFlow2/002 agotaba los 3 intentos) y a 30-45s se paga tiempo muerto por
|
|
56
|
+
* site y por ronda en el 90% de renders que asientan en segundos. `<= 0` desactiva el settle por completo.
|
|
57
|
+
*/
|
|
58
|
+
settleMs: number;
|
|
59
|
+
/** Intervalo (ms) entre sondas del settle adaptativo. */
|
|
60
|
+
settlePollMs: number;
|
|
61
|
+
/**
|
|
62
|
+
* Rondas máximas del orquestador auto-sanador `renderUntilLive`. NO baja a 1: con 1 quedaban sets ENTEROS
|
|
63
|
+
* not-Live en CI (el build/start del mock no captura TODAS las páginas en un pase; las rondas
|
|
64
|
+
* re-pend+re-render sí).
|
|
65
|
+
*/
|
|
66
|
+
maxRounds: number;
|
|
67
|
+
/** Wait entre rondas del orquestador auto-sanador (ms). */
|
|
68
|
+
roundWaitMs: number;
|
|
69
|
+
/** Reintentos máximos de build/start mientras dé 4xx transitorio (~30s con el retry por defecto). */
|
|
70
|
+
buildStartMaxWaits: number;
|
|
71
|
+
/** Backoff entre reintentos de build/start en 4xx transitorio (ms). */
|
|
72
|
+
buildStartRetryWaitMs: number;
|
|
73
|
+
}
|
|
74
|
+
/** Defaults medidos de `RenderTimings`. El shell los sobreescribe por `Cypress.env`. */
|
|
75
|
+
declare const DEFAULT_TIMINGS: RenderTimings;
|
|
76
|
+
/** Paginación con la que se pide el listado de páginas de un site. Defaults en `DEFAULT_PAGE_LIST_QUERY`. */
|
|
77
|
+
interface PageListQuery {
|
|
78
|
+
page: number;
|
|
79
|
+
itemsPerPage: number;
|
|
80
|
+
}
|
|
81
|
+
/** Defaults de `PageListQuery`. */
|
|
82
|
+
declare const DEFAULT_PAGE_LIST_QUERY: PageListQuery;
|
|
83
|
+
/**
|
|
84
|
+
* URL del listado del site (`site/{id}/pages?format=list`) que leen el diff por título y el re-pending.
|
|
85
|
+
* Centraliza la query string y la paginación — pura (string → string), sin acoplarse a Cypress (recibe
|
|
86
|
+
* `apiUrl` como argumento). ⚠️ Limitación HEREDADA: pide UNA sola página (300 ítems por defecto); un site con
|
|
87
|
+
* más páginas daría confirmación por título errónea. Se mitiga subiendo `itemsPerPage`, pero no pagina.
|
|
88
|
+
*/
|
|
89
|
+
declare const listPageUrl: (apiUrl: string, siteId: number, query?: PageListQuery) => string;
|
|
90
|
+
/**
|
|
91
|
+
* Firma ESTABLE del estado render-relevante de un listado de páginas: `id:liveStatus:hash` por página, ordenado
|
|
92
|
+
* por id (el orden del listado puede variar entre sondas sin que el estado haya cambiado). Dos firmas iguales en
|
|
93
|
+
* sondas consecutivas = el backend ha dejado de mover estados/hashes (spread y recálculos asentados) → el
|
|
94
|
+
* build/start ya puede capturar el trabajo completo en un pase. Pura (datos → string), para el settle adaptativo.
|
|
95
|
+
*/
|
|
96
|
+
declare const pageListSignature: (items: PageInfo[]) => string;
|
|
97
|
+
/** Decisión del settle adaptativo (unión discriminada: `wait` porta su `waitMs`). */
|
|
98
|
+
type SettleDecision = {
|
|
99
|
+
action: "go";
|
|
100
|
+
} | {
|
|
101
|
+
action: "wait";
|
|
102
|
+
waitMs: number;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Gate del settle ADAPTATIVO: decide si el backend ya está quiescente y el render puede arrancar.
|
|
106
|
+
* - dos sondas consecutivas con la MISMA firma → `'go'` (quiescencia: nada se ha movido en un intervalo);
|
|
107
|
+
* - `elapsedMs >= timings.settleMs` → `'go'` (techo: bajo carga patológica no se espera para siempre);
|
|
108
|
+
* - resto → `'wait'` con `min(settlePollMs, settleMs - elapsedMs)` (el último wait no desborda el techo).
|
|
109
|
+
* La PRIMERA sonda (`prevSignature = null`) nunca decide `'go'` por firma: hacen falta dos muestras para saber
|
|
110
|
+
* que nada cambia. Con `settleMs <= 0` decide `'go'` inmediato (settle desactivado; el caller puede además
|
|
111
|
+
* ahorrarse la sonda).
|
|
112
|
+
*/
|
|
113
|
+
declare const decideSettleStep: (prevSignature: string | null, signature: string, elapsedMs: number, timings?: RenderTimings) => SettleDecision;
|
|
114
|
+
/** Clasificación de una página según su `hash`/`status`, resultado de `classifyPage`. */
|
|
115
|
+
type PageClassification = "publish" | "needsHash" | "skip";
|
|
116
|
+
/**
|
|
117
|
+
* Clasifica una página por su HASH, como el CX real (griddo-cx store.ts:366-369): el TRABAJO = toda página
|
|
118
|
+
* con `hash != null`, SIN filtrar por estado. `build/end` publica el structured_data_content (has_been_published,
|
|
119
|
+
* que la API pública EXIGE vía includePendingPublishing:off) SOLO de los publishPagesIds/publishHashes que
|
|
120
|
+
* recibe; una copia GLOBAL/traducción puede alcanzar live(3) con su CONTENIDO aún sin publicar → si la
|
|
121
|
+
* SALTÁRAMOS por estar live(3) (como hacía el filtro `status===2` anterior), su contenido NUNCA se publicaría
|
|
122
|
+
* y la pública la excluiría (bug de traducciones en CI). Por eso:
|
|
123
|
+
* - `hash != null` → `'publish'` (se publica su contenido aunque ya esté live(3); el render es un solo pase);
|
|
124
|
+
* - `hash == null` y `status === PENDING_PUBLISHING` → `'needsHash'` (el spread aún no asignó el hash);
|
|
125
|
+
* - resto (p.ej. live(3) con hash=null = ya publicada y con el hash nulado por endBuild) → `'skip'`.
|
|
126
|
+
*/
|
|
127
|
+
declare const classifyPage: (hash: string | null | undefined, status: number | undefined) => PageClassification;
|
|
128
|
+
/**
|
|
129
|
+
* Diff SOFT por TÍTULO: dada la lista de páginas de los sites, devuelve los `wanted` que AÚN no están en
|
|
130
|
+
* `expectedStatus` (ausentes del listado o en otro estado). Se compara POR TÍTULO y no por id porque publicar/
|
|
131
|
+
* re-renderizar una GLOBAL canónica REASIGNA el id (memoria publish-canonical-global-page-reassigns-id) → un id
|
|
132
|
+
* de createSuite (o de un build/start previo) queda stale; el listado del site es la entidad renderizada real.
|
|
133
|
+
*
|
|
134
|
+
* Preferencia `statusByTitle`: prefiere `expectedStatus` si alguna entidad con ese título ya lo tiene (una
|
|
135
|
+
* global puede tener EDITOR + copia con distinto estado). El resultado (títulos que faltan) depende solo de si
|
|
136
|
+
* ALGUNA entidad con ese título alcanzó `expectedStatus`.
|
|
137
|
+
*/
|
|
138
|
+
declare const diffMissingByTitle: (items: PageInfo[], wanted: string[], expectedStatus: number) => string[];
|
|
139
|
+
/** Shaping del cuerpo de `POST site/{id}/build/end` (normaliza `siteHash` ausente a `null`). */
|
|
140
|
+
declare const buildEndBody: (publishHashes: string[], siteHash: string | null | undefined, unpublishHashes: string[], publishPagesIds: number[]) => BuildEndBody;
|
|
141
|
+
/**
|
|
142
|
+
* Colección de re-pending de la MITIGACIÓN cross-job: de las páginas del listado, devuelve los ids in-site
|
|
143
|
+
* (deduplicados, en orden de aparición) de los títulos `missing` que NO están live. El barrido global
|
|
144
|
+
* INCONDICIONAL de endBuild (buildGlobalUnpublishAllWhere) puede despublicar globales QA de OTRO job → una
|
|
145
|
+
* página offline ya NO la recoge build/start, así que re-renderizar no la recupera; re-pendearla (upload-pending)
|
|
146
|
+
* la devuelve a pending(2) para que renderSiteToQuiescence la renderice. Acotado a copias in-site de `wanted`
|
|
147
|
+
* (el caller solo pasa los títulos deseados-live que faltan) y solo `st !== LIVE`.
|
|
148
|
+
*/
|
|
149
|
+
declare const collectRepublishIds: (items: PageInfo[], missing: string[]) => number[];
|
|
150
|
+
/**
|
|
151
|
+
* Decisión del gate build/start-ready (unión discriminada: `retry` porta su `waitMs`, `proceed`/`fail` no).
|
|
152
|
+
*/
|
|
153
|
+
type BuildStartDecision = {
|
|
154
|
+
action: "proceed";
|
|
155
|
+
} | {
|
|
156
|
+
action: "retry";
|
|
157
|
+
waitMs: number;
|
|
158
|
+
} | {
|
|
159
|
+
action: "fail";
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Gate build/start-ready: replica el timing del CX real (que dispara ~1 min después, cuando el pathing ya
|
|
163
|
+
* asentó, y nunca ve el 4xx transitorio) sin su latencia. El publish lanza el spread de las copias GLOBAL de
|
|
164
|
+
* forma ASÍNCRONA; hasta que termina, una copia puede existir con `pathId=null` y build/start (que corre
|
|
165
|
+
* fixAllPages → `pathId.split(...)`) devuelve 4xx TRANSITORIO. Por eso reintentamos SOLO mientras dé 4xx,
|
|
166
|
+
* cortando al 1er 200; si agota el presupuesto, el shell asserta RUIDOSO (no un CypressError crudo).
|
|
167
|
+
* - `status < 400` → `'proceed'`;
|
|
168
|
+
* - agotado el presupuesto (`startWaits >= timings.buildStartMaxWaits`) → `'fail'`;
|
|
169
|
+
* - resto → `'retry'` con backoff `timings.buildStartRetryWaitMs`.
|
|
170
|
+
*/
|
|
171
|
+
declare const decideBuildStartStep: (status: number, startWaits: number, timings?: RenderTimings) => BuildStartDecision;
|
|
172
|
+
/** Forma mínima de un site en `GET sites/all`: lo justo para saber si tiene render pendiente. */
|
|
173
|
+
interface SiteBuildInfo {
|
|
174
|
+
id?: number;
|
|
175
|
+
/** La API lo marca cuando el site tiene cambios sin renderizar (señal QA-scoped: hash pendiente, no bloqueado). */
|
|
176
|
+
shouldBeUpdated?: boolean;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Ids de los sites con render PENDIENTE — lo que el CX real busca en cada pasada de su poll.
|
|
180
|
+
*
|
|
181
|
+
* `onlyIds` acota el resultado (el shell le pasa los sites del test en curso). Esa acotación NO es cosmética:
|
|
182
|
+
* el CX real es un único proceso global, pero el mock corre por test y a escala CI ~40 jobs comparten una BD;
|
|
183
|
+
* sin acotar, cada pasada arrastraría el trabajo de los demás (build/start → fixAllPages es pesado) y la BD se
|
|
184
|
+
* satura, tumbando incluso specs que no usan el mock.
|
|
185
|
+
*/
|
|
186
|
+
declare const selectPendingSites: (sites: SiteBuildInfo[], onlyIds?: number[]) => number[];
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Contrato de log mínimo, para que el paquete cuente qué está haciendo sin imponer una librería.
|
|
190
|
+
*
|
|
191
|
+
* Cuatro métodos y ninguna dependencia: encaja tal cual con `console`, y con pino, winston o el log del runner de
|
|
192
|
+
* tests envolviéndolo en cuatro líneas. Si no pasas ninguno, el paquete no escribe nada.
|
|
193
|
+
*/
|
|
194
|
+
interface Logger {
|
|
195
|
+
debug(message: string, meta?: unknown): void;
|
|
196
|
+
info(message: string, meta?: unknown): void;
|
|
197
|
+
warn(message: string, meta?: unknown): void;
|
|
198
|
+
error(message: string, meta?: unknown): void;
|
|
199
|
+
}
|
|
200
|
+
/** Logger que descarta todo. Es el que se usa cuando no inyectas uno. */
|
|
201
|
+
declare const NOOP_LOGGER: Logger;
|
|
202
|
+
/**
|
|
203
|
+
* Logger sobre `console`, por comodidad en scripts y ejemplos.
|
|
204
|
+
*
|
|
205
|
+
* @param level nivel mínimo que se emite. Por defecto `info`, así que los `debug` se descartan.
|
|
206
|
+
*/
|
|
207
|
+
declare const createConsoleLogger: (level?: "debug" | "info" | "warn" | "error") => Logger;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Contrato del transporte HTTP y decoradores opcionales para envolverlo.
|
|
211
|
+
*
|
|
212
|
+
* El transporte es la única dependencia de runtime del paquete, y se inyecta: así no arrastra ningún cliente
|
|
213
|
+
* HTTP y funciona igual en Node, en un bundler o dentro de un runner de tests. Los decoradores son funciones que
|
|
214
|
+
* reciben un transporte y devuelven otro con el mismo contrato, de modo que reintentos, timeouts o trazas se
|
|
215
|
+
* añaden componiendo, sin que el motor sepa nada.
|
|
216
|
+
*/
|
|
217
|
+
|
|
218
|
+
/** Respuesta HTTP normalizada: status y body ya parseado. */
|
|
219
|
+
interface TransportResponse {
|
|
220
|
+
status: number;
|
|
221
|
+
body: any;
|
|
222
|
+
}
|
|
223
|
+
/** Opciones de una request: cabeceras y body ya serializable. */
|
|
224
|
+
interface RequestOptions {
|
|
225
|
+
headers?: Record<string, string>;
|
|
226
|
+
body?: unknown;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Transporte HTTP inyectable. Impleméntalo sobre `fetch`, `axios`, `undici` o lo que uses.
|
|
230
|
+
*
|
|
231
|
+
* ⚠️ **No debe lanzar en 4xx/5xx**: devuelve `{ status, body }` y deja que el motor decida. Es deliberado: un
|
|
232
|
+
* 4xx de `build/start` es una respuesta esperada (la ruta del site aún no está lista), no un fallo. Un cliente
|
|
233
|
+
* que lance en error — como los que devuelven el body deserializado y tiran en no-2xx — necesita un adaptador
|
|
234
|
+
* que capture y traduzca a `{ status, body }`.
|
|
235
|
+
*/
|
|
236
|
+
interface Transport {
|
|
237
|
+
get(url: string, opts?: RequestOptions): Promise<TransportResponse>;
|
|
238
|
+
post(url: string, opts?: RequestOptions): Promise<TransportResponse>;
|
|
239
|
+
put(url: string, opts?: RequestOptions): Promise<TransportResponse>;
|
|
240
|
+
}
|
|
241
|
+
/** Métodos que un decorador puede tratar de forma distinta. */
|
|
242
|
+
type TransportMethod = "get" | "post" | "put";
|
|
243
|
+
/** Ajustes de {@link withRetry}. */
|
|
244
|
+
interface RetryOptions {
|
|
245
|
+
/** Intentos totales, incluido el primero. Por defecto 3. */
|
|
246
|
+
attempts?: number;
|
|
247
|
+
/** Espera entre intentos (ms). Por defecto 500, y crece linealmente con el intento. */
|
|
248
|
+
delayMs?: number;
|
|
249
|
+
/**
|
|
250
|
+
* Métodos que se reintentan. Por defecto **solo `get`**.
|
|
251
|
+
*
|
|
252
|
+
* No es timidez: `build/start` toma un bloqueo del site y `build/end` lo libera, así que repetir a ciegas una
|
|
253
|
+
* escritura puede dejar el protocolo a medias. Amplíalo solo si sabes que tu caso lo tolera.
|
|
254
|
+
*/
|
|
255
|
+
methods?: TransportMethod[];
|
|
256
|
+
/** Qué respuesta merece otro intento. Por defecto, los 5xx (un 4xx no se arregla repitiendo). */
|
|
257
|
+
shouldRetry?: (res: TransportResponse) => boolean;
|
|
258
|
+
/** Espera inyectable, para tests instantáneos. */
|
|
259
|
+
sleep?: (ms: number) => Promise<void>;
|
|
260
|
+
logger?: Logger;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Envuelve un transporte para que reintente los fallos pasajeros: una respuesta 5xx o un rechazo del cliente
|
|
264
|
+
* (red caída, DNS, socket cerrado). Agotados los intentos, deja pasar la última respuesta o relanza el error,
|
|
265
|
+
* para que quien llama decida.
|
|
266
|
+
*
|
|
267
|
+
* ```ts
|
|
268
|
+
* const transport = withRetry(myTransport, { attempts: 4 });
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
declare const withRetry: (transport: Transport, opts?: RetryOptions) => Transport;
|
|
272
|
+
/** Ajustes de {@link withTimeout}. */
|
|
273
|
+
interface TimeoutOptions {
|
|
274
|
+
/** Milisegundos antes de dar la request por perdida. */
|
|
275
|
+
ms: number;
|
|
276
|
+
logger?: Logger;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Envuelve un transporte para que ninguna request se quede colgada para siempre: pasado el plazo, rechaza.
|
|
280
|
+
*
|
|
281
|
+
* ⚠️ **No cancela la request subyacente**, solo deja de esperarla: el paquete no conoce tu cliente HTTP, así que
|
|
282
|
+
* no puede abortarlo. Si necesitas cancelación real, implementa el plazo dentro de tu propio transporte con un
|
|
283
|
+
* `AbortController`.
|
|
284
|
+
*/
|
|
285
|
+
declare const withTimeout: (transport: Transport, opts: TimeoutOptions) => Transport;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Error del paquete, con código.
|
|
289
|
+
*
|
|
290
|
+
* Un único tipo de error con un `code` en lugar de una jerarquía de clases: los consumidores necesitan
|
|
291
|
+
* distinguir *qué* falló para decidir (reintentar, avisar, abortar), y eso se hace igual de bien con un campo que
|
|
292
|
+
* con `instanceof`, sin obligar a importar siete clases.
|
|
293
|
+
*/
|
|
294
|
+
/** Códigos de {@link RenderError}. */
|
|
295
|
+
declare const RENDER_ERROR_CODE: {
|
|
296
|
+
/** `build/start` siguió respondiendo error hasta agotar los reintentos: el site no llega a estar listo. */
|
|
297
|
+
readonly BUILD_START_NOT_READY: "BUILD_START_NOT_READY";
|
|
298
|
+
/** `build/end` respondió >= 400: el render no se ha aplicado y el bloqueo puede seguir puesto. */
|
|
299
|
+
readonly BUILD_END_FAILED: "BUILD_END_FAILED";
|
|
300
|
+
/** Se agotaron las rondas y algunos títulos no alcanzaron el estado final. Mira `missingTitles`. */
|
|
301
|
+
readonly TITLES_NOT_FINAL: "TITLES_NOT_FINAL";
|
|
302
|
+
/** No se pudo saber qué sites tenían render pendiente. */
|
|
303
|
+
readonly SITES_LOOKUP_FAILED: "SITES_LOOKUP_FAILED";
|
|
304
|
+
/** No se pudo leer el listado de páginas de un site. */
|
|
305
|
+
readonly PAGE_LIST_FAILED: "PAGE_LIST_FAILED";
|
|
306
|
+
/** Falló marcar páginas como pendientes de publicar. */
|
|
307
|
+
readonly MARK_PENDING_FAILED: "MARK_PENDING_FAILED";
|
|
308
|
+
/** Falló publicar un site. */
|
|
309
|
+
readonly PUBLISH_FAILED: "PUBLISH_FAILED";
|
|
310
|
+
};
|
|
311
|
+
/** Uno de los {@link RENDER_ERROR_CODE}. */
|
|
312
|
+
type RenderErrorCode = (typeof RENDER_ERROR_CODE)[keyof typeof RENDER_ERROR_CODE];
|
|
313
|
+
/** Datos opcionales que acompañan a un {@link RenderError}. */
|
|
314
|
+
interface RenderErrorDetail {
|
|
315
|
+
/** Status HTTP de la respuesta que provocó el error, si lo hubo. */
|
|
316
|
+
status?: number;
|
|
317
|
+
/** Body de esa respuesta, tal cual, para poder diagnosticar sin volver a reproducir. */
|
|
318
|
+
body?: unknown;
|
|
319
|
+
/** Títulos que no alcanzaron su estado final. Solo en `TITLES_NOT_FINAL`. */
|
|
320
|
+
missingTitles?: string[];
|
|
321
|
+
/** Error original, si este envuelve a otro. Se expone también como `cause` nativa. */
|
|
322
|
+
cause?: unknown;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Error de este paquete. Siempre trae `code`; el resto del detalle depende de dónde falló.
|
|
326
|
+
*
|
|
327
|
+
* ```ts
|
|
328
|
+
* try {
|
|
329
|
+
* await engine.renderUntilLive([siteId], titles);
|
|
330
|
+
* } catch (error) {
|
|
331
|
+
* if (error instanceof RenderError && error.code === RENDER_ERROR_CODE.TITLES_NOT_FINAL) {
|
|
332
|
+
* console.warn("no llegaron:", error.missingTitles);
|
|
333
|
+
* }
|
|
334
|
+
* }
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
declare class RenderError extends Error {
|
|
338
|
+
/** Qué falló, de forma comprobable sin leer el mensaje. */
|
|
339
|
+
readonly code: RenderErrorCode;
|
|
340
|
+
/** Status HTTP asociado, si el error viene de una respuesta. */
|
|
341
|
+
readonly status?: number;
|
|
342
|
+
/** Body de la respuesta asociada. */
|
|
343
|
+
readonly body?: unknown;
|
|
344
|
+
/** Títulos que no alcanzaron su estado final (`TITLES_NOT_FINAL`). */
|
|
345
|
+
readonly missingTitles?: string[];
|
|
346
|
+
constructor(code: RenderErrorCode, message: string, detail?: RenderErrorDetail);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Cliente interno sobre el transporte inyectado. **No es API pública**: no se exporta desde el índice del
|
|
351
|
+
* paquete.
|
|
352
|
+
*
|
|
353
|
+
* Concentra las tres cosas que antes repetían el motor y el publicador: montar las cabeceras, componer la URL
|
|
354
|
+
* a partir de `apiUrl`, y convertir una respuesta con error en un {@link RenderError} con su código. Así "qué
|
|
355
|
+
* cuenta como fallo" se decide en un único sitio.
|
|
356
|
+
*/
|
|
357
|
+
|
|
358
|
+
/** Lo que necesita cualquier pieza que hable con la API: el transporte, dónde está y con qué credencial. */
|
|
359
|
+
interface ApiClientConfig {
|
|
360
|
+
transport: Transport;
|
|
361
|
+
apiUrl: string;
|
|
362
|
+
authToken: string;
|
|
363
|
+
/** Dónde contar lo que va pasando. Si no lo pasas, no se escribe nada. */
|
|
364
|
+
logger?: Logger;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Motor del protocolo de render: `build/start` → clasificar páginas por hash → `build/end`, con reintento por
|
|
369
|
+
* rondas hasta que los títulos pedidos alcanzan su estado final.
|
|
370
|
+
*
|
|
371
|
+
* Es una clase porque tiene estado (configuración, cliente HTTP) y dependencias que conviene invertir: el HTTP y
|
|
372
|
+
* el log. Las decisiones sin estado viven en `./core` como funciones puras; hablar con la API es cosa de
|
|
373
|
+
* `./api-client`.
|
|
374
|
+
*/
|
|
375
|
+
|
|
376
|
+
/** Dependencias del motor, inyectadas por constructor. */
|
|
377
|
+
interface EngineDeps extends ApiClientConfig {
|
|
378
|
+
/** Espera de `ms` milisegundos. Por defecto, `setTimeout`; inyecta un no-op para que los tests no esperen. */
|
|
379
|
+
sleep?: (ms: number) => Promise<void>;
|
|
380
|
+
/** Esperas y topes del render. Lo que no indiques se toma de {@link cxCore.DEFAULT_TIMINGS}. */
|
|
381
|
+
timings?: Partial<RenderTimings>;
|
|
382
|
+
/** Paginación del listado de páginas. Lo que no indiques se toma de {@link cxCore.DEFAULT_PAGE_LIST_QUERY}. */
|
|
383
|
+
pageList?: Partial<PageListQuery>;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Instánciala una vez con la configuración del entorno y reutilízala:
|
|
387
|
+
*
|
|
388
|
+
* ```ts
|
|
389
|
+
* const engine = new CxRenderEngine({ transport, apiUrl, authToken });
|
|
390
|
+
* await engine.renderUntilLive([siteId], ["Mi página"]);
|
|
391
|
+
* ```
|
|
392
|
+
*/
|
|
393
|
+
declare class CxRenderEngine {
|
|
394
|
+
private readonly api;
|
|
395
|
+
private readonly logger;
|
|
396
|
+
private readonly sleep;
|
|
397
|
+
private readonly timings;
|
|
398
|
+
private readonly pageList;
|
|
399
|
+
constructor(deps: EngineDeps);
|
|
400
|
+
/**
|
|
401
|
+
* Renderiza un site en **una sola pasada**: settle adaptativo → `build/start` → `GET page/{id}` →
|
|
402
|
+
* `build/end`.
|
|
403
|
+
*
|
|
404
|
+
* El settle previo da tiempo a que el backend termine de propagar las copias de las páginas globales; sin él,
|
|
405
|
+
* `build/start` devolvería una lista incompleta. Es ADAPTATIVO: sondea el listado hasta que dos sondas
|
|
406
|
+
* consecutivas coinciden (quiescencia) o hasta el techo `timings.settleMs` — rápido cuando el backend va
|
|
407
|
+
* suelto, paciente bajo carga. Las páginas que lleguen tarde no se reintentan aquí: de eso se encarga
|
|
408
|
+
* {@link renderUntilLive}.
|
|
409
|
+
*
|
|
410
|
+
* ⚠️ `build/start` deja el site bloqueado y `build/end` lo libera. En el camino normal `build/end` se emite
|
|
411
|
+
* siempre, pero si el transporte rechaza entre ambos el bloqueo queda colgado durante horas: llama a
|
|
412
|
+
* {@link releaseRenderLock} en tu `finally`.
|
|
413
|
+
*
|
|
414
|
+
* @throws {RenderError} `BUILD_START_NOT_READY` si el site no llega a estar listo, `BUILD_END_FAILED` si el
|
|
415
|
+
* render no se aplica.
|
|
416
|
+
*/
|
|
417
|
+
renderSiteOnce(siteId: number): Promise<void>;
|
|
418
|
+
/**
|
|
419
|
+
* Settle ADAPTATIVO previo al build/start: sondea el listado del site cada `timings.settlePollMs` y arranca
|
|
420
|
+
* cuando dos sondas consecutivas devuelven la misma firma (`cxCore.pageListSignature`) o al agotar el techo
|
|
421
|
+
* `timings.settleMs` (la decisión es de `cxCore.decideSettleStep`). Best-effort: si el listado falla durante
|
|
422
|
+
* el settle, se procede al render en vez de morir aquí (peor caso = el de siempre, un pase incompleto que
|
|
423
|
+
* recogen las rondas de `renderUntilLive`). Con `settleMs <= 0` no sondea (settle desactivado).
|
|
424
|
+
*/
|
|
425
|
+
private settleBeforeStart;
|
|
426
|
+
/**
|
|
427
|
+
* Renderiza **todos los sites que tengan render pendiente**, que es lo que hace el render real en cada pasada:
|
|
428
|
+
* pregunta al backend qué sites tienen cambios sin renderizar (`GET sites/all` → `shouldBeUpdated`) y los
|
|
429
|
+
* resuelve uno por uno.
|
|
430
|
+
*
|
|
431
|
+
* @param onlyIds acota a esos sites. **Úsalo si compartes la base de datos con otros procesos**: sin acotar,
|
|
432
|
+
* esta pasada arrastra también el trabajo pendiente de los demás, que es tan lento como caro.
|
|
433
|
+
* @returns los ids que ha renderizado.
|
|
434
|
+
* @throws {RenderError} `SITES_LOOKUP_FAILED` si no se puede consultar la lista de sites.
|
|
435
|
+
*/
|
|
436
|
+
renderPendingSites(onlyIds?: number[]): Promise<number[]>;
|
|
437
|
+
/**
|
|
438
|
+
* Lleva los títulos pedidos a su estado final, reintentando lo que falte.
|
|
439
|
+
*
|
|
440
|
+
* Mientras queden títulos pendientes, vuelve a marcar como pendientes las copias que se hayan quedado
|
|
441
|
+
* despublicadas y renderiza otra vez, hasta `timings.maxRounds` rondas; la última confirma o lanza. Si todo
|
|
442
|
+
* está ya en su sitio, la primera comprobación pasa y no renderiza nada.
|
|
443
|
+
*
|
|
444
|
+
* @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`; usa `LIVE_STATUS.UNPUBLISHED`
|
|
445
|
+
* para confirmar una despublicación.
|
|
446
|
+
* @throws {RenderError} `TITLES_NOT_FINAL` si tras la última ronda sigue faltando algún título.
|
|
447
|
+
*/
|
|
448
|
+
renderUntilLive(siteIds: number[], titles: string[], expectedStatus?: number): Promise<void>;
|
|
449
|
+
/**
|
|
450
|
+
* Comprueba, sin renderizar, que los títulos están en `expectedStatus`.
|
|
451
|
+
*
|
|
452
|
+
* @throws {RenderError} `TITLES_NOT_FINAL`, con los títulos que faltan en `missingTitles`.
|
|
453
|
+
*/
|
|
454
|
+
confirmPagesByTitle(siteIds: number[], titles: string[], expectedStatus?: number): Promise<void>;
|
|
455
|
+
/**
|
|
456
|
+
* Libera el bloqueo de render que deja `build/start`, emitiendo un `build/end` vacío.
|
|
457
|
+
*
|
|
458
|
+
* Llámalo en tu `finally` si un render puede abortar a medias. Es best-effort: no lanza aunque el backend
|
|
459
|
+
* responda con error, porque el caso normal es que ya no hubiera nada que liberar.
|
|
460
|
+
*/
|
|
461
|
+
releaseRenderLock(siteId: number): Promise<void>;
|
|
462
|
+
/** Títulos que aún no están en `expectedStatus`, leyendo el listado completo de cada site. */
|
|
463
|
+
private missingPagesByTitle;
|
|
464
|
+
/** Devuelve a "pendiente de publicar" las copias despublicadas de los títulos que faltan. */
|
|
465
|
+
private republishMissingWantedLive;
|
|
466
|
+
/**
|
|
467
|
+
* Páginas de los sites indicados, **paginando hasta el final** del listado.
|
|
468
|
+
*
|
|
469
|
+
* Recorre páginas mientras el backend devuelva el lote completo: si llega uno más corto, es el último. Así un
|
|
470
|
+
* site con más páginas que `pageList.itemsPerPage` no se queda a medias, que era la causa de confirmaciones
|
|
471
|
+
* por título erróneas cuando solo se leía la primera página.
|
|
472
|
+
*
|
|
473
|
+
* @throws {RenderError} `PAGE_LIST_FAILED` si el backend no puede devolver el listado.
|
|
474
|
+
*/
|
|
475
|
+
private listPages;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Publicación: marcar páginas como pendientes y publicar sites.
|
|
480
|
+
*
|
|
481
|
+
* Ojo con el reparto de responsabilidades: **esto no es parte del render**. En producción lo dispara la persona
|
|
482
|
+
* que pulsa "Publicar" en el CMS; el render solo reacciona a lo que ya está pendiente. Vive en este paquete como
|
|
483
|
+
* conveniencia para montar el escenario desde un script, no porque el render lo necesite.
|
|
484
|
+
*
|
|
485
|
+
* El orden importa: primero se marcan las páginas, luego se publica el site. Publicar es lo que hace que el
|
|
486
|
+
* backend las incluya en el siguiente `build/start`.
|
|
487
|
+
*/
|
|
488
|
+
|
|
489
|
+
/** Dependencias del publicador, inyectadas por constructor. Las mismas que el motor necesita para hablar con la API. */
|
|
490
|
+
type PublisherDeps = ApiClientConfig;
|
|
491
|
+
/**
|
|
492
|
+
* Dispara la publicación en el CMS. Complementa al motor de render: este deja el trabajo pendiente, el motor lo
|
|
493
|
+
* resuelve.
|
|
494
|
+
*
|
|
495
|
+
* ```ts
|
|
496
|
+
* const publisher = new CmsPublisher({ transport, apiUrl, authToken });
|
|
497
|
+
* await publisher.publishPages([siteId], [pageId]);
|
|
498
|
+
* await engine.renderUntilLive([siteId], ["Mi página"]);
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
declare class CmsPublisher {
|
|
502
|
+
private readonly api;
|
|
503
|
+
private readonly logger;
|
|
504
|
+
constructor(deps: PublisherDeps);
|
|
505
|
+
/**
|
|
506
|
+
* Marca páginas como pendientes de publicar. Sin ids no hace nada.
|
|
507
|
+
*
|
|
508
|
+
* @throws {RenderError} `MARK_PENDING_FAILED` si el backend responde >= 400.
|
|
509
|
+
*/
|
|
510
|
+
markPagesPending(pageIds: number[]): Promise<void>;
|
|
511
|
+
/**
|
|
512
|
+
* Publica los sites indicados, uno por uno.
|
|
513
|
+
*
|
|
514
|
+
* @throws {RenderError} `PUBLISH_FAILED` si el backend responde >= 400 para alguno.
|
|
515
|
+
*/
|
|
516
|
+
publishSites(siteIds: number[]): Promise<void>;
|
|
517
|
+
/**
|
|
518
|
+
* Los dos pasos en orden: marca las páginas y publica los sites. Es el disparo completo que deja el trabajo
|
|
519
|
+
* listo para que el motor lo renderice.
|
|
520
|
+
*/
|
|
521
|
+
publishPages(siteIds: number[], pageIds: number[]): Promise<void>;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Configuración de la fachada: la del motor, que es un superconjunto de la del publicador. */
|
|
525
|
+
type CxRenderMockDeps = EngineDeps;
|
|
526
|
+
/**
|
|
527
|
+
* Punto de entrada recomendado.
|
|
528
|
+
*
|
|
529
|
+
* ```ts
|
|
530
|
+
* const cx = new CxRenderMock({ transport, apiUrl, authToken, logger });
|
|
531
|
+
* await cx.publishAndRender([siteId], [pageId], ["Mi página"]);
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
declare class CxRenderMock {
|
|
535
|
+
/** El motor de render, por si necesitas sus métodos sueltos. */
|
|
536
|
+
readonly engine: CxRenderEngine;
|
|
537
|
+
/** El disparo de publicación, por si quieres publicar sin renderizar. */
|
|
538
|
+
readonly publisher: CmsPublisher;
|
|
539
|
+
constructor(deps: CxRenderMockDeps);
|
|
540
|
+
/**
|
|
541
|
+
* El flujo entero: publica, renderiza y confirma que los títulos llegaron a su estado final. Libera el bloqueo
|
|
542
|
+
* de render de cada site si algo falla a medias.
|
|
543
|
+
*
|
|
544
|
+
* @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`.
|
|
545
|
+
* @throws {RenderError} con el `code` de la etapa que falló.
|
|
546
|
+
*/
|
|
547
|
+
publishAndRender(siteIds: number[], pageIds: number[], titles: string[], expectedStatus?: number): Promise<void>;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export { type BuildEndBody, type BuildStartDecision, CmsPublisher, CxRenderEngine, CxRenderMock, type CxRenderMockDeps, DEFAULT_PAGE_LIST_QUERY, DEFAULT_TIMINGS, type EngineDeps, LIVE_STATUS, type LiveStatus, type Logger, NOOP_LOGGER, type PageClassification, type PageInfo, type PageListQuery, type PublisherDeps, RENDER_ERROR_CODE, RenderError, type RenderErrorCode, type RenderErrorDetail, type RenderTimings, type RequestOptions, type RetryOptions, type SettleDecision, type SiteBuildInfo, type TimeoutOptions, type Transport, type TransportMethod, type TransportResponse, buildEndBody, classifyPage, collectRepublishIds, createConsoleLogger, decideBuildStartStep, decideSettleStep, diffMissingByTitle, listPageUrl, pageListSignature, selectPendingSites, withRetry, withTimeout };
|