@salesforce/lds-drafts 1.228.1 → 1.229.0-dev10

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
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { HttpStatusCode, StoreKeyMap } from '@luvio/engine';
8
8
  import { AsyncWorkerPool } from '@salesforce/lds-utils-adapters';
9
+ import ldsIdempotencyWriteDisabled from '@salesforce/gate/lds.idempotencyWriteDisabled';
9
10
 
10
11
  var DraftActionStatus;
11
12
  (function (DraftActionStatus) {
@@ -236,8 +237,12 @@ function uuidv4() {
236
237
 
237
238
  const HTTP_HEADER_RETRY_AFTER = 'Retry-After';
238
239
  const HTTP_HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';
240
+ const ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED = 'IDEMPOTENCY_FEATURE_NOT_ENABLED';
241
+ const ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED = 'IDEMPOTENCY_NOT_SUPPORTED';
239
242
  const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = 'IDEMPOTENCY_KEY_USED_DIFFERENT_USER';
240
243
  const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = 'IDEMPOTENCY_CONCURRENT_REQUEST';
244
+ const ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED = 'IDEMPOTENCY_KEY_ALREADY_USED';
245
+ const ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR = 'IDEMPOTENCY_BACKEND_OPERATION_ERROR';
241
246
  /**
242
247
  * Get the retry after in milliseconds from the response headers, undefined if not specified.
243
248
  * The header could have two different format.
@@ -267,7 +272,9 @@ function buildLuvioOverrideForDraftAdapters(luvio, handler, extractTargetIdFromC
267
272
  const dispatchResourceRequest = async function (resourceRequest, _context) {
268
273
  const resourceRequestCopy = clone(resourceRequest);
269
274
  resourceRequestCopy.headers = resourceRequestCopy.headers || {};
270
- resourceRequestCopy.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
275
+ if (handler.hasIdempotencySupport()) {
276
+ resourceRequestCopy.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
277
+ }
271
278
  // enable return extra fields for record creation and record update http call
272
279
  if (resourceRequest.basePath === '/ui-api/records' &&
273
280
  (resourceRequest.method === 'post' || resourceRequest.method === 'patch')) {
@@ -1103,6 +1110,12 @@ class AbstractResourceRequestActionHandler {
1103
1110
  // the luvio store redirect table, during which a new draft might be enqueued
1104
1111
  // which would not see a necessary mapping.
1105
1112
  this.ephemeralRedirects = {};
1113
+ // determined by Server setup.
1114
+ this.isIdempotencySupported = true;
1115
+ // idempotency write flag set by lds
1116
+ this.isLdsIdempotencyWriteDisabled = ldsIdempotencyWriteDisabled.isOpen({
1117
+ fallback: false,
1118
+ });
1106
1119
  }
1107
1120
  enqueue(data) {
1108
1121
  return this.draftQueue.enqueue(this.handlerId, data);
@@ -1132,21 +1145,43 @@ class AbstractResourceRequestActionHandler {
1132
1145
  retryDelayInMs = getRetryAfterInMs(response.headers);
1133
1146
  shouldRetry = true;
1134
1147
  break;
1135
- case HttpStatusCode.ServerError:
1148
+ case HttpStatusCode.ServerError: {
1136
1149
  shouldRetry = true;
1150
+ if (this.handleIdempotencyServerError(response.body, updatedAction, false, ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR)) {
1151
+ this.isIdempotencySupported = false;
1152
+ retryDelayInMs = 0;
1153
+ actionDataChanged = true;
1154
+ }
1137
1155
  break;
1156
+ }
1138
1157
  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();
1158
+ if (this.isUiApiErrors(response.body)) {
1159
+ const errorCode = response.body[0].errorCode;
1160
+ if (this.handleIdempotencyServerError(response.body, updatedAction, true, ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER)) {
1161
+ retryDelayInMs = 0;
1162
+ actionDataChanged = true;
1163
+ }
1164
+ else if (errorCode === ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST) {
1165
+ retryDelayInMs = getRetryAfterInMs(response.headers);
1166
+ }
1167
+ shouldRetry = true;
1168
+ }
1169
+ break;
1170
+ }
1171
+ case HttpStatusCode.BadRequest: {
1172
+ if (this.handleIdempotencyServerError(response.body, updatedAction, false, ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED, ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED)) {
1143
1173
  retryDelayInMs = 0;
1144
1174
  actionDataChanged = true;
1175
+ shouldRetry = true;
1145
1176
  }
1146
- else if (errorCode === ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST) {
1147
- retryDelayInMs = getRetryAfterInMs(response.headers);
1177
+ break;
1178
+ }
1179
+ case 422 /* IdempotentWriteSpecificHttpStatusCode.UnProcessableEntity */: {
1180
+ if (this.handleIdempotencyServerError(response.body, updatedAction, true, ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED)) {
1181
+ retryDelayInMs = 0;
1182
+ actionDataChanged = true;
1183
+ shouldRetry = true;
1148
1184
  }
1149
- shouldRetry = true;
1150
1185
  break;
1151
1186
  }
1152
1187
  }
@@ -1165,6 +1200,27 @@ class AbstractResourceRequestActionHandler {
1165
1200
  return ProcessActionResult.NETWORK_ERROR;
1166
1201
  }
1167
1202
  }
1203
+ // 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.
1204
+ handleIdempotencyServerError(responseBody, action, updateIdempotencyKey, ...targetErrorCodes) {
1205
+ if (this.isUiApiErrors(responseBody)) {
1206
+ const errorCode = responseBody[0].errorCode;
1207
+ if (targetErrorCodes.includes(errorCode)) {
1208
+ action.data.headers = action.data.headers || {};
1209
+ if (updateIdempotencyKey) {
1210
+ action.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
1211
+ }
1212
+ else {
1213
+ delete action.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY];
1214
+ }
1215
+ return true;
1216
+ }
1217
+ }
1218
+ return false;
1219
+ }
1220
+ // 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.
1221
+ isUiApiErrors(body) {
1222
+ return body !== undefined && Array.isArray(body) && body.length > 0 && body[0].errorCode;
1223
+ }
1168
1224
  async buildPendingAction(request, queue) {
1169
1225
  const targetId = await this.getIdFromRequest(request);
1170
1226
  if (targetId === undefined) {
@@ -1378,6 +1434,10 @@ class AbstractResourceRequestActionHandler {
1378
1434
  ...targetData,
1379
1435
  body: this.mergeRequestBody(targetBody, sourceBody),
1380
1436
  };
1437
+ // Updates Idempotency key if target has one
1438
+ if (targetData.headers && targetData.headers[HTTP_HEADER_IDEMPOTENCY_KEY]) {
1439
+ merged.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
1440
+ }
1381
1441
  // overlay metadata
1382
1442
  merged.metadata = { ...targetMetadata, ...sourceMetadata };
1383
1443
  // put status back to pending to auto upload if queue is active and targed is at the head.
@@ -1414,6 +1474,9 @@ class AbstractResourceRequestActionHandler {
1414
1474
  getDraftIdsFromAction(action) {
1415
1475
  return [action.targetId];
1416
1476
  }
1477
+ hasIdempotencySupport() {
1478
+ return this.isIdempotencySupported && !this.isLdsIdempotencyWriteDisabled;
1479
+ }
1417
1480
  async ingestResponses(responses, action) {
1418
1481
  const luvio = this.getLuvio();
1419
1482
  await luvio.handleSuccessResponse(() => {
@@ -14,9 +14,13 @@ export declare abstract class AbstractResourceRequestActionHandler<ResponseType,
14
14
  ephemeralRedirects: {
15
15
  [key: string]: string;
16
16
  };
17
+ isIdempotencySupported: boolean;
18
+ isLdsIdempotencyWriteDisabled: boolean;
17
19
  constructor(draftQueue: DraftQueue, networkAdapter: NetworkAdapter, getLuvio: () => Luvio);
18
20
  enqueue(data: ResourceRequest): Promise<EnqueueResult<ResourceRequest, ResponseType>>;
19
21
  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>;
22
+ handleIdempotencyServerError(responseBody: any, action: DraftAction<ResourceRequest, ResponseType>, updateIdempotencyKey: boolean, ...targetErrorCodes: string[]): boolean;
23
+ isUiApiErrors(body: any): boolean;
20
24
  buildPendingAction(request: ResourceRequest, queue: DraftAction<unknown, unknown>[]): Promise<PendingDraftAction<ResourceRequest>>;
21
25
  handleActionEnqueued(action: PendingDraftAction<ResourceRequest>): Promise<void>;
22
26
  handleActionRemoved(action: DraftAction<ResourceRequest, ResponseType>): Promise<void>;
@@ -30,6 +34,7 @@ export declare abstract class AbstractResourceRequestActionHandler<ResponseType,
30
34
  private isActionOfType;
31
35
  protected reingestRecord(action: DraftAction<ResourceRequest, ResponseType>): Promise<void>;
32
36
  getDraftIdsFromAction(action: DraftAction<ResourceRequest, ResponseType>): string[];
37
+ hasIdempotencySupport(): boolean;
33
38
  ingestResponses(responses: ResponseIngestionEntry[], action: DraftAction<ResourceRequest, ResponseType>): Promise<void>;
34
39
  evictKey(key: string): Promise<void>;
35
40
  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.228.1",
3
+ "version": "1.229.0-dev10",
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.145.2",
28
- "@luvio/environments": "0.145.2",
29
- "@salesforce/lds-utils-adapters": "*"
27
+ "@luvio/engine": "0.146.0-dev5",
28
+ "@luvio/environments": "0.146.0-dev5",
29
+ "@salesforce/lds-utils-adapters": "1.229.0-dev10"
30
30
  }
31
31
  }