@salesforce/lds-runtime-aura 1.461.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.
@@ -14,6 +14,7 @@
14
14
  /* proxy-compat-disable */
15
15
  import { HttpStatusCode as HttpStatusCode$2, InMemoryStore, Environment, Luvio, InMemoryStoreQueryEvaluator } from 'force/luvioEngine';
16
16
  import useOneStoreGraphql from '@salesforce/gate/lds.oneStoreGraphqlEnabled.ltng';
17
+ import usePersistedQueries from '@salesforce/gate/lds.graphqlusepersistedqueries';
17
18
  import { instrument, buildRecordRepKeyFromId, configuration, InMemoryRecordRepresentationQueryEvaluator, UiApiNamespace, RecordRepresentationRepresentationType, RecordRepresentationVersion } from 'force/ldsAdaptersUiapi';
18
19
  import { getInstrumentation } from 'o11y/client';
19
20
  import { findExecutableOperation, buildGraphQLInputExtension, addTypenameToDocument } from 'force/luvioGraphqlNormalization';
@@ -24,7 +25,7 @@ import { assertIsValid, JsonSchemaViolationError, MissingRequiredPropertyError }
24
25
  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';
25
26
  import { ThirdPartyTracker } from 'instrumentation:thirdPartyTracker';
26
27
  import { markStart, markEnd, counter, registerCacheStats, perfStart, perfEnd, registerPeriodicLogger, interaction, timer, mark } from 'instrumentation/service';
27
- import { instrument as instrument$2, isCsrfMethod, getFetchMethod, isRetryableMethod, isRetryableReadPost, compilePathMatchers, setupLexNetworkAdapter, UIAPI_PUBLIC_PATHS, UIAPI_PRIVATE_PATHS, isRetryableOperationType } from 'force/ldsNetworkFetch';
28
+ import { instrument as instrument$2, isMutatingMethod, getFetchMethod, isIdempotentWrite, readUiApiErrorCode, isRetryableMethod, isRetryableReadPost, parseRetryAfterMs, compilePathMatchers, setupLexNetworkAdapter, UIAPI_PUBLIC_PATHS, UIAPI_PRIVATE_PATHS, isRetryableOperationType } from 'force/ldsNetworkFetch';
28
29
  import { REFRESH_ADAPTER_EVENT, ADAPTER_UNFULFILLED_ERROR, instrument as instrument$3 } from 'force/ldsBindings';
29
30
  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 } from 'force/ldsInstrumentation';
30
31
  import { instrument as instrument$4 } from 'force/adsBridge';
@@ -383,6 +384,15 @@ function buildServiceDescriptor$q() {
383
384
  };
384
385
  }
385
386
 
387
+ function applyOperationRequestOptions$1(init, operationType) {
388
+ if (operationType === void 0) {
389
+ return init;
390
+ }
391
+ return {
392
+ ...init,
393
+ onestore: { ...init?.onestore, retrySafe: operationType === "query" }
394
+ };
395
+ }
386
396
  class AuraNetworkCommand extends NetworkCommand$1 {
387
397
  constructor(services) {
388
398
  super(services);
@@ -401,6 +411,18 @@ class AuraNetworkCommand extends NetworkCommand$1 {
401
411
  "Fetch parameters must be specified when using HTTP transport on an Aura adapter"
402
412
  );
403
413
  }
414
+ /**
415
+ * The command's operation type. Generated command subclasses override this
416
+ * getter with their concrete operation (see the generator's
417
+ * `generateOperationType`); the base returns `undefined` for commands that
418
+ * don't declare one. On the HTTP fallback path (`fetch`) it derives the
419
+ * request's `onestore.retrySafe` — reads (`query`) are retry-safe, writes
420
+ * (`mutation`) are not. Mirrors `FetchNetworkCommand.operationType`, which
421
+ * this class does not inherit (it extends `NetworkCommand` directly).
422
+ */
423
+ get operationType() {
424
+ return void 0;
425
+ }
404
426
  coerceAuraErrors(auraErrors) {
405
427
  return new UserVisibleError(getFetchResponseFromAuraError(auraErrors[0]));
406
428
  }
@@ -502,9 +524,11 @@ class AuraNetworkCommand extends NetworkCommand$1 {
502
524
  this.coerceAuraErrors
503
525
  );
504
526
  } else if (this.shouldUseFetch()) {
505
- const params = this.fetchParams;
527
+ const [input, init] = this.fetchParams;
528
+ const initWithOptions = applyOperationRequestOptions$1(init, this.operationType);
506
529
  try {
507
- return this.convertFetchResponseToData(this.services.fetch(...params));
530
+ const fetchCall = initWithOptions === void 0 ? this.services.fetch(input) : this.services.fetch(input, initWithOptions);
531
+ return this.convertFetchResponseToData(fetchCall);
508
532
  } catch (reason) {
509
533
  return resolvedPromiseLike$2(err$1(toError(reason)));
510
534
  }
@@ -1358,6 +1382,15 @@ class NetworkCommand extends BaseCommand {
1358
1382
  async afterRequestHooks(_options) {
1359
1383
  }
1360
1384
  }
1385
+ function applyOperationRequestOptions(init, operationType) {
1386
+ if (operationType === void 0) {
1387
+ return init;
1388
+ }
1389
+ return {
1390
+ ...init,
1391
+ onestore: { ...init?.onestore, retrySafe: operationType === "query" }
1392
+ };
1393
+ }
1361
1394
  function isAbortableCommand(command) {
1362
1395
  return !!command && typeof command === "object" && "isAbortableCommand" in command && command.isAbortableCommand === true;
1363
1396
  }
@@ -1447,7 +1480,8 @@ const _FetchNetworkCommand = class _FetchNetworkCommand extends NetworkCommand {
1447
1480
  * `generateOperationType`); the base returns `undefined` for commands that
1448
1481
  * don't declare one (e.g. hand-written commands). Transport-layer
1449
1482
  * interceptors use it to tell a read (`query`) apart from a write — see
1450
- * `fetch`, which folds it into the context seed.
1483
+ * `fetch`, which folds it into the context seed and derives
1484
+ * `onestore.retrySafe` from it.
1451
1485
  */
1452
1486
  get operationType() {
1453
1487
  return void 0;
@@ -1456,8 +1490,9 @@ const _FetchNetworkCommand = class _FetchNetworkCommand extends NetworkCommand {
1456
1490
  try {
1457
1491
  const [input, init] = this.fetchParams;
1458
1492
  const operationType = this.operationType;
1493
+ const initWithOptions = applyOperationRequestOptions(init, operationType);
1459
1494
  const seed = operationType === void 0 ? contextSeed : { operationType, ...contextSeed };
1460
- const initWithSeed = seed === void 0 ? init : { ...init, __contextSeed: seed };
1495
+ const initWithSeed = seed === void 0 ? initWithOptions : { ...initWithOptions, __contextSeed: seed };
1461
1496
  const fetchCall = initWithSeed === void 0 ? this.services.fetch(input) : this.services.fetch(input, initWithSeed);
1462
1497
  return this.convertFetchResponseToData(fetchCall);
1463
1498
  } catch (reason) {
@@ -2596,7 +2631,7 @@ function buildServiceDescriptor$d(luvio) {
2596
2631
  },
2597
2632
  };
2598
2633
  }
2599
- // version: 1.461.0-37b1c9d1cb
2634
+ // version: 1.463.0-ea9ecd17fc
2600
2635
 
2601
2636
  class AuraGraphQLNormalizedCacheControlCommand extends AuraNormalizedCacheControlCommand {
2602
2637
  constructor(config, documentRootType, services) {
@@ -2935,7 +2970,7 @@ function buildServiceDescriptor$9(notifyRecordUpdateAvailable, getNormalizedLuvi
2935
2970
  },
2936
2971
  };
2937
2972
  }
2938
- // version: 1.461.0-37b1c9d1cb
2973
+ // version: 1.463.0-ea9ecd17fc
2939
2974
 
2940
2975
  class RetryService {
2941
2976
  constructor(defaultRetryPolicy) {
@@ -4539,10 +4574,14 @@ function getTextEncoder() {
4539
4574
  }
4540
4575
  return textEncoder;
4541
4576
  }
4542
- function buildCompressionInterceptor(config) {
4543
- const threshold = config.threshold ?? 1024;
4577
+ function buildCompressionInterceptor(defaultConfig) {
4544
4578
  return async (args) => {
4545
4579
  const [resource, options = {}] = args;
4580
+ const config = options.onestore?.compression ?? defaultConfig;
4581
+ if (config === void 0) {
4582
+ return resolvedPromiseLike$2(args);
4583
+ }
4584
+ const threshold = config.threshold ?? 1024;
4546
4585
  if (!options.body || typeof options.body !== "string") {
4547
4586
  return resolvedPromiseLike$2(args);
4548
4587
  }
@@ -5941,7 +5980,7 @@ function getEnvironmentSetting(name) {
5941
5980
  }
5942
5981
  return undefined;
5943
5982
  }
5944
- // version: 1.461.0-55ae54e753
5983
+ // version: 1.463.0-8515018faf
5945
5984
 
5946
5985
  const auraClientService = getAuraClientService();
5947
5986
  const defaultConfig = {
@@ -6519,7 +6558,7 @@ function buildCsrfTokenInterceptor() {
6519
6558
  return resolvedPromiseLike$2(fetchArgs);
6520
6559
  }
6521
6560
  // Only add CSRF token for mutating operations
6522
- if (isCsrfMethod(getFetchMethod(fetchArgs))) {
6561
+ if (isMutatingMethod(getFetchMethod(fetchArgs))) {
6523
6562
  const token = await getCsrfToken();
6524
6563
  if (token) {
6525
6564
  // eslint-disable-next-line no-param-reassign
@@ -6547,7 +6586,7 @@ function buildLuvioCsrfTokenInterceptor() {
6547
6586
  resourceRequest.headers = {};
6548
6587
  }
6549
6588
  // Only add CSRF token for mutating operations
6550
- if (isCsrfMethod(resourceRequest.method)) {
6589
+ if (isMutatingMethod(resourceRequest.method)) {
6551
6590
  // Don't overwrite existing CSRF token header if it already exists
6552
6591
  if (!resourceRequest.headers[CSRF_TOKEN_HEADER]) {
6553
6592
  const token = await getCsrfToken();
@@ -6560,6 +6599,53 @@ function buildLuvioCsrfTokenInterceptor() {
6560
6599
  };
6561
6600
  }
6562
6601
 
6602
+ const IDEMPOTENCY_KEY_HEADER = 'Idempotency-Key';
6603
+ // Server-side idempotency error codes (UI SDK / UI-API `ApiExceptionCode`); the
6604
+ // mobile draft-queue handler is the reference consumer.
6605
+ const ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED = 'IDEMPOTENCY_FEATURE_NOT_ENABLED';
6606
+ const ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED = 'IDEMPOTENCY_NOT_SUPPORTED';
6607
+ const ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR = 'IDEMPOTENCY_BACKEND_OPERATION_ERROR';
6608
+ const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = 'IDEMPOTENCY_KEY_USED_DIFFERENT_USER';
6609
+ const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = 'IDEMPOTENCY_CONCURRENT_REQUEST';
6610
+ const ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED = 'IDEMPOTENCY_KEY_ALREADY_USED';
6611
+ /**
6612
+ * Generates an RFC4122 v4 UUID for use as an idempotency key, preferring
6613
+ * `crypto.randomUUID()` and falling back to a `Math.random()` builder if it is
6614
+ * unavailable (matches the SFAP request-id approach in `network-sfap`).
6615
+ */
6616
+ function mintIdempotencyKey() {
6617
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
6618
+ return crypto.randomUUID();
6619
+ }
6620
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
6621
+ const r = (Math.random() * 16) | 0;
6622
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
6623
+ return v.toString(16);
6624
+ });
6625
+ }
6626
+ /**
6627
+ * Builds a Luvio request interceptor that mints and attaches an `Idempotency-Key`
6628
+ * on mutations to onboarded endpoints, once per logical write, so the retry
6629
+ * service re-sends the same key on every replay and the server can dedupe.
6630
+ *
6631
+ * Minting is gated here; retry *eligibility* is decided separately by
6632
+ * `LuvioIdempotencyRetryPolicy` from the header's presence, so a stale
6633
+ * registry flag can never cause an unsafe replay.
6634
+ */
6635
+ function buildLuvioIdempotencyKeyInterceptor() {
6636
+ return async (resourceRequest) => {
6637
+ if (!resourceRequest.headers) {
6638
+ resourceRequest.headers = {};
6639
+ }
6640
+ if (isMutatingMethod(resourceRequest.method) &&
6641
+ isIdempotentWrite(resourceRequest.basePath) &&
6642
+ !resourceRequest.headers[IDEMPOTENCY_KEY_HEADER]) {
6643
+ resourceRequest.headers[IDEMPOTENCY_KEY_HEADER] = mintIdempotencyKey();
6644
+ }
6645
+ return resolvedPromiseLike$2(resourceRequest);
6646
+ };
6647
+ }
6648
+
6563
6649
  const ENTITY_ENCODING_HEADER = 'X-Chatter-Entity-Encoding';
6564
6650
  const ENTITY_ENCODING_DISABLED_VALUE = 'false';
6565
6651
  /**
@@ -6856,11 +6942,11 @@ function buildLuvioThirdPartyTrackerFinishInterceptor() {
6856
6942
  };
6857
6943
  }
6858
6944
 
6859
- const DEFAULT_CONFIG$3 = {
6945
+ const DEFAULT_CONFIG$4 = {
6860
6946
  maxRetries: 1,
6861
6947
  };
6862
6948
  class LuvioCsrfTokenRetryPolicy extends RetryPolicy {
6863
- constructor(config = DEFAULT_CONFIG$3) {
6949
+ constructor(config = DEFAULT_CONFIG$4) {
6864
6950
  super();
6865
6951
  this.config = config;
6866
6952
  }
@@ -6904,21 +6990,12 @@ class LuvioCsrfTokenRetryPolicy extends RetryPolicy {
6904
6990
  this.requestContext.request = { ...req, headers };
6905
6991
  }
6906
6992
  }
6907
- /**
6908
- * Returns true when a Luvio FetchResponse indicates a CSRF token error.
6909
- * Body is already parsed JSON on FetchResponse, so no clone/json() needed.
6910
- *
6911
- * UIAPI error bodies come in both shapes: an object `{ errorCode, message }`
6912
- * for single errors, or an array `[{ errorCode, message }]` for endpoints that
6913
- * return collections of errors. Handle both.
6914
- */
6993
+ /** True when a Luvio FetchResponse indicates a CSRF token error. */
6915
6994
  function isCsrfError$1(response) {
6916
- const body = response.body;
6917
- const errorCode = Array.isArray(body) ? body[0]?.errorCode : body?.errorCode;
6918
- return errorCode === 'INVALID_ACCESS_TOKEN';
6995
+ return readUiApiErrorCode(response) === 'INVALID_ACCESS_TOKEN';
6919
6996
  }
6920
6997
 
6921
- const DEFAULT_CONFIG$2 = {
6998
+ const DEFAULT_CONFIG$3 = {
6922
6999
  maxRetries: 3,
6923
7000
  maxTimeToRetry: 10000,
6924
7001
  baseDelay: 250,
@@ -6932,7 +7009,7 @@ class LuvioFetchThrottlingRetryPolicy extends RetryPolicy {
6932
7009
  // is why we take the plain `ResourceRequest` rather than the mutable-container
6933
7010
  // pattern the CSRF policy uses — that policy rewrites the request (new token)
6934
7011
  // between attempts; this one never mutates it.
6935
- constructor(config = DEFAULT_CONFIG$2, request) {
7012
+ constructor(config = DEFAULT_CONFIG$3, request) {
6936
7013
  super();
6937
7014
  this.config = config;
6938
7015
  this.request = request;
@@ -6960,57 +7037,157 @@ class LuvioFetchThrottlingRetryPolicy extends RetryPolicy {
6960
7037
  return Math.max(0, delay + jitter);
6961
7038
  }
6962
7039
  parseRetryAfterHeader(result) {
6963
- const headers = result.headers;
6964
- if (!headers) {
6965
- return undefined;
6966
- }
6967
- // FetchResponse headers is a plain { [key: string]: string } object
6968
- // (no Headers.get available), so do a case-insensitive lookup manually.
6969
- let value;
6970
- for (const key in headers) {
6971
- if (key.toLowerCase() === 'retry-after') {
6972
- value = headers[key];
6973
- break;
7040
+ return parseRetryAfterMs(result.headers);
7041
+ }
7042
+ }
7043
+
7044
+ const DEFAULT_CONFIG$2 = {
7045
+ maxRetries: 3,
7046
+ };
7047
+ // The idempotency handler never emits 429 (that's throttling's domain). Its 503
7048
+ // is a keyed-write lock-acquire failure (scenario 7), not throttling's keyless
7049
+ // GET/read-POST 503 — the two never contend (see classifyResponse).
7050
+ const HTTP_STATUS_BAD_REQUEST = 400;
7051
+ const HTTP_STATUS_REQUEST_TIMEOUT = 408;
7052
+ const HTTP_STATUS_CONFLICT = 409;
7053
+ const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422;
7054
+ const HTTP_STATUS_SERVER_ERROR = 500;
7055
+ const HTTP_STATUS_SERVICE_UNAVAILABLE = 503;
7056
+ // Terminal cases (IDEMPOTENCY_AUTHENTICATION_FAILED on 401, INVALID_IDEMPOTENCY_KEY
7057
+ // on 400) are deliberately absent so they fall through to 'none'.
7058
+ const STRIP_ERROR_CODES = [
7059
+ ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED,
7060
+ ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED,
7061
+ ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR,
7062
+ ];
7063
+ const ROTATE_ERROR_CODES = [
7064
+ ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER,
7065
+ ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED,
7066
+ ];
7067
+ /**
7068
+ * Maps an idempotent-write response to its disposition. Status alone is
7069
+ * ambiguous (e.g. 409 is same-key for a concurrent request but rotate for a
7070
+ * different user), so pair it with the body's errorCode.
7071
+ */
7072
+ function classifyResponse(response) {
7073
+ const errorCode = readUiApiErrorCode(response);
7074
+ // FetchResponse.status is typed as HttpStatusCode, which omits 408/409/422
7075
+ // even though the idempotency handler emits them; widen to match.
7076
+ const status = response.status;
7077
+ switch (status) {
7078
+ case HTTP_STATUS_SERVER_ERROR:
7079
+ return errorCode === ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR ? 'strip' : 'none';
7080
+ case HTTP_STATUS_BAD_REQUEST:
7081
+ // FEATURE_NOT_ENABLED / NOT_SUPPORTED → strip. Any other 400 (incl.
7082
+ // INVALID_IDEMPOTENCY_KEY and CSRF's INVALID_ACCESS_TOKEN) is terminal.
7083
+ return errorCode !== undefined && STRIP_ERROR_CODES.includes(errorCode)
7084
+ ? 'strip'
7085
+ : 'none';
7086
+ case HTTP_STATUS_REQUEST_TIMEOUT:
7087
+ // Backend connection timeout — the write may have already committed;
7088
+ // replay same-key so the server can dedupe.
7089
+ return 'same-key';
7090
+ case HTTP_STATUS_SERVICE_UNAVAILABLE:
7091
+ // Lock-acquire failure (scenario 7): the write never committed. Only a
7092
+ // keyed write reaches here (shouldRetry gates on key presence) — a
7093
+ // keyless 503 is the throttling policy's GET/read-POST case.
7094
+ return 'same-key';
7095
+ case HTTP_STATUS_CONFLICT:
7096
+ // KEY_USED_DIFFERENT_USER → rotate; CONCURRENT_REQUEST → same key.
7097
+ if (errorCode !== undefined && ROTATE_ERROR_CODES.includes(errorCode)) {
7098
+ return 'rotate';
6974
7099
  }
7100
+ return errorCode === ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST ? 'same-key' : 'none';
7101
+ case HTTP_STATUS_UNPROCESSABLE_ENTITY:
7102
+ // Same key reused for a different request body → needs a new key.
7103
+ return errorCode === ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED ? 'rotate' : 'none';
7104
+ default:
7105
+ return 'none';
7106
+ }
7107
+ }
7108
+ /**
7109
+ * Retry policy for writes carrying an `Idempotency-Key`. A peer of the CSRF and
7110
+ * throttling policies in the composed retry chain, not a modification of
7111
+ * either: its 503 (keyed-write lock-acquire) and throttling's 503 (keyless
7112
+ * GET/read-POST) never contend because they key off different requests, and it
7113
+ * only claims a 400/409 when the body carries an idempotency-specific
7114
+ * errorCode, so it never steals CSRF's INVALID_ACCESS_TOKEN.
7115
+ *
7116
+ * Eligibility follows from the header's *presence* on the request, not from
7117
+ * the mint interceptor's registry flag — so a stale flag can never cause an
7118
+ * unsafe replay.
7119
+ */
7120
+ class LuvioIdempotencyRetryPolicy extends RetryPolicy {
7121
+ constructor(config = DEFAULT_CONFIG$2) {
7122
+ super();
7123
+ this.config = config;
7124
+ }
7125
+ setRequestContext(context) {
7126
+ this.requestContext = context;
7127
+ }
7128
+ async shouldRetry(result, context) {
7129
+ if (context.attempt >= this.config.maxRetries) {
7130
+ return false;
6975
7131
  }
6976
- if (!value) {
6977
- return undefined;
7132
+ const headers = this.requestContext?.request.headers;
7133
+ if (!headers || !headers[IDEMPOTENCY_KEY_HEADER]) {
7134
+ return false;
6978
7135
  }
6979
- const trimmed = value.trim();
6980
- if (/^\d+$/.test(trimmed)) {
6981
- const seconds = Number(trimmed);
6982
- if (Number.isFinite(seconds) && seconds >= 0) {
6983
- return seconds * 1000;
6984
- }
6985
- return undefined;
7136
+ return classifyResponse(result) !== 'none';
7137
+ }
7138
+ async calculateDelay(result, _context) {
7139
+ // Only same-key replays (408/409/503) carry a Retry-After to honor;
7140
+ // strip/rotate retry immediately.
7141
+ if (classifyResponse(result) !== 'same-key') {
7142
+ return 0;
6986
7143
  }
6987
- 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$/;
6988
- if (!IMF_FIXDATE.test(trimmed)) {
6989
- return undefined;
7144
+ // Honor the server's Retry-After in full, like the mobile handler — it's
7145
+ // a lock-hold hint, not a client-owned backoff (unlike the throttling
7146
+ // sibling's capped delay). maxRetries bounds replays instead.
7147
+ return parseRetryAfterMs(result.headers) ?? 0;
7148
+ }
7149
+ async prepareRetry(result, _context) {
7150
+ if (!this.requestContext) {
7151
+ return;
6990
7152
  }
6991
- const millis = Date.parse(trimmed);
6992
- if (Number.isFinite(millis)) {
6993
- return Math.max(0, millis - Date.now());
7153
+ const disposition = classifyResponse(result);
7154
+ if (disposition === 'same-key' || disposition === 'none') {
7155
+ return;
6994
7156
  }
6995
- return undefined;
7157
+ const req = this.requestContext.request;
7158
+ // Drop the existing key via immutable spread (matches the CSRF policy).
7159
+ const { [IDEMPOTENCY_KEY_HEADER]: _stale, ...remainingHeaders } = req.headers ?? {};
7160
+ const headers = disposition === 'rotate'
7161
+ ? { ...remainingHeaders, [IDEMPOTENCY_KEY_HEADER]: mintIdempotencyKey() }
7162
+ : // 'strip': retry plain — the feature is unavailable for this request.
7163
+ remainingHeaders;
7164
+ this.requestContext.request = { ...req, headers };
6996
7165
  }
6997
7166
  }
6998
7167
 
6999
7168
  /**
7000
- * Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter.
7169
+ * Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter. A fresh
7170
+ * RetryService and policies are created per request to keep the
7171
+ * mutableRequest context isolated across concurrent requests.
7001
7172
  *
7002
- * A fresh RetryService and policy instances are created per request to keep the
7003
- * CSRF policy's mutableRequest context isolated from concurrent requests.
7173
+ * Policy order matters: ComposedRetryPolicy uses the first matching policy's
7174
+ * shouldRetry/delay. A keyed-write 503 fails throttling's method gate, so it
7175
+ * falls to the idempotency policy to replay same-key.
7004
7176
  */
7005
7177
  function buildLuvioFetchRetryInterceptor() {
7006
7178
  return (request, doFetch) => {
7179
+ const idempotencyPolicy = new LuvioIdempotencyRetryPolicy();
7007
7180
  const csrfPolicy = new LuvioCsrfTokenRetryPolicy();
7008
7181
  const composedPolicy = new ComposedRetryPolicy([
7182
+ idempotencyPolicy,
7009
7183
  new LuvioFetchThrottlingRetryPolicy(undefined, request),
7010
7184
  csrfPolicy,
7011
7185
  ]);
7012
7186
  const retryService = new RetryService(composedPolicy);
7013
7187
  const mutableRequest = { request };
7188
+ // Both policies rewrite headers on the shared request between attempts
7189
+ // (idempotency strips/rotates the key; CSRF refreshes the token).
7190
+ idempotencyPolicy.setRequestContext(mutableRequest);
7014
7191
  csrfPolicy.setRequestContext(mutableRequest);
7015
7192
  return retryService.applyRetry(() => doFetch(mutableRequest.request));
7016
7193
  };
@@ -7244,6 +7421,7 @@ const composedFetchNetworkAdapter = {
7244
7421
  buildLuvioTransportMarksSendInterceptor(),
7245
7422
  buildLuvioPageScopedCacheRequestInterceptor(),
7246
7423
  buildLuvioCsrfTokenInterceptor(),
7424
+ buildLuvioIdempotencyKeyInterceptor(),
7247
7425
  buildLuvioFirstPartyHeaderInterceptor(),
7248
7426
  buildLuvioEntityEncodingInterceptor(),
7249
7427
  ],
@@ -9171,6 +9349,8 @@ function initializeOneStore(luvio) {
9171
9349
  // across LEX and sites. Paired with the HTTP UIAPI fetch path being
9172
9350
  // disabled on sites (see network-fetch.ts).
9173
9351
  featureFlagsService.set('graphQLUseAura', true);
9352
+ // Persisted (named) GraphQL queries; no consumers yet (W-23971639).
9353
+ featureFlagsService.set('graphQLUsePersistedQueries', usePersistedQueries.isOpen({ fallback: false }));
9174
9354
  const services = [
9175
9355
  instrumentationServiceDescriptor,
9176
9356
  buildLexRuntimeDefaultFetchServiceDescriptor(loggerService, retryService),
@@ -9237,4 +9417,4 @@ function ldsEngineCreator() {
9237
9417
  }
9238
9418
 
9239
9419
  export { PDL_ENGINE_REGISTRATION_ID, buildPredictorForContext, configService, ldsEngineCreator as default, initializeLDS, initializeOneStore, notifyUpdateAvailableFactory };
9240
- // version: 1.461.0-37b1c9d1cb
9420
+ // 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-aura",
3
- "version": "1.461.0",
3
+ "version": "1.463.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "LDS engine for Aura runtime.",
6
6
  "main": "dist/ldsEngineCreator.js",
@@ -34,53 +34,53 @@
34
34
  "release:corejar": "yarn build && ../core-build/scripts/core.js --name=lds-runtime-aura"
35
35
  },
36
36
  "devDependencies": {
37
- "@conduit-client/service-provisioner": "3.28.0",
38
- "@conduit-client/tools-core": "3.28.0",
39
- "@salesforce/lds-adapters-apex": "^1.461.0",
40
- "@salesforce/lds-adapters-uiapi": "^1.461.0",
41
- "@salesforce/lds-ads-bridge": "^1.461.0",
42
- "@salesforce/lds-aura-storage": "^1.461.0",
43
- "@salesforce/lds-bindings": "^1.461.0",
44
- "@salesforce/lds-instrumentation": "^1.461.0",
45
- "@salesforce/lds-network-adapter": "^1.461.0",
46
- "@salesforce/lds-network-aura": "^1.461.0",
47
- "@salesforce/lds-network-fetch": "^1.461.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.28.0",
52
- "@conduit-client/command-aura-network": "3.28.0",
53
- "@conduit-client/command-aura-normalized-cache-control": "3.28.0",
54
- "@conduit-client/command-fetch-network": "3.28.0",
55
- "@conduit-client/command-http-graphql-normalized-cache-control": "3.28.0",
56
- "@conduit-client/command-http-normalized-cache-control": "3.28.0",
57
- "@conduit-client/command-ndjson": "3.28.0",
58
- "@conduit-client/command-network": "3.28.0",
59
- "@conduit-client/command-sse": "3.28.0",
60
- "@conduit-client/command-streaming": "3.28.0",
61
- "@conduit-client/jwt-manager": "3.28.0",
62
- "@conduit-client/service-aura-network": "3.28.0",
63
- "@conduit-client/service-bindings-imperative": "3.28.0",
64
- "@conduit-client/service-bindings-lwc": "3.28.0",
65
- "@conduit-client/service-cache": "3.28.0",
66
- "@conduit-client/service-cache-control": "3.28.0",
67
- "@conduit-client/service-cache-inclusion-policy": "3.28.0",
68
- "@conduit-client/service-config": "3.28.0",
69
- "@conduit-client/service-feature-flags": "3.28.0",
70
- "@conduit-client/service-fetch-network": "3.28.0",
71
- "@conduit-client/service-instrument-command": "3.28.0",
72
- "@conduit-client/service-pubsub": "3.28.0",
73
- "@conduit-client/service-renewable-resource-manager": "3.28.0",
74
- "@conduit-client/service-store": "3.28.0",
75
- "@conduit-client/utils": "3.28.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.461.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.461.0",
82
- "@salesforce/lds-luvio-service": "^1.461.0",
83
- "@salesforce/lds-luvio-uiapi-records-service": "^1.461.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
  {