@adobe/spacecat-shared-tokowaka-client 1.12.1 → 1.12.3

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,3 +1,15 @@
1
+ ## [@adobe/spacecat-shared-tokowaka-client-v1.12.3](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.12.2...@adobe/spacecat-shared-tokowaka-client-v1.12.3) (2026-03-28)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **deps:** update external fixes ([#1477](https://github.com/adobe/spacecat-shared/issues/1477)) ([67bdd1a](https://github.com/adobe/spacecat-shared/commit/67bdd1a2c497bed088bc1e54ae22e60c171308d1))
6
+
7
+ ## [@adobe/spacecat-shared-tokowaka-client-v1.12.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.12.1...@adobe/spacecat-shared-tokowaka-client-v1.12.2) (2026-03-25)
8
+
9
+ ### Bug Fixes
10
+
11
+ * optimize preview api ([#1412](https://github.com/adobe/spacecat-shared/issues/1412)) ([3efb9de](https://github.com/adobe/spacecat-shared/commit/3efb9deb1ee6ad962b5288ef124f96fbfbb6e4ae))
12
+
1
13
  ## [@adobe/spacecat-shared-tokowaka-client-v1.12.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.12.0...@adobe/spacecat-shared-tokowaka-client-v1.12.1) (2026-03-21)
2
14
 
3
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.12.1",
3
+ "version": "1.12.3",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
@@ -35,8 +35,8 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@adobe/spacecat-shared-utils": "1.81.1",
38
- "@aws-sdk/client-cloudfront": "3.1014.0",
39
- "@aws-sdk/client-s3": "3.1014.0",
38
+ "@aws-sdk/client-cloudfront": "3.1019.0",
39
+ "@aws-sdk/client-s3": "3.1019.0",
40
40
  "hast-util-from-html": "2.0.3",
41
41
  "mdast-util-from-markdown": "2.0.3",
42
42
  "mdast-util-to-hast": "13.2.1",
package/src/index.js CHANGED
@@ -991,6 +991,19 @@ class TokowakaClient {
991
991
 
992
992
  const forwardedHost = calculateForwardedHost(previewUrl, this.log);
993
993
 
994
+ const originalHtmlPromise = fetchHtmlWithWarmup(
995
+ previewUrl,
996
+ apiKey,
997
+ forwardedHost,
998
+ edgeUrl,
999
+ this.log,
1000
+ false,
1001
+ options,
1002
+ ).catch((err) => {
1003
+ this.log.error(`Failed to fetch original HTML: ${err.message}`);
1004
+ return null;
1005
+ });
1006
+
994
1007
  // Generate configuration with eligible preview suggestions
995
1008
  this.log.info(`Generating preview Tokowaka config for opportunity ${opportunity.getId()}`);
996
1009
  const newConfig = this.generateConfig(previewUrl, opportunity, eligibleSuggestions);
@@ -1047,38 +1060,33 @@ class TokowakaClient {
1047
1060
  let cdnInvalidationResults = null;
1048
1061
 
1049
1062
  try {
1050
- const fetchAndInvalidateStartTime = Date.now();
1051
- // Fetch original HTML and invalidate CDN cache in parallel to save time
1052
- [originalHtml, cdnInvalidationResults] = await Promise.all([
1063
+ // Invalidate CDN so edge serves fresh content for optimized fetch
1064
+ cdnInvalidationResults = await this.invalidateCdnCache({
1065
+ urls: [previewUrl],
1066
+ isPreview: true,
1067
+ });
1068
+
1069
+ // Await original HTML (started earlier) and fetch optimized HTML in parallel
1070
+ const fetchStartTime = Date.now();
1071
+ [originalHtml, optimizedHtml] = await Promise.all([
1072
+ originalHtmlPromise,
1053
1073
  fetchHtmlWithWarmup(
1054
1074
  previewUrl,
1055
1075
  apiKey,
1056
1076
  forwardedHost,
1057
1077
  edgeUrl,
1058
1078
  this.log,
1059
- false,
1079
+ true,
1060
1080
  options,
1061
1081
  ),
1062
- this.invalidateCdnCache({
1063
- urls: [previewUrl],
1064
- isPreview: true,
1065
- }),
1066
1082
  ]);
1067
- this.log.info('Successfully fetched original HTML and invalidated CDN cache'
1068
- + ` in ${Date.now() - fetchAndInvalidateStartTime}ms`);
1069
-
1070
- // Step 2: Fetch optimized HTML after CDN invalidation and original fetch complete
1071
- this.log.info('Fetching optimized HTML...');
1072
- optimizedHtml = await fetchHtmlWithWarmup(
1073
- previewUrl,
1074
- apiKey,
1075
- forwardedHost,
1076
- edgeUrl,
1077
- this.log,
1078
- true,
1079
- options,
1080
- );
1081
- this.log.info('Successfully fetched optimized HTML');
1083
+ /* c8 ignore start */
1084
+ if (!originalHtml || !optimizedHtml) {
1085
+ throw this.#createError('Failed to fetch original or optimized HTML', HTTP_INTERNAL_SERVER_ERROR);
1086
+ }
1087
+ /* c8 ignore stop */
1088
+ this.log.info('Successfully fetched original and optimized HTML'
1089
+ + ` in ${Date.now() - fetchStartTime}ms`);
1082
1090
  } catch (error) {
1083
1091
  this.log.error(`Failed to fetch HTML for preview: ${error.message}`);
1084
1092
  throw this.#createError(
@@ -12,6 +12,9 @@
12
12
 
13
13
  import { hasText } from '@adobe/spacecat-shared-utils';
14
14
 
15
+ const ORIGINAL_FETCH_TYPE = 'original';
16
+ const OPTIMIZED_FETCH_TYPE = 'optimized';
17
+
15
18
  /**
16
19
  * Helper function to wait for a specified duration
17
20
  * @param {number} ms - Milliseconds to wait
@@ -39,10 +42,11 @@ async function fetchWithTimeout(url, options = {}, timeout = 3000) {
39
42
 
40
43
  /**
41
44
  * Makes an HTTP request with retry logic for both original and optimized HTML.
42
- * Header validation logic (same for both):
43
- * - No proxy AND no cache header: Return response immediately (success)
44
- * - Proxy header present BUT no cache header: Retry until cache header found
45
+ * Header validation logic:
45
46
  * - Cache header present (regardless of proxy): Return response (success)
47
+ * - No proxy AND no cache header: If fetching optimized, retry; else success
48
+ * - Proxy header present BUT no cache header: Retry until cache header found
49
+ * - 301: Retry with the same URL (same as other non-ok responses)
46
50
  * @param {string} url - URL to fetch
47
51
  * @param {Object} options - Fetch options
48
52
  * @param {number} maxRetries - Maximum number of retries
@@ -63,7 +67,7 @@ async function fetchWithRetry(url, options, maxRetries, retryDelayMs, log, fetch
63
67
  const requestId = response?.headers?.get('x-edgeoptimize-request-id');
64
68
  log.info(`[${fetchType}] Response status (attempt ${attempt}): ${response.status} ${response.statusText}, x-edgeoptimize-request-id: ${requestId || 'none'}`);
65
69
 
66
- if (!response.ok) {
70
+ if (!response.ok && response.status !== 301) {
67
71
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
68
72
  }
69
73
 
@@ -79,15 +83,15 @@ async function fetchWithRetry(url, options, maxRetries, retryDelayMs, log, fetch
79
83
  return response;
80
84
  }
81
85
 
82
- // Case 2: No cache header AND no proxy header -> Success (return immediately)
86
+ // No cache header AND no proxy header
83
87
  if (!proxyHeader) {
84
- log.info(`[${fetchType}] No edge optimize headers found, proceeding as successful flow`);
85
- return response;
88
+ if (fetchType === ORIGINAL_FETCH_TYPE && response.status !== 301) {
89
+ log.info(`[${fetchType}] No edge optimize headers found, proceeding as successful flow`);
90
+ return response;
91
+ }
92
+ log.info(`[${fetchType}] No proxy/cache header found`);
86
93
  }
87
94
 
88
- // Case 3: Proxy header present BUT no cache header -> Retry until cache found
89
- log.info(`[${fetchType}] Proxy header present without cache header, will retry...`);
90
-
91
95
  // If we haven't exhausted retries, continue
92
96
  if (attempt < maxRetries + 1) {
93
97
  log.debug(`[${fetchType}] Waiting ${retryDelayMs}ms before retry...`);
@@ -127,7 +131,7 @@ async function fetchWithRetry(url, options, maxRetries, retryDelayMs, log, fetch
127
131
  * @param {Object} log - Logger instance
128
132
  * @param {Object} options - Additional options
129
133
  * @param {number} options.warmupDelayMs - Delay after warmup call (default: 1000ms)
130
- * @param {number} options.maxRetries - Maximum number of retries for actual call (default: 3)
134
+ * @param {number} options.maxRetries - Maximum number of retries for actual call (default: 5)
131
135
  * @param {number} options.retryDelayMs - Delay between retries (default: 1000ms)
132
136
  * @returns {Promise<string>} - HTML content
133
137
  * @throws {Error} - If validation fails or fetch fails after retries or timeout
@@ -157,11 +161,11 @@ export async function fetchHtmlWithWarmup(
157
161
  // Default options
158
162
  const {
159
163
  warmupDelayMs = isOptimized ? 750 : 0, // milliseconds
160
- maxRetries = 3,
161
- retryDelayMs = 1000,
164
+ maxRetries = 8,
165
+ retryDelayMs = 1500,
162
166
  } = options;
163
167
 
164
- const fetchType = isOptimized ? 'optimized' : 'original';
168
+ const fetchType = isOptimized ? OPTIMIZED_FETCH_TYPE : ORIGINAL_FETCH_TYPE;
165
169
 
166
170
  // Parse the URL to extract path and construct full URL
167
171
  const urlObj = new URL(url);
@@ -3292,6 +3292,35 @@ describe('TokowakaClient', () => {
3292
3292
  }
3293
3293
  });
3294
3294
 
3295
+ it('should throw when original HTML fetch returns null (e.g. early fetch failed and resolved null)', async () => {
3296
+ const okResponse = {
3297
+ ok: true,
3298
+ status: 200,
3299
+ statusText: 'OK',
3300
+ headers: { get: (name) => (name === 'x-edgeoptimize-cache' ? 'HIT' : null) },
3301
+ text: async () => '<html><body>Test HTML</body></html>',
3302
+ };
3303
+ fetchStub.resetBehavior();
3304
+ fetchStub.onCall(0).resolves(okResponse);
3305
+ fetchStub.onCall(1).resolves(okResponse);
3306
+ fetchStub.onCall(2).resolves(okResponse);
3307
+ fetchStub.onCall(3).rejects(new Error('Original actual fetch failed'));
3308
+
3309
+ try {
3310
+ await client.previewSuggestions(
3311
+ mockSite,
3312
+ mockOpportunity,
3313
+ mockSuggestions,
3314
+ { warmupDelayMs: 0, maxRetries: 0, retryDelayMs: 0 },
3315
+ );
3316
+ expect.fail('Should have thrown error');
3317
+ } catch (error) {
3318
+ expect(error.message).to.satisfy((msg) => msg.includes('Failed to fetch original or optimized HTML')
3319
+ || msg.includes('Preview failed'));
3320
+ expect(error.status).to.equal(500);
3321
+ }
3322
+ });
3323
+
3295
3324
  it('should merge with existing deployed patches for the same URL', async () => {
3296
3325
  // Setup existing config with deployed patches
3297
3326
  const existingConfig = {
@@ -197,6 +197,7 @@ describe('HTML Utils', () => {
197
197
  });
198
198
 
199
199
  it('should use default warmup delay of 750ms for optimized HTML', async () => {
200
+ fetchStub.reset();
200
201
  fetchStub.resolves({
201
202
  ok: true,
202
203
  status: 200,
@@ -221,10 +222,10 @@ describe('HTML Utils', () => {
221
222
  const elapsed = Date.now() - startTime;
222
223
  expect(html).to.equal('<html>Optimized HTML</html>');
223
224
  expect(fetchStub.callCount).to.equal(2); // warmup + actual
224
- expect(elapsed).to.be.at.least(750); // Should not have waited (0ms warmup)
225
+ expect(elapsed).to.be.at.least(750); // Default warmup delay for optimized is 750ms
225
226
  });
226
227
 
227
- it('should return immediately for optimized HTML when no headers present', async () => {
228
+ it('should retry and throw for optimized HTML when no proxy or cache headers present', async () => {
228
229
  // Warmup succeeds
229
230
  fetchStub.onCall(0).resolves({
230
231
  ok: true,
@@ -235,30 +236,35 @@ describe('HTML Utils', () => {
235
236
  },
236
237
  text: async () => 'warmup',
237
238
  });
238
- // First actual call - no headers, should succeed
239
- fetchStub.onCall(1).resolves({
240
- ok: true,
241
- status: 200,
242
- statusText: 'OK',
243
- headers: {
244
- get: () => null,
245
- },
246
- text: async () => '<html>No headers</html>',
247
- });
248
-
249
- const html = await fetchHtmlWithWarmup(
250
- 'https://example.com/page',
251
- 'api-key',
252
- 'host',
253
- 'https://edge.example.com',
254
- log,
255
- true, // isOptimized
256
- { warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
257
- );
239
+ for (let i = 1; i <= 4; i += 1) {
240
+ fetchStub.onCall(i).resolves({
241
+ ok: true,
242
+ status: 200,
243
+ statusText: 'OK',
244
+ headers: {
245
+ get: () => null,
246
+ },
247
+ text: async () => '<html>No headers</html>',
248
+ });
249
+ }
258
250
 
259
- expect(html).to.equal('<html>No headers</html>');
260
- // Should succeed immediately (warmup + 1 attempt)
261
- expect(fetchStub.callCount).to.equal(2);
251
+ try {
252
+ await fetchHtmlWithWarmup(
253
+ 'https://example.com/page',
254
+ 'api-key',
255
+ 'host',
256
+ 'https://edge.example.com',
257
+ log,
258
+ true, // isOptimized
259
+ { warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
260
+ );
261
+ expect.fail('Should have thrown error');
262
+ } catch (error) {
263
+ expect(error.message).to.include('Failed to fetch optimized HTML');
264
+ expect(error.message).to.include('Cache header (x-edgeoptimize-cache) not found after 3 retries');
265
+ }
266
+ // Warmup + 4 actual attempts (initial + 3 retries)
267
+ expect(fetchStub.callCount).to.equal(5);
262
268
  });
263
269
 
264
270
  it('should throw error for optimized HTML when proxy present but cache not found after retries', async () => {
@@ -406,6 +412,50 @@ describe('HTML Utils', () => {
406
412
  }
407
413
  });
408
414
 
415
+ it('should retry with same URL on 301 response', async () => {
416
+ const sameUrl = 'https://edge.example.com/page';
417
+ // Warmup succeeds
418
+ fetchStub.onCall(0).resolves({
419
+ ok: true,
420
+ status: 200,
421
+ statusText: 'OK',
422
+ headers: { get: () => null },
423
+ text: async () => 'warmup',
424
+ });
425
+ // First actual call returns 301
426
+ fetchStub.onCall(1).resolves({
427
+ ok: false,
428
+ status: 301,
429
+ statusText: 'Moved Permanently',
430
+ headers: { get: () => null },
431
+ });
432
+ // Second call (same URL) succeeds with cache header
433
+ fetchStub.onCall(2).resolves({
434
+ ok: true,
435
+ status: 200,
436
+ statusText: 'OK',
437
+ headers: {
438
+ get: (name) => (name === 'x-edgeoptimize-cache' ? 'HIT' : null),
439
+ },
440
+ text: async () => '<html>OK</html>',
441
+ });
442
+
443
+ const html = await fetchHtmlWithWarmup(
444
+ 'https://example.com/page',
445
+ 'api-key',
446
+ 'host',
447
+ 'https://edge.example.com',
448
+ log,
449
+ false,
450
+ { warmupDelayMs: 0, maxRetries: 2, retryDelayMs: 0 },
451
+ );
452
+
453
+ expect(html).to.equal('<html>OK</html>');
454
+ expect(fetchStub.callCount).to.equal(3); // warmup + 301 + retry same URL
455
+ expect(fetchStub.getCall(1).args[0]).to.equal(sameUrl);
456
+ expect(fetchStub.getCall(2).args[0]).to.equal(sameUrl);
457
+ });
458
+
409
459
  it('should retry and eventually throw error after max retries', async () => {
410
460
  // Warmup succeeds
411
461
  fetchStub.onCall(0).resolves({