@griddo/cx-render-mock 1.0.0 → 1.1.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ Añadido (retrocompatible: 1.0.0 sigue funcionando sin cambios; el motor `engine.ts` no se toca).
6
+
7
+ - `withPendingHashes(items, hashesById)`: completa el listado `site/{id}/pages?format=list` (que no trae `hash`)
8
+ con los hashes reales de las páginas pendientes (`liveStatus` 2), para que `pageListSignature` detecte una
9
+ página que pasa de 2 a 2 con hash nuevo (cascada DEPENDENT_PAGES_UPDATER). Sin hashes aportados, la firma es
10
+ idéntica a la de 1.0.0.
11
+ - `isBuildStartSkipped(body)`: detecta `{ skipped: true }` en la respuesta de `build/start` (otro render tiene el
12
+ site reclamado). Solo detecta: la política de reintento queda en manos del consumidor. `decideBuildStartStep`
13
+ no cambia de firma.
14
+
3
15
  ## 0.1.0 — sin publicar
4
16
 
5
17
  Primera versión del paquete, extraída de la suite E2E de Griddo, donde este código lleva tiempo en uso.
package/README.md CHANGED
@@ -193,6 +193,10 @@ Se puede meter el motor dentro de Cypress envolviéndolo en `cy.then(() => engin
193
193
  Ambos caminos están probados: el motor con el paquete instalado desde el tarball en un consumidor de Node (ESM y
194
194
  CJS), y el núcleo con un spec real de Cypress consumiendo `@griddo/cx-render-mock` desde `node_modules`.
195
195
 
196
+ Para un settle que vea los cambios de hash de las páginas pendientes (el listado no trae `hash`), lee
197
+ `GET page/{id}` de las pendientes y pasa sus hashes: `pageListSignature(withPendingHashes(items, { [id]: hash }))`.
198
+ Para detectar un `build/start` omitido por lock de render: `isBuildStartSkipped(bs.body)`.
199
+
196
200
  ## Dos avisos que importan
197
201
 
198
202
  1. **Lock `rendering`**: `build/start` bloquea el site y `build/end` lo libera. Si el proceso muere entre ambos,
@@ -221,7 +225,7 @@ CJS), y el núcleo con un spec real de Cypress consumiendo `@griddo/cx-render-mo
221
225
  | `Logger`, `NOOP_LOGGER`, `createConsoleLogger` | El contrato de log opcional |
222
226
  | `RenderTimings`, `DEFAULT_TIMINGS`, `PageListQuery`, `DEFAULT_PAGE_LIST_QUERY` | Esperas, topes y paginación |
223
227
  | `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ú |
228
+ | `classifyPage`, `diffMissingByTitle`, `decideBuildStartStep`, `selectPendingSites`, `collectRepublishIds`, `buildEndBody`, `listPageUrl`, `withPendingHashes`, `isBuildStartSkipped` | Las decisiones puras, si orquestas tú |
225
229
 
226
230
  ## Comandos
227
231
 
package/dist/index.d.mts CHANGED
@@ -94,6 +94,28 @@ declare const listPageUrl: (apiUrl: string, siteId: number, query?: PageListQuer
94
94
  * build/start ya puede capturar el trabajo completo en un pase. Pura (datos → string), para el settle adaptativo.
95
95
  */
96
96
  declare const pageListSignature: (items: PageInfo[]) => string;
97
+ /**
98
+ * Completa un listado (`site/{id}/pages?format=list`, que NO trae `hash`) con los hashes reales de las páginas
99
+ * pendientes de publicar (`liveStatus` 2), para que `pageListSignature` vea los cambios de hash.
100
+ *
101
+ * Por qué: la cascada de la API (DEPENDENT_PAGES_UPDATER) puede dejar una página en 2 con un hash NUEVO. En el
102
+ * listado la página sigue siendo `id:2:` (sin hash), así que la firma no cambia y el settle da quiescencia con
103
+ * la cascada aún activa. El shell lee `GET page/{id}` de las pendientes y pasa aquí `{ [id]: hash }`.
104
+ *
105
+ * - Solo se inyecta en páginas con `liveStatus.id === LIVE_STATUS.PENDING_PUBLISHING` y con su id presente en
106
+ * `hashesById`; el resto queda tal cual (un hash aportado para una no pendiente no altera su firma).
107
+ * - Un valor `null` se respeta (hash ausente), y se distingue de un hash presente en la firma.
108
+ * - Retrocompatible: sin `hashesById` (o vacío) devuelve las mismas páginas, y la firma es idéntica a la de 1.0.0.
109
+ * - Pura: devuelve copias de las pendientes tocadas y no muta la entrada. Tolera `items` ausente (→ `[]`).
110
+ */
111
+ declare const withPendingHashes: (items: PageInfo[], hashesById?: Readonly<Record<number, string | null>>) => PageInfo[];
112
+ /**
113
+ * ¿`GET site/{id}/build/start` ha sido OMITIDO? La API responde `{ skipped: true, publishIds: [] }` cuando otro
114
+ * render tiene el site reclamado (griddo-api `models/sites/index.ts`, `startBuild`). Solo `skipped === true`
115
+ * (booleano estricto) cuenta: un body ausente, no objeto, o con `skipped` de otro tipo → `false`. Pura. La
116
+ * política ante un skipped (reintentar o no) NO se decide aquí: el core solo lo detecta.
117
+ */
118
+ declare const isBuildStartSkipped: (body: unknown) => boolean;
97
119
  /** Decisión del settle adaptativo (unión discriminada: `wait` porta su `waitMs`). */
98
120
  type SettleDecision = {
99
121
  action: "go";
@@ -547,4 +569,4 @@ declare class CxRenderMock {
547
569
  publishAndRender(siteIds: number[], pageIds: number[], titles: string[], expectedStatus?: number): Promise<void>;
548
570
  }
549
571
 
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 };
572
+ 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, isBuildStartSkipped, listPageUrl, pageListSignature, selectPendingSites, withPendingHashes, withRetry, withTimeout };
package/dist/index.d.ts CHANGED
@@ -94,6 +94,28 @@ declare const listPageUrl: (apiUrl: string, siteId: number, query?: PageListQuer
94
94
  * build/start ya puede capturar el trabajo completo en un pase. Pura (datos → string), para el settle adaptativo.
95
95
  */
96
96
  declare const pageListSignature: (items: PageInfo[]) => string;
97
+ /**
98
+ * Completa un listado (`site/{id}/pages?format=list`, que NO trae `hash`) con los hashes reales de las páginas
99
+ * pendientes de publicar (`liveStatus` 2), para que `pageListSignature` vea los cambios de hash.
100
+ *
101
+ * Por qué: la cascada de la API (DEPENDENT_PAGES_UPDATER) puede dejar una página en 2 con un hash NUEVO. En el
102
+ * listado la página sigue siendo `id:2:` (sin hash), así que la firma no cambia y el settle da quiescencia con
103
+ * la cascada aún activa. El shell lee `GET page/{id}` de las pendientes y pasa aquí `{ [id]: hash }`.
104
+ *
105
+ * - Solo se inyecta en páginas con `liveStatus.id === LIVE_STATUS.PENDING_PUBLISHING` y con su id presente en
106
+ * `hashesById`; el resto queda tal cual (un hash aportado para una no pendiente no altera su firma).
107
+ * - Un valor `null` se respeta (hash ausente), y se distingue de un hash presente en la firma.
108
+ * - Retrocompatible: sin `hashesById` (o vacío) devuelve las mismas páginas, y la firma es idéntica a la de 1.0.0.
109
+ * - Pura: devuelve copias de las pendientes tocadas y no muta la entrada. Tolera `items` ausente (→ `[]`).
110
+ */
111
+ declare const withPendingHashes: (items: PageInfo[], hashesById?: Readonly<Record<number, string | null>>) => PageInfo[];
112
+ /**
113
+ * ¿`GET site/{id}/build/start` ha sido OMITIDO? La API responde `{ skipped: true, publishIds: [] }` cuando otro
114
+ * render tiene el site reclamado (griddo-api `models/sites/index.ts`, `startBuild`). Solo `skipped === true`
115
+ * (booleano estricto) cuenta: un body ausente, no objeto, o con `skipped` de otro tipo → `false`. Pura. La
116
+ * política ante un skipped (reintentar o no) NO se decide aquí: el core solo lo detecta.
117
+ */
118
+ declare const isBuildStartSkipped: (body: unknown) => boolean;
97
119
  /** Decisión del settle adaptativo (unión discriminada: `wait` porta su `waitMs`). */
98
120
  type SettleDecision = {
99
121
  action: "go";
@@ -547,4 +569,4 @@ declare class CxRenderMock {
547
569
  publishAndRender(siteIds: number[], pageIds: number[], titles: string[], expectedStatus?: number): Promise<void>;
548
570
  }
549
571
 
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 };
572
+ 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, isBuildStartSkipped, listPageUrl, pageListSignature, selectPendingSites, withPendingHashes, withRetry, withTimeout };
package/dist/index.js CHANGED
@@ -36,9 +36,11 @@ __export(index_exports, {
36
36
  decideBuildStartStep: () => decideBuildStartStep,
37
37
  decideSettleStep: () => decideSettleStep,
38
38
  diffMissingByTitle: () => diffMissingByTitle,
39
+ isBuildStartSkipped: () => isBuildStartSkipped,
39
40
  listPageUrl: () => listPageUrl,
40
41
  pageListSignature: () => pageListSignature,
41
42
  selectPendingSites: () => selectPendingSites,
43
+ withPendingHashes: () => withPendingHashes,
42
44
  withRetry: () => withRetry,
43
45
  withTimeout: () => withTimeout
44
46
  });
@@ -66,6 +68,10 @@ var DEFAULT_TIMINGS = {
66
68
  var DEFAULT_PAGE_LIST_QUERY = { page: 1, itemsPerPage: 300 };
67
69
  var listPageUrl = (apiUrl, siteId, query = DEFAULT_PAGE_LIST_QUERY) => `${apiUrl}site/${siteId}/pages?deleted=false&page=${query.page}&itemsPerPage=${query.itemsPerPage}&format=list`;
68
70
  var pageListSignature = (items) => (items || []).filter((p) => p && p.id != null).map((p) => `${p.id}:${p.liveStatus?.id ?? ""}:${p.hash ?? ""}`).sort().join("|");
71
+ var withPendingHashes = (items, hashesById) => (items || []).map(
72
+ (p) => p && p.id != null && hashesById && p.liveStatus?.id === LIVE_STATUS.PENDING_PUBLISHING && p.id in hashesById ? { ...p, hash: hashesById[p.id] } : p
73
+ );
74
+ var isBuildStartSkipped = (body) => typeof body === "object" && body !== null && body.skipped === true;
69
75
  var decideSettleStep = (prevSignature, signature, elapsedMs, timings = DEFAULT_TIMINGS) => {
70
76
  if (elapsedMs >= timings.settleMs) return { action: "go" };
71
77
  if (prevSignature !== null && prevSignature === signature) return { action: "go" };
@@ -582,9 +588,11 @@ var withTimeout = (transport, opts) => {
582
588
  decideBuildStartStep,
583
589
  decideSettleStep,
584
590
  diffMissingByTitle,
591
+ isBuildStartSkipped,
585
592
  listPageUrl,
586
593
  pageListSignature,
587
594
  selectPendingSites,
595
+ withPendingHashes,
588
596
  withRetry,
589
597
  withTimeout
590
598
  });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/core.ts","../src/errors.ts","../src/logger.ts","../src/api-client.ts","../src/sleep.ts","../src/engine.ts","../src/publish.ts","../src/facade.ts","../src/transport.ts"],"sourcesContent":["/**\n * Punto de entrada público de `@griddo/cx-render-mock`.\n *\n * Los exports son explícitos a propósito: lo que el paquete publica es una decisión, no el efecto secundario de\n * un `export *`. Si añades algo interno (como `./api-client`), no aparece aquí y por tanto no es API pública.\n */\n\n// Núcleo: constantes, tipos del contrato de la API y las decisiones puras, para quien orqueste por su cuenta.\nexport {\n\ttype BuildEndBody,\n\ttype BuildStartDecision,\n\tbuildEndBody,\n\tclassifyPage,\n\tcollectRepublishIds,\n\tDEFAULT_PAGE_LIST_QUERY,\n\tDEFAULT_TIMINGS,\n\tdecideBuildStartStep,\n\tdecideSettleStep,\n\tdiffMissingByTitle,\n\tLIVE_STATUS,\n\ttype LiveStatus,\n\tlistPageUrl,\n\ttype PageClassification,\n\ttype PageInfo,\n\ttype PageListQuery,\n\tpageListSignature,\n\ttype RenderTimings,\n\ttype SettleDecision,\n\ttype SiteBuildInfo,\n\tselectPendingSites,\n} from \"./core\";\n// El protocolo de render y el disparo de publicación, por separado.\nexport { CxRenderEngine, type EngineDeps } from \"./engine\";\n// Errores: un único tipo con código.\nexport {\n\tRENDER_ERROR_CODE,\n\tRenderError,\n\ttype RenderErrorCode,\n\ttype RenderErrorDetail,\n} from \"./errors\";\n// Fachada: motor + publicador con una sola configuración. El punto de partida recomendado.\nexport { CxRenderMock, type CxRenderMockDeps } from \"./facade\";\n// Log opcional.\nexport { createConsoleLogger, type Logger, NOOP_LOGGER } from \"./logger\";\nexport { CmsPublisher, type PublisherDeps } from \"./publish\";\n// Transporte: el contrato que implementas y los decoradores para envolverlo.\nexport {\n\ttype RequestOptions,\n\ttype RetryOptions,\n\ttype TimeoutOptions,\n\ttype Transport,\n\ttype TransportMethod,\n\ttype TransportResponse,\n\twithRetry,\n\twithTimeout,\n} from \"./transport\";\n","/**\n * Núcleo PURO del test-double de render de CX — agnóstico de Cypress en VALOR y en TIPO.\n *\n * Contiene las decisiones difíciles del protocolo (clasificación por hash, diff por título, gate de\n * build/start, shaping del body de build/end, colección de re-pending de la mitigación cross-job, y los knobs\n * por defecto del render de un solo pase) SIN acoplarse a la cola de comandos de Cypress: cero `cy`/`Cypress`/`expect`/`window`,\n * cero `import ... 'cypress'`, y ninguna firma devuelve `Cypress.Chainable` (solo tipos neutrales). Es el\n * artefacto compartible: el shell Cypress de griddo-qa (`cypress/support/cx-render.ts`) y el motor de Promises\n * del paquete (`./engine`) consumen ESTE módulo, de modo que el racional que causa los bugs vive en un único\n * sitio (functional-core / imperative-shell). Ver `docs/cx-render-packaging-feasibility.md` §5/§6.\n *\n * Behavior-preserving: este código se EXTRAJO de `cx-render.ts` sin cambiar su comportamiento observable;\n * las constantes, umbrales y predicados son idénticos a los del shell original.\n */\n\n/**\n * Modelo de live_status: data-driven en la BD pero ESTABLE por orden de seed (contrato code-grounded e2e-202 →\n * griddo-api CONFIG/LiveStatusSeeder). El double no puede leer los flags de CONFIG en runtime → usa estos ids\n * verificados.\n */\nexport const LIVE_STATUS = {\n\tUNPUBLISHED: 1, // isUnpublished / offline — estado final de despublicación\n\tPENDING_PUBLISHING: 2, // isPendingPublishing / upload-pending — transitorio\n\tLIVE: 3, // isLiveStatus / active — estado final de publicación\n\tPENDING_UNPUBLISHING: 4, // isPendingUnpublishing / offline-pending — transitorio\n} as const;\n\n/** Los cuatro ids de live_status del contrato (1/2/3/4). */\nexport type LiveStatus = (typeof LIVE_STATUS)[keyof typeof LIVE_STATUS];\n\n/**\n * Forma mínima de una página tal como la exponen los endpoints que consume el double: `GET page/{id}` (trae\n * `hash` + `liveStatus`) y `site/{id}/pages?format=list` (trae `id` + `title` + `liveStatus`). Todos los\n * campos son opcionales porque el body real de la API puede venir incompleto y el core no debe asumir.\n */\nexport interface PageInfo {\n\tid?: number;\n\ttitle?: string;\n\thash?: string | null;\n\tliveStatus?: { id?: number } | null;\n}\n\n/** Cuerpo de `POST site/{id}/build/end` (lo que el CX real envía para resolver los transitorios). */\nexport interface BuildEndBody {\n\tpublishHashes: string[];\n\tsiteHash: string | null;\n\tunpublishHashes: string[];\n\tpublishPagesIds: number[];\n}\n\n// --- Esperas, topes y paginación (todo ajustable por el consumidor) -----------------------------------------\n// Antes eran constantes fijas del módulo. Ahora son datos que el consumidor puede sustituir: cada entorno tiene\n// su latencia (CI saturado ≠ local), y quien usa el paquete la conoce mejor que el paquete. Los defaults\n// (medidos) siguen aquí, y el shell Cypress los sobreescribe por `Cypress.env`.\n\n/** Esperas y topes del render. Todos sustituibles; los defaults están en `DEFAULT_TIMINGS`. */\nexport interface RenderTimings {\n\t/**\n\t * TECHO (ms) del settle ADAPTATIVO previo al ÚNICO build/start del render de un solo pase. El settle espera\n\t * al spread ASÍNCRONO de las copias GLOBAL, pero ya NO es un sleep fijo: se sondea el listado del site hasta\n\t * que dos sondas consecutivas devuelven la misma firma (`pageListSignature` → `decideSettleStep`), con este\n\t * valor como tope. Racional: una constante fija no cubre una latencia variable — a 15s el pase único perdía\n\t * rezagados bajo carga de CI (renderFlow2/002 agotaba los 3 intentos) y a 30-45s se paga tiempo muerto por\n\t * site y por ronda en el 90% de renders que asientan en segundos. `<= 0` desactiva el settle por completo.\n\t */\n\tsettleMs: number;\n\t/** Intervalo (ms) entre sondas del settle adaptativo. */\n\tsettlePollMs: number;\n\t/**\n\t * Rondas máximas del orquestador auto-sanador `renderUntilLive`. NO baja a 1: con 1 quedaban sets ENTEROS\n\t * not-Live en CI (el build/start del mock no captura TODAS las páginas en un pase; las rondas\n\t * re-pend+re-render sí).\n\t */\n\tmaxRounds: number;\n\t/** Wait entre rondas del orquestador auto-sanador (ms). */\n\troundWaitMs: number;\n\t/** Reintentos máximos de build/start mientras dé 4xx transitorio (~30s con el retry por defecto). */\n\tbuildStartMaxWaits: number;\n\t/** Backoff entre reintentos de build/start en 4xx transitorio (ms). */\n\tbuildStartRetryWaitMs: number;\n}\n\n/** Defaults medidos de `RenderTimings`. El shell los sobreescribe por `Cypress.env`. */\nexport const DEFAULT_TIMINGS: RenderTimings = {\n\tsettleMs: 45000,\n\tsettlePollMs: 3000,\n\tmaxRounds: 3,\n\troundWaitMs: 1500,\n\tbuildStartMaxWaits: 15,\n\tbuildStartRetryWaitMs: 2000,\n};\n\n/** Paginación con la que se pide el listado de páginas de un site. Defaults en `DEFAULT_PAGE_LIST_QUERY`. */\nexport interface PageListQuery {\n\tpage: number;\n\titemsPerPage: number;\n}\n\n/** Defaults de `PageListQuery`. */\nexport const DEFAULT_PAGE_LIST_QUERY: PageListQuery = { page: 1, itemsPerPage: 300 };\n\n/**\n * URL del listado del site (`site/{id}/pages?format=list`) que leen el diff por título y el re-pending.\n * Centraliza la query string y la paginación — pura (string → string), sin acoplarse a Cypress (recibe\n * `apiUrl` como argumento). ⚠️ Limitación HEREDADA: pide UNA sola página (300 ítems por defecto); un site con\n * más páginas daría confirmación por título errónea. Se mitiga subiendo `itemsPerPage`, pero no pagina.\n */\nexport const listPageUrl = (\n\tapiUrl: string,\n\tsiteId: number,\n\tquery: PageListQuery = DEFAULT_PAGE_LIST_QUERY,\n): string =>\n\t`${apiUrl}site/${siteId}/pages?deleted=false&page=${query.page}&itemsPerPage=${query.itemsPerPage}&format=list`;\n\n/**\n * Firma ESTABLE del estado render-relevante de un listado de páginas: `id:liveStatus:hash` por página, ordenado\n * por id (el orden del listado puede variar entre sondas sin que el estado haya cambiado). Dos firmas iguales en\n * sondas consecutivas = el backend ha dejado de mover estados/hashes (spread y recálculos asentados) → el\n * build/start ya puede capturar el trabajo completo en un pase. Pura (datos → string), para el settle adaptativo.\n */\nexport const pageListSignature = (items: PageInfo[]): string =>\n\t(items || [])\n\t\t.filter((p) => p && p.id != null)\n\t\t.map((p) => `${p.id}:${p.liveStatus?.id ?? \"\"}:${p.hash ?? \"\"}`)\n\t\t.sort()\n\t\t.join(\"|\");\n\n/** Decisión del settle adaptativo (unión discriminada: `wait` porta su `waitMs`). */\nexport type SettleDecision = { action: \"go\" } | { action: \"wait\"; waitMs: number };\n\n/**\n * Gate del settle ADAPTATIVO: decide si el backend ya está quiescente y el render puede arrancar.\n * - dos sondas consecutivas con la MISMA firma → `'go'` (quiescencia: nada se ha movido en un intervalo);\n * - `elapsedMs >= timings.settleMs` → `'go'` (techo: bajo carga patológica no se espera para siempre);\n * - resto → `'wait'` con `min(settlePollMs, settleMs - elapsedMs)` (el último wait no desborda el techo).\n * La PRIMERA sonda (`prevSignature = null`) nunca decide `'go'` por firma: hacen falta dos muestras para saber\n * que nada cambia. Con `settleMs <= 0` decide `'go'` inmediato (settle desactivado; el caller puede además\n * ahorrarse la sonda).\n */\nexport const decideSettleStep = (\n\tprevSignature: string | null,\n\tsignature: string,\n\telapsedMs: number,\n\ttimings: RenderTimings = DEFAULT_TIMINGS,\n): SettleDecision => {\n\tif (elapsedMs >= timings.settleMs) return { action: \"go\" };\n\tif (prevSignature !== null && prevSignature === signature) return { action: \"go\" };\n\treturn { action: \"wait\", waitMs: Math.min(timings.settlePollMs, timings.settleMs - elapsedMs) };\n};\n\n/** Clasificación de una página según su `hash`/`status`, resultado de `classifyPage`. */\nexport type PageClassification = \"publish\" | \"needsHash\" | \"skip\";\n\n/**\n * Clasifica una página por su HASH, como el CX real (griddo-cx store.ts:366-369): el TRABAJO = toda página\n * con `hash != null`, SIN filtrar por estado. `build/end` publica el structured_data_content (has_been_published,\n * que la API pública EXIGE vía includePendingPublishing:off) SOLO de los publishPagesIds/publishHashes que\n * recibe; una copia GLOBAL/traducción puede alcanzar live(3) con su CONTENIDO aún sin publicar → si la\n * SALTÁRAMOS por estar live(3) (como hacía el filtro `status===2` anterior), su contenido NUNCA se publicaría\n * y la pública la excluiría (bug de traducciones en CI). Por eso:\n * - `hash != null` → `'publish'` (se publica su contenido aunque ya esté live(3); el render es un solo pase);\n * - `hash == null` y `status === PENDING_PUBLISHING` → `'needsHash'` (el spread aún no asignó el hash);\n * - resto (p.ej. live(3) con hash=null = ya publicada y con el hash nulado por endBuild) → `'skip'`.\n */\nexport const classifyPage = (\n\thash: string | null | undefined,\n\tstatus: number | undefined,\n): PageClassification => {\n\tif (hash != null) return \"publish\";\n\tif (status === LIVE_STATUS.PENDING_PUBLISHING) return \"needsHash\";\n\treturn \"skip\";\n};\n\n/**\n * Diff SOFT por TÍTULO: dada la lista de páginas de los sites, devuelve los `wanted` que AÚN no están en\n * `expectedStatus` (ausentes del listado o en otro estado). Se compara POR TÍTULO y no por id porque publicar/\n * re-renderizar una GLOBAL canónica REASIGNA el id (memoria publish-canonical-global-page-reassigns-id) → un id\n * de createSuite (o de un build/start previo) queda stale; el listado del site es la entidad renderizada real.\n *\n * Preferencia `statusByTitle`: prefiere `expectedStatus` si alguna entidad con ese título ya lo tiene (una\n * global puede tener EDITOR + copia con distinto estado). El resultado (títulos que faltan) depende solo de si\n * ALGUNA entidad con ese título alcanzó `expectedStatus`.\n */\nexport const diffMissingByTitle = (\n\titems: PageInfo[],\n\twanted: string[],\n\texpectedStatus: number,\n): string[] => {\n\tconst w = wanted || [];\n\tif (w.length === 0) return [];\n\tconst statusByTitle: Record<string, number> = {};\n\titems.forEach((p) => {\n\t\tif (p && p.title != null && w.includes(p.title)) {\n\t\t\tconst st = p.liveStatus?.id;\n\t\t\tif (st === expectedStatus || !(p.title in statusByTitle)) statusByTitle[p.title] = st ?? -1;\n\t\t}\n\t});\n\treturn w.filter((t) => statusByTitle[t] !== expectedStatus);\n};\n\n/** Shaping del cuerpo de `POST site/{id}/build/end` (normaliza `siteHash` ausente a `null`). */\nexport const buildEndBody = (\n\tpublishHashes: string[],\n\tsiteHash: string | null | undefined,\n\tunpublishHashes: string[],\n\tpublishPagesIds: number[],\n): BuildEndBody => ({\n\tpublishHashes,\n\tsiteHash: siteHash ?? null,\n\tunpublishHashes,\n\tpublishPagesIds,\n});\n\n/**\n * Colección de re-pending de la MITIGACIÓN cross-job: de las páginas del listado, devuelve los ids in-site\n * (deduplicados, en orden de aparición) de los títulos `missing` que NO están live. El barrido global\n * INCONDICIONAL de endBuild (buildGlobalUnpublishAllWhere) puede despublicar globales QA de OTRO job → una\n * página offline ya NO la recoge build/start, así que re-renderizar no la recupera; re-pendearla (upload-pending)\n * la devuelve a pending(2) para que renderSiteToQuiescence la renderice. Acotado a copias in-site de `wanted`\n * (el caller solo pasa los títulos deseados-live que faltan) y solo `st !== LIVE`.\n */\nexport const collectRepublishIds = (items: PageInfo[], missing: string[]): number[] => {\n\tconst ids: number[] = [];\n\titems.forEach((p) => {\n\t\tconst st = p?.liveStatus?.id;\n\t\t// `p.title as string`: replica EXACTA de HEAD (`missing.includes(p.title)`, sin guard) — behavior-preserving\n\t\t// estricto; el cast solo satisface a tsc (items es PageInfo, no `any`), no cambia el runtime.\n\t\tif (p && p.id != null && missing.includes(p.title as string) && st !== LIVE_STATUS.LIVE)\n\t\t\tids.push(p.id);\n\t});\n\treturn Array.from(new Set(ids));\n};\n\n/**\n * Decisión del gate build/start-ready (unión discriminada: `retry` porta su `waitMs`, `proceed`/`fail` no).\n */\nexport type BuildStartDecision =\n\t| { action: \"proceed\" }\n\t| { action: \"retry\"; waitMs: number }\n\t| { action: \"fail\" };\n\n/**\n * Gate build/start-ready: replica el timing del CX real (que dispara ~1 min después, cuando el pathing ya\n * asentó, y nunca ve el 4xx transitorio) sin su latencia. El publish lanza el spread de las copias GLOBAL de\n * forma ASÍNCRONA; hasta que termina, una copia puede existir con `pathId=null` y build/start (que corre\n * fixAllPages → `pathId.split(...)`) devuelve 4xx TRANSITORIO. Por eso reintentamos SOLO mientras dé 4xx,\n * cortando al 1er 200; si agota el presupuesto, el shell asserta RUIDOSO (no un CypressError crudo).\n * - `status < 400` → `'proceed'`;\n * - agotado el presupuesto (`startWaits >= timings.buildStartMaxWaits`) → `'fail'`;\n * - resto → `'retry'` con backoff `timings.buildStartRetryWaitMs`.\n */\nexport const decideBuildStartStep = (\n\tstatus: number,\n\tstartWaits: number,\n\ttimings: RenderTimings = DEFAULT_TIMINGS,\n): BuildStartDecision => {\n\tif (status < 400) return { action: \"proceed\" };\n\tif (startWaits >= timings.buildStartMaxWaits) return { action: \"fail\" };\n\treturn { action: \"retry\", waitMs: timings.buildStartRetryWaitMs };\n};\n\n/** Forma mínima de un site en `GET sites/all`: lo justo para saber si tiene render pendiente. */\nexport interface SiteBuildInfo {\n\tid?: number;\n\t/** La API lo marca cuando el site tiene cambios sin renderizar (señal QA-scoped: hash pendiente, no bloqueado). */\n\tshouldBeUpdated?: boolean;\n}\n\n/**\n * Ids de los sites con render PENDIENTE — lo que el CX real busca en cada pasada de su poll.\n *\n * `onlyIds` acota el resultado (el shell le pasa los sites del test en curso). Esa acotación NO es cosmética:\n * el CX real es un único proceso global, pero el mock corre por test y a escala CI ~40 jobs comparten una BD;\n * sin acotar, cada pasada arrastraría el trabajo de los demás (build/start → fixAllPages es pesado) y la BD se\n * satura, tumbando incluso specs que no usan el mock.\n */\nexport const selectPendingSites = (sites: SiteBuildInfo[], onlyIds?: number[]): number[] => {\n\tconst ids: number[] = [];\n\t(sites || []).forEach((site) => {\n\t\tif (!site || site.id == null || !site.shouldBeUpdated) return;\n\t\tif (onlyIds && !onlyIds.includes(site.id)) return;\n\t\tids.push(site.id);\n\t});\n\treturn Array.from(new Set(ids));\n};\n","/**\n * Error del paquete, con código.\n *\n * Un único tipo de error con un `code` en lugar de una jerarquía de clases: los consumidores necesitan\n * distinguir *qué* falló para decidir (reintentar, avisar, abortar), y eso se hace igual de bien con un campo que\n * con `instanceof`, sin obligar a importar siete clases.\n */\n\n/** Códigos de {@link RenderError}. */\nexport const RENDER_ERROR_CODE = {\n\t/** `build/start` siguió respondiendo error hasta agotar los reintentos: el site no llega a estar listo. */\n\tBUILD_START_NOT_READY: \"BUILD_START_NOT_READY\",\n\t/** `build/end` respondió >= 400: el render no se ha aplicado y el bloqueo puede seguir puesto. */\n\tBUILD_END_FAILED: \"BUILD_END_FAILED\",\n\t/** Se agotaron las rondas y algunos títulos no alcanzaron el estado final. Mira `missingTitles`. */\n\tTITLES_NOT_FINAL: \"TITLES_NOT_FINAL\",\n\t/** No se pudo saber qué sites tenían render pendiente. */\n\tSITES_LOOKUP_FAILED: \"SITES_LOOKUP_FAILED\",\n\t/** No se pudo leer el listado de páginas de un site. */\n\tPAGE_LIST_FAILED: \"PAGE_LIST_FAILED\",\n\t/** Falló marcar páginas como pendientes de publicar. */\n\tMARK_PENDING_FAILED: \"MARK_PENDING_FAILED\",\n\t/** Falló publicar un site. */\n\tPUBLISH_FAILED: \"PUBLISH_FAILED\",\n} as const;\n\n/** Uno de los {@link RENDER_ERROR_CODE}. */\nexport type RenderErrorCode = (typeof RENDER_ERROR_CODE)[keyof typeof RENDER_ERROR_CODE];\n\n/** Datos opcionales que acompañan a un {@link RenderError}. */\nexport interface RenderErrorDetail {\n\t/** Status HTTP de la respuesta que provocó el error, si lo hubo. */\n\tstatus?: number;\n\t/** Body de esa respuesta, tal cual, para poder diagnosticar sin volver a reproducir. */\n\tbody?: unknown;\n\t/** Títulos que no alcanzaron su estado final. Solo en `TITLES_NOT_FINAL`. */\n\tmissingTitles?: string[];\n\t/** Error original, si este envuelve a otro. Se expone también como `cause` nativa. */\n\tcause?: unknown;\n}\n\n/**\n * Error de este paquete. Siempre trae `code`; el resto del detalle depende de dónde falló.\n *\n * ```ts\n * try {\n * \tawait engine.renderUntilLive([siteId], titles);\n * } catch (error) {\n * \tif (error instanceof RenderError && error.code === RENDER_ERROR_CODE.TITLES_NOT_FINAL) {\n * \t\tconsole.warn(\"no llegaron:\", error.missingTitles);\n * \t}\n * }\n * ```\n */\nexport class RenderError extends Error {\n\t/** Qué falló, de forma comprobable sin leer el mensaje. */\n\treadonly code: RenderErrorCode;\n\t/** Status HTTP asociado, si el error viene de una respuesta. */\n\treadonly status?: number;\n\t/** Body de la respuesta asociada. */\n\treadonly body?: unknown;\n\t/** Títulos que no alcanzaron su estado final (`TITLES_NOT_FINAL`). */\n\treadonly missingTitles?: string[];\n\n\tconstructor(code: RenderErrorCode, message: string, detail: RenderErrorDetail = {}) {\n\t\tsuper(message);\n\t\tthis.name = \"RenderError\";\n\t\tthis.code = code;\n\t\tthis.status = detail.status;\n\t\tthis.body = detail.body;\n\t\tthis.missingTitles = detail.missingTitles;\n\t\t// `cause` se asigna en lugar de pasarse al constructor: el segundo argumento de Error es ES2022, y así el\n\t\t// paquete sigue compilando contra librerías más antiguas sin perder el encadenado de errores en runtime.\n\t\tif (detail.cause !== undefined) (this as { cause?: unknown }).cause = detail.cause;\n\t}\n}\n","/**\n * Contrato de log mínimo, para que el paquete cuente qué está haciendo sin imponer una librería.\n *\n * Cuatro métodos y ninguna dependencia: encaja tal cual con `console`, y con pino, winston o el log del runner de\n * tests envolviéndolo en cuatro líneas. Si no pasas ninguno, el paquete no escribe nada.\n */\nexport interface Logger {\n\tdebug(message: string, meta?: unknown): void;\n\tinfo(message: string, meta?: unknown): void;\n\twarn(message: string, meta?: unknown): void;\n\terror(message: string, meta?: unknown): void;\n}\n\n/** Logger que descarta todo. Es el que se usa cuando no inyectas uno. */\nexport const NOOP_LOGGER: Logger = {\n\tdebug: () => undefined,\n\tinfo: () => undefined,\n\twarn: () => undefined,\n\terror: () => undefined,\n};\n\n/**\n * Logger sobre `console`, por comodidad en scripts y ejemplos.\n *\n * @param level nivel mínimo que se emite. Por defecto `info`, así que los `debug` se descartan.\n */\nexport const createConsoleLogger = (\n\tlevel: \"debug\" | \"info\" | \"warn\" | \"error\" = \"info\",\n): Logger => {\n\tconst order = { debug: 10, info: 20, warn: 30, error: 40 };\n\tconst min = order[level];\n\tconst emit =\n\t\t(method: \"debug\" | \"info\" | \"warn\" | \"error\") =>\n\t\t(message: string, meta?: unknown): void => {\n\t\t\tif (order[method] < min) return;\n\t\t\tif (meta === undefined) console[method](message);\n\t\t\telse console[method](message, meta);\n\t\t};\n\treturn { debug: emit(\"debug\"), info: emit(\"info\"), warn: emit(\"warn\"), error: emit(\"error\") };\n};\n","/**\n * Cliente interno sobre el transporte inyectado. **No es API pública**: no se exporta desde el índice del\n * paquete.\n *\n * Concentra las tres cosas que antes repetían el motor y el publicador: montar las cabeceras, componer la URL\n * a partir de `apiUrl`, y convertir una respuesta con error en un {@link RenderError} con su código. Así \"qué\n * cuenta como fallo\" se decide en un único sitio.\n */\n\nimport type { Transport, TransportResponse } from \"./transport\";\n\nimport { RenderError, type RenderErrorCode } from \"./errors\";\nimport { type Logger, NOOP_LOGGER } from \"./logger\";\n\n/** Lo que necesita cualquier pieza que hable con la API: el transporte, dónde está y con qué credencial. */\nexport interface ApiClientConfig {\n\ttransport: Transport;\n\tapiUrl: string;\n\tauthToken: string;\n\t/** Dónde contar lo que va pasando. Si no lo pasas, no se escribe nada. */\n\tlogger?: Logger;\n}\n\n/** Detalle de una llamada. Con `expectOk`, una respuesta >= 400 se convierte en error con ese código. */\nexport interface RequestSpec {\n\tbody?: unknown;\n\texpectOk?: RenderErrorCode;\n}\n\nexport class ApiClient {\n\treadonly logger: Logger;\n\tprivate readonly transport: Transport;\n\tprivate readonly apiUrl: string;\n\tprivate readonly headers: Record<string, string>;\n\n\tconstructor(config: ApiClientConfig) {\n\t\tthis.transport = config.transport;\n\t\tthis.apiUrl = config.apiUrl;\n\t\tthis.headers = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\tauthorization: `Bearer ${config.authToken}`,\n\t\t};\n\t\tthis.logger = config.logger ?? NOOP_LOGGER;\n\t}\n\n\t/** URL absoluta de un path relativo a `apiUrl`. Útil cuando la URL la compone el núcleo. */\n\turl(path: string): string {\n\t\treturn `${this.apiUrl}${path}`;\n\t}\n\n\tget(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"GET\", path, spec);\n\t}\n\n\tpost(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"POST\", path, spec);\n\t}\n\n\tput(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"PUT\", path, spec);\n\t}\n\n\t/**\n\t * Lanza si la respuesta trae error. Se usa cuando la llamada ya se hizo sin `expectOk` porque había que\n\t * inspeccionar el status antes de decidir.\n\t */\n\tensureOk(res: TransportResponse, code: RenderErrorCode, describe: string): void {\n\t\tif (res.status < 400) return;\n\t\tthrow new RenderError(code, `${describe} respondió ${res.status}`, {\n\t\t\tstatus: res.status,\n\t\t\tbody: res.body,\n\t\t});\n\t}\n\n\tprivate async send(\n\t\tmethod: \"GET\" | \"POST\" | \"PUT\",\n\t\tpath: string,\n\t\tspec: RequestSpec,\n\t): Promise<TransportResponse> {\n\t\tconst url = path.startsWith(\"http\") ? path : this.url(path);\n\t\tconst opts = { headers: this.headers, body: spec.body };\n\t\tconst res = await (method === \"GET\"\n\t\t\t? this.transport.get(url, opts)\n\t\t\t: method === \"POST\"\n\t\t\t\t? this.transport.post(url, opts)\n\t\t\t\t: this.transport.put(url, opts));\n\t\tif (spec.expectOk) this.ensureOk(res, spec.expectOk, `${method} ${path}`);\n\t\treturn res;\n\t}\n}\n","/** Espera de `ms` milisegundos. Es el `sleep` por defecto de todo el paquete; los tests inyectan un no-op. */\nexport const defaultSleep = (ms: number): Promise<void> =>\n\tnew Promise((resolve) => setTimeout(resolve, ms));\n","/**\n * Motor del protocolo de render: `build/start` → clasificar páginas por hash → `build/end`, con reintento por\n * rondas hasta que los títulos pedidos alcanzan su estado final.\n *\n * Es una clase porque tiene estado (configuración, cliente HTTP) y dependencias que conviene invertir: el HTTP y\n * el log. Las decisiones sin estado viven en `./core` como funciones puras; hablar con la API es cosa de\n * `./api-client`.\n */\n\nimport type { Logger } from \"./logger\";\n\nimport { ApiClient, type ApiClientConfig } from \"./api-client\";\nimport * as cxCore from \"./core\";\nimport { RENDER_ERROR_CODE, RenderError } from \"./errors\";\nimport { defaultSleep } from \"./sleep\";\n\n/** Dependencias del motor, inyectadas por constructor. */\nexport interface EngineDeps extends ApiClientConfig {\n\t/** Espera de `ms` milisegundos. Por defecto, `setTimeout`; inyecta un no-op para que los tests no esperen. */\n\tsleep?: (ms: number) => Promise<void>;\n\t/** Esperas y topes del render. Lo que no indiques se toma de {@link cxCore.DEFAULT_TIMINGS}. */\n\ttimings?: Partial<cxCore.RenderTimings>;\n\t/** Paginación del listado de páginas. Lo que no indiques se toma de {@link cxCore.DEFAULT_PAGE_LIST_QUERY}. */\n\tpageList?: Partial<cxCore.PageListQuery>;\n}\n\n/**\n * Instánciala una vez con la configuración del entorno y reutilízala:\n *\n * ```ts\n * const engine = new CxRenderEngine({ transport, apiUrl, authToken });\n * await engine.renderUntilLive([siteId], [\"Mi página\"]);\n * ```\n */\nexport class CxRenderEngine {\n\tprivate readonly api: ApiClient;\n\tprivate readonly logger: Logger;\n\tprivate readonly sleep: (ms: number) => Promise<void>;\n\tprivate readonly timings: cxCore.RenderTimings;\n\tprivate readonly pageList: cxCore.PageListQuery;\n\n\tconstructor(deps: EngineDeps) {\n\t\tthis.api = new ApiClient(deps);\n\t\tthis.logger = this.api.logger;\n\t\tthis.sleep = deps.sleep ?? defaultSleep;\n\t\tthis.timings = { ...cxCore.DEFAULT_TIMINGS, ...deps.timings };\n\t\tthis.pageList = { ...cxCore.DEFAULT_PAGE_LIST_QUERY, ...deps.pageList };\n\t}\n\n\t/**\n\t * Renderiza un site en **una sola pasada**: settle adaptativo → `build/start` → `GET page/{id}` →\n\t * `build/end`.\n\t *\n\t * El settle previo da tiempo a que el backend termine de propagar las copias de las páginas globales; sin él,\n\t * `build/start` devolvería una lista incompleta. Es ADAPTATIVO: sondea el listado hasta que dos sondas\n\t * consecutivas coinciden (quiescencia) o hasta el techo `timings.settleMs` — rápido cuando el backend va\n\t * suelto, paciente bajo carga. Las páginas que lleguen tarde no se reintentan aquí: de eso se encarga\n\t * {@link renderUntilLive}.\n\t *\n\t * ⚠️ `build/start` deja el site bloqueado y `build/end` lo libera. En el camino normal `build/end` se emite\n\t * siempre, pero si el transporte rechaza entre ambos el bloqueo queda colgado durante horas: llama a\n\t * {@link releaseRenderLock} en tu `finally`.\n\t *\n\t * @throws {RenderError} `BUILD_START_NOT_READY` si el site no llega a estar listo, `BUILD_END_FAILED` si el\n\t * render no se aplica.\n\t */\n\tasync renderSiteOnce(siteId: number): Promise<void> {\n\t\tconst startOnce = async (startWaits = 0): Promise<void> => {\n\t\t\t// Sin `expectOk`: un 4xx aquí puede ser transitorio y lo decide el núcleo, no es un fallo todavía.\n\t\t\tconst bs = await this.api.get(`site/${siteId}/build/start`);\n\t\t\tconst startStep = cxCore.decideBuildStartStep(bs.status, startWaits, this.timings);\n\t\t\tif (startStep.action === \"fail\") {\n\t\t\t\tthrow new RenderError(\n\t\t\t\t\tRENDER_ERROR_CODE.BUILD_START_NOT_READY,\n\t\t\t\t\t`GET site/${siteId}/build/start sigue en ${bs.status} tras ${startWaits} esperas (¿el site no acaba de asentarse?)`,\n\t\t\t\t\t{ status: bs.status, body: bs.body },\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (startStep.action === \"retry\") {\n\t\t\t\tthis.logger.debug(\n\t\t\t\t\t`build/start del site ${siteId} en ${bs.status}: espera ${startStep.waitMs}ms y reintenta`,\n\t\t\t\t);\n\t\t\t\tawait this.sleep(startStep.waitMs);\n\t\t\t\treturn startOnce(startWaits + 1);\n\t\t\t}\n\n\t\t\tconst siteHash: string | null = bs.body?.siteHash || null;\n\t\t\tconst publishIds: number[] = bs.body?.publishIds || [];\n\t\t\tconst unpublishHashes: string[] = bs.body?.unpublishHashes || [];\n\t\t\tthis.logger.debug(\n\t\t\t\t`site ${siteId}: ${publishIds.length} páginas candidatas, ${unpublishHashes.length} a despublicar`,\n\t\t\t);\n\n\t\t\t// El trabajo es toda página con hash, sin mirar su estado (ver classifyPage). Una sola pasada, así que no\n\t\t\t// hace falta deduplicar ids; las páginas que aún no tienen hash las recoge renderUntilLive.\n\t\t\tconst publishHashes: string[] = [];\n\t\t\tconst publishPagesIds: number[] = [];\n\t\t\tfor (const pageId of publishIds) {\n\t\t\t\tconst pres = await this.api.get(`page/${pageId}`);\n\t\t\t\tconst hash = pres.body?.hash;\n\t\t\t\tconst status = pres.body?.liveStatus?.id;\n\t\t\t\tif (cxCore.classifyPage(hash, status) === \"publish\") {\n\t\t\t\t\tpublishHashes.push(hash);\n\t\t\t\t\tpublishPagesIds.push(pageId);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait this.api.post(`site/${siteId}/build/end`, {\n\t\t\t\tbody: cxCore.buildEndBody(publishHashes, siteHash, unpublishHashes, publishPagesIds),\n\t\t\t\texpectOk: RENDER_ERROR_CODE.BUILD_END_FAILED,\n\t\t\t});\n\t\t\tthis.logger.info(`site ${siteId} renderizado: ${publishPagesIds.length} páginas publicadas`);\n\t\t};\n\n\t\t// Asentamiento primero, luego una sola pasada.\n\t\tawait this.settleBeforeStart(siteId);\n\t\treturn startOnce();\n\t}\n\n\t/**\n\t * Settle ADAPTATIVO previo al build/start: sondea el listado del site cada `timings.settlePollMs` y arranca\n\t * cuando dos sondas consecutivas devuelven la misma firma (`cxCore.pageListSignature`) o al agotar el techo\n\t * `timings.settleMs` (la decisión es de `cxCore.decideSettleStep`). Best-effort: si el listado falla durante\n\t * el settle, se procede al render en vez de morir aquí (peor caso = el de siempre, un pase incompleto que\n\t * recogen las rondas de `renderUntilLive`). Con `settleMs <= 0` no sondea (settle desactivado).\n\t */\n\tprivate async settleBeforeStart(siteId: number): Promise<void> {\n\t\tif (this.timings.settleMs <= 0) return;\n\t\ttry {\n\t\t\tlet prev: string | null = null;\n\t\t\tlet elapsed = 0;\n\t\t\tfor (;;) {\n\t\t\t\tconst sig = cxCore.pageListSignature(await this.listPages([siteId]));\n\t\t\t\tconst step = cxCore.decideSettleStep(prev, sig, elapsed, this.timings);\n\t\t\t\tif (step.action === \"go\") return;\n\t\t\t\tprev = sig;\n\t\t\t\tawait this.sleep(step.waitMs);\n\t\t\t\telapsed += step.waitMs;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.logger.warn(\n\t\t\t\t`settle del site ${siteId}: el sondeo del listado falló (${err instanceof Error ? err.message : String(err)}); se procede al render sin asentar`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Renderiza **todos los sites que tengan render pendiente**, que es lo que hace el render real en cada pasada:\n\t * pregunta al backend qué sites tienen cambios sin renderizar (`GET sites/all` → `shouldBeUpdated`) y los\n\t * resuelve uno por uno.\n\t *\n\t * @param onlyIds acota a esos sites. **Úsalo si compartes la base de datos con otros procesos**: sin acotar,\n\t * esta pasada arrastra también el trabajo pendiente de los demás, que es tan lento como caro.\n\t * @returns los ids que ha renderizado.\n\t * @throws {RenderError} `SITES_LOOKUP_FAILED` si no se puede consultar la lista de sites.\n\t */\n\tasync renderPendingSites(onlyIds?: number[]): Promise<number[]> {\n\t\tconst res = await this.api.get(\"sites/all\", {\n\t\t\texpectOk: RENDER_ERROR_CODE.SITES_LOOKUP_FAILED,\n\t\t});\n\t\tconst all: cxCore.SiteBuildInfo[] = Array.isArray(res.body) ? res.body : res.body?.items || [];\n\t\tconst pending = cxCore.selectPendingSites(all, onlyIds);\n\t\tthis.logger.info(\n\t\t\tpending.length > 0\n\t\t\t\t? `sites con render pendiente: ${pending.join(\", \")}`\n\t\t\t\t: \"ningún site con render pendiente\",\n\t\t);\n\t\tfor (const siteId of pending) await this.renderSiteOnce(siteId);\n\t\treturn pending;\n\t}\n\n\t/**\n\t * Lleva los títulos pedidos a su estado final, reintentando lo que falte.\n\t *\n\t * Mientras queden títulos pendientes, vuelve a marcar como pendientes las copias que se hayan quedado\n\t * despublicadas y renderiza otra vez, hasta `timings.maxRounds` rondas; la última confirma o lanza. Si todo\n\t * está ya en su sitio, la primera comprobación pasa y no renderiza nada.\n\t *\n\t * @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`; usa `LIVE_STATUS.UNPUBLISHED`\n\t * para confirmar una despublicación.\n\t * @throws {RenderError} `TITLES_NOT_FINAL` si tras la última ronda sigue faltando algún título.\n\t */\n\tasync renderUntilLive(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return;\n\n\t\tconst attempt = async (round: number): Promise<void> => {\n\t\t\tconst missing = await this.missingPagesByTitle(siteIds, wanted, expectedStatus);\n\t\t\tif (missing.length === 0) return;\n\t\t\tif (round >= this.timings.maxRounds)\n\t\t\t\treturn this.confirmPagesByTitle(siteIds, wanted, expectedStatus);\n\t\t\tthis.logger.warn(\n\t\t\t\t`ronda ${round}/${this.timings.maxRounds}: faltan ${missing.length}/${wanted.length} títulos (${missing.join(\", \")})`,\n\t\t\t);\n\t\t\tawait this.republishMissingWantedLive(siteIds, missing, expectedStatus);\n\t\t\tfor (const siteId of siteIds) await this.renderSiteOnce(siteId);\n\t\t\tawait this.sleep(this.timings.roundWaitMs);\n\t\t\treturn attempt(round + 1);\n\t\t};\n\t\treturn attempt(1);\n\t}\n\n\t/**\n\t * Comprueba, sin renderizar, que los títulos están en `expectedStatus`.\n\t *\n\t * @throws {RenderError} `TITLES_NOT_FINAL`, con los títulos que faltan en `missingTitles`.\n\t */\n\tasync confirmPagesByTitle(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return;\n\t\tconst missing = await this.missingPagesByTitle(siteIds, wanted, expectedStatus);\n\t\tif (missing.length > 0) {\n\t\t\tthrow new RenderError(\n\t\t\t\tRENDER_ERROR_CODE.TITLES_NOT_FINAL,\n\t\t\t\t`estos títulos no están en liveStatus ${expectedStatus}: ${missing.join(\", \")}`,\n\t\t\t\t{\n\t\t\t\t\tmissingTitles: missing,\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Libera el bloqueo de render que deja `build/start`, emitiendo un `build/end` vacío.\n\t *\n\t * Llámalo en tu `finally` si un render puede abortar a medias. Es best-effort: no lanza aunque el backend\n\t * responda con error, porque el caso normal es que ya no hubiera nada que liberar.\n\t */\n\tasync releaseRenderLock(siteId: number): Promise<void> {\n\t\tconst res = await this.api.post(`site/${siteId}/build/end`, {\n\t\t\tbody: cxCore.buildEndBody([], null, [], []),\n\t\t});\n\t\tif (res.status >= 400) {\n\t\t\tthis.logger.debug(\n\t\t\t\t`releaseRenderLock del site ${siteId}: el backend respondió ${res.status} (probablemente no había bloqueo)`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Títulos que aún no están en `expectedStatus`, leyendo el listado completo de cada site. */\n\tprivate async missingPagesByTitle(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number,\n\t): Promise<string[]> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return [];\n\t\tconst items = await this.listPages(siteIds);\n\t\treturn cxCore.diffMissingByTitle(items, wanted, expectedStatus);\n\t}\n\n\t/** Devuelve a \"pendiente de publicar\" las copias despublicadas de los títulos que faltan. */\n\tprivate async republishMissingWantedLive(\n\t\tsiteIds: number[],\n\t\tmissing: string[],\n\t\texpectedStatus: number,\n\t): Promise<void> {\n\t\tif (expectedStatus !== cxCore.LIVE_STATUS.LIVE || missing.length === 0) return;\n\t\tconst items = await this.listPages(siteIds);\n\t\tconst uniqueIds = cxCore.collectRepublishIds(items, missing);\n\t\tif (uniqueIds.length === 0) return;\n\t\tthis.logger.info(`re-marcando ${uniqueIds.length} páginas como pendientes de publicar`);\n\t\tawait this.api.put(\"pages/status/upload-pending\", { body: { ids: uniqueIds } });\n\t}\n\n\t/**\n\t * Páginas de los sites indicados, **paginando hasta el final** del listado.\n\t *\n\t * Recorre páginas mientras el backend devuelva el lote completo: si llega uno más corto, es el último. Así un\n\t * site con más páginas que `pageList.itemsPerPage` no se queda a medias, que era la causa de confirmaciones\n\t * por título erróneas cuando solo se leía la primera página.\n\t *\n\t * @throws {RenderError} `PAGE_LIST_FAILED` si el backend no puede devolver el listado.\n\t */\n\tprivate async listPages(siteIds: number[]): Promise<cxCore.PageInfo[]> {\n\t\tconst items: cxCore.PageInfo[] = [];\n\t\tfor (const siteId of siteIds) {\n\t\t\tlet page = this.pageList.page;\n\t\t\t// Tope de seguridad: con el itemsPerPage por defecto son 300.000 páginas. Si se alcanza, algo va mal en\n\t\t\t// el backend (por ejemplo, ignora el parámetro `page` y devuelve siempre el mismo lote).\n\t\t\tfor (let fetched = 0; fetched < MAX_LIST_PAGES; fetched++) {\n\t\t\t\tconst query = { page, itemsPerPage: this.pageList.itemsPerPage };\n\t\t\t\tconst res = await this.api.get(cxCore.listPageUrl(this.api.url(\"\"), siteId, query), {\n\t\t\t\t\texpectOk: RENDER_ERROR_CODE.PAGE_LIST_FAILED,\n\t\t\t\t});\n\t\t\t\tconst batch: cxCore.PageInfo[] = (res.body && (res.body.items || res.body)) || [];\n\t\t\t\titems.push(...batch);\n\t\t\t\tif (batch.length < this.pageList.itemsPerPage) break;\n\t\t\t\tpage += 1;\n\t\t\t\tif (fetched === MAX_LIST_PAGES - 1) {\n\t\t\t\t\tthis.logger.warn(\n\t\t\t\t\t\t`listado del site ${siteId} cortado en ${MAX_LIST_PAGES} páginas: ¿el backend ignora el parámetro page?`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn items;\n\t}\n}\n\n/** Tope de páginas que se piden por site antes de asumir que el backend no pagina bien. */\nconst MAX_LIST_PAGES = 1000;\n","/**\n * Publicación: marcar páginas como pendientes y publicar sites.\n *\n * Ojo con el reparto de responsabilidades: **esto no es parte del render**. En producción lo dispara la persona\n * que pulsa \"Publicar\" en el CMS; el render solo reacciona a lo que ya está pendiente. Vive en este paquete como\n * conveniencia para montar el escenario desde un script, no porque el render lo necesite.\n *\n * El orden importa: primero se marcan las páginas, luego se publica el site. Publicar es lo que hace que el\n * backend las incluya en el siguiente `build/start`.\n */\n\nimport type { Logger } from \"./logger\";\n\nimport { ApiClient, type ApiClientConfig } from \"./api-client\";\nimport { RENDER_ERROR_CODE } from \"./errors\";\n\n/** Dependencias del publicador, inyectadas por constructor. Las mismas que el motor necesita para hablar con la API. */\nexport type PublisherDeps = ApiClientConfig;\n\n/**\n * Dispara la publicación en el CMS. Complementa al motor de render: este deja el trabajo pendiente, el motor lo\n * resuelve.\n *\n * ```ts\n * const publisher = new CmsPublisher({ transport, apiUrl, authToken });\n * await publisher.publishPages([siteId], [pageId]);\n * await engine.renderUntilLive([siteId], [\"Mi página\"]);\n * ```\n */\nexport class CmsPublisher {\n\tprivate readonly api: ApiClient;\n\tprivate readonly logger: Logger;\n\n\tconstructor(deps: PublisherDeps) {\n\t\tthis.api = new ApiClient(deps);\n\t\tthis.logger = this.api.logger;\n\t}\n\n\t/**\n\t * Marca páginas como pendientes de publicar. Sin ids no hace nada.\n\t *\n\t * @throws {RenderError} `MARK_PENDING_FAILED` si el backend responde >= 400.\n\t */\n\tasync markPagesPending(pageIds: number[]): Promise<void> {\n\t\tif (!pageIds || pageIds.length === 0) return;\n\t\tawait this.api.put(\"pages/status/upload-pending\", {\n\t\t\tbody: { ids: pageIds },\n\t\t\texpectOk: RENDER_ERROR_CODE.MARK_PENDING_FAILED,\n\t\t});\n\t\tthis.logger.info(`${pageIds.length} páginas marcadas como pendientes de publicar`);\n\t}\n\n\t/**\n\t * Publica los sites indicados, uno por uno.\n\t *\n\t * @throws {RenderError} `PUBLISH_FAILED` si el backend responde >= 400 para alguno.\n\t */\n\tasync publishSites(siteIds: number[]): Promise<void> {\n\t\tfor (const siteId of siteIds || []) {\n\t\t\tawait this.api.post(`site/${siteId}/publish`, { expectOk: RENDER_ERROR_CODE.PUBLISH_FAILED });\n\t\t\tthis.logger.info(`site ${siteId} publicado`);\n\t\t}\n\t}\n\n\t/**\n\t * Los dos pasos en orden: marca las páginas y publica los sites. Es el disparo completo que deja el trabajo\n\t * listo para que el motor lo renderice.\n\t */\n\tasync publishPages(siteIds: number[], pageIds: number[]): Promise<void> {\n\t\tawait this.markPagesPending(pageIds);\n\t\tawait this.publishSites(siteIds);\n\t}\n}\n","/**\n * Fachada: construye motor y publicador con una sola configuración, y encapsula el flujo completo.\n *\n * Existe por dos motivos concretos, no por simetría: instanciar las dos clases obliga a repetir las mismas tres\n * dependencias, y el flujo tiene un orden que hay que conocer (publicar → renderizar → confirmar, liberando el\n * bloqueo si algo se rompe a medias). Si necesitas solo una parte, usa `CxRenderEngine` o `CmsPublisher`\n * directamente: siguen expuestos.\n */\nimport * as cxCore from \"./core\";\nimport { CxRenderEngine, type EngineDeps } from \"./engine\";\nimport { CmsPublisher } from \"./publish\";\n\n/** Configuración de la fachada: la del motor, que es un superconjunto de la del publicador. */\nexport type CxRenderMockDeps = EngineDeps;\n\n/**\n * Punto de entrada recomendado.\n *\n * ```ts\n * const cx = new CxRenderMock({ transport, apiUrl, authToken, logger });\n * await cx.publishAndRender([siteId], [pageId], [\"Mi página\"]);\n * ```\n */\nexport class CxRenderMock {\n\t/** El motor de render, por si necesitas sus métodos sueltos. */\n\treadonly engine: CxRenderEngine;\n\t/** El disparo de publicación, por si quieres publicar sin renderizar. */\n\treadonly publisher: CmsPublisher;\n\n\tconstructor(deps: CxRenderMockDeps) {\n\t\tthis.engine = new CxRenderEngine(deps);\n\t\tthis.publisher = new CmsPublisher(deps);\n\t}\n\n\t/**\n\t * El flujo entero: publica, renderiza y confirma que los títulos llegaron a su estado final. Libera el bloqueo\n\t * de render de cada site si algo falla a medias.\n\t *\n\t * @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`.\n\t * @throws {RenderError} con el `code` de la etapa que falló.\n\t */\n\tasync publishAndRender(\n\t\tsiteIds: number[],\n\t\tpageIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tawait this.publisher.publishPages(siteIds, pageIds);\n\t\ttry {\n\t\t\tawait this.engine.renderUntilLive(siteIds, titles, expectedStatus);\n\t\t} catch (error) {\n\t\t\t// El bloqueo lo pone build/start y lo quita build/end. Si el render murió entre ambos, aquí es donde se\n\t\t\t// suelta; si ya no había bloqueo, releaseRenderLock no protesta.\n\t\t\tfor (const siteId of siteIds) await this.engine.releaseRenderLock(siteId);\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n","/**\n * Contrato del transporte HTTP y decoradores opcionales para envolverlo.\n *\n * El transporte es la única dependencia de runtime del paquete, y se inyecta: así no arrastra ningún cliente\n * HTTP y funciona igual en Node, en un bundler o dentro de un runner de tests. Los decoradores son funciones que\n * reciben un transporte y devuelven otro con el mismo contrato, de modo que reintentos, timeouts o trazas se\n * añaden componiendo, sin que el motor sepa nada.\n */\nimport { type Logger, NOOP_LOGGER } from \"./logger\";\nimport { defaultSleep } from \"./sleep\";\n\n/** Respuesta HTTP normalizada: status y body ya parseado. */\nexport interface TransportResponse {\n\tstatus: number;\n\tbody: any;\n}\n\n/** Opciones de una request: cabeceras y body ya serializable. */\nexport interface RequestOptions {\n\theaders?: Record<string, string>;\n\tbody?: unknown;\n}\n\n/**\n * Transporte HTTP inyectable. Impleméntalo sobre `fetch`, `axios`, `undici` o lo que uses.\n *\n * ⚠️ **No debe lanzar en 4xx/5xx**: devuelve `{ status, body }` y deja que el motor decida. Es deliberado: un\n * 4xx de `build/start` es una respuesta esperada (la ruta del site aún no está lista), no un fallo. Un cliente\n * que lance en error — como los que devuelven el body deserializado y tiran en no-2xx — necesita un adaptador\n * que capture y traduzca a `{ status, body }`.\n */\nexport interface Transport {\n\tget(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n\tpost(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n\tput(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n}\n\n/** Métodos que un decorador puede tratar de forma distinta. */\nexport type TransportMethod = \"get\" | \"post\" | \"put\";\n\n/** Ajustes de {@link withRetry}. */\nexport interface RetryOptions {\n\t/** Intentos totales, incluido el primero. Por defecto 3. */\n\tattempts?: number;\n\t/** Espera entre intentos (ms). Por defecto 500, y crece linealmente con el intento. */\n\tdelayMs?: number;\n\t/**\n\t * Métodos que se reintentan. Por defecto **solo `get`**.\n\t *\n\t * No es timidez: `build/start` toma un bloqueo del site y `build/end` lo libera, así que repetir a ciegas una\n\t * escritura puede dejar el protocolo a medias. Amplíalo solo si sabes que tu caso lo tolera.\n\t */\n\tmethods?: TransportMethod[];\n\t/** Qué respuesta merece otro intento. Por defecto, los 5xx (un 4xx no se arregla repitiendo). */\n\tshouldRetry?: (res: TransportResponse) => boolean;\n\t/** Espera inyectable, para tests instantáneos. */\n\tsleep?: (ms: number) => Promise<void>;\n\tlogger?: Logger;\n}\n\n/**\n * Envuelve un transporte para que reintente los fallos pasajeros: una respuesta 5xx o un rechazo del cliente\n * (red caída, DNS, socket cerrado). Agotados los intentos, deja pasar la última respuesta o relanza el error,\n * para que quien llama decida.\n *\n * ```ts\n * const transport = withRetry(myTransport, { attempts: 4 });\n * ```\n */\nexport const withRetry = (transport: Transport, opts: RetryOptions = {}): Transport => {\n\tconst attempts = opts.attempts ?? 3;\n\tconst delayMs = opts.delayMs ?? 500;\n\tconst methods = opts.methods ?? [\"get\"];\n\tconst shouldRetry = opts.shouldRetry ?? ((res: TransportResponse) => res.status >= 500);\n\tconst sleep = opts.sleep ?? defaultSleep;\n\tconst logger = opts.logger ?? NOOP_LOGGER;\n\n\tconst run = async (\n\t\tmethod: TransportMethod,\n\t\turl: string,\n\t\treqOpts?: RequestOptions,\n\t): Promise<TransportResponse> => {\n\t\tif (!methods.includes(method)) return transport[method](url, reqOpts);\n\n\t\tlet lastError: unknown;\n\t\tfor (let attempt = 1; attempt <= attempts; attempt++) {\n\t\t\ttry {\n\t\t\t\tconst res = await transport[method](url, reqOpts);\n\t\t\t\tif (attempt === attempts || !shouldRetry(res)) return res;\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`retry ${method.toUpperCase()} ${url}: status ${res.status}, intento ${attempt}/${attempts}`,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tlastError = error;\n\t\t\t\tif (attempt === attempts) throw error;\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`retry ${method.toUpperCase()} ${url}: el transporte falló, intento ${attempt}/${attempts}`,\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait sleep(delayMs * attempt);\n\t\t}\n\t\t// Inalcanzable: el último intento sale por return o por throw. Está por si `attempts` llega como 0.\n\t\tthrow (\n\t\t\tlastError ??\n\t\t\tnew Error(`withRetry: ningún intento ejecutado para ${method.toUpperCase()} ${url}`)\n\t\t);\n\t};\n\n\treturn {\n\t\tget: (url, reqOpts) => run(\"get\", url, reqOpts),\n\t\tpost: (url, reqOpts) => run(\"post\", url, reqOpts),\n\t\tput: (url, reqOpts) => run(\"put\", url, reqOpts),\n\t};\n};\n\n/** Ajustes de {@link withTimeout}. */\nexport interface TimeoutOptions {\n\t/** Milisegundos antes de dar la request por perdida. */\n\tms: number;\n\tlogger?: Logger;\n}\n\n/**\n * Envuelve un transporte para que ninguna request se quede colgada para siempre: pasado el plazo, rechaza.\n *\n * ⚠️ **No cancela la request subyacente**, solo deja de esperarla: el paquete no conoce tu cliente HTTP, así que\n * no puede abortarlo. Si necesitas cancelación real, implementa el plazo dentro de tu propio transporte con un\n * `AbortController`.\n */\nexport const withTimeout = (transport: Transport, opts: TimeoutOptions): Transport => {\n\tconst logger = opts.logger ?? NOOP_LOGGER;\n\n\tconst race = (\n\t\tmethod: TransportMethod,\n\t\turl: string,\n\t\treqOpts?: RequestOptions,\n\t): Promise<TransportResponse> => {\n\t\tlet timer: ReturnType<typeof setTimeout>;\n\t\tconst timeout = new Promise<never>((_resolve, reject) => {\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tlogger.warn(`timeout ${method.toUpperCase()} ${url}: sin respuesta en ${opts.ms}ms`);\n\t\t\t\treject(new Error(`Timeout de ${opts.ms}ms en ${method.toUpperCase()} ${url}`));\n\t\t\t}, opts.ms);\n\t\t});\n\t\treturn Promise.race([transport[method](url, reqOpts), timeout]).finally(() =>\n\t\t\tclearTimeout(timer),\n\t\t);\n\t};\n\n\treturn {\n\t\tget: (url, reqOpts) => race(\"get\", url, reqOpts),\n\t\tpost: (url, reqOpts) => race(\"post\", url, reqOpts),\n\t\tput: (url, reqOpts) => race(\"put\", url, reqOpts),\n\t};\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/core.ts","../src/errors.ts","../src/logger.ts","../src/api-client.ts","../src/sleep.ts","../src/engine.ts","../src/publish.ts","../src/facade.ts","../src/transport.ts"],"sourcesContent":["/**\n * Punto de entrada público de `@griddo/cx-render-mock`.\n *\n * Los exports son explícitos a propósito: lo que el paquete publica es una decisión, no el efecto secundario de\n * un `export *`. Si añades algo interno (como `./api-client`), no aparece aquí y por tanto no es API pública.\n */\n\n// Núcleo: constantes, tipos del contrato de la API y las decisiones puras, para quien orqueste por su cuenta.\nexport {\n\ttype BuildEndBody,\n\ttype BuildStartDecision,\n\tbuildEndBody,\n\tclassifyPage,\n\tcollectRepublishIds,\n\tDEFAULT_PAGE_LIST_QUERY,\n\tDEFAULT_TIMINGS,\n\tdecideBuildStartStep,\n\tdecideSettleStep,\n\tdiffMissingByTitle,\n\tisBuildStartSkipped,\n\tLIVE_STATUS,\n\ttype LiveStatus,\n\tlistPageUrl,\n\ttype PageClassification,\n\ttype PageInfo,\n\ttype PageListQuery,\n\tpageListSignature,\n\ttype RenderTimings,\n\ttype SettleDecision,\n\ttype SiteBuildInfo,\n\tselectPendingSites,\n\twithPendingHashes,\n} from \"./core\";\n// El protocolo de render y el disparo de publicación, por separado.\nexport { CxRenderEngine, type EngineDeps } from \"./engine\";\n// Errores: un único tipo con código.\nexport {\n\tRENDER_ERROR_CODE,\n\tRenderError,\n\ttype RenderErrorCode,\n\ttype RenderErrorDetail,\n} from \"./errors\";\n// Fachada: motor + publicador con una sola configuración. El punto de partida recomendado.\nexport { CxRenderMock, type CxRenderMockDeps } from \"./facade\";\n// Log opcional.\nexport { createConsoleLogger, type Logger, NOOP_LOGGER } from \"./logger\";\nexport { CmsPublisher, type PublisherDeps } from \"./publish\";\n// Transporte: el contrato que implementas y los decoradores para envolverlo.\nexport {\n\ttype RequestOptions,\n\ttype RetryOptions,\n\ttype TimeoutOptions,\n\ttype Transport,\n\ttype TransportMethod,\n\ttype TransportResponse,\n\twithRetry,\n\twithTimeout,\n} from \"./transport\";\n","/**\n * Núcleo PURO del test-double de render de CX — agnóstico de Cypress en VALOR y en TIPO.\n *\n * Contiene las decisiones difíciles del protocolo (clasificación por hash, diff por título, gate de\n * build/start, shaping del body de build/end, colección de re-pending de la mitigación cross-job, y los knobs\n * por defecto del render de un solo pase) SIN acoplarse a la cola de comandos de Cypress: cero `cy`/`Cypress`/`expect`/`window`,\n * cero `import ... 'cypress'`, y ninguna firma devuelve `Cypress.Chainable` (solo tipos neutrales). Es el\n * artefacto compartible: el shell Cypress de griddo-qa (`cypress/support/cx-render.ts`) y el motor de Promises\n * del paquete (`./engine`) consumen ESTE módulo, de modo que el racional que causa los bugs vive en un único\n * sitio (functional-core / imperative-shell). Ver `docs/cx-render-packaging-feasibility.md` §5/§6.\n *\n * Behavior-preserving: este código se EXTRAJO de `cx-render.ts` sin cambiar su comportamiento observable;\n * las constantes, umbrales y predicados son idénticos a los del shell original.\n */\n\n/**\n * Modelo de live_status: data-driven en la BD pero ESTABLE por orden de seed (contrato code-grounded e2e-202 →\n * griddo-api CONFIG/LiveStatusSeeder). El double no puede leer los flags de CONFIG en runtime → usa estos ids\n * verificados.\n */\nexport const LIVE_STATUS = {\n\tUNPUBLISHED: 1, // isUnpublished / offline — estado final de despublicación\n\tPENDING_PUBLISHING: 2, // isPendingPublishing / upload-pending — transitorio\n\tLIVE: 3, // isLiveStatus / active — estado final de publicación\n\tPENDING_UNPUBLISHING: 4, // isPendingUnpublishing / offline-pending — transitorio\n} as const;\n\n/** Los cuatro ids de live_status del contrato (1/2/3/4). */\nexport type LiveStatus = (typeof LIVE_STATUS)[keyof typeof LIVE_STATUS];\n\n/**\n * Forma mínima de una página tal como la exponen los endpoints que consume el double: `GET page/{id}` (trae\n * `hash` + `liveStatus`) y `site/{id}/pages?format=list` (trae `id` + `title` + `liveStatus`). Todos los\n * campos son opcionales porque el body real de la API puede venir incompleto y el core no debe asumir.\n */\nexport interface PageInfo {\n\tid?: number;\n\ttitle?: string;\n\thash?: string | null;\n\tliveStatus?: { id?: number } | null;\n}\n\n/** Cuerpo de `POST site/{id}/build/end` (lo que el CX real envía para resolver los transitorios). */\nexport interface BuildEndBody {\n\tpublishHashes: string[];\n\tsiteHash: string | null;\n\tunpublishHashes: string[];\n\tpublishPagesIds: number[];\n}\n\n// --- Esperas, topes y paginación (todo ajustable por el consumidor) -----------------------------------------\n// Antes eran constantes fijas del módulo. Ahora son datos que el consumidor puede sustituir: cada entorno tiene\n// su latencia (CI saturado ≠ local), y quien usa el paquete la conoce mejor que el paquete. Los defaults\n// (medidos) siguen aquí, y el shell Cypress los sobreescribe por `Cypress.env`.\n\n/** Esperas y topes del render. Todos sustituibles; los defaults están en `DEFAULT_TIMINGS`. */\nexport interface RenderTimings {\n\t/**\n\t * TECHO (ms) del settle ADAPTATIVO previo al ÚNICO build/start del render de un solo pase. El settle espera\n\t * al spread ASÍNCRONO de las copias GLOBAL, pero ya NO es un sleep fijo: se sondea el listado del site hasta\n\t * que dos sondas consecutivas devuelven la misma firma (`pageListSignature` → `decideSettleStep`), con este\n\t * valor como tope. Racional: una constante fija no cubre una latencia variable — a 15s el pase único perdía\n\t * rezagados bajo carga de CI (renderFlow2/002 agotaba los 3 intentos) y a 30-45s se paga tiempo muerto por\n\t * site y por ronda en el 90% de renders que asientan en segundos. `<= 0` desactiva el settle por completo.\n\t */\n\tsettleMs: number;\n\t/** Intervalo (ms) entre sondas del settle adaptativo. */\n\tsettlePollMs: number;\n\t/**\n\t * Rondas máximas del orquestador auto-sanador `renderUntilLive`. NO baja a 1: con 1 quedaban sets ENTEROS\n\t * not-Live en CI (el build/start del mock no captura TODAS las páginas en un pase; las rondas\n\t * re-pend+re-render sí).\n\t */\n\tmaxRounds: number;\n\t/** Wait entre rondas del orquestador auto-sanador (ms). */\n\troundWaitMs: number;\n\t/** Reintentos máximos de build/start mientras dé 4xx transitorio (~30s con el retry por defecto). */\n\tbuildStartMaxWaits: number;\n\t/** Backoff entre reintentos de build/start en 4xx transitorio (ms). */\n\tbuildStartRetryWaitMs: number;\n}\n\n/** Defaults medidos de `RenderTimings`. El shell los sobreescribe por `Cypress.env`. */\nexport const DEFAULT_TIMINGS: RenderTimings = {\n\tsettleMs: 45000,\n\tsettlePollMs: 3000,\n\tmaxRounds: 3,\n\troundWaitMs: 1500,\n\tbuildStartMaxWaits: 15,\n\tbuildStartRetryWaitMs: 2000,\n};\n\n/** Paginación con la que se pide el listado de páginas de un site. Defaults en `DEFAULT_PAGE_LIST_QUERY`. */\nexport interface PageListQuery {\n\tpage: number;\n\titemsPerPage: number;\n}\n\n/** Defaults de `PageListQuery`. */\nexport const DEFAULT_PAGE_LIST_QUERY: PageListQuery = { page: 1, itemsPerPage: 300 };\n\n/**\n * URL del listado del site (`site/{id}/pages?format=list`) que leen el diff por título y el re-pending.\n * Centraliza la query string y la paginación — pura (string → string), sin acoplarse a Cypress (recibe\n * `apiUrl` como argumento). ⚠️ Limitación HEREDADA: pide UNA sola página (300 ítems por defecto); un site con\n * más páginas daría confirmación por título errónea. Se mitiga subiendo `itemsPerPage`, pero no pagina.\n */\nexport const listPageUrl = (\n\tapiUrl: string,\n\tsiteId: number,\n\tquery: PageListQuery = DEFAULT_PAGE_LIST_QUERY,\n): string =>\n\t`${apiUrl}site/${siteId}/pages?deleted=false&page=${query.page}&itemsPerPage=${query.itemsPerPage}&format=list`;\n\n/**\n * Firma ESTABLE del estado render-relevante de un listado de páginas: `id:liveStatus:hash` por página, ordenado\n * por id (el orden del listado puede variar entre sondas sin que el estado haya cambiado). Dos firmas iguales en\n * sondas consecutivas = el backend ha dejado de mover estados/hashes (spread y recálculos asentados) → el\n * build/start ya puede capturar el trabajo completo en un pase. Pura (datos → string), para el settle adaptativo.\n */\nexport const pageListSignature = (items: PageInfo[]): string =>\n\t(items || [])\n\t\t.filter((p) => p && p.id != null)\n\t\t.map((p) => `${p.id}:${p.liveStatus?.id ?? \"\"}:${p.hash ?? \"\"}`)\n\t\t.sort()\n\t\t.join(\"|\");\n\n/**\n * Completa un listado (`site/{id}/pages?format=list`, que NO trae `hash`) con los hashes reales de las páginas\n * pendientes de publicar (`liveStatus` 2), para que `pageListSignature` vea los cambios de hash.\n *\n * Por qué: la cascada de la API (DEPENDENT_PAGES_UPDATER) puede dejar una página en 2 con un hash NUEVO. En el\n * listado la página sigue siendo `id:2:` (sin hash), así que la firma no cambia y el settle da quiescencia con\n * la cascada aún activa. El shell lee `GET page/{id}` de las pendientes y pasa aquí `{ [id]: hash }`.\n *\n * - Solo se inyecta en páginas con `liveStatus.id === LIVE_STATUS.PENDING_PUBLISHING` y con su id presente en\n * `hashesById`; el resto queda tal cual (un hash aportado para una no pendiente no altera su firma).\n * - Un valor `null` se respeta (hash ausente), y se distingue de un hash presente en la firma.\n * - Retrocompatible: sin `hashesById` (o vacío) devuelve las mismas páginas, y la firma es idéntica a la de 1.0.0.\n * - Pura: devuelve copias de las pendientes tocadas y no muta la entrada. Tolera `items` ausente (→ `[]`).\n */\nexport const withPendingHashes = (\n\titems: PageInfo[],\n\thashesById?: Readonly<Record<number, string | null>>,\n): PageInfo[] =>\n\t(items || []).map((p) =>\n\t\tp &&\n\t\tp.id != null &&\n\t\thashesById &&\n\t\tp.liveStatus?.id === LIVE_STATUS.PENDING_PUBLISHING &&\n\t\tp.id in hashesById\n\t\t\t? { ...p, hash: hashesById[p.id] }\n\t\t\t: p,\n\t);\n\n/**\n * ¿`GET site/{id}/build/start` ha sido OMITIDO? La API responde `{ skipped: true, publishIds: [] }` cuando otro\n * render tiene el site reclamado (griddo-api `models/sites/index.ts`, `startBuild`). Solo `skipped === true`\n * (booleano estricto) cuenta: un body ausente, no objeto, o con `skipped` de otro tipo → `false`. Pura. La\n * política ante un skipped (reintentar o no) NO se decide aquí: el core solo lo detecta.\n */\nexport const isBuildStartSkipped = (body: unknown): boolean =>\n\ttypeof body === \"object\" && body !== null && (body as { skipped?: unknown }).skipped === true;\n\n/** Decisión del settle adaptativo (unión discriminada: `wait` porta su `waitMs`). */\nexport type SettleDecision = { action: \"go\" } | { action: \"wait\"; waitMs: number };\n\n/**\n * Gate del settle ADAPTATIVO: decide si el backend ya está quiescente y el render puede arrancar.\n * - dos sondas consecutivas con la MISMA firma → `'go'` (quiescencia: nada se ha movido en un intervalo);\n * - `elapsedMs >= timings.settleMs` → `'go'` (techo: bajo carga patológica no se espera para siempre);\n * - resto → `'wait'` con `min(settlePollMs, settleMs - elapsedMs)` (el último wait no desborda el techo).\n * La PRIMERA sonda (`prevSignature = null`) nunca decide `'go'` por firma: hacen falta dos muestras para saber\n * que nada cambia. Con `settleMs <= 0` decide `'go'` inmediato (settle desactivado; el caller puede además\n * ahorrarse la sonda).\n */\nexport const decideSettleStep = (\n\tprevSignature: string | null,\n\tsignature: string,\n\telapsedMs: number,\n\ttimings: RenderTimings = DEFAULT_TIMINGS,\n): SettleDecision => {\n\tif (elapsedMs >= timings.settleMs) return { action: \"go\" };\n\tif (prevSignature !== null && prevSignature === signature) return { action: \"go\" };\n\treturn { action: \"wait\", waitMs: Math.min(timings.settlePollMs, timings.settleMs - elapsedMs) };\n};\n\n/** Clasificación de una página según su `hash`/`status`, resultado de `classifyPage`. */\nexport type PageClassification = \"publish\" | \"needsHash\" | \"skip\";\n\n/**\n * Clasifica una página por su HASH, como el CX real (griddo-cx store.ts:366-369): el TRABAJO = toda página\n * con `hash != null`, SIN filtrar por estado. `build/end` publica el structured_data_content (has_been_published,\n * que la API pública EXIGE vía includePendingPublishing:off) SOLO de los publishPagesIds/publishHashes que\n * recibe; una copia GLOBAL/traducción puede alcanzar live(3) con su CONTENIDO aún sin publicar → si la\n * SALTÁRAMOS por estar live(3) (como hacía el filtro `status===2` anterior), su contenido NUNCA se publicaría\n * y la pública la excluiría (bug de traducciones en CI). Por eso:\n * - `hash != null` → `'publish'` (se publica su contenido aunque ya esté live(3); el render es un solo pase);\n * - `hash == null` y `status === PENDING_PUBLISHING` → `'needsHash'` (el spread aún no asignó el hash);\n * - resto (p.ej. live(3) con hash=null = ya publicada y con el hash nulado por endBuild) → `'skip'`.\n */\nexport const classifyPage = (\n\thash: string | null | undefined,\n\tstatus: number | undefined,\n): PageClassification => {\n\tif (hash != null) return \"publish\";\n\tif (status === LIVE_STATUS.PENDING_PUBLISHING) return \"needsHash\";\n\treturn \"skip\";\n};\n\n/**\n * Diff SOFT por TÍTULO: dada la lista de páginas de los sites, devuelve los `wanted` que AÚN no están en\n * `expectedStatus` (ausentes del listado o en otro estado). Se compara POR TÍTULO y no por id porque publicar/\n * re-renderizar una GLOBAL canónica REASIGNA el id (memoria publish-canonical-global-page-reassigns-id) → un id\n * de createSuite (o de un build/start previo) queda stale; el listado del site es la entidad renderizada real.\n *\n * Preferencia `statusByTitle`: prefiere `expectedStatus` si alguna entidad con ese título ya lo tiene (una\n * global puede tener EDITOR + copia con distinto estado). El resultado (títulos que faltan) depende solo de si\n * ALGUNA entidad con ese título alcanzó `expectedStatus`.\n */\nexport const diffMissingByTitle = (\n\titems: PageInfo[],\n\twanted: string[],\n\texpectedStatus: number,\n): string[] => {\n\tconst w = wanted || [];\n\tif (w.length === 0) return [];\n\tconst statusByTitle: Record<string, number> = {};\n\titems.forEach((p) => {\n\t\tif (p && p.title != null && w.includes(p.title)) {\n\t\t\tconst st = p.liveStatus?.id;\n\t\t\tif (st === expectedStatus || !(p.title in statusByTitle)) statusByTitle[p.title] = st ?? -1;\n\t\t}\n\t});\n\treturn w.filter((t) => statusByTitle[t] !== expectedStatus);\n};\n\n/** Shaping del cuerpo de `POST site/{id}/build/end` (normaliza `siteHash` ausente a `null`). */\nexport const buildEndBody = (\n\tpublishHashes: string[],\n\tsiteHash: string | null | undefined,\n\tunpublishHashes: string[],\n\tpublishPagesIds: number[],\n): BuildEndBody => ({\n\tpublishHashes,\n\tsiteHash: siteHash ?? null,\n\tunpublishHashes,\n\tpublishPagesIds,\n});\n\n/**\n * Colección de re-pending de la MITIGACIÓN cross-job: de las páginas del listado, devuelve los ids in-site\n * (deduplicados, en orden de aparición) de los títulos `missing` que NO están live. El barrido global\n * INCONDICIONAL de endBuild (buildGlobalUnpublishAllWhere) puede despublicar globales QA de OTRO job → una\n * página offline ya NO la recoge build/start, así que re-renderizar no la recupera; re-pendearla (upload-pending)\n * la devuelve a pending(2) para que renderSiteToQuiescence la renderice. Acotado a copias in-site de `wanted`\n * (el caller solo pasa los títulos deseados-live que faltan) y solo `st !== LIVE`.\n */\nexport const collectRepublishIds = (items: PageInfo[], missing: string[]): number[] => {\n\tconst ids: number[] = [];\n\titems.forEach((p) => {\n\t\tconst st = p?.liveStatus?.id;\n\t\t// `p.title as string`: replica EXACTA de HEAD (`missing.includes(p.title)`, sin guard) — behavior-preserving\n\t\t// estricto; el cast solo satisface a tsc (items es PageInfo, no `any`), no cambia el runtime.\n\t\tif (p && p.id != null && missing.includes(p.title as string) && st !== LIVE_STATUS.LIVE)\n\t\t\tids.push(p.id);\n\t});\n\treturn Array.from(new Set(ids));\n};\n\n/**\n * Decisión del gate build/start-ready (unión discriminada: `retry` porta su `waitMs`, `proceed`/`fail` no).\n */\nexport type BuildStartDecision =\n\t| { action: \"proceed\" }\n\t| { action: \"retry\"; waitMs: number }\n\t| { action: \"fail\" };\n\n/**\n * Gate build/start-ready: replica el timing del CX real (que dispara ~1 min después, cuando el pathing ya\n * asentó, y nunca ve el 4xx transitorio) sin su latencia. El publish lanza el spread de las copias GLOBAL de\n * forma ASÍNCRONA; hasta que termina, una copia puede existir con `pathId=null` y build/start (que corre\n * fixAllPages → `pathId.split(...)`) devuelve 4xx TRANSITORIO. Por eso reintentamos SOLO mientras dé 4xx,\n * cortando al 1er 200; si agota el presupuesto, el shell asserta RUIDOSO (no un CypressError crudo).\n * - `status < 400` → `'proceed'`;\n * - agotado el presupuesto (`startWaits >= timings.buildStartMaxWaits`) → `'fail'`;\n * - resto → `'retry'` con backoff `timings.buildStartRetryWaitMs`.\n */\nexport const decideBuildStartStep = (\n\tstatus: number,\n\tstartWaits: number,\n\ttimings: RenderTimings = DEFAULT_TIMINGS,\n): BuildStartDecision => {\n\tif (status < 400) return { action: \"proceed\" };\n\tif (startWaits >= timings.buildStartMaxWaits) return { action: \"fail\" };\n\treturn { action: \"retry\", waitMs: timings.buildStartRetryWaitMs };\n};\n\n/** Forma mínima de un site en `GET sites/all`: lo justo para saber si tiene render pendiente. */\nexport interface SiteBuildInfo {\n\tid?: number;\n\t/** La API lo marca cuando el site tiene cambios sin renderizar (señal QA-scoped: hash pendiente, no bloqueado). */\n\tshouldBeUpdated?: boolean;\n}\n\n/**\n * Ids de los sites con render PENDIENTE — lo que el CX real busca en cada pasada de su poll.\n *\n * `onlyIds` acota el resultado (el shell le pasa los sites del test en curso). Esa acotación NO es cosmética:\n * el CX real es un único proceso global, pero el mock corre por test y a escala CI ~40 jobs comparten una BD;\n * sin acotar, cada pasada arrastraría el trabajo de los demás (build/start → fixAllPages es pesado) y la BD se\n * satura, tumbando incluso specs que no usan el mock.\n */\nexport const selectPendingSites = (sites: SiteBuildInfo[], onlyIds?: number[]): number[] => {\n\tconst ids: number[] = [];\n\t(sites || []).forEach((site) => {\n\t\tif (!site || site.id == null || !site.shouldBeUpdated) return;\n\t\tif (onlyIds && !onlyIds.includes(site.id)) return;\n\t\tids.push(site.id);\n\t});\n\treturn Array.from(new Set(ids));\n};\n","/**\n * Error del paquete, con código.\n *\n * Un único tipo de error con un `code` en lugar de una jerarquía de clases: los consumidores necesitan\n * distinguir *qué* falló para decidir (reintentar, avisar, abortar), y eso se hace igual de bien con un campo que\n * con `instanceof`, sin obligar a importar siete clases.\n */\n\n/** Códigos de {@link RenderError}. */\nexport const RENDER_ERROR_CODE = {\n\t/** `build/start` siguió respondiendo error hasta agotar los reintentos: el site no llega a estar listo. */\n\tBUILD_START_NOT_READY: \"BUILD_START_NOT_READY\",\n\t/** `build/end` respondió >= 400: el render no se ha aplicado y el bloqueo puede seguir puesto. */\n\tBUILD_END_FAILED: \"BUILD_END_FAILED\",\n\t/** Se agotaron las rondas y algunos títulos no alcanzaron el estado final. Mira `missingTitles`. */\n\tTITLES_NOT_FINAL: \"TITLES_NOT_FINAL\",\n\t/** No se pudo saber qué sites tenían render pendiente. */\n\tSITES_LOOKUP_FAILED: \"SITES_LOOKUP_FAILED\",\n\t/** No se pudo leer el listado de páginas de un site. */\n\tPAGE_LIST_FAILED: \"PAGE_LIST_FAILED\",\n\t/** Falló marcar páginas como pendientes de publicar. */\n\tMARK_PENDING_FAILED: \"MARK_PENDING_FAILED\",\n\t/** Falló publicar un site. */\n\tPUBLISH_FAILED: \"PUBLISH_FAILED\",\n} as const;\n\n/** Uno de los {@link RENDER_ERROR_CODE}. */\nexport type RenderErrorCode = (typeof RENDER_ERROR_CODE)[keyof typeof RENDER_ERROR_CODE];\n\n/** Datos opcionales que acompañan a un {@link RenderError}. */\nexport interface RenderErrorDetail {\n\t/** Status HTTP de la respuesta que provocó el error, si lo hubo. */\n\tstatus?: number;\n\t/** Body de esa respuesta, tal cual, para poder diagnosticar sin volver a reproducir. */\n\tbody?: unknown;\n\t/** Títulos que no alcanzaron su estado final. Solo en `TITLES_NOT_FINAL`. */\n\tmissingTitles?: string[];\n\t/** Error original, si este envuelve a otro. Se expone también como `cause` nativa. */\n\tcause?: unknown;\n}\n\n/**\n * Error de este paquete. Siempre trae `code`; el resto del detalle depende de dónde falló.\n *\n * ```ts\n * try {\n * \tawait engine.renderUntilLive([siteId], titles);\n * } catch (error) {\n * \tif (error instanceof RenderError && error.code === RENDER_ERROR_CODE.TITLES_NOT_FINAL) {\n * \t\tconsole.warn(\"no llegaron:\", error.missingTitles);\n * \t}\n * }\n * ```\n */\nexport class RenderError extends Error {\n\t/** Qué falló, de forma comprobable sin leer el mensaje. */\n\treadonly code: RenderErrorCode;\n\t/** Status HTTP asociado, si el error viene de una respuesta. */\n\treadonly status?: number;\n\t/** Body de la respuesta asociada. */\n\treadonly body?: unknown;\n\t/** Títulos que no alcanzaron su estado final (`TITLES_NOT_FINAL`). */\n\treadonly missingTitles?: string[];\n\n\tconstructor(code: RenderErrorCode, message: string, detail: RenderErrorDetail = {}) {\n\t\tsuper(message);\n\t\tthis.name = \"RenderError\";\n\t\tthis.code = code;\n\t\tthis.status = detail.status;\n\t\tthis.body = detail.body;\n\t\tthis.missingTitles = detail.missingTitles;\n\t\t// `cause` se asigna en lugar de pasarse al constructor: el segundo argumento de Error es ES2022, y así el\n\t\t// paquete sigue compilando contra librerías más antiguas sin perder el encadenado de errores en runtime.\n\t\tif (detail.cause !== undefined) (this as { cause?: unknown }).cause = detail.cause;\n\t}\n}\n","/**\n * Contrato de log mínimo, para que el paquete cuente qué está haciendo sin imponer una librería.\n *\n * Cuatro métodos y ninguna dependencia: encaja tal cual con `console`, y con pino, winston o el log del runner de\n * tests envolviéndolo en cuatro líneas. Si no pasas ninguno, el paquete no escribe nada.\n */\nexport interface Logger {\n\tdebug(message: string, meta?: unknown): void;\n\tinfo(message: string, meta?: unknown): void;\n\twarn(message: string, meta?: unknown): void;\n\terror(message: string, meta?: unknown): void;\n}\n\n/** Logger que descarta todo. Es el que se usa cuando no inyectas uno. */\nexport const NOOP_LOGGER: Logger = {\n\tdebug: () => undefined,\n\tinfo: () => undefined,\n\twarn: () => undefined,\n\terror: () => undefined,\n};\n\n/**\n * Logger sobre `console`, por comodidad en scripts y ejemplos.\n *\n * @param level nivel mínimo que se emite. Por defecto `info`, así que los `debug` se descartan.\n */\nexport const createConsoleLogger = (\n\tlevel: \"debug\" | \"info\" | \"warn\" | \"error\" = \"info\",\n): Logger => {\n\tconst order = { debug: 10, info: 20, warn: 30, error: 40 };\n\tconst min = order[level];\n\tconst emit =\n\t\t(method: \"debug\" | \"info\" | \"warn\" | \"error\") =>\n\t\t(message: string, meta?: unknown): void => {\n\t\t\tif (order[method] < min) return;\n\t\t\tif (meta === undefined) console[method](message);\n\t\t\telse console[method](message, meta);\n\t\t};\n\treturn { debug: emit(\"debug\"), info: emit(\"info\"), warn: emit(\"warn\"), error: emit(\"error\") };\n};\n","/**\n * Cliente interno sobre el transporte inyectado. **No es API pública**: no se exporta desde el índice del\n * paquete.\n *\n * Concentra las tres cosas que antes repetían el motor y el publicador: montar las cabeceras, componer la URL\n * a partir de `apiUrl`, y convertir una respuesta con error en un {@link RenderError} con su código. Así \"qué\n * cuenta como fallo\" se decide en un único sitio.\n */\n\nimport type { Transport, TransportResponse } from \"./transport\";\n\nimport { RenderError, type RenderErrorCode } from \"./errors\";\nimport { type Logger, NOOP_LOGGER } from \"./logger\";\n\n/** Lo que necesita cualquier pieza que hable con la API: el transporte, dónde está y con qué credencial. */\nexport interface ApiClientConfig {\n\ttransport: Transport;\n\tapiUrl: string;\n\tauthToken: string;\n\t/** Dónde contar lo que va pasando. Si no lo pasas, no se escribe nada. */\n\tlogger?: Logger;\n}\n\n/** Detalle de una llamada. Con `expectOk`, una respuesta >= 400 se convierte en error con ese código. */\nexport interface RequestSpec {\n\tbody?: unknown;\n\texpectOk?: RenderErrorCode;\n}\n\nexport class ApiClient {\n\treadonly logger: Logger;\n\tprivate readonly transport: Transport;\n\tprivate readonly apiUrl: string;\n\tprivate readonly headers: Record<string, string>;\n\n\tconstructor(config: ApiClientConfig) {\n\t\tthis.transport = config.transport;\n\t\tthis.apiUrl = config.apiUrl;\n\t\tthis.headers = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\tauthorization: `Bearer ${config.authToken}`,\n\t\t};\n\t\tthis.logger = config.logger ?? NOOP_LOGGER;\n\t}\n\n\t/** URL absoluta de un path relativo a `apiUrl`. Útil cuando la URL la compone el núcleo. */\n\turl(path: string): string {\n\t\treturn `${this.apiUrl}${path}`;\n\t}\n\n\tget(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"GET\", path, spec);\n\t}\n\n\tpost(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"POST\", path, spec);\n\t}\n\n\tput(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"PUT\", path, spec);\n\t}\n\n\t/**\n\t * Lanza si la respuesta trae error. Se usa cuando la llamada ya se hizo sin `expectOk` porque había que\n\t * inspeccionar el status antes de decidir.\n\t */\n\tensureOk(res: TransportResponse, code: RenderErrorCode, describe: string): void {\n\t\tif (res.status < 400) return;\n\t\tthrow new RenderError(code, `${describe} respondió ${res.status}`, {\n\t\t\tstatus: res.status,\n\t\t\tbody: res.body,\n\t\t});\n\t}\n\n\tprivate async send(\n\t\tmethod: \"GET\" | \"POST\" | \"PUT\",\n\t\tpath: string,\n\t\tspec: RequestSpec,\n\t): Promise<TransportResponse> {\n\t\tconst url = path.startsWith(\"http\") ? path : this.url(path);\n\t\tconst opts = { headers: this.headers, body: spec.body };\n\t\tconst res = await (method === \"GET\"\n\t\t\t? this.transport.get(url, opts)\n\t\t\t: method === \"POST\"\n\t\t\t\t? this.transport.post(url, opts)\n\t\t\t\t: this.transport.put(url, opts));\n\t\tif (spec.expectOk) this.ensureOk(res, spec.expectOk, `${method} ${path}`);\n\t\treturn res;\n\t}\n}\n","/** Espera de `ms` milisegundos. Es el `sleep` por defecto de todo el paquete; los tests inyectan un no-op. */\nexport const defaultSleep = (ms: number): Promise<void> =>\n\tnew Promise((resolve) => setTimeout(resolve, ms));\n","/**\n * Motor del protocolo de render: `build/start` → clasificar páginas por hash → `build/end`, con reintento por\n * rondas hasta que los títulos pedidos alcanzan su estado final.\n *\n * Es una clase porque tiene estado (configuración, cliente HTTP) y dependencias que conviene invertir: el HTTP y\n * el log. Las decisiones sin estado viven en `./core` como funciones puras; hablar con la API es cosa de\n * `./api-client`.\n */\n\nimport type { Logger } from \"./logger\";\n\nimport { ApiClient, type ApiClientConfig } from \"./api-client\";\nimport * as cxCore from \"./core\";\nimport { RENDER_ERROR_CODE, RenderError } from \"./errors\";\nimport { defaultSleep } from \"./sleep\";\n\n/** Dependencias del motor, inyectadas por constructor. */\nexport interface EngineDeps extends ApiClientConfig {\n\t/** Espera de `ms` milisegundos. Por defecto, `setTimeout`; inyecta un no-op para que los tests no esperen. */\n\tsleep?: (ms: number) => Promise<void>;\n\t/** Esperas y topes del render. Lo que no indiques se toma de {@link cxCore.DEFAULT_TIMINGS}. */\n\ttimings?: Partial<cxCore.RenderTimings>;\n\t/** Paginación del listado de páginas. Lo que no indiques se toma de {@link cxCore.DEFAULT_PAGE_LIST_QUERY}. */\n\tpageList?: Partial<cxCore.PageListQuery>;\n}\n\n/**\n * Instánciala una vez con la configuración del entorno y reutilízala:\n *\n * ```ts\n * const engine = new CxRenderEngine({ transport, apiUrl, authToken });\n * await engine.renderUntilLive([siteId], [\"Mi página\"]);\n * ```\n */\nexport class CxRenderEngine {\n\tprivate readonly api: ApiClient;\n\tprivate readonly logger: Logger;\n\tprivate readonly sleep: (ms: number) => Promise<void>;\n\tprivate readonly timings: cxCore.RenderTimings;\n\tprivate readonly pageList: cxCore.PageListQuery;\n\n\tconstructor(deps: EngineDeps) {\n\t\tthis.api = new ApiClient(deps);\n\t\tthis.logger = this.api.logger;\n\t\tthis.sleep = deps.sleep ?? defaultSleep;\n\t\tthis.timings = { ...cxCore.DEFAULT_TIMINGS, ...deps.timings };\n\t\tthis.pageList = { ...cxCore.DEFAULT_PAGE_LIST_QUERY, ...deps.pageList };\n\t}\n\n\t/**\n\t * Renderiza un site en **una sola pasada**: settle adaptativo → `build/start` → `GET page/{id}` →\n\t * `build/end`.\n\t *\n\t * El settle previo da tiempo a que el backend termine de propagar las copias de las páginas globales; sin él,\n\t * `build/start` devolvería una lista incompleta. Es ADAPTATIVO: sondea el listado hasta que dos sondas\n\t * consecutivas coinciden (quiescencia) o hasta el techo `timings.settleMs` — rápido cuando el backend va\n\t * suelto, paciente bajo carga. Las páginas que lleguen tarde no se reintentan aquí: de eso se encarga\n\t * {@link renderUntilLive}.\n\t *\n\t * ⚠️ `build/start` deja el site bloqueado y `build/end` lo libera. En el camino normal `build/end` se emite\n\t * siempre, pero si el transporte rechaza entre ambos el bloqueo queda colgado durante horas: llama a\n\t * {@link releaseRenderLock} en tu `finally`.\n\t *\n\t * @throws {RenderError} `BUILD_START_NOT_READY` si el site no llega a estar listo, `BUILD_END_FAILED` si el\n\t * render no se aplica.\n\t */\n\tasync renderSiteOnce(siteId: number): Promise<void> {\n\t\tconst startOnce = async (startWaits = 0): Promise<void> => {\n\t\t\t// Sin `expectOk`: un 4xx aquí puede ser transitorio y lo decide el núcleo, no es un fallo todavía.\n\t\t\tconst bs = await this.api.get(`site/${siteId}/build/start`);\n\t\t\tconst startStep = cxCore.decideBuildStartStep(bs.status, startWaits, this.timings);\n\t\t\tif (startStep.action === \"fail\") {\n\t\t\t\tthrow new RenderError(\n\t\t\t\t\tRENDER_ERROR_CODE.BUILD_START_NOT_READY,\n\t\t\t\t\t`GET site/${siteId}/build/start sigue en ${bs.status} tras ${startWaits} esperas (¿el site no acaba de asentarse?)`,\n\t\t\t\t\t{ status: bs.status, body: bs.body },\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (startStep.action === \"retry\") {\n\t\t\t\tthis.logger.debug(\n\t\t\t\t\t`build/start del site ${siteId} en ${bs.status}: espera ${startStep.waitMs}ms y reintenta`,\n\t\t\t\t);\n\t\t\t\tawait this.sleep(startStep.waitMs);\n\t\t\t\treturn startOnce(startWaits + 1);\n\t\t\t}\n\n\t\t\tconst siteHash: string | null = bs.body?.siteHash || null;\n\t\t\tconst publishIds: number[] = bs.body?.publishIds || [];\n\t\t\tconst unpublishHashes: string[] = bs.body?.unpublishHashes || [];\n\t\t\tthis.logger.debug(\n\t\t\t\t`site ${siteId}: ${publishIds.length} páginas candidatas, ${unpublishHashes.length} a despublicar`,\n\t\t\t);\n\n\t\t\t// El trabajo es toda página con hash, sin mirar su estado (ver classifyPage). Una sola pasada, así que no\n\t\t\t// hace falta deduplicar ids; las páginas que aún no tienen hash las recoge renderUntilLive.\n\t\t\tconst publishHashes: string[] = [];\n\t\t\tconst publishPagesIds: number[] = [];\n\t\t\tfor (const pageId of publishIds) {\n\t\t\t\tconst pres = await this.api.get(`page/${pageId}`);\n\t\t\t\tconst hash = pres.body?.hash;\n\t\t\t\tconst status = pres.body?.liveStatus?.id;\n\t\t\t\tif (cxCore.classifyPage(hash, status) === \"publish\") {\n\t\t\t\t\tpublishHashes.push(hash);\n\t\t\t\t\tpublishPagesIds.push(pageId);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait this.api.post(`site/${siteId}/build/end`, {\n\t\t\t\tbody: cxCore.buildEndBody(publishHashes, siteHash, unpublishHashes, publishPagesIds),\n\t\t\t\texpectOk: RENDER_ERROR_CODE.BUILD_END_FAILED,\n\t\t\t});\n\t\t\tthis.logger.info(`site ${siteId} renderizado: ${publishPagesIds.length} páginas publicadas`);\n\t\t};\n\n\t\t// Asentamiento primero, luego una sola pasada.\n\t\tawait this.settleBeforeStart(siteId);\n\t\treturn startOnce();\n\t}\n\n\t/**\n\t * Settle ADAPTATIVO previo al build/start: sondea el listado del site cada `timings.settlePollMs` y arranca\n\t * cuando dos sondas consecutivas devuelven la misma firma (`cxCore.pageListSignature`) o al agotar el techo\n\t * `timings.settleMs` (la decisión es de `cxCore.decideSettleStep`). Best-effort: si el listado falla durante\n\t * el settle, se procede al render en vez de morir aquí (peor caso = el de siempre, un pase incompleto que\n\t * recogen las rondas de `renderUntilLive`). Con `settleMs <= 0` no sondea (settle desactivado).\n\t */\n\tprivate async settleBeforeStart(siteId: number): Promise<void> {\n\t\tif (this.timings.settleMs <= 0) return;\n\t\ttry {\n\t\t\tlet prev: string | null = null;\n\t\t\tlet elapsed = 0;\n\t\t\tfor (;;) {\n\t\t\t\tconst sig = cxCore.pageListSignature(await this.listPages([siteId]));\n\t\t\t\tconst step = cxCore.decideSettleStep(prev, sig, elapsed, this.timings);\n\t\t\t\tif (step.action === \"go\") return;\n\t\t\t\tprev = sig;\n\t\t\t\tawait this.sleep(step.waitMs);\n\t\t\t\telapsed += step.waitMs;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.logger.warn(\n\t\t\t\t`settle del site ${siteId}: el sondeo del listado falló (${err instanceof Error ? err.message : String(err)}); se procede al render sin asentar`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Renderiza **todos los sites que tengan render pendiente**, que es lo que hace el render real en cada pasada:\n\t * pregunta al backend qué sites tienen cambios sin renderizar (`GET sites/all` → `shouldBeUpdated`) y los\n\t * resuelve uno por uno.\n\t *\n\t * @param onlyIds acota a esos sites. **Úsalo si compartes la base de datos con otros procesos**: sin acotar,\n\t * esta pasada arrastra también el trabajo pendiente de los demás, que es tan lento como caro.\n\t * @returns los ids que ha renderizado.\n\t * @throws {RenderError} `SITES_LOOKUP_FAILED` si no se puede consultar la lista de sites.\n\t */\n\tasync renderPendingSites(onlyIds?: number[]): Promise<number[]> {\n\t\tconst res = await this.api.get(\"sites/all\", {\n\t\t\texpectOk: RENDER_ERROR_CODE.SITES_LOOKUP_FAILED,\n\t\t});\n\t\tconst all: cxCore.SiteBuildInfo[] = Array.isArray(res.body) ? res.body : res.body?.items || [];\n\t\tconst pending = cxCore.selectPendingSites(all, onlyIds);\n\t\tthis.logger.info(\n\t\t\tpending.length > 0\n\t\t\t\t? `sites con render pendiente: ${pending.join(\", \")}`\n\t\t\t\t: \"ningún site con render pendiente\",\n\t\t);\n\t\tfor (const siteId of pending) await this.renderSiteOnce(siteId);\n\t\treturn pending;\n\t}\n\n\t/**\n\t * Lleva los títulos pedidos a su estado final, reintentando lo que falte.\n\t *\n\t * Mientras queden títulos pendientes, vuelve a marcar como pendientes las copias que se hayan quedado\n\t * despublicadas y renderiza otra vez, hasta `timings.maxRounds` rondas; la última confirma o lanza. Si todo\n\t * está ya en su sitio, la primera comprobación pasa y no renderiza nada.\n\t *\n\t * @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`; usa `LIVE_STATUS.UNPUBLISHED`\n\t * para confirmar una despublicación.\n\t * @throws {RenderError} `TITLES_NOT_FINAL` si tras la última ronda sigue faltando algún título.\n\t */\n\tasync renderUntilLive(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return;\n\n\t\tconst attempt = async (round: number): Promise<void> => {\n\t\t\tconst missing = await this.missingPagesByTitle(siteIds, wanted, expectedStatus);\n\t\t\tif (missing.length === 0) return;\n\t\t\tif (round >= this.timings.maxRounds)\n\t\t\t\treturn this.confirmPagesByTitle(siteIds, wanted, expectedStatus);\n\t\t\tthis.logger.warn(\n\t\t\t\t`ronda ${round}/${this.timings.maxRounds}: faltan ${missing.length}/${wanted.length} títulos (${missing.join(\", \")})`,\n\t\t\t);\n\t\t\tawait this.republishMissingWantedLive(siteIds, missing, expectedStatus);\n\t\t\tfor (const siteId of siteIds) await this.renderSiteOnce(siteId);\n\t\t\tawait this.sleep(this.timings.roundWaitMs);\n\t\t\treturn attempt(round + 1);\n\t\t};\n\t\treturn attempt(1);\n\t}\n\n\t/**\n\t * Comprueba, sin renderizar, que los títulos están en `expectedStatus`.\n\t *\n\t * @throws {RenderError} `TITLES_NOT_FINAL`, con los títulos que faltan en `missingTitles`.\n\t */\n\tasync confirmPagesByTitle(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return;\n\t\tconst missing = await this.missingPagesByTitle(siteIds, wanted, expectedStatus);\n\t\tif (missing.length > 0) {\n\t\t\tthrow new RenderError(\n\t\t\t\tRENDER_ERROR_CODE.TITLES_NOT_FINAL,\n\t\t\t\t`estos títulos no están en liveStatus ${expectedStatus}: ${missing.join(\", \")}`,\n\t\t\t\t{\n\t\t\t\t\tmissingTitles: missing,\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Libera el bloqueo de render que deja `build/start`, emitiendo un `build/end` vacío.\n\t *\n\t * Llámalo en tu `finally` si un render puede abortar a medias. Es best-effort: no lanza aunque el backend\n\t * responda con error, porque el caso normal es que ya no hubiera nada que liberar.\n\t */\n\tasync releaseRenderLock(siteId: number): Promise<void> {\n\t\tconst res = await this.api.post(`site/${siteId}/build/end`, {\n\t\t\tbody: cxCore.buildEndBody([], null, [], []),\n\t\t});\n\t\tif (res.status >= 400) {\n\t\t\tthis.logger.debug(\n\t\t\t\t`releaseRenderLock del site ${siteId}: el backend respondió ${res.status} (probablemente no había bloqueo)`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Títulos que aún no están en `expectedStatus`, leyendo el listado completo de cada site. */\n\tprivate async missingPagesByTitle(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number,\n\t): Promise<string[]> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return [];\n\t\tconst items = await this.listPages(siteIds);\n\t\treturn cxCore.diffMissingByTitle(items, wanted, expectedStatus);\n\t}\n\n\t/** Devuelve a \"pendiente de publicar\" las copias despublicadas de los títulos que faltan. */\n\tprivate async republishMissingWantedLive(\n\t\tsiteIds: number[],\n\t\tmissing: string[],\n\t\texpectedStatus: number,\n\t): Promise<void> {\n\t\tif (expectedStatus !== cxCore.LIVE_STATUS.LIVE || missing.length === 0) return;\n\t\tconst items = await this.listPages(siteIds);\n\t\tconst uniqueIds = cxCore.collectRepublishIds(items, missing);\n\t\tif (uniqueIds.length === 0) return;\n\t\tthis.logger.info(`re-marcando ${uniqueIds.length} páginas como pendientes de publicar`);\n\t\tawait this.api.put(\"pages/status/upload-pending\", { body: { ids: uniqueIds } });\n\t}\n\n\t/**\n\t * Páginas de los sites indicados, **paginando hasta el final** del listado.\n\t *\n\t * Recorre páginas mientras el backend devuelva el lote completo: si llega uno más corto, es el último. Así un\n\t * site con más páginas que `pageList.itemsPerPage` no se queda a medias, que era la causa de confirmaciones\n\t * por título erróneas cuando solo se leía la primera página.\n\t *\n\t * @throws {RenderError} `PAGE_LIST_FAILED` si el backend no puede devolver el listado.\n\t */\n\tprivate async listPages(siteIds: number[]): Promise<cxCore.PageInfo[]> {\n\t\tconst items: cxCore.PageInfo[] = [];\n\t\tfor (const siteId of siteIds) {\n\t\t\tlet page = this.pageList.page;\n\t\t\t// Tope de seguridad: con el itemsPerPage por defecto son 300.000 páginas. Si se alcanza, algo va mal en\n\t\t\t// el backend (por ejemplo, ignora el parámetro `page` y devuelve siempre el mismo lote).\n\t\t\tfor (let fetched = 0; fetched < MAX_LIST_PAGES; fetched++) {\n\t\t\t\tconst query = { page, itemsPerPage: this.pageList.itemsPerPage };\n\t\t\t\tconst res = await this.api.get(cxCore.listPageUrl(this.api.url(\"\"), siteId, query), {\n\t\t\t\t\texpectOk: RENDER_ERROR_CODE.PAGE_LIST_FAILED,\n\t\t\t\t});\n\t\t\t\tconst batch: cxCore.PageInfo[] = (res.body && (res.body.items || res.body)) || [];\n\t\t\t\titems.push(...batch);\n\t\t\t\tif (batch.length < this.pageList.itemsPerPage) break;\n\t\t\t\tpage += 1;\n\t\t\t\tif (fetched === MAX_LIST_PAGES - 1) {\n\t\t\t\t\tthis.logger.warn(\n\t\t\t\t\t\t`listado del site ${siteId} cortado en ${MAX_LIST_PAGES} páginas: ¿el backend ignora el parámetro page?`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn items;\n\t}\n}\n\n/** Tope de páginas que se piden por site antes de asumir que el backend no pagina bien. */\nconst MAX_LIST_PAGES = 1000;\n","/**\n * Publicación: marcar páginas como pendientes y publicar sites.\n *\n * Ojo con el reparto de responsabilidades: **esto no es parte del render**. En producción lo dispara la persona\n * que pulsa \"Publicar\" en el CMS; el render solo reacciona a lo que ya está pendiente. Vive en este paquete como\n * conveniencia para montar el escenario desde un script, no porque el render lo necesite.\n *\n * El orden importa: primero se marcan las páginas, luego se publica el site. Publicar es lo que hace que el\n * backend las incluya en el siguiente `build/start`.\n */\n\nimport type { Logger } from \"./logger\";\n\nimport { ApiClient, type ApiClientConfig } from \"./api-client\";\nimport { RENDER_ERROR_CODE } from \"./errors\";\n\n/** Dependencias del publicador, inyectadas por constructor. Las mismas que el motor necesita para hablar con la API. */\nexport type PublisherDeps = ApiClientConfig;\n\n/**\n * Dispara la publicación en el CMS. Complementa al motor de render: este deja el trabajo pendiente, el motor lo\n * resuelve.\n *\n * ```ts\n * const publisher = new CmsPublisher({ transport, apiUrl, authToken });\n * await publisher.publishPages([siteId], [pageId]);\n * await engine.renderUntilLive([siteId], [\"Mi página\"]);\n * ```\n */\nexport class CmsPublisher {\n\tprivate readonly api: ApiClient;\n\tprivate readonly logger: Logger;\n\n\tconstructor(deps: PublisherDeps) {\n\t\tthis.api = new ApiClient(deps);\n\t\tthis.logger = this.api.logger;\n\t}\n\n\t/**\n\t * Marca páginas como pendientes de publicar. Sin ids no hace nada.\n\t *\n\t * @throws {RenderError} `MARK_PENDING_FAILED` si el backend responde >= 400.\n\t */\n\tasync markPagesPending(pageIds: number[]): Promise<void> {\n\t\tif (!pageIds || pageIds.length === 0) return;\n\t\tawait this.api.put(\"pages/status/upload-pending\", {\n\t\t\tbody: { ids: pageIds },\n\t\t\texpectOk: RENDER_ERROR_CODE.MARK_PENDING_FAILED,\n\t\t});\n\t\tthis.logger.info(`${pageIds.length} páginas marcadas como pendientes de publicar`);\n\t}\n\n\t/**\n\t * Publica los sites indicados, uno por uno.\n\t *\n\t * @throws {RenderError} `PUBLISH_FAILED` si el backend responde >= 400 para alguno.\n\t */\n\tasync publishSites(siteIds: number[]): Promise<void> {\n\t\tfor (const siteId of siteIds || []) {\n\t\t\tawait this.api.post(`site/${siteId}/publish`, { expectOk: RENDER_ERROR_CODE.PUBLISH_FAILED });\n\t\t\tthis.logger.info(`site ${siteId} publicado`);\n\t\t}\n\t}\n\n\t/**\n\t * Los dos pasos en orden: marca las páginas y publica los sites. Es el disparo completo que deja el trabajo\n\t * listo para que el motor lo renderice.\n\t */\n\tasync publishPages(siteIds: number[], pageIds: number[]): Promise<void> {\n\t\tawait this.markPagesPending(pageIds);\n\t\tawait this.publishSites(siteIds);\n\t}\n}\n","/**\n * Fachada: construye motor y publicador con una sola configuración, y encapsula el flujo completo.\n *\n * Existe por dos motivos concretos, no por simetría: instanciar las dos clases obliga a repetir las mismas tres\n * dependencias, y el flujo tiene un orden que hay que conocer (publicar → renderizar → confirmar, liberando el\n * bloqueo si algo se rompe a medias). Si necesitas solo una parte, usa `CxRenderEngine` o `CmsPublisher`\n * directamente: siguen expuestos.\n */\nimport * as cxCore from \"./core\";\nimport { CxRenderEngine, type EngineDeps } from \"./engine\";\nimport { CmsPublisher } from \"./publish\";\n\n/** Configuración de la fachada: la del motor, que es un superconjunto de la del publicador. */\nexport type CxRenderMockDeps = EngineDeps;\n\n/**\n * Punto de entrada recomendado.\n *\n * ```ts\n * const cx = new CxRenderMock({ transport, apiUrl, authToken, logger });\n * await cx.publishAndRender([siteId], [pageId], [\"Mi página\"]);\n * ```\n */\nexport class CxRenderMock {\n\t/** El motor de render, por si necesitas sus métodos sueltos. */\n\treadonly engine: CxRenderEngine;\n\t/** El disparo de publicación, por si quieres publicar sin renderizar. */\n\treadonly publisher: CmsPublisher;\n\n\tconstructor(deps: CxRenderMockDeps) {\n\t\tthis.engine = new CxRenderEngine(deps);\n\t\tthis.publisher = new CmsPublisher(deps);\n\t}\n\n\t/**\n\t * El flujo entero: publica, renderiza y confirma que los títulos llegaron a su estado final. Libera el bloqueo\n\t * de render de cada site si algo falla a medias.\n\t *\n\t * @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`.\n\t * @throws {RenderError} con el `code` de la etapa que falló.\n\t */\n\tasync publishAndRender(\n\t\tsiteIds: number[],\n\t\tpageIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tawait this.publisher.publishPages(siteIds, pageIds);\n\t\ttry {\n\t\t\tawait this.engine.renderUntilLive(siteIds, titles, expectedStatus);\n\t\t} catch (error) {\n\t\t\t// El bloqueo lo pone build/start y lo quita build/end. Si el render murió entre ambos, aquí es donde se\n\t\t\t// suelta; si ya no había bloqueo, releaseRenderLock no protesta.\n\t\t\tfor (const siteId of siteIds) await this.engine.releaseRenderLock(siteId);\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n","/**\n * Contrato del transporte HTTP y decoradores opcionales para envolverlo.\n *\n * El transporte es la única dependencia de runtime del paquete, y se inyecta: así no arrastra ningún cliente\n * HTTP y funciona igual en Node, en un bundler o dentro de un runner de tests. Los decoradores son funciones que\n * reciben un transporte y devuelven otro con el mismo contrato, de modo que reintentos, timeouts o trazas se\n * añaden componiendo, sin que el motor sepa nada.\n */\nimport { type Logger, NOOP_LOGGER } from \"./logger\";\nimport { defaultSleep } from \"./sleep\";\n\n/** Respuesta HTTP normalizada: status y body ya parseado. */\nexport interface TransportResponse {\n\tstatus: number;\n\tbody: any;\n}\n\n/** Opciones de una request: cabeceras y body ya serializable. */\nexport interface RequestOptions {\n\theaders?: Record<string, string>;\n\tbody?: unknown;\n}\n\n/**\n * Transporte HTTP inyectable. Impleméntalo sobre `fetch`, `axios`, `undici` o lo que uses.\n *\n * ⚠️ **No debe lanzar en 4xx/5xx**: devuelve `{ status, body }` y deja que el motor decida. Es deliberado: un\n * 4xx de `build/start` es una respuesta esperada (la ruta del site aún no está lista), no un fallo. Un cliente\n * que lance en error — como los que devuelven el body deserializado y tiran en no-2xx — necesita un adaptador\n * que capture y traduzca a `{ status, body }`.\n */\nexport interface Transport {\n\tget(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n\tpost(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n\tput(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n}\n\n/** Métodos que un decorador puede tratar de forma distinta. */\nexport type TransportMethod = \"get\" | \"post\" | \"put\";\n\n/** Ajustes de {@link withRetry}. */\nexport interface RetryOptions {\n\t/** Intentos totales, incluido el primero. Por defecto 3. */\n\tattempts?: number;\n\t/** Espera entre intentos (ms). Por defecto 500, y crece linealmente con el intento. */\n\tdelayMs?: number;\n\t/**\n\t * Métodos que se reintentan. Por defecto **solo `get`**.\n\t *\n\t * No es timidez: `build/start` toma un bloqueo del site y `build/end` lo libera, así que repetir a ciegas una\n\t * escritura puede dejar el protocolo a medias. Amplíalo solo si sabes que tu caso lo tolera.\n\t */\n\tmethods?: TransportMethod[];\n\t/** Qué respuesta merece otro intento. Por defecto, los 5xx (un 4xx no se arregla repitiendo). */\n\tshouldRetry?: (res: TransportResponse) => boolean;\n\t/** Espera inyectable, para tests instantáneos. */\n\tsleep?: (ms: number) => Promise<void>;\n\tlogger?: Logger;\n}\n\n/**\n * Envuelve un transporte para que reintente los fallos pasajeros: una respuesta 5xx o un rechazo del cliente\n * (red caída, DNS, socket cerrado). Agotados los intentos, deja pasar la última respuesta o relanza el error,\n * para que quien llama decida.\n *\n * ```ts\n * const transport = withRetry(myTransport, { attempts: 4 });\n * ```\n */\nexport const withRetry = (transport: Transport, opts: RetryOptions = {}): Transport => {\n\tconst attempts = opts.attempts ?? 3;\n\tconst delayMs = opts.delayMs ?? 500;\n\tconst methods = opts.methods ?? [\"get\"];\n\tconst shouldRetry = opts.shouldRetry ?? ((res: TransportResponse) => res.status >= 500);\n\tconst sleep = opts.sleep ?? defaultSleep;\n\tconst logger = opts.logger ?? NOOP_LOGGER;\n\n\tconst run = async (\n\t\tmethod: TransportMethod,\n\t\turl: string,\n\t\treqOpts?: RequestOptions,\n\t): Promise<TransportResponse> => {\n\t\tif (!methods.includes(method)) return transport[method](url, reqOpts);\n\n\t\tlet lastError: unknown;\n\t\tfor (let attempt = 1; attempt <= attempts; attempt++) {\n\t\t\ttry {\n\t\t\t\tconst res = await transport[method](url, reqOpts);\n\t\t\t\tif (attempt === attempts || !shouldRetry(res)) return res;\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`retry ${method.toUpperCase()} ${url}: status ${res.status}, intento ${attempt}/${attempts}`,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tlastError = error;\n\t\t\t\tif (attempt === attempts) throw error;\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`retry ${method.toUpperCase()} ${url}: el transporte falló, intento ${attempt}/${attempts}`,\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait sleep(delayMs * attempt);\n\t\t}\n\t\t// Inalcanzable: el último intento sale por return o por throw. Está por si `attempts` llega como 0.\n\t\tthrow (\n\t\t\tlastError ??\n\t\t\tnew Error(`withRetry: ningún intento ejecutado para ${method.toUpperCase()} ${url}`)\n\t\t);\n\t};\n\n\treturn {\n\t\tget: (url, reqOpts) => run(\"get\", url, reqOpts),\n\t\tpost: (url, reqOpts) => run(\"post\", url, reqOpts),\n\t\tput: (url, reqOpts) => run(\"put\", url, reqOpts),\n\t};\n};\n\n/** Ajustes de {@link withTimeout}. */\nexport interface TimeoutOptions {\n\t/** Milisegundos antes de dar la request por perdida. */\n\tms: number;\n\tlogger?: Logger;\n}\n\n/**\n * Envuelve un transporte para que ninguna request se quede colgada para siempre: pasado el plazo, rechaza.\n *\n * ⚠️ **No cancela la request subyacente**, solo deja de esperarla: el paquete no conoce tu cliente HTTP, así que\n * no puede abortarlo. Si necesitas cancelación real, implementa el plazo dentro de tu propio transporte con un\n * `AbortController`.\n */\nexport const withTimeout = (transport: Transport, opts: TimeoutOptions): Transport => {\n\tconst logger = opts.logger ?? NOOP_LOGGER;\n\n\tconst race = (\n\t\tmethod: TransportMethod,\n\t\turl: string,\n\t\treqOpts?: RequestOptions,\n\t): Promise<TransportResponse> => {\n\t\tlet timer: ReturnType<typeof setTimeout>;\n\t\tconst timeout = new Promise<never>((_resolve, reject) => {\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tlogger.warn(`timeout ${method.toUpperCase()} ${url}: sin respuesta en ${opts.ms}ms`);\n\t\t\t\treject(new Error(`Timeout de ${opts.ms}ms en ${method.toUpperCase()} ${url}`));\n\t\t\t}, opts.ms);\n\t\t});\n\t\treturn Promise.race([transport[method](url, reqOpts), timeout]).finally(() =>\n\t\t\tclearTimeout(timer),\n\t\t);\n\t};\n\n\treturn {\n\t\tget: (url, reqOpts) => race(\"get\", url, reqOpts),\n\t\tpost: (url, reqOpts) => race(\"post\", url, reqOpts),\n\t\tput: (url, reqOpts) => race(\"put\", url, reqOpts),\n\t};\n};\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -20,6 +20,10 @@ var DEFAULT_TIMINGS = {
20
20
  var DEFAULT_PAGE_LIST_QUERY = { page: 1, itemsPerPage: 300 };
21
21
  var listPageUrl = (apiUrl, siteId, query = DEFAULT_PAGE_LIST_QUERY) => `${apiUrl}site/${siteId}/pages?deleted=false&page=${query.page}&itemsPerPage=${query.itemsPerPage}&format=list`;
22
22
  var pageListSignature = (items) => (items || []).filter((p) => p && p.id != null).map((p) => `${p.id}:${p.liveStatus?.id ?? ""}:${p.hash ?? ""}`).sort().join("|");
23
+ var withPendingHashes = (items, hashesById) => (items || []).map(
24
+ (p) => p && p.id != null && hashesById && p.liveStatus?.id === LIVE_STATUS.PENDING_PUBLISHING && p.id in hashesById ? { ...p, hash: hashesById[p.id] } : p
25
+ );
26
+ var isBuildStartSkipped = (body) => typeof body === "object" && body !== null && body.skipped === true;
23
27
  var decideSettleStep = (prevSignature, signature, elapsedMs, timings = DEFAULT_TIMINGS) => {
24
28
  if (elapsedMs >= timings.settleMs) return { action: "go" };
25
29
  if (prevSignature !== null && prevSignature === signature) return { action: "go" };
@@ -535,9 +539,11 @@ export {
535
539
  decideBuildStartStep,
536
540
  decideSettleStep,
537
541
  diffMissingByTitle,
542
+ isBuildStartSkipped,
538
543
  listPageUrl,
539
544
  pageListSignature,
540
545
  selectPendingSites,
546
+ withPendingHashes,
541
547
  withRetry,
542
548
  withTimeout
543
549
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core.ts","../src/errors.ts","../src/logger.ts","../src/api-client.ts","../src/sleep.ts","../src/engine.ts","../src/publish.ts","../src/facade.ts","../src/transport.ts"],"sourcesContent":["/**\n * Núcleo PURO del test-double de render de CX — agnóstico de Cypress en VALOR y en TIPO.\n *\n * Contiene las decisiones difíciles del protocolo (clasificación por hash, diff por título, gate de\n * build/start, shaping del body de build/end, colección de re-pending de la mitigación cross-job, y los knobs\n * por defecto del render de un solo pase) SIN acoplarse a la cola de comandos de Cypress: cero `cy`/`Cypress`/`expect`/`window`,\n * cero `import ... 'cypress'`, y ninguna firma devuelve `Cypress.Chainable` (solo tipos neutrales). Es el\n * artefacto compartible: el shell Cypress de griddo-qa (`cypress/support/cx-render.ts`) y el motor de Promises\n * del paquete (`./engine`) consumen ESTE módulo, de modo que el racional que causa los bugs vive en un único\n * sitio (functional-core / imperative-shell). Ver `docs/cx-render-packaging-feasibility.md` §5/§6.\n *\n * Behavior-preserving: este código se EXTRAJO de `cx-render.ts` sin cambiar su comportamiento observable;\n * las constantes, umbrales y predicados son idénticos a los del shell original.\n */\n\n/**\n * Modelo de live_status: data-driven en la BD pero ESTABLE por orden de seed (contrato code-grounded e2e-202 →\n * griddo-api CONFIG/LiveStatusSeeder). El double no puede leer los flags de CONFIG en runtime → usa estos ids\n * verificados.\n */\nexport const LIVE_STATUS = {\n\tUNPUBLISHED: 1, // isUnpublished / offline — estado final de despublicación\n\tPENDING_PUBLISHING: 2, // isPendingPublishing / upload-pending — transitorio\n\tLIVE: 3, // isLiveStatus / active — estado final de publicación\n\tPENDING_UNPUBLISHING: 4, // isPendingUnpublishing / offline-pending — transitorio\n} as const;\n\n/** Los cuatro ids de live_status del contrato (1/2/3/4). */\nexport type LiveStatus = (typeof LIVE_STATUS)[keyof typeof LIVE_STATUS];\n\n/**\n * Forma mínima de una página tal como la exponen los endpoints que consume el double: `GET page/{id}` (trae\n * `hash` + `liveStatus`) y `site/{id}/pages?format=list` (trae `id` + `title` + `liveStatus`). Todos los\n * campos son opcionales porque el body real de la API puede venir incompleto y el core no debe asumir.\n */\nexport interface PageInfo {\n\tid?: number;\n\ttitle?: string;\n\thash?: string | null;\n\tliveStatus?: { id?: number } | null;\n}\n\n/** Cuerpo de `POST site/{id}/build/end` (lo que el CX real envía para resolver los transitorios). */\nexport interface BuildEndBody {\n\tpublishHashes: string[];\n\tsiteHash: string | null;\n\tunpublishHashes: string[];\n\tpublishPagesIds: number[];\n}\n\n// --- Esperas, topes y paginación (todo ajustable por el consumidor) -----------------------------------------\n// Antes eran constantes fijas del módulo. Ahora son datos que el consumidor puede sustituir: cada entorno tiene\n// su latencia (CI saturado ≠ local), y quien usa el paquete la conoce mejor que el paquete. Los defaults\n// (medidos) siguen aquí, y el shell Cypress los sobreescribe por `Cypress.env`.\n\n/** Esperas y topes del render. Todos sustituibles; los defaults están en `DEFAULT_TIMINGS`. */\nexport interface RenderTimings {\n\t/**\n\t * TECHO (ms) del settle ADAPTATIVO previo al ÚNICO build/start del render de un solo pase. El settle espera\n\t * al spread ASÍNCRONO de las copias GLOBAL, pero ya NO es un sleep fijo: se sondea el listado del site hasta\n\t * que dos sondas consecutivas devuelven la misma firma (`pageListSignature` → `decideSettleStep`), con este\n\t * valor como tope. Racional: una constante fija no cubre una latencia variable — a 15s el pase único perdía\n\t * rezagados bajo carga de CI (renderFlow2/002 agotaba los 3 intentos) y a 30-45s se paga tiempo muerto por\n\t * site y por ronda en el 90% de renders que asientan en segundos. `<= 0` desactiva el settle por completo.\n\t */\n\tsettleMs: number;\n\t/** Intervalo (ms) entre sondas del settle adaptativo. */\n\tsettlePollMs: number;\n\t/**\n\t * Rondas máximas del orquestador auto-sanador `renderUntilLive`. NO baja a 1: con 1 quedaban sets ENTEROS\n\t * not-Live en CI (el build/start del mock no captura TODAS las páginas en un pase; las rondas\n\t * re-pend+re-render sí).\n\t */\n\tmaxRounds: number;\n\t/** Wait entre rondas del orquestador auto-sanador (ms). */\n\troundWaitMs: number;\n\t/** Reintentos máximos de build/start mientras dé 4xx transitorio (~30s con el retry por defecto). */\n\tbuildStartMaxWaits: number;\n\t/** Backoff entre reintentos de build/start en 4xx transitorio (ms). */\n\tbuildStartRetryWaitMs: number;\n}\n\n/** Defaults medidos de `RenderTimings`. El shell los sobreescribe por `Cypress.env`. */\nexport const DEFAULT_TIMINGS: RenderTimings = {\n\tsettleMs: 45000,\n\tsettlePollMs: 3000,\n\tmaxRounds: 3,\n\troundWaitMs: 1500,\n\tbuildStartMaxWaits: 15,\n\tbuildStartRetryWaitMs: 2000,\n};\n\n/** Paginación con la que se pide el listado de páginas de un site. Defaults en `DEFAULT_PAGE_LIST_QUERY`. */\nexport interface PageListQuery {\n\tpage: number;\n\titemsPerPage: number;\n}\n\n/** Defaults de `PageListQuery`. */\nexport const DEFAULT_PAGE_LIST_QUERY: PageListQuery = { page: 1, itemsPerPage: 300 };\n\n/**\n * URL del listado del site (`site/{id}/pages?format=list`) que leen el diff por título y el re-pending.\n * Centraliza la query string y la paginación — pura (string → string), sin acoplarse a Cypress (recibe\n * `apiUrl` como argumento). ⚠️ Limitación HEREDADA: pide UNA sola página (300 ítems por defecto); un site con\n * más páginas daría confirmación por título errónea. Se mitiga subiendo `itemsPerPage`, pero no pagina.\n */\nexport const listPageUrl = (\n\tapiUrl: string,\n\tsiteId: number,\n\tquery: PageListQuery = DEFAULT_PAGE_LIST_QUERY,\n): string =>\n\t`${apiUrl}site/${siteId}/pages?deleted=false&page=${query.page}&itemsPerPage=${query.itemsPerPage}&format=list`;\n\n/**\n * Firma ESTABLE del estado render-relevante de un listado de páginas: `id:liveStatus:hash` por página, ordenado\n * por id (el orden del listado puede variar entre sondas sin que el estado haya cambiado). Dos firmas iguales en\n * sondas consecutivas = el backend ha dejado de mover estados/hashes (spread y recálculos asentados) → el\n * build/start ya puede capturar el trabajo completo en un pase. Pura (datos → string), para el settle adaptativo.\n */\nexport const pageListSignature = (items: PageInfo[]): string =>\n\t(items || [])\n\t\t.filter((p) => p && p.id != null)\n\t\t.map((p) => `${p.id}:${p.liveStatus?.id ?? \"\"}:${p.hash ?? \"\"}`)\n\t\t.sort()\n\t\t.join(\"|\");\n\n/** Decisión del settle adaptativo (unión discriminada: `wait` porta su `waitMs`). */\nexport type SettleDecision = { action: \"go\" } | { action: \"wait\"; waitMs: number };\n\n/**\n * Gate del settle ADAPTATIVO: decide si el backend ya está quiescente y el render puede arrancar.\n * - dos sondas consecutivas con la MISMA firma → `'go'` (quiescencia: nada se ha movido en un intervalo);\n * - `elapsedMs >= timings.settleMs` → `'go'` (techo: bajo carga patológica no se espera para siempre);\n * - resto → `'wait'` con `min(settlePollMs, settleMs - elapsedMs)` (el último wait no desborda el techo).\n * La PRIMERA sonda (`prevSignature = null`) nunca decide `'go'` por firma: hacen falta dos muestras para saber\n * que nada cambia. Con `settleMs <= 0` decide `'go'` inmediato (settle desactivado; el caller puede además\n * ahorrarse la sonda).\n */\nexport const decideSettleStep = (\n\tprevSignature: string | null,\n\tsignature: string,\n\telapsedMs: number,\n\ttimings: RenderTimings = DEFAULT_TIMINGS,\n): SettleDecision => {\n\tif (elapsedMs >= timings.settleMs) return { action: \"go\" };\n\tif (prevSignature !== null && prevSignature === signature) return { action: \"go\" };\n\treturn { action: \"wait\", waitMs: Math.min(timings.settlePollMs, timings.settleMs - elapsedMs) };\n};\n\n/** Clasificación de una página según su `hash`/`status`, resultado de `classifyPage`. */\nexport type PageClassification = \"publish\" | \"needsHash\" | \"skip\";\n\n/**\n * Clasifica una página por su HASH, como el CX real (griddo-cx store.ts:366-369): el TRABAJO = toda página\n * con `hash != null`, SIN filtrar por estado. `build/end` publica el structured_data_content (has_been_published,\n * que la API pública EXIGE vía includePendingPublishing:off) SOLO de los publishPagesIds/publishHashes que\n * recibe; una copia GLOBAL/traducción puede alcanzar live(3) con su CONTENIDO aún sin publicar → si la\n * SALTÁRAMOS por estar live(3) (como hacía el filtro `status===2` anterior), su contenido NUNCA se publicaría\n * y la pública la excluiría (bug de traducciones en CI). Por eso:\n * - `hash != null` → `'publish'` (se publica su contenido aunque ya esté live(3); el render es un solo pase);\n * - `hash == null` y `status === PENDING_PUBLISHING` → `'needsHash'` (el spread aún no asignó el hash);\n * - resto (p.ej. live(3) con hash=null = ya publicada y con el hash nulado por endBuild) → `'skip'`.\n */\nexport const classifyPage = (\n\thash: string | null | undefined,\n\tstatus: number | undefined,\n): PageClassification => {\n\tif (hash != null) return \"publish\";\n\tif (status === LIVE_STATUS.PENDING_PUBLISHING) return \"needsHash\";\n\treturn \"skip\";\n};\n\n/**\n * Diff SOFT por TÍTULO: dada la lista de páginas de los sites, devuelve los `wanted` que AÚN no están en\n * `expectedStatus` (ausentes del listado o en otro estado). Se compara POR TÍTULO y no por id porque publicar/\n * re-renderizar una GLOBAL canónica REASIGNA el id (memoria publish-canonical-global-page-reassigns-id) → un id\n * de createSuite (o de un build/start previo) queda stale; el listado del site es la entidad renderizada real.\n *\n * Preferencia `statusByTitle`: prefiere `expectedStatus` si alguna entidad con ese título ya lo tiene (una\n * global puede tener EDITOR + copia con distinto estado). El resultado (títulos que faltan) depende solo de si\n * ALGUNA entidad con ese título alcanzó `expectedStatus`.\n */\nexport const diffMissingByTitle = (\n\titems: PageInfo[],\n\twanted: string[],\n\texpectedStatus: number,\n): string[] => {\n\tconst w = wanted || [];\n\tif (w.length === 0) return [];\n\tconst statusByTitle: Record<string, number> = {};\n\titems.forEach((p) => {\n\t\tif (p && p.title != null && w.includes(p.title)) {\n\t\t\tconst st = p.liveStatus?.id;\n\t\t\tif (st === expectedStatus || !(p.title in statusByTitle)) statusByTitle[p.title] = st ?? -1;\n\t\t}\n\t});\n\treturn w.filter((t) => statusByTitle[t] !== expectedStatus);\n};\n\n/** Shaping del cuerpo de `POST site/{id}/build/end` (normaliza `siteHash` ausente a `null`). */\nexport const buildEndBody = (\n\tpublishHashes: string[],\n\tsiteHash: string | null | undefined,\n\tunpublishHashes: string[],\n\tpublishPagesIds: number[],\n): BuildEndBody => ({\n\tpublishHashes,\n\tsiteHash: siteHash ?? null,\n\tunpublishHashes,\n\tpublishPagesIds,\n});\n\n/**\n * Colección de re-pending de la MITIGACIÓN cross-job: de las páginas del listado, devuelve los ids in-site\n * (deduplicados, en orden de aparición) de los títulos `missing` que NO están live. El barrido global\n * INCONDICIONAL de endBuild (buildGlobalUnpublishAllWhere) puede despublicar globales QA de OTRO job → una\n * página offline ya NO la recoge build/start, así que re-renderizar no la recupera; re-pendearla (upload-pending)\n * la devuelve a pending(2) para que renderSiteToQuiescence la renderice. Acotado a copias in-site de `wanted`\n * (el caller solo pasa los títulos deseados-live que faltan) y solo `st !== LIVE`.\n */\nexport const collectRepublishIds = (items: PageInfo[], missing: string[]): number[] => {\n\tconst ids: number[] = [];\n\titems.forEach((p) => {\n\t\tconst st = p?.liveStatus?.id;\n\t\t// `p.title as string`: replica EXACTA de HEAD (`missing.includes(p.title)`, sin guard) — behavior-preserving\n\t\t// estricto; el cast solo satisface a tsc (items es PageInfo, no `any`), no cambia el runtime.\n\t\tif (p && p.id != null && missing.includes(p.title as string) && st !== LIVE_STATUS.LIVE)\n\t\t\tids.push(p.id);\n\t});\n\treturn Array.from(new Set(ids));\n};\n\n/**\n * Decisión del gate build/start-ready (unión discriminada: `retry` porta su `waitMs`, `proceed`/`fail` no).\n */\nexport type BuildStartDecision =\n\t| { action: \"proceed\" }\n\t| { action: \"retry\"; waitMs: number }\n\t| { action: \"fail\" };\n\n/**\n * Gate build/start-ready: replica el timing del CX real (que dispara ~1 min después, cuando el pathing ya\n * asentó, y nunca ve el 4xx transitorio) sin su latencia. El publish lanza el spread de las copias GLOBAL de\n * forma ASÍNCRONA; hasta que termina, una copia puede existir con `pathId=null` y build/start (que corre\n * fixAllPages → `pathId.split(...)`) devuelve 4xx TRANSITORIO. Por eso reintentamos SOLO mientras dé 4xx,\n * cortando al 1er 200; si agota el presupuesto, el shell asserta RUIDOSO (no un CypressError crudo).\n * - `status < 400` → `'proceed'`;\n * - agotado el presupuesto (`startWaits >= timings.buildStartMaxWaits`) → `'fail'`;\n * - resto → `'retry'` con backoff `timings.buildStartRetryWaitMs`.\n */\nexport const decideBuildStartStep = (\n\tstatus: number,\n\tstartWaits: number,\n\ttimings: RenderTimings = DEFAULT_TIMINGS,\n): BuildStartDecision => {\n\tif (status < 400) return { action: \"proceed\" };\n\tif (startWaits >= timings.buildStartMaxWaits) return { action: \"fail\" };\n\treturn { action: \"retry\", waitMs: timings.buildStartRetryWaitMs };\n};\n\n/** Forma mínima de un site en `GET sites/all`: lo justo para saber si tiene render pendiente. */\nexport interface SiteBuildInfo {\n\tid?: number;\n\t/** La API lo marca cuando el site tiene cambios sin renderizar (señal QA-scoped: hash pendiente, no bloqueado). */\n\tshouldBeUpdated?: boolean;\n}\n\n/**\n * Ids de los sites con render PENDIENTE — lo que el CX real busca en cada pasada de su poll.\n *\n * `onlyIds` acota el resultado (el shell le pasa los sites del test en curso). Esa acotación NO es cosmética:\n * el CX real es un único proceso global, pero el mock corre por test y a escala CI ~40 jobs comparten una BD;\n * sin acotar, cada pasada arrastraría el trabajo de los demás (build/start → fixAllPages es pesado) y la BD se\n * satura, tumbando incluso specs que no usan el mock.\n */\nexport const selectPendingSites = (sites: SiteBuildInfo[], onlyIds?: number[]): number[] => {\n\tconst ids: number[] = [];\n\t(sites || []).forEach((site) => {\n\t\tif (!site || site.id == null || !site.shouldBeUpdated) return;\n\t\tif (onlyIds && !onlyIds.includes(site.id)) return;\n\t\tids.push(site.id);\n\t});\n\treturn Array.from(new Set(ids));\n};\n","/**\n * Error del paquete, con código.\n *\n * Un único tipo de error con un `code` en lugar de una jerarquía de clases: los consumidores necesitan\n * distinguir *qué* falló para decidir (reintentar, avisar, abortar), y eso se hace igual de bien con un campo que\n * con `instanceof`, sin obligar a importar siete clases.\n */\n\n/** Códigos de {@link RenderError}. */\nexport const RENDER_ERROR_CODE = {\n\t/** `build/start` siguió respondiendo error hasta agotar los reintentos: el site no llega a estar listo. */\n\tBUILD_START_NOT_READY: \"BUILD_START_NOT_READY\",\n\t/** `build/end` respondió >= 400: el render no se ha aplicado y el bloqueo puede seguir puesto. */\n\tBUILD_END_FAILED: \"BUILD_END_FAILED\",\n\t/** Se agotaron las rondas y algunos títulos no alcanzaron el estado final. Mira `missingTitles`. */\n\tTITLES_NOT_FINAL: \"TITLES_NOT_FINAL\",\n\t/** No se pudo saber qué sites tenían render pendiente. */\n\tSITES_LOOKUP_FAILED: \"SITES_LOOKUP_FAILED\",\n\t/** No se pudo leer el listado de páginas de un site. */\n\tPAGE_LIST_FAILED: \"PAGE_LIST_FAILED\",\n\t/** Falló marcar páginas como pendientes de publicar. */\n\tMARK_PENDING_FAILED: \"MARK_PENDING_FAILED\",\n\t/** Falló publicar un site. */\n\tPUBLISH_FAILED: \"PUBLISH_FAILED\",\n} as const;\n\n/** Uno de los {@link RENDER_ERROR_CODE}. */\nexport type RenderErrorCode = (typeof RENDER_ERROR_CODE)[keyof typeof RENDER_ERROR_CODE];\n\n/** Datos opcionales que acompañan a un {@link RenderError}. */\nexport interface RenderErrorDetail {\n\t/** Status HTTP de la respuesta que provocó el error, si lo hubo. */\n\tstatus?: number;\n\t/** Body de esa respuesta, tal cual, para poder diagnosticar sin volver a reproducir. */\n\tbody?: unknown;\n\t/** Títulos que no alcanzaron su estado final. Solo en `TITLES_NOT_FINAL`. */\n\tmissingTitles?: string[];\n\t/** Error original, si este envuelve a otro. Se expone también como `cause` nativa. */\n\tcause?: unknown;\n}\n\n/**\n * Error de este paquete. Siempre trae `code`; el resto del detalle depende de dónde falló.\n *\n * ```ts\n * try {\n * \tawait engine.renderUntilLive([siteId], titles);\n * } catch (error) {\n * \tif (error instanceof RenderError && error.code === RENDER_ERROR_CODE.TITLES_NOT_FINAL) {\n * \t\tconsole.warn(\"no llegaron:\", error.missingTitles);\n * \t}\n * }\n * ```\n */\nexport class RenderError extends Error {\n\t/** Qué falló, de forma comprobable sin leer el mensaje. */\n\treadonly code: RenderErrorCode;\n\t/** Status HTTP asociado, si el error viene de una respuesta. */\n\treadonly status?: number;\n\t/** Body de la respuesta asociada. */\n\treadonly body?: unknown;\n\t/** Títulos que no alcanzaron su estado final (`TITLES_NOT_FINAL`). */\n\treadonly missingTitles?: string[];\n\n\tconstructor(code: RenderErrorCode, message: string, detail: RenderErrorDetail = {}) {\n\t\tsuper(message);\n\t\tthis.name = \"RenderError\";\n\t\tthis.code = code;\n\t\tthis.status = detail.status;\n\t\tthis.body = detail.body;\n\t\tthis.missingTitles = detail.missingTitles;\n\t\t// `cause` se asigna en lugar de pasarse al constructor: el segundo argumento de Error es ES2022, y así el\n\t\t// paquete sigue compilando contra librerías más antiguas sin perder el encadenado de errores en runtime.\n\t\tif (detail.cause !== undefined) (this as { cause?: unknown }).cause = detail.cause;\n\t}\n}\n","/**\n * Contrato de log mínimo, para que el paquete cuente qué está haciendo sin imponer una librería.\n *\n * Cuatro métodos y ninguna dependencia: encaja tal cual con `console`, y con pino, winston o el log del runner de\n * tests envolviéndolo en cuatro líneas. Si no pasas ninguno, el paquete no escribe nada.\n */\nexport interface Logger {\n\tdebug(message: string, meta?: unknown): void;\n\tinfo(message: string, meta?: unknown): void;\n\twarn(message: string, meta?: unknown): void;\n\terror(message: string, meta?: unknown): void;\n}\n\n/** Logger que descarta todo. Es el que se usa cuando no inyectas uno. */\nexport const NOOP_LOGGER: Logger = {\n\tdebug: () => undefined,\n\tinfo: () => undefined,\n\twarn: () => undefined,\n\terror: () => undefined,\n};\n\n/**\n * Logger sobre `console`, por comodidad en scripts y ejemplos.\n *\n * @param level nivel mínimo que se emite. Por defecto `info`, así que los `debug` se descartan.\n */\nexport const createConsoleLogger = (\n\tlevel: \"debug\" | \"info\" | \"warn\" | \"error\" = \"info\",\n): Logger => {\n\tconst order = { debug: 10, info: 20, warn: 30, error: 40 };\n\tconst min = order[level];\n\tconst emit =\n\t\t(method: \"debug\" | \"info\" | \"warn\" | \"error\") =>\n\t\t(message: string, meta?: unknown): void => {\n\t\t\tif (order[method] < min) return;\n\t\t\tif (meta === undefined) console[method](message);\n\t\t\telse console[method](message, meta);\n\t\t};\n\treturn { debug: emit(\"debug\"), info: emit(\"info\"), warn: emit(\"warn\"), error: emit(\"error\") };\n};\n","/**\n * Cliente interno sobre el transporte inyectado. **No es API pública**: no se exporta desde el índice del\n * paquete.\n *\n * Concentra las tres cosas que antes repetían el motor y el publicador: montar las cabeceras, componer la URL\n * a partir de `apiUrl`, y convertir una respuesta con error en un {@link RenderError} con su código. Así \"qué\n * cuenta como fallo\" se decide en un único sitio.\n */\n\nimport type { Transport, TransportResponse } from \"./transport\";\n\nimport { RenderError, type RenderErrorCode } from \"./errors\";\nimport { type Logger, NOOP_LOGGER } from \"./logger\";\n\n/** Lo que necesita cualquier pieza que hable con la API: el transporte, dónde está y con qué credencial. */\nexport interface ApiClientConfig {\n\ttransport: Transport;\n\tapiUrl: string;\n\tauthToken: string;\n\t/** Dónde contar lo que va pasando. Si no lo pasas, no se escribe nada. */\n\tlogger?: Logger;\n}\n\n/** Detalle de una llamada. Con `expectOk`, una respuesta >= 400 se convierte en error con ese código. */\nexport interface RequestSpec {\n\tbody?: unknown;\n\texpectOk?: RenderErrorCode;\n}\n\nexport class ApiClient {\n\treadonly logger: Logger;\n\tprivate readonly transport: Transport;\n\tprivate readonly apiUrl: string;\n\tprivate readonly headers: Record<string, string>;\n\n\tconstructor(config: ApiClientConfig) {\n\t\tthis.transport = config.transport;\n\t\tthis.apiUrl = config.apiUrl;\n\t\tthis.headers = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\tauthorization: `Bearer ${config.authToken}`,\n\t\t};\n\t\tthis.logger = config.logger ?? NOOP_LOGGER;\n\t}\n\n\t/** URL absoluta de un path relativo a `apiUrl`. Útil cuando la URL la compone el núcleo. */\n\turl(path: string): string {\n\t\treturn `${this.apiUrl}${path}`;\n\t}\n\n\tget(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"GET\", path, spec);\n\t}\n\n\tpost(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"POST\", path, spec);\n\t}\n\n\tput(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"PUT\", path, spec);\n\t}\n\n\t/**\n\t * Lanza si la respuesta trae error. Se usa cuando la llamada ya se hizo sin `expectOk` porque había que\n\t * inspeccionar el status antes de decidir.\n\t */\n\tensureOk(res: TransportResponse, code: RenderErrorCode, describe: string): void {\n\t\tif (res.status < 400) return;\n\t\tthrow new RenderError(code, `${describe} respondió ${res.status}`, {\n\t\t\tstatus: res.status,\n\t\t\tbody: res.body,\n\t\t});\n\t}\n\n\tprivate async send(\n\t\tmethod: \"GET\" | \"POST\" | \"PUT\",\n\t\tpath: string,\n\t\tspec: RequestSpec,\n\t): Promise<TransportResponse> {\n\t\tconst url = path.startsWith(\"http\") ? path : this.url(path);\n\t\tconst opts = { headers: this.headers, body: spec.body };\n\t\tconst res = await (method === \"GET\"\n\t\t\t? this.transport.get(url, opts)\n\t\t\t: method === \"POST\"\n\t\t\t\t? this.transport.post(url, opts)\n\t\t\t\t: this.transport.put(url, opts));\n\t\tif (spec.expectOk) this.ensureOk(res, spec.expectOk, `${method} ${path}`);\n\t\treturn res;\n\t}\n}\n","/** Espera de `ms` milisegundos. Es el `sleep` por defecto de todo el paquete; los tests inyectan un no-op. */\nexport const defaultSleep = (ms: number): Promise<void> =>\n\tnew Promise((resolve) => setTimeout(resolve, ms));\n","/**\n * Motor del protocolo de render: `build/start` → clasificar páginas por hash → `build/end`, con reintento por\n * rondas hasta que los títulos pedidos alcanzan su estado final.\n *\n * Es una clase porque tiene estado (configuración, cliente HTTP) y dependencias que conviene invertir: el HTTP y\n * el log. Las decisiones sin estado viven en `./core` como funciones puras; hablar con la API es cosa de\n * `./api-client`.\n */\n\nimport type { Logger } from \"./logger\";\n\nimport { ApiClient, type ApiClientConfig } from \"./api-client\";\nimport * as cxCore from \"./core\";\nimport { RENDER_ERROR_CODE, RenderError } from \"./errors\";\nimport { defaultSleep } from \"./sleep\";\n\n/** Dependencias del motor, inyectadas por constructor. */\nexport interface EngineDeps extends ApiClientConfig {\n\t/** Espera de `ms` milisegundos. Por defecto, `setTimeout`; inyecta un no-op para que los tests no esperen. */\n\tsleep?: (ms: number) => Promise<void>;\n\t/** Esperas y topes del render. Lo que no indiques se toma de {@link cxCore.DEFAULT_TIMINGS}. */\n\ttimings?: Partial<cxCore.RenderTimings>;\n\t/** Paginación del listado de páginas. Lo que no indiques se toma de {@link cxCore.DEFAULT_PAGE_LIST_QUERY}. */\n\tpageList?: Partial<cxCore.PageListQuery>;\n}\n\n/**\n * Instánciala una vez con la configuración del entorno y reutilízala:\n *\n * ```ts\n * const engine = new CxRenderEngine({ transport, apiUrl, authToken });\n * await engine.renderUntilLive([siteId], [\"Mi página\"]);\n * ```\n */\nexport class CxRenderEngine {\n\tprivate readonly api: ApiClient;\n\tprivate readonly logger: Logger;\n\tprivate readonly sleep: (ms: number) => Promise<void>;\n\tprivate readonly timings: cxCore.RenderTimings;\n\tprivate readonly pageList: cxCore.PageListQuery;\n\n\tconstructor(deps: EngineDeps) {\n\t\tthis.api = new ApiClient(deps);\n\t\tthis.logger = this.api.logger;\n\t\tthis.sleep = deps.sleep ?? defaultSleep;\n\t\tthis.timings = { ...cxCore.DEFAULT_TIMINGS, ...deps.timings };\n\t\tthis.pageList = { ...cxCore.DEFAULT_PAGE_LIST_QUERY, ...deps.pageList };\n\t}\n\n\t/**\n\t * Renderiza un site en **una sola pasada**: settle adaptativo → `build/start` → `GET page/{id}` →\n\t * `build/end`.\n\t *\n\t * El settle previo da tiempo a que el backend termine de propagar las copias de las páginas globales; sin él,\n\t * `build/start` devolvería una lista incompleta. Es ADAPTATIVO: sondea el listado hasta que dos sondas\n\t * consecutivas coinciden (quiescencia) o hasta el techo `timings.settleMs` — rápido cuando el backend va\n\t * suelto, paciente bajo carga. Las páginas que lleguen tarde no se reintentan aquí: de eso se encarga\n\t * {@link renderUntilLive}.\n\t *\n\t * ⚠️ `build/start` deja el site bloqueado y `build/end` lo libera. En el camino normal `build/end` se emite\n\t * siempre, pero si el transporte rechaza entre ambos el bloqueo queda colgado durante horas: llama a\n\t * {@link releaseRenderLock} en tu `finally`.\n\t *\n\t * @throws {RenderError} `BUILD_START_NOT_READY` si el site no llega a estar listo, `BUILD_END_FAILED` si el\n\t * render no se aplica.\n\t */\n\tasync renderSiteOnce(siteId: number): Promise<void> {\n\t\tconst startOnce = async (startWaits = 0): Promise<void> => {\n\t\t\t// Sin `expectOk`: un 4xx aquí puede ser transitorio y lo decide el núcleo, no es un fallo todavía.\n\t\t\tconst bs = await this.api.get(`site/${siteId}/build/start`);\n\t\t\tconst startStep = cxCore.decideBuildStartStep(bs.status, startWaits, this.timings);\n\t\t\tif (startStep.action === \"fail\") {\n\t\t\t\tthrow new RenderError(\n\t\t\t\t\tRENDER_ERROR_CODE.BUILD_START_NOT_READY,\n\t\t\t\t\t`GET site/${siteId}/build/start sigue en ${bs.status} tras ${startWaits} esperas (¿el site no acaba de asentarse?)`,\n\t\t\t\t\t{ status: bs.status, body: bs.body },\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (startStep.action === \"retry\") {\n\t\t\t\tthis.logger.debug(\n\t\t\t\t\t`build/start del site ${siteId} en ${bs.status}: espera ${startStep.waitMs}ms y reintenta`,\n\t\t\t\t);\n\t\t\t\tawait this.sleep(startStep.waitMs);\n\t\t\t\treturn startOnce(startWaits + 1);\n\t\t\t}\n\n\t\t\tconst siteHash: string | null = bs.body?.siteHash || null;\n\t\t\tconst publishIds: number[] = bs.body?.publishIds || [];\n\t\t\tconst unpublishHashes: string[] = bs.body?.unpublishHashes || [];\n\t\t\tthis.logger.debug(\n\t\t\t\t`site ${siteId}: ${publishIds.length} páginas candidatas, ${unpublishHashes.length} a despublicar`,\n\t\t\t);\n\n\t\t\t// El trabajo es toda página con hash, sin mirar su estado (ver classifyPage). Una sola pasada, así que no\n\t\t\t// hace falta deduplicar ids; las páginas que aún no tienen hash las recoge renderUntilLive.\n\t\t\tconst publishHashes: string[] = [];\n\t\t\tconst publishPagesIds: number[] = [];\n\t\t\tfor (const pageId of publishIds) {\n\t\t\t\tconst pres = await this.api.get(`page/${pageId}`);\n\t\t\t\tconst hash = pres.body?.hash;\n\t\t\t\tconst status = pres.body?.liveStatus?.id;\n\t\t\t\tif (cxCore.classifyPage(hash, status) === \"publish\") {\n\t\t\t\t\tpublishHashes.push(hash);\n\t\t\t\t\tpublishPagesIds.push(pageId);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait this.api.post(`site/${siteId}/build/end`, {\n\t\t\t\tbody: cxCore.buildEndBody(publishHashes, siteHash, unpublishHashes, publishPagesIds),\n\t\t\t\texpectOk: RENDER_ERROR_CODE.BUILD_END_FAILED,\n\t\t\t});\n\t\t\tthis.logger.info(`site ${siteId} renderizado: ${publishPagesIds.length} páginas publicadas`);\n\t\t};\n\n\t\t// Asentamiento primero, luego una sola pasada.\n\t\tawait this.settleBeforeStart(siteId);\n\t\treturn startOnce();\n\t}\n\n\t/**\n\t * Settle ADAPTATIVO previo al build/start: sondea el listado del site cada `timings.settlePollMs` y arranca\n\t * cuando dos sondas consecutivas devuelven la misma firma (`cxCore.pageListSignature`) o al agotar el techo\n\t * `timings.settleMs` (la decisión es de `cxCore.decideSettleStep`). Best-effort: si el listado falla durante\n\t * el settle, se procede al render en vez de morir aquí (peor caso = el de siempre, un pase incompleto que\n\t * recogen las rondas de `renderUntilLive`). Con `settleMs <= 0` no sondea (settle desactivado).\n\t */\n\tprivate async settleBeforeStart(siteId: number): Promise<void> {\n\t\tif (this.timings.settleMs <= 0) return;\n\t\ttry {\n\t\t\tlet prev: string | null = null;\n\t\t\tlet elapsed = 0;\n\t\t\tfor (;;) {\n\t\t\t\tconst sig = cxCore.pageListSignature(await this.listPages([siteId]));\n\t\t\t\tconst step = cxCore.decideSettleStep(prev, sig, elapsed, this.timings);\n\t\t\t\tif (step.action === \"go\") return;\n\t\t\t\tprev = sig;\n\t\t\t\tawait this.sleep(step.waitMs);\n\t\t\t\telapsed += step.waitMs;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.logger.warn(\n\t\t\t\t`settle del site ${siteId}: el sondeo del listado falló (${err instanceof Error ? err.message : String(err)}); se procede al render sin asentar`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Renderiza **todos los sites que tengan render pendiente**, que es lo que hace el render real en cada pasada:\n\t * pregunta al backend qué sites tienen cambios sin renderizar (`GET sites/all` → `shouldBeUpdated`) y los\n\t * resuelve uno por uno.\n\t *\n\t * @param onlyIds acota a esos sites. **Úsalo si compartes la base de datos con otros procesos**: sin acotar,\n\t * esta pasada arrastra también el trabajo pendiente de los demás, que es tan lento como caro.\n\t * @returns los ids que ha renderizado.\n\t * @throws {RenderError} `SITES_LOOKUP_FAILED` si no se puede consultar la lista de sites.\n\t */\n\tasync renderPendingSites(onlyIds?: number[]): Promise<number[]> {\n\t\tconst res = await this.api.get(\"sites/all\", {\n\t\t\texpectOk: RENDER_ERROR_CODE.SITES_LOOKUP_FAILED,\n\t\t});\n\t\tconst all: cxCore.SiteBuildInfo[] = Array.isArray(res.body) ? res.body : res.body?.items || [];\n\t\tconst pending = cxCore.selectPendingSites(all, onlyIds);\n\t\tthis.logger.info(\n\t\t\tpending.length > 0\n\t\t\t\t? `sites con render pendiente: ${pending.join(\", \")}`\n\t\t\t\t: \"ningún site con render pendiente\",\n\t\t);\n\t\tfor (const siteId of pending) await this.renderSiteOnce(siteId);\n\t\treturn pending;\n\t}\n\n\t/**\n\t * Lleva los títulos pedidos a su estado final, reintentando lo que falte.\n\t *\n\t * Mientras queden títulos pendientes, vuelve a marcar como pendientes las copias que se hayan quedado\n\t * despublicadas y renderiza otra vez, hasta `timings.maxRounds` rondas; la última confirma o lanza. Si todo\n\t * está ya en su sitio, la primera comprobación pasa y no renderiza nada.\n\t *\n\t * @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`; usa `LIVE_STATUS.UNPUBLISHED`\n\t * para confirmar una despublicación.\n\t * @throws {RenderError} `TITLES_NOT_FINAL` si tras la última ronda sigue faltando algún título.\n\t */\n\tasync renderUntilLive(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return;\n\n\t\tconst attempt = async (round: number): Promise<void> => {\n\t\t\tconst missing = await this.missingPagesByTitle(siteIds, wanted, expectedStatus);\n\t\t\tif (missing.length === 0) return;\n\t\t\tif (round >= this.timings.maxRounds)\n\t\t\t\treturn this.confirmPagesByTitle(siteIds, wanted, expectedStatus);\n\t\t\tthis.logger.warn(\n\t\t\t\t`ronda ${round}/${this.timings.maxRounds}: faltan ${missing.length}/${wanted.length} títulos (${missing.join(\", \")})`,\n\t\t\t);\n\t\t\tawait this.republishMissingWantedLive(siteIds, missing, expectedStatus);\n\t\t\tfor (const siteId of siteIds) await this.renderSiteOnce(siteId);\n\t\t\tawait this.sleep(this.timings.roundWaitMs);\n\t\t\treturn attempt(round + 1);\n\t\t};\n\t\treturn attempt(1);\n\t}\n\n\t/**\n\t * Comprueba, sin renderizar, que los títulos están en `expectedStatus`.\n\t *\n\t * @throws {RenderError} `TITLES_NOT_FINAL`, con los títulos que faltan en `missingTitles`.\n\t */\n\tasync confirmPagesByTitle(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return;\n\t\tconst missing = await this.missingPagesByTitle(siteIds, wanted, expectedStatus);\n\t\tif (missing.length > 0) {\n\t\t\tthrow new RenderError(\n\t\t\t\tRENDER_ERROR_CODE.TITLES_NOT_FINAL,\n\t\t\t\t`estos títulos no están en liveStatus ${expectedStatus}: ${missing.join(\", \")}`,\n\t\t\t\t{\n\t\t\t\t\tmissingTitles: missing,\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Libera el bloqueo de render que deja `build/start`, emitiendo un `build/end` vacío.\n\t *\n\t * Llámalo en tu `finally` si un render puede abortar a medias. Es best-effort: no lanza aunque el backend\n\t * responda con error, porque el caso normal es que ya no hubiera nada que liberar.\n\t */\n\tasync releaseRenderLock(siteId: number): Promise<void> {\n\t\tconst res = await this.api.post(`site/${siteId}/build/end`, {\n\t\t\tbody: cxCore.buildEndBody([], null, [], []),\n\t\t});\n\t\tif (res.status >= 400) {\n\t\t\tthis.logger.debug(\n\t\t\t\t`releaseRenderLock del site ${siteId}: el backend respondió ${res.status} (probablemente no había bloqueo)`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Títulos que aún no están en `expectedStatus`, leyendo el listado completo de cada site. */\n\tprivate async missingPagesByTitle(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number,\n\t): Promise<string[]> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return [];\n\t\tconst items = await this.listPages(siteIds);\n\t\treturn cxCore.diffMissingByTitle(items, wanted, expectedStatus);\n\t}\n\n\t/** Devuelve a \"pendiente de publicar\" las copias despublicadas de los títulos que faltan. */\n\tprivate async republishMissingWantedLive(\n\t\tsiteIds: number[],\n\t\tmissing: string[],\n\t\texpectedStatus: number,\n\t): Promise<void> {\n\t\tif (expectedStatus !== cxCore.LIVE_STATUS.LIVE || missing.length === 0) return;\n\t\tconst items = await this.listPages(siteIds);\n\t\tconst uniqueIds = cxCore.collectRepublishIds(items, missing);\n\t\tif (uniqueIds.length === 0) return;\n\t\tthis.logger.info(`re-marcando ${uniqueIds.length} páginas como pendientes de publicar`);\n\t\tawait this.api.put(\"pages/status/upload-pending\", { body: { ids: uniqueIds } });\n\t}\n\n\t/**\n\t * Páginas de los sites indicados, **paginando hasta el final** del listado.\n\t *\n\t * Recorre páginas mientras el backend devuelva el lote completo: si llega uno más corto, es el último. Así un\n\t * site con más páginas que `pageList.itemsPerPage` no se queda a medias, que era la causa de confirmaciones\n\t * por título erróneas cuando solo se leía la primera página.\n\t *\n\t * @throws {RenderError} `PAGE_LIST_FAILED` si el backend no puede devolver el listado.\n\t */\n\tprivate async listPages(siteIds: number[]): Promise<cxCore.PageInfo[]> {\n\t\tconst items: cxCore.PageInfo[] = [];\n\t\tfor (const siteId of siteIds) {\n\t\t\tlet page = this.pageList.page;\n\t\t\t// Tope de seguridad: con el itemsPerPage por defecto son 300.000 páginas. Si se alcanza, algo va mal en\n\t\t\t// el backend (por ejemplo, ignora el parámetro `page` y devuelve siempre el mismo lote).\n\t\t\tfor (let fetched = 0; fetched < MAX_LIST_PAGES; fetched++) {\n\t\t\t\tconst query = { page, itemsPerPage: this.pageList.itemsPerPage };\n\t\t\t\tconst res = await this.api.get(cxCore.listPageUrl(this.api.url(\"\"), siteId, query), {\n\t\t\t\t\texpectOk: RENDER_ERROR_CODE.PAGE_LIST_FAILED,\n\t\t\t\t});\n\t\t\t\tconst batch: cxCore.PageInfo[] = (res.body && (res.body.items || res.body)) || [];\n\t\t\t\titems.push(...batch);\n\t\t\t\tif (batch.length < this.pageList.itemsPerPage) break;\n\t\t\t\tpage += 1;\n\t\t\t\tif (fetched === MAX_LIST_PAGES - 1) {\n\t\t\t\t\tthis.logger.warn(\n\t\t\t\t\t\t`listado del site ${siteId} cortado en ${MAX_LIST_PAGES} páginas: ¿el backend ignora el parámetro page?`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn items;\n\t}\n}\n\n/** Tope de páginas que se piden por site antes de asumir que el backend no pagina bien. */\nconst MAX_LIST_PAGES = 1000;\n","/**\n * Publicación: marcar páginas como pendientes y publicar sites.\n *\n * Ojo con el reparto de responsabilidades: **esto no es parte del render**. En producción lo dispara la persona\n * que pulsa \"Publicar\" en el CMS; el render solo reacciona a lo que ya está pendiente. Vive en este paquete como\n * conveniencia para montar el escenario desde un script, no porque el render lo necesite.\n *\n * El orden importa: primero se marcan las páginas, luego se publica el site. Publicar es lo que hace que el\n * backend las incluya en el siguiente `build/start`.\n */\n\nimport type { Logger } from \"./logger\";\n\nimport { ApiClient, type ApiClientConfig } from \"./api-client\";\nimport { RENDER_ERROR_CODE } from \"./errors\";\n\n/** Dependencias del publicador, inyectadas por constructor. Las mismas que el motor necesita para hablar con la API. */\nexport type PublisherDeps = ApiClientConfig;\n\n/**\n * Dispara la publicación en el CMS. Complementa al motor de render: este deja el trabajo pendiente, el motor lo\n * resuelve.\n *\n * ```ts\n * const publisher = new CmsPublisher({ transport, apiUrl, authToken });\n * await publisher.publishPages([siteId], [pageId]);\n * await engine.renderUntilLive([siteId], [\"Mi página\"]);\n * ```\n */\nexport class CmsPublisher {\n\tprivate readonly api: ApiClient;\n\tprivate readonly logger: Logger;\n\n\tconstructor(deps: PublisherDeps) {\n\t\tthis.api = new ApiClient(deps);\n\t\tthis.logger = this.api.logger;\n\t}\n\n\t/**\n\t * Marca páginas como pendientes de publicar. Sin ids no hace nada.\n\t *\n\t * @throws {RenderError} `MARK_PENDING_FAILED` si el backend responde >= 400.\n\t */\n\tasync markPagesPending(pageIds: number[]): Promise<void> {\n\t\tif (!pageIds || pageIds.length === 0) return;\n\t\tawait this.api.put(\"pages/status/upload-pending\", {\n\t\t\tbody: { ids: pageIds },\n\t\t\texpectOk: RENDER_ERROR_CODE.MARK_PENDING_FAILED,\n\t\t});\n\t\tthis.logger.info(`${pageIds.length} páginas marcadas como pendientes de publicar`);\n\t}\n\n\t/**\n\t * Publica los sites indicados, uno por uno.\n\t *\n\t * @throws {RenderError} `PUBLISH_FAILED` si el backend responde >= 400 para alguno.\n\t */\n\tasync publishSites(siteIds: number[]): Promise<void> {\n\t\tfor (const siteId of siteIds || []) {\n\t\t\tawait this.api.post(`site/${siteId}/publish`, { expectOk: RENDER_ERROR_CODE.PUBLISH_FAILED });\n\t\t\tthis.logger.info(`site ${siteId} publicado`);\n\t\t}\n\t}\n\n\t/**\n\t * Los dos pasos en orden: marca las páginas y publica los sites. Es el disparo completo que deja el trabajo\n\t * listo para que el motor lo renderice.\n\t */\n\tasync publishPages(siteIds: number[], pageIds: number[]): Promise<void> {\n\t\tawait this.markPagesPending(pageIds);\n\t\tawait this.publishSites(siteIds);\n\t}\n}\n","/**\n * Fachada: construye motor y publicador con una sola configuración, y encapsula el flujo completo.\n *\n * Existe por dos motivos concretos, no por simetría: instanciar las dos clases obliga a repetir las mismas tres\n * dependencias, y el flujo tiene un orden que hay que conocer (publicar → renderizar → confirmar, liberando el\n * bloqueo si algo se rompe a medias). Si necesitas solo una parte, usa `CxRenderEngine` o `CmsPublisher`\n * directamente: siguen expuestos.\n */\nimport * as cxCore from \"./core\";\nimport { CxRenderEngine, type EngineDeps } from \"./engine\";\nimport { CmsPublisher } from \"./publish\";\n\n/** Configuración de la fachada: la del motor, que es un superconjunto de la del publicador. */\nexport type CxRenderMockDeps = EngineDeps;\n\n/**\n * Punto de entrada recomendado.\n *\n * ```ts\n * const cx = new CxRenderMock({ transport, apiUrl, authToken, logger });\n * await cx.publishAndRender([siteId], [pageId], [\"Mi página\"]);\n * ```\n */\nexport class CxRenderMock {\n\t/** El motor de render, por si necesitas sus métodos sueltos. */\n\treadonly engine: CxRenderEngine;\n\t/** El disparo de publicación, por si quieres publicar sin renderizar. */\n\treadonly publisher: CmsPublisher;\n\n\tconstructor(deps: CxRenderMockDeps) {\n\t\tthis.engine = new CxRenderEngine(deps);\n\t\tthis.publisher = new CmsPublisher(deps);\n\t}\n\n\t/**\n\t * El flujo entero: publica, renderiza y confirma que los títulos llegaron a su estado final. Libera el bloqueo\n\t * de render de cada site si algo falla a medias.\n\t *\n\t * @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`.\n\t * @throws {RenderError} con el `code` de la etapa que falló.\n\t */\n\tasync publishAndRender(\n\t\tsiteIds: number[],\n\t\tpageIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tawait this.publisher.publishPages(siteIds, pageIds);\n\t\ttry {\n\t\t\tawait this.engine.renderUntilLive(siteIds, titles, expectedStatus);\n\t\t} catch (error) {\n\t\t\t// El bloqueo lo pone build/start y lo quita build/end. Si el render murió entre ambos, aquí es donde se\n\t\t\t// suelta; si ya no había bloqueo, releaseRenderLock no protesta.\n\t\t\tfor (const siteId of siteIds) await this.engine.releaseRenderLock(siteId);\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n","/**\n * Contrato del transporte HTTP y decoradores opcionales para envolverlo.\n *\n * El transporte es la única dependencia de runtime del paquete, y se inyecta: así no arrastra ningún cliente\n * HTTP y funciona igual en Node, en un bundler o dentro de un runner de tests. Los decoradores son funciones que\n * reciben un transporte y devuelven otro con el mismo contrato, de modo que reintentos, timeouts o trazas se\n * añaden componiendo, sin que el motor sepa nada.\n */\nimport { type Logger, NOOP_LOGGER } from \"./logger\";\nimport { defaultSleep } from \"./sleep\";\n\n/** Respuesta HTTP normalizada: status y body ya parseado. */\nexport interface TransportResponse {\n\tstatus: number;\n\tbody: any;\n}\n\n/** Opciones de una request: cabeceras y body ya serializable. */\nexport interface RequestOptions {\n\theaders?: Record<string, string>;\n\tbody?: unknown;\n}\n\n/**\n * Transporte HTTP inyectable. Impleméntalo sobre `fetch`, `axios`, `undici` o lo que uses.\n *\n * ⚠️ **No debe lanzar en 4xx/5xx**: devuelve `{ status, body }` y deja que el motor decida. Es deliberado: un\n * 4xx de `build/start` es una respuesta esperada (la ruta del site aún no está lista), no un fallo. Un cliente\n * que lance en error — como los que devuelven el body deserializado y tiran en no-2xx — necesita un adaptador\n * que capture y traduzca a `{ status, body }`.\n */\nexport interface Transport {\n\tget(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n\tpost(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n\tput(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n}\n\n/** Métodos que un decorador puede tratar de forma distinta. */\nexport type TransportMethod = \"get\" | \"post\" | \"put\";\n\n/** Ajustes de {@link withRetry}. */\nexport interface RetryOptions {\n\t/** Intentos totales, incluido el primero. Por defecto 3. */\n\tattempts?: number;\n\t/** Espera entre intentos (ms). Por defecto 500, y crece linealmente con el intento. */\n\tdelayMs?: number;\n\t/**\n\t * Métodos que se reintentan. Por defecto **solo `get`**.\n\t *\n\t * No es timidez: `build/start` toma un bloqueo del site y `build/end` lo libera, así que repetir a ciegas una\n\t * escritura puede dejar el protocolo a medias. Amplíalo solo si sabes que tu caso lo tolera.\n\t */\n\tmethods?: TransportMethod[];\n\t/** Qué respuesta merece otro intento. Por defecto, los 5xx (un 4xx no se arregla repitiendo). */\n\tshouldRetry?: (res: TransportResponse) => boolean;\n\t/** Espera inyectable, para tests instantáneos. */\n\tsleep?: (ms: number) => Promise<void>;\n\tlogger?: Logger;\n}\n\n/**\n * Envuelve un transporte para que reintente los fallos pasajeros: una respuesta 5xx o un rechazo del cliente\n * (red caída, DNS, socket cerrado). Agotados los intentos, deja pasar la última respuesta o relanza el error,\n * para que quien llama decida.\n *\n * ```ts\n * const transport = withRetry(myTransport, { attempts: 4 });\n * ```\n */\nexport const withRetry = (transport: Transport, opts: RetryOptions = {}): Transport => {\n\tconst attempts = opts.attempts ?? 3;\n\tconst delayMs = opts.delayMs ?? 500;\n\tconst methods = opts.methods ?? [\"get\"];\n\tconst shouldRetry = opts.shouldRetry ?? ((res: TransportResponse) => res.status >= 500);\n\tconst sleep = opts.sleep ?? defaultSleep;\n\tconst logger = opts.logger ?? NOOP_LOGGER;\n\n\tconst run = async (\n\t\tmethod: TransportMethod,\n\t\turl: string,\n\t\treqOpts?: RequestOptions,\n\t): Promise<TransportResponse> => {\n\t\tif (!methods.includes(method)) return transport[method](url, reqOpts);\n\n\t\tlet lastError: unknown;\n\t\tfor (let attempt = 1; attempt <= attempts; attempt++) {\n\t\t\ttry {\n\t\t\t\tconst res = await transport[method](url, reqOpts);\n\t\t\t\tif (attempt === attempts || !shouldRetry(res)) return res;\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`retry ${method.toUpperCase()} ${url}: status ${res.status}, intento ${attempt}/${attempts}`,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tlastError = error;\n\t\t\t\tif (attempt === attempts) throw error;\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`retry ${method.toUpperCase()} ${url}: el transporte falló, intento ${attempt}/${attempts}`,\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait sleep(delayMs * attempt);\n\t\t}\n\t\t// Inalcanzable: el último intento sale por return o por throw. Está por si `attempts` llega como 0.\n\t\tthrow (\n\t\t\tlastError ??\n\t\t\tnew Error(`withRetry: ningún intento ejecutado para ${method.toUpperCase()} ${url}`)\n\t\t);\n\t};\n\n\treturn {\n\t\tget: (url, reqOpts) => run(\"get\", url, reqOpts),\n\t\tpost: (url, reqOpts) => run(\"post\", url, reqOpts),\n\t\tput: (url, reqOpts) => run(\"put\", url, reqOpts),\n\t};\n};\n\n/** Ajustes de {@link withTimeout}. */\nexport interface TimeoutOptions {\n\t/** Milisegundos antes de dar la request por perdida. */\n\tms: number;\n\tlogger?: Logger;\n}\n\n/**\n * Envuelve un transporte para que ninguna request se quede colgada para siempre: pasado el plazo, rechaza.\n *\n * ⚠️ **No cancela la request subyacente**, solo deja de esperarla: el paquete no conoce tu cliente HTTP, así que\n * no puede abortarlo. Si necesitas cancelación real, implementa el plazo dentro de tu propio transporte con un\n * `AbortController`.\n */\nexport const withTimeout = (transport: Transport, opts: TimeoutOptions): Transport => {\n\tconst logger = opts.logger ?? NOOP_LOGGER;\n\n\tconst race = (\n\t\tmethod: TransportMethod,\n\t\turl: string,\n\t\treqOpts?: RequestOptions,\n\t): Promise<TransportResponse> => {\n\t\tlet timer: ReturnType<typeof setTimeout>;\n\t\tconst timeout = new Promise<never>((_resolve, reject) => {\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tlogger.warn(`timeout ${method.toUpperCase()} ${url}: sin respuesta en ${opts.ms}ms`);\n\t\t\t\treject(new Error(`Timeout de ${opts.ms}ms en ${method.toUpperCase()} ${url}`));\n\t\t\t}, opts.ms);\n\t\t});\n\t\treturn Promise.race([transport[method](url, reqOpts), timeout]).finally(() =>\n\t\t\tclearTimeout(timer),\n\t\t);\n\t};\n\n\treturn {\n\t\tget: (url, reqOpts) => race(\"get\", url, reqOpts),\n\t\tpost: (url, reqOpts) => race(\"post\", url, reqOpts),\n\t\tput: (url, reqOpts) => race(\"put\", url, reqOpts),\n\t};\n};\n"],"mappings":";AAoBO,IAAM,cAAc;AAAA,EAC1B,aAAa;AAAA;AAAA,EACb,oBAAoB;AAAA;AAAA,EACpB,MAAM;AAAA;AAAA,EACN,sBAAsB;AAAA;AACvB;AA0DO,IAAM,kBAAiC;AAAA,EAC7C,UAAU;AAAA,EACV,cAAc;AAAA,EACd,WAAW;AAAA,EACX,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,uBAAuB;AACxB;AASO,IAAM,0BAAyC,EAAE,MAAM,GAAG,cAAc,IAAI;AAQ5E,IAAM,cAAc,CAC1B,QACA,QACA,QAAuB,4BAEvB,GAAG,MAAM,QAAQ,MAAM,6BAA6B,MAAM,IAAI,iBAAiB,MAAM,YAAY;AAQ3F,IAAM,oBAAoB,CAAC,WAChC,SAAS,CAAC,GACT,OAAO,CAAC,MAAM,KAAK,EAAE,MAAM,IAAI,EAC/B,IAAI,CAAC,MAAM,GAAG,EAAE,EAAE,IAAI,EAAE,YAAY,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAC9D,KAAK,EACL,KAAK,GAAG;AAcJ,IAAM,mBAAmB,CAC/B,eACA,WACA,WACA,UAAyB,oBACL;AACpB,MAAI,aAAa,QAAQ,SAAU,QAAO,EAAE,QAAQ,KAAK;AACzD,MAAI,kBAAkB,QAAQ,kBAAkB,UAAW,QAAO,EAAE,QAAQ,KAAK;AACjF,SAAO,EAAE,QAAQ,QAAQ,QAAQ,KAAK,IAAI,QAAQ,cAAc,QAAQ,WAAW,SAAS,EAAE;AAC/F;AAgBO,IAAM,eAAe,CAC3B,MACA,WACwB;AACxB,MAAI,QAAQ,KAAM,QAAO;AACzB,MAAI,WAAW,YAAY,mBAAoB,QAAO;AACtD,SAAO;AACR;AAYO,IAAM,qBAAqB,CACjC,OACA,QACA,mBACc;AACd,QAAM,IAAI,UAAU,CAAC;AACrB,MAAI,EAAE,WAAW,EAAG,QAAO,CAAC;AAC5B,QAAM,gBAAwC,CAAC;AAC/C,QAAM,QAAQ,CAAC,MAAM;AACpB,QAAI,KAAK,EAAE,SAAS,QAAQ,EAAE,SAAS,EAAE,KAAK,GAAG;AAChD,YAAM,KAAK,EAAE,YAAY;AACzB,UAAI,OAAO,kBAAkB,EAAE,EAAE,SAAS,eAAgB,eAAc,EAAE,KAAK,IAAI,MAAM;AAAA,IAC1F;AAAA,EACD,CAAC;AACD,SAAO,EAAE,OAAO,CAAC,MAAM,cAAc,CAAC,MAAM,cAAc;AAC3D;AAGO,IAAM,eAAe,CAC3B,eACA,UACA,iBACA,qBACmB;AAAA,EACnB;AAAA,EACA,UAAU,YAAY;AAAA,EACtB;AAAA,EACA;AACD;AAUO,IAAM,sBAAsB,CAAC,OAAmB,YAAgC;AACtF,QAAM,MAAgB,CAAC;AACvB,QAAM,QAAQ,CAAC,MAAM;AACpB,UAAM,KAAK,GAAG,YAAY;AAG1B,QAAI,KAAK,EAAE,MAAM,QAAQ,QAAQ,SAAS,EAAE,KAAe,KAAK,OAAO,YAAY;AAClF,UAAI,KAAK,EAAE,EAAE;AAAA,EACf,CAAC;AACD,SAAO,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC;AAC/B;AAoBO,IAAM,uBAAuB,CACnC,QACA,YACA,UAAyB,oBACD;AACxB,MAAI,SAAS,IAAK,QAAO,EAAE,QAAQ,UAAU;AAC7C,MAAI,cAAc,QAAQ,mBAAoB,QAAO,EAAE,QAAQ,OAAO;AACtE,SAAO,EAAE,QAAQ,SAAS,QAAQ,QAAQ,sBAAsB;AACjE;AAiBO,IAAM,qBAAqB,CAAC,OAAwB,YAAiC;AAC3F,QAAM,MAAgB,CAAC;AACvB,GAAC,SAAS,CAAC,GAAG,QAAQ,CAAC,SAAS;AAC/B,QAAI,CAAC,QAAQ,KAAK,MAAM,QAAQ,CAAC,KAAK,gBAAiB;AACvD,QAAI,WAAW,CAAC,QAAQ,SAAS,KAAK,EAAE,EAAG;AAC3C,QAAI,KAAK,KAAK,EAAE;AAAA,EACjB,CAAC;AACD,SAAO,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC;AAC/B;;;ACnRO,IAAM,oBAAoB;AAAA;AAAA,EAEhC,uBAAuB;AAAA;AAAA,EAEvB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA;AAAA,EAElB,qBAAqB;AAAA;AAAA,EAErB,kBAAkB;AAAA;AAAA,EAElB,qBAAqB;AAAA;AAAA,EAErB,gBAAgB;AACjB;AA8BO,IAAM,cAAN,cAA0B,MAAM;AAAA,EAUtC,YAAY,MAAuB,SAAiB,SAA4B,CAAC,GAAG;AACnF,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,SAAS,OAAO;AACrB,SAAK,OAAO,OAAO;AACnB,SAAK,gBAAgB,OAAO;AAG5B,QAAI,OAAO,UAAU,OAAW,CAAC,KAA6B,QAAQ,OAAO;AAAA,EAC9E;AACD;;;AC7DO,IAAM,cAAsB;AAAA,EAClC,OAAO,MAAM;AAAA,EACb,MAAM,MAAM;AAAA,EACZ,MAAM,MAAM;AAAA,EACZ,OAAO,MAAM;AACd;AAOO,IAAM,sBAAsB,CAClC,QAA6C,WACjC;AACZ,QAAM,QAAQ,EAAE,OAAO,IAAI,MAAM,IAAI,MAAM,IAAI,OAAO,GAAG;AACzD,QAAM,MAAM,MAAM,KAAK;AACvB,QAAM,OACL,CAAC,WACD,CAAC,SAAiB,SAAyB;AAC1C,QAAI,MAAM,MAAM,IAAI,IAAK;AACzB,QAAI,SAAS,OAAW,SAAQ,MAAM,EAAE,OAAO;AAAA,QAC1C,SAAQ,MAAM,EAAE,SAAS,IAAI;AAAA,EACnC;AACD,SAAO,EAAE,OAAO,KAAK,OAAO,GAAG,MAAM,KAAK,MAAM,GAAG,MAAM,KAAK,MAAM,GAAG,OAAO,KAAK,OAAO,EAAE;AAC7F;;;ACVO,IAAM,YAAN,MAAgB;AAAA,EAMtB,YAAY,QAAyB;AACpC,SAAK,YAAY,OAAO;AACxB,SAAK,SAAS,OAAO;AACrB,SAAK,UAAU;AAAA,MACd,gBAAgB;AAAA,MAChB,eAAe,UAAU,OAAO,SAAS;AAAA,IAC1C;AACA,SAAK,SAAS,OAAO,UAAU;AAAA,EAChC;AAAA;AAAA,EAGA,IAAI,MAAsB;AACzB,WAAO,GAAG,KAAK,MAAM,GAAG,IAAI;AAAA,EAC7B;AAAA,EAEA,IAAI,MAAc,OAAoB,CAAC,GAA+B;AACrE,WAAO,KAAK,KAAK,OAAO,MAAM,IAAI;AAAA,EACnC;AAAA,EAEA,KAAK,MAAc,OAAoB,CAAC,GAA+B;AACtE,WAAO,KAAK,KAAK,QAAQ,MAAM,IAAI;AAAA,EACpC;AAAA,EAEA,IAAI,MAAc,OAAoB,CAAC,GAA+B;AACrE,WAAO,KAAK,KAAK,OAAO,MAAM,IAAI;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,KAAwB,MAAuB,UAAwB;AAC/E,QAAI,IAAI,SAAS,IAAK;AACtB,UAAM,IAAI,YAAY,MAAM,GAAG,QAAQ,iBAAc,IAAI,MAAM,IAAI;AAAA,MAClE,QAAQ,IAAI;AAAA,MACZ,MAAM,IAAI;AAAA,IACX,CAAC;AAAA,EACF;AAAA,EAEA,MAAc,KACb,QACA,MACA,MAC6B;AAC7B,UAAM,MAAM,KAAK,WAAW,MAAM,IAAI,OAAO,KAAK,IAAI,IAAI;AAC1D,UAAM,OAAO,EAAE,SAAS,KAAK,SAAS,MAAM,KAAK,KAAK;AACtD,UAAM,MAAM,OAAO,WAAW,QAC3B,KAAK,UAAU,IAAI,KAAK,IAAI,IAC5B,WAAW,SACV,KAAK,UAAU,KAAK,KAAK,IAAI,IAC7B,KAAK,UAAU,IAAI,KAAK,IAAI;AAChC,QAAI,KAAK,SAAU,MAAK,SAAS,KAAK,KAAK,UAAU,GAAG,MAAM,IAAI,IAAI,EAAE;AACxE,WAAO;AAAA,EACR;AACD;;;ACxFO,IAAM,eAAe,CAAC,OAC5B,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;;;ACgC1C,IAAM,iBAAN,MAAqB;AAAA,EAO3B,YAAY,MAAkB;AAC7B,SAAK,MAAM,IAAI,UAAU,IAAI;AAC7B,SAAK,SAAS,KAAK,IAAI;AACvB,SAAK,QAAQ,KAAK,SAAS;AAC3B,SAAK,UAAU,EAAE,GAAU,iBAAiB,GAAG,KAAK,QAAQ;AAC5D,SAAK,WAAW,EAAE,GAAU,yBAAyB,GAAG,KAAK,SAAS;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,MAAM,eAAe,QAA+B;AACnD,UAAM,YAAY,OAAO,aAAa,MAAqB;AAE1D,YAAM,KAAK,MAAM,KAAK,IAAI,IAAI,QAAQ,MAAM,cAAc;AAC1D,YAAM,YAAmB,qBAAqB,GAAG,QAAQ,YAAY,KAAK,OAAO;AACjF,UAAI,UAAU,WAAW,QAAQ;AAChC,cAAM,IAAI;AAAA,UACT,kBAAkB;AAAA,UAClB,YAAY,MAAM,yBAAyB,GAAG,MAAM,SAAS,UAAU;AAAA,UACvE,EAAE,QAAQ,GAAG,QAAQ,MAAM,GAAG,KAAK;AAAA,QACpC;AAAA,MACD;AACA,UAAI,UAAU,WAAW,SAAS;AACjC,aAAK,OAAO;AAAA,UACX,wBAAwB,MAAM,OAAO,GAAG,MAAM,YAAY,UAAU,MAAM;AAAA,QAC3E;AACA,cAAM,KAAK,MAAM,UAAU,MAAM;AACjC,eAAO,UAAU,aAAa,CAAC;AAAA,MAChC;AAEA,YAAM,WAA0B,GAAG,MAAM,YAAY;AACrD,YAAM,aAAuB,GAAG,MAAM,cAAc,CAAC;AACrD,YAAM,kBAA4B,GAAG,MAAM,mBAAmB,CAAC;AAC/D,WAAK,OAAO;AAAA,QACX,QAAQ,MAAM,KAAK,WAAW,MAAM,2BAAwB,gBAAgB,MAAM;AAAA,MACnF;AAIA,YAAM,gBAA0B,CAAC;AACjC,YAAM,kBAA4B,CAAC;AACnC,iBAAW,UAAU,YAAY;AAChC,cAAM,OAAO,MAAM,KAAK,IAAI,IAAI,QAAQ,MAAM,EAAE;AAChD,cAAM,OAAO,KAAK,MAAM;AACxB,cAAM,SAAS,KAAK,MAAM,YAAY;AACtC,YAAW,aAAa,MAAM,MAAM,MAAM,WAAW;AACpD,wBAAc,KAAK,IAAI;AACvB,0BAAgB,KAAK,MAAM;AAAA,QAC5B;AAAA,MACD;AAEA,YAAM,KAAK,IAAI,KAAK,QAAQ,MAAM,cAAc;AAAA,QAC/C,MAAa,aAAa,eAAe,UAAU,iBAAiB,eAAe;AAAA,QACnF,UAAU,kBAAkB;AAAA,MAC7B,CAAC;AACD,WAAK,OAAO,KAAK,QAAQ,MAAM,iBAAiB,gBAAgB,MAAM,wBAAqB;AAAA,IAC5F;AAGA,UAAM,KAAK,kBAAkB,MAAM;AACnC,WAAO,UAAU;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,kBAAkB,QAA+B;AAC9D,QAAI,KAAK,QAAQ,YAAY,EAAG;AAChC,QAAI;AACH,UAAI,OAAsB;AAC1B,UAAI,UAAU;AACd,iBAAS;AACR,cAAM,MAAa,kBAAkB,MAAM,KAAK,UAAU,CAAC,MAAM,CAAC,CAAC;AACnE,cAAM,OAAc,iBAAiB,MAAM,KAAK,SAAS,KAAK,OAAO;AACrE,YAAI,KAAK,WAAW,KAAM;AAC1B,eAAO;AACP,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,mBAAW,KAAK;AAAA,MACjB;AAAA,IACD,SAAS,KAAK;AACb,WAAK,OAAO;AAAA,QACX,mBAAmB,MAAM,qCAAkC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MAC5G;AAAA,IACD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,mBAAmB,SAAuC;AAC/D,UAAM,MAAM,MAAM,KAAK,IAAI,IAAI,aAAa;AAAA,MAC3C,UAAU,kBAAkB;AAAA,IAC7B,CAAC;AACD,UAAM,MAA8B,MAAM,QAAQ,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,MAAM,SAAS,CAAC;AAC7F,UAAM,UAAiB,mBAAmB,KAAK,OAAO;AACtD,SAAK,OAAO;AAAA,MACX,QAAQ,SAAS,IACd,+BAA+B,QAAQ,KAAK,IAAI,CAAC,KACjD;AAAA,IACJ;AACA,eAAW,UAAU,QAAS,OAAM,KAAK,eAAe,MAAM;AAC9D,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,gBACL,SACA,QACA,iBAAgC,YAAY,MAC5B;AAChB,UAAM,SAAS,UAAU,CAAC;AAC1B,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,UAAU,OAAO,UAAiC;AACvD,YAAM,UAAU,MAAM,KAAK,oBAAoB,SAAS,QAAQ,cAAc;AAC9E,UAAI,QAAQ,WAAW,EAAG;AAC1B,UAAI,SAAS,KAAK,QAAQ;AACzB,eAAO,KAAK,oBAAoB,SAAS,QAAQ,cAAc;AAChE,WAAK,OAAO;AAAA,QACX,SAAS,KAAK,IAAI,KAAK,QAAQ,SAAS,YAAY,QAAQ,MAAM,IAAI,OAAO,MAAM,gBAAa,QAAQ,KAAK,IAAI,CAAC;AAAA,MACnH;AACA,YAAM,KAAK,2BAA2B,SAAS,SAAS,cAAc;AACtE,iBAAW,UAAU,QAAS,OAAM,KAAK,eAAe,MAAM;AAC9D,YAAM,KAAK,MAAM,KAAK,QAAQ,WAAW;AACzC,aAAO,QAAQ,QAAQ,CAAC;AAAA,IACzB;AACA,WAAO,QAAQ,CAAC;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,oBACL,SACA,QACA,iBAAgC,YAAY,MAC5B;AAChB,UAAM,SAAS,UAAU,CAAC;AAC1B,QAAI,OAAO,WAAW,EAAG;AACzB,UAAM,UAAU,MAAM,KAAK,oBAAoB,SAAS,QAAQ,cAAc;AAC9E,QAAI,QAAQ,SAAS,GAAG;AACvB,YAAM,IAAI;AAAA,QACT,kBAAkB;AAAA,QAClB,8CAAwC,cAAc,KAAK,QAAQ,KAAK,IAAI,CAAC;AAAA,QAC7E;AAAA,UACC,eAAe;AAAA,QAChB;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,kBAAkB,QAA+B;AACtD,UAAM,MAAM,MAAM,KAAK,IAAI,KAAK,QAAQ,MAAM,cAAc;AAAA,MAC3D,MAAa,aAAa,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;AAAA,IAC3C,CAAC;AACD,QAAI,IAAI,UAAU,KAAK;AACtB,WAAK,OAAO;AAAA,QACX,8BAA8B,MAAM,6BAA0B,IAAI,MAAM;AAAA,MACzE;AAAA,IACD;AAAA,EACD;AAAA;AAAA,EAGA,MAAc,oBACb,SACA,QACA,gBACoB;AACpB,UAAM,SAAS,UAAU,CAAC;AAC1B,QAAI,OAAO,WAAW,EAAG,QAAO,CAAC;AACjC,UAAM,QAAQ,MAAM,KAAK,UAAU,OAAO;AAC1C,WAAc,mBAAmB,OAAO,QAAQ,cAAc;AAAA,EAC/D;AAAA;AAAA,EAGA,MAAc,2BACb,SACA,SACA,gBACgB;AAChB,QAAI,mBAA0B,YAAY,QAAQ,QAAQ,WAAW,EAAG;AACxE,UAAM,QAAQ,MAAM,KAAK,UAAU,OAAO;AAC1C,UAAM,YAAmB,oBAAoB,OAAO,OAAO;AAC3D,QAAI,UAAU,WAAW,EAAG;AAC5B,SAAK,OAAO,KAAK,eAAe,UAAU,MAAM,yCAAsC;AACtF,UAAM,KAAK,IAAI,IAAI,+BAA+B,EAAE,MAAM,EAAE,KAAK,UAAU,EAAE,CAAC;AAAA,EAC/E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAc,UAAU,SAA+C;AACtE,UAAM,QAA2B,CAAC;AAClC,eAAW,UAAU,SAAS;AAC7B,UAAI,OAAO,KAAK,SAAS;AAGzB,eAAS,UAAU,GAAG,UAAU,gBAAgB,WAAW;AAC1D,cAAM,QAAQ,EAAE,MAAM,cAAc,KAAK,SAAS,aAAa;AAC/D,cAAM,MAAM,MAAM,KAAK,IAAI,IAAW,YAAY,KAAK,IAAI,IAAI,EAAE,GAAG,QAAQ,KAAK,GAAG;AAAA,UACnF,UAAU,kBAAkB;AAAA,QAC7B,CAAC;AACD,cAAM,QAA4B,IAAI,SAAS,IAAI,KAAK,SAAS,IAAI,SAAU,CAAC;AAChF,cAAM,KAAK,GAAG,KAAK;AACnB,YAAI,MAAM,SAAS,KAAK,SAAS,aAAc;AAC/C,gBAAQ;AACR,YAAI,YAAY,iBAAiB,GAAG;AACnC,eAAK,OAAO;AAAA,YACX,oBAAoB,MAAM,eAAe,cAAc;AAAA,UACxD;AAAA,QACD;AAAA,MACD;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;AAGA,IAAM,iBAAiB;;;ACxRhB,IAAM,eAAN,MAAmB;AAAA,EAIzB,YAAY,MAAqB;AAChC,SAAK,MAAM,IAAI,UAAU,IAAI;AAC7B,SAAK,SAAS,KAAK,IAAI;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,iBAAiB,SAAkC;AACxD,QAAI,CAAC,WAAW,QAAQ,WAAW,EAAG;AACtC,UAAM,KAAK,IAAI,IAAI,+BAA+B;AAAA,MACjD,MAAM,EAAE,KAAK,QAAQ;AAAA,MACrB,UAAU,kBAAkB;AAAA,IAC7B,CAAC;AACD,SAAK,OAAO,KAAK,GAAG,QAAQ,MAAM,kDAA+C;AAAA,EAClF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,SAAkC;AACpD,eAAW,UAAU,WAAW,CAAC,GAAG;AACnC,YAAM,KAAK,IAAI,KAAK,QAAQ,MAAM,YAAY,EAAE,UAAU,kBAAkB,eAAe,CAAC;AAC5F,WAAK,OAAO,KAAK,QAAQ,MAAM,YAAY;AAAA,IAC5C;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,aAAa,SAAmB,SAAkC;AACvE,UAAM,KAAK,iBAAiB,OAAO;AACnC,UAAM,KAAK,aAAa,OAAO;AAAA,EAChC;AACD;;;ACjDO,IAAM,eAAN,MAAmB;AAAA,EAMzB,YAAY,MAAwB;AACnC,SAAK,SAAS,IAAI,eAAe,IAAI;AACrC,SAAK,YAAY,IAAI,aAAa,IAAI;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,iBACL,SACA,SACA,QACA,iBAAgC,YAAY,MAC5B;AAChB,UAAM,KAAK,UAAU,aAAa,SAAS,OAAO;AAClD,QAAI;AACH,YAAM,KAAK,OAAO,gBAAgB,SAAS,QAAQ,cAAc;AAAA,IAClE,SAAS,OAAO;AAGf,iBAAW,UAAU,QAAS,OAAM,KAAK,OAAO,kBAAkB,MAAM;AACxE,YAAM;AAAA,IACP;AAAA,EACD;AACD;;;ACYO,IAAM,YAAY,CAAC,WAAsB,OAAqB,CAAC,MAAiB;AACtF,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,UAAU,KAAK,WAAW,CAAC,KAAK;AACtC,QAAM,cAAc,KAAK,gBAAgB,CAAC,QAA2B,IAAI,UAAU;AACnF,QAAM,QAAQ,KAAK,SAAS;AAC5B,QAAM,SAAS,KAAK,UAAU;AAE9B,QAAM,MAAM,OACX,QACA,KACA,YACgC;AAChC,QAAI,CAAC,QAAQ,SAAS,MAAM,EAAG,QAAO,UAAU,MAAM,EAAE,KAAK,OAAO;AAEpE,QAAI;AACJ,aAAS,UAAU,GAAG,WAAW,UAAU,WAAW;AACrD,UAAI;AACH,cAAM,MAAM,MAAM,UAAU,MAAM,EAAE,KAAK,OAAO;AAChD,YAAI,YAAY,YAAY,CAAC,YAAY,GAAG,EAAG,QAAO;AACtD,eAAO;AAAA,UACN,SAAS,OAAO,YAAY,CAAC,IAAI,GAAG,YAAY,IAAI,MAAM,aAAa,OAAO,IAAI,QAAQ;AAAA,QAC3F;AAAA,MACD,SAAS,OAAO;AACf,oBAAY;AACZ,YAAI,YAAY,SAAU,OAAM;AAChC,eAAO;AAAA,UACN,SAAS,OAAO,YAAY,CAAC,IAAI,GAAG,qCAAkC,OAAO,IAAI,QAAQ;AAAA,UACzF;AAAA,QACD;AAAA,MACD;AACA,YAAM,MAAM,UAAU,OAAO;AAAA,IAC9B;AAEA,UACC,aACA,IAAI,MAAM,+CAA4C,OAAO,YAAY,CAAC,IAAI,GAAG,EAAE;AAAA,EAErF;AAEA,SAAO;AAAA,IACN,KAAK,CAAC,KAAK,YAAY,IAAI,OAAO,KAAK,OAAO;AAAA,IAC9C,MAAM,CAAC,KAAK,YAAY,IAAI,QAAQ,KAAK,OAAO;AAAA,IAChD,KAAK,CAAC,KAAK,YAAY,IAAI,OAAO,KAAK,OAAO;AAAA,EAC/C;AACD;AAgBO,IAAM,cAAc,CAAC,WAAsB,SAAoC;AACrF,QAAM,SAAS,KAAK,UAAU;AAE9B,QAAM,OAAO,CACZ,QACA,KACA,YACgC;AAChC,QAAI;AACJ,UAAM,UAAU,IAAI,QAAe,CAAC,UAAU,WAAW;AACxD,cAAQ,WAAW,MAAM;AACxB,eAAO,KAAK,WAAW,OAAO,YAAY,CAAC,IAAI,GAAG,sBAAsB,KAAK,EAAE,IAAI;AACnF,eAAO,IAAI,MAAM,cAAc,KAAK,EAAE,SAAS,OAAO,YAAY,CAAC,IAAI,GAAG,EAAE,CAAC;AAAA,MAC9E,GAAG,KAAK,EAAE;AAAA,IACX,CAAC;AACD,WAAO,QAAQ,KAAK,CAAC,UAAU,MAAM,EAAE,KAAK,OAAO,GAAG,OAAO,CAAC,EAAE;AAAA,MAAQ,MACvE,aAAa,KAAK;AAAA,IACnB;AAAA,EACD;AAEA,SAAO;AAAA,IACN,KAAK,CAAC,KAAK,YAAY,KAAK,OAAO,KAAK,OAAO;AAAA,IAC/C,MAAM,CAAC,KAAK,YAAY,KAAK,QAAQ,KAAK,OAAO;AAAA,IACjD,KAAK,CAAC,KAAK,YAAY,KAAK,OAAO,KAAK,OAAO;AAAA,EAChD;AACD;","names":[]}
1
+ {"version":3,"sources":["../src/core.ts","../src/errors.ts","../src/logger.ts","../src/api-client.ts","../src/sleep.ts","../src/engine.ts","../src/publish.ts","../src/facade.ts","../src/transport.ts"],"sourcesContent":["/**\n * Núcleo PURO del test-double de render de CX — agnóstico de Cypress en VALOR y en TIPO.\n *\n * Contiene las decisiones difíciles del protocolo (clasificación por hash, diff por título, gate de\n * build/start, shaping del body de build/end, colección de re-pending de la mitigación cross-job, y los knobs\n * por defecto del render de un solo pase) SIN acoplarse a la cola de comandos de Cypress: cero `cy`/`Cypress`/`expect`/`window`,\n * cero `import ... 'cypress'`, y ninguna firma devuelve `Cypress.Chainable` (solo tipos neutrales). Es el\n * artefacto compartible: el shell Cypress de griddo-qa (`cypress/support/cx-render.ts`) y el motor de Promises\n * del paquete (`./engine`) consumen ESTE módulo, de modo que el racional que causa los bugs vive en un único\n * sitio (functional-core / imperative-shell). Ver `docs/cx-render-packaging-feasibility.md` §5/§6.\n *\n * Behavior-preserving: este código se EXTRAJO de `cx-render.ts` sin cambiar su comportamiento observable;\n * las constantes, umbrales y predicados son idénticos a los del shell original.\n */\n\n/**\n * Modelo de live_status: data-driven en la BD pero ESTABLE por orden de seed (contrato code-grounded e2e-202 →\n * griddo-api CONFIG/LiveStatusSeeder). El double no puede leer los flags de CONFIG en runtime → usa estos ids\n * verificados.\n */\nexport const LIVE_STATUS = {\n\tUNPUBLISHED: 1, // isUnpublished / offline — estado final de despublicación\n\tPENDING_PUBLISHING: 2, // isPendingPublishing / upload-pending — transitorio\n\tLIVE: 3, // isLiveStatus / active — estado final de publicación\n\tPENDING_UNPUBLISHING: 4, // isPendingUnpublishing / offline-pending — transitorio\n} as const;\n\n/** Los cuatro ids de live_status del contrato (1/2/3/4). */\nexport type LiveStatus = (typeof LIVE_STATUS)[keyof typeof LIVE_STATUS];\n\n/**\n * Forma mínima de una página tal como la exponen los endpoints que consume el double: `GET page/{id}` (trae\n * `hash` + `liveStatus`) y `site/{id}/pages?format=list` (trae `id` + `title` + `liveStatus`). Todos los\n * campos son opcionales porque el body real de la API puede venir incompleto y el core no debe asumir.\n */\nexport interface PageInfo {\n\tid?: number;\n\ttitle?: string;\n\thash?: string | null;\n\tliveStatus?: { id?: number } | null;\n}\n\n/** Cuerpo de `POST site/{id}/build/end` (lo que el CX real envía para resolver los transitorios). */\nexport interface BuildEndBody {\n\tpublishHashes: string[];\n\tsiteHash: string | null;\n\tunpublishHashes: string[];\n\tpublishPagesIds: number[];\n}\n\n// --- Esperas, topes y paginación (todo ajustable por el consumidor) -----------------------------------------\n// Antes eran constantes fijas del módulo. Ahora son datos que el consumidor puede sustituir: cada entorno tiene\n// su latencia (CI saturado ≠ local), y quien usa el paquete la conoce mejor que el paquete. Los defaults\n// (medidos) siguen aquí, y el shell Cypress los sobreescribe por `Cypress.env`.\n\n/** Esperas y topes del render. Todos sustituibles; los defaults están en `DEFAULT_TIMINGS`. */\nexport interface RenderTimings {\n\t/**\n\t * TECHO (ms) del settle ADAPTATIVO previo al ÚNICO build/start del render de un solo pase. El settle espera\n\t * al spread ASÍNCRONO de las copias GLOBAL, pero ya NO es un sleep fijo: se sondea el listado del site hasta\n\t * que dos sondas consecutivas devuelven la misma firma (`pageListSignature` → `decideSettleStep`), con este\n\t * valor como tope. Racional: una constante fija no cubre una latencia variable — a 15s el pase único perdía\n\t * rezagados bajo carga de CI (renderFlow2/002 agotaba los 3 intentos) y a 30-45s se paga tiempo muerto por\n\t * site y por ronda en el 90% de renders que asientan en segundos. `<= 0` desactiva el settle por completo.\n\t */\n\tsettleMs: number;\n\t/** Intervalo (ms) entre sondas del settle adaptativo. */\n\tsettlePollMs: number;\n\t/**\n\t * Rondas máximas del orquestador auto-sanador `renderUntilLive`. NO baja a 1: con 1 quedaban sets ENTEROS\n\t * not-Live en CI (el build/start del mock no captura TODAS las páginas en un pase; las rondas\n\t * re-pend+re-render sí).\n\t */\n\tmaxRounds: number;\n\t/** Wait entre rondas del orquestador auto-sanador (ms). */\n\troundWaitMs: number;\n\t/** Reintentos máximos de build/start mientras dé 4xx transitorio (~30s con el retry por defecto). */\n\tbuildStartMaxWaits: number;\n\t/** Backoff entre reintentos de build/start en 4xx transitorio (ms). */\n\tbuildStartRetryWaitMs: number;\n}\n\n/** Defaults medidos de `RenderTimings`. El shell los sobreescribe por `Cypress.env`. */\nexport const DEFAULT_TIMINGS: RenderTimings = {\n\tsettleMs: 45000,\n\tsettlePollMs: 3000,\n\tmaxRounds: 3,\n\troundWaitMs: 1500,\n\tbuildStartMaxWaits: 15,\n\tbuildStartRetryWaitMs: 2000,\n};\n\n/** Paginación con la que se pide el listado de páginas de un site. Defaults en `DEFAULT_PAGE_LIST_QUERY`. */\nexport interface PageListQuery {\n\tpage: number;\n\titemsPerPage: number;\n}\n\n/** Defaults de `PageListQuery`. */\nexport const DEFAULT_PAGE_LIST_QUERY: PageListQuery = { page: 1, itemsPerPage: 300 };\n\n/**\n * URL del listado del site (`site/{id}/pages?format=list`) que leen el diff por título y el re-pending.\n * Centraliza la query string y la paginación — pura (string → string), sin acoplarse a Cypress (recibe\n * `apiUrl` como argumento). ⚠️ Limitación HEREDADA: pide UNA sola página (300 ítems por defecto); un site con\n * más páginas daría confirmación por título errónea. Se mitiga subiendo `itemsPerPage`, pero no pagina.\n */\nexport const listPageUrl = (\n\tapiUrl: string,\n\tsiteId: number,\n\tquery: PageListQuery = DEFAULT_PAGE_LIST_QUERY,\n): string =>\n\t`${apiUrl}site/${siteId}/pages?deleted=false&page=${query.page}&itemsPerPage=${query.itemsPerPage}&format=list`;\n\n/**\n * Firma ESTABLE del estado render-relevante de un listado de páginas: `id:liveStatus:hash` por página, ordenado\n * por id (el orden del listado puede variar entre sondas sin que el estado haya cambiado). Dos firmas iguales en\n * sondas consecutivas = el backend ha dejado de mover estados/hashes (spread y recálculos asentados) → el\n * build/start ya puede capturar el trabajo completo en un pase. Pura (datos → string), para el settle adaptativo.\n */\nexport const pageListSignature = (items: PageInfo[]): string =>\n\t(items || [])\n\t\t.filter((p) => p && p.id != null)\n\t\t.map((p) => `${p.id}:${p.liveStatus?.id ?? \"\"}:${p.hash ?? \"\"}`)\n\t\t.sort()\n\t\t.join(\"|\");\n\n/**\n * Completa un listado (`site/{id}/pages?format=list`, que NO trae `hash`) con los hashes reales de las páginas\n * pendientes de publicar (`liveStatus` 2), para que `pageListSignature` vea los cambios de hash.\n *\n * Por qué: la cascada de la API (DEPENDENT_PAGES_UPDATER) puede dejar una página en 2 con un hash NUEVO. En el\n * listado la página sigue siendo `id:2:` (sin hash), así que la firma no cambia y el settle da quiescencia con\n * la cascada aún activa. El shell lee `GET page/{id}` de las pendientes y pasa aquí `{ [id]: hash }`.\n *\n * - Solo se inyecta en páginas con `liveStatus.id === LIVE_STATUS.PENDING_PUBLISHING` y con su id presente en\n * `hashesById`; el resto queda tal cual (un hash aportado para una no pendiente no altera su firma).\n * - Un valor `null` se respeta (hash ausente), y se distingue de un hash presente en la firma.\n * - Retrocompatible: sin `hashesById` (o vacío) devuelve las mismas páginas, y la firma es idéntica a la de 1.0.0.\n * - Pura: devuelve copias de las pendientes tocadas y no muta la entrada. Tolera `items` ausente (→ `[]`).\n */\nexport const withPendingHashes = (\n\titems: PageInfo[],\n\thashesById?: Readonly<Record<number, string | null>>,\n): PageInfo[] =>\n\t(items || []).map((p) =>\n\t\tp &&\n\t\tp.id != null &&\n\t\thashesById &&\n\t\tp.liveStatus?.id === LIVE_STATUS.PENDING_PUBLISHING &&\n\t\tp.id in hashesById\n\t\t\t? { ...p, hash: hashesById[p.id] }\n\t\t\t: p,\n\t);\n\n/**\n * ¿`GET site/{id}/build/start` ha sido OMITIDO? La API responde `{ skipped: true, publishIds: [] }` cuando otro\n * render tiene el site reclamado (griddo-api `models/sites/index.ts`, `startBuild`). Solo `skipped === true`\n * (booleano estricto) cuenta: un body ausente, no objeto, o con `skipped` de otro tipo → `false`. Pura. La\n * política ante un skipped (reintentar o no) NO se decide aquí: el core solo lo detecta.\n */\nexport const isBuildStartSkipped = (body: unknown): boolean =>\n\ttypeof body === \"object\" && body !== null && (body as { skipped?: unknown }).skipped === true;\n\n/** Decisión del settle adaptativo (unión discriminada: `wait` porta su `waitMs`). */\nexport type SettleDecision = { action: \"go\" } | { action: \"wait\"; waitMs: number };\n\n/**\n * Gate del settle ADAPTATIVO: decide si el backend ya está quiescente y el render puede arrancar.\n * - dos sondas consecutivas con la MISMA firma → `'go'` (quiescencia: nada se ha movido en un intervalo);\n * - `elapsedMs >= timings.settleMs` → `'go'` (techo: bajo carga patológica no se espera para siempre);\n * - resto → `'wait'` con `min(settlePollMs, settleMs - elapsedMs)` (el último wait no desborda el techo).\n * La PRIMERA sonda (`prevSignature = null`) nunca decide `'go'` por firma: hacen falta dos muestras para saber\n * que nada cambia. Con `settleMs <= 0` decide `'go'` inmediato (settle desactivado; el caller puede además\n * ahorrarse la sonda).\n */\nexport const decideSettleStep = (\n\tprevSignature: string | null,\n\tsignature: string,\n\telapsedMs: number,\n\ttimings: RenderTimings = DEFAULT_TIMINGS,\n): SettleDecision => {\n\tif (elapsedMs >= timings.settleMs) return { action: \"go\" };\n\tif (prevSignature !== null && prevSignature === signature) return { action: \"go\" };\n\treturn { action: \"wait\", waitMs: Math.min(timings.settlePollMs, timings.settleMs - elapsedMs) };\n};\n\n/** Clasificación de una página según su `hash`/`status`, resultado de `classifyPage`. */\nexport type PageClassification = \"publish\" | \"needsHash\" | \"skip\";\n\n/**\n * Clasifica una página por su HASH, como el CX real (griddo-cx store.ts:366-369): el TRABAJO = toda página\n * con `hash != null`, SIN filtrar por estado. `build/end` publica el structured_data_content (has_been_published,\n * que la API pública EXIGE vía includePendingPublishing:off) SOLO de los publishPagesIds/publishHashes que\n * recibe; una copia GLOBAL/traducción puede alcanzar live(3) con su CONTENIDO aún sin publicar → si la\n * SALTÁRAMOS por estar live(3) (como hacía el filtro `status===2` anterior), su contenido NUNCA se publicaría\n * y la pública la excluiría (bug de traducciones en CI). Por eso:\n * - `hash != null` → `'publish'` (se publica su contenido aunque ya esté live(3); el render es un solo pase);\n * - `hash == null` y `status === PENDING_PUBLISHING` → `'needsHash'` (el spread aún no asignó el hash);\n * - resto (p.ej. live(3) con hash=null = ya publicada y con el hash nulado por endBuild) → `'skip'`.\n */\nexport const classifyPage = (\n\thash: string | null | undefined,\n\tstatus: number | undefined,\n): PageClassification => {\n\tif (hash != null) return \"publish\";\n\tif (status === LIVE_STATUS.PENDING_PUBLISHING) return \"needsHash\";\n\treturn \"skip\";\n};\n\n/**\n * Diff SOFT por TÍTULO: dada la lista de páginas de los sites, devuelve los `wanted` que AÚN no están en\n * `expectedStatus` (ausentes del listado o en otro estado). Se compara POR TÍTULO y no por id porque publicar/\n * re-renderizar una GLOBAL canónica REASIGNA el id (memoria publish-canonical-global-page-reassigns-id) → un id\n * de createSuite (o de un build/start previo) queda stale; el listado del site es la entidad renderizada real.\n *\n * Preferencia `statusByTitle`: prefiere `expectedStatus` si alguna entidad con ese título ya lo tiene (una\n * global puede tener EDITOR + copia con distinto estado). El resultado (títulos que faltan) depende solo de si\n * ALGUNA entidad con ese título alcanzó `expectedStatus`.\n */\nexport const diffMissingByTitle = (\n\titems: PageInfo[],\n\twanted: string[],\n\texpectedStatus: number,\n): string[] => {\n\tconst w = wanted || [];\n\tif (w.length === 0) return [];\n\tconst statusByTitle: Record<string, number> = {};\n\titems.forEach((p) => {\n\t\tif (p && p.title != null && w.includes(p.title)) {\n\t\t\tconst st = p.liveStatus?.id;\n\t\t\tif (st === expectedStatus || !(p.title in statusByTitle)) statusByTitle[p.title] = st ?? -1;\n\t\t}\n\t});\n\treturn w.filter((t) => statusByTitle[t] !== expectedStatus);\n};\n\n/** Shaping del cuerpo de `POST site/{id}/build/end` (normaliza `siteHash` ausente a `null`). */\nexport const buildEndBody = (\n\tpublishHashes: string[],\n\tsiteHash: string | null | undefined,\n\tunpublishHashes: string[],\n\tpublishPagesIds: number[],\n): BuildEndBody => ({\n\tpublishHashes,\n\tsiteHash: siteHash ?? null,\n\tunpublishHashes,\n\tpublishPagesIds,\n});\n\n/**\n * Colección de re-pending de la MITIGACIÓN cross-job: de las páginas del listado, devuelve los ids in-site\n * (deduplicados, en orden de aparición) de los títulos `missing` que NO están live. El barrido global\n * INCONDICIONAL de endBuild (buildGlobalUnpublishAllWhere) puede despublicar globales QA de OTRO job → una\n * página offline ya NO la recoge build/start, así que re-renderizar no la recupera; re-pendearla (upload-pending)\n * la devuelve a pending(2) para que renderSiteToQuiescence la renderice. Acotado a copias in-site de `wanted`\n * (el caller solo pasa los títulos deseados-live que faltan) y solo `st !== LIVE`.\n */\nexport const collectRepublishIds = (items: PageInfo[], missing: string[]): number[] => {\n\tconst ids: number[] = [];\n\titems.forEach((p) => {\n\t\tconst st = p?.liveStatus?.id;\n\t\t// `p.title as string`: replica EXACTA de HEAD (`missing.includes(p.title)`, sin guard) — behavior-preserving\n\t\t// estricto; el cast solo satisface a tsc (items es PageInfo, no `any`), no cambia el runtime.\n\t\tif (p && p.id != null && missing.includes(p.title as string) && st !== LIVE_STATUS.LIVE)\n\t\t\tids.push(p.id);\n\t});\n\treturn Array.from(new Set(ids));\n};\n\n/**\n * Decisión del gate build/start-ready (unión discriminada: `retry` porta su `waitMs`, `proceed`/`fail` no).\n */\nexport type BuildStartDecision =\n\t| { action: \"proceed\" }\n\t| { action: \"retry\"; waitMs: number }\n\t| { action: \"fail\" };\n\n/**\n * Gate build/start-ready: replica el timing del CX real (que dispara ~1 min después, cuando el pathing ya\n * asentó, y nunca ve el 4xx transitorio) sin su latencia. El publish lanza el spread de las copias GLOBAL de\n * forma ASÍNCRONA; hasta que termina, una copia puede existir con `pathId=null` y build/start (que corre\n * fixAllPages → `pathId.split(...)`) devuelve 4xx TRANSITORIO. Por eso reintentamos SOLO mientras dé 4xx,\n * cortando al 1er 200; si agota el presupuesto, el shell asserta RUIDOSO (no un CypressError crudo).\n * - `status < 400` → `'proceed'`;\n * - agotado el presupuesto (`startWaits >= timings.buildStartMaxWaits`) → `'fail'`;\n * - resto → `'retry'` con backoff `timings.buildStartRetryWaitMs`.\n */\nexport const decideBuildStartStep = (\n\tstatus: number,\n\tstartWaits: number,\n\ttimings: RenderTimings = DEFAULT_TIMINGS,\n): BuildStartDecision => {\n\tif (status < 400) return { action: \"proceed\" };\n\tif (startWaits >= timings.buildStartMaxWaits) return { action: \"fail\" };\n\treturn { action: \"retry\", waitMs: timings.buildStartRetryWaitMs };\n};\n\n/** Forma mínima de un site en `GET sites/all`: lo justo para saber si tiene render pendiente. */\nexport interface SiteBuildInfo {\n\tid?: number;\n\t/** La API lo marca cuando el site tiene cambios sin renderizar (señal QA-scoped: hash pendiente, no bloqueado). */\n\tshouldBeUpdated?: boolean;\n}\n\n/**\n * Ids de los sites con render PENDIENTE — lo que el CX real busca en cada pasada de su poll.\n *\n * `onlyIds` acota el resultado (el shell le pasa los sites del test en curso). Esa acotación NO es cosmética:\n * el CX real es un único proceso global, pero el mock corre por test y a escala CI ~40 jobs comparten una BD;\n * sin acotar, cada pasada arrastraría el trabajo de los demás (build/start → fixAllPages es pesado) y la BD se\n * satura, tumbando incluso specs que no usan el mock.\n */\nexport const selectPendingSites = (sites: SiteBuildInfo[], onlyIds?: number[]): number[] => {\n\tconst ids: number[] = [];\n\t(sites || []).forEach((site) => {\n\t\tif (!site || site.id == null || !site.shouldBeUpdated) return;\n\t\tif (onlyIds && !onlyIds.includes(site.id)) return;\n\t\tids.push(site.id);\n\t});\n\treturn Array.from(new Set(ids));\n};\n","/**\n * Error del paquete, con código.\n *\n * Un único tipo de error con un `code` en lugar de una jerarquía de clases: los consumidores necesitan\n * distinguir *qué* falló para decidir (reintentar, avisar, abortar), y eso se hace igual de bien con un campo que\n * con `instanceof`, sin obligar a importar siete clases.\n */\n\n/** Códigos de {@link RenderError}. */\nexport const RENDER_ERROR_CODE = {\n\t/** `build/start` siguió respondiendo error hasta agotar los reintentos: el site no llega a estar listo. */\n\tBUILD_START_NOT_READY: \"BUILD_START_NOT_READY\",\n\t/** `build/end` respondió >= 400: el render no se ha aplicado y el bloqueo puede seguir puesto. */\n\tBUILD_END_FAILED: \"BUILD_END_FAILED\",\n\t/** Se agotaron las rondas y algunos títulos no alcanzaron el estado final. Mira `missingTitles`. */\n\tTITLES_NOT_FINAL: \"TITLES_NOT_FINAL\",\n\t/** No se pudo saber qué sites tenían render pendiente. */\n\tSITES_LOOKUP_FAILED: \"SITES_LOOKUP_FAILED\",\n\t/** No se pudo leer el listado de páginas de un site. */\n\tPAGE_LIST_FAILED: \"PAGE_LIST_FAILED\",\n\t/** Falló marcar páginas como pendientes de publicar. */\n\tMARK_PENDING_FAILED: \"MARK_PENDING_FAILED\",\n\t/** Falló publicar un site. */\n\tPUBLISH_FAILED: \"PUBLISH_FAILED\",\n} as const;\n\n/** Uno de los {@link RENDER_ERROR_CODE}. */\nexport type RenderErrorCode = (typeof RENDER_ERROR_CODE)[keyof typeof RENDER_ERROR_CODE];\n\n/** Datos opcionales que acompañan a un {@link RenderError}. */\nexport interface RenderErrorDetail {\n\t/** Status HTTP de la respuesta que provocó el error, si lo hubo. */\n\tstatus?: number;\n\t/** Body de esa respuesta, tal cual, para poder diagnosticar sin volver a reproducir. */\n\tbody?: unknown;\n\t/** Títulos que no alcanzaron su estado final. Solo en `TITLES_NOT_FINAL`. */\n\tmissingTitles?: string[];\n\t/** Error original, si este envuelve a otro. Se expone también como `cause` nativa. */\n\tcause?: unknown;\n}\n\n/**\n * Error de este paquete. Siempre trae `code`; el resto del detalle depende de dónde falló.\n *\n * ```ts\n * try {\n * \tawait engine.renderUntilLive([siteId], titles);\n * } catch (error) {\n * \tif (error instanceof RenderError && error.code === RENDER_ERROR_CODE.TITLES_NOT_FINAL) {\n * \t\tconsole.warn(\"no llegaron:\", error.missingTitles);\n * \t}\n * }\n * ```\n */\nexport class RenderError extends Error {\n\t/** Qué falló, de forma comprobable sin leer el mensaje. */\n\treadonly code: RenderErrorCode;\n\t/** Status HTTP asociado, si el error viene de una respuesta. */\n\treadonly status?: number;\n\t/** Body de la respuesta asociada. */\n\treadonly body?: unknown;\n\t/** Títulos que no alcanzaron su estado final (`TITLES_NOT_FINAL`). */\n\treadonly missingTitles?: string[];\n\n\tconstructor(code: RenderErrorCode, message: string, detail: RenderErrorDetail = {}) {\n\t\tsuper(message);\n\t\tthis.name = \"RenderError\";\n\t\tthis.code = code;\n\t\tthis.status = detail.status;\n\t\tthis.body = detail.body;\n\t\tthis.missingTitles = detail.missingTitles;\n\t\t// `cause` se asigna en lugar de pasarse al constructor: el segundo argumento de Error es ES2022, y así el\n\t\t// paquete sigue compilando contra librerías más antiguas sin perder el encadenado de errores en runtime.\n\t\tif (detail.cause !== undefined) (this as { cause?: unknown }).cause = detail.cause;\n\t}\n}\n","/**\n * Contrato de log mínimo, para que el paquete cuente qué está haciendo sin imponer una librería.\n *\n * Cuatro métodos y ninguna dependencia: encaja tal cual con `console`, y con pino, winston o el log del runner de\n * tests envolviéndolo en cuatro líneas. Si no pasas ninguno, el paquete no escribe nada.\n */\nexport interface Logger {\n\tdebug(message: string, meta?: unknown): void;\n\tinfo(message: string, meta?: unknown): void;\n\twarn(message: string, meta?: unknown): void;\n\terror(message: string, meta?: unknown): void;\n}\n\n/** Logger que descarta todo. Es el que se usa cuando no inyectas uno. */\nexport const NOOP_LOGGER: Logger = {\n\tdebug: () => undefined,\n\tinfo: () => undefined,\n\twarn: () => undefined,\n\terror: () => undefined,\n};\n\n/**\n * Logger sobre `console`, por comodidad en scripts y ejemplos.\n *\n * @param level nivel mínimo que se emite. Por defecto `info`, así que los `debug` se descartan.\n */\nexport const createConsoleLogger = (\n\tlevel: \"debug\" | \"info\" | \"warn\" | \"error\" = \"info\",\n): Logger => {\n\tconst order = { debug: 10, info: 20, warn: 30, error: 40 };\n\tconst min = order[level];\n\tconst emit =\n\t\t(method: \"debug\" | \"info\" | \"warn\" | \"error\") =>\n\t\t(message: string, meta?: unknown): void => {\n\t\t\tif (order[method] < min) return;\n\t\t\tif (meta === undefined) console[method](message);\n\t\t\telse console[method](message, meta);\n\t\t};\n\treturn { debug: emit(\"debug\"), info: emit(\"info\"), warn: emit(\"warn\"), error: emit(\"error\") };\n};\n","/**\n * Cliente interno sobre el transporte inyectado. **No es API pública**: no se exporta desde el índice del\n * paquete.\n *\n * Concentra las tres cosas que antes repetían el motor y el publicador: montar las cabeceras, componer la URL\n * a partir de `apiUrl`, y convertir una respuesta con error en un {@link RenderError} con su código. Así \"qué\n * cuenta como fallo\" se decide en un único sitio.\n */\n\nimport type { Transport, TransportResponse } from \"./transport\";\n\nimport { RenderError, type RenderErrorCode } from \"./errors\";\nimport { type Logger, NOOP_LOGGER } from \"./logger\";\n\n/** Lo que necesita cualquier pieza que hable con la API: el transporte, dónde está y con qué credencial. */\nexport interface ApiClientConfig {\n\ttransport: Transport;\n\tapiUrl: string;\n\tauthToken: string;\n\t/** Dónde contar lo que va pasando. Si no lo pasas, no se escribe nada. */\n\tlogger?: Logger;\n}\n\n/** Detalle de una llamada. Con `expectOk`, una respuesta >= 400 se convierte en error con ese código. */\nexport interface RequestSpec {\n\tbody?: unknown;\n\texpectOk?: RenderErrorCode;\n}\n\nexport class ApiClient {\n\treadonly logger: Logger;\n\tprivate readonly transport: Transport;\n\tprivate readonly apiUrl: string;\n\tprivate readonly headers: Record<string, string>;\n\n\tconstructor(config: ApiClientConfig) {\n\t\tthis.transport = config.transport;\n\t\tthis.apiUrl = config.apiUrl;\n\t\tthis.headers = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\tauthorization: `Bearer ${config.authToken}`,\n\t\t};\n\t\tthis.logger = config.logger ?? NOOP_LOGGER;\n\t}\n\n\t/** URL absoluta de un path relativo a `apiUrl`. Útil cuando la URL la compone el núcleo. */\n\turl(path: string): string {\n\t\treturn `${this.apiUrl}${path}`;\n\t}\n\n\tget(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"GET\", path, spec);\n\t}\n\n\tpost(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"POST\", path, spec);\n\t}\n\n\tput(path: string, spec: RequestSpec = {}): Promise<TransportResponse> {\n\t\treturn this.send(\"PUT\", path, spec);\n\t}\n\n\t/**\n\t * Lanza si la respuesta trae error. Se usa cuando la llamada ya se hizo sin `expectOk` porque había que\n\t * inspeccionar el status antes de decidir.\n\t */\n\tensureOk(res: TransportResponse, code: RenderErrorCode, describe: string): void {\n\t\tif (res.status < 400) return;\n\t\tthrow new RenderError(code, `${describe} respondió ${res.status}`, {\n\t\t\tstatus: res.status,\n\t\t\tbody: res.body,\n\t\t});\n\t}\n\n\tprivate async send(\n\t\tmethod: \"GET\" | \"POST\" | \"PUT\",\n\t\tpath: string,\n\t\tspec: RequestSpec,\n\t): Promise<TransportResponse> {\n\t\tconst url = path.startsWith(\"http\") ? path : this.url(path);\n\t\tconst opts = { headers: this.headers, body: spec.body };\n\t\tconst res = await (method === \"GET\"\n\t\t\t? this.transport.get(url, opts)\n\t\t\t: method === \"POST\"\n\t\t\t\t? this.transport.post(url, opts)\n\t\t\t\t: this.transport.put(url, opts));\n\t\tif (spec.expectOk) this.ensureOk(res, spec.expectOk, `${method} ${path}`);\n\t\treturn res;\n\t}\n}\n","/** Espera de `ms` milisegundos. Es el `sleep` por defecto de todo el paquete; los tests inyectan un no-op. */\nexport const defaultSleep = (ms: number): Promise<void> =>\n\tnew Promise((resolve) => setTimeout(resolve, ms));\n","/**\n * Motor del protocolo de render: `build/start` → clasificar páginas por hash → `build/end`, con reintento por\n * rondas hasta que los títulos pedidos alcanzan su estado final.\n *\n * Es una clase porque tiene estado (configuración, cliente HTTP) y dependencias que conviene invertir: el HTTP y\n * el log. Las decisiones sin estado viven en `./core` como funciones puras; hablar con la API es cosa de\n * `./api-client`.\n */\n\nimport type { Logger } from \"./logger\";\n\nimport { ApiClient, type ApiClientConfig } from \"./api-client\";\nimport * as cxCore from \"./core\";\nimport { RENDER_ERROR_CODE, RenderError } from \"./errors\";\nimport { defaultSleep } from \"./sleep\";\n\n/** Dependencias del motor, inyectadas por constructor. */\nexport interface EngineDeps extends ApiClientConfig {\n\t/** Espera de `ms` milisegundos. Por defecto, `setTimeout`; inyecta un no-op para que los tests no esperen. */\n\tsleep?: (ms: number) => Promise<void>;\n\t/** Esperas y topes del render. Lo que no indiques se toma de {@link cxCore.DEFAULT_TIMINGS}. */\n\ttimings?: Partial<cxCore.RenderTimings>;\n\t/** Paginación del listado de páginas. Lo que no indiques se toma de {@link cxCore.DEFAULT_PAGE_LIST_QUERY}. */\n\tpageList?: Partial<cxCore.PageListQuery>;\n}\n\n/**\n * Instánciala una vez con la configuración del entorno y reutilízala:\n *\n * ```ts\n * const engine = new CxRenderEngine({ transport, apiUrl, authToken });\n * await engine.renderUntilLive([siteId], [\"Mi página\"]);\n * ```\n */\nexport class CxRenderEngine {\n\tprivate readonly api: ApiClient;\n\tprivate readonly logger: Logger;\n\tprivate readonly sleep: (ms: number) => Promise<void>;\n\tprivate readonly timings: cxCore.RenderTimings;\n\tprivate readonly pageList: cxCore.PageListQuery;\n\n\tconstructor(deps: EngineDeps) {\n\t\tthis.api = new ApiClient(deps);\n\t\tthis.logger = this.api.logger;\n\t\tthis.sleep = deps.sleep ?? defaultSleep;\n\t\tthis.timings = { ...cxCore.DEFAULT_TIMINGS, ...deps.timings };\n\t\tthis.pageList = { ...cxCore.DEFAULT_PAGE_LIST_QUERY, ...deps.pageList };\n\t}\n\n\t/**\n\t * Renderiza un site en **una sola pasada**: settle adaptativo → `build/start` → `GET page/{id}` →\n\t * `build/end`.\n\t *\n\t * El settle previo da tiempo a que el backend termine de propagar las copias de las páginas globales; sin él,\n\t * `build/start` devolvería una lista incompleta. Es ADAPTATIVO: sondea el listado hasta que dos sondas\n\t * consecutivas coinciden (quiescencia) o hasta el techo `timings.settleMs` — rápido cuando el backend va\n\t * suelto, paciente bajo carga. Las páginas que lleguen tarde no se reintentan aquí: de eso se encarga\n\t * {@link renderUntilLive}.\n\t *\n\t * ⚠️ `build/start` deja el site bloqueado y `build/end` lo libera. En el camino normal `build/end` se emite\n\t * siempre, pero si el transporte rechaza entre ambos el bloqueo queda colgado durante horas: llama a\n\t * {@link releaseRenderLock} en tu `finally`.\n\t *\n\t * @throws {RenderError} `BUILD_START_NOT_READY` si el site no llega a estar listo, `BUILD_END_FAILED` si el\n\t * render no se aplica.\n\t */\n\tasync renderSiteOnce(siteId: number): Promise<void> {\n\t\tconst startOnce = async (startWaits = 0): Promise<void> => {\n\t\t\t// Sin `expectOk`: un 4xx aquí puede ser transitorio y lo decide el núcleo, no es un fallo todavía.\n\t\t\tconst bs = await this.api.get(`site/${siteId}/build/start`);\n\t\t\tconst startStep = cxCore.decideBuildStartStep(bs.status, startWaits, this.timings);\n\t\t\tif (startStep.action === \"fail\") {\n\t\t\t\tthrow new RenderError(\n\t\t\t\t\tRENDER_ERROR_CODE.BUILD_START_NOT_READY,\n\t\t\t\t\t`GET site/${siteId}/build/start sigue en ${bs.status} tras ${startWaits} esperas (¿el site no acaba de asentarse?)`,\n\t\t\t\t\t{ status: bs.status, body: bs.body },\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (startStep.action === \"retry\") {\n\t\t\t\tthis.logger.debug(\n\t\t\t\t\t`build/start del site ${siteId} en ${bs.status}: espera ${startStep.waitMs}ms y reintenta`,\n\t\t\t\t);\n\t\t\t\tawait this.sleep(startStep.waitMs);\n\t\t\t\treturn startOnce(startWaits + 1);\n\t\t\t}\n\n\t\t\tconst siteHash: string | null = bs.body?.siteHash || null;\n\t\t\tconst publishIds: number[] = bs.body?.publishIds || [];\n\t\t\tconst unpublishHashes: string[] = bs.body?.unpublishHashes || [];\n\t\t\tthis.logger.debug(\n\t\t\t\t`site ${siteId}: ${publishIds.length} páginas candidatas, ${unpublishHashes.length} a despublicar`,\n\t\t\t);\n\n\t\t\t// El trabajo es toda página con hash, sin mirar su estado (ver classifyPage). Una sola pasada, así que no\n\t\t\t// hace falta deduplicar ids; las páginas que aún no tienen hash las recoge renderUntilLive.\n\t\t\tconst publishHashes: string[] = [];\n\t\t\tconst publishPagesIds: number[] = [];\n\t\t\tfor (const pageId of publishIds) {\n\t\t\t\tconst pres = await this.api.get(`page/${pageId}`);\n\t\t\t\tconst hash = pres.body?.hash;\n\t\t\t\tconst status = pres.body?.liveStatus?.id;\n\t\t\t\tif (cxCore.classifyPage(hash, status) === \"publish\") {\n\t\t\t\t\tpublishHashes.push(hash);\n\t\t\t\t\tpublishPagesIds.push(pageId);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait this.api.post(`site/${siteId}/build/end`, {\n\t\t\t\tbody: cxCore.buildEndBody(publishHashes, siteHash, unpublishHashes, publishPagesIds),\n\t\t\t\texpectOk: RENDER_ERROR_CODE.BUILD_END_FAILED,\n\t\t\t});\n\t\t\tthis.logger.info(`site ${siteId} renderizado: ${publishPagesIds.length} páginas publicadas`);\n\t\t};\n\n\t\t// Asentamiento primero, luego una sola pasada.\n\t\tawait this.settleBeforeStart(siteId);\n\t\treturn startOnce();\n\t}\n\n\t/**\n\t * Settle ADAPTATIVO previo al build/start: sondea el listado del site cada `timings.settlePollMs` y arranca\n\t * cuando dos sondas consecutivas devuelven la misma firma (`cxCore.pageListSignature`) o al agotar el techo\n\t * `timings.settleMs` (la decisión es de `cxCore.decideSettleStep`). Best-effort: si el listado falla durante\n\t * el settle, se procede al render en vez de morir aquí (peor caso = el de siempre, un pase incompleto que\n\t * recogen las rondas de `renderUntilLive`). Con `settleMs <= 0` no sondea (settle desactivado).\n\t */\n\tprivate async settleBeforeStart(siteId: number): Promise<void> {\n\t\tif (this.timings.settleMs <= 0) return;\n\t\ttry {\n\t\t\tlet prev: string | null = null;\n\t\t\tlet elapsed = 0;\n\t\t\tfor (;;) {\n\t\t\t\tconst sig = cxCore.pageListSignature(await this.listPages([siteId]));\n\t\t\t\tconst step = cxCore.decideSettleStep(prev, sig, elapsed, this.timings);\n\t\t\t\tif (step.action === \"go\") return;\n\t\t\t\tprev = sig;\n\t\t\t\tawait this.sleep(step.waitMs);\n\t\t\t\telapsed += step.waitMs;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.logger.warn(\n\t\t\t\t`settle del site ${siteId}: el sondeo del listado falló (${err instanceof Error ? err.message : String(err)}); se procede al render sin asentar`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Renderiza **todos los sites que tengan render pendiente**, que es lo que hace el render real en cada pasada:\n\t * pregunta al backend qué sites tienen cambios sin renderizar (`GET sites/all` → `shouldBeUpdated`) y los\n\t * resuelve uno por uno.\n\t *\n\t * @param onlyIds acota a esos sites. **Úsalo si compartes la base de datos con otros procesos**: sin acotar,\n\t * esta pasada arrastra también el trabajo pendiente de los demás, que es tan lento como caro.\n\t * @returns los ids que ha renderizado.\n\t * @throws {RenderError} `SITES_LOOKUP_FAILED` si no se puede consultar la lista de sites.\n\t */\n\tasync renderPendingSites(onlyIds?: number[]): Promise<number[]> {\n\t\tconst res = await this.api.get(\"sites/all\", {\n\t\t\texpectOk: RENDER_ERROR_CODE.SITES_LOOKUP_FAILED,\n\t\t});\n\t\tconst all: cxCore.SiteBuildInfo[] = Array.isArray(res.body) ? res.body : res.body?.items || [];\n\t\tconst pending = cxCore.selectPendingSites(all, onlyIds);\n\t\tthis.logger.info(\n\t\t\tpending.length > 0\n\t\t\t\t? `sites con render pendiente: ${pending.join(\", \")}`\n\t\t\t\t: \"ningún site con render pendiente\",\n\t\t);\n\t\tfor (const siteId of pending) await this.renderSiteOnce(siteId);\n\t\treturn pending;\n\t}\n\n\t/**\n\t * Lleva los títulos pedidos a su estado final, reintentando lo que falte.\n\t *\n\t * Mientras queden títulos pendientes, vuelve a marcar como pendientes las copias que se hayan quedado\n\t * despublicadas y renderiza otra vez, hasta `timings.maxRounds` rondas; la última confirma o lanza. Si todo\n\t * está ya en su sitio, la primera comprobación pasa y no renderiza nada.\n\t *\n\t * @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`; usa `LIVE_STATUS.UNPUBLISHED`\n\t * para confirmar una despublicación.\n\t * @throws {RenderError} `TITLES_NOT_FINAL` si tras la última ronda sigue faltando algún título.\n\t */\n\tasync renderUntilLive(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return;\n\n\t\tconst attempt = async (round: number): Promise<void> => {\n\t\t\tconst missing = await this.missingPagesByTitle(siteIds, wanted, expectedStatus);\n\t\t\tif (missing.length === 0) return;\n\t\t\tif (round >= this.timings.maxRounds)\n\t\t\t\treturn this.confirmPagesByTitle(siteIds, wanted, expectedStatus);\n\t\t\tthis.logger.warn(\n\t\t\t\t`ronda ${round}/${this.timings.maxRounds}: faltan ${missing.length}/${wanted.length} títulos (${missing.join(\", \")})`,\n\t\t\t);\n\t\t\tawait this.republishMissingWantedLive(siteIds, missing, expectedStatus);\n\t\t\tfor (const siteId of siteIds) await this.renderSiteOnce(siteId);\n\t\t\tawait this.sleep(this.timings.roundWaitMs);\n\t\t\treturn attempt(round + 1);\n\t\t};\n\t\treturn attempt(1);\n\t}\n\n\t/**\n\t * Comprueba, sin renderizar, que los títulos están en `expectedStatus`.\n\t *\n\t * @throws {RenderError} `TITLES_NOT_FINAL`, con los títulos que faltan en `missingTitles`.\n\t */\n\tasync confirmPagesByTitle(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return;\n\t\tconst missing = await this.missingPagesByTitle(siteIds, wanted, expectedStatus);\n\t\tif (missing.length > 0) {\n\t\t\tthrow new RenderError(\n\t\t\t\tRENDER_ERROR_CODE.TITLES_NOT_FINAL,\n\t\t\t\t`estos títulos no están en liveStatus ${expectedStatus}: ${missing.join(\", \")}`,\n\t\t\t\t{\n\t\t\t\t\tmissingTitles: missing,\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Libera el bloqueo de render que deja `build/start`, emitiendo un `build/end` vacío.\n\t *\n\t * Llámalo en tu `finally` si un render puede abortar a medias. Es best-effort: no lanza aunque el backend\n\t * responda con error, porque el caso normal es que ya no hubiera nada que liberar.\n\t */\n\tasync releaseRenderLock(siteId: number): Promise<void> {\n\t\tconst res = await this.api.post(`site/${siteId}/build/end`, {\n\t\t\tbody: cxCore.buildEndBody([], null, [], []),\n\t\t});\n\t\tif (res.status >= 400) {\n\t\t\tthis.logger.debug(\n\t\t\t\t`releaseRenderLock del site ${siteId}: el backend respondió ${res.status} (probablemente no había bloqueo)`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Títulos que aún no están en `expectedStatus`, leyendo el listado completo de cada site. */\n\tprivate async missingPagesByTitle(\n\t\tsiteIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number,\n\t): Promise<string[]> {\n\t\tconst wanted = titles || [];\n\t\tif (wanted.length === 0) return [];\n\t\tconst items = await this.listPages(siteIds);\n\t\treturn cxCore.diffMissingByTitle(items, wanted, expectedStatus);\n\t}\n\n\t/** Devuelve a \"pendiente de publicar\" las copias despublicadas de los títulos que faltan. */\n\tprivate async republishMissingWantedLive(\n\t\tsiteIds: number[],\n\t\tmissing: string[],\n\t\texpectedStatus: number,\n\t): Promise<void> {\n\t\tif (expectedStatus !== cxCore.LIVE_STATUS.LIVE || missing.length === 0) return;\n\t\tconst items = await this.listPages(siteIds);\n\t\tconst uniqueIds = cxCore.collectRepublishIds(items, missing);\n\t\tif (uniqueIds.length === 0) return;\n\t\tthis.logger.info(`re-marcando ${uniqueIds.length} páginas como pendientes de publicar`);\n\t\tawait this.api.put(\"pages/status/upload-pending\", { body: { ids: uniqueIds } });\n\t}\n\n\t/**\n\t * Páginas de los sites indicados, **paginando hasta el final** del listado.\n\t *\n\t * Recorre páginas mientras el backend devuelva el lote completo: si llega uno más corto, es el último. Así un\n\t * site con más páginas que `pageList.itemsPerPage` no se queda a medias, que era la causa de confirmaciones\n\t * por título erróneas cuando solo se leía la primera página.\n\t *\n\t * @throws {RenderError} `PAGE_LIST_FAILED` si el backend no puede devolver el listado.\n\t */\n\tprivate async listPages(siteIds: number[]): Promise<cxCore.PageInfo[]> {\n\t\tconst items: cxCore.PageInfo[] = [];\n\t\tfor (const siteId of siteIds) {\n\t\t\tlet page = this.pageList.page;\n\t\t\t// Tope de seguridad: con el itemsPerPage por defecto son 300.000 páginas. Si se alcanza, algo va mal en\n\t\t\t// el backend (por ejemplo, ignora el parámetro `page` y devuelve siempre el mismo lote).\n\t\t\tfor (let fetched = 0; fetched < MAX_LIST_PAGES; fetched++) {\n\t\t\t\tconst query = { page, itemsPerPage: this.pageList.itemsPerPage };\n\t\t\t\tconst res = await this.api.get(cxCore.listPageUrl(this.api.url(\"\"), siteId, query), {\n\t\t\t\t\texpectOk: RENDER_ERROR_CODE.PAGE_LIST_FAILED,\n\t\t\t\t});\n\t\t\t\tconst batch: cxCore.PageInfo[] = (res.body && (res.body.items || res.body)) || [];\n\t\t\t\titems.push(...batch);\n\t\t\t\tif (batch.length < this.pageList.itemsPerPage) break;\n\t\t\t\tpage += 1;\n\t\t\t\tif (fetched === MAX_LIST_PAGES - 1) {\n\t\t\t\t\tthis.logger.warn(\n\t\t\t\t\t\t`listado del site ${siteId} cortado en ${MAX_LIST_PAGES} páginas: ¿el backend ignora el parámetro page?`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn items;\n\t}\n}\n\n/** Tope de páginas que se piden por site antes de asumir que el backend no pagina bien. */\nconst MAX_LIST_PAGES = 1000;\n","/**\n * Publicación: marcar páginas como pendientes y publicar sites.\n *\n * Ojo con el reparto de responsabilidades: **esto no es parte del render**. En producción lo dispara la persona\n * que pulsa \"Publicar\" en el CMS; el render solo reacciona a lo que ya está pendiente. Vive en este paquete como\n * conveniencia para montar el escenario desde un script, no porque el render lo necesite.\n *\n * El orden importa: primero se marcan las páginas, luego se publica el site. Publicar es lo que hace que el\n * backend las incluya en el siguiente `build/start`.\n */\n\nimport type { Logger } from \"./logger\";\n\nimport { ApiClient, type ApiClientConfig } from \"./api-client\";\nimport { RENDER_ERROR_CODE } from \"./errors\";\n\n/** Dependencias del publicador, inyectadas por constructor. Las mismas que el motor necesita para hablar con la API. */\nexport type PublisherDeps = ApiClientConfig;\n\n/**\n * Dispara la publicación en el CMS. Complementa al motor de render: este deja el trabajo pendiente, el motor lo\n * resuelve.\n *\n * ```ts\n * const publisher = new CmsPublisher({ transport, apiUrl, authToken });\n * await publisher.publishPages([siteId], [pageId]);\n * await engine.renderUntilLive([siteId], [\"Mi página\"]);\n * ```\n */\nexport class CmsPublisher {\n\tprivate readonly api: ApiClient;\n\tprivate readonly logger: Logger;\n\n\tconstructor(deps: PublisherDeps) {\n\t\tthis.api = new ApiClient(deps);\n\t\tthis.logger = this.api.logger;\n\t}\n\n\t/**\n\t * Marca páginas como pendientes de publicar. Sin ids no hace nada.\n\t *\n\t * @throws {RenderError} `MARK_PENDING_FAILED` si el backend responde >= 400.\n\t */\n\tasync markPagesPending(pageIds: number[]): Promise<void> {\n\t\tif (!pageIds || pageIds.length === 0) return;\n\t\tawait this.api.put(\"pages/status/upload-pending\", {\n\t\t\tbody: { ids: pageIds },\n\t\t\texpectOk: RENDER_ERROR_CODE.MARK_PENDING_FAILED,\n\t\t});\n\t\tthis.logger.info(`${pageIds.length} páginas marcadas como pendientes de publicar`);\n\t}\n\n\t/**\n\t * Publica los sites indicados, uno por uno.\n\t *\n\t * @throws {RenderError} `PUBLISH_FAILED` si el backend responde >= 400 para alguno.\n\t */\n\tasync publishSites(siteIds: number[]): Promise<void> {\n\t\tfor (const siteId of siteIds || []) {\n\t\t\tawait this.api.post(`site/${siteId}/publish`, { expectOk: RENDER_ERROR_CODE.PUBLISH_FAILED });\n\t\t\tthis.logger.info(`site ${siteId} publicado`);\n\t\t}\n\t}\n\n\t/**\n\t * Los dos pasos en orden: marca las páginas y publica los sites. Es el disparo completo que deja el trabajo\n\t * listo para que el motor lo renderice.\n\t */\n\tasync publishPages(siteIds: number[], pageIds: number[]): Promise<void> {\n\t\tawait this.markPagesPending(pageIds);\n\t\tawait this.publishSites(siteIds);\n\t}\n}\n","/**\n * Fachada: construye motor y publicador con una sola configuración, y encapsula el flujo completo.\n *\n * Existe por dos motivos concretos, no por simetría: instanciar las dos clases obliga a repetir las mismas tres\n * dependencias, y el flujo tiene un orden que hay que conocer (publicar → renderizar → confirmar, liberando el\n * bloqueo si algo se rompe a medias). Si necesitas solo una parte, usa `CxRenderEngine` o `CmsPublisher`\n * directamente: siguen expuestos.\n */\nimport * as cxCore from \"./core\";\nimport { CxRenderEngine, type EngineDeps } from \"./engine\";\nimport { CmsPublisher } from \"./publish\";\n\n/** Configuración de la fachada: la del motor, que es un superconjunto de la del publicador. */\nexport type CxRenderMockDeps = EngineDeps;\n\n/**\n * Punto de entrada recomendado.\n *\n * ```ts\n * const cx = new CxRenderMock({ transport, apiUrl, authToken, logger });\n * await cx.publishAndRender([siteId], [pageId], [\"Mi página\"]);\n * ```\n */\nexport class CxRenderMock {\n\t/** El motor de render, por si necesitas sus métodos sueltos. */\n\treadonly engine: CxRenderEngine;\n\t/** El disparo de publicación, por si quieres publicar sin renderizar. */\n\treadonly publisher: CmsPublisher;\n\n\tconstructor(deps: CxRenderMockDeps) {\n\t\tthis.engine = new CxRenderEngine(deps);\n\t\tthis.publisher = new CmsPublisher(deps);\n\t}\n\n\t/**\n\t * El flujo entero: publica, renderiza y confirma que los títulos llegaron a su estado final. Libera el bloqueo\n\t * de render de cada site si algo falla a medias.\n\t *\n\t * @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`.\n\t * @throws {RenderError} con el `code` de la etapa que falló.\n\t */\n\tasync publishAndRender(\n\t\tsiteIds: number[],\n\t\tpageIds: number[],\n\t\ttitles: string[],\n\t\texpectedStatus: number = cxCore.LIVE_STATUS.LIVE,\n\t): Promise<void> {\n\t\tawait this.publisher.publishPages(siteIds, pageIds);\n\t\ttry {\n\t\t\tawait this.engine.renderUntilLive(siteIds, titles, expectedStatus);\n\t\t} catch (error) {\n\t\t\t// El bloqueo lo pone build/start y lo quita build/end. Si el render murió entre ambos, aquí es donde se\n\t\t\t// suelta; si ya no había bloqueo, releaseRenderLock no protesta.\n\t\t\tfor (const siteId of siteIds) await this.engine.releaseRenderLock(siteId);\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n","/**\n * Contrato del transporte HTTP y decoradores opcionales para envolverlo.\n *\n * El transporte es la única dependencia de runtime del paquete, y se inyecta: así no arrastra ningún cliente\n * HTTP y funciona igual en Node, en un bundler o dentro de un runner de tests. Los decoradores son funciones que\n * reciben un transporte y devuelven otro con el mismo contrato, de modo que reintentos, timeouts o trazas se\n * añaden componiendo, sin que el motor sepa nada.\n */\nimport { type Logger, NOOP_LOGGER } from \"./logger\";\nimport { defaultSleep } from \"./sleep\";\n\n/** Respuesta HTTP normalizada: status y body ya parseado. */\nexport interface TransportResponse {\n\tstatus: number;\n\tbody: any;\n}\n\n/** Opciones de una request: cabeceras y body ya serializable. */\nexport interface RequestOptions {\n\theaders?: Record<string, string>;\n\tbody?: unknown;\n}\n\n/**\n * Transporte HTTP inyectable. Impleméntalo sobre `fetch`, `axios`, `undici` o lo que uses.\n *\n * ⚠️ **No debe lanzar en 4xx/5xx**: devuelve `{ status, body }` y deja que el motor decida. Es deliberado: un\n * 4xx de `build/start` es una respuesta esperada (la ruta del site aún no está lista), no un fallo. Un cliente\n * que lance en error — como los que devuelven el body deserializado y tiran en no-2xx — necesita un adaptador\n * que capture y traduzca a `{ status, body }`.\n */\nexport interface Transport {\n\tget(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n\tpost(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n\tput(url: string, opts?: RequestOptions): Promise<TransportResponse>;\n}\n\n/** Métodos que un decorador puede tratar de forma distinta. */\nexport type TransportMethod = \"get\" | \"post\" | \"put\";\n\n/** Ajustes de {@link withRetry}. */\nexport interface RetryOptions {\n\t/** Intentos totales, incluido el primero. Por defecto 3. */\n\tattempts?: number;\n\t/** Espera entre intentos (ms). Por defecto 500, y crece linealmente con el intento. */\n\tdelayMs?: number;\n\t/**\n\t * Métodos que se reintentan. Por defecto **solo `get`**.\n\t *\n\t * No es timidez: `build/start` toma un bloqueo del site y `build/end` lo libera, así que repetir a ciegas una\n\t * escritura puede dejar el protocolo a medias. Amplíalo solo si sabes que tu caso lo tolera.\n\t */\n\tmethods?: TransportMethod[];\n\t/** Qué respuesta merece otro intento. Por defecto, los 5xx (un 4xx no se arregla repitiendo). */\n\tshouldRetry?: (res: TransportResponse) => boolean;\n\t/** Espera inyectable, para tests instantáneos. */\n\tsleep?: (ms: number) => Promise<void>;\n\tlogger?: Logger;\n}\n\n/**\n * Envuelve un transporte para que reintente los fallos pasajeros: una respuesta 5xx o un rechazo del cliente\n * (red caída, DNS, socket cerrado). Agotados los intentos, deja pasar la última respuesta o relanza el error,\n * para que quien llama decida.\n *\n * ```ts\n * const transport = withRetry(myTransport, { attempts: 4 });\n * ```\n */\nexport const withRetry = (transport: Transport, opts: RetryOptions = {}): Transport => {\n\tconst attempts = opts.attempts ?? 3;\n\tconst delayMs = opts.delayMs ?? 500;\n\tconst methods = opts.methods ?? [\"get\"];\n\tconst shouldRetry = opts.shouldRetry ?? ((res: TransportResponse) => res.status >= 500);\n\tconst sleep = opts.sleep ?? defaultSleep;\n\tconst logger = opts.logger ?? NOOP_LOGGER;\n\n\tconst run = async (\n\t\tmethod: TransportMethod,\n\t\turl: string,\n\t\treqOpts?: RequestOptions,\n\t): Promise<TransportResponse> => {\n\t\tif (!methods.includes(method)) return transport[method](url, reqOpts);\n\n\t\tlet lastError: unknown;\n\t\tfor (let attempt = 1; attempt <= attempts; attempt++) {\n\t\t\ttry {\n\t\t\t\tconst res = await transport[method](url, reqOpts);\n\t\t\t\tif (attempt === attempts || !shouldRetry(res)) return res;\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`retry ${method.toUpperCase()} ${url}: status ${res.status}, intento ${attempt}/${attempts}`,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tlastError = error;\n\t\t\t\tif (attempt === attempts) throw error;\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`retry ${method.toUpperCase()} ${url}: el transporte falló, intento ${attempt}/${attempts}`,\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait sleep(delayMs * attempt);\n\t\t}\n\t\t// Inalcanzable: el último intento sale por return o por throw. Está por si `attempts` llega como 0.\n\t\tthrow (\n\t\t\tlastError ??\n\t\t\tnew Error(`withRetry: ningún intento ejecutado para ${method.toUpperCase()} ${url}`)\n\t\t);\n\t};\n\n\treturn {\n\t\tget: (url, reqOpts) => run(\"get\", url, reqOpts),\n\t\tpost: (url, reqOpts) => run(\"post\", url, reqOpts),\n\t\tput: (url, reqOpts) => run(\"put\", url, reqOpts),\n\t};\n};\n\n/** Ajustes de {@link withTimeout}. */\nexport interface TimeoutOptions {\n\t/** Milisegundos antes de dar la request por perdida. */\n\tms: number;\n\tlogger?: Logger;\n}\n\n/**\n * Envuelve un transporte para que ninguna request se quede colgada para siempre: pasado el plazo, rechaza.\n *\n * ⚠️ **No cancela la request subyacente**, solo deja de esperarla: el paquete no conoce tu cliente HTTP, así que\n * no puede abortarlo. Si necesitas cancelación real, implementa el plazo dentro de tu propio transporte con un\n * `AbortController`.\n */\nexport const withTimeout = (transport: Transport, opts: TimeoutOptions): Transport => {\n\tconst logger = opts.logger ?? NOOP_LOGGER;\n\n\tconst race = (\n\t\tmethod: TransportMethod,\n\t\turl: string,\n\t\treqOpts?: RequestOptions,\n\t): Promise<TransportResponse> => {\n\t\tlet timer: ReturnType<typeof setTimeout>;\n\t\tconst timeout = new Promise<never>((_resolve, reject) => {\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tlogger.warn(`timeout ${method.toUpperCase()} ${url}: sin respuesta en ${opts.ms}ms`);\n\t\t\t\treject(new Error(`Timeout de ${opts.ms}ms en ${method.toUpperCase()} ${url}`));\n\t\t\t}, opts.ms);\n\t\t});\n\t\treturn Promise.race([transport[method](url, reqOpts), timeout]).finally(() =>\n\t\t\tclearTimeout(timer),\n\t\t);\n\t};\n\n\treturn {\n\t\tget: (url, reqOpts) => race(\"get\", url, reqOpts),\n\t\tpost: (url, reqOpts) => race(\"post\", url, reqOpts),\n\t\tput: (url, reqOpts) => race(\"put\", url, reqOpts),\n\t};\n};\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@griddo/cx-render-mock",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Test-double del render de griddo-cx: núcleo de decisiones puro + motor de protocolo con transporte HTTP inyectable. Cero dependencias de runtime.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -42,15 +42,19 @@
42
42
  "format": "biome format --write .",
43
43
  "verify": "yarn lint && yarn typecheck && yarn test",
44
44
  "prepublishOnly": "yarn verify && yarn build",
45
- "test:coverage": "vitest run --coverage"
45
+ "test:coverage": "vitest run --coverage",
46
+ "mutation": "stryker run"
46
47
  },
47
48
  "devDependencies": {
48
- "@biomejs/biome": "2.5.7",
49
+ "@biomejs/biome": "2.5.15",
50
+ "@stryker-mutator/core": "^10.0.0",
51
+ "@stryker-mutator/vitest-runner": "^10.0.0",
49
52
  "@types/node": "^26.1.2",
50
- "@vitest/coverage-v8": "^4.1.10",
53
+ "@vitest/coverage-v8": "4.1.11",
51
54
  "tsup": "^8.5.1",
52
55
  "typescript": "^5.9.3",
53
- "vitest": "^4.1.10"
56
+ "vite": "8.3.1",
57
+ "vitest": "4.1.11"
54
58
  },
55
59
  "engines": {
56
60
  "node": ">=18"