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