@salesforce/lds-drafts 1.173.0 → 1.174.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
@@ -234,13 +234,40 @@ function uuidv4() {
234
234
  return uuid.join('');
235
235
  }
236
236
 
237
- const IdempotencyKey = 'Idempotency-Key';
237
+ const HTTP_HEADER_RETRY_AFTER = 'Retry-After';
238
+ const HTTP_HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';
239
+ const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = 'IDEMPOTENCY_KEY_USED_DIFFERENT_USER';
240
+ const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = 'IDEMPOTENCY_CONCURRENT_REQUEST';
241
+ /**
242
+ * Get the retry after in milliseconds from the response headers, undefined if not specified.
243
+ * The header could have two different format.
244
+ * Retry-After: <http-date>, like Wed, 21 Oct 2015 07:28:00 GMT
245
+ * Retry-After: <delay-seconds>, like 1.5s
246
+ * @param headers http headers
247
+ * @returns the time to delat in millisconds.
248
+ */
249
+ function getRetryAfterInMs(headers) {
250
+ const retryAfterHeader = headers && headers[HTTP_HEADER_RETRY_AFTER];
251
+ if (retryAfterHeader === undefined) {
252
+ return undefined;
253
+ }
254
+ const delayInSecond = parseFloat(retryAfterHeader);
255
+ if (retryAfterHeader === delayInSecond.toString()) {
256
+ return Math.round(delayInSecond * 1000);
257
+ }
258
+ const delayUntilDateTime = Date.parse(retryAfterHeader);
259
+ if (isNaN(delayUntilDateTime)) {
260
+ return undefined;
261
+ }
262
+ return delayUntilDateTime - Date.now();
263
+ }
264
+
238
265
  function buildLuvioOverrideForDraftAdapters(luvio, handler, extractTargetIdFromCacheKey, options = {}) {
239
266
  // override this to create and enqueue a new draft action, and return synthetic response
240
267
  const dispatchResourceRequest = async function (resourceRequest, _context) {
241
268
  const resourceRequestCopy = clone(resourceRequest);
242
269
  resourceRequestCopy.headers = resourceRequestCopy.headers || {};
243
- resourceRequestCopy.headers[IdempotencyKey] = uuidv4();
270
+ resourceRequestCopy.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
244
271
  const { data } = await handler.enqueue(resourceRequestCopy).catch((err) => {
245
272
  throw transformErrorToDraftSynthesisError(err);
246
273
  });
@@ -603,11 +630,19 @@ class DurableDraftQueue {
603
630
  },
604
631
  });
605
632
  }
606
- async actionFailed(action, retry) {
633
+ async actionFailed(action, retry, retryDelayInMs, actionDataChanged) {
634
+ if (actionDataChanged === true) {
635
+ await this.draftStore.writeAction({
636
+ ...action,
637
+ status: DraftActionStatus.Pending,
638
+ });
639
+ }
607
640
  this.uploadingActionId = undefined;
608
641
  if (retry && this.state !== DraftQueueState.Stopped) {
609
642
  this.state = DraftQueueState.Waiting;
610
- this.scheduleRetry();
643
+ return retryDelayInMs !== undefined
644
+ ? this.scheduleRetryWithSpecifiedDelay(retryDelayInMs)
645
+ : this.scheduleRetry();
611
646
  }
612
647
  else if (isDraftError(action)) {
613
648
  return this.handleServerError(action, action.error);
@@ -753,14 +788,17 @@ class DurableDraftQueue {
753
788
  });
754
789
  return action;
755
790
  }
756
- scheduleRetry() {
757
- const newInterval = this.retryIntervalMilliseconds * 2;
758
- this.retryIntervalMilliseconds = Math.min(Math.max(newInterval, this.minimumRetryInterval), this.maximumRetryInterval);
791
+ scheduleRetryWithSpecifiedDelay(retryDelayInMs) {
759
792
  this.timeoutHandler = setTimeout(() => {
760
793
  if (this.state !== DraftQueueState.Stopped) {
761
794
  this.processNextAction();
762
795
  }
763
- }, this.retryIntervalMilliseconds);
796
+ }, retryDelayInMs);
797
+ }
798
+ scheduleRetry() {
799
+ const newInterval = this.retryIntervalMilliseconds * 2;
800
+ this.retryIntervalMilliseconds = Math.min(Math.max(newInterval, this.minimumRetryInterval), this.maximumRetryInterval);
801
+ this.scheduleRetryWithSpecifiedDelay(this.retryIntervalMilliseconds);
764
802
  }
765
803
  async getActionsForReplaceOrMerge(targetActionId, sourceActionId) {
766
804
  const actions = await this.getQueueActions();
@@ -1076,14 +1114,47 @@ class AbstractResourceRequestActionHandler {
1076
1114
  });
1077
1115
  return ProcessActionResult.ACTION_SUCCEEDED;
1078
1116
  }
1079
- await actionErrored({
1080
- ...action,
1081
- error: response,
1082
- status: DraftActionStatus.Error,
1083
- }, false);
1117
+ let shouldRetry = false;
1118
+ let retryDelayInMs = undefined;
1119
+ let actionDataChanged = false;
1120
+ let updatedAction = action;
1121
+ if (request && request.headers && request.headers[HTTP_HEADER_IDEMPOTENCY_KEY]) {
1122
+ const status = response.status;
1123
+ switch (status) {
1124
+ case 408 /* IdempotentWriteSpecificHttpStatusCode.RequestTimeout */:
1125
+ case 503 /* IdempotentWriteSpecificHttpStatusCode.ServiceUnavailable */:
1126
+ retryDelayInMs = getRetryAfterInMs(response.headers);
1127
+ shouldRetry = true;
1128
+ break;
1129
+ case HttpStatusCode.ServerError:
1130
+ shouldRetry = true;
1131
+ break;
1132
+ case 409 /* IdempotentWriteSpecificHttpStatusCode.Conflict */: {
1133
+ const errorCode = JSON.parse(response.body)[0].errorCode;
1134
+ if (errorCode === ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER) {
1135
+ updatedAction.data.headers = updatedAction.data.headers || {};
1136
+ updatedAction.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
1137
+ retryDelayInMs = 0;
1138
+ actionDataChanged = true;
1139
+ }
1140
+ else if (errorCode === ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST) {
1141
+ retryDelayInMs = getRetryAfterInMs(response.headers);
1142
+ }
1143
+ shouldRetry = true;
1144
+ break;
1145
+ }
1146
+ }
1147
+ }
1148
+ await actionErrored(shouldRetry
1149
+ ? updatedAction
1150
+ : {
1151
+ ...updatedAction,
1152
+ error: response,
1153
+ status: DraftActionStatus.Error,
1154
+ }, shouldRetry, retryDelayInMs, actionDataChanged);
1084
1155
  return ProcessActionResult.ACTION_ERRORED;
1085
1156
  }
1086
- catch (_a) {
1157
+ catch (e) {
1087
1158
  await actionErrored(action, true);
1088
1159
  return ProcessActionResult.NETWORK_ERROR;
1089
1160
  }
@@ -1,7 +1,6 @@
1
1
  import type { DispatchResourceRequestContext, Luvio, Snapshot } from '@luvio/engine';
2
2
  import type { AbstractResourceRequestActionHandler } from './actionHandlers/AbstractResourceRequestActionHandler';
3
3
  export type AdapterBuildNetworkSnapshot<Config, Response> = (luvio: Luvio, config: Config, dispatchResourceRequestContext?: DispatchResourceRequestContext) => Promise<Snapshot<Response>>;
4
- export declare const IdempotencyKey = "Idempotency-Key";
5
4
  export declare function buildLuvioOverrideForDraftAdapters<ResponseType = unknown, DraftMetadata = unknown>(luvio: Luvio, handler: AbstractResourceRequestActionHandler<ResponseType, DraftMetadata>, extractTargetIdFromCacheKey: (cacheKey: string) => string | undefined, options?: {
6
5
  forDeleteAdapter?: boolean;
7
6
  }): Luvio;
@@ -34,7 +34,7 @@ export declare class DurableDraftQueue implements DraftQueue {
34
34
  enqueue<Data, Response>(handlerId: string, data: unknown): Promise<EnqueueResult<Data, Response>>;
35
35
  registerOnChangedListener(listener: DraftQueueChangeListener): () => Promise<void>;
36
36
  actionCompleted(action: CompletedDraftAction<unknown, unknown>): Promise<void>;
37
- actionFailed(action: DraftAction<unknown, unknown>, retry: boolean): Promise<void>;
37
+ actionFailed(action: DraftAction<unknown, unknown>, retry: boolean, retryDelayInMs?: number, actionDataChanged?: boolean): Promise<void>;
38
38
  handle(action: DraftAction<unknown, unknown>): Promise<ProcessActionResult>;
39
39
  processNextAction(): Promise<ProcessActionResult>;
40
40
  private handleServerError;
@@ -48,6 +48,7 @@ export declare class DurableDraftQueue implements DraftQueue {
48
48
  replaceAction<Data, Response>(targetActionId: string, sourceActionId: string): Promise<DraftAction<Data, Response>>;
49
49
  mergeActions<Data, Response>(targetActionId: string, sourceActionId: string): Promise<DraftAction<Data, Response>>;
50
50
  setMetadata(actionId: string, metadata: DraftActionMetadata): Promise<DraftAction<unknown, unknown>>;
51
+ private scheduleRetryWithSpecifiedDelay;
51
52
  private scheduleRetry;
52
53
  private getActionsForReplaceOrMerge;
53
54
  private assertReplaceOrMergePrerequisites;
@@ -16,7 +16,7 @@ export declare abstract class AbstractResourceRequestActionHandler<ResponseType,
16
16
  };
17
17
  constructor(draftQueue: DraftQueue, networkAdapter: NetworkAdapter, getLuvio: () => Luvio);
18
18
  enqueue(data: ResourceRequest): Promise<EnqueueResult<ResourceRequest, ResponseType>>;
19
- handleAction(action: DraftAction<ResourceRequest, ResponseType>, actionCompleted: (action: CompletedDraftAction<ResourceRequest, ResponseType>) => Promise<void>, actionErrored: (action: DraftAction<ResourceRequest, ResponseType>, retry: boolean) => Promise<void>): Promise<ProcessActionResult>;
19
+ 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>;
20
20
  buildPendingAction(request: ResourceRequest, queue: DraftAction<unknown, unknown>[]): Promise<PendingDraftAction<ResourceRequest>>;
21
21
  handleActionEnqueued(action: PendingDraftAction<ResourceRequest>): Promise<void>;
22
22
  handleActionRemoved(action: DraftAction<ResourceRequest, ResponseType>): Promise<void>;
@@ -57,7 +57,7 @@ export interface ActionHandler<Data, DraftMetadata, Type> {
57
57
  * @param actionCompleted callback function when the action completes successfully
58
58
  * @param actionErrored callback function invoked when the action errors
59
59
  */
60
- handleAction(action: DraftAction<Data, Type>, actionCompleted: (action: CompletedDraftAction<Data, Type>) => Promise<void>, actionErrored: (action: DraftAction<Data, Type>, retry: boolean) => Promise<void>): Promise<ProcessActionResult>;
60
+ handleAction(action: DraftAction<Data, Type>, actionCompleted: (action: CompletedDraftAction<Data, Type>) => Promise<void>, actionErrored: (action: DraftAction<Data, Type>, retry: boolean, retryDelayInMs?: number, actionDataChanged?: boolean) => Promise<void>): Promise<ProcessActionResult>;
61
61
  /**
62
62
  * Invoked by the draft queue after an action for this handler is removed from the queue
63
63
  * @param action: the removed action
@@ -0,0 +1,28 @@
1
+ import type { HttpStatusCode, Headers } from '@luvio/engine';
2
+ export interface UIApiError {
3
+ errorCode: string;
4
+ message: string | null;
5
+ }
6
+ export declare const enum IdempotentWriteSpecificHttpStatusCode {
7
+ RequestTimeout = 408,
8
+ Conflict = 409,
9
+ BadGateway = 502,
10
+ ServiceUnavailable = 503
11
+ }
12
+ /**
13
+ * The http status code which could be returned from idempotent write.
14
+ */
15
+ export type IdempotentWriteHttpStatusCode = IdempotentWriteSpecificHttpStatusCode | HttpStatusCode;
16
+ export declare const HTTP_HEADER_RETRY_AFTER = "Retry-After";
17
+ export declare const HTTP_HEADER_IDEMPOTENCY_KEY = "Idempotency-Key";
18
+ export declare const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = "IDEMPOTENCY_KEY_USED_DIFFERENT_USER";
19
+ export declare const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = "IDEMPOTENCY_CONCURRENT_REQUEST";
20
+ /**
21
+ * Get the retry after in milliseconds from the response headers, undefined if not specified.
22
+ * The header could have two different format.
23
+ * Retry-After: <http-date>, like Wed, 21 Oct 2015 07:28:00 GMT
24
+ * Retry-After: <delay-seconds>, like 1.5s
25
+ * @param headers http headers
26
+ * @returns the time to delat in millisconds.
27
+ */
28
+ export declare function getRetryAfterInMs(headers?: Headers): number | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-drafts",
3
- "version": "1.173.0",
3
+ "version": "1.174.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "LDS Drafts",
6
6
  "main": "dist/ldsDrafts.js",