@griddo/cx 11.13.0 → 11.13.2

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.
Files changed (60) hide show
  1. package/README.md +2 -3
  2. package/build/commands/end-render.js +150 -20
  3. package/build/commands/end-render.js.map +4 -4
  4. package/build/commands/prepare-assets-directory.js +146 -8
  5. package/build/commands/prepare-assets-directory.js.map +4 -4
  6. package/build/commands/prepare-domains-render.js +158 -28
  7. package/build/commands/prepare-domains-render.js.map +4 -4
  8. package/build/commands/reset-render.js +150 -20
  9. package/build/commands/reset-render.js.map +4 -4
  10. package/build/commands/start-render.js +179 -49
  11. package/build/commands/start-render.js.map +4 -4
  12. package/build/commands/upload-search-content.js +151 -21
  13. package/build/commands/upload-search-content.js.map +4 -4
  14. package/build/core/check-env-health.d.ts +47 -1
  15. package/build/core/http/create-adapter.d.ts +13 -0
  16. package/build/core/http/index.d.ts +6 -0
  17. package/build/core/http/types.d.ts +20 -0
  18. package/build/core/http/undici-adapter.d.ts +7 -0
  19. package/build/core/http/with-circuit-breaker.d.ts +24 -0
  20. package/build/core/http/with-retry.d.ts +32 -0
  21. package/build/index.js +25026 -42
  22. package/build/react/GriddoIntegrations/utils.d.ts +1 -1
  23. package/build/services/manage-store.d.ts +8 -1
  24. package/build/services/pages.d.ts +73 -2
  25. package/build/services/reference-fields.d.ts +1 -1
  26. package/build/shared/envs.d.ts +5 -2
  27. package/build/shared/types/api.d.ts +0 -2
  28. package/cli.mjs +28 -10
  29. package/exporter/commands/README.md +1 -1
  30. package/exporter/commands/end-render.ts +1 -1
  31. package/exporter/commands/prepare-domains-render.ts +1 -1
  32. package/exporter/commands/{single-domain-upload-search-content.ts → single-domain-upload-search-content.noop} +2 -4
  33. package/exporter/commands/upload-search-content.ts +1 -4
  34. package/exporter/core/check-env-health.ts +1 -1
  35. package/exporter/core/errors.ts +13 -13
  36. package/exporter/core/fs.ts +35 -31
  37. package/exporter/core/http/create-adapter.ts +58 -0
  38. package/exporter/core/http/index.ts +7 -0
  39. package/exporter/core/http/types.ts +22 -0
  40. package/exporter/core/http/undici-adapter.ts +53 -0
  41. package/exporter/core/http/with-circuit-breaker.ts +86 -0
  42. package/exporter/core/http/with-retry.ts +87 -0
  43. package/exporter/services/api.ts +22 -66
  44. package/exporter/services/auth.ts +11 -2
  45. package/exporter/services/domains.ts +6 -1
  46. package/exporter/services/llms.ts +1 -1
  47. package/exporter/services/manage-store.ts +16 -18
  48. package/exporter/services/pages.ts +7 -0
  49. package/exporter/services/reference-fields.ts +3 -5
  50. package/exporter/services/render.ts +3 -7
  51. package/exporter/services/store.ts +10 -4
  52. package/exporter/shared/envs.ts +20 -6
  53. package/exporter/shared/types/api.ts +0 -2
  54. package/exporter/ssg-adapters/gatsby/index.ts +4 -2
  55. package/exporter/ssg-adapters/gatsby/shared/sync-render.ts +5 -1
  56. package/package.json +15 -16
  57. package/tsconfig.commands.json +9 -22
  58. package/tsconfig.exporter.json +3 -4
  59. package/tsconfig.json +2 -3
  60. package/build/commands/single-domain-upload-search-content.d.ts +0 -1
@@ -8,7 +8,7 @@ declare function filterHeadIntegrations(integrations: Core.PageIntegration[]): {
8
8
  content: string;
9
9
  type: "addon" | "analytics" | "datalayer";
10
10
  }[];
11
- declare const filterPositionIntegrations: (integrations: Core.PageIntegration[], position: "end" | "head" | "start") => {
11
+ declare const filterPositionIntegrations: (integrations: Core.PageIntegration[], position: "head" | "start" | "end") => {
12
12
  content: string;
13
13
  type: "addon" | "analytics" | "datalayer";
14
14
  }[];
@@ -29,4 +29,11 @@ declare function saveSitePagesInStore(siteDirName: string, pages: GriddoPageObje
29
29
  */
30
30
  declare function removeOrphanSites(sitesToPublish: Site[], domain: string): Promise<void>;
31
31
  declare function writeUniqueFileSync(filePath: string, content: string): Promise<void>;
32
- export { getBuildMetadata, getPageInStoreDir, removeOrphanSites, saveRenderInfoInStore, saveSitePagesInStore, writeUniqueFileSync, };
32
+ /**
33
+ * Remove props from an object
34
+ *
35
+ * @param obj The object
36
+ * @param props An array of props to be removed
37
+ */
38
+ declare function removeProperties(obj: Record<string, unknown>, propsToRemove: Set<string>): void;
39
+ export { getBuildMetadata, getPageInStoreDir, removeOrphanSites, removeProperties, saveRenderInfoInStore, saveSitePagesInStore, writeUniqueFileSync, };
@@ -1,6 +1,20 @@
1
- import type { Fields } from "@griddo/core";
1
+ import type { Core, Fields } from "@griddo/core";
2
2
  import type { GriddoListPage, GriddoMultiPage, GriddoPageObject, GriddoSinglePage, MultiPageElements, PageAdditionalInfo } from "../shared/types/pages";
3
3
  import type { TemplateWithReferenceField } from "../shared/types/templates";
4
+ /**
5
+ * Return an OpenGraph object.
6
+ *
7
+ * @param props.socialTitle The social title.
8
+ * @param props.socialDescription The social description.
9
+ * @param props.socialImage The social image in Cloudinary or DAM url format.
10
+ */
11
+ declare function getOpenGraph({ socialTitle, socialDescription, socialImage }: Pick<Core.Page, "socialTitle" | "socialDescription" | "socialImage">): Core.Page["openGraph"];
12
+ /**
13
+ * Get the Page metadata object from a page Object.
14
+ *
15
+ * @param params A Page object
16
+ */
17
+ declare function getPageMetadata(params: Core.Page): GriddoPageObject["context"]["pageMetadata"];
4
18
  /**
5
19
  * Create a single Griddo page object.
6
20
  *
@@ -25,10 +39,67 @@ declare function createGriddoMultiPages(page: GriddoMultiPage, additionalInfo: P
25
39
  * @param page The page to get the multipage parts.
26
40
  */
27
41
  declare function getMultiPageElements(page: TemplateWithReferenceField): Promise<MultiPageElements> | null;
42
+ /**
43
+ * Get `itemsPerPage` elements from the `items` using as offset `page` number.
44
+ * It's a kind of paginator for an array.
45
+ *
46
+ * @param itemsPerPage The items per page.
47
+ * @param items An array of items, the queriedData.
48
+ * @param page The page to be returned.
49
+ *
50
+ * @example
51
+ * getPage(3, ["a", "b", "c", "d", "e", "f", "g", "h"], 2)
52
+ * // -> ["d", "e", "f"]
53
+ */
54
+ declare function getPage(itemsPerPage: number, items: Fields.QueriedDataItem[], page: number): Fields.SimpleContentType<Omit<unknown, "__contentTypeKind">>[];
55
+ /**
56
+ * Takes an array of items and split it grouping in arrays of pages based on `itemsPerPage`.
57
+ *
58
+ * @param itemsPerPage The items per page.
59
+ * @param items An array of items, the queriedData.
60
+ *
61
+ * @example
62
+ * getPageCluster(3, ["a", "b", "c", "d", "e", "f", "g", "h"])
63
+ * // -> [["a", "b", "c"], ["d", "e", "f"], ["g", "h"]]
64
+ */
65
+ declare function getPageCluster(itemsPerPage: number, items: Fields.QueriedDataItem[]): Fields.SimpleContentType<Omit<unknown, "__contentTypeKind">>[][];
28
66
  /**
29
67
  * Takes a template object and split the whole queriedItems into separated queriedItems to use in Griddo static list templates.
30
68
  *
31
69
  * @param listTemplate A template schema with the ReferenceField data included.
32
70
  */
33
71
  declare function getPaginatedPages(listTemplate: TemplateWithReferenceField): Fields.SimpleContentType<Omit<unknown, "__contentTypeKind">>[][];
34
- export { createGriddoListPages, createGriddoMultiPages, createGriddoSinglePage, getMultiPageElements, getPaginatedPages, };
72
+ /**
73
+ * Remove duplicate ending trailing character from a string
74
+ *
75
+ * @param url the strin url
76
+ * @example
77
+ * removeDuplicateTrailingSlash('http://griddo.com/foo/bar//')
78
+ * // -> http://griddo.com/foo/bar/
79
+ */
80
+ declare function removeDuplicateTrailing(url: string): string;
81
+ /**
82
+ * Adds a number to an URL adding optionally an ending slash.
83
+ *
84
+ * @param url The url.
85
+ * @param pageNumber A number to be added to the url.
86
+ * @param options.addEndingSlash Boolean that indicates if return the final url with an end slash or not.
87
+ *
88
+ * @example
89
+ * addPageNumberToUrl("web.com", 3) // "web.com/3"
90
+ * addPageNumberToUrl("web.com", 3, { addEndingSlash: true }) // "web.com/3/"
91
+ */
92
+ declare function addPageNumberToUrl(url: string, pageNumber: number, options?: {
93
+ addEndingSlash: boolean;
94
+ }): string;
95
+ /**
96
+ * Adds a number to an string with the format "string - number"
97
+ *
98
+ * @param title The title
99
+ * @param pageNumber A number to be added to the title.
100
+ *
101
+ * @example
102
+ * addPageNumberToTitle("The page", 3) // "The page - 3"
103
+ */
104
+ declare function addPageNumberToTitle(title: string, pageNumber: number): string;
105
+ export { addPageNumberToTitle, addPageNumberToUrl, createGriddoListPages, createGriddoMultiPages, createGriddoSinglePage, getMultiPageElements, getOpenGraph, getPage, getPageCluster, getPageMetadata, getPaginatedPages, removeDuplicateTrailing, };
@@ -12,7 +12,7 @@ declare function getReferenceFieldData({ page, cacheKey }: {
12
12
  cacheKey: string;
13
13
  }): Promise<{
14
14
  [key: string]: any;
15
- type: "formTemplate" | "template";
15
+ type: "template" | "formTemplate";
16
16
  templateType: string;
17
17
  activeSectionSlug: string;
18
18
  activeSectionBase: string;
@@ -15,8 +15,11 @@ declare const GRIDDO_SSG_BUNDLE_ANALYZER: boolean;
15
15
  declare const GRIDDO_SSG_VERBOSE_LOGS: boolean;
16
16
  declare const GRIDDO_USE_DIST_BACKUP: boolean;
17
17
  declare const GRIDDO_VERBOSE_LOGS: boolean;
18
+ declare const GRIDDO_RENDER_ENABLED_LLM_MD: boolean;
18
19
  declare const GRIDDO_RENDER_API_FETCH_RETRY_WAIT_SECONDS: number;
19
20
  declare const GRIDDO_RENDER_API_FETCH_RETRY_ATTEMPTS: number;
21
+ declare const GRIDDO_RENDER_API_TIMEOUT_MS: number;
22
+ declare const GRIDDO_RENDER_CIRCUIT_BREAKER_FAILURE_THRESHOLD: number;
23
+ declare const GRIDDO_RENDER_CIRCUIT_BREAKER_COOLDOWN_MS: number;
20
24
  declare const GRIDDO_RENDER_LIFECYCLE_RETRY_ATTEMPTS: number;
21
- declare const GRIDDO_RENDER_ENABLED_LLM_MD: boolean;
22
- export { GRIDDO_AI_EMBEDDINGS, GRIDDO_API_CONCURRENCY_COUNT, GRIDDO_API_URL, GRIDDO_ASSET_PREFIX, GRIDDO_BOT_PASSWORD, GRIDDO_BOT_USER, GRIDDO_BUILD_LOGS, GRIDDO_BUILD_LOGS_BUFFER_SIZE, GRIDDO_PUBLIC_API_URL, GRIDDO_REACT_APP_INSTANCE, GRIDDO_RENDER_API_FETCH_RETRY_ATTEMPTS, GRIDDO_RENDER_API_FETCH_RETRY_WAIT_SECONDS, GRIDDO_RENDER_DISABLE_LLMS_TXT, GRIDDO_RENDER_ENABLED_LLM_MD, GRIDDO_RENDER_LIFECYCLE_RETRY_ATTEMPTS, GRIDDO_SEARCH_FEATURE, GRIDDO_SKIP_BUILD_CHECKS, GRIDDO_SSG_BUNDLE_ANALYZER, GRIDDO_SSG_VERBOSE_LOGS, GRIDDO_USE_DIST_BACKUP, GRIDDO_VERBOSE_LOGS, };
25
+ export { GRIDDO_AI_EMBEDDINGS, GRIDDO_API_CONCURRENCY_COUNT, GRIDDO_API_URL, GRIDDO_ASSET_PREFIX, GRIDDO_BOT_PASSWORD, GRIDDO_BOT_USER, GRIDDO_BUILD_LOGS, GRIDDO_BUILD_LOGS_BUFFER_SIZE, GRIDDO_PUBLIC_API_URL, GRIDDO_REACT_APP_INSTANCE, GRIDDO_RENDER_API_FETCH_RETRY_ATTEMPTS, GRIDDO_RENDER_API_FETCH_RETRY_WAIT_SECONDS, GRIDDO_RENDER_API_TIMEOUT_MS, GRIDDO_RENDER_CIRCUIT_BREAKER_COOLDOWN_MS, GRIDDO_RENDER_CIRCUIT_BREAKER_FAILURE_THRESHOLD, GRIDDO_RENDER_DISABLE_LLMS_TXT, GRIDDO_RENDER_ENABLED_LLM_MD, GRIDDO_RENDER_LIFECYCLE_RETRY_ATTEMPTS, GRIDDO_SEARCH_FEATURE, GRIDDO_SKIP_BUILD_CHECKS, GRIDDO_SSG_BUNDLE_ANALYZER, GRIDDO_SSG_VERBOSE_LOGS, GRIDDO_USE_DIST_BACKUP, GRIDDO_VERBOSE_LOGS, };
@@ -81,8 +81,6 @@ export interface APIRequest {
81
81
  body?: any;
82
82
  /** Reference id to manage cache between renders. */
83
83
  cacheKey?: string;
84
- /** Number of connection attempts (in case it fails on the first attempt). */
85
- attempt?: number;
86
84
  /**
87
85
  * Headers for the post api fetch
88
86
  */
package/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { execSync } from "node:child_process";
3
+ import { execFileSync } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -12,7 +12,7 @@ function getRoot(rootFlag) {
12
12
  if (rootFlag) {
13
13
  return path.resolve(rootFlag);
14
14
  }
15
- return path.resolve(__dirname, "../../");
15
+ return process.cwd();
16
16
  }
17
17
 
18
18
  function getDBPath(rootFlag) {
@@ -96,8 +96,8 @@ export const AVAILABLE_COMMANDS = [
96
96
  },
97
97
  ];
98
98
 
99
- function showHelp() {
100
- console.log("Griddo CX CLI - Available commands:\n");
99
+ export function showHelp() {
100
+ console.log("Griddo Render Engine CLI - Available commands:\n");
101
101
 
102
102
  AVAILABLE_COMMANDS.forEach((cmd) => {
103
103
  const domainArg = cmd.domainArgument ? " --domain=<domain>" : "";
@@ -114,11 +114,11 @@ function showHelp() {
114
114
  console.log(" griddo-render --help");
115
115
  }
116
116
 
117
- function findCommand(commandName) {
117
+ export function findCommand(commandName) {
118
118
  return AVAILABLE_COMMANDS.find((cmd) => cmd.name === commandName);
119
119
  }
120
120
 
121
- function validateCommand(commandName, config) {
121
+ export function validateCommand(commandName, config) {
122
122
  const command = findCommand(commandName);
123
123
 
124
124
  if (!command) {
@@ -154,7 +154,7 @@ function validateCommand(commandName, config) {
154
154
  return { command, config: validatedConfig };
155
155
  }
156
156
 
157
- function executeCommand(command, config) {
157
+ export function executeCommand(command, config) {
158
158
  try {
159
159
  const env = { ...process.env };
160
160
  const scriptPath = path.join(COMMANDS_PATH, command.script);
@@ -164,9 +164,11 @@ function executeCommand(command, config) {
164
164
  return;
165
165
  }
166
166
 
167
- const commandToRun = `node ${scriptPath}${config.domain ? ` ${config.domain}` : ""}${config.noStaticFiles ? " --dry-render" : ""}`;
167
+ const args = [scriptPath];
168
+ if (config.domain) args.push(config.domain);
169
+ if (config.noStaticFiles) args.push("--dry-render");
168
170
 
169
- execSync(commandToRun, {
171
+ execFileSync("node", args, {
170
172
  stdio: "inherit",
171
173
  env,
172
174
  cwd: process.cwd(),
@@ -245,4 +247,20 @@ function main() {
245
247
  executeCommand(command, config);
246
248
  }
247
249
 
248
- main();
250
+ // Only run when executed directly, not when imported for testing
251
+ // fs.realpathSync is needed to resolve symlinks (e.g. node_modules/.bin/ entries)
252
+ const isDirectRun = (() => {
253
+ try {
254
+ return (
255
+ !!process.argv[1] &&
256
+ fs.realpathSync(path.resolve(process.argv[1])) ===
257
+ fs.realpathSync(path.resolve(fileURLToPath(import.meta.url)))
258
+ );
259
+ } catch {
260
+ return false;
261
+ }
262
+ })();
263
+
264
+ if (isDirectRun) {
265
+ main();
266
+ }
@@ -1,4 +1,4 @@
1
- # Comandos del Orquestador Griddo CX
1
+ # Comandos del Orquestador Griddo Render Engine
2
2
 
3
3
  Este directorio contiene los comandos principales de Griddo Exporter (CX). Estos comandos forman parte del ciclo de vida completo del proceso de render de una instancia.
4
4
 
@@ -41,7 +41,7 @@ async function endRender() {
41
41
  const { renderMode, reason } = await getRenderModeFromDB(domain);
42
42
  if (renderMode === RENDER_MODE.IDLE) {
43
43
  GriddoLog.info(
44
- `(From Current Render) [${domain}]: Skipping end-render as it is marked as IDLE with the reason ${reason}.`,
44
+ `(Pre-check) ${domain} skipped end-render as it is marked as <${RENDER_MODE.IDLE}> with the reason <${reason}>`,
45
45
  );
46
46
  return;
47
47
  }
@@ -132,7 +132,7 @@ async function prepareDomains() {
132
132
 
133
133
  // Log RenderModes/Reason
134
134
  GriddoLog.info(
135
- `(From Initial Render) [${domainSlug}]: Marked as ${renderMode} with the reason: ${reason}`,
135
+ `(Pre-check) ${domainSlug} marked as <${renderMode}> with the reason <${reason}>`,
136
136
  );
137
137
 
138
138
  db.domains[domainSlug] = db.domains[domainSlug] || {};
@@ -133,9 +133,7 @@ async function uploadRenderedSearchContentToAPI(options: {
133
133
  const { htmlContentDir, jsonContentDir } = options;
134
134
 
135
135
  if (!(await pathExists(jsonContentDir)) || !(await pathExists(htmlContentDir))) {
136
- GriddoLog.info(
137
- `(From Current Render) Skipping uploading content to the search endpoint because it has not exported sites.`,
138
- );
136
+ GriddoLog.info(`(Pre-check) skipped uploading content because it has not exported sites`);
139
137
 
140
138
  return;
141
139
  }
@@ -184,7 +182,7 @@ async function uploadSearchContent() {
184
182
 
185
183
  if (renderMode === RENDER_MODE.IDLE) {
186
184
  GriddoLog.info(
187
- `(From Current Render) [${domain}]: Skipping upload-search-content as it is marked as IDLE with the reason ${reason}.`,
185
+ `(Pre-check) ${domain} skipped upload-search-content as it is marked as <${RENDER_MODE.IDLE}> with the reason <${reason}>`,
188
186
  );
189
187
  return;
190
188
  }
@@ -73,10 +73,7 @@ async function uploadRenderedSearchContentToAPI(options: {
73
73
  const { htmlContentDir, jsonContentDir } = options;
74
74
 
75
75
  if (!(await pathExists(jsonContentDir)) || !(await pathExists(htmlContentDir))) {
76
- GriddoLog.info(
77
- `Skipping uploading content to the search endpoint because it has not exported sites.`,
78
- );
79
-
76
+ GriddoLog.info(`uploading content skipped because it has not exported sites`);
80
77
  return;
81
78
  }
82
79
 
@@ -171,4 +171,4 @@ function checkEnvironmentHealth() {
171
171
  throwError(CheckHealthError);
172
172
  }
173
173
 
174
- export { checkEnvironmentHealth };
174
+ export { checkDeprecatedEnvVars, checkEnvVarValues, checkEnvironmentHealth, checkRecommendedEnvVars, checkRequiredEnvVars };
@@ -1,7 +1,5 @@
1
1
  import type { ErrorsType } from "../shared/errors";
2
2
 
3
- import path from "node:path";
4
-
5
3
  import { brush } from "../shared/brush";
6
4
  import { RENDER_MODE } from "../shared/types/render";
7
5
  import { readDB, writeDB } from "./db";
@@ -66,26 +64,28 @@ async function withErrorHandler(fn: () => Promise<void>) {
66
64
  GriddoLog.error(`An unexpected error occurred ${error}`);
67
65
  }
68
66
 
69
- // Try to rollback the exports directory if needed
70
67
  try {
71
68
  const data = await readDB();
72
- const { root } = data.paths;
73
- if (data.needsRollbackOnError) {
74
- GriddoLog.info("Cleaning exports dir...");
75
- GriddoLog.verbose(`Deleting ${path.join(root, "exports")}...`);
69
+ const domain = data.currentRenderingDomain;
76
70
 
77
- await distRollback(data.currentRenderingDomain!);
71
+ if (!domain) {
72
+ GriddoLog.warn("No currentRenderingDomain set, skipping cleanup");
78
73
  } else {
79
- GriddoLog.info("No rollback needed, skipping...");
74
+ if (data.needsRollbackOnError) {
75
+ GriddoLog.info("Cleaning exports dir...");
76
+ await distRollback(domain);
77
+ } else {
78
+ GriddoLog.info("No rollback needed, skipping...");
79
+ }
80
+
81
+ data.domains[domain].isRendering = false;
82
+ data.domains[domain].renderMode = RENDER_MODE.ERROR;
83
+ await writeDB(data);
80
84
  }
81
85
  } catch (_e) {
82
86
  GriddoLog.info("Early render stage, no db.json created yet...");
83
87
  }
84
88
 
85
- const data = await readDB();
86
- data.domains[data.currentRenderingDomain!].isRendering = false;
87
- data.domains[data.currentRenderingDomain!].renderMode = RENDER_MODE.ERROR;
88
- await writeDB(data);
89
89
  throw error;
90
90
  }
91
91
  }
@@ -39,10 +39,8 @@ async function deleteDisposableSiteDirs(baseDir: string) {
39
39
  async function mkDirs(dirs: string[], options?: MakeDirectoryOptions) {
40
40
  for (const dir of dirs) {
41
41
  try {
42
- if (!(await pathExists(dir))) {
43
- await fsp.mkdir(dir, { recursive: true, ...options });
44
- GriddoLog.verbose(`create directory: ${dir}`);
45
- }
42
+ await fsp.mkdir(dir, { recursive: true, ...options });
43
+ GriddoLog.verbose(`create directory: ${dir}`);
46
44
  } catch (error) {
47
45
  throwError(ArtifactError, error);
48
46
  }
@@ -96,10 +94,8 @@ async function cpDirs(
96
94
  // Copy directory
97
95
  try {
98
96
  // First clean destination
99
- if (await pathExists(dstCompose)) {
100
- await fsp.rm(dstCompose, { recursive: true, force: true });
101
- GriddoLog.verbose(`clean destination: ${dstCompose}`);
102
- }
97
+ await fsp.rm(dstCompose, { recursive: true, force: true });
98
+ GriddoLog.verbose(`clean destination: ${dstCompose}`);
103
99
 
104
100
  // Then copy src to dst
105
101
  await fsp.cp(srcCompose, dstCompose, {
@@ -153,7 +149,7 @@ async function mvDirs(
153
149
 
154
150
  try {
155
151
  // Clean destination
156
- if (override && (await pathExists(dstCompose))) {
152
+ if (override) {
157
153
  await fsp.rm(dstCompose, { recursive: true, force: true });
158
154
  }
159
155
 
@@ -203,10 +199,6 @@ async function restoreBackup(src: string, suffix = "-BACKUP") {
203
199
  async function deleteBackup(src: string, suffix = "-BACKUP") {
204
200
  const dst = src + suffix;
205
201
 
206
- if (!(await pathExists(dst))) {
207
- return;
208
- }
209
-
210
202
  try {
211
203
  await fsp.rm(dst, { recursive: true, force: true });
212
204
  GriddoLog.verbose(`Backup ${dst} has been deleted`);
@@ -252,6 +244,8 @@ async function siteIsEmpty(sitePath: string) {
252
244
  if (siteFiles.length === xmlFiles.length) {
253
245
  return true;
254
246
  }
247
+
248
+ return false;
255
249
  }
256
250
 
257
251
  /**
@@ -334,14 +328,18 @@ async function pathExists(dir: string) {
334
328
  */
335
329
  async function* findFilesBySuffix(dir: string, suffix: string): AsyncGenerator<string> {
336
330
  const dirHandle = await fsp.opendir(dir);
337
- for await (const item of dirHandle) {
338
- const fullPath = path.join(dir, item.name);
339
- if (item.isDirectory()) {
340
- // yield* para encadenar otro generator.
341
- yield* findFilesBySuffix(fullPath, suffix);
342
- } else if (item.isFile() && item.name.endsWith(suffix)) {
343
- yield fullPath;
331
+ try {
332
+ for await (const item of dirHandle) {
333
+ const fullPath = path.join(dir, item.name);
334
+ if (item.isDirectory()) {
335
+ // yield* para encadenar otro generator.
336
+ yield* findFilesBySuffix(fullPath, suffix);
337
+ } else if (item.isFile() && item.name.endsWith(suffix)) {
338
+ yield fullPath;
339
+ }
344
340
  }
341
+ } finally {
342
+ await dirHandle.close().catch(() => {});
345
343
  }
346
344
  }
347
345
 
@@ -354,20 +352,26 @@ async function* findFilesBySuffix(dir: string, suffix: string): AsyncGenerator<s
354
352
  */
355
353
  async function* walkStore(storeDir: string): AsyncGenerator<string> {
356
354
  const storeDirHandle = await fsp.opendir(storeDir);
357
-
358
- for await (const siteDirent of storeDirHandle) {
359
- if (siteDirent.isDirectory()) {
360
- const siteDirPath = path.join(storeDir, siteDirent.name);
361
- const siteDirHandle = await fsp.opendir(siteDirPath);
362
-
363
- for await (const fileDirent of siteDirHandle) {
364
- const filePath = path.join(siteDirPath, fileDirent.name);
365
-
366
- if (fileDirent.isFile() && path.extname(filePath) === ".json") {
367
- yield filePath;
355
+ try {
356
+ for await (const siteDirent of storeDirHandle) {
357
+ if (siteDirent.isDirectory()) {
358
+ const siteDirPath = path.join(storeDir, siteDirent.name);
359
+ const siteDirHandle = await fsp.opendir(siteDirPath);
360
+ try {
361
+ for await (const fileDirent of siteDirHandle) {
362
+ const filePath = path.join(siteDirPath, fileDirent.name);
363
+
364
+ if (fileDirent.isFile() && path.extname(filePath) === ".json") {
365
+ yield filePath;
366
+ }
367
+ }
368
+ } finally {
369
+ await siteDirHandle.close().catch(() => {});
368
370
  }
369
371
  }
370
372
  }
373
+ } finally {
374
+ await storeDirHandle.close().catch(() => {});
371
375
  }
372
376
  }
373
377
 
@@ -0,0 +1,58 @@
1
+ import type { HttpAdapter } from "./types";
2
+
3
+ import {
4
+ GRIDDO_RENDER_API_FETCH_RETRY_ATTEMPTS,
5
+ GRIDDO_RENDER_API_FETCH_RETRY_WAIT_SECONDS,
6
+ GRIDDO_RENDER_API_TIMEOUT_MS,
7
+ GRIDDO_RENDER_CIRCUIT_BREAKER_COOLDOWN_MS,
8
+ GRIDDO_RENDER_CIRCUIT_BREAKER_FAILURE_THRESHOLD,
9
+ } from "../../shared/envs";
10
+ import { GriddoLog } from "../GriddoLog";
11
+ import { createUndiciAdapter } from "./undici-adapter";
12
+ import { withCircuitBreaker } from "./with-circuit-breaker";
13
+ import { withRetry } from "./with-retry";
14
+
15
+ let adapter: HttpAdapter | undefined;
16
+
17
+ /**
18
+ * Sync getter del adapter HTTP.
19
+ *
20
+ * Composición:
21
+ * circuitBreaker → retry(exponential + jitter) → undici(timeout nativo)
22
+ */
23
+ function getHttpAdapter(): HttpAdapter {
24
+ if (!adapter) {
25
+ adapter = withCircuitBreaker(
26
+ withRetry(createUndiciAdapter({ timeoutMs: GRIDDO_RENDER_API_TIMEOUT_MS }), {
27
+ attempts: GRIDDO_RENDER_API_FETCH_RETRY_ATTEMPTS,
28
+ delayMs: GRIDDO_RENDER_API_FETCH_RETRY_WAIT_SECONDS * 1000,
29
+ backoff: "exponential",
30
+ jitter: true,
31
+ retryOn: (res) => res.status >= 500,
32
+ onRetry: ({ request: req, attempt, delayMs, error, response }) => {
33
+ const cause = response ? `HTTP ${response.status}` : error?.message;
34
+ GriddoLog.warn(
35
+ `Retry ${attempt}/${GRIDDO_RENDER_API_FETCH_RETRY_ATTEMPTS}: ${req.method} ${req.url} — ${cause} (next in ${delayMs}ms)`,
36
+ );
37
+ },
38
+ }),
39
+ {
40
+ failureThreshold: GRIDDO_RENDER_CIRCUIT_BREAKER_FAILURE_THRESHOLD,
41
+ cooldownMs: GRIDDO_RENDER_CIRCUIT_BREAKER_COOLDOWN_MS,
42
+ onOpen: () => GriddoLog.warn("Circuit breaker OPEN — requests will fail fast"),
43
+ onClose: () => GriddoLog.info("Circuit breaker CLOSED — recovered"),
44
+ },
45
+ );
46
+ GriddoLog.verbose("HTTP adapter: undici (timeout + retry + circuit breaker)");
47
+ }
48
+ return adapter;
49
+ }
50
+
51
+ /**
52
+ * Reset del singleton (para tests).
53
+ */
54
+ function resetHttpAdapter(): void {
55
+ adapter = undefined;
56
+ }
57
+
58
+ export { getHttpAdapter, resetHttpAdapter };
@@ -0,0 +1,7 @@
1
+ export type { HttpAdapter, HttpRequest, HttpResponse } from "./types";
2
+ export type { CircuitBreakerOptions } from "./with-circuit-breaker";
3
+ export type { RetryOptions } from "./with-retry";
4
+
5
+ export { getHttpAdapter, resetHttpAdapter } from "./create-adapter";
6
+ export { CircuitOpenError, withCircuitBreaker } from "./with-circuit-breaker";
7
+ export { withRetry } from "./with-retry";
@@ -0,0 +1,22 @@
1
+ /** Descriptor de request que recibe el adapter. */
2
+ export interface HttpRequest {
3
+ url: string;
4
+ method: string;
5
+ headers: Record<string, string>;
6
+ body?: string;
7
+ }
8
+
9
+ /** Descriptor de response que devuelve el adapter. */
10
+ export interface HttpResponse {
11
+ status: number;
12
+ statusText: string;
13
+ ok: boolean;
14
+ headers: Record<string, string>;
15
+ json: <T = unknown>() => Promise<T>;
16
+ text: () => Promise<string>;
17
+ }
18
+
19
+ /** Contrato del adapter. Un solo metodo, sin estado. */
20
+ export interface HttpAdapter {
21
+ request: (req: HttpRequest) => Promise<HttpResponse>;
22
+ }
@@ -0,0 +1,53 @@
1
+ import type { HttpAdapter, HttpRequest, HttpResponse } from "./types";
2
+
3
+ import { request } from "undici";
4
+
5
+ export interface UndiciAdapterOptions {
6
+ /** Request timeout in milliseconds. Cancels the request if exceeded. */
7
+ timeoutMs?: number;
8
+ }
9
+
10
+ function createUndiciAdapter(options: UndiciAdapterOptions = {}): HttpAdapter {
11
+ const { timeoutMs } = options;
12
+
13
+ return {
14
+ async request(req: HttpRequest): Promise<HttpResponse> {
15
+ const {
16
+ statusCode,
17
+ headers: rawHeaders,
18
+ body,
19
+ } = await request(req.url, {
20
+ method: req.method as "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
21
+ headers: req.headers,
22
+ body: req.body,
23
+ signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,
24
+ });
25
+
26
+ const headers: Record<string, string> = {};
27
+ for (const [key, value] of Object.entries(rawHeaders)) {
28
+ if (value !== undefined) {
29
+ headers[key] = Array.isArray(value) ? value.join(", ") : String(value);
30
+ }
31
+ }
32
+
33
+ let buffered: string | undefined;
34
+ const getText = async () => {
35
+ if (buffered === undefined) {
36
+ buffered = await body.text();
37
+ }
38
+ return buffered;
39
+ };
40
+
41
+ return {
42
+ status: statusCode,
43
+ statusText: "",
44
+ ok: statusCode >= 200 && statusCode < 300,
45
+ headers,
46
+ json: async <T>() => JSON.parse(await getText()) as T,
47
+ text: getText,
48
+ };
49
+ },
50
+ };
51
+ }
52
+
53
+ export { createUndiciAdapter };