@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 +12 -0
- package/package.json +3 -3
- package/src/index.js +31 -23
- package/src/utils/custom-html-utils.js +18 -14
- package/test/index.test.js +29 -0
- package/test/utils/html-utils.test.js +75 -25
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.
|
|
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.
|
|
39
|
-
"@aws-sdk/client-s3": "3.
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
1079
|
+
true,
|
|
1060
1080
|
options,
|
|
1061
1081
|
),
|
|
1062
|
-
this.invalidateCdnCache({
|
|
1063
|
-
urls: [previewUrl],
|
|
1064
|
-
isPreview: true,
|
|
1065
|
-
}),
|
|
1066
1082
|
]);
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
|
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
|
-
//
|
|
86
|
+
// No cache header AND no proxy header
|
|
83
87
|
if (!proxyHeader) {
|
|
84
|
-
|
|
85
|
-
|
|
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:
|
|
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 =
|
|
161
|
-
retryDelayMs =
|
|
164
|
+
maxRetries = 8,
|
|
165
|
+
retryDelayMs = 1500,
|
|
162
166
|
} = options;
|
|
163
167
|
|
|
164
|
-
const fetchType = isOptimized ?
|
|
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);
|
package/test/index.test.js
CHANGED
|
@@ -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); //
|
|
225
|
+
expect(elapsed).to.be.at.least(750); // Default warmup delay for optimized is 750ms
|
|
225
226
|
});
|
|
226
227
|
|
|
227
|
-
it('should
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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({
|