@salesforce/lds-drafts 1.124.4 → 1.124.5

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
@@ -367,10 +367,6 @@ function customActionHandler(executor, id, draftQueue) {
367
367
  });
368
368
  return queueOperations;
369
369
  };
370
- const replaceAction = (actionId, _withActionId, _uploadingActionId, _actions) => {
371
- // eslint-disable-next-line @salesforce/lds/no-error-in-production
372
- throw new Error(`${actionId} does not support action replacing. You can only delete ${actionId}`);
373
- };
374
370
  const getRedirectMappings = (_action) => {
375
371
  return undefined;
376
372
  };
@@ -382,7 +378,9 @@ function customActionHandler(executor, id, draftQueue) {
382
378
  handleAction: handle,
383
379
  buildPendingAction,
384
380
  getQueueOperationsForCompletingDrafts: getQueueOperationsForCompletingDrafts,
385
- handleReplaceAction: replaceAction,
381
+ handleReplaceAction: () => {
382
+ throw Error('replaceAction not supported for custom actions');
383
+ },
386
384
  getRedirectMappings,
387
385
  handleActionRemoved: () => Promise.resolve(),
388
386
  handleActionCompleted: () => Promise.resolve(),
@@ -449,13 +447,13 @@ class DurableDraftQueue {
449
447
  this.userState = DraftQueueState.Started;
450
448
  if (this.state === DraftQueueState.Started) {
451
449
  // Do nothing if the queue state is already started
452
- return Promise.resolve();
450
+ return;
453
451
  }
454
452
  if (this.replacingAction !== undefined) {
455
453
  // If we're replacing an action do nothing
456
454
  // replace will restart the queue for us as long as the user
457
455
  // has last set the queue to be started
458
- return Promise.resolve();
456
+ return;
459
457
  }
460
458
  this.retryIntervalMilliseconds = 0;
461
459
  this.state = DraftQueueState.Started;
@@ -695,92 +693,11 @@ class DurableDraftQueue {
695
693
  await this.startQueue();
696
694
  }
697
695
  }
698
- replaceAction(actionId, withActionId) {
699
- // ids must be unique
700
- if (actionId === withActionId) {
701
- return Promise.reject('Swapped and swapping action ids cannot be the same');
702
- }
703
- // cannot have a replace action already in progress
704
- if (this.replacingAction !== undefined) {
705
- return Promise.reject('Cannot replace actions while a replace action is in progress');
706
- }
707
- this.stopQueueManually();
708
- const replacing = this.getQueueActions().then(async (actions) => {
709
- const first = actions.filter((action) => action.id === actionId)[0];
710
- if (first === undefined) {
711
- this.replacingAction = undefined;
712
- await this.startQueueSafe();
713
- return Promise.reject('No action to replace');
714
- }
715
- // put in a try/finally block so we don't leave this.replacingAction
716
- // indefinitely set
717
- let actionToReplace;
718
- try {
719
- const replaceResult = this.getHandler(first.handler).handleReplaceAction(actionId, withActionId, this.uploadingActionId, actions);
720
- actionToReplace = replaceResult.actionToReplace;
721
- // TODO [W-8873834]: Will add batching support to durable store
722
- // we should use that here to remove and set both actions in one operation
723
- await this.removeDraftAction(replaceResult.replacingAction.id);
724
- await this.draftStore.writeAction(actionToReplace);
725
- await this.notifyChangedListeners({
726
- type: DraftQueueEventType.ActionUpdated,
727
- action: actionToReplace,
728
- });
729
- }
730
- finally {
731
- this.replacingAction = undefined;
732
- }
733
- await this.startQueueSafe();
734
- return actionToReplace;
735
- });
736
- this.replacingAction = replacing;
737
- return replacing;
696
+ replaceAction(targetActionId, sourceActionId) {
697
+ return this.replaceOrMergeActions(targetActionId, sourceActionId, false);
738
698
  }
739
699
  mergeActions(targetActionId, sourceActionId) {
740
- // ids must be unique
741
- if (targetActionId === sourceActionId) {
742
- return Promise.reject(new Error('targetActionId and sourceActionId cannot be the same.'));
743
- }
744
- // cannot have a replace action already in progress
745
- if (this.replacingAction !== undefined) {
746
- return Promise.reject(new Error('Cannot merge actions while a replace/merge action operation is in progress.'));
747
- }
748
- this.stopQueueManually();
749
- const promise = this.getQueueActions().then(async (actions) => {
750
- const target = actions.find((action) => action.id === targetActionId);
751
- if (target === undefined) {
752
- this.replacingAction = undefined;
753
- await this.startQueueSafe();
754
- throw Error('targetActionId not found in the draft queue.');
755
- }
756
- const source = actions.find((action) => action.id === sourceActionId);
757
- if (source === undefined) {
758
- this.replacingAction = undefined;
759
- await this.startQueueSafe();
760
- throw Error('sourceActionId not found in the draft queue.');
761
- }
762
- // put in a try/finally block so we don't leave this.replacingAction
763
- // indefinitely set
764
- let merged;
765
- try {
766
- merged = this.getHandler(target.handler).mergeActions(target, source);
767
- // update the target
768
- await this.draftStore.writeAction(merged);
769
- await this.notifyChangedListeners({
770
- type: DraftQueueEventType.ActionUpdated,
771
- action: merged,
772
- });
773
- // remove the source from queue
774
- await this.removeDraftAction(sourceActionId);
775
- }
776
- finally {
777
- this.replacingAction = undefined;
778
- }
779
- await this.startQueueSafe();
780
- return merged;
781
- });
782
- this.replacingAction = promise;
783
- return promise;
700
+ return this.replaceOrMergeActions(targetActionId, sourceActionId, true);
784
701
  }
785
702
  async setMetadata(actionId, metadata) {
786
703
  const keys$1 = keys(metadata);
@@ -815,6 +732,81 @@ class DurableDraftQueue {
815
732
  }
816
733
  }, this.retryIntervalMilliseconds);
817
734
  }
735
+ async getActionsForReplaceOrMerge(targetActionId, sourceActionId) {
736
+ const actions = await this.getQueueActions();
737
+ const target = actions.find((action) => action.id === targetActionId);
738
+ if (target === undefined) {
739
+ this.replacingAction = undefined;
740
+ await this.startQueueSafe();
741
+ throw Error(`targetActionId ${targetActionId} not found in the draft queue.`);
742
+ }
743
+ const source = actions.find((action) => action.id === sourceActionId);
744
+ if (source === undefined) {
745
+ this.replacingAction = undefined;
746
+ await this.startQueueSafe();
747
+ throw Error(`sourceActionId ${sourceActionId} not found in the draft queue.`);
748
+ }
749
+ return { target, source };
750
+ }
751
+ assertReplaceOrMergePrerequisites(target, source) {
752
+ // ensure actions are in a state to be replaced/merged
753
+ const { targetId: targetCacheKey, tag: targetTag, status: targetStatus, version: targetVersion, } = target;
754
+ const { targetId: sourceCacheKey, tag: sourceTag, status: sourceStatus, version: sourceVersion, } = source;
755
+ if (targetCacheKey !== sourceCacheKey) {
756
+ throw Error('Cannot replace/merge actions for different targetIds.');
757
+ }
758
+ if (targetTag !== sourceTag) {
759
+ throw Error('Cannot replace/merge actions for different tags.');
760
+ }
761
+ if (targetStatus === DraftActionStatus.Completed ||
762
+ targetStatus === DraftActionStatus.Uploading) {
763
+ throw Error(`Cannot replace/merge actions when targetAction is in ${targetStatus} status.`);
764
+ }
765
+ if (sourceStatus !== DraftActionStatus.Pending) {
766
+ throw Error(`Cannot replace/merge actions when sourceAction is in ${sourceStatus} status.`);
767
+ }
768
+ if (targetVersion !== sourceVersion) {
769
+ throw Error('Cannot replace/merge actions with different versions.');
770
+ }
771
+ }
772
+ async replaceOrMergeActions(targetActionId, sourceActionId, merge) {
773
+ // ids must be unique
774
+ if (targetActionId === sourceActionId) {
775
+ throw Error('targetActionId and sourceActionId cannot be the same.');
776
+ }
777
+ // cannot have a replace action already in progress
778
+ if (this.replacingAction !== undefined) {
779
+ throw Error('Cannot replace/merge actions while a replace/merge action operation is in progress.');
780
+ }
781
+ this.stopQueueManually();
782
+ const promise = this.getActionsForReplaceOrMerge(targetActionId, sourceActionId).then(async ({ target, source }) => {
783
+ // put in a try/finally block so we don't leave this.replacingAction
784
+ // indefinitely set
785
+ let updatedTarget;
786
+ try {
787
+ this.assertReplaceOrMergePrerequisites(target, source);
788
+ const handler = this.getHandler(target.handler);
789
+ updatedTarget = merge
790
+ ? handler.mergeActions(target, source)
791
+ : handler.handleReplaceAction(target, source);
792
+ // update the target
793
+ await this.draftStore.writeAction(updatedTarget);
794
+ await this.notifyChangedListeners({
795
+ type: DraftQueueEventType.ActionUpdated,
796
+ action: updatedTarget,
797
+ });
798
+ // remove the source from queue
799
+ await this.removeDraftAction(sourceActionId);
800
+ }
801
+ finally {
802
+ this.replacingAction = undefined;
803
+ await this.startQueueSafe();
804
+ }
805
+ return updatedTarget;
806
+ });
807
+ this.replacingAction = promise;
808
+ return promise;
809
+ }
818
810
  }
819
811
 
820
812
  const DRAFT_ACTION_KEY_JUNCTION = '__DraftAction__';
@@ -1248,55 +1240,52 @@ class AbstractResourceRequestActionHandler {
1248
1240
  }
1249
1241
  await this.ingestResponses(recordsToIngest, action);
1250
1242
  }
1251
- handleReplaceAction(actionId, withActionId, uploadingActionId, actions) {
1252
- // get the action to replace
1253
- const actionToReplace = actions.filter((action) => action.id === actionId)[0];
1254
- // get the replacing action
1255
- const replacingAction = actions.filter((action) => action.id === withActionId)[0];
1256
- // reject if either action is undefined
1257
- if (actionToReplace === undefined || replacingAction === undefined) {
1258
- // eslint-disable-next-line @salesforce/lds/no-error-in-production
1259
- throw new Error('One or both actions does not exist');
1260
- }
1261
- // reject if either action is uploading
1262
- if (actionToReplace.id === uploadingActionId || replacingAction.id === uploadingActionId) {
1263
- // eslint-disable-next-line @salesforce/lds/no-error-in-production
1264
- throw new Error('Cannot replace an draft action that is uploading');
1265
- }
1266
- // reject if these two draft actions aren't acting on the same target
1267
- if (actionToReplace.tag !== replacingAction.tag) {
1268
- // eslint-disable-next-line @salesforce/lds/no-error-in-production
1269
- throw new Error('Cannot swap actions targeting different targets');
1270
- }
1271
- // reject if the replacing action is not pending
1272
- if (replacingAction.status !== DraftActionStatus.Pending) {
1273
- // eslint-disable-next-line @salesforce/lds/no-error-in-production
1274
- throw new Error('Cannot replace with a non-pending action');
1275
- }
1243
+ handleReplaceAction(targetAction, sourceAction) {
1276
1244
  //reject if the action to replace is a POST action
1277
- const pendingAction = actionToReplace;
1245
+ const pendingAction = targetAction;
1278
1246
  if (pendingAction.data.method === 'post') {
1279
- // eslint-disable-next-line @salesforce/lds/no-error-in-production
1280
- throw new Error('Cannot replace a POST action');
1247
+ throw Error('Cannot replace a POST action');
1281
1248
  }
1282
- if (this.isActionOfType(actionToReplace) &&
1283
- this.isActionOfType(replacingAction)) {
1284
- const actionToReplaceCopy = {
1285
- ...actionToReplace,
1286
- status: DraftActionStatus.Pending,
1287
- };
1288
- actionToReplace.status = DraftActionStatus.Pending;
1289
- actionToReplace.data = replacingAction.data;
1290
- return {
1291
- original: actionToReplaceCopy,
1292
- actionToReplace: actionToReplace,
1293
- replacingAction: replacingAction,
1294
- };
1249
+ if (this.isActionOfType(targetAction) &&
1250
+ this.isActionOfType(sourceAction)) {
1251
+ targetAction.status = DraftActionStatus.Pending;
1252
+ targetAction.data = sourceAction.data;
1253
+ return targetAction;
1295
1254
  }
1296
1255
  else {
1297
- // eslint-disable-next-line @salesforce/lds/no-error-in-production
1298
- throw new Error('Incompatible Action types to replace one another');
1299
- }
1256
+ throw Error('Incompatible Action types to replace one another');
1257
+ }
1258
+ }
1259
+ mergeActions(targetAction, sourceAction) {
1260
+ const { id: targetId, data: targetData, metadata: targetMetadata, timestamp: targetTimestamp, } = targetAction;
1261
+ const { method: targetMethod, body: targetBody } = targetData;
1262
+ const { data: sourceData, metadata: sourceMetadata } = sourceAction;
1263
+ const { method: sourceMethod, body: sourceBody } = sourceData;
1264
+ if (targetMethod.toLowerCase() === 'delete' || sourceMethod.toLowerCase() === 'delete') {
1265
+ throw Error('Cannot merge DELETE actions.');
1266
+ }
1267
+ if (targetMethod.toLowerCase() === 'patch' && sourceMethod.toLowerCase() === 'post') {
1268
+ // overlaying a POST over a PATCH is not supported
1269
+ throw Error('Cannot merge a POST action over top of a PATCH action.');
1270
+ }
1271
+ // overlay top-level properties, maintain target's timestamp and id
1272
+ const merged = {
1273
+ ...targetAction,
1274
+ ...sourceAction,
1275
+ timestamp: targetTimestamp,
1276
+ id: targetId,
1277
+ };
1278
+ // overlay data
1279
+ // NOTE: we stick to the target's ResourceRequest properties (except body
1280
+ // which is merged) because we don't want to overwrite a POST with a PATCH
1281
+ // (all other cases should be fine or wouldn't have passed pre-requisites)
1282
+ merged.data = {
1283
+ ...targetData,
1284
+ body: this.mergeRequestBody(targetBody, sourceBody),
1285
+ };
1286
+ // overlay metadata
1287
+ merged.metadata = { ...targetMetadata, ...sourceMetadata };
1288
+ return merged;
1300
1289
  }
1301
1290
  shouldDeleteActionByTagOnRemoval(action) {
1302
1291
  return action.data.method === 'post';
@@ -201,10 +201,13 @@ export interface DraftQueue {
201
201
  * of `actionId`. Action ids cannot be equal. Both actions must be acting
202
202
  * on the same target object, and neither can currently be in progress.
203
203
  *
204
- * @param actionId The id of the draft action to replace
205
- * @param withActionId The id of the draft action that will replace the other
204
+ * @param targetActionId The draft action id of the target action. This action
205
+ * will be replaced by the source action while maintaining its position in
206
+ * the queue.
207
+ * @param sourceActionId The draft action id of the source action. This
208
+ * action will replace the target action and will be removed from the queue.
206
209
  */
207
- replaceAction(actionId: string, withActionId: string): Promise<DraftAction<unknown, unknown>>;
210
+ replaceAction<Data, Response>(targetActionId: string, sourceActionId: string): Promise<DraftAction<Data, Response>>;
208
211
  /**
209
212
  * Merges two actions into a single target action. The target action maintains
210
213
  * its position in the queue, while the source action is removed from the queue.
@@ -45,8 +45,11 @@ export declare class DurableDraftQueue implements DraftQueue {
45
45
  */
46
46
  private startQueueSafe;
47
47
  removeDraftAction(actionId: string): Promise<void>;
48
- replaceAction(actionId: string, withActionId: string): Promise<DraftAction<unknown, unknown>>;
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
51
  private scheduleRetry;
52
+ private getActionsForReplaceOrMerge;
53
+ private assertReplaceOrMergePrerequisites;
54
+ private replaceOrMergeActions;
52
55
  }
@@ -1,7 +1,7 @@
1
1
  import type { NetworkAdapter, ResourceRequest, Luvio, DurableStoreKeyMetadataMap } from '@luvio/engine';
2
2
  import type { DraftAction, CompletedDraftAction, QueueOperation, PendingDraftAction, DraftActionMetadata, DraftQueue, EnqueueResult } from '../DraftQueue';
3
3
  import { ProcessActionResult } from '../DraftQueue';
4
- import type { ActionHandler, DraftIdAndKeyMapping, ReplacingActions } from './ActionHandler';
4
+ import type { ActionHandler, DraftIdAndKeyMapping } from './ActionHandler';
5
5
  export type ResponseIngestionEntry<T = unknown> = {
6
6
  response: T;
7
7
  synchronousIngest: AbstractResourceRequestActionHandler<T, unknown>['synchronousIngest'];
@@ -23,7 +23,8 @@ export declare abstract class AbstractResourceRequestActionHandler<ResponseType,
23
23
  getQueueOperationsForCompletingDrafts(queue: DraftAction<unknown, unknown>[], action: CompletedDraftAction<ResourceRequest, ResponseType>): QueueOperation[];
24
24
  getRedirectMappings(action: CompletedDraftAction<ResourceRequest, ResponseType>): DraftIdAndKeyMapping[] | undefined;
25
25
  handleActionCompleted(action: CompletedDraftAction<ResourceRequest, ResponseType>, queueOperations: QueueOperation[], _queue: DraftAction<unknown, unknown>[], allHandlers: ActionHandler<unknown, unknown, unknown>[]): Promise<void>;
26
- handleReplaceAction<Response>(actionId: string, withActionId: string, uploadingActionId: String | undefined, actions: DraftAction<unknown, unknown>[]): ReplacingActions<ResourceRequest, Response>;
26
+ handleReplaceAction(targetAction: DraftAction<ResourceRequest, ResponseType>, sourceAction: DraftAction<ResourceRequest, ResponseType>): DraftAction<ResourceRequest, ResponseType>;
27
+ mergeActions(targetAction: DraftAction<ResourceRequest, ResponseType>, sourceAction: DraftAction<ResourceRequest, ResponseType>): DraftAction<ResourceRequest, ResponseType>;
27
28
  shouldDeleteActionByTagOnRemoval(action: DraftAction<ResourceRequest, ResponseType>): boolean;
28
29
  updateMetadata(_existingMetadata: DraftActionMetadata, incomingMetadata: DraftActionMetadata): DraftActionMetadata;
29
30
  private isActionOfType;
@@ -48,6 +49,9 @@ export declare abstract class AbstractResourceRequestActionHandler<ResponseType,
48
49
  * @param targetId the targetId to check
49
50
  */
50
51
  abstract isDraftId(targetId: string): boolean;
51
- abstract mergeActions(targetAction: DraftAction<ResourceRequest, ResponseType>, sourceAction: DraftAction<ResourceRequest, ResponseType>): DraftAction<ResourceRequest, ResponseType>;
52
+ /**
53
+ * Overlay the sourceBody over top of the targetBody
54
+ */
55
+ abstract mergeRequestBody<T = unknown>(targetBody: T, sourceBody: T): T;
52
56
  }
53
57
  export declare function isResourceRequestAction(action: DraftAction<unknown, unknown>): action is DraftAction<ResourceRequest, unknown>;
@@ -87,13 +87,19 @@ export interface ActionHandler<Data, DraftMetadata, Type> {
87
87
  */
88
88
  handleActionCompleted(action: CompletedDraftAction<Data, Type>, queueOperations: QueueOperation[], queue: DraftAction<unknown, unknown>[], allHandlers: ActionHandler<unknown, unknown, unknown>[]): Promise<void>;
89
89
  /**
90
- * Replace an item in the queue with another to take its place (if supported by handler)
91
- * @param actionId
92
- * @param withActionId
93
- * @param uploadingActionId
94
- * @param actions
90
+ * Replace the targetAction's data with the sourceAction's data and return
91
+ * the targetAction. Also sets the targetAction's status to Pending.
92
+ *
93
+ * NOTE: this method mutates the targetAction.
94
+ *
95
+ * Implementations can assume the caller has already ensured the given draft
96
+ * actions have the same targetId, same tag, same version, and are not currently
97
+ * being uploaded.
98
+ *
99
+ * Replacing draft actions with different targetIds or different tags will result
100
+ * in an error.
95
101
  */
96
- handleReplaceAction<Response>(actionId: string, withActionId: string, uploadingActionId: string | undefined, actions: DraftAction<unknown, unknown>[]): ReplacingActions<Data, Response>;
102
+ handleReplaceAction(targetAction: DraftAction<Data, Type>, sourceAction: DraftAction<Data, Type>): DraftAction<Data, Type>;
97
103
  /**
98
104
  * Invoked by the durable store and the draft environment to get the data that is associated with this draft action
99
105
  * The data should include all drafts applied to it (if any)
@@ -140,8 +146,9 @@ export interface ActionHandler<Data, DraftMetadata, Type> {
140
146
  * both actions will be combined, if there are conflicts the source's data will
141
147
  * be used).
142
148
  *
143
- * Merging draft actions with different targetIds or different tags will result
144
- * in an error.
149
+ * Implementations can assume the caller has already ensured the given draft
150
+ * actions have the same targetId, same tag, same version, and are not currently
151
+ * being uploaded.
145
152
  *
146
153
  * NOTE: the resulting merged action will use the target's timestamp and id.
147
154
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-drafts",
3
- "version": "1.124.4",
3
+ "version": "1.124.5",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "LDS Drafts",
6
6
  "main": "dist/ldsDrafts.js",