@adobe/spacecat-shared-tokowaka-client 1.7.6 → 1.7.8

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,17 @@
1
+ # [@adobe/spacecat-shared-tokowaka-client-v1.7.8](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.7.7...@adobe/spacecat-shared-tokowaka-client-v1.7.8) (2026-02-16)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * timeout added for edge optimize status ([#1329](https://github.com/adobe/spacecat-shared/issues/1329)) ([93ef065](https://github.com/adobe/spacecat-shared/commit/93ef0653840929e9c76b14d370dbfa6db03cb97e))
7
+
8
+ # [@adobe/spacecat-shared-tokowaka-client-v1.7.7](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.7.6...@adobe/spacecat-shared-tokowaka-client-v1.7.7) (2026-02-12)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * export forwarded host compute method ([#1338](https://github.com/adobe/spacecat-shared/issues/1338)) ([91e5f5f](https://github.com/adobe/spacecat-shared/commit/91e5f5fb2b5a7d592886caea940a6313fe1c1c8c))
14
+
1
15
  # [@adobe/spacecat-shared-tokowaka-client-v1.7.6](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.7.5...@adobe/spacecat-shared-tokowaka-client-v1.7.6) (2026-02-09)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.7.6",
3
+ "version": "1.7.8",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
@@ -46,6 +46,7 @@
46
46
  "c8": "^10.1.3",
47
47
  "chai": "^6.0.1",
48
48
  "eslint": "^9.36.0",
49
+ "esmock": "^2.7.3",
49
50
  "mocha": "^11.7.2",
50
51
  "nock": "^14.0.10",
51
52
  "sinon": "^21.0.0",
package/src/index.d.ts CHANGED
@@ -147,6 +147,14 @@ export class FastlyKVClient {
147
147
  }): Promise<FastlyKVEntry[]>;
148
148
  }
149
149
 
150
+ /**
151
+ * Compute the forwarded host for edge optimize (bare domain → www; subdomains unchanged).
152
+ * @param url - Full base URL (e.g. https://example.com)
153
+ * @param logger - Optional logger with debug and error methods
154
+ * @returns Host to use (e.g. www.example.com)
155
+ */
156
+ export function calculateForwardedHost(url: string, logger?: { debug?: (msg: string) => void; error?: (msg: string) => void }): string;
157
+
150
158
  /**
151
159
  * Base class for opportunity mappers
152
160
  * Extend this class to create custom mappers for new opportunity types
package/src/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  import crypto from 'crypto';
14
14
  import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
15
- import { hasText, isNonEmptyObject } from '@adobe/spacecat-shared-utils';
15
+ import { hasText, isNonEmptyObject, tracingFetch } from '@adobe/spacecat-shared-utils';
16
16
  import { v4 as uuidv4 } from 'uuid';
17
17
  import MapperRegistry from './mappers/mapper-registry.js';
18
18
  import CdnClientRegistry from './cdn/cdn-client-registry.js';
@@ -28,6 +28,7 @@ import { getEffectiveBaseURL } from './utils/site-utils.js';
28
28
  import { fetchHtmlWithWarmup, calculateForwardedHost } from './utils/custom-html-utils.js';
29
29
 
30
30
  export { FastlyKVClient } from './fastly-kv-client.js';
31
+ export { calculateForwardedHost } from './utils/custom-html-utils.js';
31
32
 
32
33
  const HTTP_BAD_REQUEST = 400;
33
34
  const HTTP_INTERNAL_SERVER_ERROR = 500;
@@ -1071,17 +1072,20 @@ class TokowakaClient {
1071
1072
  const maxRetries = 3;
1072
1073
  let attempt = 0;
1073
1074
 
1075
+ const REQUEST_TIMEOUT_MS = 5000;
1076
+
1074
1077
  while (attempt <= maxRetries) {
1075
1078
  try {
1076
1079
  this.log.debug(`Attempt ${attempt + 1}/${maxRetries + 1}: Checking edge optimize status for ${targetUrl}`);
1077
1080
 
1078
1081
  // eslint-disable-next-line no-await-in-loop
1079
- const response = await fetch(targetUrl, {
1082
+ const response = await tracingFetch(targetUrl, {
1080
1083
  method: 'GET',
1081
1084
  headers: {
1082
1085
  'User-Agent': 'Tokowaka-AI Tokowaka/1.0 AdobeEdgeOptimize-AI AdobeEdgeOptimize/1.0',
1083
1086
  'fastly-debug': '1',
1084
1087
  },
1088
+ timeout: REQUEST_TIMEOUT_MS,
1085
1089
  });
1086
1090
 
1087
1091
  this.log.debug(`Response status: ${response.status}`);
@@ -1095,6 +1099,13 @@ class TokowakaClient {
1095
1099
  edgeOptimizeEnabled,
1096
1100
  };
1097
1101
  } catch (error) {
1102
+ const isTimeout = error?.code === 'ETIMEOUT';
1103
+
1104
+ if (isTimeout) {
1105
+ this.log.warn(`Request timed out after ${REQUEST_TIMEOUT_MS}ms for ${targetUrl}, returning edgeOptimizeEnabled: false`);
1106
+ return { edgeOptimizeEnabled: false };
1107
+ }
1108
+
1098
1109
  attempt += 1;
1099
1110
 
1100
1111
  if (attempt > maxRetries) {
@@ -15,6 +15,7 @@
15
15
  import { expect, use } from 'chai';
16
16
  import sinon from 'sinon';
17
17
  import sinonChai from 'sinon-chai';
18
+ import esmock from 'esmock';
18
19
  import TokowakaClient from '../src/index.js';
19
20
 
20
21
  use(sinonChai);
@@ -3610,20 +3611,45 @@ describe('TokowakaClient', () => {
3610
3611
  });
3611
3612
 
3612
3613
  describe('checkEdgeOptimizeStatus', () => {
3613
- let fetchStub;
3614
+ let tracingFetchStub;
3615
+ let esmockClient;
3614
3616
 
3615
- beforeEach(() => {
3616
- fetchStub = sinon.stub(global, 'fetch');
3617
- });
3617
+ beforeEach(async () => {
3618
+ tracingFetchStub = sinon.stub();
3618
3619
 
3619
- afterEach(() => {
3620
- fetchStub.restore();
3620
+ const MockedTokowakaClient = await esmock('../src/index.js', {
3621
+ '@adobe/spacecat-shared-utils': {
3622
+ hasText: (val) => typeof val === 'string' && val.trim().length > 0,
3623
+ isNonEmptyObject: (val) => val !== null && typeof val === 'object' && Object.keys(val).length > 0,
3624
+ tracingFetch: tracingFetchStub,
3625
+ },
3626
+ });
3627
+
3628
+ const env = {
3629
+ TOKOWAKA_CDN_PROVIDER: 'cloudfront',
3630
+ TOKOWAKA_CDN_CONFIG: JSON.stringify({
3631
+ cloudfront: {
3632
+ distributionId: 'E123456',
3633
+ region: 'us-east-1',
3634
+ },
3635
+ }),
3636
+ };
3637
+
3638
+ esmockClient = new MockedTokowakaClient(
3639
+ {
3640
+ bucketName: 'test-bucket',
3641
+ previewBucketName: 'test-preview-bucket',
3642
+ s3Client: { send: sinon.stub().resolves() },
3643
+ env,
3644
+ },
3645
+ log,
3646
+ );
3621
3647
  });
3622
3648
 
3623
3649
  describe('Input Validation', () => {
3624
3650
  it('should throw error when site is not provided', async () => {
3625
3651
  try {
3626
- await client.checkEdgeOptimizeStatus(null, '/');
3652
+ await esmockClient.checkEdgeOptimizeStatus(null, '/');
3627
3653
  expect.fail('Should have thrown error');
3628
3654
  } catch (error) {
3629
3655
  expect(error.message).to.include('Site is required');
@@ -3633,7 +3659,7 @@ describe('TokowakaClient', () => {
3633
3659
 
3634
3660
  it('should throw error when site is empty object', async () => {
3635
3661
  try {
3636
- await client.checkEdgeOptimizeStatus({}, '/');
3662
+ await esmockClient.checkEdgeOptimizeStatus({}, '/');
3637
3663
  expect.fail('Should have thrown error');
3638
3664
  } catch (error) {
3639
3665
  expect(error.message).to.include('Site is required');
@@ -3649,7 +3675,7 @@ describe('TokowakaClient', () => {
3649
3675
  };
3650
3676
 
3651
3677
  try {
3652
- await client.checkEdgeOptimizeStatus(site, '');
3678
+ await esmockClient.checkEdgeOptimizeStatus(site, '');
3653
3679
  expect.fail('Should have thrown error');
3654
3680
  } catch (error) {
3655
3681
  expect(error.message).to.include('Path is required');
@@ -3666,7 +3692,7 @@ describe('TokowakaClient', () => {
3666
3692
  };
3667
3693
 
3668
3694
  try {
3669
- await client.checkEdgeOptimizeStatus(site, null);
3695
+ await esmockClient.checkEdgeOptimizeStatus(site, null);
3670
3696
  expect.fail('Should have thrown error');
3671
3697
  } catch (error) {
3672
3698
  expect(error.message).to.include('Path is required');
@@ -3697,15 +3723,15 @@ describe('TokowakaClient', () => {
3697
3723
  },
3698
3724
  };
3699
3725
 
3700
- fetchStub.resolves(mockResponse);
3726
+ tracingFetchStub.resolves(mockResponse);
3701
3727
 
3702
- const result = await client.checkEdgeOptimizeStatus(site, '/');
3728
+ const result = await esmockClient.checkEdgeOptimizeStatus(site, '/');
3703
3729
 
3704
3730
  expect(result).to.deep.equal({
3705
3731
  edgeOptimizeEnabled: true,
3706
3732
  });
3707
- expect(fetchStub).to.have.been.calledOnce;
3708
- expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/');
3733
+ expect(tracingFetchStub).to.have.been.calledOnce;
3734
+ expect(tracingFetchStub.firstCall.args[0]).to.equal('https://example.com/');
3709
3735
  });
3710
3736
 
3711
3737
  it('should return edgeOptimizeEnabled: true when x-edgeoptimize-request-id header is present', async () => {
@@ -3719,14 +3745,14 @@ describe('TokowakaClient', () => {
3719
3745
  },
3720
3746
  };
3721
3747
 
3722
- fetchStub.resolves(mockResponse);
3748
+ tracingFetchStub.resolves(mockResponse);
3723
3749
 
3724
- const result = await client.checkEdgeOptimizeStatus(site, '/products');
3750
+ const result = await esmockClient.checkEdgeOptimizeStatus(site, '/products');
3725
3751
 
3726
3752
  expect(result).to.deep.equal({
3727
3753
  edgeOptimizeEnabled: true,
3728
3754
  });
3729
- expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/products');
3755
+ expect(tracingFetchStub.firstCall.args[0]).to.equal('https://example.com/products');
3730
3756
  });
3731
3757
 
3732
3758
  it('should return edgeOptimizeEnabled: false when headers are not present', async () => {
@@ -3736,9 +3762,9 @@ describe('TokowakaClient', () => {
3736
3762
  };
3737
3763
  mockResponse.headers.get = () => null;
3738
3764
 
3739
- fetchStub.resolves(mockResponse);
3765
+ tracingFetchStub.resolves(mockResponse);
3740
3766
 
3741
- const result = await client.checkEdgeOptimizeStatus(site, '/');
3767
+ const result = await esmockClient.checkEdgeOptimizeStatus(site, '/');
3742
3768
 
3743
3769
  expect(result).to.deep.equal({
3744
3770
  edgeOptimizeEnabled: false,
@@ -3756,9 +3782,9 @@ describe('TokowakaClient', () => {
3756
3782
  },
3757
3783
  };
3758
3784
 
3759
- fetchStub.resolves(mockResponse);
3785
+ tracingFetchStub.resolves(mockResponse);
3760
3786
 
3761
- const result = await client.checkEdgeOptimizeStatus(site, '/not-found');
3787
+ const result = await esmockClient.checkEdgeOptimizeStatus(site, '/not-found');
3762
3788
 
3763
3789
  expect(result).to.deep.equal({
3764
3790
  edgeOptimizeEnabled: true,
@@ -3772,13 +3798,28 @@ describe('TokowakaClient', () => {
3772
3798
  };
3773
3799
  mockResponse.headers.get = () => null;
3774
3800
 
3775
- fetchStub.resolves(mockResponse);
3801
+ tracingFetchStub.resolves(mockResponse);
3776
3802
 
3777
- await client.checkEdgeOptimizeStatus(site, '/');
3803
+ await esmockClient.checkEdgeOptimizeStatus(site, '/');
3778
3804
 
3779
- const fetchOptions = fetchStub.firstCall.args[1];
3805
+ const fetchOptions = tracingFetchStub.firstCall.args[1];
3780
3806
  expect(fetchOptions.headers['User-Agent']).to.equal('Tokowaka-AI Tokowaka/1.0 AdobeEdgeOptimize-AI AdobeEdgeOptimize/1.0');
3781
3807
  });
3808
+
3809
+ it('should pass timeout option to tracingFetch', async () => {
3810
+ const mockResponse = {
3811
+ status: 200,
3812
+ headers: new Map(),
3813
+ };
3814
+ mockResponse.headers.get = () => null;
3815
+
3816
+ tracingFetchStub.resolves(mockResponse);
3817
+
3818
+ await esmockClient.checkEdgeOptimizeStatus(site, '/');
3819
+
3820
+ const fetchOptions = tracingFetchStub.firstCall.args[1];
3821
+ expect(fetchOptions.timeout).to.equal(5000);
3822
+ });
3782
3823
  });
3783
3824
 
3784
3825
  describe('Retry Logic', () => {
@@ -3801,9 +3842,9 @@ describe('TokowakaClient', () => {
3801
3842
 
3802
3843
  it('should retry 3 times on network error with exponential backoff', async () => {
3803
3844
  const networkError = new Error('Network timeout');
3804
- fetchStub.rejects(networkError);
3845
+ tracingFetchStub.rejects(networkError);
3805
3846
 
3806
- const promise = client.checkEdgeOptimizeStatus(site, '/');
3847
+ const promise = esmockClient.checkEdgeOptimizeStatus(site, '/');
3807
3848
 
3808
3849
  // Wait for first attempt
3809
3850
  await clock.tickAsync(0);
@@ -3824,7 +3865,7 @@ describe('TokowakaClient', () => {
3824
3865
  expect(error.message).to.include('Failed to check edge optimize status');
3825
3866
  expect(error.message).to.include('Network timeout');
3826
3867
  expect(error.status).to.equal(500);
3827
- expect(fetchStub.callCount).to.equal(4); // Initial + 3 retries
3868
+ expect(tracingFetchStub.callCount).to.equal(4); // Initial + 3 retries
3828
3869
  }
3829
3870
  });
3830
3871
 
@@ -3839,10 +3880,10 @@ describe('TokowakaClient', () => {
3839
3880
  },
3840
3881
  };
3841
3882
 
3842
- fetchStub.onFirstCall().rejects(new Error('Temporary failure'));
3843
- fetchStub.onSecondCall().resolves(mockResponse);
3883
+ tracingFetchStub.onFirstCall().rejects(new Error('Temporary failure'));
3884
+ tracingFetchStub.onSecondCall().resolves(mockResponse);
3844
3885
 
3845
- const promise = client.checkEdgeOptimizeStatus(site, '/');
3886
+ const promise = esmockClient.checkEdgeOptimizeStatus(site, '/');
3846
3887
 
3847
3888
  await clock.tickAsync(200);
3848
3889
 
@@ -3851,7 +3892,7 @@ describe('TokowakaClient', () => {
3851
3892
  expect(result).to.deep.equal({
3852
3893
  edgeOptimizeEnabled: true,
3853
3894
  });
3854
- expect(fetchStub).to.have.been.calledTwice;
3895
+ expect(tracingFetchStub).to.have.been.calledTwice;
3855
3896
  });
3856
3897
 
3857
3898
  it('should succeed on third attempt after two failures', async () => {
@@ -3865,11 +3906,11 @@ describe('TokowakaClient', () => {
3865
3906
  },
3866
3907
  };
3867
3908
 
3868
- fetchStub.onFirstCall().rejects(new Error('Failure 1'));
3869
- fetchStub.onSecondCall().rejects(new Error('Failure 2'));
3870
- fetchStub.onThirdCall().resolves(mockResponse);
3909
+ tracingFetchStub.onFirstCall().rejects(new Error('Failure 1'));
3910
+ tracingFetchStub.onSecondCall().rejects(new Error('Failure 2'));
3911
+ tracingFetchStub.onThirdCall().resolves(mockResponse);
3871
3912
 
3872
- const promise = client.checkEdgeOptimizeStatus(site, '/');
3913
+ const promise = esmockClient.checkEdgeOptimizeStatus(site, '/');
3873
3914
 
3874
3915
  await clock.tickAsync(200); // First retry delay
3875
3916
  await clock.tickAsync(400); // Second retry delay
@@ -3879,7 +3920,7 @@ describe('TokowakaClient', () => {
3879
3920
  expect(result).to.deep.equal({
3880
3921
  edgeOptimizeEnabled: true,
3881
3922
  });
3882
- expect(fetchStub.callCount).to.equal(3);
3923
+ expect(tracingFetchStub.callCount).to.equal(3);
3883
3924
  });
3884
3925
 
3885
3926
  it('should log warnings on retries', async () => {
@@ -3890,10 +3931,10 @@ describe('TokowakaClient', () => {
3890
3931
  };
3891
3932
  mockResponse.headers.get = () => null;
3892
3933
 
3893
- fetchStub.onFirstCall().rejects(networkError);
3894
- fetchStub.onSecondCall().resolves(mockResponse);
3934
+ tracingFetchStub.onFirstCall().rejects(networkError);
3935
+ tracingFetchStub.onSecondCall().resolves(mockResponse);
3895
3936
 
3896
- const promise = client.checkEdgeOptimizeStatus(site, '/');
3937
+ const promise = esmockClient.checkEdgeOptimizeStatus(site, '/');
3897
3938
  await clock.tickAsync(200);
3898
3939
  await promise;
3899
3940
 
@@ -3901,6 +3942,20 @@ describe('TokowakaClient', () => {
3901
3942
  sinon.match(/Attempt 1 to fetch failed.*Connection refused.*Retrying in 200ms/),
3902
3943
  );
3903
3944
  });
3945
+
3946
+ it('should return edgeOptimizeEnabled: false on request timeout (ETIMEOUT) without retrying', async () => {
3947
+ const timeoutError = new Error('Request timeout after 5000ms');
3948
+ timeoutError.code = 'ETIMEOUT';
3949
+ tracingFetchStub.rejects(timeoutError);
3950
+
3951
+ const result = await esmockClient.checkEdgeOptimizeStatus(site, '/');
3952
+
3953
+ expect(result).to.deep.equal({ edgeOptimizeEnabled: false });
3954
+ expect(tracingFetchStub).to.have.been.calledOnce;
3955
+ expect(log.warn).to.have.been.calledWith(
3956
+ sinon.match(/Request timed out after 5000ms for https:\/\/example.com\/, returning edgeOptimizeEnabled: false/),
3957
+ );
3958
+ });
3904
3959
  });
3905
3960
 
3906
3961
  describe('URL Construction', () => {
@@ -3922,11 +3977,11 @@ describe('TokowakaClient', () => {
3922
3977
  };
3923
3978
  mockResponse.headers.get = () => null;
3924
3979
 
3925
- fetchStub.resolves(mockResponse);
3980
+ tracingFetchStub.resolves(mockResponse);
3926
3981
 
3927
- await client.checkEdgeOptimizeStatus(site, '/products/chairs');
3982
+ await esmockClient.checkEdgeOptimizeStatus(site, '/products/chairs');
3928
3983
 
3929
- expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/products/chairs');
3984
+ expect(tracingFetchStub.firstCall.args[0]).to.equal('https://example.com/products/chairs');
3930
3985
  });
3931
3986
 
3932
3987
  it('should construct URL correctly with multi-level path', async () => {
@@ -3936,11 +3991,11 @@ describe('TokowakaClient', () => {
3936
3991
  };
3937
3992
  mockResponse.headers.get = () => null;
3938
3993
 
3939
- fetchStub.resolves(mockResponse);
3994
+ tracingFetchStub.resolves(mockResponse);
3940
3995
 
3941
- await client.checkEdgeOptimizeStatus(site, '/a/b/c/d');
3996
+ await esmockClient.checkEdgeOptimizeStatus(site, '/a/b/c/d');
3942
3997
 
3943
- expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/a/b/c/d');
3998
+ expect(tracingFetchStub.firstCall.args[0]).to.equal('https://example.com/a/b/c/d');
3944
3999
  });
3945
4000
 
3946
4001
  it('should handle baseURL with trailing slash', async () => {
@@ -3957,11 +4012,11 @@ describe('TokowakaClient', () => {
3957
4012
  };
3958
4013
  mockResponse.headers.get = () => null;
3959
4014
 
3960
- fetchStub.resolves(mockResponse);
4015
+ tracingFetchStub.resolves(mockResponse);
3961
4016
 
3962
- await client.checkEdgeOptimizeStatus(site, '/about');
4017
+ await esmockClient.checkEdgeOptimizeStatus(site, '/about');
3963
4018
 
3964
- expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/about');
4019
+ expect(tracingFetchStub.firstCall.args[0]).to.equal('https://example.com/about');
3965
4020
  });
3966
4021
 
3967
4022
  it('should handle baseURL without trailing slash', async () => {
@@ -3978,11 +4033,11 @@ describe('TokowakaClient', () => {
3978
4033
  };
3979
4034
  mockResponse.headers.get = () => null;
3980
4035
 
3981
- fetchStub.resolves(mockResponse);
4036
+ tracingFetchStub.resolves(mockResponse);
3982
4037
 
3983
- await client.checkEdgeOptimizeStatus(site, '/about');
4038
+ await esmockClient.checkEdgeOptimizeStatus(site, '/about');
3984
4039
 
3985
- expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/about');
4040
+ expect(tracingFetchStub.firstCall.args[0]).to.equal('https://example.com/about');
3986
4041
  });
3987
4042
  });
3988
4043
 
@@ -4010,9 +4065,9 @@ describe('TokowakaClient', () => {
4010
4065
  },
4011
4066
  };
4012
4067
 
4013
- fetchStub.resolves(mockResponse);
4068
+ tracingFetchStub.resolves(mockResponse);
4014
4069
 
4015
- const result = await client.checkEdgeOptimizeStatus(site, '/');
4070
+ const result = await esmockClient.checkEdgeOptimizeStatus(site, '/');
4016
4071
 
4017
4072
  expect(result.edgeOptimizeEnabled).to.be.true;
4018
4073
  });
@@ -4028,9 +4083,9 @@ describe('TokowakaClient', () => {
4028
4083
  },
4029
4084
  };
4030
4085
 
4031
- fetchStub.resolves(mockResponse);
4086
+ tracingFetchStub.resolves(mockResponse);
4032
4087
 
4033
- const result = await client.checkEdgeOptimizeStatus(site, '/error');
4088
+ const result = await esmockClient.checkEdgeOptimizeStatus(site, '/error');
4034
4089
 
4035
4090
  expect(result).to.deep.equal({
4036
4091
  edgeOptimizeEnabled: true,