@salesforce/lds-drafts 0.1.0-dev1

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.
@@ -0,0 +1,1373 @@
1
+ /**
2
+ * Copyright (c) 2022, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ import { HttpStatusCode } from '@luvio/engine';
8
+ import { AsyncWorkerPool } from '@salesforce/lds-utils-adapters';
9
+ export { uuidv4 } from '@salesforce/lds-utils-adapters';
10
+
11
+ var DraftActionStatus;
12
+ (function (DraftActionStatus) {
13
+ DraftActionStatus["Pending"] = "pending";
14
+ DraftActionStatus["Uploading"] = "uploading";
15
+ DraftActionStatus["Error"] = "error";
16
+ DraftActionStatus["Completed"] = "completed";
17
+ })(DraftActionStatus || (DraftActionStatus = {}));
18
+ function isDraftError(draft) {
19
+ return draft.status === DraftActionStatus.Error;
20
+ }
21
+ function isDraftQueueStateChangeEvent(event) {
22
+ return event.type === DraftQueueEventType.QueueStateChanged;
23
+ }
24
+ var ProcessActionResult;
25
+ (function (ProcessActionResult) {
26
+ // non-2xx network error, requires user intervention
27
+ ProcessActionResult["ACTION_ERRORED"] = "ERROR";
28
+ // upload succeeded
29
+ ProcessActionResult["ACTION_SUCCEEDED"] = "SUCCESS";
30
+ // queue is empty
31
+ ProcessActionResult["NO_ACTION_TO_PROCESS"] = "NO_ACTION_TO_PROCESS";
32
+ // network request is in flight
33
+ ProcessActionResult["ACTION_ALREADY_PROCESSING"] = "ACTION_ALREADY_PROCESSING";
34
+ // network call failed (offline)
35
+ ProcessActionResult["NETWORK_ERROR"] = "NETWORK_ERROR";
36
+ // queue is blocked on an error that requires user intervention
37
+ ProcessActionResult["BLOCKED_ON_ERROR"] = "BLOCKED_ON_ERROR";
38
+ //waiting for user to execute custom action
39
+ ProcessActionResult["CUSTOM_ACTION_WAITING"] = "CUSTOM_ACTION_WAITING";
40
+ })(ProcessActionResult || (ProcessActionResult = {}));
41
+ var DraftQueueState;
42
+ (function (DraftQueueState) {
43
+ /** Currently processing an item in the queue or queue is empty and waiting to process the next item. */
44
+ DraftQueueState["Started"] = "started";
45
+ /**
46
+ * The queue is stopped and will not attempt to upload any drafts until startDraftQueue() is called.
47
+ * This is the initial state when the DraftQueue gets instantiated.
48
+ */
49
+ DraftQueueState["Stopped"] = "stopped";
50
+ /**
51
+ * The queue is stopped due to a blocking error from the last upload attempt.
52
+ * The queue will not run again until startDraftQueue() is called.
53
+ */
54
+ DraftQueueState["Error"] = "error";
55
+ /**
56
+ * There was a network error and the queue will attempt to upload again shortly.
57
+ * To attempt to force an upload now call startDraftQueue().
58
+ */
59
+ DraftQueueState["Waiting"] = "waiting";
60
+ })(DraftQueueState || (DraftQueueState = {}));
61
+ var DraftQueueEventType;
62
+ (function (DraftQueueEventType) {
63
+ /**
64
+ * Triggered after an action had been added to the queue
65
+ */
66
+ DraftQueueEventType["ActionAdded"] = "added";
67
+ /**
68
+ * Triggered when starting to upload and process an action
69
+ */
70
+ DraftQueueEventType["ActionUploading"] = "uploading";
71
+ /**
72
+ * Triggered once an action failed
73
+ */
74
+ DraftQueueEventType["ActionFailed"] = "failed";
75
+ /**
76
+ * Triggered after an action has been deleted from the queue
77
+ */
78
+ DraftQueueEventType["ActionDeleted"] = "deleted";
79
+ /**
80
+ * Triggered after an action has been completed and after it has been removed from the queue
81
+ */
82
+ DraftQueueEventType["ActionCompleted"] = "completed";
83
+ /**
84
+ * Triggered after an action has been updated by the updateAction API
85
+ */
86
+ DraftQueueEventType["ActionUpdated"] = "updated";
87
+ /**
88
+ * Triggered after the Draft Queue state changes
89
+ */
90
+ DraftQueueEventType["QueueStateChanged"] = "state";
91
+ })(DraftQueueEventType || (DraftQueueEventType = {}));
92
+ var QueueOperationType;
93
+ (function (QueueOperationType) {
94
+ QueueOperationType["Add"] = "add";
95
+ QueueOperationType["Delete"] = "delete";
96
+ QueueOperationType["Update"] = "update";
97
+ })(QueueOperationType || (QueueOperationType = {}));
98
+
99
+ const { keys, create, assign, values } = Object;
100
+ const { stringify, parse } = JSON;
101
+ const { isArray } = Array;
102
+
103
+ class DraftSynthesisError extends Error {
104
+ constructor(message, errorType) {
105
+ super(message);
106
+ this.errorType = errorType;
107
+ }
108
+ }
109
+ function isDraftSynthesisError(error) {
110
+ return error.errorType !== undefined;
111
+ }
112
+
113
+ const DRAFT_ERROR_CODE = 'DRAFT_ERROR';
114
+ class DraftFetchResponse {
115
+ constructor(status, body) {
116
+ this.headers = {};
117
+ this.status = status;
118
+ this.body = body;
119
+ }
120
+ get statusText() {
121
+ const { status } = this;
122
+ switch (status) {
123
+ case HttpStatusCode.Ok:
124
+ return 'OK';
125
+ case HttpStatusCode.Created:
126
+ return 'Created';
127
+ case HttpStatusCode.NoContent:
128
+ return 'No Content';
129
+ case HttpStatusCode.BadRequest:
130
+ return 'Bad Request';
131
+ case HttpStatusCode.ServerError:
132
+ return 'Server Error';
133
+ default:
134
+ return `Unexpected HTTP Status Code: ${status}`;
135
+ }
136
+ }
137
+ get ok() {
138
+ return this.status >= 200 && this.status < 300;
139
+ }
140
+ }
141
+ class DraftErrorFetchResponse {
142
+ constructor(status, body) {
143
+ this.ok = false;
144
+ this.headers = {};
145
+ this.errorType = 'fetchResponse';
146
+ this.status = status;
147
+ this.body = body;
148
+ }
149
+ get statusText() {
150
+ const { status } = this;
151
+ switch (status) {
152
+ case HttpStatusCode.BadRequest:
153
+ return 'Bad Request';
154
+ case HttpStatusCode.ServerError:
155
+ return 'Server Error';
156
+ case HttpStatusCode.NotFound:
157
+ return 'Not Found';
158
+ default:
159
+ return `Unexpected HTTP Status Code: ${status}`;
160
+ }
161
+ }
162
+ }
163
+ function createOkResponse(body) {
164
+ return new DraftFetchResponse(HttpStatusCode.Ok, body);
165
+ }
166
+ function createBadRequestResponse(body) {
167
+ return new DraftErrorFetchResponse(HttpStatusCode.BadRequest, body);
168
+ }
169
+ function createNotFoundResponse(body) {
170
+ return new DraftErrorFetchResponse(HttpStatusCode.NotFound, body);
171
+ }
172
+ function transformErrorToDraftSynthesisError(error) {
173
+ if (isDraftSynthesisError(error)) {
174
+ const { errorType, message } = error;
175
+ return createDraftSynthesisErrorResponse(message, errorType);
176
+ }
177
+ return createDraftSynthesisErrorResponse(error.message);
178
+ }
179
+ function createDraftSynthesisErrorResponse(message = 'failed to synthesize draft response', errorType) {
180
+ const error = {
181
+ errorCode: DRAFT_ERROR_CODE,
182
+ message: message,
183
+ };
184
+ if (errorType !== undefined) {
185
+ error.errorType = errorType;
186
+ }
187
+ return new DraftErrorFetchResponse(HttpStatusCode.BadRequest, error);
188
+ }
189
+ function createDeletedResponse() {
190
+ return new DraftFetchResponse(HttpStatusCode.NoContent, undefined);
191
+ }
192
+ function createInternalErrorResponse() {
193
+ return new DraftErrorFetchResponse(HttpStatusCode.ServerError, undefined);
194
+ }
195
+
196
+ /**
197
+ * Generates a time-ordered, unique id to associate with a DraftAction. Ensures
198
+ * no collisions with existing draft action IDs.
199
+ */
200
+ function generateUniqueDraftActionId(existingIds) {
201
+ // new id in milliseconds with some extra digits for collisions
202
+ let newId = new Date().getTime() * 100;
203
+ const existingAsNumbers = existingIds
204
+ .map((id) => parseInt(id, 10))
205
+ .filter((parsed) => !isNaN(parsed));
206
+ let counter = 0;
207
+ while (existingAsNumbers.includes(newId)) {
208
+ newId += 1;
209
+ counter += 1;
210
+ // if the counter is 100+ then somehow this method has been called 100
211
+ // times in one millisecond
212
+ if (counter >= 100) {
213
+ // eslint-disable-next-line @salesforce/lds/no-error-in-production
214
+ throw new Error('Unable to generate unique new draft ID');
215
+ }
216
+ }
217
+ return newId.toString();
218
+ }
219
+
220
+ var CustomActionResultType;
221
+ (function (CustomActionResultType) {
222
+ CustomActionResultType["SUCCESS"] = "SUCCESS";
223
+ CustomActionResultType["FAILURE"] = "FAILURE";
224
+ })(CustomActionResultType || (CustomActionResultType = {}));
225
+ var CustomActionErrorType;
226
+ (function (CustomActionErrorType) {
227
+ CustomActionErrorType["NETWORK_ERROR"] = "NETWORK_ERROR";
228
+ CustomActionErrorType["CLIENT_ERROR"] = "CLIENT_ERROR";
229
+ })(CustomActionErrorType || (CustomActionErrorType = {}));
230
+ function isCustomActionSuccess(result) {
231
+ return result.type === CustomActionResultType.SUCCESS;
232
+ }
233
+ function isCustomActionFailed(result) {
234
+ return result.type === CustomActionResultType.FAILURE;
235
+ }
236
+ function customActionHandler(executor, id, draftQueue) {
237
+ const handle = (action, actionCompleted, actionErrored) => {
238
+ notifyCustomActionToExecute(action, actionCompleted, actionErrored);
239
+ return Promise.resolve(ProcessActionResult.CUSTOM_ACTION_WAITING);
240
+ };
241
+ const notifyCustomActionToExecute = (action, actionCompleted, actionErrored) => {
242
+ if (executor !== undefined) {
243
+ executor(action, executorCompleted(action, actionCompleted, actionErrored));
244
+ }
245
+ };
246
+ const executorCompleted = (action, actionCompleted, actionErrored) => (result) => {
247
+ if (isCustomActionSuccess(result)) {
248
+ actionCompleted({
249
+ ...action,
250
+ status: DraftActionStatus.Completed,
251
+ response: createOkResponse(undefined),
252
+ });
253
+ }
254
+ else if (isCustomActionFailed(result)) {
255
+ actionErrored({
256
+ ...action,
257
+ status: DraftActionStatus.Error,
258
+ error: result.error.message,
259
+ }, result.error.type === CustomActionErrorType.NETWORK_ERROR);
260
+ }
261
+ };
262
+ const buildPendingAction = (action, queue) => {
263
+ const { data, tag, targetId, handler } = action;
264
+ const id = generateUniqueDraftActionId(queue.map((a) => a.id));
265
+ return Promise.resolve({
266
+ id,
267
+ targetId,
268
+ status: DraftActionStatus.Pending,
269
+ data,
270
+ tag,
271
+ timestamp: Date.now(),
272
+ metadata: data,
273
+ handler,
274
+ });
275
+ };
276
+ const getQueueOperationsForCompletingDrafts = (_queue, action) => {
277
+ const { id } = action;
278
+ const queueOperations = [];
279
+ queueOperations.push({
280
+ type: QueueOperationType.Delete,
281
+ id: id,
282
+ });
283
+ return queueOperations;
284
+ };
285
+ return {
286
+ handlerId: id,
287
+ enqueue: (data) => {
288
+ return draftQueue.enqueue(id, data);
289
+ },
290
+ handleAction: handle,
291
+ buildPendingAction,
292
+ getQueueOperationsForCompletingDrafts: getQueueOperationsForCompletingDrafts,
293
+ handleReplaceAction: () => {
294
+ throw Error('replaceAction not supported for custom actions');
295
+ },
296
+ handleActionRemoved: () => Promise.resolve(),
297
+ handleActionCompleted: () => Promise.resolve(),
298
+ handleActionEnqueued: () => Promise.resolve(),
299
+ handleActionReplaced: () => Promise.resolve(),
300
+ shouldDeleteActionByTagOnRemoval: () => false,
301
+ updateMetadata: (existing, incoming) => incoming,
302
+ mergeActions: () => {
303
+ throw Error('mergeActions not supported for custom actions');
304
+ },
305
+ };
306
+ }
307
+
308
+ const DRAFT_SEGMENT = 'DRAFT';
309
+ class DurableDraftQueue {
310
+ getHandler(id) {
311
+ const handler = this.handlers[id];
312
+ if (handler === undefined) {
313
+ throw Error(`No handler registered for ${id}`);
314
+ }
315
+ return handler;
316
+ }
317
+ constructor(draftStore) {
318
+ this.retryIntervalMilliseconds = 0;
319
+ this.minimumRetryInterval = 250;
320
+ this.maximumRetryInterval = 32000;
321
+ this.draftQueueChangedListeners = [];
322
+ this.state = DraftQueueState.Stopped;
323
+ this.userState = DraftQueueState.Stopped;
324
+ this.uploadingActionId = undefined;
325
+ this.timeoutHandler = undefined;
326
+ this.logger = typeof __nimbus !== 'undefined' &&
327
+ __nimbus.plugins !== undefined &&
328
+ __nimbus.plugins.JSLoggerPlugin !== undefined
329
+ ? __nimbus.plugins.JSLoggerPlugin
330
+ : undefined;
331
+ this.handlers = {};
332
+ this.draftStore = draftStore;
333
+ this.workerPool = new AsyncWorkerPool(1);
334
+ }
335
+ addHandler(handler) {
336
+ const id = handler.handlerId;
337
+ if (this.handlers[id] !== undefined) {
338
+ return Promise.reject(`Unable to add handler to id: ${id} because it already exists.`);
339
+ }
340
+ this.handlers[id] = handler;
341
+ return Promise.resolve();
342
+ }
343
+ removeHandler(id) {
344
+ delete this.handlers[id];
345
+ return Promise.resolve();
346
+ }
347
+ addCustomHandler(id, executor) {
348
+ const handler = customActionHandler(executor, id, this);
349
+ return this.addHandler(handler);
350
+ }
351
+ getQueueState() {
352
+ return this.state;
353
+ }
354
+ async startQueue() {
355
+ this.userState = DraftQueueState.Started;
356
+ if (this.state === DraftQueueState.Started) {
357
+ // Do nothing if the queue state is already started
358
+ return;
359
+ }
360
+ if (this.replacingAction !== undefined) {
361
+ // If we're replacing an action do nothing
362
+ // replace will restart the queue for us as long as the user
363
+ // has last set the queue to be started
364
+ return;
365
+ }
366
+ this.retryIntervalMilliseconds = 0;
367
+ this.state = DraftQueueState.Started;
368
+ await this.notifyChangedListeners({
369
+ type: DraftQueueEventType.QueueStateChanged,
370
+ state: this.state,
371
+ draftCount: this.draftStore.getCount(),
372
+ });
373
+ const result = await this.processNextAction();
374
+ switch (result) {
375
+ case ProcessActionResult.BLOCKED_ON_ERROR:
376
+ this.state = DraftQueueState.Error;
377
+ return Promise.reject('Unable to start queue - first action is in error state');
378
+ default:
379
+ return Promise.resolve();
380
+ }
381
+ }
382
+ stopQueue() {
383
+ this.userState = DraftQueueState.Stopped;
384
+ if (this.state === DraftQueueState.Stopped) {
385
+ // Do nothing if the queue state is already stopped
386
+ return Promise.resolve();
387
+ }
388
+ this.stopQueueManually(false);
389
+ return this.notifyChangedListeners({
390
+ type: DraftQueueEventType.QueueStateChanged,
391
+ state: DraftQueueState.Stopped,
392
+ draftCount: this.draftStore.getCount(),
393
+ });
394
+ }
395
+ stopQueueWhileRunning(action) {
396
+ this.stopQueueManually(true);
397
+ return action().finally(() => {
398
+ this.startQueueSafe();
399
+ });
400
+ }
401
+ /**
402
+ * Used to stop the queue within DraftQueue without user interaction
403
+ */
404
+ stopQueueManually(internalReason) {
405
+ if (this.logger) {
406
+ this.logger.logInfo(internalReason
407
+ ? 'Draft queue stopped for internal reason'
408
+ : 'Draft queue stopped by app');
409
+ }
410
+ if (this.timeoutHandler) {
411
+ clearTimeout(this.timeoutHandler);
412
+ this.timeoutHandler = undefined;
413
+ }
414
+ this.state = DraftQueueState.Stopped;
415
+ }
416
+ async getQueueActions() {
417
+ const drafts = (await this.draftStore.getAllDrafts());
418
+ const queue = [];
419
+ drafts.forEach((draft) => {
420
+ if (draft.id === this.uploadingActionId) {
421
+ draft.status = DraftActionStatus.Uploading;
422
+ }
423
+ queue.push(draft);
424
+ });
425
+ return queue.sort((a, b) => {
426
+ const aTime = parseInt(a.id, 10);
427
+ const bTime = parseInt(b.id, 10);
428
+ // safety check
429
+ if (isNaN(aTime)) {
430
+ return 1;
431
+ }
432
+ if (isNaN(bTime)) {
433
+ return -1;
434
+ }
435
+ return aTime - bTime;
436
+ });
437
+ }
438
+ async enqueue(handlerId, data) {
439
+ return this.workerPool.push({
440
+ workFn: async () => {
441
+ let queue = await this.getQueueActions();
442
+ const handler = this.getHandler(handlerId);
443
+ const pendingAction = (await handler.buildPendingAction(data, queue));
444
+ await this.draftStore.writeAction(pendingAction);
445
+ queue = await this.getQueueActions();
446
+ await this.notifyChangedListeners({
447
+ type: DraftQueueEventType.ActionAdded,
448
+ action: pendingAction,
449
+ draftCount: this.draftStore.getCount(),
450
+ });
451
+ await handler.handleActionEnqueued(pendingAction, queue);
452
+ if (this.state === DraftQueueState.Started) {
453
+ this.processNextAction();
454
+ }
455
+ return pendingAction;
456
+ },
457
+ });
458
+ }
459
+ registerOnChangedListener(listener) {
460
+ this.draftQueueChangedListeners.push(listener);
461
+ return () => {
462
+ this.draftQueueChangedListeners = this.draftQueueChangedListeners.filter((l) => {
463
+ return l !== listener;
464
+ });
465
+ return Promise.resolve();
466
+ };
467
+ }
468
+ async actionCompleted(action) {
469
+ return this.workerPool.push({
470
+ workFn: async () => {
471
+ const handler = this.getHandler(action.handler);
472
+ let queue = await this.getQueueActions();
473
+ const queueOperations = handler.getQueueOperationsForCompletingDrafts(queue, action);
474
+ // write the queue operations to the store prior to ingesting the result
475
+ await this.draftStore.completeAction(queueOperations);
476
+ await handler.handleActionCompleted(action, queueOperations, values(this.handlers));
477
+ this.retryIntervalMilliseconds = 0;
478
+ this.uploadingActionId = undefined;
479
+ await this.notifyChangedListeners({
480
+ type: DraftQueueEventType.ActionCompleted,
481
+ action,
482
+ draftCount: this.draftStore.getCount(),
483
+ });
484
+ if (this.state === DraftQueueState.Started) {
485
+ this.processNextAction();
486
+ }
487
+ },
488
+ });
489
+ }
490
+ async actionFailed(action, retry, retryDelayInMs, actionDataChanged) {
491
+ if (actionDataChanged === true) {
492
+ await this.draftStore.writeAction({
493
+ ...action,
494
+ status: DraftActionStatus.Pending,
495
+ });
496
+ }
497
+ this.uploadingActionId = undefined;
498
+ if (retry && this.state !== DraftQueueState.Stopped) {
499
+ this.state = DraftQueueState.Waiting;
500
+ return retryDelayInMs !== undefined
501
+ ? this.scheduleRetryWithSpecifiedDelay(retryDelayInMs)
502
+ : this.scheduleRetry();
503
+ }
504
+ else if (isDraftError(action)) {
505
+ return this.handleServerError(action, action.error);
506
+ }
507
+ }
508
+ handle(action) {
509
+ const handler = this.getHandler(action.handler);
510
+ if (handler === undefined) {
511
+ return Promise.reject(`No handler for ${action.handler}.`);
512
+ }
513
+ return handler.handleAction(action, this.actionCompleted.bind(this), this.actionFailed.bind(this));
514
+ }
515
+ async processNextAction() {
516
+ if (this.processingAction !== undefined) {
517
+ return this.processingAction;
518
+ }
519
+ const queue = await this.getQueueActions();
520
+ const action = queue[0];
521
+ if (action === undefined) {
522
+ this.processingAction = undefined;
523
+ return ProcessActionResult.NO_ACTION_TO_PROCESS;
524
+ }
525
+ const { status, id } = action;
526
+ if (status === DraftActionStatus.Error) {
527
+ this.state = DraftQueueState.Error;
528
+ this.processingAction = undefined;
529
+ await this.notifyChangedListeners({
530
+ type: DraftQueueEventType.ActionFailed,
531
+ action: action,
532
+ draftCount: this.draftStore.getCount(),
533
+ });
534
+ return ProcessActionResult.BLOCKED_ON_ERROR;
535
+ }
536
+ if (id === this.uploadingActionId) {
537
+ this.state = DraftQueueState.Started;
538
+ this.processingAction = undefined;
539
+ return ProcessActionResult.ACTION_ALREADY_PROCESSING;
540
+ }
541
+ this.uploadingActionId = id;
542
+ this.processingAction = undefined;
543
+ if (this.state === DraftQueueState.Waiting) {
544
+ this.state = DraftQueueState.Started;
545
+ }
546
+ await this.notifyChangedListeners({
547
+ type: DraftQueueEventType.ActionUploading,
548
+ action: { ...action, status: DraftActionStatus.Uploading },
549
+ draftCount: this.draftStore.getCount(),
550
+ });
551
+ return this.handle(action);
552
+ }
553
+ async handleServerError(action, error) {
554
+ const queue = await this.getQueueActions();
555
+ const localAction = queue.filter((qAction) => qAction.id === action.id)[0];
556
+ let newMetadata = {};
557
+ if (localAction !== undefined) {
558
+ newMetadata = localAction.metadata || {};
559
+ }
560
+ const errorAction = {
561
+ ...action,
562
+ status: DraftActionStatus.Error,
563
+ error,
564
+ metadata: newMetadata,
565
+ };
566
+ await this.draftStore.writeAction(errorAction);
567
+ this.state = DraftQueueState.Error;
568
+ return this.notifyChangedListeners({
569
+ type: DraftQueueEventType.ActionFailed,
570
+ action: errorAction,
571
+ draftCount: this.draftStore.getCount(),
572
+ });
573
+ }
574
+ async notifyChangedListeners(event) {
575
+ const results = [];
576
+ const { draftQueueChangedListeners } = this;
577
+ const { length: draftQueueLen } = draftQueueChangedListeners;
578
+ for (let i = 0; i < draftQueueLen; i++) {
579
+ const listener = draftQueueChangedListeners[i];
580
+ results.push(listener(event));
581
+ }
582
+ await Promise.all(results);
583
+ }
584
+ /**
585
+ * only starts the queue if user state is "Started" and if queue not already
586
+ * started
587
+ */
588
+ async startQueueSafe() {
589
+ if (this.userState === DraftQueueState.Started && this.state !== DraftQueueState.Started) {
590
+ await this.startQueue();
591
+ }
592
+ }
593
+ async removeDraftAction(actionId) {
594
+ const queue = await this.getQueueActions();
595
+ //Get the store key for the removed action
596
+ const actions = queue.filter((action) => action.id === actionId);
597
+ if (actions.length === 0) {
598
+ // eslint-disable-next-line @salesforce/lds/no-error-in-production
599
+ throw new Error(`No removable action with id ${actionId}`);
600
+ }
601
+ const action = actions[0];
602
+ if (action.id === this.uploadingActionId) {
603
+ // eslint-disable-next-line @salesforce/lds/no-error-in-production
604
+ throw new Error(`Cannot remove an uploading draft action with ID ${actionId}`);
605
+ }
606
+ const handler = this.getHandler(action.handler);
607
+ const shouldDeleteRelated = handler.shouldDeleteActionByTagOnRemoval(action);
608
+ if (shouldDeleteRelated) {
609
+ await this.draftStore.deleteByTag(action.tag);
610
+ }
611
+ else {
612
+ await this.draftStore.deleteDraft(action.id);
613
+ }
614
+ await handler.handleActionRemoved(action, queue.filter((x) => x.id !== actionId));
615
+ await this.notifyChangedListeners({
616
+ type: DraftQueueEventType.ActionDeleted,
617
+ action,
618
+ draftCount: this.draftStore.getCount(),
619
+ });
620
+ if (this.userState === DraftQueueState.Started &&
621
+ this.state !== DraftQueueState.Started &&
622
+ this.replacingAction === undefined) {
623
+ await this.startQueue();
624
+ }
625
+ }
626
+ async updateDraftAction(action) {
627
+ // stop queue manually
628
+ this.stopQueueManually(true);
629
+ const actionStatus = await this.statusOfAction(action.id);
630
+ if (actionStatus === DraftActionStatus.Uploading) {
631
+ return Promise.reject('cannot update an uploading action');
632
+ }
633
+ // save the action into the draft store
634
+ await this.draftStore.writeAction(action);
635
+ // make the handler replay these drafts on the record
636
+ const handler = this.getHandler(action.handler);
637
+ const queue = await this.getQueueActions();
638
+ await handler.handleActionEnqueued(action, queue);
639
+ // start queue safely
640
+ return this.startQueueSafe();
641
+ }
642
+ async statusOfAction(actionId) {
643
+ const queue = await this.getQueueActions();
644
+ const actions = queue.filter((action) => action.id === actionId);
645
+ if (actions.length === 0) {
646
+ return Promise.reject('cannot update non-existent action');
647
+ }
648
+ const action = actions[0];
649
+ return action.status;
650
+ }
651
+ replaceAction(targetActionId, sourceActionId) {
652
+ return this.replaceOrMergeActions(targetActionId, sourceActionId, false);
653
+ }
654
+ mergeActions(targetActionId, sourceActionId) {
655
+ return this.replaceOrMergeActions(targetActionId, sourceActionId, true);
656
+ }
657
+ async retryAction(actionId) {
658
+ this.stopQueueManually(true);
659
+ const actions = await this.getQueueActions();
660
+ const target = actions[0];
661
+ if (!target || target.id !== actionId) {
662
+ throw Error(`Action ${actionId} not found at the head of the draft queue`);
663
+ }
664
+ if (!isDraftError(target)) {
665
+ throw Error(`Action ${actionId} is not in Error state`);
666
+ }
667
+ let pendingAction = {
668
+ ...target,
669
+ status: DraftActionStatus.Pending,
670
+ error: undefined,
671
+ };
672
+ await this.draftStore.writeAction(pendingAction);
673
+ await this.notifyChangedListeners({
674
+ type: DraftQueueEventType.ActionUpdated,
675
+ action: pendingAction,
676
+ draftCount: this.draftStore.getCount(),
677
+ });
678
+ await this.startQueueSafe();
679
+ return pendingAction;
680
+ }
681
+ async setMetadata(actionId, metadata) {
682
+ const keys$1 = keys(metadata);
683
+ const compatibleKeys = keys$1.filter((key) => {
684
+ const value = metadata[key];
685
+ return typeof key === 'string' && typeof value === 'string';
686
+ });
687
+ if (keys$1.length !== compatibleKeys.length) {
688
+ return Promise.reject('Cannot save incompatible metadata');
689
+ }
690
+ const queue = await this.getQueueActions();
691
+ const actions = queue.filter((action) => action.id === actionId);
692
+ if (actions.length === 0) {
693
+ return Promise.reject('cannot save metadata to non-existent action');
694
+ }
695
+ const action = actions[0];
696
+ const handler = this.getHandler(action.handler);
697
+ action.metadata = handler.updateMetadata(action.metadata, metadata);
698
+ await this.draftStore.writeAction(action);
699
+ await this.notifyChangedListeners({
700
+ type: DraftQueueEventType.ActionUpdated,
701
+ action: action,
702
+ draftCount: this.draftStore.getCount(),
703
+ });
704
+ return action;
705
+ }
706
+ async scheduleRetryWithSpecifiedDelay(retryDelayInMs) {
707
+ await this.notifyChangedListeners({
708
+ type: DraftQueueEventType.QueueStateChanged,
709
+ state: DraftQueueState.Waiting,
710
+ draftCount: this.draftStore.getCount(),
711
+ });
712
+ this.timeoutHandler = setTimeout(() => {
713
+ if (this.state !== DraftQueueState.Stopped) {
714
+ this.processNextAction();
715
+ }
716
+ }, retryDelayInMs);
717
+ }
718
+ async scheduleRetry() {
719
+ const newInterval = this.retryIntervalMilliseconds * 2;
720
+ this.retryIntervalMilliseconds = Math.min(Math.max(newInterval, this.minimumRetryInterval), this.maximumRetryInterval);
721
+ return this.scheduleRetryWithSpecifiedDelay(this.retryIntervalMilliseconds);
722
+ }
723
+ async getActionsForReplaceOrMerge(targetActionId, sourceActionId) {
724
+ const actions = await this.getQueueActions();
725
+ const target = actions.find((action) => action.id === targetActionId);
726
+ if (target === undefined) {
727
+ this.replacingAction = undefined;
728
+ await this.startQueueSafe();
729
+ throw Error(`targetActionId ${targetActionId} not found in the draft queue.`);
730
+ }
731
+ const source = actions.find((action) => action.id === sourceActionId);
732
+ if (source === undefined) {
733
+ this.replacingAction = undefined;
734
+ await this.startQueueSafe();
735
+ throw Error(`sourceActionId ${sourceActionId} not found in the draft queue.`);
736
+ }
737
+ return { target, source };
738
+ }
739
+ assertReplaceOrMergePrerequisites(target, source) {
740
+ // ensure actions are in a state to be replaced/merged
741
+ const { targetId: targetCacheKey, tag: targetTag, status: targetStatus, version: targetVersion, } = target;
742
+ const { targetId: sourceCacheKey, tag: sourceTag, status: sourceStatus, version: sourceVersion, } = source;
743
+ if (targetCacheKey !== sourceCacheKey) {
744
+ throw Error('Cannot replace/merge actions for different targetIds.');
745
+ }
746
+ if (targetTag !== sourceTag) {
747
+ throw Error('Cannot replace/merge actions for different tags.');
748
+ }
749
+ if (targetStatus === DraftActionStatus.Completed ||
750
+ targetStatus === DraftActionStatus.Uploading) {
751
+ throw Error(`Cannot replace/merge actions when targetAction is in ${targetStatus} status.`);
752
+ }
753
+ if (sourceStatus !== DraftActionStatus.Pending) {
754
+ throw Error(`Cannot replace/merge actions when sourceAction is in ${sourceStatus} status.`);
755
+ }
756
+ if (targetVersion !== sourceVersion) {
757
+ throw Error('Cannot replace/merge actions with different versions.');
758
+ }
759
+ }
760
+ async replaceOrMergeActions(targetActionId, sourceActionId, merge) {
761
+ // ids must be unique
762
+ if (targetActionId === sourceActionId) {
763
+ throw Error('targetActionId and sourceActionId cannot be the same.');
764
+ }
765
+ // cannot have a replace action already in progress
766
+ if (this.replacingAction !== undefined) {
767
+ throw Error('Cannot replace/merge actions while a replace/merge action operation is in progress.');
768
+ }
769
+ this.stopQueueManually(true);
770
+ const promise = this.getActionsForReplaceOrMerge(targetActionId, sourceActionId).then(async ({ target, source }) => {
771
+ // put in a try/finally block so we don't leave this.replacingAction
772
+ // indefinitely set
773
+ let updatedTarget;
774
+ try {
775
+ this.assertReplaceOrMergePrerequisites(target, source);
776
+ const handler = this.getHandler(target.handler);
777
+ updatedTarget = merge
778
+ ? handler.mergeActions(target, source)
779
+ : handler.handleReplaceAction(target, source);
780
+ // update the target
781
+ await this.draftStore.writeAction(updatedTarget);
782
+ await handler.handleActionReplaced(updatedTarget, source);
783
+ await this.notifyChangedListeners({
784
+ type: DraftQueueEventType.ActionUpdated,
785
+ action: updatedTarget,
786
+ draftCount: this.draftStore.getCount(),
787
+ });
788
+ // remove the source from queue
789
+ await this.removeDraftAction(sourceActionId);
790
+ }
791
+ finally {
792
+ this.replacingAction = undefined;
793
+ try {
794
+ await this.startQueueSafe();
795
+ }
796
+ catch {
797
+ // An error starting the queue should not bubble up from this method
798
+ }
799
+ }
800
+ return updatedTarget;
801
+ });
802
+ this.replacingAction = promise;
803
+ return promise;
804
+ }
805
+ }
806
+
807
+ function clone(obj) {
808
+ return parse(stringify(obj));
809
+ }
810
+
811
+ const DRAFT_ACTION_KEY_JUNCTION = '__DraftAction__';
812
+ function buildDraftDurableStoreKey(recordKey, draftActionId) {
813
+ return `${recordKey}${DRAFT_ACTION_KEY_JUNCTION}${draftActionId}`;
814
+ }
815
+ /**
816
+ * Implements a write-through InMemoryStore for Drafts, storing all drafts in a
817
+ * in-memory store with a write through to the DurableStore.
818
+ *
819
+ * Before any reads or writes come in from the draft queue, we need to revive the draft
820
+ * queue into memory. During this initial revive, any writes are queued up and operated on the
821
+ * queue once it's in memory. Similarly any reads are delayed until the queue is in memory.
822
+ *
823
+ */
824
+ class DurableDraftStore {
825
+ constructor(durableStore) {
826
+ this.draftStore = {};
827
+ // queue of writes that were made during the initial sync
828
+ this.writeQueue = [];
829
+ this.durableStore = durableStore;
830
+ this.resyncDraftStore();
831
+ }
832
+ writeAction(action) {
833
+ const addAction = () => {
834
+ const { id, tag } = action;
835
+ this.draftStore[id] = action;
836
+ const durableEntryKey = buildDraftDurableStoreKey(tag, id);
837
+ const entry = {
838
+ data: action,
839
+ };
840
+ const entries = { [durableEntryKey]: entry };
841
+ return this.durableStore.setEntries(entries, DRAFT_SEGMENT);
842
+ };
843
+ return this.enqueueAction(addAction);
844
+ }
845
+ getAllDrafts() {
846
+ const waitForOngoingSync = this.syncPromise || Promise.resolve();
847
+ return waitForOngoingSync.then(() => {
848
+ const { draftStore } = this;
849
+ const keys$1 = keys(draftStore);
850
+ const actionArray = [];
851
+ for (let i = 0, len = keys$1.length; i < len; i++) {
852
+ const key = keys$1[i];
853
+ // clone draft so we don't expose the internal draft store
854
+ actionArray.push(clone(draftStore[key]));
855
+ }
856
+ return actionArray;
857
+ });
858
+ }
859
+ deleteDraft(id) {
860
+ const deleteAction = () => {
861
+ const draft = this.draftStore[id];
862
+ if (draft !== undefined) {
863
+ delete this.draftStore[id];
864
+ const durableKey = buildDraftDurableStoreKey(draft.tag, draft.id);
865
+ return this.durableStore.evictEntries([durableKey], DRAFT_SEGMENT);
866
+ }
867
+ return Promise.resolve();
868
+ };
869
+ return this.enqueueAction(deleteAction);
870
+ }
871
+ deleteByTag(tag) {
872
+ const deleteAction = () => {
873
+ const { draftStore } = this;
874
+ const keys$1 = keys(draftStore);
875
+ const durableKeys = [];
876
+ for (let i = 0, len = keys$1.length; i < len; i++) {
877
+ const key = keys$1[i];
878
+ const action = draftStore[key];
879
+ if (action.tag === tag) {
880
+ delete draftStore[action.id];
881
+ durableKeys.push(buildDraftDurableStoreKey(action.tag, action.id));
882
+ }
883
+ }
884
+ return this.durableStore.evictEntries(durableKeys, DRAFT_SEGMENT);
885
+ };
886
+ return this.enqueueAction(deleteAction);
887
+ }
888
+ completeAction(queueOperations) {
889
+ const action = () => {
890
+ const durableStoreOperations = [];
891
+ const { draftStore } = this;
892
+ for (let i = 0, len = queueOperations.length; i < len; i++) {
893
+ const operation = queueOperations[i];
894
+ if (operation.type === QueueOperationType.Delete) {
895
+ const action = draftStore[operation.id];
896
+ if (action !== undefined) {
897
+ delete draftStore[operation.id];
898
+ const key = buildDraftDurableStoreKey(action.tag, action.id);
899
+ durableStoreOperations.push({
900
+ ids: [key],
901
+ type: 'evictEntries',
902
+ segment: DRAFT_SEGMENT,
903
+ });
904
+ }
905
+ }
906
+ else {
907
+ const { action } = operation;
908
+ const key = buildDraftDurableStoreKey(action.tag, action.id);
909
+ draftStore[action.id] = action;
910
+ durableStoreOperations.push({
911
+ type: 'setEntries',
912
+ segment: DRAFT_SEGMENT,
913
+ entries: {
914
+ [key]: {
915
+ data: operation.action,
916
+ },
917
+ },
918
+ });
919
+ }
920
+ }
921
+ return this.durableStore.batchOperations(durableStoreOperations);
922
+ };
923
+ return this.enqueueAction(action);
924
+ }
925
+ getCount() {
926
+ return keys(this.draftStore).length;
927
+ }
928
+ /**
929
+ * Runs a write operation against the draft store, if the initial
930
+ * revive is still in progress, the action gets enqueued to run once the
931
+ * initial revive is complete
932
+ * @param action
933
+ * @returns a promise that is resolved once the action has run
934
+ */
935
+ enqueueAction(action) {
936
+ const { syncPromise, writeQueue: pendingMerges } = this;
937
+ // if the initial sync is done and existing operations have been run, no need to queue, just run
938
+ if (syncPromise === undefined && pendingMerges.length === 0) {
939
+ return action();
940
+ }
941
+ const deferred = new Promise((resolve, reject) => {
942
+ this.writeQueue.push(() => {
943
+ return action()
944
+ .then((x) => {
945
+ resolve(x);
946
+ })
947
+ .catch((err) => reject(err));
948
+ });
949
+ });
950
+ return deferred;
951
+ }
952
+ /**
953
+ * Revives the draft store from the durable store. Once the draft store is
954
+ * revived, executes any queued up draft store operations that came in while
955
+ * reviving
956
+ */
957
+ resyncDraftStore() {
958
+ const sync = () => {
959
+ this.syncPromise = this.durableStore
960
+ .getAllEntries(DRAFT_SEGMENT)
961
+ .then((durableEntries) => {
962
+ if (durableEntries === undefined) {
963
+ this.draftStore = {};
964
+ return this.runQueuedOperations();
965
+ }
966
+ const { draftStore } = this;
967
+ const keys$1 = keys(durableEntries);
968
+ for (let i = 0, len = keys$1.length; i < len; i++) {
969
+ const entry = durableEntries[keys$1[i]];
970
+ const action = entry.data;
971
+ if (process.env.NODE_ENV !== 'production') {
972
+ // the `version` property was introduced in 242, we should assert version
973
+ // exists once we are sure there are no durable stores that contain
974
+ // versionless actions
975
+ if (action.version && action.version !== '242.0.0') {
976
+ return Promise.reject('Unexpected draft action version found in the durable store');
977
+ }
978
+ }
979
+ draftStore[action.id] = action;
980
+ }
981
+ return this.runQueuedOperations();
982
+ })
983
+ .finally(() => {
984
+ this.syncPromise = undefined;
985
+ });
986
+ return this.syncPromise;
987
+ };
988
+ // if there's an ongoing sync populating the in memory store, wait for it to complete before re-syncing
989
+ const { syncPromise } = this;
990
+ if (syncPromise === undefined) {
991
+ return sync();
992
+ }
993
+ return syncPromise.then(() => {
994
+ return sync();
995
+ });
996
+ }
997
+ /**
998
+ * Runs the operations that were queued up while reviving the
999
+ * draft store from the durable store
1000
+ */
1001
+ runQueuedOperations() {
1002
+ const { writeQueue } = this;
1003
+ if (writeQueue.length > 0) {
1004
+ const queueItem = writeQueue.shift();
1005
+ if (queueItem !== undefined) {
1006
+ return queueItem().then(() => {
1007
+ return this.runQueuedOperations();
1008
+ });
1009
+ }
1010
+ }
1011
+ return Promise.resolve();
1012
+ }
1013
+ }
1014
+
1015
+ /**
1016
+ * Denotes what kind of operation a DraftQueueItem represents.
1017
+ */
1018
+ var DraftActionOperationType;
1019
+ (function (DraftActionOperationType) {
1020
+ DraftActionOperationType["Create"] = "create";
1021
+ DraftActionOperationType["Update"] = "update";
1022
+ DraftActionOperationType["Delete"] = "delete";
1023
+ DraftActionOperationType["Custom"] = "custom";
1024
+ })(DraftActionOperationType || (DraftActionOperationType = {}));
1025
+ var DraftQueueOperationType;
1026
+ (function (DraftQueueOperationType) {
1027
+ DraftQueueOperationType["ItemAdded"] = "added";
1028
+ DraftQueueOperationType["ItemUploading"] = "uploading";
1029
+ DraftQueueOperationType["ItemDeleted"] = "deleted";
1030
+ DraftQueueOperationType["ItemCompleted"] = "completed";
1031
+ DraftQueueOperationType["ItemFailed"] = "failed";
1032
+ DraftQueueOperationType["ItemUpdated"] = "updated";
1033
+ DraftQueueOperationType["QueueStateChanged"] = "queueStateChanged";
1034
+ })(DraftQueueOperationType || (DraftQueueOperationType = {}));
1035
+ /**
1036
+ * Converts the internal DraftAction's ResourceRequest into
1037
+ * a DraftActionOperationType.
1038
+ * Returns a DraftActionOperationType as long as the http request is a
1039
+ * valid method type for DraftQueue or else it is undefined.
1040
+ * @param action
1041
+ */
1042
+ function getOperationTypeFrom(action) {
1043
+ if (isResourceRequestAction(action)) {
1044
+ if (action.data !== undefined && action.data.method !== undefined) {
1045
+ switch (action.data.method) {
1046
+ case 'put':
1047
+ case 'patch':
1048
+ return DraftActionOperationType.Update;
1049
+ case 'post':
1050
+ return DraftActionOperationType.Create;
1051
+ case 'delete':
1052
+ return DraftActionOperationType.Delete;
1053
+ default:
1054
+ // eslint-disable-next-line @salesforce/lds/no-error-in-production
1055
+ throw new Error(`${action.data.method} is an unsupported request method type for DraftQueue.`);
1056
+ }
1057
+ }
1058
+ else {
1059
+ // eslint-disable-next-line @salesforce/lds/no-error-in-production
1060
+ throw new Error(`action has no data found`);
1061
+ }
1062
+ }
1063
+ else {
1064
+ return DraftActionOperationType.Custom;
1065
+ }
1066
+ }
1067
+ function toQueueState(queue) {
1068
+ return (states) => {
1069
+ return {
1070
+ queueState: queue.getQueueState(),
1071
+ items: states,
1072
+ };
1073
+ };
1074
+ }
1075
+ class DraftManager {
1076
+ shouldEmitEvent(event) {
1077
+ return this.draftEventsShouldBeEmitted.includes(event.type);
1078
+ }
1079
+ constructor(draftQueue) {
1080
+ this.listeners = [];
1081
+ this.draftEventsShouldBeEmitted = [
1082
+ DraftQueueEventType.ActionAdded,
1083
+ DraftQueueEventType.ActionUploading,
1084
+ DraftQueueEventType.ActionCompleted,
1085
+ DraftQueueEventType.ActionDeleted,
1086
+ DraftQueueEventType.ActionFailed,
1087
+ DraftQueueEventType.ActionUpdated,
1088
+ DraftQueueEventType.QueueStateChanged,
1089
+ ];
1090
+ this.draftQueue = draftQueue;
1091
+ draftQueue.registerOnChangedListener((event) => {
1092
+ if (this.shouldEmitEvent(event)) {
1093
+ return this.callListeners(event);
1094
+ }
1095
+ return Promise.resolve();
1096
+ });
1097
+ }
1098
+ draftQueueEventTypeToOperationType(type) {
1099
+ switch (type) {
1100
+ case DraftQueueEventType.ActionAdded:
1101
+ return DraftQueueOperationType.ItemAdded;
1102
+ case DraftQueueEventType.ActionUploading:
1103
+ return DraftQueueOperationType.ItemUploading;
1104
+ case DraftQueueEventType.ActionCompleted:
1105
+ return DraftQueueOperationType.ItemCompleted;
1106
+ case DraftQueueEventType.ActionDeleted:
1107
+ return DraftQueueOperationType.ItemDeleted;
1108
+ case DraftQueueEventType.ActionFailed:
1109
+ return DraftQueueOperationType.ItemFailed;
1110
+ case DraftQueueEventType.ActionUpdated:
1111
+ return DraftQueueOperationType.ItemUpdated;
1112
+ default:
1113
+ throw Error('Unsupported event type');
1114
+ }
1115
+ }
1116
+ /**
1117
+ * Enqueue a custom action on the DraftQueue for a handler
1118
+ * @param handler the handler's id
1119
+ * @param targetId
1120
+ * @param tag - the key to group with in durable store
1121
+ * @param metadata
1122
+ * @returns
1123
+ */
1124
+ addCustomAction(handler, targetId, tag, metadata) {
1125
+ return this.draftQueue
1126
+ .enqueue(handler, {
1127
+ data: metadata,
1128
+ handler,
1129
+ targetId,
1130
+ tag,
1131
+ })
1132
+ .then((result) => {
1133
+ return this.buildDraftQueueItem(result);
1134
+ });
1135
+ }
1136
+ /**
1137
+ * Get the current state of each of the DraftActions in the DraftQueue
1138
+ * @returns A promise of an array of the state of each item in the DraftQueue
1139
+ */
1140
+ getQueue() {
1141
+ return this.draftQueue
1142
+ .getQueueActions()
1143
+ .then((queueActions) => {
1144
+ return queueActions.map(this.buildDraftQueueItem);
1145
+ })
1146
+ .then(toQueueState(this.draftQueue));
1147
+ }
1148
+ /**
1149
+ * Starts the draft queue and begins processing the first item in the queue.
1150
+ */
1151
+ startQueue() {
1152
+ return this.draftQueue.startQueue();
1153
+ }
1154
+ /**
1155
+ * Stops the draft queue from processing more draft items after any current
1156
+ * in progress items are finished.
1157
+ */
1158
+ stopQueue() {
1159
+ return this.draftQueue.stopQueue();
1160
+ }
1161
+ /**
1162
+ * Stops the draft queue manually without notifying the user and then runs the passed action.
1163
+ * After the action is completed it will safely start the queue if the user had set it to be
1164
+ * started.
1165
+ */
1166
+ stopQueueWhileRunning(action) {
1167
+ return this.draftQueue.stopQueueWhileRunning(action);
1168
+ }
1169
+ /**
1170
+ * Subscribes the listener to changes to the draft queue.
1171
+ *
1172
+ * Returns a closure to invoke in order to unsubscribe the listener
1173
+ * from changes to the draft queue.
1174
+ *
1175
+ * @param listener The listener closure to subscribe to changes
1176
+ */
1177
+ registerDraftQueueChangedListener(listener, _version = undefined) {
1178
+ this.listeners.push(listener);
1179
+ return () => {
1180
+ this.listeners = this.listeners.filter((l) => {
1181
+ return l !== listener;
1182
+ });
1183
+ return Promise.resolve();
1184
+ };
1185
+ }
1186
+ /**
1187
+ * Creates a custom action handler for the given handler
1188
+ * @param handlerId
1189
+ * @param executor
1190
+ * @returns
1191
+ */
1192
+ setCustomActionExecutor(handlerId, executor) {
1193
+ return this.draftQueue
1194
+ .addCustomHandler(handlerId, (action, completed) => {
1195
+ executor(this.buildDraftQueueItem(action), completed);
1196
+ })
1197
+ .then(() => {
1198
+ return () => {
1199
+ this.draftQueue.removeHandler(handlerId);
1200
+ return Promise.resolve();
1201
+ };
1202
+ });
1203
+ }
1204
+ async mergePerformQuickAction(actionId, fields) {
1205
+ if (!this.isValidFieldMap(fields)) {
1206
+ return Promise.reject('fields is not valid');
1207
+ }
1208
+ const queue = await this.draftQueue.getQueueActions();
1209
+ const actions = queue.filter((action) => action.id === actionId);
1210
+ if (actions.length === 0) {
1211
+ return Promise.reject('cannot edit non-existent action');
1212
+ }
1213
+ const action = actions[0];
1214
+ if (!this.isPerformQuickActionDraft(action, 'post')) {
1215
+ return Promise.reject('cannot edit incompatible action type or uploading actions');
1216
+ }
1217
+ action.data.body.fields = { ...action.data.body.fields, ...fields };
1218
+ action.status = DraftActionStatus.Pending;
1219
+ await this.draftQueue.updateDraftAction(action);
1220
+ return this.buildDraftQueueItem(action);
1221
+ }
1222
+ isValidFieldMap(fields) {
1223
+ const keys$1 = keys(fields);
1224
+ const validTypes = ['string', 'number', 'boolean'];
1225
+ for (let i = 0; i < keys$1.length; i++) {
1226
+ const key = keys$1[i];
1227
+ const value = fields[key];
1228
+ if (!validTypes.includes(typeof value) && value !== null) {
1229
+ return false;
1230
+ }
1231
+ }
1232
+ return true;
1233
+ }
1234
+ isPerformQuickActionDraft(action, method) {
1235
+ const data = action.data;
1236
+ const isPerformQuickAction = data.basePath.startsWith('/ui-api/actions/perform-quick-action/');
1237
+ const methodMatches = data.method === method;
1238
+ const notUploading = action.status !== DraftActionStatus.Uploading;
1239
+ return isPerformQuickAction && methodMatches && notUploading;
1240
+ }
1241
+ async mergePerformUpdateRecordQuickAction(actionId, fields) {
1242
+ if (!this.isValidFieldMap(fields)) {
1243
+ return Promise.reject('fields is not valid');
1244
+ }
1245
+ const queue = await this.draftQueue.getQueueActions();
1246
+ const actions = queue.filter((action) => action.id === actionId);
1247
+ if (actions.length === 0) {
1248
+ return Promise.reject('cannot edit non-existent action');
1249
+ }
1250
+ const action = actions[0];
1251
+ if (!this.isPerformQuickActionDraft(action, 'patch')) {
1252
+ return Promise.reject('cannot edit incompatible action type or uploading actions');
1253
+ }
1254
+ const data = action.data;
1255
+ data.body.fields = { ...data.body.fields, ...fields };
1256
+ action.status = DraftActionStatus.Pending;
1257
+ await this.draftQueue.updateDraftAction(action);
1258
+ return this.buildDraftQueueItem(action);
1259
+ }
1260
+ buildDraftQueueItem(action) {
1261
+ const operationType = getOperationTypeFrom(action);
1262
+ const { id, status, timestamp, targetId, metadata } = action;
1263
+ const item = {
1264
+ id,
1265
+ targetId,
1266
+ state: status,
1267
+ timestamp,
1268
+ operationType,
1269
+ metadata,
1270
+ };
1271
+ if (isDraftError(action)) {
1272
+ // We should always return an array, if the body is just a dictionary,
1273
+ // stick it in an array
1274
+ const body = isArray(action.error.body) ? action.error.body : [action.error.body];
1275
+ const bodyString = stringify(body);
1276
+ item.error = {
1277
+ status: action.error.status || 0,
1278
+ ok: action.error.ok || false,
1279
+ headers: action.error.headers || {},
1280
+ statusText: action.error.statusText || '',
1281
+ bodyString,
1282
+ };
1283
+ }
1284
+ return item;
1285
+ }
1286
+ async callListeners(event) {
1287
+ if (this.listeners.length < 1) {
1288
+ return;
1289
+ }
1290
+ const managerState = await this.getQueue();
1291
+ let operationType, item;
1292
+ if (isDraftQueueStateChangeEvent(event)) {
1293
+ operationType = DraftQueueOperationType.QueueStateChanged;
1294
+ }
1295
+ else {
1296
+ const { action, type } = event;
1297
+ item = this.buildDraftQueueItem(action);
1298
+ operationType = this.draftQueueEventTypeToOperationType(type);
1299
+ }
1300
+ for (let i = 0, len = this.listeners.length; i < len; i++) {
1301
+ const listener = this.listeners[i];
1302
+ listener(managerState, operationType, item);
1303
+ }
1304
+ }
1305
+ /**
1306
+ * Removes the draft action identified by actionId from the draft queue.
1307
+ *
1308
+ * @param actionId The action identifier
1309
+ *
1310
+ * @returns The current state of the draft queue
1311
+ */
1312
+ removeDraftAction(actionId) {
1313
+ return this.draftQueue.removeDraftAction(actionId).then(() => this.getQueue());
1314
+ }
1315
+ /**
1316
+ * Replaces the resource request of `withAction` for the resource request
1317
+ * of `actionId`. Action ids cannot be equal. Both actions must be acting
1318
+ * on the same target object, and neither can currently be in progress.
1319
+ *
1320
+ * @param actionId The id of the draft action to replace
1321
+ * @param withActionId The id of the draft action that will replace the other
1322
+ */
1323
+ replaceAction(actionId, withActionId) {
1324
+ return this.draftQueue.replaceAction(actionId, withActionId).then((replaced) => {
1325
+ return this.buildDraftQueueItem(replaced);
1326
+ });
1327
+ }
1328
+ /**
1329
+ * Merges two actions into a single target action. The target action maintains
1330
+ * its position in the queue, while the source action is removed from the queue.
1331
+ * Action ids cannot be equal. Both actions must be acting on the same target
1332
+ * object, and neither can currently be in progress.
1333
+ *
1334
+ * @param targetActionId The draft action id of the target action. This action
1335
+ * will be replaced with the merged result.
1336
+ * @param sourceActionId The draft action id to merge onto the target. This
1337
+ * action will be removed after the merge.
1338
+ */
1339
+ mergeActions(targetActionId, sourceActionId) {
1340
+ return this.draftQueue.mergeActions(targetActionId, sourceActionId).then((merged) => {
1341
+ return this.buildDraftQueueItem(merged);
1342
+ });
1343
+ }
1344
+ /**
1345
+ * Sets the metadata object of the specified action to the
1346
+ * provided metadata
1347
+ * @param actionId The id of the action to set the metadata on
1348
+ * @param metadata The metadata to set on the specified action
1349
+ */
1350
+ setMetadata(actionId, metadata) {
1351
+ return this.draftQueue.setMetadata(actionId, metadata).then((updatedAction) => {
1352
+ return this.buildDraftQueueItem(updatedAction);
1353
+ });
1354
+ }
1355
+ /**
1356
+ * Retries a draft action that is in error state without any modification.
1357
+ *
1358
+ * @param actionId The id of the draft action that should be retried
1359
+ * @throws If the draft action is not at the front of the draft queue or is not
1360
+ * in error state.
1361
+ */
1362
+ async retryAction(actionId) {
1363
+ return this.draftQueue.retryAction(actionId).then((updatedAction) => {
1364
+ return this.buildDraftQueueItem(updatedAction);
1365
+ });
1366
+ }
1367
+ }
1368
+ function isResourceRequestAction(action) {
1369
+ const dataAsAny = action.data;
1370
+ return (dataAsAny !== undefined && dataAsAny.method !== undefined && dataAsAny.body !== undefined);
1371
+ }
1372
+
1373
+ export { CustomActionResultType, DRAFT_ERROR_CODE, DRAFT_SEGMENT, DraftActionOperationType, DraftActionStatus, DraftErrorFetchResponse, DraftFetchResponse, DraftManager, DraftQueueEventType, DraftQueueState, DraftSynthesisError, DurableDraftQueue, DurableDraftStore, ProcessActionResult, QueueOperationType, createBadRequestResponse, createDeletedResponse, createDraftSynthesisErrorResponse, createInternalErrorResponse, createNotFoundResponse, createOkResponse, generateUniqueDraftActionId, isDraftSynthesisError, transformErrorToDraftSynthesisError };