@salesforce/lds-network-fetch 1.462.0 → 1.464.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.
@@ -850,7 +850,15 @@ async function handleSendFetchRequest(resourceRequest, resourceRequestContext, i
850
850
  // the intial then here can also most likely be moved to a response interceptor as well
851
851
  interceptedResponse
852
852
  .then((value) => {
853
- if (storageAndLogger !== null && storageAndLogger.storage !== null) {
853
+ // Only persist successful responses. fetchNetworkAdapter resolves (does not reject)
854
+ // for 4xx/5xx and returns null/text bodies for 204/non-JSON, and a durable cache hit
855
+ // is replayed as a synthetic 200 (see createOkResponse above), so caching a failure
856
+ // body would later be ingested as a successful response.
857
+ if (storageAndLogger !== null &&
858
+ storageAndLogger.storage !== null &&
859
+ value.ok !== false &&
860
+ value.body !== null &&
861
+ value.body !== undefined) {
854
862
  storageAndLogger.storage.set(transactionKey, value.body).catch((_error) => { });
855
863
  }
856
864
  internalRequestLogger.resolve(resourceRequest, value);
@@ -909,13 +917,11 @@ function instrument(newInstrumentation) {
909
917
  }
910
918
 
911
919
  /**
912
- * Canonical registry of the UIAPI url paths LDS sends over HTTP and their
913
- * read/write nature — the single source of truth for every runtime's transport
914
- * allowlist and read-POST retry classifier. A leaf module (no local imports) so
915
- * both can consume it without an import cycle: `network-fetch.ts` imports the
916
- * retry interceptor, so the retry layer must import paths from here, not back
917
- * from `network-fetch.ts`. Templates use `{segment}` placeholders (see
918
- * `compilePathMatchers`); callers normalize trailing slashes.
920
+ * Canonical registry of UIAPI url paths LDS sends over HTTP, and their
921
+ * read/write nature — shared by every runtime's transport allowlist and
922
+ * read-POST retry classifier. A leaf module (no local imports), since
923
+ * `network-fetch.ts` imports the retry interceptor, which must import paths
924
+ * from here rather than back from `network-fetch.ts`.
919
925
  */
920
926
  /**
921
927
  * Public UIAPI paths verified for HTTP transport: no `UiTier` Connect
@@ -933,10 +939,13 @@ const UIAPI_PUBLIC_PATH_ENTRIES = [
933
939
  // getPicklistValuesByRecordType — IPicklistValuesByRecordTypeResource
934
940
  { path: '/ui-api/object-info/{objectApiName}/picklist-values/{recordTypeId}' },
935
941
  // ----- Records -----
936
- // createRecord (POST) — IRecordDataCollectionResource
937
- { path: '/ui-api/records' },
938
- // getRecord / updateRecord (PATCH) / deleteRecord (DELETE) — IRecordDataResource
939
- { path: '/ui-api/records/{recordId}' },
942
+ // createRecord (POST) — IRecordDataCollectionResource.
943
+ // Onboarded to the server Idempotency-Key feature: the interceptor mints a
944
+ // key for the POST so a throttled create can replay safely (dedupe by key).
945
+ { path: '/ui-api/records', idempotentWrite: true },
946
+ // getRecord / updateRecord (PATCH) / deleteRecord (DELETE) — IRecordDataResource.
947
+ // Onboarded to the server Idempotency-Key feature (PATCH/DELETE writes).
948
+ { path: '/ui-api/records/{recordId}', idempotentWrite: true },
940
949
  // getRecords (batch GET) — IBatchRecordDataResource
941
950
  { path: '/ui-api/records/batch/{recordIds}' },
942
951
  // executeBatchRecordOperations (POST, no recordIds segment) — IBatchRecordResource
@@ -1127,6 +1136,34 @@ function isRetryableReadPost(basePath) {
1127
1136
  }
1128
1137
  return RETRYABLE_READ_POST_MATCHERS.some((matcher) => matcher.test(path));
1129
1138
  }
1139
+ // The idempotent-write subset, precompiled once at module load from the same
1140
+ // entries so a flagged path is always also a registered transport path.
1141
+ const IDEMPOTENT_WRITE_MATCHERS = compilePathMatchers([...UIAPI_PUBLIC_PATH_ENTRIES, ...UIAPI_PRIVATE_PATH_ENTRIES]
1142
+ .filter((entry) => entry.idempotentWrite)
1143
+ .map((entry) => entry.path));
1144
+ // Non-flagged templates that structurally collide with a flagged one (e.g.
1145
+ // `/ui-api/records/batch` vs `/ui-api/records/{recordId}`) — a match here means
1146
+ // the concrete path is the sibling, not an idempotent write. Derived from the
1147
+ // registry so a new sibling can't silently inherit minting.
1148
+ const IDEMPOTENT_WRITE_EXCLUSION_MATCHERS = compilePathMatchers([...UIAPI_PUBLIC_PATH_ENTRIES, ...UIAPI_PRIVATE_PATH_ENTRIES]
1149
+ .filter((entry) => !entry.idempotentWrite)
1150
+ .map((entry) => entry.path));
1151
+ /**
1152
+ * True when the request path is onboarded to the Idempotency-Key feature.
1153
+ * Gates minting only; must match a flagged template and not a more-specific
1154
+ * non-flagged sibling (see IDEMPOTENT_WRITE_EXCLUSION_MATCHERS).
1155
+ */
1156
+ function isIdempotentWrite(basePath) {
1157
+ if (!basePath) {
1158
+ return false;
1159
+ }
1160
+ let path = basePath.trim();
1161
+ if (path.endsWith('/')) {
1162
+ path = path.substring(0, path.length - 1);
1163
+ }
1164
+ return (IDEMPOTENT_WRITE_MATCHERS.some((matcher) => matcher.test(path)) &&
1165
+ !IDEMPOTENT_WRITE_EXCLUSION_MATCHERS.some((matcher) => matcher.test(path)));
1166
+ }
1130
1167
 
1131
1168
  /**
1132
1169
  * Extracts the HTTP method from FetchParameters, matching the Fetch API's own
@@ -1165,10 +1202,11 @@ function isRetryableOperationType(operationType) {
1165
1202
  return operationType === 'query';
1166
1203
  }
1167
1204
  /**
1168
- * Determines if the HTTP method requires CSRF protection.
1169
- * Only mutating operations (POST, PUT, PATCH, DELETE) require CSRF tokens.
1205
+ * True for a mutating HTTP method (POST/PUT/PATCH/DELETE) — gates both CSRF
1206
+ * token attachment and Idempotency-Key minting, since both apply to the same
1207
+ * set of methods.
1170
1208
  */
1171
- function isCsrfMethod(method) {
1209
+ function isMutatingMethod(method) {
1172
1210
  if (!method) {
1173
1211
  return false;
1174
1212
  }
@@ -1178,6 +1216,48 @@ function isCsrfMethod(method) {
1178
1216
  normalizedMethod === 'patch' ||
1179
1217
  normalizedMethod === 'delete');
1180
1218
  }
1219
+ const HTTP_HEADER_RETRY_AFTER = 'retry-after';
1220
+ 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$/;
1221
+ /**
1222
+ * Parses a `Retry-After` header (case-insensitive lookup) into milliseconds.
1223
+ * Accepts fractional delay-seconds — the idempotency handler and the mobile
1224
+ * reference parser both emit them — or an IMF-fixdate. `Number.isFinite`
1225
+ * rejects `Infinity`/`-Infinity`, which otherwise round-trip cleanly through
1226
+ * `parseFloat` and would produce a non-finite delay.
1227
+ */
1228
+ function parseRetryAfterMs(headers) {
1229
+ if (!headers) {
1230
+ return undefined;
1231
+ }
1232
+ let value;
1233
+ for (const key in headers) {
1234
+ if (key.toLowerCase() === HTTP_HEADER_RETRY_AFTER) {
1235
+ value = headers[key];
1236
+ break;
1237
+ }
1238
+ }
1239
+ if (!value) {
1240
+ return undefined;
1241
+ }
1242
+ const trimmed = value.trim();
1243
+ const seconds = parseFloat(trimmed);
1244
+ if (Number.isFinite(seconds) && String(seconds) === trimmed) {
1245
+ return seconds >= 0 ? Math.round(seconds * 1000) : undefined;
1246
+ }
1247
+ if (!IMF_FIXDATE.test(trimmed)) {
1248
+ return undefined;
1249
+ }
1250
+ const millis = Date.parse(trimmed);
1251
+ return Number.isFinite(millis) ? Math.max(0, millis - Date.now()) : undefined;
1252
+ }
1253
+ /**
1254
+ * Reads the `errorCode` off a UiApiError body. UIAPI returns either a single
1255
+ * `{ errorCode, message }` object or a `[{ errorCode }]` array; handle both.
1256
+ */
1257
+ function readUiApiErrorCode(response) {
1258
+ const body = response.body;
1259
+ return Array.isArray(body) ? body[0]?.errorCode : body?.errorCode;
1260
+ }
1181
1261
 
1182
- export { UIAPI_PRIVATE_PATHS, UIAPI_PUBLIC_PATHS, compilePathMatchers, getFetchMethod, instrument, isCsrfMethod, isRetryableMethod, isRetryableOperationType, isRetryableReadPost, setupFetchNetworkAdapter, setupLexNetworkAdapter };
1183
- // version: 1.462.0-50d9a13ba4
1262
+ export { UIAPI_PRIVATE_PATHS, UIAPI_PUBLIC_PATHS, compilePathMatchers, getFetchMethod, instrument, isIdempotentWrite, isMutatingMethod, isRetryableMethod, isRetryableOperationType, isRetryableReadPost, parseRetryAfterMs, readUiApiErrorCode, setupFetchNetworkAdapter, setupLexNetworkAdapter };
1263
+ // version: 1.464.0-e54c238397
@@ -1,4 +1,5 @@
1
1
  import type { FetchParameters } from '@conduit-client/service-fetch-network/v1';
2
+ import type { FetchResponse } from '@luvio/engine';
2
3
  /**
3
4
  * Extracts the HTTP method from FetchParameters, matching the Fetch API's own
4
5
  * precedence: an options.method override wins over a Request object's method
@@ -24,7 +25,21 @@ export declare function isRetryableMethod(method: string | undefined): boolean;
24
25
  */
25
26
  export declare function isRetryableOperationType(operationType: string | undefined): boolean;
26
27
  /**
27
- * Determines if the HTTP method requires CSRF protection.
28
- * Only mutating operations (POST, PUT, PATCH, DELETE) require CSRF tokens.
28
+ * True for a mutating HTTP method (POST/PUT/PATCH/DELETE) — gates both CSRF
29
+ * token attachment and Idempotency-Key minting, since both apply to the same
30
+ * set of methods.
29
31
  */
30
- export declare function isCsrfMethod(method: string | undefined): boolean;
32
+ export declare function isMutatingMethod(method: string | undefined): boolean;
33
+ /**
34
+ * Parses a `Retry-After` header (case-insensitive lookup) into milliseconds.
35
+ * Accepts fractional delay-seconds — the idempotency handler and the mobile
36
+ * reference parser both emit them — or an IMF-fixdate. `Number.isFinite`
37
+ * rejects `Infinity`/`-Infinity`, which otherwise round-trip cleanly through
38
+ * `parseFloat` and would produce a non-finite delay.
39
+ */
40
+ export declare function parseRetryAfterMs(headers: Record<string, string> | undefined): number | undefined;
41
+ /**
42
+ * Reads the `errorCode` off a UiApiError body. UIAPI returns either a single
43
+ * `{ errorCode, message }` object or a `[{ errorCode }]` array; handle both.
44
+ */
45
+ export declare function readUiApiErrorCode(response: FetchResponse<any>): string | undefined;
@@ -2,5 +2,5 @@ export { setupFetchNetworkAdapter, setupLexNetworkAdapter } from './networkAdapt
2
2
  export type { RequestLogger } from './networkAdapter';
3
3
  export type { RequestInterceptor, ResponseInterceptor, LuvioRetryInterceptor, Interceptors, } from './utils';
4
4
  export { instrument, FetchNetworkInstrumentation } from './instrumentation';
5
- export { UIAPI_PUBLIC_PATHS, UIAPI_PRIVATE_PATHS, compilePathMatchers, isRetryableReadPost, } from './uiapi-http-paths';
6
- export { getFetchMethod, isRetryableMethod, isRetryableOperationType, isCsrfMethod, } from './http-utils';
5
+ export { UIAPI_PUBLIC_PATHS, UIAPI_PRIVATE_PATHS, compilePathMatchers, isRetryableReadPost, isIdempotentWrite, } from './uiapi-http-paths';
6
+ export { getFetchMethod, isRetryableMethod, isRetryableOperationType, isMutatingMethod, parseRetryAfterMs, readUiApiErrorCode, } from './http-utils';
@@ -1,11 +1,9 @@
1
1
  /**
2
- * Canonical registry of the UIAPI url paths LDS sends over HTTP and their
3
- * read/write nature — the single source of truth for every runtime's transport
4
- * allowlist and read-POST retry classifier. A leaf module (no local imports) so
5
- * both can consume it without an import cycle: `network-fetch.ts` imports the
6
- * retry interceptor, so the retry layer must import paths from here, not back
7
- * from `network-fetch.ts`. Templates use `{segment}` placeholders (see
8
- * `compilePathMatchers`); callers normalize trailing slashes.
2
+ * Canonical registry of UIAPI url paths LDS sends over HTTP, and their
3
+ * read/write nature — shared by every runtime's transport allowlist and
4
+ * read-POST retry classifier. A leaf module (no local imports), since
5
+ * `network-fetch.ts` imports the retry interceptor, which must import paths
6
+ * from here rather than back from `network-fetch.ts`.
9
7
  */
10
8
  /** Public transport paths, derived from the canonical registry above. */
11
9
  export declare const UIAPI_PUBLIC_PATHS: string[];
@@ -23,3 +21,9 @@ export declare function compilePathMatchers(paths: string[]): RegExp[];
23
21
  * the same way the transport allowlist does before matching.
24
22
  */
25
23
  export declare function isRetryableReadPost(basePath: string | undefined): boolean;
24
+ /**
25
+ * True when the request path is onboarded to the Idempotency-Key feature.
26
+ * Gates minting only; must match a flagged template and not a more-specific
27
+ * non-flagged sibling (see IDEMPOTENT_WRITE_EXCLUSION_MATCHERS).
28
+ */
29
+ export declare function isIdempotentWrite(basePath: string | undefined): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-network-fetch",
3
- "version": "1.462.0",
3
+ "version": "1.464.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "LDS Network Adapter using fetch",
6
6
  "main": "dist/ldsNetworkFetch.js",
@@ -34,12 +34,12 @@
34
34
  "release:corejar": "yarn build && ../core-build/scripts/core.js --name=lds-network-fetch"
35
35
  },
36
36
  "dependencies": {
37
- "@conduit-client/service-fetch-network": "3.29.0",
37
+ "@conduit-client/service-fetch-network": "3.32.0",
38
38
  "@luvio/engine": "0.161.2",
39
39
  "@luvio/network-adapter-fetch": "0.161.2"
40
40
  },
41
41
  "devDependencies": {
42
- "@salesforce/lds-network-aura": "^1.462.0"
42
+ "@salesforce/lds-network-aura": "^1.464.0"
43
43
  },
44
44
  "volta": {
45
45
  "extends": "../../package.json"