@salesforce/lds-drafts 1.246.0 → 1.248.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.
package/dist/ldsDrafts.js CHANGED
@@ -236,8 +236,12 @@ function uuidv4() {
236
236
 
237
237
  const HTTP_HEADER_RETRY_AFTER = 'Retry-After';
238
238
  const HTTP_HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';
239
+ const ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED = 'IDEMPOTENCY_FEATURE_NOT_ENABLED';
240
+ const ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED = 'IDEMPOTENCY_NOT_SUPPORTED';
239
241
  const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = 'IDEMPOTENCY_KEY_USED_DIFFERENT_USER';
240
242
  const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = 'IDEMPOTENCY_CONCURRENT_REQUEST';
243
+ const ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED = 'IDEMPOTENCY_KEY_ALREADY_USED';
244
+ const ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR = 'IDEMPOTENCY_BACKEND_OPERATION_ERROR';
241
245
  /**
242
246
  * Get the retry after in milliseconds from the response headers, undefined if not specified.
243
247
  * The header could have two different format.
@@ -267,7 +271,9 @@ function buildLuvioOverrideForDraftAdapters(luvio, handler, extractTargetIdFromC
267
271
  const dispatchResourceRequest = async function (resourceRequest, _context) {
268
272
  const resourceRequestCopy = clone(resourceRequest);
269
273
  resourceRequestCopy.headers = resourceRequestCopy.headers || {};
270
- resourceRequestCopy.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
274
+ if (handler.hasIdempotencySupport()) {
275
+ resourceRequestCopy.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
276
+ }
271
277
  // enable return extra fields for record creation and record update http call
272
278
  if (resourceRequest.basePath === '/ui-api/records' &&
273
279
  (resourceRequest.method === 'post' || resourceRequest.method === 'patch')) {
@@ -1103,6 +1109,7 @@ class AbstractResourceRequestActionHandler {
1103
1109
  // the luvio store redirect table, during which a new draft might be enqueued
1104
1110
  // which would not see a necessary mapping.
1105
1111
  this.ephemeralRedirects = {};
1112
+ this.isIdempotencySupported = true;
1106
1113
  }
1107
1114
  enqueue(data) {
1108
1115
  return this.draftQueue.enqueue(this.handlerId, data);
@@ -1132,21 +1139,43 @@ class AbstractResourceRequestActionHandler {
1132
1139
  retryDelayInMs = getRetryAfterInMs(response.headers);
1133
1140
  shouldRetry = true;
1134
1141
  break;
1135
- case HttpStatusCode.ServerError:
1142
+ case HttpStatusCode.ServerError: {
1136
1143
  shouldRetry = true;
1144
+ if (this.handleIdempotencyServerError(response.body, updatedAction, false, ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR)) {
1145
+ this.isIdempotencySupported = false;
1146
+ retryDelayInMs = 0;
1147
+ actionDataChanged = true;
1148
+ }
1137
1149
  break;
1150
+ }
1138
1151
  case 409 /* IdempotentWriteSpecificHttpStatusCode.Conflict */: {
1139
- const errorCode = response.body[0].errorCode;
1140
- if (errorCode === ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER) {
1141
- updatedAction.data.headers = updatedAction.data.headers || {};
1142
- updatedAction.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
1152
+ if (this.isUiApiErrors(response.body)) {
1153
+ const errorCode = response.body[0].errorCode;
1154
+ if (this.handleIdempotencyServerError(response.body, updatedAction, true, ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER)) {
1155
+ retryDelayInMs = 0;
1156
+ actionDataChanged = true;
1157
+ }
1158
+ else if (errorCode === ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST) {
1159
+ retryDelayInMs = getRetryAfterInMs(response.headers);
1160
+ }
1161
+ shouldRetry = true;
1162
+ }
1163
+ break;
1164
+ }
1165
+ case HttpStatusCode.BadRequest: {
1166
+ if (this.handleIdempotencyServerError(response.body, updatedAction, false, ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED, ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED)) {
1143
1167
  retryDelayInMs = 0;
1144
1168
  actionDataChanged = true;
1169
+ shouldRetry = true;
1145
1170
  }
1146
- else if (errorCode === ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST) {
1147
- retryDelayInMs = getRetryAfterInMs(response.headers);
1171
+ break;
1172
+ }
1173
+ case 422 /* IdempotentWriteSpecificHttpStatusCode.UnProcessableEntity */: {
1174
+ if (this.handleIdempotencyServerError(response.body, updatedAction, true, ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED)) {
1175
+ retryDelayInMs = 0;
1176
+ actionDataChanged = true;
1177
+ shouldRetry = true;
1148
1178
  }
1149
- shouldRetry = true;
1150
1179
  break;
1151
1180
  }
1152
1181
  }
@@ -1165,6 +1194,27 @@ class AbstractResourceRequestActionHandler {
1165
1194
  return ProcessActionResult.NETWORK_ERROR;
1166
1195
  }
1167
1196
  }
1197
+ // true if response is an idempotency server error. updates or deletes idempotency key if the reponse is idempotency related error. Idempotency related error is in format of UiApiError array.
1198
+ handleIdempotencyServerError(responseBody, action, updateIdempotencyKey, ...targetErrorCodes) {
1199
+ if (this.isUiApiErrors(responseBody)) {
1200
+ const errorCode = responseBody[0].errorCode;
1201
+ if (targetErrorCodes.includes(errorCode)) {
1202
+ action.data.headers = action.data.headers || {};
1203
+ if (updateIdempotencyKey) {
1204
+ action.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
1205
+ }
1206
+ else {
1207
+ delete action.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY];
1208
+ }
1209
+ return true;
1210
+ }
1211
+ }
1212
+ return false;
1213
+ }
1214
+ // checks if the body is an array of UiApiError. Sometimes the body has `enhancedErrorType` field as an error indicator(one example is the field validation failure). In such case Action being processed updates to an Error Action.
1215
+ isUiApiErrors(body) {
1216
+ return body !== undefined && Array.isArray(body) && body.length > 0 && body[0].errorCode;
1217
+ }
1168
1218
  async buildPendingAction(request, queue) {
1169
1219
  const targetId = await this.getIdFromRequest(request);
1170
1220
  const tag = this.buildTagForTargetId(targetId);
@@ -1375,6 +1425,10 @@ class AbstractResourceRequestActionHandler {
1375
1425
  ...targetData,
1376
1426
  body: this.mergeRequestBody(targetBody, sourceBody),
1377
1427
  };
1428
+ // Updates Idempotency key if target has one
1429
+ if (targetData.headers && targetData.headers[HTTP_HEADER_IDEMPOTENCY_KEY]) {
1430
+ merged.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
1431
+ }
1378
1432
  // overlay metadata
1379
1433
  merged.metadata = { ...targetMetadata, ...sourceMetadata };
1380
1434
  // put status back to pending to auto upload if queue is active and targed is at the head.
@@ -1411,6 +1465,9 @@ class AbstractResourceRequestActionHandler {
1411
1465
  getDraftIdsFromAction(action) {
1412
1466
  return [action.targetId];
1413
1467
  }
1468
+ hasIdempotencySupport() {
1469
+ return this.isIdempotencySupported;
1470
+ }
1414
1471
  async ingestResponses(responses, action) {
1415
1472
  const luvio = this.getLuvio();
1416
1473
  await luvio.handleSuccessResponse(() => {
@@ -14,9 +14,12 @@ export declare abstract class AbstractResourceRequestActionHandler<ResponseType,
14
14
  ephemeralRedirects: {
15
15
  [key: string]: string;
16
16
  };
17
+ isIdempotencySupported: boolean;
17
18
  constructor(draftQueue: DraftQueue, networkAdapter: NetworkAdapter, getLuvio: () => Luvio);
18
19
  enqueue(data: ResourceRequest): Promise<EnqueueResult<ResourceRequest, ResponseType>>;
19
20
  handleAction(action: DraftAction<ResourceRequest, ResponseType>, actionCompleted: (action: CompletedDraftAction<ResourceRequest, ResponseType>) => Promise<void>, actionErrored: (action: DraftAction<ResourceRequest, ResponseType>, retry: boolean, retryDelayInMs?: number, actionDataChanged?: boolean) => Promise<void>): Promise<ProcessActionResult>;
21
+ handleIdempotencyServerError(responseBody: any, action: DraftAction<ResourceRequest, ResponseType>, updateIdempotencyKey: boolean, ...targetErrorCodes: string[]): boolean;
22
+ isUiApiErrors(body: any): boolean;
20
23
  buildPendingAction(request: ResourceRequest, queue: DraftAction<unknown, unknown>[]): Promise<PendingDraftAction<ResourceRequest>>;
21
24
  handleActionEnqueued(action: PendingDraftAction<ResourceRequest>): Promise<void>;
22
25
  handleActionRemoved(action: DraftAction<ResourceRequest, ResponseType>): Promise<void>;
@@ -30,6 +33,7 @@ export declare abstract class AbstractResourceRequestActionHandler<ResponseType,
30
33
  private isActionOfType;
31
34
  protected reingestRecord(action: DraftAction<ResourceRequest, ResponseType>): Promise<void>;
32
35
  getDraftIdsFromAction(action: DraftAction<ResourceRequest, ResponseType>): string[];
36
+ hasIdempotencySupport(): boolean;
33
37
  ingestResponses(responses: ResponseIngestionEntry[], action: DraftAction<ResourceRequest, ResponseType>): Promise<void>;
34
38
  evictKey(key: string): Promise<void>;
35
39
  abstract handlerId: string;
@@ -7,7 +7,8 @@ export declare const enum IdempotentWriteSpecificHttpStatusCode {
7
7
  RequestTimeout = 408,
8
8
  Conflict = 409,
9
9
  BadGateway = 502,
10
- ServiceUnavailable = 503
10
+ ServiceUnavailable = 503,
11
+ UnProcessableEntity = 422
11
12
  }
12
13
  /**
13
14
  * The http status code which could be returned from idempotent write.
@@ -15,8 +16,12 @@ export declare const enum IdempotentWriteSpecificHttpStatusCode {
15
16
  export type IdempotentWriteHttpStatusCode = IdempotentWriteSpecificHttpStatusCode | HttpStatusCode;
16
17
  export declare const HTTP_HEADER_RETRY_AFTER = "Retry-After";
17
18
  export declare const HTTP_HEADER_IDEMPOTENCY_KEY = "Idempotency-Key";
19
+ export declare const ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED = "IDEMPOTENCY_FEATURE_NOT_ENABLED";
20
+ export declare const ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED = "IDEMPOTENCY_NOT_SUPPORTED";
18
21
  export declare const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = "IDEMPOTENCY_KEY_USED_DIFFERENT_USER";
19
22
  export declare const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = "IDEMPOTENCY_CONCURRENT_REQUEST";
23
+ export declare const ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED = "IDEMPOTENCY_KEY_ALREADY_USED";
24
+ export declare const ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR = "IDEMPOTENCY_BACKEND_OPERATION_ERROR";
20
25
  /**
21
26
  * Get the retry after in milliseconds from the response headers, undefined if not specified.
22
27
  * The header could have two different format.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-drafts",
3
- "version": "1.246.0",
3
+ "version": "1.248.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "LDS Drafts",
6
6
  "main": "dist/ldsDrafts.js",
@@ -24,8 +24,8 @@
24
24
  "release:corejar": "yarn build && ../core-build/scripts/core.js --adapter=lds-drafts"
25
25
  },
26
26
  "dependencies": {
27
- "@luvio/engine": "0.150.6",
28
- "@luvio/environments": "0.150.6",
27
+ "@luvio/engine": "0.151.1",
28
+ "@luvio/environments": "0.151.1",
29
29
  "@salesforce/lds-utils-adapters": "*"
30
30
  }
31
31
  }