@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
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
-
|
|
1147
|
-
|
|
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.
|
|
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.
|
|
28
|
-
"@luvio/environments": "0.
|
|
27
|
+
"@luvio/engine": "0.151.1",
|
|
28
|
+
"@luvio/environments": "0.151.1",
|
|
29
29
|
"@salesforce/lds-utils-adapters": "*"
|
|
30
30
|
}
|
|
31
31
|
}
|