@griddo/cx-render-mock 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,591 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CmsPublisher: () => CmsPublisher,
24
+ CxRenderEngine: () => CxRenderEngine,
25
+ CxRenderMock: () => CxRenderMock,
26
+ DEFAULT_PAGE_LIST_QUERY: () => DEFAULT_PAGE_LIST_QUERY,
27
+ DEFAULT_TIMINGS: () => DEFAULT_TIMINGS,
28
+ LIVE_STATUS: () => LIVE_STATUS,
29
+ NOOP_LOGGER: () => NOOP_LOGGER,
30
+ RENDER_ERROR_CODE: () => RENDER_ERROR_CODE,
31
+ RenderError: () => RenderError,
32
+ buildEndBody: () => buildEndBody,
33
+ classifyPage: () => classifyPage,
34
+ collectRepublishIds: () => collectRepublishIds,
35
+ createConsoleLogger: () => createConsoleLogger,
36
+ decideBuildStartStep: () => decideBuildStartStep,
37
+ decideSettleStep: () => decideSettleStep,
38
+ diffMissingByTitle: () => diffMissingByTitle,
39
+ listPageUrl: () => listPageUrl,
40
+ pageListSignature: () => pageListSignature,
41
+ selectPendingSites: () => selectPendingSites,
42
+ withRetry: () => withRetry,
43
+ withTimeout: () => withTimeout
44
+ });
45
+ module.exports = __toCommonJS(index_exports);
46
+
47
+ // src/core.ts
48
+ var LIVE_STATUS = {
49
+ UNPUBLISHED: 1,
50
+ // isUnpublished / offline — estado final de despublicación
51
+ PENDING_PUBLISHING: 2,
52
+ // isPendingPublishing / upload-pending — transitorio
53
+ LIVE: 3,
54
+ // isLiveStatus / active — estado final de publicación
55
+ PENDING_UNPUBLISHING: 4
56
+ // isPendingUnpublishing / offline-pending — transitorio
57
+ };
58
+ var DEFAULT_TIMINGS = {
59
+ settleMs: 45e3,
60
+ settlePollMs: 3e3,
61
+ maxRounds: 3,
62
+ roundWaitMs: 1500,
63
+ buildStartMaxWaits: 15,
64
+ buildStartRetryWaitMs: 2e3
65
+ };
66
+ var DEFAULT_PAGE_LIST_QUERY = { page: 1, itemsPerPage: 300 };
67
+ var listPageUrl = (apiUrl, siteId, query = DEFAULT_PAGE_LIST_QUERY) => `${apiUrl}site/${siteId}/pages?deleted=false&page=${query.page}&itemsPerPage=${query.itemsPerPage}&format=list`;
68
+ var pageListSignature = (items) => (items || []).filter((p) => p && p.id != null).map((p) => `${p.id}:${p.liveStatus?.id ?? ""}:${p.hash ?? ""}`).sort().join("|");
69
+ var decideSettleStep = (prevSignature, signature, elapsedMs, timings = DEFAULT_TIMINGS) => {
70
+ if (elapsedMs >= timings.settleMs) return { action: "go" };
71
+ if (prevSignature !== null && prevSignature === signature) return { action: "go" };
72
+ return { action: "wait", waitMs: Math.min(timings.settlePollMs, timings.settleMs - elapsedMs) };
73
+ };
74
+ var classifyPage = (hash, status) => {
75
+ if (hash != null) return "publish";
76
+ if (status === LIVE_STATUS.PENDING_PUBLISHING) return "needsHash";
77
+ return "skip";
78
+ };
79
+ var diffMissingByTitle = (items, wanted, expectedStatus) => {
80
+ const w = wanted || [];
81
+ if (w.length === 0) return [];
82
+ const statusByTitle = {};
83
+ items.forEach((p) => {
84
+ if (p && p.title != null && w.includes(p.title)) {
85
+ const st = p.liveStatus?.id;
86
+ if (st === expectedStatus || !(p.title in statusByTitle)) statusByTitle[p.title] = st ?? -1;
87
+ }
88
+ });
89
+ return w.filter((t) => statusByTitle[t] !== expectedStatus);
90
+ };
91
+ var buildEndBody = (publishHashes, siteHash, unpublishHashes, publishPagesIds) => ({
92
+ publishHashes,
93
+ siteHash: siteHash ?? null,
94
+ unpublishHashes,
95
+ publishPagesIds
96
+ });
97
+ var collectRepublishIds = (items, missing) => {
98
+ const ids = [];
99
+ items.forEach((p) => {
100
+ const st = p?.liveStatus?.id;
101
+ if (p && p.id != null && missing.includes(p.title) && st !== LIVE_STATUS.LIVE)
102
+ ids.push(p.id);
103
+ });
104
+ return Array.from(new Set(ids));
105
+ };
106
+ var decideBuildStartStep = (status, startWaits, timings = DEFAULT_TIMINGS) => {
107
+ if (status < 400) return { action: "proceed" };
108
+ if (startWaits >= timings.buildStartMaxWaits) return { action: "fail" };
109
+ return { action: "retry", waitMs: timings.buildStartRetryWaitMs };
110
+ };
111
+ var selectPendingSites = (sites, onlyIds) => {
112
+ const ids = [];
113
+ (sites || []).forEach((site) => {
114
+ if (!site || site.id == null || !site.shouldBeUpdated) return;
115
+ if (onlyIds && !onlyIds.includes(site.id)) return;
116
+ ids.push(site.id);
117
+ });
118
+ return Array.from(new Set(ids));
119
+ };
120
+
121
+ // src/errors.ts
122
+ var RENDER_ERROR_CODE = {
123
+ /** `build/start` siguió respondiendo error hasta agotar los reintentos: el site no llega a estar listo. */
124
+ BUILD_START_NOT_READY: "BUILD_START_NOT_READY",
125
+ /** `build/end` respondió >= 400: el render no se ha aplicado y el bloqueo puede seguir puesto. */
126
+ BUILD_END_FAILED: "BUILD_END_FAILED",
127
+ /** Se agotaron las rondas y algunos títulos no alcanzaron el estado final. Mira `missingTitles`. */
128
+ TITLES_NOT_FINAL: "TITLES_NOT_FINAL",
129
+ /** No se pudo saber qué sites tenían render pendiente. */
130
+ SITES_LOOKUP_FAILED: "SITES_LOOKUP_FAILED",
131
+ /** No se pudo leer el listado de páginas de un site. */
132
+ PAGE_LIST_FAILED: "PAGE_LIST_FAILED",
133
+ /** Falló marcar páginas como pendientes de publicar. */
134
+ MARK_PENDING_FAILED: "MARK_PENDING_FAILED",
135
+ /** Falló publicar un site. */
136
+ PUBLISH_FAILED: "PUBLISH_FAILED"
137
+ };
138
+ var RenderError = class extends Error {
139
+ constructor(code, message, detail = {}) {
140
+ super(message);
141
+ this.name = "RenderError";
142
+ this.code = code;
143
+ this.status = detail.status;
144
+ this.body = detail.body;
145
+ this.missingTitles = detail.missingTitles;
146
+ if (detail.cause !== void 0) this.cause = detail.cause;
147
+ }
148
+ };
149
+
150
+ // src/logger.ts
151
+ var NOOP_LOGGER = {
152
+ debug: () => void 0,
153
+ info: () => void 0,
154
+ warn: () => void 0,
155
+ error: () => void 0
156
+ };
157
+ var createConsoleLogger = (level = "info") => {
158
+ const order = { debug: 10, info: 20, warn: 30, error: 40 };
159
+ const min = order[level];
160
+ const emit = (method) => (message, meta) => {
161
+ if (order[method] < min) return;
162
+ if (meta === void 0) console[method](message);
163
+ else console[method](message, meta);
164
+ };
165
+ return { debug: emit("debug"), info: emit("info"), warn: emit("warn"), error: emit("error") };
166
+ };
167
+
168
+ // src/api-client.ts
169
+ var ApiClient = class {
170
+ constructor(config) {
171
+ this.transport = config.transport;
172
+ this.apiUrl = config.apiUrl;
173
+ this.headers = {
174
+ "Content-Type": "application/json",
175
+ authorization: `Bearer ${config.authToken}`
176
+ };
177
+ this.logger = config.logger ?? NOOP_LOGGER;
178
+ }
179
+ /** URL absoluta de un path relativo a `apiUrl`. Útil cuando la URL la compone el núcleo. */
180
+ url(path) {
181
+ return `${this.apiUrl}${path}`;
182
+ }
183
+ get(path, spec = {}) {
184
+ return this.send("GET", path, spec);
185
+ }
186
+ post(path, spec = {}) {
187
+ return this.send("POST", path, spec);
188
+ }
189
+ put(path, spec = {}) {
190
+ return this.send("PUT", path, spec);
191
+ }
192
+ /**
193
+ * Lanza si la respuesta trae error. Se usa cuando la llamada ya se hizo sin `expectOk` porque había que
194
+ * inspeccionar el status antes de decidir.
195
+ */
196
+ ensureOk(res, code, describe) {
197
+ if (res.status < 400) return;
198
+ throw new RenderError(code, `${describe} respondi\xF3 ${res.status}`, {
199
+ status: res.status,
200
+ body: res.body
201
+ });
202
+ }
203
+ async send(method, path, spec) {
204
+ const url = path.startsWith("http") ? path : this.url(path);
205
+ const opts = { headers: this.headers, body: spec.body };
206
+ const res = await (method === "GET" ? this.transport.get(url, opts) : method === "POST" ? this.transport.post(url, opts) : this.transport.put(url, opts));
207
+ if (spec.expectOk) this.ensureOk(res, spec.expectOk, `${method} ${path}`);
208
+ return res;
209
+ }
210
+ };
211
+
212
+ // src/sleep.ts
213
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
214
+
215
+ // src/engine.ts
216
+ var CxRenderEngine = class {
217
+ constructor(deps) {
218
+ this.api = new ApiClient(deps);
219
+ this.logger = this.api.logger;
220
+ this.sleep = deps.sleep ?? defaultSleep;
221
+ this.timings = { ...DEFAULT_TIMINGS, ...deps.timings };
222
+ this.pageList = { ...DEFAULT_PAGE_LIST_QUERY, ...deps.pageList };
223
+ }
224
+ /**
225
+ * Renderiza un site en **una sola pasada**: settle adaptativo → `build/start` → `GET page/{id}` →
226
+ * `build/end`.
227
+ *
228
+ * El settle previo da tiempo a que el backend termine de propagar las copias de las páginas globales; sin él,
229
+ * `build/start` devolvería una lista incompleta. Es ADAPTATIVO: sondea el listado hasta que dos sondas
230
+ * consecutivas coinciden (quiescencia) o hasta el techo `timings.settleMs` — rápido cuando el backend va
231
+ * suelto, paciente bajo carga. Las páginas que lleguen tarde no se reintentan aquí: de eso se encarga
232
+ * {@link renderUntilLive}.
233
+ *
234
+ * ⚠️ `build/start` deja el site bloqueado y `build/end` lo libera. En el camino normal `build/end` se emite
235
+ * siempre, pero si el transporte rechaza entre ambos el bloqueo queda colgado durante horas: llama a
236
+ * {@link releaseRenderLock} en tu `finally`.
237
+ *
238
+ * @throws {RenderError} `BUILD_START_NOT_READY` si el site no llega a estar listo, `BUILD_END_FAILED` si el
239
+ * render no se aplica.
240
+ */
241
+ async renderSiteOnce(siteId) {
242
+ const startOnce = async (startWaits = 0) => {
243
+ const bs = await this.api.get(`site/${siteId}/build/start`);
244
+ const startStep = decideBuildStartStep(bs.status, startWaits, this.timings);
245
+ if (startStep.action === "fail") {
246
+ throw new RenderError(
247
+ RENDER_ERROR_CODE.BUILD_START_NOT_READY,
248
+ `GET site/${siteId}/build/start sigue en ${bs.status} tras ${startWaits} esperas (\xBFel site no acaba de asentarse?)`,
249
+ { status: bs.status, body: bs.body }
250
+ );
251
+ }
252
+ if (startStep.action === "retry") {
253
+ this.logger.debug(
254
+ `build/start del site ${siteId} en ${bs.status}: espera ${startStep.waitMs}ms y reintenta`
255
+ );
256
+ await this.sleep(startStep.waitMs);
257
+ return startOnce(startWaits + 1);
258
+ }
259
+ const siteHash = bs.body?.siteHash || null;
260
+ const publishIds = bs.body?.publishIds || [];
261
+ const unpublishHashes = bs.body?.unpublishHashes || [];
262
+ this.logger.debug(
263
+ `site ${siteId}: ${publishIds.length} p\xE1ginas candidatas, ${unpublishHashes.length} a despublicar`
264
+ );
265
+ const publishHashes = [];
266
+ const publishPagesIds = [];
267
+ for (const pageId of publishIds) {
268
+ const pres = await this.api.get(`page/${pageId}`);
269
+ const hash = pres.body?.hash;
270
+ const status = pres.body?.liveStatus?.id;
271
+ if (classifyPage(hash, status) === "publish") {
272
+ publishHashes.push(hash);
273
+ publishPagesIds.push(pageId);
274
+ }
275
+ }
276
+ await this.api.post(`site/${siteId}/build/end`, {
277
+ body: buildEndBody(publishHashes, siteHash, unpublishHashes, publishPagesIds),
278
+ expectOk: RENDER_ERROR_CODE.BUILD_END_FAILED
279
+ });
280
+ this.logger.info(`site ${siteId} renderizado: ${publishPagesIds.length} p\xE1ginas publicadas`);
281
+ };
282
+ await this.settleBeforeStart(siteId);
283
+ return startOnce();
284
+ }
285
+ /**
286
+ * Settle ADAPTATIVO previo al build/start: sondea el listado del site cada `timings.settlePollMs` y arranca
287
+ * cuando dos sondas consecutivas devuelven la misma firma (`cxCore.pageListSignature`) o al agotar el techo
288
+ * `timings.settleMs` (la decisión es de `cxCore.decideSettleStep`). Best-effort: si el listado falla durante
289
+ * el settle, se procede al render en vez de morir aquí (peor caso = el de siempre, un pase incompleto que
290
+ * recogen las rondas de `renderUntilLive`). Con `settleMs <= 0` no sondea (settle desactivado).
291
+ */
292
+ async settleBeforeStart(siteId) {
293
+ if (this.timings.settleMs <= 0) return;
294
+ try {
295
+ let prev = null;
296
+ let elapsed = 0;
297
+ for (; ; ) {
298
+ const sig = pageListSignature(await this.listPages([siteId]));
299
+ const step = decideSettleStep(prev, sig, elapsed, this.timings);
300
+ if (step.action === "go") return;
301
+ prev = sig;
302
+ await this.sleep(step.waitMs);
303
+ elapsed += step.waitMs;
304
+ }
305
+ } catch (err) {
306
+ this.logger.warn(
307
+ `settle del site ${siteId}: el sondeo del listado fall\xF3 (${err instanceof Error ? err.message : String(err)}); se procede al render sin asentar`
308
+ );
309
+ }
310
+ }
311
+ /**
312
+ * Renderiza **todos los sites que tengan render pendiente**, que es lo que hace el render real en cada pasada:
313
+ * pregunta al backend qué sites tienen cambios sin renderizar (`GET sites/all` → `shouldBeUpdated`) y los
314
+ * resuelve uno por uno.
315
+ *
316
+ * @param onlyIds acota a esos sites. **Úsalo si compartes la base de datos con otros procesos**: sin acotar,
317
+ * esta pasada arrastra también el trabajo pendiente de los demás, que es tan lento como caro.
318
+ * @returns los ids que ha renderizado.
319
+ * @throws {RenderError} `SITES_LOOKUP_FAILED` si no se puede consultar la lista de sites.
320
+ */
321
+ async renderPendingSites(onlyIds) {
322
+ const res = await this.api.get("sites/all", {
323
+ expectOk: RENDER_ERROR_CODE.SITES_LOOKUP_FAILED
324
+ });
325
+ const all = Array.isArray(res.body) ? res.body : res.body?.items || [];
326
+ const pending = selectPendingSites(all, onlyIds);
327
+ this.logger.info(
328
+ pending.length > 0 ? `sites con render pendiente: ${pending.join(", ")}` : "ning\xFAn site con render pendiente"
329
+ );
330
+ for (const siteId of pending) await this.renderSiteOnce(siteId);
331
+ return pending;
332
+ }
333
+ /**
334
+ * Lleva los títulos pedidos a su estado final, reintentando lo que falte.
335
+ *
336
+ * Mientras queden títulos pendientes, vuelve a marcar como pendientes las copias que se hayan quedado
337
+ * despublicadas y renderiza otra vez, hasta `timings.maxRounds` rondas; la última confirma o lanza. Si todo
338
+ * está ya en su sitio, la primera comprobación pasa y no renderiza nada.
339
+ *
340
+ * @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`; usa `LIVE_STATUS.UNPUBLISHED`
341
+ * para confirmar una despublicación.
342
+ * @throws {RenderError} `TITLES_NOT_FINAL` si tras la última ronda sigue faltando algún título.
343
+ */
344
+ async renderUntilLive(siteIds, titles, expectedStatus = LIVE_STATUS.LIVE) {
345
+ const wanted = titles || [];
346
+ if (wanted.length === 0) return;
347
+ const attempt = async (round) => {
348
+ const missing = await this.missingPagesByTitle(siteIds, wanted, expectedStatus);
349
+ if (missing.length === 0) return;
350
+ if (round >= this.timings.maxRounds)
351
+ return this.confirmPagesByTitle(siteIds, wanted, expectedStatus);
352
+ this.logger.warn(
353
+ `ronda ${round}/${this.timings.maxRounds}: faltan ${missing.length}/${wanted.length} t\xEDtulos (${missing.join(", ")})`
354
+ );
355
+ await this.republishMissingWantedLive(siteIds, missing, expectedStatus);
356
+ for (const siteId of siteIds) await this.renderSiteOnce(siteId);
357
+ await this.sleep(this.timings.roundWaitMs);
358
+ return attempt(round + 1);
359
+ };
360
+ return attempt(1);
361
+ }
362
+ /**
363
+ * Comprueba, sin renderizar, que los títulos están en `expectedStatus`.
364
+ *
365
+ * @throws {RenderError} `TITLES_NOT_FINAL`, con los títulos que faltan en `missingTitles`.
366
+ */
367
+ async confirmPagesByTitle(siteIds, titles, expectedStatus = LIVE_STATUS.LIVE) {
368
+ const wanted = titles || [];
369
+ if (wanted.length === 0) return;
370
+ const missing = await this.missingPagesByTitle(siteIds, wanted, expectedStatus);
371
+ if (missing.length > 0) {
372
+ throw new RenderError(
373
+ RENDER_ERROR_CODE.TITLES_NOT_FINAL,
374
+ `estos t\xEDtulos no est\xE1n en liveStatus ${expectedStatus}: ${missing.join(", ")}`,
375
+ {
376
+ missingTitles: missing
377
+ }
378
+ );
379
+ }
380
+ }
381
+ /**
382
+ * Libera el bloqueo de render que deja `build/start`, emitiendo un `build/end` vacío.
383
+ *
384
+ * Llámalo en tu `finally` si un render puede abortar a medias. Es best-effort: no lanza aunque el backend
385
+ * responda con error, porque el caso normal es que ya no hubiera nada que liberar.
386
+ */
387
+ async releaseRenderLock(siteId) {
388
+ const res = await this.api.post(`site/${siteId}/build/end`, {
389
+ body: buildEndBody([], null, [], [])
390
+ });
391
+ if (res.status >= 400) {
392
+ this.logger.debug(
393
+ `releaseRenderLock del site ${siteId}: el backend respondi\xF3 ${res.status} (probablemente no hab\xEDa bloqueo)`
394
+ );
395
+ }
396
+ }
397
+ /** Títulos que aún no están en `expectedStatus`, leyendo el listado completo de cada site. */
398
+ async missingPagesByTitle(siteIds, titles, expectedStatus) {
399
+ const wanted = titles || [];
400
+ if (wanted.length === 0) return [];
401
+ const items = await this.listPages(siteIds);
402
+ return diffMissingByTitle(items, wanted, expectedStatus);
403
+ }
404
+ /** Devuelve a "pendiente de publicar" las copias despublicadas de los títulos que faltan. */
405
+ async republishMissingWantedLive(siteIds, missing, expectedStatus) {
406
+ if (expectedStatus !== LIVE_STATUS.LIVE || missing.length === 0) return;
407
+ const items = await this.listPages(siteIds);
408
+ const uniqueIds = collectRepublishIds(items, missing);
409
+ if (uniqueIds.length === 0) return;
410
+ this.logger.info(`re-marcando ${uniqueIds.length} p\xE1ginas como pendientes de publicar`);
411
+ await this.api.put("pages/status/upload-pending", { body: { ids: uniqueIds } });
412
+ }
413
+ /**
414
+ * Páginas de los sites indicados, **paginando hasta el final** del listado.
415
+ *
416
+ * Recorre páginas mientras el backend devuelva el lote completo: si llega uno más corto, es el último. Así un
417
+ * site con más páginas que `pageList.itemsPerPage` no se queda a medias, que era la causa de confirmaciones
418
+ * por título erróneas cuando solo se leía la primera página.
419
+ *
420
+ * @throws {RenderError} `PAGE_LIST_FAILED` si el backend no puede devolver el listado.
421
+ */
422
+ async listPages(siteIds) {
423
+ const items = [];
424
+ for (const siteId of siteIds) {
425
+ let page = this.pageList.page;
426
+ for (let fetched = 0; fetched < MAX_LIST_PAGES; fetched++) {
427
+ const query = { page, itemsPerPage: this.pageList.itemsPerPage };
428
+ const res = await this.api.get(listPageUrl(this.api.url(""), siteId, query), {
429
+ expectOk: RENDER_ERROR_CODE.PAGE_LIST_FAILED
430
+ });
431
+ const batch = res.body && (res.body.items || res.body) || [];
432
+ items.push(...batch);
433
+ if (batch.length < this.pageList.itemsPerPage) break;
434
+ page += 1;
435
+ if (fetched === MAX_LIST_PAGES - 1) {
436
+ this.logger.warn(
437
+ `listado del site ${siteId} cortado en ${MAX_LIST_PAGES} p\xE1ginas: \xBFel backend ignora el par\xE1metro page?`
438
+ );
439
+ }
440
+ }
441
+ }
442
+ return items;
443
+ }
444
+ };
445
+ var MAX_LIST_PAGES = 1e3;
446
+
447
+ // src/publish.ts
448
+ var CmsPublisher = class {
449
+ constructor(deps) {
450
+ this.api = new ApiClient(deps);
451
+ this.logger = this.api.logger;
452
+ }
453
+ /**
454
+ * Marca páginas como pendientes de publicar. Sin ids no hace nada.
455
+ *
456
+ * @throws {RenderError} `MARK_PENDING_FAILED` si el backend responde >= 400.
457
+ */
458
+ async markPagesPending(pageIds) {
459
+ if (!pageIds || pageIds.length === 0) return;
460
+ await this.api.put("pages/status/upload-pending", {
461
+ body: { ids: pageIds },
462
+ expectOk: RENDER_ERROR_CODE.MARK_PENDING_FAILED
463
+ });
464
+ this.logger.info(`${pageIds.length} p\xE1ginas marcadas como pendientes de publicar`);
465
+ }
466
+ /**
467
+ * Publica los sites indicados, uno por uno.
468
+ *
469
+ * @throws {RenderError} `PUBLISH_FAILED` si el backend responde >= 400 para alguno.
470
+ */
471
+ async publishSites(siteIds) {
472
+ for (const siteId of siteIds || []) {
473
+ await this.api.post(`site/${siteId}/publish`, { expectOk: RENDER_ERROR_CODE.PUBLISH_FAILED });
474
+ this.logger.info(`site ${siteId} publicado`);
475
+ }
476
+ }
477
+ /**
478
+ * Los dos pasos en orden: marca las páginas y publica los sites. Es el disparo completo que deja el trabajo
479
+ * listo para que el motor lo renderice.
480
+ */
481
+ async publishPages(siteIds, pageIds) {
482
+ await this.markPagesPending(pageIds);
483
+ await this.publishSites(siteIds);
484
+ }
485
+ };
486
+
487
+ // src/facade.ts
488
+ var CxRenderMock = class {
489
+ constructor(deps) {
490
+ this.engine = new CxRenderEngine(deps);
491
+ this.publisher = new CmsPublisher(deps);
492
+ }
493
+ /**
494
+ * El flujo entero: publica, renderiza y confirma que los títulos llegaron a su estado final. Libera el bloqueo
495
+ * de render de cada site si algo falla a medias.
496
+ *
497
+ * @param expectedStatus estado final esperado. Por defecto `LIVE_STATUS.LIVE`.
498
+ * @throws {RenderError} con el `code` de la etapa que falló.
499
+ */
500
+ async publishAndRender(siteIds, pageIds, titles, expectedStatus = LIVE_STATUS.LIVE) {
501
+ await this.publisher.publishPages(siteIds, pageIds);
502
+ try {
503
+ await this.engine.renderUntilLive(siteIds, titles, expectedStatus);
504
+ } catch (error) {
505
+ for (const siteId of siteIds) await this.engine.releaseRenderLock(siteId);
506
+ throw error;
507
+ }
508
+ }
509
+ };
510
+
511
+ // src/transport.ts
512
+ var withRetry = (transport, opts = {}) => {
513
+ const attempts = opts.attempts ?? 3;
514
+ const delayMs = opts.delayMs ?? 500;
515
+ const methods = opts.methods ?? ["get"];
516
+ const shouldRetry = opts.shouldRetry ?? ((res) => res.status >= 500);
517
+ const sleep = opts.sleep ?? defaultSleep;
518
+ const logger = opts.logger ?? NOOP_LOGGER;
519
+ const run = async (method, url, reqOpts) => {
520
+ if (!methods.includes(method)) return transport[method](url, reqOpts);
521
+ let lastError;
522
+ for (let attempt = 1; attempt <= attempts; attempt++) {
523
+ try {
524
+ const res = await transport[method](url, reqOpts);
525
+ if (attempt === attempts || !shouldRetry(res)) return res;
526
+ logger.warn(
527
+ `retry ${method.toUpperCase()} ${url}: status ${res.status}, intento ${attempt}/${attempts}`
528
+ );
529
+ } catch (error) {
530
+ lastError = error;
531
+ if (attempt === attempts) throw error;
532
+ logger.warn(
533
+ `retry ${method.toUpperCase()} ${url}: el transporte fall\xF3, intento ${attempt}/${attempts}`,
534
+ error
535
+ );
536
+ }
537
+ await sleep(delayMs * attempt);
538
+ }
539
+ throw lastError ?? new Error(`withRetry: ning\xFAn intento ejecutado para ${method.toUpperCase()} ${url}`);
540
+ };
541
+ return {
542
+ get: (url, reqOpts) => run("get", url, reqOpts),
543
+ post: (url, reqOpts) => run("post", url, reqOpts),
544
+ put: (url, reqOpts) => run("put", url, reqOpts)
545
+ };
546
+ };
547
+ var withTimeout = (transport, opts) => {
548
+ const logger = opts.logger ?? NOOP_LOGGER;
549
+ const race = (method, url, reqOpts) => {
550
+ let timer;
551
+ const timeout = new Promise((_resolve, reject) => {
552
+ timer = setTimeout(() => {
553
+ logger.warn(`timeout ${method.toUpperCase()} ${url}: sin respuesta en ${opts.ms}ms`);
554
+ reject(new Error(`Timeout de ${opts.ms}ms en ${method.toUpperCase()} ${url}`));
555
+ }, opts.ms);
556
+ });
557
+ return Promise.race([transport[method](url, reqOpts), timeout]).finally(
558
+ () => clearTimeout(timer)
559
+ );
560
+ };
561
+ return {
562
+ get: (url, reqOpts) => race("get", url, reqOpts),
563
+ post: (url, reqOpts) => race("post", url, reqOpts),
564
+ put: (url, reqOpts) => race("put", url, reqOpts)
565
+ };
566
+ };
567
+ // Annotate the CommonJS export names for ESM import in node:
568
+ 0 && (module.exports = {
569
+ CmsPublisher,
570
+ CxRenderEngine,
571
+ CxRenderMock,
572
+ DEFAULT_PAGE_LIST_QUERY,
573
+ DEFAULT_TIMINGS,
574
+ LIVE_STATUS,
575
+ NOOP_LOGGER,
576
+ RENDER_ERROR_CODE,
577
+ RenderError,
578
+ buildEndBody,
579
+ classifyPage,
580
+ collectRepublishIds,
581
+ createConsoleLogger,
582
+ decideBuildStartStep,
583
+ decideSettleStep,
584
+ diffMissingByTitle,
585
+ listPageUrl,
586
+ pageListSignature,
587
+ selectPendingSites,
588
+ withRetry,
589
+ withTimeout
590
+ });
591
+ //# sourceMappingURL=index.js.map