@salesforce/lds-runtime-aura 1.436.0 → 1.437.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.
@@ -2762,7 +2762,7 @@ function buildServiceDescriptor$d(luvio) {
2762
2762
  },
2763
2763
  };
2764
2764
  }
2765
- // version: 1.436.0-581b8a5964
2765
+ // version: 1.437.0-90398d3223
2766
2766
 
2767
2767
  /*!
2768
2768
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -3115,7 +3115,7 @@ function buildServiceDescriptor$9(notifyRecordUpdateAvailable, getNormalizedLuvi
3115
3115
  },
3116
3116
  };
3117
3117
  }
3118
- // version: 1.436.0-581b8a5964
3118
+ // version: 1.437.0-90398d3223
3119
3119
 
3120
3120
  /*!
3121
3121
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -4625,7 +4625,9 @@ var HttpStatusCode;
4625
4625
  HttpStatusCode[HttpStatusCode["Unauthorized"] = 401] = "Unauthorized";
4626
4626
  HttpStatusCode[HttpStatusCode["Forbidden"] = 403] = "Forbidden";
4627
4627
  HttpStatusCode[HttpStatusCode["NotFound"] = 404] = "NotFound";
4628
+ HttpStatusCode[HttpStatusCode["TooManyRequests"] = 429] = "TooManyRequests";
4628
4629
  HttpStatusCode[HttpStatusCode["ServerError"] = 500] = "ServerError";
4630
+ HttpStatusCode[HttpStatusCode["ServiceUnavailable"] = 503] = "ServiceUnavailable";
4629
4631
  HttpStatusCode[HttpStatusCode["GatewayTimeout"] = 504] = "GatewayTimeout";
4630
4632
  })(HttpStatusCode || (HttpStatusCode = {}));
4631
4633
  /**
@@ -4688,7 +4690,7 @@ var TypeCheckShapes;
4688
4690
  TypeCheckShapes[TypeCheckShapes["Integer"] = 3] = "Integer";
4689
4691
  TypeCheckShapes[TypeCheckShapes["Unsupported"] = 4] = "Unsupported";
4690
4692
  })(TypeCheckShapes || (TypeCheckShapes = {}));
4691
- // engine version: 0.160.4-b7e0ea82
4693
+ // engine version: 0.160.5-e6ada846
4692
4694
 
4693
4695
  const { keys: keys$1 } = Object;
4694
4696
 
@@ -5745,7 +5747,7 @@ function getEnvironmentSetting(name) {
5745
5747
  }
5746
5748
  return undefined;
5747
5749
  }
5748
- // version: 1.436.0-7cd4295391
5750
+ // version: 1.437.0-f680421dc4
5749
5751
 
5750
5752
  const environmentHasAura = typeof window !== 'undefined' && typeof window.$A !== 'undefined';
5751
5753
  const defaultConfig = {
@@ -6212,6 +6214,7 @@ const CSRF_STORAGE_CONFIG = {
6212
6214
  class CsrfTokenManager {
6213
6215
  constructor() {
6214
6216
  this.tokenPromise = null;
6217
+ this.refreshInFlight = null;
6215
6218
  // Initialize AuraStorage
6216
6219
  this.storage = createStorage(CSRF_STORAGE_CONFIG);
6217
6220
  }
@@ -6287,20 +6290,37 @@ class CsrfTokenManager {
6287
6290
  /**
6288
6291
  * Obtains and returns a new token value as a promise.
6289
6292
  * This will clear the cached token and fetch a fresh one.
6293
+ *
6294
+ * Concurrent calls coalesce onto a single in-flight refresh — important when
6295
+ * multiple requests fail with INVALID_ACCESS_TOKEN simultaneously and each
6296
+ * retry policy independently calls refreshToken(). Without this, every caller
6297
+ * triggers its own /session/csrf round-trip.
6290
6298
  */
6291
- async refreshToken() {
6292
- // Clear cached token
6293
- if (this.storage) {
6294
- try {
6295
- await this.storage.remove(CSRF_TOKEN_KEY);
6299
+ refreshToken() {
6300
+ if (this.refreshInFlight) {
6301
+ return this.refreshInFlight;
6302
+ }
6303
+ const refresh = (async () => {
6304
+ if (this.storage) {
6305
+ try {
6306
+ await this.storage.remove(CSRF_TOKEN_KEY);
6307
+ }
6308
+ catch {
6309
+ // Non-fatal: continue with refresh even if clear fails
6310
+ }
6296
6311
  }
6297
- catch {
6298
- // Non-fatal: continue with refresh even if clear fails
6312
+ return this.fetchFreshToken();
6313
+ })();
6314
+ this.refreshInFlight = refresh;
6315
+ this.tokenPromise = refresh;
6316
+ // Clear the in-flight reference once settled (success or failure) so
6317
+ // subsequent token expirations can trigger a fresh refresh.
6318
+ refresh.finally(() => {
6319
+ if (this.refreshInFlight === refresh) {
6320
+ this.refreshInFlight = null;
6299
6321
  }
6300
- }
6301
- // Fetch (and cache) fresh token
6302
- this.tokenPromise = this.fetchFreshToken();
6303
- return this.tokenPromise;
6322
+ });
6323
+ return refresh;
6304
6324
  }
6305
6325
  /**
6306
6326
  * Reset the singleton instance (useful for testing).
@@ -6635,6 +6655,147 @@ function buildLuvioThirdPartyTrackerFinishInterceptor() {
6635
6655
  };
6636
6656
  }
6637
6657
 
6658
+ const DEFAULT_CONFIG$3 = {
6659
+ maxRetries: 1,
6660
+ };
6661
+ class LuvioCsrfTokenRetryPolicy extends RetryPolicy {
6662
+ constructor(config = DEFAULT_CONFIG$3) {
6663
+ super();
6664
+ this.config = config;
6665
+ this.csrfTokenManager = CsrfTokenManager.getInstance();
6666
+ }
6667
+ setRequestContext(context) {
6668
+ this.requestContext = context;
6669
+ }
6670
+ async shouldRetry(result, context) {
6671
+ if (context.attempt >= this.config.maxRetries) {
6672
+ return false;
6673
+ }
6674
+ // UIAPI returns 401 for invalid/expired CSRF tokens; the default is 400
6675
+ if (result.status !== 400 && result.status !== 401) {
6676
+ return false;
6677
+ }
6678
+ return isCsrfError$1(result);
6679
+ }
6680
+ async calculateDelay(_result, _context) {
6681
+ return 0;
6682
+ }
6683
+ async prepareRetry(_result, _context) {
6684
+ if (!this.requestContext) {
6685
+ return;
6686
+ }
6687
+ const newToken = await this.csrfTokenManager.refreshToken();
6688
+ const req = this.requestContext.request;
6689
+ const { [CSRF_TOKEN_HEADER]: _stale, ...remainingHeaders } = req.headers ?? {};
6690
+ // If refresh failed, drop the stale token entirely so the request interceptor
6691
+ // will fetch a fresh one via getToken() on the retry.
6692
+ const headers = newToken
6693
+ ? { ...remainingHeaders, [CSRF_TOKEN_HEADER]: newToken }
6694
+ : remainingHeaders;
6695
+ this.requestContext.request = { ...req, headers };
6696
+ }
6697
+ }
6698
+ /**
6699
+ * Returns true when a Luvio FetchResponse indicates a CSRF token error.
6700
+ * Body is already parsed JSON on FetchResponse, so no clone/json() needed.
6701
+ *
6702
+ * UIAPI error bodies come in both shapes: an object `{ errorCode, message }`
6703
+ * for single errors, or an array `[{ errorCode, message }]` for endpoints that
6704
+ * return collections of errors. Handle both.
6705
+ */
6706
+ function isCsrfError$1(response) {
6707
+ const body = response.body;
6708
+ const errorCode = Array.isArray(body) ? body[0]?.errorCode : body?.errorCode;
6709
+ return errorCode === 'INVALID_ACCESS_TOKEN';
6710
+ }
6711
+
6712
+ const DEFAULT_CONFIG$2 = {
6713
+ maxRetries: 3,
6714
+ maxTimeToRetry: 10000,
6715
+ baseDelay: 250,
6716
+ maxDelay: 5000,
6717
+ exponentialFactor: 2,
6718
+ jitterPercent: 0.5,
6719
+ };
6720
+ class LuvioFetchThrottlingRetryPolicy extends RetryPolicy {
6721
+ constructor(config = DEFAULT_CONFIG$2) {
6722
+ super();
6723
+ this.config = config;
6724
+ }
6725
+ async shouldRetry(result, context) {
6726
+ return ((result.status === 429 || result.status === 503) &&
6727
+ context.attempt < this.config.maxRetries &&
6728
+ context.totalElapsedMs <= this.config.maxTimeToRetry);
6729
+ }
6730
+ async calculateDelay(result, context) {
6731
+ let delay;
6732
+ const retryAfterHeader = this.parseRetryAfterHeader(result);
6733
+ if (retryAfterHeader !== undefined) {
6734
+ delay = Math.min(retryAfterHeader, this.config.maxDelay);
6735
+ }
6736
+ else {
6737
+ delay = Math.min(this.config.baseDelay * Math.pow(this.config.exponentialFactor, context.attempt), this.config.maxDelay);
6738
+ }
6739
+ const jitter = delay * this.config.jitterPercent * (Math.random() - 0.5);
6740
+ return Math.max(0, delay + jitter);
6741
+ }
6742
+ parseRetryAfterHeader(result) {
6743
+ const headers = result.headers;
6744
+ if (!headers) {
6745
+ return undefined;
6746
+ }
6747
+ // FetchResponse headers is a plain { [key: string]: string } object
6748
+ // (no Headers.get available), so do a case-insensitive lookup manually.
6749
+ let value;
6750
+ for (const key in headers) {
6751
+ if (key.toLowerCase() === 'retry-after') {
6752
+ value = headers[key];
6753
+ break;
6754
+ }
6755
+ }
6756
+ if (!value) {
6757
+ return undefined;
6758
+ }
6759
+ const trimmed = value.trim();
6760
+ if (/^\d+$/.test(trimmed)) {
6761
+ const seconds = Number(trimmed);
6762
+ if (Number.isFinite(seconds) && seconds >= 0) {
6763
+ return seconds * 1000;
6764
+ }
6765
+ return undefined;
6766
+ }
6767
+ const IMF_FIXDATE = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/;
6768
+ if (!IMF_FIXDATE.test(trimmed)) {
6769
+ return undefined;
6770
+ }
6771
+ const millis = Date.parse(trimmed);
6772
+ if (Number.isFinite(millis)) {
6773
+ return Math.max(0, millis - Date.now());
6774
+ }
6775
+ return undefined;
6776
+ }
6777
+ }
6778
+
6779
+ /**
6780
+ * Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter.
6781
+ *
6782
+ * A fresh RetryService and policy instances are created per request to keep the
6783
+ * CSRF policy's mutableRequest context isolated from concurrent requests.
6784
+ */
6785
+ function buildLuvioFetchRetryInterceptor() {
6786
+ return (request, doFetch) => {
6787
+ const csrfPolicy = new LuvioCsrfTokenRetryPolicy();
6788
+ const composedPolicy = new ComposedRetryPolicy([
6789
+ new LuvioFetchThrottlingRetryPolicy(),
6790
+ csrfPolicy,
6791
+ ]);
6792
+ const retryService = new RetryService(composedPolicy);
6793
+ const mutableRequest = { request };
6794
+ csrfPolicy.setRequestContext(mutableRequest);
6795
+ return retryService.applyRetry(() => doFetch(mutableRequest.request));
6796
+ };
6797
+ }
6798
+
6638
6799
  const SFDC_REQUEST_ID_KEY = 'X-SFDC-Request-Id';
6639
6800
  function buildTransportMarksSendInterceptor() {
6640
6801
  return async (fetchArgs, context) => {
@@ -7033,6 +7194,7 @@ const composedFetchNetworkAdapter = {
7033
7194
  buildLuvioPageScopedCacheRequestInterceptor(),
7034
7195
  buildLuvioCsrfTokenInterceptor(),
7035
7196
  ],
7197
+ retry: buildLuvioFetchRetryInterceptor(),
7036
7198
  response: [
7037
7199
  buildLexRuntimeLuvio5xxStatusResponseInterceptor(),
7038
7200
  buildLexRuntimeLuvioAuthExpirationRedirectResponseInterceptor(),
@@ -7099,12 +7261,12 @@ class CsrfTokenRetryPolicy extends RetryPolicy {
7099
7261
  if (context.attempt >= this.config.maxRetries) {
7100
7262
  return false;
7101
7263
  }
7102
- // Only retry on 400 status
7103
- if (result.status !== 400) {
7264
+ // UIAPI returns 401 for invalid/expired CSRF tokens; the default is 400.
7265
+ if (result.status !== 400 && result.status !== 401) {
7104
7266
  return false;
7105
7267
  }
7106
7268
  // Check if this is a CSRF error by examining the response body
7107
- // This avoids retrying all 400s (validation errors, bad requests, etc.)
7269
+ // This avoids retrying all 400s/401s (validation errors, auth errors, etc.)
7108
7270
  return isCsrfError(result);
7109
7271
  }
7110
7272
  /**
@@ -7153,8 +7315,10 @@ async function isCsrfError(response) {
7153
7315
  // Clone to avoid consuming the original response
7154
7316
  const cloned = response.clone();
7155
7317
  const body = await cloned.json();
7156
- // Check the error array format: body[0].errorCode
7157
- const errorCode = body?.[0]?.errorCode;
7318
+ // UIAPI error bodies come in both shapes: an object `{ errorCode, message }`
7319
+ // for single errors, or an array `[{ errorCode, message }]` for endpoints
7320
+ // that return collections of errors. Handle both.
7321
+ const errorCode = Array.isArray(body) ? body[0]?.errorCode : body?.errorCode;
7158
7322
  return errorCode === 'INVALID_ACCESS_TOKEN';
7159
7323
  }
7160
7324
  catch {
@@ -10753,4 +10917,4 @@ function ldsEngineCreator() {
10753
10917
  }
10754
10918
 
10755
10919
  export { LexRequestStrategy, PdlPrefetcherEventType, PdlRequestPriority, buildPredictorForContext, configService, ldsEngineCreator as default, initializeLDS, initializeOneStore, notifyUpdateAvailableFactory, registerRequestStrategy, saveRequestAsPrediction, subscribeToPrefetcherEvents, unregisterRequestStrategy, whenPredictionsReady };
10756
- // version: 1.436.0-581b8a5964
10920
+ // version: 1.437.0-90398d3223
@@ -5,6 +5,7 @@
5
5
  declare class CsrfTokenManager {
6
6
  private static instance;
7
7
  private tokenPromise;
8
+ private refreshInFlight;
8
9
  private storage;
9
10
  private constructor();
10
11
  static getInstance(): CsrfTokenManager;
@@ -28,6 +29,11 @@ declare class CsrfTokenManager {
28
29
  /**
29
30
  * Obtains and returns a new token value as a promise.
30
31
  * This will clear the cached token and fetch a fresh one.
32
+ *
33
+ * Concurrent calls coalesce onto a single in-flight refresh — important when
34
+ * multiple requests fail with INVALID_ACCESS_TOKEN simultaneously and each
35
+ * retry policy independently calls refreshToken(). Without this, every caller
36
+ * triggers its own /session/csrf round-trip.
31
37
  */
32
38
  refreshToken(): Promise<string | undefined>;
33
39
  /**
@@ -1,5 +1,6 @@
1
1
  import type { RequestInterceptor } from '@conduit-client/service-fetch-network/v1';
2
2
  import type { ResourceRequest } from '@luvio/engine';
3
+ export declare const CSRF_TOKEN_HEADER = "X-CSRF-Token";
3
4
  /**
4
5
  * Builds a request interceptor that adds CSRF token headers to mutating requests.
5
6
  * The CSRF token is fetched once and cached for subsequent requests.
@@ -0,0 +1,8 @@
1
+ import type { LuvioRetryInterceptor } from '@salesforce/lds-network-fetch';
2
+ /**
3
+ * Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter.
4
+ *
5
+ * A fresh RetryService and policy instances are created per request to keep the
6
+ * CSRF policy's mutableRequest context isolated from concurrent requests.
7
+ */
8
+ export declare function buildLuvioFetchRetryInterceptor(): LuvioRetryInterceptor;
@@ -0,0 +1,32 @@
1
+ import type { RetryContext } from '@conduit-client/service-retry/v1';
2
+ import { RetryPolicy } from '@conduit-client/service-retry/v1';
3
+ import type { FetchResponse, ResourceRequest } from '@luvio/engine';
4
+ type LuvioCsrfTokenRetryPolicyConfig = {
5
+ maxRetries: number;
6
+ };
7
+ /**
8
+ * Mutable container for the ResourceRequest that can be updated between retries.
9
+ */
10
+ export interface MutableLuvioRequest {
11
+ request: ResourceRequest;
12
+ }
13
+ export declare class LuvioCsrfTokenRetryPolicy extends RetryPolicy<FetchResponse<any>> {
14
+ private config;
15
+ private csrfTokenManager;
16
+ private requestContext?;
17
+ constructor(config?: LuvioCsrfTokenRetryPolicyConfig);
18
+ setRequestContext(context: MutableLuvioRequest): void;
19
+ shouldRetry(result: FetchResponse<any>, context: RetryContext<FetchResponse<any>>): Promise<boolean>;
20
+ calculateDelay(_result: FetchResponse<any>, _context: RetryContext<FetchResponse<any>>): Promise<number>;
21
+ prepareRetry(_result: FetchResponse<any>, _context: RetryContext<FetchResponse<any>>): Promise<void>;
22
+ }
23
+ /**
24
+ * Returns true when a Luvio FetchResponse indicates a CSRF token error.
25
+ * Body is already parsed JSON on FetchResponse, so no clone/json() needed.
26
+ *
27
+ * UIAPI error bodies come in both shapes: an object `{ errorCode, message }`
28
+ * for single errors, or an array `[{ errorCode, message }]` for endpoints that
29
+ * return collections of errors. Handle both.
30
+ */
31
+ export declare function isCsrfError(response: FetchResponse<any>): boolean;
32
+ export {};
@@ -0,0 +1,18 @@
1
+ import { RetryPolicy, type RetryContext } from '@conduit-client/service-retry/v1';
2
+ import type { FetchResponse } from '@luvio/engine';
3
+ type LuvioFetchThrottlingRetryPolicyConfig = {
4
+ maxRetries: number;
5
+ maxTimeToRetry: number;
6
+ baseDelay: number;
7
+ maxDelay: number;
8
+ exponentialFactor: number;
9
+ jitterPercent: number;
10
+ };
11
+ export declare class LuvioFetchThrottlingRetryPolicy extends RetryPolicy<FetchResponse<any>> {
12
+ private config;
13
+ constructor(config?: LuvioFetchThrottlingRetryPolicyConfig);
14
+ shouldRetry(result: FetchResponse<any>, context: RetryContext<FetchResponse<any>>): Promise<boolean>;
15
+ calculateDelay(result: FetchResponse<any>, context: RetryContext<FetchResponse<any>>): Promise<number>;
16
+ parseRetryAfterHeader(result: FetchResponse<any>): number | undefined;
17
+ }
18
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-runtime-aura",
3
- "version": "1.436.0",
3
+ "version": "1.437.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "LDS engine for Aura runtime",
6
6
  "main": "dist/ldsEngineCreator.js",
@@ -36,15 +36,15 @@
36
36
  "devDependencies": {
37
37
  "@conduit-client/service-provisioner": "3.19.5",
38
38
  "@conduit-client/tools-core": "3.19.5",
39
- "@salesforce/lds-adapters-apex": "^1.436.0",
40
- "@salesforce/lds-adapters-uiapi": "^1.436.0",
41
- "@salesforce/lds-ads-bridge": "^1.436.0",
42
- "@salesforce/lds-aura-storage": "^1.436.0",
43
- "@salesforce/lds-bindings": "^1.436.0",
44
- "@salesforce/lds-instrumentation": "^1.436.0",
45
- "@salesforce/lds-network-adapter": "^1.436.0",
46
- "@salesforce/lds-network-aura": "^1.436.0",
47
- "@salesforce/lds-network-fetch": "^1.436.0",
39
+ "@salesforce/lds-adapters-apex": "^1.437.0",
40
+ "@salesforce/lds-adapters-uiapi": "^1.437.0",
41
+ "@salesforce/lds-ads-bridge": "^1.437.0",
42
+ "@salesforce/lds-aura-storage": "^1.437.0",
43
+ "@salesforce/lds-bindings": "^1.437.0",
44
+ "@salesforce/lds-instrumentation": "^1.437.0",
45
+ "@salesforce/lds-network-adapter": "^1.437.0",
46
+ "@salesforce/lds-network-aura": "^1.437.0",
47
+ "@salesforce/lds-network-fetch": "^1.437.0",
48
48
  "jwt-encode": "1.0.1"
49
49
  },
50
50
  "dependencies": {
@@ -72,22 +72,22 @@
72
72
  "@conduit-client/service-pubsub": "3.19.5",
73
73
  "@conduit-client/service-store": "3.19.5",
74
74
  "@conduit-client/utils": "3.19.5",
75
- "@luvio/network-adapter-composable": "0.160.4",
76
- "@luvio/network-adapter-fetch": "0.160.4",
75
+ "@luvio/network-adapter-composable": "0.160.5",
76
+ "@luvio/network-adapter-fetch": "0.160.5",
77
77
  "@lwc/state": "^0.29.0",
78
- "@salesforce/lds-adapters-onestore-graphql": "^1.436.0",
78
+ "@salesforce/lds-adapters-onestore-graphql": "^1.437.0",
79
79
  "@salesforce/lds-adapters-uiapi-lex": "^1.415.0",
80
- "@salesforce/lds-durable-storage": "^1.436.0",
81
- "@salesforce/lds-luvio-service": "^1.436.0",
82
- "@salesforce/lds-luvio-uiapi-records-service": "^1.436.0"
80
+ "@salesforce/lds-durable-storage": "^1.437.0",
81
+ "@salesforce/lds-luvio-service": "^1.437.0",
82
+ "@salesforce/lds-luvio-uiapi-records-service": "^1.437.0"
83
83
  },
84
84
  "luvioBundlesize": [
85
85
  {
86
86
  "path": "./dist/ldsEngineCreator.js",
87
87
  "maxSize": {
88
- "none": "376 kB",
88
+ "none": "383 kB",
89
89
  "min": "190 kB",
90
- "compressed": "61.9 kB"
90
+ "compressed": "63 kB"
91
91
  }
92
92
  }
93
93
  ],