@salesforce/lds-runtime-one-app 1.462.0 → 1.463.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.
@@ -16,6 +16,7 @@ import { HttpStatusCode as HttpStatusCode$2, InMemoryStore, Environment, Luvio,
16
16
  import usePredictiveLoading from '@salesforce/gate/lds.usePredictiveLoading';
17
17
  import useLocalStorage from '@salesforce/gate/lds.pdl.useLocalStorage';
18
18
  import useOneStoreGraphql from '@salesforce/gate/lds.oneStoreGraphqlEnabled.ltng';
19
+ import usePersistedQueries from '@salesforce/gate/lds.graphqlusepersistedqueries';
19
20
  import canCombineRecordLayoutRequests from '@salesforce/gate/lds.pdl.canCombineRecordLayoutRequests';
20
21
  import { GetApexWireAdapterFactory, registerPrefetcher as registerPrefetcher$1 } from 'force/ldsAdaptersApex';
21
22
  import { instrument, getRecordAvatarsAdapterFactory, getRecordAdapterFactory, coerceFieldIdArray, coerceLayoutTypeArray, coerceLayoutModeArray, getRecordsAdapterFactory, getRecordActionsAdapterFactory, getObjectInfosAdapterFactory, coerceObjectIdArray, getObjectInfoAdapterFactory, coerceObjectId, getRelatedListsActionsAdapterFactory, getRelatedListInfoBatchAdapterFactory, getRelatedListInfoAdapterFactory, getRelatedListRecordsBatchAdapterFactory, getRelatedListRecordsAdapterFactory, getListInfoByNameAdapterFactory, getListInfosByObjectNameAdapterFactory, getListRecordsByNameAdapterFactory, getListObjectInfoAdapterFactory, getRelatedListsInfoAdapterFactory, getRelatedListActionsAdapterFactory, getRecordId18Array, buildRecordRepKeyFromId, configuration, InMemoryRecordRepresentationQueryEvaluator, UiApiNamespace, RecordRepresentationRepresentationType, registerPrefetcher, RecordRepresentationVersion } from 'force/ldsAdaptersUiapi';
@@ -28,7 +29,7 @@ import { dispatchGlobalEvent as dispatchGlobalEvent$1, executeGlobalControllerRa
28
29
  import auraNetworkAdapter, { dispatchAuraAction, defaultActionConfig, instrument as instrument$1, forceRecordTransactionsDisabled as forceRecordTransactionsDisabled$1, ldsNetworkAdapterInstrument, CrudEventState, CrudEventType, UIAPI_RECORDS_PATH, UIAPI_RELATED_LIST_RECORDS_BATCH_PATH, UIAPI_RELATED_LIST_RECORDS_PATH } from 'force/ldsNetwork';
29
30
  import { ThirdPartyTracker } from 'instrumentation:thirdPartyTracker';
30
31
  import { markStart, markEnd, counter, registerCacheStats, perfStart, perfEnd, registerPeriodicLogger, interaction, timer, mark } from 'instrumentation/service';
31
- import { instrument as instrument$2, isCsrfMethod, getFetchMethod, isRetryableMethod, isRetryableReadPost, compilePathMatchers, setupLexNetworkAdapter, UIAPI_PUBLIC_PATHS, UIAPI_PRIVATE_PATHS, isRetryableOperationType } from 'force/ldsNetworkFetch';
32
+ import { instrument as instrument$2, isMutatingMethod, getFetchMethod, isIdempotentWrite, readUiApiErrorCode, isRetryableMethod, isRetryableReadPost, parseRetryAfterMs, compilePathMatchers, setupLexNetworkAdapter, UIAPI_PUBLIC_PATHS, UIAPI_PRIVATE_PATHS, isRetryableOperationType } from 'force/ldsNetworkFetch';
32
33
  import { REFRESH_ADAPTER_EVENT, ADAPTER_UNFULFILLED_ERROR, instrument as instrument$3 } from 'force/ldsBindings';
33
34
  import { stateManagerInstrumentation, OperationTypeValue, LRUCache, instrumentAdapter, instrumentLuvio, logMessage as logMessage$1, setupInstrumentation as setupInstrumentation$1, logObjectInfoChanged as logObjectInfoChanged$1, updatePercentileHistogramMetric, incrementCounterMetric, incrementGetRecordNotifyChangeAllowCount, incrementGetRecordNotifyChangeDropCount, incrementNotifyRecordUpdateAvailableAllowCount, incrementNotifyRecordUpdateAvailableDropCount, setLdsAdaptersUiapiInstrumentation, logError as logError$2, setLdsNetworkAdapterInstrumentation, executeAsyncActivity, METRIC_KEYS, onIdleDetected } from 'force/ldsInstrumentation';
34
35
  import { instrument as instrument$4 } from 'force/adsBridge';
@@ -388,6 +389,15 @@ function buildServiceDescriptor$q() {
388
389
  };
389
390
  }
390
391
 
392
+ function applyOperationRequestOptions$1(init, operationType) {
393
+ if (operationType === void 0) {
394
+ return init;
395
+ }
396
+ return {
397
+ ...init,
398
+ onestore: { ...init?.onestore, retrySafe: operationType === "query" }
399
+ };
400
+ }
391
401
  class AuraNetworkCommand extends NetworkCommand$1 {
392
402
  constructor(services) {
393
403
  super(services);
@@ -406,6 +416,18 @@ class AuraNetworkCommand extends NetworkCommand$1 {
406
416
  "Fetch parameters must be specified when using HTTP transport on an Aura adapter"
407
417
  );
408
418
  }
419
+ /**
420
+ * The command's operation type. Generated command subclasses override this
421
+ * getter with their concrete operation (see the generator's
422
+ * `generateOperationType`); the base returns `undefined` for commands that
423
+ * don't declare one. On the HTTP fallback path (`fetch`) it derives the
424
+ * request's `onestore.retrySafe` — reads (`query`) are retry-safe, writes
425
+ * (`mutation`) are not. Mirrors `FetchNetworkCommand.operationType`, which
426
+ * this class does not inherit (it extends `NetworkCommand` directly).
427
+ */
428
+ get operationType() {
429
+ return void 0;
430
+ }
409
431
  coerceAuraErrors(auraErrors) {
410
432
  return new UserVisibleError(getFetchResponseFromAuraError(auraErrors[0]));
411
433
  }
@@ -507,9 +529,11 @@ class AuraNetworkCommand extends NetworkCommand$1 {
507
529
  this.coerceAuraErrors
508
530
  );
509
531
  } else if (this.shouldUseFetch()) {
510
- const params = this.fetchParams;
532
+ const [input, init] = this.fetchParams;
533
+ const initWithOptions = applyOperationRequestOptions$1(init, this.operationType);
511
534
  try {
512
- return this.convertFetchResponseToData(this.services.fetch(...params));
535
+ const fetchCall = initWithOptions === void 0 ? this.services.fetch(input) : this.services.fetch(input, initWithOptions);
536
+ return this.convertFetchResponseToData(fetchCall);
513
537
  } catch (reason) {
514
538
  return resolvedPromiseLike$2(err$1(toError(reason)));
515
539
  }
@@ -1363,6 +1387,15 @@ class NetworkCommand extends BaseCommand {
1363
1387
  async afterRequestHooks(_options) {
1364
1388
  }
1365
1389
  }
1390
+ function applyOperationRequestOptions(init, operationType) {
1391
+ if (operationType === void 0) {
1392
+ return init;
1393
+ }
1394
+ return {
1395
+ ...init,
1396
+ onestore: { ...init?.onestore, retrySafe: operationType === "query" }
1397
+ };
1398
+ }
1366
1399
  function isAbortableCommand(command) {
1367
1400
  return !!command && typeof command === "object" && "isAbortableCommand" in command && command.isAbortableCommand === true;
1368
1401
  }
@@ -1452,7 +1485,8 @@ const _FetchNetworkCommand = class _FetchNetworkCommand extends NetworkCommand {
1452
1485
  * `generateOperationType`); the base returns `undefined` for commands that
1453
1486
  * don't declare one (e.g. hand-written commands). Transport-layer
1454
1487
  * interceptors use it to tell a read (`query`) apart from a write — see
1455
- * `fetch`, which folds it into the context seed.
1488
+ * `fetch`, which folds it into the context seed and derives
1489
+ * `onestore.retrySafe` from it.
1456
1490
  */
1457
1491
  get operationType() {
1458
1492
  return void 0;
@@ -1461,8 +1495,9 @@ const _FetchNetworkCommand = class _FetchNetworkCommand extends NetworkCommand {
1461
1495
  try {
1462
1496
  const [input, init] = this.fetchParams;
1463
1497
  const operationType = this.operationType;
1498
+ const initWithOptions = applyOperationRequestOptions(init, operationType);
1464
1499
  const seed = operationType === void 0 ? contextSeed : { operationType, ...contextSeed };
1465
- const initWithSeed = seed === void 0 ? init : { ...init, __contextSeed: seed };
1500
+ const initWithSeed = seed === void 0 ? initWithOptions : { ...initWithOptions, __contextSeed: seed };
1466
1501
  const fetchCall = initWithSeed === void 0 ? this.services.fetch(input) : this.services.fetch(input, initWithSeed);
1467
1502
  return this.convertFetchResponseToData(fetchCall);
1468
1503
  } catch (reason) {
@@ -2601,7 +2636,7 @@ function buildServiceDescriptor$d(luvio) {
2601
2636
  },
2602
2637
  };
2603
2638
  }
2604
- // version: 1.462.0-50d9a13ba4
2639
+ // version: 1.463.0-ea9ecd17fc
2605
2640
 
2606
2641
  class AuraGraphQLNormalizedCacheControlCommand extends AuraNormalizedCacheControlCommand {
2607
2642
  constructor(config, documentRootType, services) {
@@ -2940,7 +2975,7 @@ function buildServiceDescriptor$9(notifyRecordUpdateAvailable, getNormalizedLuvi
2940
2975
  },
2941
2976
  };
2942
2977
  }
2943
- // version: 1.462.0-50d9a13ba4
2978
+ // version: 1.463.0-ea9ecd17fc
2944
2979
 
2945
2980
  class RetryService {
2946
2981
  constructor(defaultRetryPolicy) {
@@ -4460,10 +4495,14 @@ function getTextEncoder() {
4460
4495
  }
4461
4496
  return textEncoder;
4462
4497
  }
4463
- function buildCompressionInterceptor(config) {
4464
- const threshold = config.threshold ?? 1024;
4498
+ function buildCompressionInterceptor(defaultConfig) {
4465
4499
  return async (args) => {
4466
4500
  const [resource, options = {}] = args;
4501
+ const config = options.onestore?.compression ?? defaultConfig;
4502
+ if (config === void 0) {
4503
+ return resolvedPromiseLike$2(args);
4504
+ }
4505
+ const threshold = config.threshold ?? 1024;
4467
4506
  if (!options.body || typeof options.body !== "string") {
4468
4507
  return resolvedPromiseLike$2(args);
4469
4508
  }
@@ -5862,7 +5901,7 @@ function getEnvironmentSetting(name) {
5862
5901
  }
5863
5902
  return undefined;
5864
5903
  }
5865
- // version: 1.462.0-2919385b97
5904
+ // version: 1.463.0-8515018faf
5866
5905
 
5867
5906
  /**
5868
5907
  * Helpers for reaching the Aura framework from the LDS Aura runtime.
@@ -6481,7 +6520,7 @@ function buildCsrfTokenInterceptor() {
6481
6520
  return resolvedPromiseLike$2(fetchArgs);
6482
6521
  }
6483
6522
  // Only add CSRF token for mutating operations
6484
- if (isCsrfMethod(getFetchMethod(fetchArgs))) {
6523
+ if (isMutatingMethod(getFetchMethod(fetchArgs))) {
6485
6524
  const token = await getCsrfToken();
6486
6525
  if (token) {
6487
6526
  // eslint-disable-next-line no-param-reassign
@@ -6509,7 +6548,7 @@ function buildLuvioCsrfTokenInterceptor() {
6509
6548
  resourceRequest.headers = {};
6510
6549
  }
6511
6550
  // Only add CSRF token for mutating operations
6512
- if (isCsrfMethod(resourceRequest.method)) {
6551
+ if (isMutatingMethod(resourceRequest.method)) {
6513
6552
  // Don't overwrite existing CSRF token header if it already exists
6514
6553
  if (!resourceRequest.headers[CSRF_TOKEN_HEADER]) {
6515
6554
  const token = await getCsrfToken();
@@ -6522,6 +6561,53 @@ function buildLuvioCsrfTokenInterceptor() {
6522
6561
  };
6523
6562
  }
6524
6563
 
6564
+ const IDEMPOTENCY_KEY_HEADER = 'Idempotency-Key';
6565
+ // Server-side idempotency error codes (UI SDK / UI-API `ApiExceptionCode`); the
6566
+ // mobile draft-queue handler is the reference consumer.
6567
+ const ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED = 'IDEMPOTENCY_FEATURE_NOT_ENABLED';
6568
+ const ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED = 'IDEMPOTENCY_NOT_SUPPORTED';
6569
+ const ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR = 'IDEMPOTENCY_BACKEND_OPERATION_ERROR';
6570
+ const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = 'IDEMPOTENCY_KEY_USED_DIFFERENT_USER';
6571
+ const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = 'IDEMPOTENCY_CONCURRENT_REQUEST';
6572
+ const ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED = 'IDEMPOTENCY_KEY_ALREADY_USED';
6573
+ /**
6574
+ * Generates an RFC4122 v4 UUID for use as an idempotency key, preferring
6575
+ * `crypto.randomUUID()` and falling back to a `Math.random()` builder if it is
6576
+ * unavailable (matches the SFAP request-id approach in `network-sfap`).
6577
+ */
6578
+ function mintIdempotencyKey() {
6579
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
6580
+ return crypto.randomUUID();
6581
+ }
6582
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
6583
+ const r = (Math.random() * 16) | 0;
6584
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
6585
+ return v.toString(16);
6586
+ });
6587
+ }
6588
+ /**
6589
+ * Builds a Luvio request interceptor that mints and attaches an `Idempotency-Key`
6590
+ * on mutations to onboarded endpoints, once per logical write, so the retry
6591
+ * service re-sends the same key on every replay and the server can dedupe.
6592
+ *
6593
+ * Minting is gated here; retry *eligibility* is decided separately by
6594
+ * `LuvioIdempotencyRetryPolicy` from the header's presence, so a stale
6595
+ * registry flag can never cause an unsafe replay.
6596
+ */
6597
+ function buildLuvioIdempotencyKeyInterceptor() {
6598
+ return async (resourceRequest) => {
6599
+ if (!resourceRequest.headers) {
6600
+ resourceRequest.headers = {};
6601
+ }
6602
+ if (isMutatingMethod(resourceRequest.method) &&
6603
+ isIdempotentWrite(resourceRequest.basePath) &&
6604
+ !resourceRequest.headers[IDEMPOTENCY_KEY_HEADER]) {
6605
+ resourceRequest.headers[IDEMPOTENCY_KEY_HEADER] = mintIdempotencyKey();
6606
+ }
6607
+ return resolvedPromiseLike$2(resourceRequest);
6608
+ };
6609
+ }
6610
+
6525
6611
  const ENTITY_ENCODING_HEADER = 'X-Chatter-Entity-Encoding';
6526
6612
  const ENTITY_ENCODING_DISABLED_VALUE = 'false';
6527
6613
  /**
@@ -6818,11 +6904,11 @@ function buildLuvioThirdPartyTrackerFinishInterceptor() {
6818
6904
  };
6819
6905
  }
6820
6906
 
6821
- const DEFAULT_CONFIG$3 = {
6907
+ const DEFAULT_CONFIG$4 = {
6822
6908
  maxRetries: 1,
6823
6909
  };
6824
6910
  class LuvioCsrfTokenRetryPolicy extends RetryPolicy {
6825
- constructor(config = DEFAULT_CONFIG$3) {
6911
+ constructor(config = DEFAULT_CONFIG$4) {
6826
6912
  super();
6827
6913
  this.config = config;
6828
6914
  }
@@ -6866,21 +6952,12 @@ class LuvioCsrfTokenRetryPolicy extends RetryPolicy {
6866
6952
  this.requestContext.request = { ...req, headers };
6867
6953
  }
6868
6954
  }
6869
- /**
6870
- * Returns true when a Luvio FetchResponse indicates a CSRF token error.
6871
- * Body is already parsed JSON on FetchResponse, so no clone/json() needed.
6872
- *
6873
- * UIAPI error bodies come in both shapes: an object `{ errorCode, message }`
6874
- * for single errors, or an array `[{ errorCode, message }]` for endpoints that
6875
- * return collections of errors. Handle both.
6876
- */
6955
+ /** True when a Luvio FetchResponse indicates a CSRF token error. */
6877
6956
  function isCsrfError$1(response) {
6878
- const body = response.body;
6879
- const errorCode = Array.isArray(body) ? body[0]?.errorCode : body?.errorCode;
6880
- return errorCode === 'INVALID_ACCESS_TOKEN';
6957
+ return readUiApiErrorCode(response) === 'INVALID_ACCESS_TOKEN';
6881
6958
  }
6882
6959
 
6883
- const DEFAULT_CONFIG$2 = {
6960
+ const DEFAULT_CONFIG$3 = {
6884
6961
  maxRetries: 3,
6885
6962
  maxTimeToRetry: 10000,
6886
6963
  baseDelay: 250,
@@ -6894,7 +6971,7 @@ class LuvioFetchThrottlingRetryPolicy extends RetryPolicy {
6894
6971
  // is why we take the plain `ResourceRequest` rather than the mutable-container
6895
6972
  // pattern the CSRF policy uses — that policy rewrites the request (new token)
6896
6973
  // between attempts; this one never mutates it.
6897
- constructor(config = DEFAULT_CONFIG$2, request) {
6974
+ constructor(config = DEFAULT_CONFIG$3, request) {
6898
6975
  super();
6899
6976
  this.config = config;
6900
6977
  this.request = request;
@@ -6922,57 +6999,157 @@ class LuvioFetchThrottlingRetryPolicy extends RetryPolicy {
6922
6999
  return Math.max(0, delay + jitter);
6923
7000
  }
6924
7001
  parseRetryAfterHeader(result) {
6925
- const headers = result.headers;
6926
- if (!headers) {
6927
- return undefined;
6928
- }
6929
- // FetchResponse headers is a plain { [key: string]: string } object
6930
- // (no Headers.get available), so do a case-insensitive lookup manually.
6931
- let value;
6932
- for (const key in headers) {
6933
- if (key.toLowerCase() === 'retry-after') {
6934
- value = headers[key];
6935
- break;
7002
+ return parseRetryAfterMs(result.headers);
7003
+ }
7004
+ }
7005
+
7006
+ const DEFAULT_CONFIG$2 = {
7007
+ maxRetries: 3,
7008
+ };
7009
+ // The idempotency handler never emits 429 (that's throttling's domain). Its 503
7010
+ // is a keyed-write lock-acquire failure (scenario 7), not throttling's keyless
7011
+ // GET/read-POST 503 — the two never contend (see classifyResponse).
7012
+ const HTTP_STATUS_BAD_REQUEST = 400;
7013
+ const HTTP_STATUS_REQUEST_TIMEOUT = 408;
7014
+ const HTTP_STATUS_CONFLICT = 409;
7015
+ const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422;
7016
+ const HTTP_STATUS_SERVER_ERROR = 500;
7017
+ const HTTP_STATUS_SERVICE_UNAVAILABLE = 503;
7018
+ // Terminal cases (IDEMPOTENCY_AUTHENTICATION_FAILED on 401, INVALID_IDEMPOTENCY_KEY
7019
+ // on 400) are deliberately absent so they fall through to 'none'.
7020
+ const STRIP_ERROR_CODES = [
7021
+ ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED,
7022
+ ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED,
7023
+ ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR,
7024
+ ];
7025
+ const ROTATE_ERROR_CODES = [
7026
+ ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER,
7027
+ ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED,
7028
+ ];
7029
+ /**
7030
+ * Maps an idempotent-write response to its disposition. Status alone is
7031
+ * ambiguous (e.g. 409 is same-key for a concurrent request but rotate for a
7032
+ * different user), so pair it with the body's errorCode.
7033
+ */
7034
+ function classifyResponse(response) {
7035
+ const errorCode = readUiApiErrorCode(response);
7036
+ // FetchResponse.status is typed as HttpStatusCode, which omits 408/409/422
7037
+ // even though the idempotency handler emits them; widen to match.
7038
+ const status = response.status;
7039
+ switch (status) {
7040
+ case HTTP_STATUS_SERVER_ERROR:
7041
+ return errorCode === ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR ? 'strip' : 'none';
7042
+ case HTTP_STATUS_BAD_REQUEST:
7043
+ // FEATURE_NOT_ENABLED / NOT_SUPPORTED → strip. Any other 400 (incl.
7044
+ // INVALID_IDEMPOTENCY_KEY and CSRF's INVALID_ACCESS_TOKEN) is terminal.
7045
+ return errorCode !== undefined && STRIP_ERROR_CODES.includes(errorCode)
7046
+ ? 'strip'
7047
+ : 'none';
7048
+ case HTTP_STATUS_REQUEST_TIMEOUT:
7049
+ // Backend connection timeout — the write may have already committed;
7050
+ // replay same-key so the server can dedupe.
7051
+ return 'same-key';
7052
+ case HTTP_STATUS_SERVICE_UNAVAILABLE:
7053
+ // Lock-acquire failure (scenario 7): the write never committed. Only a
7054
+ // keyed write reaches here (shouldRetry gates on key presence) — a
7055
+ // keyless 503 is the throttling policy's GET/read-POST case.
7056
+ return 'same-key';
7057
+ case HTTP_STATUS_CONFLICT:
7058
+ // KEY_USED_DIFFERENT_USER → rotate; CONCURRENT_REQUEST → same key.
7059
+ if (errorCode !== undefined && ROTATE_ERROR_CODES.includes(errorCode)) {
7060
+ return 'rotate';
6936
7061
  }
7062
+ return errorCode === ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST ? 'same-key' : 'none';
7063
+ case HTTP_STATUS_UNPROCESSABLE_ENTITY:
7064
+ // Same key reused for a different request body → needs a new key.
7065
+ return errorCode === ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED ? 'rotate' : 'none';
7066
+ default:
7067
+ return 'none';
7068
+ }
7069
+ }
7070
+ /**
7071
+ * Retry policy for writes carrying an `Idempotency-Key`. A peer of the CSRF and
7072
+ * throttling policies in the composed retry chain, not a modification of
7073
+ * either: its 503 (keyed-write lock-acquire) and throttling's 503 (keyless
7074
+ * GET/read-POST) never contend because they key off different requests, and it
7075
+ * only claims a 400/409 when the body carries an idempotency-specific
7076
+ * errorCode, so it never steals CSRF's INVALID_ACCESS_TOKEN.
7077
+ *
7078
+ * Eligibility follows from the header's *presence* on the request, not from
7079
+ * the mint interceptor's registry flag — so a stale flag can never cause an
7080
+ * unsafe replay.
7081
+ */
7082
+ class LuvioIdempotencyRetryPolicy extends RetryPolicy {
7083
+ constructor(config = DEFAULT_CONFIG$2) {
7084
+ super();
7085
+ this.config = config;
7086
+ }
7087
+ setRequestContext(context) {
7088
+ this.requestContext = context;
7089
+ }
7090
+ async shouldRetry(result, context) {
7091
+ if (context.attempt >= this.config.maxRetries) {
7092
+ return false;
6937
7093
  }
6938
- if (!value) {
6939
- return undefined;
7094
+ const headers = this.requestContext?.request.headers;
7095
+ if (!headers || !headers[IDEMPOTENCY_KEY_HEADER]) {
7096
+ return false;
6940
7097
  }
6941
- const trimmed = value.trim();
6942
- if (/^\d+$/.test(trimmed)) {
6943
- const seconds = Number(trimmed);
6944
- if (Number.isFinite(seconds) && seconds >= 0) {
6945
- return seconds * 1000;
6946
- }
6947
- return undefined;
7098
+ return classifyResponse(result) !== 'none';
7099
+ }
7100
+ async calculateDelay(result, _context) {
7101
+ // Only same-key replays (408/409/503) carry a Retry-After to honor;
7102
+ // strip/rotate retry immediately.
7103
+ if (classifyResponse(result) !== 'same-key') {
7104
+ return 0;
6948
7105
  }
6949
- 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$/;
6950
- if (!IMF_FIXDATE.test(trimmed)) {
6951
- return undefined;
7106
+ // Honor the server's Retry-After in full, like the mobile handler — it's
7107
+ // a lock-hold hint, not a client-owned backoff (unlike the throttling
7108
+ // sibling's capped delay). maxRetries bounds replays instead.
7109
+ return parseRetryAfterMs(result.headers) ?? 0;
7110
+ }
7111
+ async prepareRetry(result, _context) {
7112
+ if (!this.requestContext) {
7113
+ return;
6952
7114
  }
6953
- const millis = Date.parse(trimmed);
6954
- if (Number.isFinite(millis)) {
6955
- return Math.max(0, millis - Date.now());
7115
+ const disposition = classifyResponse(result);
7116
+ if (disposition === 'same-key' || disposition === 'none') {
7117
+ return;
6956
7118
  }
6957
- return undefined;
7119
+ const req = this.requestContext.request;
7120
+ // Drop the existing key via immutable spread (matches the CSRF policy).
7121
+ const { [IDEMPOTENCY_KEY_HEADER]: _stale, ...remainingHeaders } = req.headers ?? {};
7122
+ const headers = disposition === 'rotate'
7123
+ ? { ...remainingHeaders, [IDEMPOTENCY_KEY_HEADER]: mintIdempotencyKey() }
7124
+ : // 'strip': retry plain — the feature is unavailable for this request.
7125
+ remainingHeaders;
7126
+ this.requestContext.request = { ...req, headers };
6958
7127
  }
6959
7128
  }
6960
7129
 
6961
7130
  /**
6962
- * Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter.
7131
+ * Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter. A fresh
7132
+ * RetryService and policies are created per request to keep the
7133
+ * mutableRequest context isolated across concurrent requests.
6963
7134
  *
6964
- * A fresh RetryService and policy instances are created per request to keep the
6965
- * CSRF policy's mutableRequest context isolated from concurrent requests.
7135
+ * Policy order matters: ComposedRetryPolicy uses the first matching policy's
7136
+ * shouldRetry/delay. A keyed-write 503 fails throttling's method gate, so it
7137
+ * falls to the idempotency policy to replay same-key.
6966
7138
  */
6967
7139
  function buildLuvioFetchRetryInterceptor() {
6968
7140
  return (request, doFetch) => {
7141
+ const idempotencyPolicy = new LuvioIdempotencyRetryPolicy();
6969
7142
  const csrfPolicy = new LuvioCsrfTokenRetryPolicy();
6970
7143
  const composedPolicy = new ComposedRetryPolicy([
7144
+ idempotencyPolicy,
6971
7145
  new LuvioFetchThrottlingRetryPolicy(undefined, request),
6972
7146
  csrfPolicy,
6973
7147
  ]);
6974
7148
  const retryService = new RetryService(composedPolicy);
6975
7149
  const mutableRequest = { request };
7150
+ // Both policies rewrite headers on the shared request between attempts
7151
+ // (idempotency strips/rotates the key; CSRF refreshes the token).
7152
+ idempotencyPolicy.setRequestContext(mutableRequest);
6976
7153
  csrfPolicy.setRequestContext(mutableRequest);
6977
7154
  return retryService.applyRetry(() => doFetch(mutableRequest.request));
6978
7155
  };
@@ -7184,6 +7361,7 @@ const composedFetchNetworkAdapter = {
7184
7361
  buildLuvioTransportMarksSendInterceptor(),
7185
7362
  buildLuvioPageScopedCacheRequestInterceptor(),
7186
7363
  buildLuvioCsrfTokenInterceptor(),
7364
+ buildLuvioIdempotencyKeyInterceptor(),
7187
7365
  buildLuvioFirstPartyHeaderInterceptor(),
7188
7366
  buildLuvioEntityEncodingInterceptor(),
7189
7367
  ],
@@ -11319,6 +11497,8 @@ function initializeOneStore(luvio) {
11319
11497
  // across LEX and sites. Paired with the HTTP UIAPI fetch path being
11320
11498
  // disabled on sites (see network-fetch.ts).
11321
11499
  featureFlagsService.set('graphQLUseAura', true);
11500
+ // Persisted (named) GraphQL queries; no consumers yet (W-23971639).
11501
+ featureFlagsService.set('graphQLUsePersistedQueries', usePersistedQueries.isOpen({ fallback: false }));
11322
11502
  const services = [
11323
11503
  instrumentationServiceDescriptor,
11324
11504
  buildLexRuntimeDefaultFetchServiceDescriptor(loggerService, retryService),
@@ -11379,4 +11559,4 @@ function ldsEngineCreator() {
11379
11559
  }
11380
11560
 
11381
11561
  export { LexRequestStrategy, PDL_ENGINE_REGISTRATION_ID, PdlPrefetcherEventType, PdlRequestPriority, buildPredictorForContext, configService, ldsEngineCreator as default, initializeLDS, initializeOneStore, notifyUpdateAvailableFactory, registerRequestStrategy, saveRequestAsPrediction, subscribeToPrefetcherEvents, unregisterRequestStrategy, whenPredictionsReady };
11382
- // version: 1.462.0-50d9a13ba4
11562
+ // version: 1.463.0-ea9ecd17fc
@@ -0,0 +1,24 @@
1
+ import type { ResourceRequest } from '@luvio/engine';
2
+ export declare const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
3
+ export declare const ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED = "IDEMPOTENCY_FEATURE_NOT_ENABLED";
4
+ export declare const ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED = "IDEMPOTENCY_NOT_SUPPORTED";
5
+ export declare const ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR = "IDEMPOTENCY_BACKEND_OPERATION_ERROR";
6
+ export declare const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = "IDEMPOTENCY_KEY_USED_DIFFERENT_USER";
7
+ export declare const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = "IDEMPOTENCY_CONCURRENT_REQUEST";
8
+ export declare const ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED = "IDEMPOTENCY_KEY_ALREADY_USED";
9
+ /**
10
+ * Generates an RFC4122 v4 UUID for use as an idempotency key, preferring
11
+ * `crypto.randomUUID()` and falling back to a `Math.random()` builder if it is
12
+ * unavailable (matches the SFAP request-id approach in `network-sfap`).
13
+ */
14
+ export declare function mintIdempotencyKey(): string;
15
+ /**
16
+ * Builds a Luvio request interceptor that mints and attaches an `Idempotency-Key`
17
+ * on mutations to onboarded endpoints, once per logical write, so the retry
18
+ * service re-sends the same key on every replay and the server can dedupe.
19
+ *
20
+ * Minting is gated here; retry *eligibility* is decided separately by
21
+ * `LuvioIdempotencyRetryPolicy` from the header's presence, so a stale
22
+ * registry flag can never cause an unsafe replay.
23
+ */
24
+ export declare function buildLuvioIdempotencyKeyInterceptor(): (resourceRequest: ResourceRequest) => Promise<ResourceRequest>;
@@ -1,8 +1,11 @@
1
1
  import type { LuvioRetryInterceptor } from '@salesforce/lds-network-fetch';
2
2
  /**
3
- * Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter.
3
+ * Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter. A fresh
4
+ * RetryService and policies are created per request to keep the
5
+ * mutableRequest context isolated across concurrent requests.
4
6
  *
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
+ * Policy order matters: ComposedRetryPolicy uses the first matching policy's
8
+ * shouldRetry/delay. A keyed-write 503 fails throttling's method gate, so it
9
+ * falls to the idempotency policy to replay same-key.
7
10
  */
8
11
  export declare function buildLuvioFetchRetryInterceptor(): LuvioRetryInterceptor;
@@ -19,13 +19,6 @@ export declare class LuvioCsrfTokenRetryPolicy extends RetryPolicy<FetchResponse
19
19
  calculateDelay(_result: FetchResponse<any>, _context: RetryContext<FetchResponse<any>>): Promise<number>;
20
20
  prepareRetry(_result: FetchResponse<any>, _context: RetryContext<FetchResponse<any>>): Promise<void>;
21
21
  }
22
- /**
23
- * Returns true when a Luvio FetchResponse indicates a CSRF token error.
24
- * Body is already parsed JSON on FetchResponse, so no clone/json() needed.
25
- *
26
- * UIAPI error bodies come in both shapes: an object `{ errorCode, message }`
27
- * for single errors, or an array `[{ errorCode, message }]` for endpoints that
28
- * return collections of errors. Handle both.
29
- */
22
+ /** True when a Luvio FetchResponse indicates a CSRF token error. */
30
23
  export declare function isCsrfError(response: FetchResponse<any>): boolean;
31
24
  export {};
@@ -0,0 +1,29 @@
1
+ import type { RetryContext } from '@conduit-client/service-retry/v1';
2
+ import { RetryPolicy } from '@conduit-client/service-retry/v1';
3
+ import type { FetchResponse } from '@luvio/engine';
4
+ import type { MutableLuvioRequest } from './luvio-csrf-token-retry-policy';
5
+ type LuvioIdempotencyRetryPolicyConfig = {
6
+ maxRetries: number;
7
+ };
8
+ /**
9
+ * Retry policy for writes carrying an `Idempotency-Key`. A peer of the CSRF and
10
+ * throttling policies in the composed retry chain, not a modification of
11
+ * either: its 503 (keyed-write lock-acquire) and throttling's 503 (keyless
12
+ * GET/read-POST) never contend because they key off different requests, and it
13
+ * only claims a 400/409 when the body carries an idempotency-specific
14
+ * errorCode, so it never steals CSRF's INVALID_ACCESS_TOKEN.
15
+ *
16
+ * Eligibility follows from the header's *presence* on the request, not from
17
+ * the mint interceptor's registry flag — so a stale flag can never cause an
18
+ * unsafe replay.
19
+ */
20
+ export declare class LuvioIdempotencyRetryPolicy extends RetryPolicy<FetchResponse<any>> {
21
+ private config;
22
+ private requestContext?;
23
+ constructor(config?: LuvioIdempotencyRetryPolicyConfig);
24
+ setRequestContext(context: MutableLuvioRequest): void;
25
+ shouldRetry(result: FetchResponse<any>, context: RetryContext<FetchResponse<any>>): Promise<boolean>;
26
+ calculateDelay(result: FetchResponse<any>, _context: RetryContext<FetchResponse<any>>): Promise<number>;
27
+ prepareRetry(result: FetchResponse<any>, _context: RetryContext<FetchResponse<any>>): Promise<void>;
28
+ }
29
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-runtime-one-app",
3
- "version": "1.462.0",
3
+ "version": "1.463.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "LDS engine for the one.app (LEX) runtime.",
6
6
  "main": "dist/oneAppLdsEngineCreator.js",
@@ -34,53 +34,53 @@
34
34
  "release:corejar": "yarn build && ../core-build/scripts/core.js --name=lds-runtime-one-app"
35
35
  },
36
36
  "devDependencies": {
37
- "@conduit-client/service-provisioner": "3.29.0",
38
- "@conduit-client/tools-core": "3.29.0",
39
- "@salesforce/lds-adapters-apex": "^1.462.0",
40
- "@salesforce/lds-adapters-uiapi": "^1.462.0",
41
- "@salesforce/lds-ads-bridge": "^1.462.0",
42
- "@salesforce/lds-aura-storage": "^1.462.0",
43
- "@salesforce/lds-bindings": "^1.462.0",
44
- "@salesforce/lds-instrumentation": "^1.462.0",
45
- "@salesforce/lds-network-adapter": "^1.462.0",
46
- "@salesforce/lds-network-aura": "^1.462.0",
47
- "@salesforce/lds-network-fetch": "^1.462.0",
37
+ "@conduit-client/service-provisioner": "3.30.0",
38
+ "@conduit-client/tools-core": "3.30.0",
39
+ "@salesforce/lds-adapters-apex": "^1.463.0",
40
+ "@salesforce/lds-adapters-uiapi": "^1.463.0",
41
+ "@salesforce/lds-ads-bridge": "^1.463.0",
42
+ "@salesforce/lds-aura-storage": "^1.463.0",
43
+ "@salesforce/lds-bindings": "^1.463.0",
44
+ "@salesforce/lds-instrumentation": "^1.463.0",
45
+ "@salesforce/lds-network-adapter": "^1.463.0",
46
+ "@salesforce/lds-network-aura": "^1.463.0",
47
+ "@salesforce/lds-network-fetch": "^1.463.0",
48
48
  "jwt-encode": "1.0.1"
49
49
  },
50
50
  "dependencies": {
51
- "@conduit-client/command-aura-graphql-normalized-cache-control": "3.29.0",
52
- "@conduit-client/command-aura-network": "3.29.0",
53
- "@conduit-client/command-aura-normalized-cache-control": "3.29.0",
54
- "@conduit-client/command-fetch-network": "3.29.0",
55
- "@conduit-client/command-http-graphql-normalized-cache-control": "3.29.0",
56
- "@conduit-client/command-http-normalized-cache-control": "3.29.0",
57
- "@conduit-client/command-ndjson": "3.29.0",
58
- "@conduit-client/command-network": "3.29.0",
59
- "@conduit-client/command-sse": "3.29.0",
60
- "@conduit-client/command-streaming": "3.29.0",
61
- "@conduit-client/jwt-manager": "3.29.0",
62
- "@conduit-client/service-aura-network": "3.29.0",
63
- "@conduit-client/service-bindings-imperative": "3.29.0",
64
- "@conduit-client/service-bindings-lwc": "3.29.0",
65
- "@conduit-client/service-cache": "3.29.0",
66
- "@conduit-client/service-cache-control": "3.29.0",
67
- "@conduit-client/service-cache-inclusion-policy": "3.29.0",
68
- "@conduit-client/service-config": "3.29.0",
69
- "@conduit-client/service-feature-flags": "3.29.0",
70
- "@conduit-client/service-fetch-network": "3.29.0",
71
- "@conduit-client/service-instrument-command": "3.29.0",
72
- "@conduit-client/service-pubsub": "3.29.0",
73
- "@conduit-client/service-renewable-resource-manager": "3.29.0",
74
- "@conduit-client/service-store": "3.29.0",
75
- "@conduit-client/utils": "3.29.0",
51
+ "@conduit-client/command-aura-graphql-normalized-cache-control": "3.30.0",
52
+ "@conduit-client/command-aura-network": "3.30.0",
53
+ "@conduit-client/command-aura-normalized-cache-control": "3.30.0",
54
+ "@conduit-client/command-fetch-network": "3.30.0",
55
+ "@conduit-client/command-http-graphql-normalized-cache-control": "3.30.0",
56
+ "@conduit-client/command-http-normalized-cache-control": "3.30.0",
57
+ "@conduit-client/command-ndjson": "3.30.0",
58
+ "@conduit-client/command-network": "3.30.0",
59
+ "@conduit-client/command-sse": "3.30.0",
60
+ "@conduit-client/command-streaming": "3.30.0",
61
+ "@conduit-client/jwt-manager": "3.30.0",
62
+ "@conduit-client/service-aura-network": "3.30.0",
63
+ "@conduit-client/service-bindings-imperative": "3.30.0",
64
+ "@conduit-client/service-bindings-lwc": "3.30.0",
65
+ "@conduit-client/service-cache": "3.30.0",
66
+ "@conduit-client/service-cache-control": "3.30.0",
67
+ "@conduit-client/service-cache-inclusion-policy": "3.30.0",
68
+ "@conduit-client/service-config": "3.30.0",
69
+ "@conduit-client/service-feature-flags": "3.30.0",
70
+ "@conduit-client/service-fetch-network": "3.30.0",
71
+ "@conduit-client/service-instrument-command": "3.30.0",
72
+ "@conduit-client/service-pubsub": "3.30.0",
73
+ "@conduit-client/service-renewable-resource-manager": "3.30.0",
74
+ "@conduit-client/service-store": "3.30.0",
75
+ "@conduit-client/utils": "3.30.0",
76
76
  "@luvio/network-adapter-composable": "0.161.2",
77
77
  "@luvio/network-adapter-fetch": "0.161.2",
78
78
  "@lwc/state": "^0.29.0",
79
- "@salesforce/lds-adapters-onestore-graphql": "^1.462.0",
79
+ "@salesforce/lds-adapters-onestore-graphql": "^1.463.0",
80
80
  "@salesforce/lds-adapters-uiapi-lex": "^1.415.0",
81
- "@salesforce/lds-durable-storage": "^1.462.0",
82
- "@salesforce/lds-luvio-service": "^1.462.0",
83
- "@salesforce/lds-luvio-uiapi-records-service": "^1.462.0"
81
+ "@salesforce/lds-durable-storage": "^1.463.0",
82
+ "@salesforce/lds-luvio-service": "^1.463.0",
83
+ "@salesforce/lds-luvio-uiapi-records-service": "^1.463.0"
84
84
  },
85
85
  "luvioBundlesize": [
86
86
  {