@salesforce/lds-drafts 1.124.2 → 1.124.3
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 +1713 -1713
- package/dist/{DraftAwareAdapter.d.ts → types/DraftAwareAdapter.d.ts} +6 -6
- package/dist/{DraftFetchResponse.d.ts → types/DraftFetchResponse.d.ts} +28 -28
- package/dist/{DraftIdMapping.d.ts → types/DraftIdMapping.d.ts} +12 -12
- package/dist/{DraftManager.d.ts → types/DraftManager.d.ts} +161 -161
- package/dist/{DraftQueue.d.ts → types/DraftQueue.d.ts} +233 -233
- package/dist/{DraftStore.d.ts → types/DraftStore.d.ts} +12 -12
- package/dist/{DraftSynthesisError.d.ts → types/DraftSynthesisError.d.ts} +6 -6
- package/dist/{DurableDraftQueue.d.ts → types/DurableDraftQueue.d.ts} +52 -52
- package/dist/{DurableDraftStore.d.ts → types/DurableDraftStore.d.ts} +44 -44
- package/dist/{actionHandlers → types/actionHandlers}/AbstractResourceRequestActionHandler.d.ts +53 -53
- package/dist/{actionHandlers → types/actionHandlers}/ActionHandler.d.ts +149 -149
- package/dist/{actionHandlers → types/actionHandlers}/CustomActionHandler.d.ts +33 -33
- package/dist/{main.d.ts → types/main.d.ts} +15 -15
- package/dist/{makeEnvironmentDraftAware.d.ts → types/makeEnvironmentDraftAware.d.ts} +4 -4
- package/dist/{utils → types/utils}/adapter.d.ts +2 -2
- package/dist/{utils → types/utils}/clone.d.ts +1 -1
- package/dist/{utils → types/utils}/id.d.ts +5 -5
- package/dist/{utils → types/utils}/language.d.ts +24 -24
- package/package.json +3 -3
package/dist/ldsDrafts.js
CHANGED
|
@@ -7,1743 +7,1743 @@
|
|
|
7
7
|
import { HttpStatusCode, StoreKeyMap } from '@luvio/engine';
|
|
8
8
|
import { AsyncWorkerPool } from '@salesforce/lds-utils-adapters';
|
|
9
9
|
|
|
10
|
-
var DraftActionStatus;
|
|
11
|
-
(function (DraftActionStatus) {
|
|
12
|
-
DraftActionStatus["Pending"] = "pending";
|
|
13
|
-
DraftActionStatus["Uploading"] = "uploading";
|
|
14
|
-
DraftActionStatus["Error"] = "error";
|
|
15
|
-
DraftActionStatus["Completed"] = "completed";
|
|
16
|
-
})(DraftActionStatus || (DraftActionStatus = {}));
|
|
17
|
-
function isDraftError(draft) {
|
|
18
|
-
return draft.status === DraftActionStatus.Error;
|
|
19
|
-
}
|
|
20
|
-
function isDraftQueueStateChangeEvent(event) {
|
|
21
|
-
return event.type === DraftQueueEventType.QueueStateChanged;
|
|
22
|
-
}
|
|
23
|
-
var ProcessActionResult;
|
|
24
|
-
(function (ProcessActionResult) {
|
|
25
|
-
// non-2xx network error, requires user intervention
|
|
26
|
-
ProcessActionResult["ACTION_ERRORED"] = "ERROR";
|
|
27
|
-
// upload succeeded
|
|
28
|
-
ProcessActionResult["ACTION_SUCCEEDED"] = "SUCCESS";
|
|
29
|
-
// queue is empty
|
|
30
|
-
ProcessActionResult["NO_ACTION_TO_PROCESS"] = "NO_ACTION_TO_PROCESS";
|
|
31
|
-
// network request is in flight
|
|
32
|
-
ProcessActionResult["ACTION_ALREADY_PROCESSING"] = "ACTION_ALREADY_PROCESSING";
|
|
33
|
-
// network call failed (offline)
|
|
34
|
-
ProcessActionResult["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
35
|
-
// queue is blocked on an error that requires user intervention
|
|
36
|
-
ProcessActionResult["BLOCKED_ON_ERROR"] = "BLOCKED_ON_ERROR";
|
|
37
|
-
//waiting for user to execute custom action
|
|
38
|
-
ProcessActionResult["CUSTOM_ACTION_WAITING"] = "CUSTOM_ACTION_WAITING";
|
|
39
|
-
})(ProcessActionResult || (ProcessActionResult = {}));
|
|
40
|
-
var DraftQueueState;
|
|
41
|
-
(function (DraftQueueState) {
|
|
42
|
-
/** Currently processing an item in the queue or queue is empty and waiting to process the next item. */
|
|
43
|
-
DraftQueueState["Started"] = "started";
|
|
44
|
-
/**
|
|
45
|
-
* The queue is stopped and will not attempt to upload any drafts until startDraftQueue() is called.
|
|
46
|
-
* This is the initial state when the DraftQueue gets instantiated.
|
|
47
|
-
*/
|
|
48
|
-
DraftQueueState["Stopped"] = "stopped";
|
|
49
|
-
/**
|
|
50
|
-
* The queue is stopped due to a blocking error from the last upload attempt.
|
|
51
|
-
* The queue will not run again until startDraftQueue() is called.
|
|
52
|
-
*/
|
|
53
|
-
DraftQueueState["Error"] = "error";
|
|
54
|
-
/**
|
|
55
|
-
* There was a network error and the queue will attempt to upload again shortly.
|
|
56
|
-
* To attempt to force an upload now call startDraftQueue().
|
|
57
|
-
*/
|
|
58
|
-
DraftQueueState["Waiting"] = "waiting";
|
|
59
|
-
})(DraftQueueState || (DraftQueueState = {}));
|
|
60
|
-
var DraftQueueEventType;
|
|
61
|
-
(function (DraftQueueEventType) {
|
|
62
|
-
/**
|
|
63
|
-
* Triggered after an action had been added to the queue
|
|
64
|
-
*/
|
|
65
|
-
DraftQueueEventType["ActionAdded"] = "added";
|
|
66
|
-
/**
|
|
67
|
-
* Triggered once an action failed
|
|
68
|
-
*/
|
|
69
|
-
DraftQueueEventType["ActionFailed"] = "failed";
|
|
70
|
-
/**
|
|
71
|
-
* Triggered after an action has been deleted from the queue
|
|
72
|
-
*/
|
|
73
|
-
DraftQueueEventType["ActionDeleted"] = "deleted";
|
|
74
|
-
/**
|
|
75
|
-
* Triggered after an action has been completed and after it has been removed from the queue
|
|
76
|
-
*/
|
|
77
|
-
DraftQueueEventType["ActionCompleted"] = "completed";
|
|
78
|
-
/**
|
|
79
|
-
* Triggered after an action has been updated by the updateAction API
|
|
80
|
-
*/
|
|
81
|
-
DraftQueueEventType["ActionUpdated"] = "updated";
|
|
82
|
-
/**
|
|
83
|
-
* Triggered after the Draft Queue state changes
|
|
84
|
-
*/
|
|
85
|
-
DraftQueueEventType["QueueStateChanged"] = "state";
|
|
86
|
-
})(DraftQueueEventType || (DraftQueueEventType = {}));
|
|
87
|
-
var QueueOperationType;
|
|
88
|
-
(function (QueueOperationType) {
|
|
89
|
-
QueueOperationType["Add"] = "add";
|
|
90
|
-
QueueOperationType["Delete"] = "delete";
|
|
91
|
-
QueueOperationType["Update"] = "update";
|
|
10
|
+
var DraftActionStatus;
|
|
11
|
+
(function (DraftActionStatus) {
|
|
12
|
+
DraftActionStatus["Pending"] = "pending";
|
|
13
|
+
DraftActionStatus["Uploading"] = "uploading";
|
|
14
|
+
DraftActionStatus["Error"] = "error";
|
|
15
|
+
DraftActionStatus["Completed"] = "completed";
|
|
16
|
+
})(DraftActionStatus || (DraftActionStatus = {}));
|
|
17
|
+
function isDraftError(draft) {
|
|
18
|
+
return draft.status === DraftActionStatus.Error;
|
|
19
|
+
}
|
|
20
|
+
function isDraftQueueStateChangeEvent(event) {
|
|
21
|
+
return event.type === DraftQueueEventType.QueueStateChanged;
|
|
22
|
+
}
|
|
23
|
+
var ProcessActionResult;
|
|
24
|
+
(function (ProcessActionResult) {
|
|
25
|
+
// non-2xx network error, requires user intervention
|
|
26
|
+
ProcessActionResult["ACTION_ERRORED"] = "ERROR";
|
|
27
|
+
// upload succeeded
|
|
28
|
+
ProcessActionResult["ACTION_SUCCEEDED"] = "SUCCESS";
|
|
29
|
+
// queue is empty
|
|
30
|
+
ProcessActionResult["NO_ACTION_TO_PROCESS"] = "NO_ACTION_TO_PROCESS";
|
|
31
|
+
// network request is in flight
|
|
32
|
+
ProcessActionResult["ACTION_ALREADY_PROCESSING"] = "ACTION_ALREADY_PROCESSING";
|
|
33
|
+
// network call failed (offline)
|
|
34
|
+
ProcessActionResult["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
35
|
+
// queue is blocked on an error that requires user intervention
|
|
36
|
+
ProcessActionResult["BLOCKED_ON_ERROR"] = "BLOCKED_ON_ERROR";
|
|
37
|
+
//waiting for user to execute custom action
|
|
38
|
+
ProcessActionResult["CUSTOM_ACTION_WAITING"] = "CUSTOM_ACTION_WAITING";
|
|
39
|
+
})(ProcessActionResult || (ProcessActionResult = {}));
|
|
40
|
+
var DraftQueueState;
|
|
41
|
+
(function (DraftQueueState) {
|
|
42
|
+
/** Currently processing an item in the queue or queue is empty and waiting to process the next item. */
|
|
43
|
+
DraftQueueState["Started"] = "started";
|
|
44
|
+
/**
|
|
45
|
+
* The queue is stopped and will not attempt to upload any drafts until startDraftQueue() is called.
|
|
46
|
+
* This is the initial state when the DraftQueue gets instantiated.
|
|
47
|
+
*/
|
|
48
|
+
DraftQueueState["Stopped"] = "stopped";
|
|
49
|
+
/**
|
|
50
|
+
* The queue is stopped due to a blocking error from the last upload attempt.
|
|
51
|
+
* The queue will not run again until startDraftQueue() is called.
|
|
52
|
+
*/
|
|
53
|
+
DraftQueueState["Error"] = "error";
|
|
54
|
+
/**
|
|
55
|
+
* There was a network error and the queue will attempt to upload again shortly.
|
|
56
|
+
* To attempt to force an upload now call startDraftQueue().
|
|
57
|
+
*/
|
|
58
|
+
DraftQueueState["Waiting"] = "waiting";
|
|
59
|
+
})(DraftQueueState || (DraftQueueState = {}));
|
|
60
|
+
var DraftQueueEventType;
|
|
61
|
+
(function (DraftQueueEventType) {
|
|
62
|
+
/**
|
|
63
|
+
* Triggered after an action had been added to the queue
|
|
64
|
+
*/
|
|
65
|
+
DraftQueueEventType["ActionAdded"] = "added";
|
|
66
|
+
/**
|
|
67
|
+
* Triggered once an action failed
|
|
68
|
+
*/
|
|
69
|
+
DraftQueueEventType["ActionFailed"] = "failed";
|
|
70
|
+
/**
|
|
71
|
+
* Triggered after an action has been deleted from the queue
|
|
72
|
+
*/
|
|
73
|
+
DraftQueueEventType["ActionDeleted"] = "deleted";
|
|
74
|
+
/**
|
|
75
|
+
* Triggered after an action has been completed and after it has been removed from the queue
|
|
76
|
+
*/
|
|
77
|
+
DraftQueueEventType["ActionCompleted"] = "completed";
|
|
78
|
+
/**
|
|
79
|
+
* Triggered after an action has been updated by the updateAction API
|
|
80
|
+
*/
|
|
81
|
+
DraftQueueEventType["ActionUpdated"] = "updated";
|
|
82
|
+
/**
|
|
83
|
+
* Triggered after the Draft Queue state changes
|
|
84
|
+
*/
|
|
85
|
+
DraftQueueEventType["QueueStateChanged"] = "state";
|
|
86
|
+
})(DraftQueueEventType || (DraftQueueEventType = {}));
|
|
87
|
+
var QueueOperationType;
|
|
88
|
+
(function (QueueOperationType) {
|
|
89
|
+
QueueOperationType["Add"] = "add";
|
|
90
|
+
QueueOperationType["Delete"] = "delete";
|
|
91
|
+
QueueOperationType["Update"] = "update";
|
|
92
92
|
})(QueueOperationType || (QueueOperationType = {}));
|
|
93
93
|
|
|
94
|
-
class DraftSynthesisError extends Error {
|
|
95
|
-
constructor(message, errorType) {
|
|
96
|
-
super(message);
|
|
97
|
-
this.errorType = errorType;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function isDraftSynthesisError(error) {
|
|
101
|
-
return error.errorType !== undefined;
|
|
94
|
+
class DraftSynthesisError extends Error {
|
|
95
|
+
constructor(message, errorType) {
|
|
96
|
+
super(message);
|
|
97
|
+
this.errorType = errorType;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function isDraftSynthesisError(error) {
|
|
101
|
+
return error.errorType !== undefined;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const DRAFT_ERROR_CODE = 'DRAFT_ERROR';
|
|
105
|
-
class DraftFetchResponse {
|
|
106
|
-
constructor(status, body) {
|
|
107
|
-
this.headers = {};
|
|
108
|
-
this.status = status;
|
|
109
|
-
this.body = body;
|
|
110
|
-
}
|
|
111
|
-
get statusText() {
|
|
112
|
-
const { status } = this;
|
|
113
|
-
switch (status) {
|
|
114
|
-
case HttpStatusCode.Ok:
|
|
115
|
-
return 'OK';
|
|
116
|
-
case HttpStatusCode.Created:
|
|
117
|
-
return 'Created';
|
|
118
|
-
case HttpStatusCode.NoContent:
|
|
119
|
-
return 'No Content';
|
|
120
|
-
case HttpStatusCode.BadRequest:
|
|
121
|
-
return 'Bad Request';
|
|
122
|
-
case HttpStatusCode.ServerError:
|
|
123
|
-
return 'Server Error';
|
|
124
|
-
default:
|
|
125
|
-
return `Unexpected HTTP Status Code: ${status}`;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
get ok() {
|
|
129
|
-
return this.status >= 200 && this.status < 300;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
class DraftErrorFetchResponse {
|
|
133
|
-
constructor(status, body) {
|
|
134
|
-
this.ok = false;
|
|
135
|
-
this.headers = {};
|
|
136
|
-
this.errorType = 'fetchResponse';
|
|
137
|
-
this.status = status;
|
|
138
|
-
this.body = body;
|
|
139
|
-
}
|
|
140
|
-
get statusText() {
|
|
141
|
-
const { status } = this;
|
|
142
|
-
switch (status) {
|
|
143
|
-
case HttpStatusCode.BadRequest:
|
|
144
|
-
return 'Bad Request';
|
|
145
|
-
case HttpStatusCode.ServerError:
|
|
146
|
-
return 'Server Error';
|
|
147
|
-
case HttpStatusCode.NotFound:
|
|
148
|
-
return 'Not Found';
|
|
149
|
-
default:
|
|
150
|
-
return `Unexpected HTTP Status Code: ${status}`;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function createOkResponse(body) {
|
|
155
|
-
return new DraftFetchResponse(HttpStatusCode.Ok, body);
|
|
156
|
-
}
|
|
157
|
-
function createBadRequestResponse(body) {
|
|
158
|
-
return new DraftErrorFetchResponse(HttpStatusCode.BadRequest, body);
|
|
159
|
-
}
|
|
160
|
-
function createNotFoundResponse(body) {
|
|
161
|
-
return new DraftErrorFetchResponse(HttpStatusCode.NotFound, body);
|
|
162
|
-
}
|
|
163
|
-
function transformErrorToDraftSynthesisError(error) {
|
|
164
|
-
if (isDraftSynthesisError(error)) {
|
|
165
|
-
const { errorType, message } = error;
|
|
166
|
-
return createDraftSynthesisErrorResponse(message, errorType);
|
|
167
|
-
}
|
|
168
|
-
return createDraftSynthesisErrorResponse(error.message);
|
|
169
|
-
}
|
|
170
|
-
function createDraftSynthesisErrorResponse(message = 'failed to synthesize draft response', errorType) {
|
|
171
|
-
const error = {
|
|
172
|
-
errorCode: DRAFT_ERROR_CODE,
|
|
173
|
-
message: message,
|
|
174
|
-
};
|
|
175
|
-
if (errorType !== undefined) {
|
|
176
|
-
error.errorType = errorType;
|
|
177
|
-
}
|
|
178
|
-
return new DraftErrorFetchResponse(HttpStatusCode.BadRequest, error);
|
|
179
|
-
}
|
|
180
|
-
function createDeletedResponse() {
|
|
181
|
-
return new DraftFetchResponse(HttpStatusCode.NoContent, undefined);
|
|
182
|
-
}
|
|
183
|
-
function createInternalErrorResponse() {
|
|
184
|
-
return new DraftErrorFetchResponse(HttpStatusCode.ServerError, undefined);
|
|
104
|
+
const DRAFT_ERROR_CODE = 'DRAFT_ERROR';
|
|
105
|
+
class DraftFetchResponse {
|
|
106
|
+
constructor(status, body) {
|
|
107
|
+
this.headers = {};
|
|
108
|
+
this.status = status;
|
|
109
|
+
this.body = body;
|
|
110
|
+
}
|
|
111
|
+
get statusText() {
|
|
112
|
+
const { status } = this;
|
|
113
|
+
switch (status) {
|
|
114
|
+
case HttpStatusCode.Ok:
|
|
115
|
+
return 'OK';
|
|
116
|
+
case HttpStatusCode.Created:
|
|
117
|
+
return 'Created';
|
|
118
|
+
case HttpStatusCode.NoContent:
|
|
119
|
+
return 'No Content';
|
|
120
|
+
case HttpStatusCode.BadRequest:
|
|
121
|
+
return 'Bad Request';
|
|
122
|
+
case HttpStatusCode.ServerError:
|
|
123
|
+
return 'Server Error';
|
|
124
|
+
default:
|
|
125
|
+
return `Unexpected HTTP Status Code: ${status}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
get ok() {
|
|
129
|
+
return this.status >= 200 && this.status < 300;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
class DraftErrorFetchResponse {
|
|
133
|
+
constructor(status, body) {
|
|
134
|
+
this.ok = false;
|
|
135
|
+
this.headers = {};
|
|
136
|
+
this.errorType = 'fetchResponse';
|
|
137
|
+
this.status = status;
|
|
138
|
+
this.body = body;
|
|
139
|
+
}
|
|
140
|
+
get statusText() {
|
|
141
|
+
const { status } = this;
|
|
142
|
+
switch (status) {
|
|
143
|
+
case HttpStatusCode.BadRequest:
|
|
144
|
+
return 'Bad Request';
|
|
145
|
+
case HttpStatusCode.ServerError:
|
|
146
|
+
return 'Server Error';
|
|
147
|
+
case HttpStatusCode.NotFound:
|
|
148
|
+
return 'Not Found';
|
|
149
|
+
default:
|
|
150
|
+
return `Unexpected HTTP Status Code: ${status}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function createOkResponse(body) {
|
|
155
|
+
return new DraftFetchResponse(HttpStatusCode.Ok, body);
|
|
156
|
+
}
|
|
157
|
+
function createBadRequestResponse(body) {
|
|
158
|
+
return new DraftErrorFetchResponse(HttpStatusCode.BadRequest, body);
|
|
159
|
+
}
|
|
160
|
+
function createNotFoundResponse(body) {
|
|
161
|
+
return new DraftErrorFetchResponse(HttpStatusCode.NotFound, body);
|
|
162
|
+
}
|
|
163
|
+
function transformErrorToDraftSynthesisError(error) {
|
|
164
|
+
if (isDraftSynthesisError(error)) {
|
|
165
|
+
const { errorType, message } = error;
|
|
166
|
+
return createDraftSynthesisErrorResponse(message, errorType);
|
|
167
|
+
}
|
|
168
|
+
return createDraftSynthesisErrorResponse(error.message);
|
|
169
|
+
}
|
|
170
|
+
function createDraftSynthesisErrorResponse(message = 'failed to synthesize draft response', errorType) {
|
|
171
|
+
const error = {
|
|
172
|
+
errorCode: DRAFT_ERROR_CODE,
|
|
173
|
+
message: message,
|
|
174
|
+
};
|
|
175
|
+
if (errorType !== undefined) {
|
|
176
|
+
error.errorType = errorType;
|
|
177
|
+
}
|
|
178
|
+
return new DraftErrorFetchResponse(HttpStatusCode.BadRequest, error);
|
|
179
|
+
}
|
|
180
|
+
function createDeletedResponse() {
|
|
181
|
+
return new DraftFetchResponse(HttpStatusCode.NoContent, undefined);
|
|
182
|
+
}
|
|
183
|
+
function createInternalErrorResponse() {
|
|
184
|
+
return new DraftErrorFetchResponse(HttpStatusCode.ServerError, undefined);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
const { keys, create, assign, values } = Object;
|
|
188
|
-
const { stringify, parse } = JSON;
|
|
187
|
+
const { keys, create, assign, values } = Object;
|
|
188
|
+
const { stringify, parse } = JSON;
|
|
189
189
|
const { isArray } = Array;
|
|
190
190
|
|
|
191
|
-
function buildLuvioOverrideForDraftAdapters(luvio, handler, extractTargetIdFromCacheKey, options = {}) {
|
|
192
|
-
// override this to create and enqueue a new draft action, and return synthetic response
|
|
193
|
-
const dispatchResourceRequest = async function (resourceRequest, _context) {
|
|
194
|
-
const { data } = await handler.enqueue(resourceRequest).catch((err) => {
|
|
195
|
-
throw transformErrorToDraftSynthesisError(err);
|
|
196
|
-
});
|
|
197
|
-
if (data === undefined) {
|
|
198
|
-
return Promise.reject(createDraftSynthesisErrorResponse());
|
|
199
|
-
}
|
|
200
|
-
return createOkResponse(data);
|
|
201
|
-
};
|
|
202
|
-
// override this to use an infinitely large ttl so the cache entry never expires
|
|
203
|
-
const publishStoreMetadata = function (key, storeMetadataParams) {
|
|
204
|
-
// if we aren't publishing a draft then use base luvio method
|
|
205
|
-
const id = extractTargetIdFromCacheKey(key);
|
|
206
|
-
if (id === undefined || !handler.isDraftId(id)) {
|
|
207
|
-
return luvio.publishStoreMetadata(key, storeMetadataParams);
|
|
208
|
-
}
|
|
209
|
-
return luvio.publishStoreMetadata(key, {
|
|
210
|
-
...storeMetadataParams,
|
|
211
|
-
ttl: Number.MAX_SAFE_INTEGER,
|
|
212
|
-
});
|
|
213
|
-
};
|
|
214
|
-
if (options.forDeleteAdapter === true) {
|
|
215
|
-
// delete adapters attempt to evict the record on successful network response,
|
|
216
|
-
// since draft-aware delete adapters do soft-delete (record stays in cache
|
|
217
|
-
// with a "drafts.deleted" property) we want storeEvict to just be a no-op
|
|
218
|
-
const storeEvict = function (_key) {
|
|
219
|
-
// no-op
|
|
220
|
-
};
|
|
221
|
-
return create(luvio, {
|
|
222
|
-
dispatchResourceRequest: { value: dispatchResourceRequest },
|
|
223
|
-
publishStoreMetadata: { value: publishStoreMetadata },
|
|
224
|
-
storeEvict: { value: storeEvict },
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
return create(luvio, {
|
|
228
|
-
dispatchResourceRequest: { value: dispatchResourceRequest },
|
|
229
|
-
publishStoreMetadata: { value: publishStoreMetadata },
|
|
230
|
-
});
|
|
191
|
+
function buildLuvioOverrideForDraftAdapters(luvio, handler, extractTargetIdFromCacheKey, options = {}) {
|
|
192
|
+
// override this to create and enqueue a new draft action, and return synthetic response
|
|
193
|
+
const dispatchResourceRequest = async function (resourceRequest, _context) {
|
|
194
|
+
const { data } = await handler.enqueue(resourceRequest).catch((err) => {
|
|
195
|
+
throw transformErrorToDraftSynthesisError(err);
|
|
196
|
+
});
|
|
197
|
+
if (data === undefined) {
|
|
198
|
+
return Promise.reject(createDraftSynthesisErrorResponse());
|
|
199
|
+
}
|
|
200
|
+
return createOkResponse(data);
|
|
201
|
+
};
|
|
202
|
+
// override this to use an infinitely large ttl so the cache entry never expires
|
|
203
|
+
const publishStoreMetadata = function (key, storeMetadataParams) {
|
|
204
|
+
// if we aren't publishing a draft then use base luvio method
|
|
205
|
+
const id = extractTargetIdFromCacheKey(key);
|
|
206
|
+
if (id === undefined || !handler.isDraftId(id)) {
|
|
207
|
+
return luvio.publishStoreMetadata(key, storeMetadataParams);
|
|
208
|
+
}
|
|
209
|
+
return luvio.publishStoreMetadata(key, {
|
|
210
|
+
...storeMetadataParams,
|
|
211
|
+
ttl: Number.MAX_SAFE_INTEGER,
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
if (options.forDeleteAdapter === true) {
|
|
215
|
+
// delete adapters attempt to evict the record on successful network response,
|
|
216
|
+
// since draft-aware delete adapters do soft-delete (record stays in cache
|
|
217
|
+
// with a "drafts.deleted" property) we want storeEvict to just be a no-op
|
|
218
|
+
const storeEvict = function (_key) {
|
|
219
|
+
// no-op
|
|
220
|
+
};
|
|
221
|
+
return create(luvio, {
|
|
222
|
+
dispatchResourceRequest: { value: dispatchResourceRequest },
|
|
223
|
+
publishStoreMetadata: { value: publishStoreMetadata },
|
|
224
|
+
storeEvict: { value: storeEvict },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return create(luvio, {
|
|
228
|
+
dispatchResourceRequest: { value: dispatchResourceRequest },
|
|
229
|
+
publishStoreMetadata: { value: publishStoreMetadata },
|
|
230
|
+
});
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
const DraftIdMappingKeyPrefix240 = 'DraftIdMapping::';
|
|
234
|
-
const DraftKeyMappingKeyPrefix = 'DraftKeyMapping::V2::';
|
|
235
|
-
const DRAFT_ID_MAPPINGS_SEGMENT = 'DRAFT_ID_MAPPINGS';
|
|
236
|
-
function isLegacyDraftIdMapping(key, data) {
|
|
237
|
-
return key.startsWith(DraftIdMappingKeyPrefix240);
|
|
238
|
-
}
|
|
239
|
-
// TODO [W-11677776]: in 242 we changed the format to store keys instead of ids
|
|
240
|
-
// this can be removed when we drop support for id storing
|
|
241
|
-
function getRecordKeyForId(id) {
|
|
242
|
-
return `UiApi::RecordRepresentation:${id}`;
|
|
243
|
-
}
|
|
244
|
-
function generateDraftIdMappingKey(draftIdMapping) {
|
|
245
|
-
return `${DraftKeyMappingKeyPrefix}${draftIdMapping.draftKey}::${draftIdMapping.canonicalKey}`;
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
*
|
|
249
|
-
* @param mappingIds (optional) requested mapping ids, if undefined all will be retrieved
|
|
250
|
-
*/
|
|
251
|
-
async function getDraftIdMappings(durableStore, mappingIds) {
|
|
252
|
-
const mappings = [];
|
|
253
|
-
let durableStoreOperation;
|
|
254
|
-
if (mappingIds === undefined) {
|
|
255
|
-
durableStoreOperation =
|
|
256
|
-
durableStore.getAllEntries(DRAFT_ID_MAPPINGS_SEGMENT);
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
durableStoreOperation = durableStore.getEntries(mappingIds, DRAFT_ID_MAPPINGS_SEGMENT);
|
|
260
|
-
}
|
|
261
|
-
const entries = await durableStoreOperation;
|
|
262
|
-
if (entries === undefined) {
|
|
263
|
-
return mappings;
|
|
264
|
-
}
|
|
265
|
-
const keys$1 = keys(entries);
|
|
266
|
-
for (const key of keys$1) {
|
|
267
|
-
const entry = entries[key].data;
|
|
268
|
-
if (isLegacyDraftIdMapping(key)) {
|
|
269
|
-
mappings.push({
|
|
270
|
-
draftKey: getRecordKeyForId(entry.draftId),
|
|
271
|
-
canonicalKey: getRecordKeyForId(entry.canonicalId),
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
mappings.push(entry);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return mappings;
|
|
233
|
+
const DraftIdMappingKeyPrefix240 = 'DraftIdMapping::';
|
|
234
|
+
const DraftKeyMappingKeyPrefix = 'DraftKeyMapping::V2::';
|
|
235
|
+
const DRAFT_ID_MAPPINGS_SEGMENT = 'DRAFT_ID_MAPPINGS';
|
|
236
|
+
function isLegacyDraftIdMapping(key, data) {
|
|
237
|
+
return key.startsWith(DraftIdMappingKeyPrefix240);
|
|
238
|
+
}
|
|
239
|
+
// TODO [W-11677776]: in 242 we changed the format to store keys instead of ids
|
|
240
|
+
// this can be removed when we drop support for id storing
|
|
241
|
+
function getRecordKeyForId(id) {
|
|
242
|
+
return `UiApi::RecordRepresentation:${id}`;
|
|
243
|
+
}
|
|
244
|
+
function generateDraftIdMappingKey(draftIdMapping) {
|
|
245
|
+
return `${DraftKeyMappingKeyPrefix}${draftIdMapping.draftKey}::${draftIdMapping.canonicalKey}`;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
*
|
|
249
|
+
* @param mappingIds (optional) requested mapping ids, if undefined all will be retrieved
|
|
250
|
+
*/
|
|
251
|
+
async function getDraftIdMappings(durableStore, mappingIds) {
|
|
252
|
+
const mappings = [];
|
|
253
|
+
let durableStoreOperation;
|
|
254
|
+
if (mappingIds === undefined) {
|
|
255
|
+
durableStoreOperation =
|
|
256
|
+
durableStore.getAllEntries(DRAFT_ID_MAPPINGS_SEGMENT);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
durableStoreOperation = durableStore.getEntries(mappingIds, DRAFT_ID_MAPPINGS_SEGMENT);
|
|
260
|
+
}
|
|
261
|
+
const entries = await durableStoreOperation;
|
|
262
|
+
if (entries === undefined) {
|
|
263
|
+
return mappings;
|
|
264
|
+
}
|
|
265
|
+
const keys$1 = keys(entries);
|
|
266
|
+
for (const key of keys$1) {
|
|
267
|
+
const entry = entries[key].data;
|
|
268
|
+
if (isLegacyDraftIdMapping(key)) {
|
|
269
|
+
mappings.push({
|
|
270
|
+
draftKey: getRecordKeyForId(entry.draftId),
|
|
271
|
+
canonicalKey: getRecordKeyForId(entry.canonicalId),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
mappings.push(entry);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return mappings;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
/**
|
|
282
|
-
* Generates a time-ordered, unique id to associate with a DraftAction. Ensures
|
|
283
|
-
* no collisions with existing draft action IDs.
|
|
284
|
-
*/
|
|
285
|
-
function generateUniqueDraftActionId(existingIds) {
|
|
286
|
-
// new id in milliseconds with some extra digits for collisions
|
|
287
|
-
let newId = new Date().getTime() * 100;
|
|
288
|
-
const existingAsNumbers = existingIds
|
|
289
|
-
.map((id) => parseInt(id, 10))
|
|
290
|
-
.filter((parsed) => !isNaN(parsed));
|
|
291
|
-
let counter = 0;
|
|
292
|
-
while (existingAsNumbers.includes(newId)) {
|
|
293
|
-
newId += 1;
|
|
294
|
-
counter += 1;
|
|
295
|
-
// if the counter is 100+ then somehow this method has been called 100
|
|
296
|
-
// times in one millisecond
|
|
297
|
-
if (counter >= 100) {
|
|
298
|
-
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
299
|
-
throw new Error('Unable to generate unique new draft ID');
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return newId.toString();
|
|
281
|
+
/**
|
|
282
|
+
* Generates a time-ordered, unique id to associate with a DraftAction. Ensures
|
|
283
|
+
* no collisions with existing draft action IDs.
|
|
284
|
+
*/
|
|
285
|
+
function generateUniqueDraftActionId(existingIds) {
|
|
286
|
+
// new id in milliseconds with some extra digits for collisions
|
|
287
|
+
let newId = new Date().getTime() * 100;
|
|
288
|
+
const existingAsNumbers = existingIds
|
|
289
|
+
.map((id) => parseInt(id, 10))
|
|
290
|
+
.filter((parsed) => !isNaN(parsed));
|
|
291
|
+
let counter = 0;
|
|
292
|
+
while (existingAsNumbers.includes(newId)) {
|
|
293
|
+
newId += 1;
|
|
294
|
+
counter += 1;
|
|
295
|
+
// if the counter is 100+ then somehow this method has been called 100
|
|
296
|
+
// times in one millisecond
|
|
297
|
+
if (counter >= 100) {
|
|
298
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
299
|
+
throw new Error('Unable to generate unique new draft ID');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return newId.toString();
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
var CustomActionResultType;
|
|
306
|
-
(function (CustomActionResultType) {
|
|
307
|
-
CustomActionResultType["SUCCESS"] = "SUCCESS";
|
|
308
|
-
CustomActionResultType["FAILURE"] = "FAILURE";
|
|
309
|
-
})(CustomActionResultType || (CustomActionResultType = {}));
|
|
310
|
-
var CustomActionErrorType;
|
|
311
|
-
(function (CustomActionErrorType) {
|
|
312
|
-
CustomActionErrorType["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
313
|
-
CustomActionErrorType["CLIENT_ERROR"] = "CLIENT_ERROR";
|
|
314
|
-
})(CustomActionErrorType || (CustomActionErrorType = {}));
|
|
315
|
-
function isCustomActionSuccess(result) {
|
|
316
|
-
return result.type === CustomActionResultType.SUCCESS;
|
|
317
|
-
}
|
|
318
|
-
function isCustomActionFailed(result) {
|
|
319
|
-
return result.type === CustomActionResultType.FAILURE;
|
|
320
|
-
}
|
|
321
|
-
function customActionHandler(executor, id, draftQueue) {
|
|
322
|
-
const handle = (action, actionCompleted, actionErrored) => {
|
|
323
|
-
notifyCustomActionToExecute(action, actionCompleted, actionErrored);
|
|
324
|
-
return Promise.resolve(ProcessActionResult.CUSTOM_ACTION_WAITING);
|
|
325
|
-
};
|
|
326
|
-
const notifyCustomActionToExecute = (action, actionCompleted, actionErrored) => {
|
|
327
|
-
if (executor !== undefined) {
|
|
328
|
-
executor(action, executorCompleted(action, actionCompleted, actionErrored));
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
const executorCompleted = (action, actionCompleted, actionErrored) => (result) => {
|
|
332
|
-
if (isCustomActionSuccess(result)) {
|
|
333
|
-
actionCompleted({
|
|
334
|
-
...action,
|
|
335
|
-
status: DraftActionStatus.Completed,
|
|
336
|
-
response: createOkResponse(undefined),
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
else if (isCustomActionFailed(result)) {
|
|
340
|
-
actionErrored({
|
|
341
|
-
...action,
|
|
342
|
-
status: DraftActionStatus.Error,
|
|
343
|
-
error: result.error.message,
|
|
344
|
-
}, result.error.type === CustomActionErrorType.NETWORK_ERROR);
|
|
345
|
-
}
|
|
346
|
-
};
|
|
347
|
-
const buildPendingAction = (action, queue) => {
|
|
348
|
-
const { data, tag, targetId, handler } = action;
|
|
349
|
-
const id = generateUniqueDraftActionId(queue.map((a) => a.id));
|
|
350
|
-
return Promise.resolve({
|
|
351
|
-
id,
|
|
352
|
-
targetId,
|
|
353
|
-
status: DraftActionStatus.Pending,
|
|
354
|
-
data,
|
|
355
|
-
tag,
|
|
356
|
-
timestamp: Date.now(),
|
|
357
|
-
metadata: data,
|
|
358
|
-
handler,
|
|
359
|
-
});
|
|
360
|
-
};
|
|
361
|
-
const getQueueOperationsForCompletingDrafts = (_queue, action) => {
|
|
362
|
-
const { id } = action;
|
|
363
|
-
const queueOperations = [];
|
|
364
|
-
queueOperations.push({
|
|
365
|
-
type: QueueOperationType.Delete,
|
|
366
|
-
id: id,
|
|
367
|
-
});
|
|
368
|
-
return queueOperations;
|
|
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
|
-
const getRedirectMappings = (_action) => {
|
|
375
|
-
return undefined;
|
|
376
|
-
};
|
|
377
|
-
return {
|
|
378
|
-
handlerId: id,
|
|
379
|
-
enqueue: (data) => {
|
|
380
|
-
return draftQueue.enqueue(id, data);
|
|
381
|
-
},
|
|
382
|
-
handleAction: handle,
|
|
383
|
-
buildPendingAction,
|
|
384
|
-
getQueueOperationsForCompletingDrafts: getQueueOperationsForCompletingDrafts,
|
|
385
|
-
handleReplaceAction: replaceAction,
|
|
386
|
-
getRedirectMappings,
|
|
387
|
-
handleActionRemoved: () => Promise.resolve(),
|
|
388
|
-
handleActionCompleted: () => Promise.resolve(),
|
|
389
|
-
handleActionEnqueued: () => Promise.resolve(),
|
|
390
|
-
getDataForAction: () => Promise.resolve(undefined),
|
|
391
|
-
getDraftMetadata: () => {
|
|
392
|
-
throw Error('getDraftMetadata not supported for custom actions');
|
|
393
|
-
},
|
|
394
|
-
applyDraftsToIncomingData: () => {
|
|
395
|
-
throw Error('applyDraftsToIncomingData not supported for custom actions');
|
|
396
|
-
},
|
|
397
|
-
shouldDeleteActionByTagOnRemoval: () => false,
|
|
398
|
-
updateMetadata: (existing, incoming) => incoming,
|
|
399
|
-
canHandlePublish: () => false,
|
|
400
|
-
canRepresentationContainDraftMetadata: () => false,
|
|
401
|
-
mergeActions: () => {
|
|
402
|
-
throw Error('mergeActions not supported for custom actions');
|
|
403
|
-
},
|
|
404
|
-
};
|
|
305
|
+
var CustomActionResultType;
|
|
306
|
+
(function (CustomActionResultType) {
|
|
307
|
+
CustomActionResultType["SUCCESS"] = "SUCCESS";
|
|
308
|
+
CustomActionResultType["FAILURE"] = "FAILURE";
|
|
309
|
+
})(CustomActionResultType || (CustomActionResultType = {}));
|
|
310
|
+
var CustomActionErrorType;
|
|
311
|
+
(function (CustomActionErrorType) {
|
|
312
|
+
CustomActionErrorType["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
313
|
+
CustomActionErrorType["CLIENT_ERROR"] = "CLIENT_ERROR";
|
|
314
|
+
})(CustomActionErrorType || (CustomActionErrorType = {}));
|
|
315
|
+
function isCustomActionSuccess(result) {
|
|
316
|
+
return result.type === CustomActionResultType.SUCCESS;
|
|
317
|
+
}
|
|
318
|
+
function isCustomActionFailed(result) {
|
|
319
|
+
return result.type === CustomActionResultType.FAILURE;
|
|
320
|
+
}
|
|
321
|
+
function customActionHandler(executor, id, draftQueue) {
|
|
322
|
+
const handle = (action, actionCompleted, actionErrored) => {
|
|
323
|
+
notifyCustomActionToExecute(action, actionCompleted, actionErrored);
|
|
324
|
+
return Promise.resolve(ProcessActionResult.CUSTOM_ACTION_WAITING);
|
|
325
|
+
};
|
|
326
|
+
const notifyCustomActionToExecute = (action, actionCompleted, actionErrored) => {
|
|
327
|
+
if (executor !== undefined) {
|
|
328
|
+
executor(action, executorCompleted(action, actionCompleted, actionErrored));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const executorCompleted = (action, actionCompleted, actionErrored) => (result) => {
|
|
332
|
+
if (isCustomActionSuccess(result)) {
|
|
333
|
+
actionCompleted({
|
|
334
|
+
...action,
|
|
335
|
+
status: DraftActionStatus.Completed,
|
|
336
|
+
response: createOkResponse(undefined),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
else if (isCustomActionFailed(result)) {
|
|
340
|
+
actionErrored({
|
|
341
|
+
...action,
|
|
342
|
+
status: DraftActionStatus.Error,
|
|
343
|
+
error: result.error.message,
|
|
344
|
+
}, result.error.type === CustomActionErrorType.NETWORK_ERROR);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const buildPendingAction = (action, queue) => {
|
|
348
|
+
const { data, tag, targetId, handler } = action;
|
|
349
|
+
const id = generateUniqueDraftActionId(queue.map((a) => a.id));
|
|
350
|
+
return Promise.resolve({
|
|
351
|
+
id,
|
|
352
|
+
targetId,
|
|
353
|
+
status: DraftActionStatus.Pending,
|
|
354
|
+
data,
|
|
355
|
+
tag,
|
|
356
|
+
timestamp: Date.now(),
|
|
357
|
+
metadata: data,
|
|
358
|
+
handler,
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
const getQueueOperationsForCompletingDrafts = (_queue, action) => {
|
|
362
|
+
const { id } = action;
|
|
363
|
+
const queueOperations = [];
|
|
364
|
+
queueOperations.push({
|
|
365
|
+
type: QueueOperationType.Delete,
|
|
366
|
+
id: id,
|
|
367
|
+
});
|
|
368
|
+
return queueOperations;
|
|
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
|
+
const getRedirectMappings = (_action) => {
|
|
375
|
+
return undefined;
|
|
376
|
+
};
|
|
377
|
+
return {
|
|
378
|
+
handlerId: id,
|
|
379
|
+
enqueue: (data) => {
|
|
380
|
+
return draftQueue.enqueue(id, data);
|
|
381
|
+
},
|
|
382
|
+
handleAction: handle,
|
|
383
|
+
buildPendingAction,
|
|
384
|
+
getQueueOperationsForCompletingDrafts: getQueueOperationsForCompletingDrafts,
|
|
385
|
+
handleReplaceAction: replaceAction,
|
|
386
|
+
getRedirectMappings,
|
|
387
|
+
handleActionRemoved: () => Promise.resolve(),
|
|
388
|
+
handleActionCompleted: () => Promise.resolve(),
|
|
389
|
+
handleActionEnqueued: () => Promise.resolve(),
|
|
390
|
+
getDataForAction: () => Promise.resolve(undefined),
|
|
391
|
+
getDraftMetadata: () => {
|
|
392
|
+
throw Error('getDraftMetadata not supported for custom actions');
|
|
393
|
+
},
|
|
394
|
+
applyDraftsToIncomingData: () => {
|
|
395
|
+
throw Error('applyDraftsToIncomingData not supported for custom actions');
|
|
396
|
+
},
|
|
397
|
+
shouldDeleteActionByTagOnRemoval: () => false,
|
|
398
|
+
updateMetadata: (existing, incoming) => incoming,
|
|
399
|
+
canHandlePublish: () => false,
|
|
400
|
+
canRepresentationContainDraftMetadata: () => false,
|
|
401
|
+
mergeActions: () => {
|
|
402
|
+
throw Error('mergeActions not supported for custom actions');
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
const DRAFT_SEGMENT = 'DRAFT';
|
|
408
|
-
class DurableDraftQueue {
|
|
409
|
-
getHandler(id) {
|
|
410
|
-
const handler = this.handlers[id];
|
|
411
|
-
if (handler === undefined) {
|
|
412
|
-
throw Error(`No handler registered for ${id}`);
|
|
413
|
-
}
|
|
414
|
-
return handler;
|
|
415
|
-
}
|
|
416
|
-
constructor(draftStore) {
|
|
417
|
-
this.retryIntervalMilliseconds = 0;
|
|
418
|
-
this.minimumRetryInterval = 250;
|
|
419
|
-
this.maximumRetryInterval = 32000;
|
|
420
|
-
this.draftQueueChangedListeners = [];
|
|
421
|
-
this.state = DraftQueueState.Stopped;
|
|
422
|
-
this.userState = DraftQueueState.Stopped;
|
|
423
|
-
this.uploadingActionId = undefined;
|
|
424
|
-
this.timeoutHandler = undefined;
|
|
425
|
-
this.handlers = {};
|
|
426
|
-
this.draftStore = draftStore;
|
|
427
|
-
this.workerPool = new AsyncWorkerPool(1);
|
|
428
|
-
}
|
|
429
|
-
addHandler(handler) {
|
|
430
|
-
const id = handler.handlerId;
|
|
431
|
-
if (this.handlers[id] !== undefined) {
|
|
432
|
-
return Promise.reject(`Unable to add handler to id: ${id} because it already exists.`);
|
|
433
|
-
}
|
|
434
|
-
this.handlers[id] = handler;
|
|
435
|
-
return Promise.resolve();
|
|
436
|
-
}
|
|
437
|
-
removeHandler(id) {
|
|
438
|
-
delete this.handlers[id];
|
|
439
|
-
return Promise.resolve();
|
|
440
|
-
}
|
|
441
|
-
addCustomHandler(id, executor) {
|
|
442
|
-
const handler = customActionHandler(executor, id, this);
|
|
443
|
-
return this.addHandler(handler);
|
|
444
|
-
}
|
|
445
|
-
getQueueState() {
|
|
446
|
-
return this.state;
|
|
447
|
-
}
|
|
448
|
-
async startQueue() {
|
|
449
|
-
this.userState = DraftQueueState.Started;
|
|
450
|
-
if (this.state === DraftQueueState.Started) {
|
|
451
|
-
// Do nothing if the queue state is already started
|
|
452
|
-
return Promise.resolve();
|
|
453
|
-
}
|
|
454
|
-
if (this.replacingAction !== undefined) {
|
|
455
|
-
// If we're replacing an action do nothing
|
|
456
|
-
// replace will restart the queue for us as long as the user
|
|
457
|
-
// has last set the queue to be started
|
|
458
|
-
return Promise.resolve();
|
|
459
|
-
}
|
|
460
|
-
this.retryIntervalMilliseconds = 0;
|
|
461
|
-
this.state = DraftQueueState.Started;
|
|
462
|
-
await this.notifyChangedListeners({
|
|
463
|
-
type: DraftQueueEventType.QueueStateChanged,
|
|
464
|
-
state: this.state,
|
|
465
|
-
});
|
|
466
|
-
const result = await this.processNextAction();
|
|
467
|
-
switch (result) {
|
|
468
|
-
case ProcessActionResult.BLOCKED_ON_ERROR:
|
|
469
|
-
this.state = DraftQueueState.Error;
|
|
470
|
-
return Promise.reject();
|
|
471
|
-
default:
|
|
472
|
-
return Promise.resolve();
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
stopQueue() {
|
|
476
|
-
this.userState = DraftQueueState.Stopped;
|
|
477
|
-
if (this.state === DraftQueueState.Stopped) {
|
|
478
|
-
// Do nothing if the queue state is already stopped
|
|
479
|
-
return Promise.resolve();
|
|
480
|
-
}
|
|
481
|
-
this.stopQueueManually();
|
|
482
|
-
return this.notifyChangedListeners({
|
|
483
|
-
type: DraftQueueEventType.QueueStateChanged,
|
|
484
|
-
state: DraftQueueState.Stopped,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Used to stop the queue within DraftQueue without user interaction
|
|
489
|
-
*/
|
|
490
|
-
stopQueueManually() {
|
|
491
|
-
if (this.timeoutHandler) {
|
|
492
|
-
clearTimeout(this.timeoutHandler);
|
|
493
|
-
this.timeoutHandler = undefined;
|
|
494
|
-
}
|
|
495
|
-
this.state = DraftQueueState.Stopped;
|
|
496
|
-
}
|
|
497
|
-
async getQueueActions() {
|
|
498
|
-
const drafts = (await this.draftStore.getAllDrafts());
|
|
499
|
-
const queue = [];
|
|
500
|
-
if (drafts === undefined) {
|
|
501
|
-
return queue;
|
|
502
|
-
}
|
|
503
|
-
drafts.forEach((draft) => {
|
|
504
|
-
if (draft.id === this.uploadingActionId) {
|
|
505
|
-
draft.status = DraftActionStatus.Uploading;
|
|
506
|
-
}
|
|
507
|
-
queue.push(draft);
|
|
508
|
-
});
|
|
509
|
-
return queue.sort((a, b) => {
|
|
510
|
-
const aTime = parseInt(a.id, 10);
|
|
511
|
-
const bTime = parseInt(b.id, 10);
|
|
512
|
-
// safety check
|
|
513
|
-
if (isNaN(aTime)) {
|
|
514
|
-
return 1;
|
|
515
|
-
}
|
|
516
|
-
if (isNaN(bTime)) {
|
|
517
|
-
return -1;
|
|
518
|
-
}
|
|
519
|
-
return aTime - bTime;
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
async enqueue(handlerId, data) {
|
|
523
|
-
return this.workerPool.push({
|
|
524
|
-
workFn: async () => {
|
|
525
|
-
let queue = await this.getQueueActions();
|
|
526
|
-
const handler = this.getHandler(handlerId);
|
|
527
|
-
const pendingAction = (await handler.buildPendingAction(data, queue));
|
|
528
|
-
await this.draftStore.writeAction(pendingAction);
|
|
529
|
-
queue = await this.getQueueActions();
|
|
530
|
-
await this.notifyChangedListeners({
|
|
531
|
-
type: DraftQueueEventType.ActionAdded,
|
|
532
|
-
action: pendingAction,
|
|
533
|
-
});
|
|
534
|
-
await handler.handleActionEnqueued(pendingAction, queue);
|
|
535
|
-
if (this.state === DraftQueueState.Started) {
|
|
536
|
-
this.processNextAction();
|
|
537
|
-
}
|
|
538
|
-
const actionData = (await handler.getDataForAction(pendingAction));
|
|
539
|
-
return {
|
|
540
|
-
action: pendingAction,
|
|
541
|
-
data: actionData,
|
|
542
|
-
};
|
|
543
|
-
},
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
registerOnChangedListener(listener) {
|
|
547
|
-
this.draftQueueChangedListeners.push(listener);
|
|
548
|
-
return () => {
|
|
549
|
-
this.draftQueueChangedListeners = this.draftQueueChangedListeners.filter((l) => {
|
|
550
|
-
return l !== listener;
|
|
551
|
-
});
|
|
552
|
-
return Promise.resolve();
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
async actionCompleted(action) {
|
|
556
|
-
return this.workerPool.push({
|
|
557
|
-
workFn: async () => {
|
|
558
|
-
const handler = this.getHandler(action.handler);
|
|
559
|
-
let queue = await this.getQueueActions();
|
|
560
|
-
const queueOperations = handler.getQueueOperationsForCompletingDrafts(queue, action);
|
|
561
|
-
const idAndKeyMappings = handler.getRedirectMappings(action);
|
|
562
|
-
const keyMappings = idAndKeyMappings === undefined
|
|
563
|
-
? undefined
|
|
564
|
-
: idAndKeyMappings.map((m) => {
|
|
565
|
-
return { draftKey: m.draftKey, canonicalKey: m.canonicalKey };
|
|
566
|
-
});
|
|
567
|
-
await this.draftStore.completeAction(queueOperations, keyMappings);
|
|
568
|
-
queue = await this.getQueueActions();
|
|
569
|
-
this.retryIntervalMilliseconds = 0;
|
|
570
|
-
this.uploadingActionId = undefined;
|
|
571
|
-
await handler.handleActionCompleted(action, queueOperations, queue, values(this.handlers));
|
|
572
|
-
await this.notifyChangedListeners({
|
|
573
|
-
type: DraftQueueEventType.ActionCompleted,
|
|
574
|
-
action,
|
|
575
|
-
});
|
|
576
|
-
if (this.state === DraftQueueState.Started) {
|
|
577
|
-
this.processNextAction();
|
|
578
|
-
}
|
|
579
|
-
},
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
async actionFailed(action, retry) {
|
|
583
|
-
this.uploadingActionId = undefined;
|
|
584
|
-
if (retry && this.state !== DraftQueueState.Stopped) {
|
|
585
|
-
this.state = DraftQueueState.Waiting;
|
|
586
|
-
this.scheduleRetry();
|
|
587
|
-
}
|
|
588
|
-
else if (isDraftError(action)) {
|
|
589
|
-
return this.handleServerError(action, action.error);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
handle(action) {
|
|
593
|
-
const handler = this.getHandler(action.handler);
|
|
594
|
-
if (handler === undefined) {
|
|
595
|
-
return Promise.reject(`No handler for ${action.handler}.`);
|
|
596
|
-
}
|
|
597
|
-
return handler.handleAction(action, this.actionCompleted.bind(this), this.actionFailed.bind(this));
|
|
598
|
-
}
|
|
599
|
-
async processNextAction() {
|
|
600
|
-
if (this.processingAction !== undefined) {
|
|
601
|
-
return this.processingAction;
|
|
602
|
-
}
|
|
603
|
-
const queue = await this.getQueueActions();
|
|
604
|
-
const action = queue[0];
|
|
605
|
-
if (action === undefined) {
|
|
606
|
-
this.processingAction = undefined;
|
|
607
|
-
return ProcessActionResult.NO_ACTION_TO_PROCESS;
|
|
608
|
-
}
|
|
609
|
-
const { status, id } = action;
|
|
610
|
-
if (status === DraftActionStatus.Error) {
|
|
611
|
-
this.state = DraftQueueState.Error;
|
|
612
|
-
this.processingAction = undefined;
|
|
613
|
-
return ProcessActionResult.BLOCKED_ON_ERROR;
|
|
614
|
-
}
|
|
615
|
-
if (id === this.uploadingActionId) {
|
|
616
|
-
this.state = DraftQueueState.Started;
|
|
617
|
-
this.processingAction = undefined;
|
|
618
|
-
return ProcessActionResult.ACTION_ALREADY_PROCESSING;
|
|
619
|
-
}
|
|
620
|
-
this.uploadingActionId = id;
|
|
621
|
-
this.processingAction = undefined;
|
|
622
|
-
if (this.state === DraftQueueState.Waiting) {
|
|
623
|
-
this.state = DraftQueueState.Started;
|
|
624
|
-
}
|
|
625
|
-
return this.handle(action);
|
|
626
|
-
}
|
|
627
|
-
async handleServerError(action, error) {
|
|
628
|
-
const queue = await this.getQueueActions();
|
|
629
|
-
const localAction = queue.filter((qAction) => qAction.id === action.id)[0];
|
|
630
|
-
let newMetadata = {};
|
|
631
|
-
if (localAction !== undefined) {
|
|
632
|
-
newMetadata = localAction.metadata || {};
|
|
633
|
-
}
|
|
634
|
-
const errorAction = {
|
|
635
|
-
...action,
|
|
636
|
-
status: DraftActionStatus.Error,
|
|
637
|
-
error,
|
|
638
|
-
metadata: newMetadata,
|
|
639
|
-
};
|
|
640
|
-
await this.draftStore.writeAction(errorAction);
|
|
641
|
-
this.state = DraftQueueState.Error;
|
|
642
|
-
return this.notifyChangedListeners({
|
|
643
|
-
type: DraftQueueEventType.ActionFailed,
|
|
644
|
-
action: errorAction,
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
async notifyChangedListeners(event) {
|
|
648
|
-
const results = [];
|
|
649
|
-
const { draftQueueChangedListeners } = this;
|
|
650
|
-
const { length: draftQueueLen } = draftQueueChangedListeners;
|
|
651
|
-
for (let i = 0; i < draftQueueLen; i++) {
|
|
652
|
-
const listener = draftQueueChangedListeners[i];
|
|
653
|
-
results.push(listener(event));
|
|
654
|
-
}
|
|
655
|
-
await Promise.all(results);
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* only starts the queue if user state is "Started" and if queue not already
|
|
659
|
-
* started
|
|
660
|
-
*/
|
|
661
|
-
async startQueueSafe() {
|
|
662
|
-
if (this.userState === DraftQueueState.Started && this.state !== DraftQueueState.Started) {
|
|
663
|
-
await this.startQueue();
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
async removeDraftAction(actionId) {
|
|
667
|
-
const queue = await this.getQueueActions();
|
|
668
|
-
//Get the store key for the removed action
|
|
669
|
-
const actions = queue.filter((action) => action.id === actionId);
|
|
670
|
-
if (actions.length === 0) {
|
|
671
|
-
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
672
|
-
throw new Error(`No removable action with id ${actionId}`);
|
|
673
|
-
}
|
|
674
|
-
const action = actions[0];
|
|
675
|
-
if (action.id === this.uploadingActionId) {
|
|
676
|
-
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
677
|
-
throw new Error(`Cannot remove an uploading draft action with ID ${actionId}`);
|
|
678
|
-
}
|
|
679
|
-
const handler = this.getHandler(action.handler);
|
|
680
|
-
const shouldDeleteRelated = handler.shouldDeleteActionByTagOnRemoval(action);
|
|
681
|
-
if (shouldDeleteRelated) {
|
|
682
|
-
await this.draftStore.deleteByTag(action.tag);
|
|
683
|
-
}
|
|
684
|
-
else {
|
|
685
|
-
await this.draftStore.deleteDraft(action.id);
|
|
686
|
-
}
|
|
687
|
-
await handler.handleActionRemoved(action, queue.filter((x) => x.id !== actionId));
|
|
688
|
-
await this.notifyChangedListeners({
|
|
689
|
-
type: DraftQueueEventType.ActionDeleted,
|
|
690
|
-
action,
|
|
691
|
-
});
|
|
692
|
-
if (this.userState === DraftQueueState.Started &&
|
|
693
|
-
this.state !== DraftQueueState.Started &&
|
|
694
|
-
this.replacingAction === undefined) {
|
|
695
|
-
await this.startQueue();
|
|
696
|
-
}
|
|
697
|
-
}
|
|
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;
|
|
738
|
-
}
|
|
739
|
-
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;
|
|
784
|
-
}
|
|
785
|
-
async setMetadata(actionId, metadata) {
|
|
786
|
-
const keys$1 = keys(metadata);
|
|
787
|
-
const compatibleKeys = keys$1.filter((key) => {
|
|
788
|
-
const value = metadata[key];
|
|
789
|
-
return typeof key === 'string' && typeof value === 'string';
|
|
790
|
-
});
|
|
791
|
-
if (keys$1.length !== compatibleKeys.length) {
|
|
792
|
-
return Promise.reject('Cannot save incompatible metadata');
|
|
793
|
-
}
|
|
794
|
-
const queue = await this.getQueueActions();
|
|
795
|
-
const actions = queue.filter((action) => action.id === actionId);
|
|
796
|
-
if (actions.length === 0) {
|
|
797
|
-
return Promise.reject('cannot save metadata to non-existent action');
|
|
798
|
-
}
|
|
799
|
-
const action = actions[0];
|
|
800
|
-
const handler = this.getHandler(action.handler);
|
|
801
|
-
action.metadata = handler.updateMetadata(action.metadata, metadata);
|
|
802
|
-
await this.draftStore.writeAction(action);
|
|
803
|
-
await this.notifyChangedListeners({
|
|
804
|
-
type: DraftQueueEventType.ActionUpdated,
|
|
805
|
-
action: action,
|
|
806
|
-
});
|
|
807
|
-
return action;
|
|
808
|
-
}
|
|
809
|
-
scheduleRetry() {
|
|
810
|
-
const newInterval = this.retryIntervalMilliseconds * 2;
|
|
811
|
-
this.retryIntervalMilliseconds = Math.min(Math.max(newInterval, this.minimumRetryInterval), this.maximumRetryInterval);
|
|
812
|
-
this.timeoutHandler = setTimeout(() => {
|
|
813
|
-
if (this.state !== DraftQueueState.Stopped) {
|
|
814
|
-
this.processNextAction();
|
|
815
|
-
}
|
|
816
|
-
}, this.retryIntervalMilliseconds);
|
|
817
|
-
}
|
|
407
|
+
const DRAFT_SEGMENT = 'DRAFT';
|
|
408
|
+
class DurableDraftQueue {
|
|
409
|
+
getHandler(id) {
|
|
410
|
+
const handler = this.handlers[id];
|
|
411
|
+
if (handler === undefined) {
|
|
412
|
+
throw Error(`No handler registered for ${id}`);
|
|
413
|
+
}
|
|
414
|
+
return handler;
|
|
415
|
+
}
|
|
416
|
+
constructor(draftStore) {
|
|
417
|
+
this.retryIntervalMilliseconds = 0;
|
|
418
|
+
this.minimumRetryInterval = 250;
|
|
419
|
+
this.maximumRetryInterval = 32000;
|
|
420
|
+
this.draftQueueChangedListeners = [];
|
|
421
|
+
this.state = DraftQueueState.Stopped;
|
|
422
|
+
this.userState = DraftQueueState.Stopped;
|
|
423
|
+
this.uploadingActionId = undefined;
|
|
424
|
+
this.timeoutHandler = undefined;
|
|
425
|
+
this.handlers = {};
|
|
426
|
+
this.draftStore = draftStore;
|
|
427
|
+
this.workerPool = new AsyncWorkerPool(1);
|
|
428
|
+
}
|
|
429
|
+
addHandler(handler) {
|
|
430
|
+
const id = handler.handlerId;
|
|
431
|
+
if (this.handlers[id] !== undefined) {
|
|
432
|
+
return Promise.reject(`Unable to add handler to id: ${id} because it already exists.`);
|
|
433
|
+
}
|
|
434
|
+
this.handlers[id] = handler;
|
|
435
|
+
return Promise.resolve();
|
|
436
|
+
}
|
|
437
|
+
removeHandler(id) {
|
|
438
|
+
delete this.handlers[id];
|
|
439
|
+
return Promise.resolve();
|
|
440
|
+
}
|
|
441
|
+
addCustomHandler(id, executor) {
|
|
442
|
+
const handler = customActionHandler(executor, id, this);
|
|
443
|
+
return this.addHandler(handler);
|
|
444
|
+
}
|
|
445
|
+
getQueueState() {
|
|
446
|
+
return this.state;
|
|
447
|
+
}
|
|
448
|
+
async startQueue() {
|
|
449
|
+
this.userState = DraftQueueState.Started;
|
|
450
|
+
if (this.state === DraftQueueState.Started) {
|
|
451
|
+
// Do nothing if the queue state is already started
|
|
452
|
+
return Promise.resolve();
|
|
453
|
+
}
|
|
454
|
+
if (this.replacingAction !== undefined) {
|
|
455
|
+
// If we're replacing an action do nothing
|
|
456
|
+
// replace will restart the queue for us as long as the user
|
|
457
|
+
// has last set the queue to be started
|
|
458
|
+
return Promise.resolve();
|
|
459
|
+
}
|
|
460
|
+
this.retryIntervalMilliseconds = 0;
|
|
461
|
+
this.state = DraftQueueState.Started;
|
|
462
|
+
await this.notifyChangedListeners({
|
|
463
|
+
type: DraftQueueEventType.QueueStateChanged,
|
|
464
|
+
state: this.state,
|
|
465
|
+
});
|
|
466
|
+
const result = await this.processNextAction();
|
|
467
|
+
switch (result) {
|
|
468
|
+
case ProcessActionResult.BLOCKED_ON_ERROR:
|
|
469
|
+
this.state = DraftQueueState.Error;
|
|
470
|
+
return Promise.reject();
|
|
471
|
+
default:
|
|
472
|
+
return Promise.resolve();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
stopQueue() {
|
|
476
|
+
this.userState = DraftQueueState.Stopped;
|
|
477
|
+
if (this.state === DraftQueueState.Stopped) {
|
|
478
|
+
// Do nothing if the queue state is already stopped
|
|
479
|
+
return Promise.resolve();
|
|
480
|
+
}
|
|
481
|
+
this.stopQueueManually();
|
|
482
|
+
return this.notifyChangedListeners({
|
|
483
|
+
type: DraftQueueEventType.QueueStateChanged,
|
|
484
|
+
state: DraftQueueState.Stopped,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Used to stop the queue within DraftQueue without user interaction
|
|
489
|
+
*/
|
|
490
|
+
stopQueueManually() {
|
|
491
|
+
if (this.timeoutHandler) {
|
|
492
|
+
clearTimeout(this.timeoutHandler);
|
|
493
|
+
this.timeoutHandler = undefined;
|
|
494
|
+
}
|
|
495
|
+
this.state = DraftQueueState.Stopped;
|
|
496
|
+
}
|
|
497
|
+
async getQueueActions() {
|
|
498
|
+
const drafts = (await this.draftStore.getAllDrafts());
|
|
499
|
+
const queue = [];
|
|
500
|
+
if (drafts === undefined) {
|
|
501
|
+
return queue;
|
|
502
|
+
}
|
|
503
|
+
drafts.forEach((draft) => {
|
|
504
|
+
if (draft.id === this.uploadingActionId) {
|
|
505
|
+
draft.status = DraftActionStatus.Uploading;
|
|
506
|
+
}
|
|
507
|
+
queue.push(draft);
|
|
508
|
+
});
|
|
509
|
+
return queue.sort((a, b) => {
|
|
510
|
+
const aTime = parseInt(a.id, 10);
|
|
511
|
+
const bTime = parseInt(b.id, 10);
|
|
512
|
+
// safety check
|
|
513
|
+
if (isNaN(aTime)) {
|
|
514
|
+
return 1;
|
|
515
|
+
}
|
|
516
|
+
if (isNaN(bTime)) {
|
|
517
|
+
return -1;
|
|
518
|
+
}
|
|
519
|
+
return aTime - bTime;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
async enqueue(handlerId, data) {
|
|
523
|
+
return this.workerPool.push({
|
|
524
|
+
workFn: async () => {
|
|
525
|
+
let queue = await this.getQueueActions();
|
|
526
|
+
const handler = this.getHandler(handlerId);
|
|
527
|
+
const pendingAction = (await handler.buildPendingAction(data, queue));
|
|
528
|
+
await this.draftStore.writeAction(pendingAction);
|
|
529
|
+
queue = await this.getQueueActions();
|
|
530
|
+
await this.notifyChangedListeners({
|
|
531
|
+
type: DraftQueueEventType.ActionAdded,
|
|
532
|
+
action: pendingAction,
|
|
533
|
+
});
|
|
534
|
+
await handler.handleActionEnqueued(pendingAction, queue);
|
|
535
|
+
if (this.state === DraftQueueState.Started) {
|
|
536
|
+
this.processNextAction();
|
|
537
|
+
}
|
|
538
|
+
const actionData = (await handler.getDataForAction(pendingAction));
|
|
539
|
+
return {
|
|
540
|
+
action: pendingAction,
|
|
541
|
+
data: actionData,
|
|
542
|
+
};
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
registerOnChangedListener(listener) {
|
|
547
|
+
this.draftQueueChangedListeners.push(listener);
|
|
548
|
+
return () => {
|
|
549
|
+
this.draftQueueChangedListeners = this.draftQueueChangedListeners.filter((l) => {
|
|
550
|
+
return l !== listener;
|
|
551
|
+
});
|
|
552
|
+
return Promise.resolve();
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
async actionCompleted(action) {
|
|
556
|
+
return this.workerPool.push({
|
|
557
|
+
workFn: async () => {
|
|
558
|
+
const handler = this.getHandler(action.handler);
|
|
559
|
+
let queue = await this.getQueueActions();
|
|
560
|
+
const queueOperations = handler.getQueueOperationsForCompletingDrafts(queue, action);
|
|
561
|
+
const idAndKeyMappings = handler.getRedirectMappings(action);
|
|
562
|
+
const keyMappings = idAndKeyMappings === undefined
|
|
563
|
+
? undefined
|
|
564
|
+
: idAndKeyMappings.map((m) => {
|
|
565
|
+
return { draftKey: m.draftKey, canonicalKey: m.canonicalKey };
|
|
566
|
+
});
|
|
567
|
+
await this.draftStore.completeAction(queueOperations, keyMappings);
|
|
568
|
+
queue = await this.getQueueActions();
|
|
569
|
+
this.retryIntervalMilliseconds = 0;
|
|
570
|
+
this.uploadingActionId = undefined;
|
|
571
|
+
await handler.handleActionCompleted(action, queueOperations, queue, values(this.handlers));
|
|
572
|
+
await this.notifyChangedListeners({
|
|
573
|
+
type: DraftQueueEventType.ActionCompleted,
|
|
574
|
+
action,
|
|
575
|
+
});
|
|
576
|
+
if (this.state === DraftQueueState.Started) {
|
|
577
|
+
this.processNextAction();
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
async actionFailed(action, retry) {
|
|
583
|
+
this.uploadingActionId = undefined;
|
|
584
|
+
if (retry && this.state !== DraftQueueState.Stopped) {
|
|
585
|
+
this.state = DraftQueueState.Waiting;
|
|
586
|
+
this.scheduleRetry();
|
|
587
|
+
}
|
|
588
|
+
else if (isDraftError(action)) {
|
|
589
|
+
return this.handleServerError(action, action.error);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
handle(action) {
|
|
593
|
+
const handler = this.getHandler(action.handler);
|
|
594
|
+
if (handler === undefined) {
|
|
595
|
+
return Promise.reject(`No handler for ${action.handler}.`);
|
|
596
|
+
}
|
|
597
|
+
return handler.handleAction(action, this.actionCompleted.bind(this), this.actionFailed.bind(this));
|
|
598
|
+
}
|
|
599
|
+
async processNextAction() {
|
|
600
|
+
if (this.processingAction !== undefined) {
|
|
601
|
+
return this.processingAction;
|
|
602
|
+
}
|
|
603
|
+
const queue = await this.getQueueActions();
|
|
604
|
+
const action = queue[0];
|
|
605
|
+
if (action === undefined) {
|
|
606
|
+
this.processingAction = undefined;
|
|
607
|
+
return ProcessActionResult.NO_ACTION_TO_PROCESS;
|
|
608
|
+
}
|
|
609
|
+
const { status, id } = action;
|
|
610
|
+
if (status === DraftActionStatus.Error) {
|
|
611
|
+
this.state = DraftQueueState.Error;
|
|
612
|
+
this.processingAction = undefined;
|
|
613
|
+
return ProcessActionResult.BLOCKED_ON_ERROR;
|
|
614
|
+
}
|
|
615
|
+
if (id === this.uploadingActionId) {
|
|
616
|
+
this.state = DraftQueueState.Started;
|
|
617
|
+
this.processingAction = undefined;
|
|
618
|
+
return ProcessActionResult.ACTION_ALREADY_PROCESSING;
|
|
619
|
+
}
|
|
620
|
+
this.uploadingActionId = id;
|
|
621
|
+
this.processingAction = undefined;
|
|
622
|
+
if (this.state === DraftQueueState.Waiting) {
|
|
623
|
+
this.state = DraftQueueState.Started;
|
|
624
|
+
}
|
|
625
|
+
return this.handle(action);
|
|
626
|
+
}
|
|
627
|
+
async handleServerError(action, error) {
|
|
628
|
+
const queue = await this.getQueueActions();
|
|
629
|
+
const localAction = queue.filter((qAction) => qAction.id === action.id)[0];
|
|
630
|
+
let newMetadata = {};
|
|
631
|
+
if (localAction !== undefined) {
|
|
632
|
+
newMetadata = localAction.metadata || {};
|
|
633
|
+
}
|
|
634
|
+
const errorAction = {
|
|
635
|
+
...action,
|
|
636
|
+
status: DraftActionStatus.Error,
|
|
637
|
+
error,
|
|
638
|
+
metadata: newMetadata,
|
|
639
|
+
};
|
|
640
|
+
await this.draftStore.writeAction(errorAction);
|
|
641
|
+
this.state = DraftQueueState.Error;
|
|
642
|
+
return this.notifyChangedListeners({
|
|
643
|
+
type: DraftQueueEventType.ActionFailed,
|
|
644
|
+
action: errorAction,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async notifyChangedListeners(event) {
|
|
648
|
+
const results = [];
|
|
649
|
+
const { draftQueueChangedListeners } = this;
|
|
650
|
+
const { length: draftQueueLen } = draftQueueChangedListeners;
|
|
651
|
+
for (let i = 0; i < draftQueueLen; i++) {
|
|
652
|
+
const listener = draftQueueChangedListeners[i];
|
|
653
|
+
results.push(listener(event));
|
|
654
|
+
}
|
|
655
|
+
await Promise.all(results);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* only starts the queue if user state is "Started" and if queue not already
|
|
659
|
+
* started
|
|
660
|
+
*/
|
|
661
|
+
async startQueueSafe() {
|
|
662
|
+
if (this.userState === DraftQueueState.Started && this.state !== DraftQueueState.Started) {
|
|
663
|
+
await this.startQueue();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async removeDraftAction(actionId) {
|
|
667
|
+
const queue = await this.getQueueActions();
|
|
668
|
+
//Get the store key for the removed action
|
|
669
|
+
const actions = queue.filter((action) => action.id === actionId);
|
|
670
|
+
if (actions.length === 0) {
|
|
671
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
672
|
+
throw new Error(`No removable action with id ${actionId}`);
|
|
673
|
+
}
|
|
674
|
+
const action = actions[0];
|
|
675
|
+
if (action.id === this.uploadingActionId) {
|
|
676
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
677
|
+
throw new Error(`Cannot remove an uploading draft action with ID ${actionId}`);
|
|
678
|
+
}
|
|
679
|
+
const handler = this.getHandler(action.handler);
|
|
680
|
+
const shouldDeleteRelated = handler.shouldDeleteActionByTagOnRemoval(action);
|
|
681
|
+
if (shouldDeleteRelated) {
|
|
682
|
+
await this.draftStore.deleteByTag(action.tag);
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
await this.draftStore.deleteDraft(action.id);
|
|
686
|
+
}
|
|
687
|
+
await handler.handleActionRemoved(action, queue.filter((x) => x.id !== actionId));
|
|
688
|
+
await this.notifyChangedListeners({
|
|
689
|
+
type: DraftQueueEventType.ActionDeleted,
|
|
690
|
+
action,
|
|
691
|
+
});
|
|
692
|
+
if (this.userState === DraftQueueState.Started &&
|
|
693
|
+
this.state !== DraftQueueState.Started &&
|
|
694
|
+
this.replacingAction === undefined) {
|
|
695
|
+
await this.startQueue();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
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;
|
|
738
|
+
}
|
|
739
|
+
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;
|
|
784
|
+
}
|
|
785
|
+
async setMetadata(actionId, metadata) {
|
|
786
|
+
const keys$1 = keys(metadata);
|
|
787
|
+
const compatibleKeys = keys$1.filter((key) => {
|
|
788
|
+
const value = metadata[key];
|
|
789
|
+
return typeof key === 'string' && typeof value === 'string';
|
|
790
|
+
});
|
|
791
|
+
if (keys$1.length !== compatibleKeys.length) {
|
|
792
|
+
return Promise.reject('Cannot save incompatible metadata');
|
|
793
|
+
}
|
|
794
|
+
const queue = await this.getQueueActions();
|
|
795
|
+
const actions = queue.filter((action) => action.id === actionId);
|
|
796
|
+
if (actions.length === 0) {
|
|
797
|
+
return Promise.reject('cannot save metadata to non-existent action');
|
|
798
|
+
}
|
|
799
|
+
const action = actions[0];
|
|
800
|
+
const handler = this.getHandler(action.handler);
|
|
801
|
+
action.metadata = handler.updateMetadata(action.metadata, metadata);
|
|
802
|
+
await this.draftStore.writeAction(action);
|
|
803
|
+
await this.notifyChangedListeners({
|
|
804
|
+
type: DraftQueueEventType.ActionUpdated,
|
|
805
|
+
action: action,
|
|
806
|
+
});
|
|
807
|
+
return action;
|
|
808
|
+
}
|
|
809
|
+
scheduleRetry() {
|
|
810
|
+
const newInterval = this.retryIntervalMilliseconds * 2;
|
|
811
|
+
this.retryIntervalMilliseconds = Math.min(Math.max(newInterval, this.minimumRetryInterval), this.maximumRetryInterval);
|
|
812
|
+
this.timeoutHandler = setTimeout(() => {
|
|
813
|
+
if (this.state !== DraftQueueState.Stopped) {
|
|
814
|
+
this.processNextAction();
|
|
815
|
+
}
|
|
816
|
+
}, this.retryIntervalMilliseconds);
|
|
817
|
+
}
|
|
818
818
|
}
|
|
819
819
|
|
|
820
|
-
const DRAFT_ACTION_KEY_JUNCTION = '__DraftAction__';
|
|
821
|
-
function buildDraftDurableStoreKey(recordKey, draftActionId) {
|
|
822
|
-
return `${recordKey}${DRAFT_ACTION_KEY_JUNCTION}${draftActionId}`;
|
|
823
|
-
}
|
|
824
|
-
/**
|
|
825
|
-
* Implements a write-through InMemoryStore for Drafts, storing all drafts in a
|
|
826
|
-
* in-memory store with a write through to the DurableStore.
|
|
827
|
-
*
|
|
828
|
-
* Before any reads or writes come in from the draft queue, we need to revive the draft
|
|
829
|
-
* queue into memory. During this initial revive, any writes are queued up and operated on the
|
|
830
|
-
* queue once it's in memory. Similarly any reads are delayed until the queue is in memory.
|
|
831
|
-
*
|
|
832
|
-
*/
|
|
833
|
-
class DurableDraftStore {
|
|
834
|
-
constructor(durableStore) {
|
|
835
|
-
this.draftStore = {};
|
|
836
|
-
// queue of writes that were made during the initial sync
|
|
837
|
-
this.writeQueue = [];
|
|
838
|
-
this.durableStore = durableStore;
|
|
839
|
-
this.resyncDraftStore();
|
|
840
|
-
}
|
|
841
|
-
writeAction(action) {
|
|
842
|
-
const addAction = () => {
|
|
843
|
-
const { id, tag } = action;
|
|
844
|
-
this.draftStore[id] = action;
|
|
845
|
-
const durableEntryKey = buildDraftDurableStoreKey(tag, id);
|
|
846
|
-
const entry = {
|
|
847
|
-
data: action,
|
|
848
|
-
};
|
|
849
|
-
const entries = { [durableEntryKey]: entry };
|
|
850
|
-
return this.durableStore.setEntries(entries, DRAFT_SEGMENT);
|
|
851
|
-
};
|
|
852
|
-
return this.enqueueAction(addAction);
|
|
853
|
-
}
|
|
854
|
-
getAllDrafts() {
|
|
855
|
-
const waitForOngoingSync = this.syncPromise || Promise.resolve();
|
|
856
|
-
return waitForOngoingSync.then(() => {
|
|
857
|
-
const { draftStore } = this;
|
|
858
|
-
const keys$1 = keys(draftStore);
|
|
859
|
-
const actionArray = [];
|
|
860
|
-
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
861
|
-
const key = keys$1[i];
|
|
862
|
-
actionArray.push(draftStore[key]);
|
|
863
|
-
}
|
|
864
|
-
return actionArray;
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
deleteDraft(id) {
|
|
868
|
-
const deleteAction = () => {
|
|
869
|
-
const draft = this.draftStore[id];
|
|
870
|
-
if (draft !== undefined) {
|
|
871
|
-
delete this.draftStore[id];
|
|
872
|
-
const durableKey = buildDraftDurableStoreKey(draft.tag, draft.id);
|
|
873
|
-
return this.durableStore.evictEntries([durableKey], DRAFT_SEGMENT);
|
|
874
|
-
}
|
|
875
|
-
return Promise.resolve();
|
|
876
|
-
};
|
|
877
|
-
return this.enqueueAction(deleteAction);
|
|
878
|
-
}
|
|
879
|
-
deleteByTag(tag) {
|
|
880
|
-
const deleteAction = () => {
|
|
881
|
-
const { draftStore } = this;
|
|
882
|
-
const keys$1 = keys(draftStore);
|
|
883
|
-
const durableKeys = [];
|
|
884
|
-
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
885
|
-
const key = keys$1[i];
|
|
886
|
-
const action = draftStore[key];
|
|
887
|
-
if (action.tag === tag) {
|
|
888
|
-
delete draftStore[action.id];
|
|
889
|
-
durableKeys.push(buildDraftDurableStoreKey(action.tag, action.id));
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
return this.durableStore.evictEntries(durableKeys, DRAFT_SEGMENT);
|
|
893
|
-
};
|
|
894
|
-
return this.enqueueAction(deleteAction);
|
|
895
|
-
}
|
|
896
|
-
completeAction(queueOperations, mappings) {
|
|
897
|
-
const action = () => {
|
|
898
|
-
const durableStoreOperations = [];
|
|
899
|
-
const { draftStore } = this;
|
|
900
|
-
for (let i = 0, len = queueOperations.length; i < len; i++) {
|
|
901
|
-
const operation = queueOperations[i];
|
|
902
|
-
if (operation.type === QueueOperationType.Delete) {
|
|
903
|
-
const action = draftStore[operation.id];
|
|
904
|
-
if (action !== undefined) {
|
|
905
|
-
delete draftStore[operation.id];
|
|
906
|
-
const key = buildDraftDurableStoreKey(action.tag, action.id);
|
|
907
|
-
durableStoreOperations.push({
|
|
908
|
-
ids: [key],
|
|
909
|
-
type: 'evictEntries',
|
|
910
|
-
segment: DRAFT_SEGMENT,
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
else {
|
|
915
|
-
const { action } = operation;
|
|
916
|
-
const key = buildDraftDurableStoreKey(action.tag, action.id);
|
|
917
|
-
draftStore[action.id] = action;
|
|
918
|
-
durableStoreOperations.push({
|
|
919
|
-
type: 'setEntries',
|
|
920
|
-
segment: DRAFT_SEGMENT,
|
|
921
|
-
entries: {
|
|
922
|
-
[key]: {
|
|
923
|
-
data: operation.action,
|
|
924
|
-
},
|
|
925
|
-
},
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
if (mappings !== undefined) {
|
|
930
|
-
const entries = {};
|
|
931
|
-
for (const mapping of mappings) {
|
|
932
|
-
const mappingKey = generateDraftIdMappingKey(mapping);
|
|
933
|
-
entries[mappingKey] = { data: mapping };
|
|
934
|
-
}
|
|
935
|
-
durableStoreOperations.push({
|
|
936
|
-
entries,
|
|
937
|
-
type: 'setEntries',
|
|
938
|
-
segment: DRAFT_ID_MAPPINGS_SEGMENT,
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
return this.durableStore.batchOperations(durableStoreOperations);
|
|
942
|
-
};
|
|
943
|
-
return this.enqueueAction(action);
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Runs a write operation against the draft store, if the initial
|
|
947
|
-
* revive is still in progress, the action gets enqueued to run once the
|
|
948
|
-
* initial revive is complete
|
|
949
|
-
* @param action
|
|
950
|
-
* @returns a promise that is resolved once the action has run
|
|
951
|
-
*/
|
|
952
|
-
enqueueAction(action) {
|
|
953
|
-
const { syncPromise, writeQueue: pendingMerges } = this;
|
|
954
|
-
// if the initial sync is done and existing operations have been run, no need to queue, just run
|
|
955
|
-
if (syncPromise === undefined && pendingMerges.length === 0) {
|
|
956
|
-
return action();
|
|
957
|
-
}
|
|
958
|
-
const deferred = new Promise((resolve, reject) => {
|
|
959
|
-
this.writeQueue.push(() => {
|
|
960
|
-
return action()
|
|
961
|
-
.then((x) => {
|
|
962
|
-
resolve(x);
|
|
963
|
-
})
|
|
964
|
-
.catch((err) => reject(err));
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
return deferred;
|
|
968
|
-
}
|
|
969
|
-
/**
|
|
970
|
-
* Revives the draft store from the durable store. Once the draft store is
|
|
971
|
-
* revived, executes any queued up draft store operations that came in while
|
|
972
|
-
* reviving
|
|
973
|
-
*/
|
|
974
|
-
resyncDraftStore() {
|
|
975
|
-
const sync = () => {
|
|
976
|
-
this.syncPromise = this.durableStore
|
|
977
|
-
.getAllEntries(DRAFT_SEGMENT)
|
|
978
|
-
.then((durableEntries) => {
|
|
979
|
-
if (durableEntries === undefined) {
|
|
980
|
-
this.draftStore = {};
|
|
981
|
-
return this.runQueuedOperations();
|
|
982
|
-
}
|
|
983
|
-
const { draftStore } = this;
|
|
984
|
-
const keys$1 = keys(durableEntries);
|
|
985
|
-
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
986
|
-
const entry = durableEntries[keys$1[i]];
|
|
987
|
-
const action = entry.data;
|
|
988
|
-
if (action !== undefined) {
|
|
989
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
990
|
-
// the `version` property was introduced in 242, we should assert version
|
|
991
|
-
// exists once we are sure there are no durable stores that contain
|
|
992
|
-
// versionless actions
|
|
993
|
-
if (action.version && action.version !== '242.0.0') {
|
|
994
|
-
return Promise.reject('Unexpected draft action version found in the durable store');
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
draftStore[action.id] = action;
|
|
998
|
-
}
|
|
999
|
-
else {
|
|
1000
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
1001
|
-
const err = new Error('Expected draft action to be defined in the durable store');
|
|
1002
|
-
return Promise.reject(err);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
return this.runQueuedOperations();
|
|
1007
|
-
})
|
|
1008
|
-
.finally(() => {
|
|
1009
|
-
this.syncPromise = undefined;
|
|
1010
|
-
});
|
|
1011
|
-
return this.syncPromise;
|
|
1012
|
-
};
|
|
1013
|
-
// if there's an ongoing sync populating the in memory store, wait for it to complete before re-syncing
|
|
1014
|
-
const { syncPromise } = this;
|
|
1015
|
-
if (syncPromise === undefined) {
|
|
1016
|
-
return sync();
|
|
1017
|
-
}
|
|
1018
|
-
return syncPromise.then(() => {
|
|
1019
|
-
return sync();
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
/**
|
|
1023
|
-
* Runs the operations that were queued up while reviving the
|
|
1024
|
-
* draft store from the durable store
|
|
1025
|
-
*/
|
|
1026
|
-
runQueuedOperations() {
|
|
1027
|
-
const { writeQueue } = this;
|
|
1028
|
-
if (writeQueue.length > 0) {
|
|
1029
|
-
const queueItem = writeQueue.shift();
|
|
1030
|
-
if (queueItem !== undefined) {
|
|
1031
|
-
return queueItem().then(() => {
|
|
1032
|
-
return this.runQueuedOperations();
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
return Promise.resolve();
|
|
1037
|
-
}
|
|
820
|
+
const DRAFT_ACTION_KEY_JUNCTION = '__DraftAction__';
|
|
821
|
+
function buildDraftDurableStoreKey(recordKey, draftActionId) {
|
|
822
|
+
return `${recordKey}${DRAFT_ACTION_KEY_JUNCTION}${draftActionId}`;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Implements a write-through InMemoryStore for Drafts, storing all drafts in a
|
|
826
|
+
* in-memory store with a write through to the DurableStore.
|
|
827
|
+
*
|
|
828
|
+
* Before any reads or writes come in from the draft queue, we need to revive the draft
|
|
829
|
+
* queue into memory. During this initial revive, any writes are queued up and operated on the
|
|
830
|
+
* queue once it's in memory. Similarly any reads are delayed until the queue is in memory.
|
|
831
|
+
*
|
|
832
|
+
*/
|
|
833
|
+
class DurableDraftStore {
|
|
834
|
+
constructor(durableStore) {
|
|
835
|
+
this.draftStore = {};
|
|
836
|
+
// queue of writes that were made during the initial sync
|
|
837
|
+
this.writeQueue = [];
|
|
838
|
+
this.durableStore = durableStore;
|
|
839
|
+
this.resyncDraftStore();
|
|
840
|
+
}
|
|
841
|
+
writeAction(action) {
|
|
842
|
+
const addAction = () => {
|
|
843
|
+
const { id, tag } = action;
|
|
844
|
+
this.draftStore[id] = action;
|
|
845
|
+
const durableEntryKey = buildDraftDurableStoreKey(tag, id);
|
|
846
|
+
const entry = {
|
|
847
|
+
data: action,
|
|
848
|
+
};
|
|
849
|
+
const entries = { [durableEntryKey]: entry };
|
|
850
|
+
return this.durableStore.setEntries(entries, DRAFT_SEGMENT);
|
|
851
|
+
};
|
|
852
|
+
return this.enqueueAction(addAction);
|
|
853
|
+
}
|
|
854
|
+
getAllDrafts() {
|
|
855
|
+
const waitForOngoingSync = this.syncPromise || Promise.resolve();
|
|
856
|
+
return waitForOngoingSync.then(() => {
|
|
857
|
+
const { draftStore } = this;
|
|
858
|
+
const keys$1 = keys(draftStore);
|
|
859
|
+
const actionArray = [];
|
|
860
|
+
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
861
|
+
const key = keys$1[i];
|
|
862
|
+
actionArray.push(draftStore[key]);
|
|
863
|
+
}
|
|
864
|
+
return actionArray;
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
deleteDraft(id) {
|
|
868
|
+
const deleteAction = () => {
|
|
869
|
+
const draft = this.draftStore[id];
|
|
870
|
+
if (draft !== undefined) {
|
|
871
|
+
delete this.draftStore[id];
|
|
872
|
+
const durableKey = buildDraftDurableStoreKey(draft.tag, draft.id);
|
|
873
|
+
return this.durableStore.evictEntries([durableKey], DRAFT_SEGMENT);
|
|
874
|
+
}
|
|
875
|
+
return Promise.resolve();
|
|
876
|
+
};
|
|
877
|
+
return this.enqueueAction(deleteAction);
|
|
878
|
+
}
|
|
879
|
+
deleteByTag(tag) {
|
|
880
|
+
const deleteAction = () => {
|
|
881
|
+
const { draftStore } = this;
|
|
882
|
+
const keys$1 = keys(draftStore);
|
|
883
|
+
const durableKeys = [];
|
|
884
|
+
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
885
|
+
const key = keys$1[i];
|
|
886
|
+
const action = draftStore[key];
|
|
887
|
+
if (action.tag === tag) {
|
|
888
|
+
delete draftStore[action.id];
|
|
889
|
+
durableKeys.push(buildDraftDurableStoreKey(action.tag, action.id));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return this.durableStore.evictEntries(durableKeys, DRAFT_SEGMENT);
|
|
893
|
+
};
|
|
894
|
+
return this.enqueueAction(deleteAction);
|
|
895
|
+
}
|
|
896
|
+
completeAction(queueOperations, mappings) {
|
|
897
|
+
const action = () => {
|
|
898
|
+
const durableStoreOperations = [];
|
|
899
|
+
const { draftStore } = this;
|
|
900
|
+
for (let i = 0, len = queueOperations.length; i < len; i++) {
|
|
901
|
+
const operation = queueOperations[i];
|
|
902
|
+
if (operation.type === QueueOperationType.Delete) {
|
|
903
|
+
const action = draftStore[operation.id];
|
|
904
|
+
if (action !== undefined) {
|
|
905
|
+
delete draftStore[operation.id];
|
|
906
|
+
const key = buildDraftDurableStoreKey(action.tag, action.id);
|
|
907
|
+
durableStoreOperations.push({
|
|
908
|
+
ids: [key],
|
|
909
|
+
type: 'evictEntries',
|
|
910
|
+
segment: DRAFT_SEGMENT,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
const { action } = operation;
|
|
916
|
+
const key = buildDraftDurableStoreKey(action.tag, action.id);
|
|
917
|
+
draftStore[action.id] = action;
|
|
918
|
+
durableStoreOperations.push({
|
|
919
|
+
type: 'setEntries',
|
|
920
|
+
segment: DRAFT_SEGMENT,
|
|
921
|
+
entries: {
|
|
922
|
+
[key]: {
|
|
923
|
+
data: operation.action,
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (mappings !== undefined) {
|
|
930
|
+
const entries = {};
|
|
931
|
+
for (const mapping of mappings) {
|
|
932
|
+
const mappingKey = generateDraftIdMappingKey(mapping);
|
|
933
|
+
entries[mappingKey] = { data: mapping };
|
|
934
|
+
}
|
|
935
|
+
durableStoreOperations.push({
|
|
936
|
+
entries,
|
|
937
|
+
type: 'setEntries',
|
|
938
|
+
segment: DRAFT_ID_MAPPINGS_SEGMENT,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return this.durableStore.batchOperations(durableStoreOperations);
|
|
942
|
+
};
|
|
943
|
+
return this.enqueueAction(action);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Runs a write operation against the draft store, if the initial
|
|
947
|
+
* revive is still in progress, the action gets enqueued to run once the
|
|
948
|
+
* initial revive is complete
|
|
949
|
+
* @param action
|
|
950
|
+
* @returns a promise that is resolved once the action has run
|
|
951
|
+
*/
|
|
952
|
+
enqueueAction(action) {
|
|
953
|
+
const { syncPromise, writeQueue: pendingMerges } = this;
|
|
954
|
+
// if the initial sync is done and existing operations have been run, no need to queue, just run
|
|
955
|
+
if (syncPromise === undefined && pendingMerges.length === 0) {
|
|
956
|
+
return action();
|
|
957
|
+
}
|
|
958
|
+
const deferred = new Promise((resolve, reject) => {
|
|
959
|
+
this.writeQueue.push(() => {
|
|
960
|
+
return action()
|
|
961
|
+
.then((x) => {
|
|
962
|
+
resolve(x);
|
|
963
|
+
})
|
|
964
|
+
.catch((err) => reject(err));
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
return deferred;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Revives the draft store from the durable store. Once the draft store is
|
|
971
|
+
* revived, executes any queued up draft store operations that came in while
|
|
972
|
+
* reviving
|
|
973
|
+
*/
|
|
974
|
+
resyncDraftStore() {
|
|
975
|
+
const sync = () => {
|
|
976
|
+
this.syncPromise = this.durableStore
|
|
977
|
+
.getAllEntries(DRAFT_SEGMENT)
|
|
978
|
+
.then((durableEntries) => {
|
|
979
|
+
if (durableEntries === undefined) {
|
|
980
|
+
this.draftStore = {};
|
|
981
|
+
return this.runQueuedOperations();
|
|
982
|
+
}
|
|
983
|
+
const { draftStore } = this;
|
|
984
|
+
const keys$1 = keys(durableEntries);
|
|
985
|
+
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
986
|
+
const entry = durableEntries[keys$1[i]];
|
|
987
|
+
const action = entry.data;
|
|
988
|
+
if (action !== undefined) {
|
|
989
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
990
|
+
// the `version` property was introduced in 242, we should assert version
|
|
991
|
+
// exists once we are sure there are no durable stores that contain
|
|
992
|
+
// versionless actions
|
|
993
|
+
if (action.version && action.version !== '242.0.0') {
|
|
994
|
+
return Promise.reject('Unexpected draft action version found in the durable store');
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
draftStore[action.id] = action;
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
1001
|
+
const err = new Error('Expected draft action to be defined in the durable store');
|
|
1002
|
+
return Promise.reject(err);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return this.runQueuedOperations();
|
|
1007
|
+
})
|
|
1008
|
+
.finally(() => {
|
|
1009
|
+
this.syncPromise = undefined;
|
|
1010
|
+
});
|
|
1011
|
+
return this.syncPromise;
|
|
1012
|
+
};
|
|
1013
|
+
// if there's an ongoing sync populating the in memory store, wait for it to complete before re-syncing
|
|
1014
|
+
const { syncPromise } = this;
|
|
1015
|
+
if (syncPromise === undefined) {
|
|
1016
|
+
return sync();
|
|
1017
|
+
}
|
|
1018
|
+
return syncPromise.then(() => {
|
|
1019
|
+
return sync();
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Runs the operations that were queued up while reviving the
|
|
1024
|
+
* draft store from the durable store
|
|
1025
|
+
*/
|
|
1026
|
+
runQueuedOperations() {
|
|
1027
|
+
const { writeQueue } = this;
|
|
1028
|
+
if (writeQueue.length > 0) {
|
|
1029
|
+
const queueItem = writeQueue.shift();
|
|
1030
|
+
if (queueItem !== undefined) {
|
|
1031
|
+
return queueItem().then(() => {
|
|
1032
|
+
return this.runQueuedOperations();
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return Promise.resolve();
|
|
1037
|
+
}
|
|
1038
1038
|
}
|
|
1039
1039
|
|
|
1040
|
-
class AbstractResourceRequestActionHandler {
|
|
1041
|
-
constructor(draftQueue, networkAdapter, getLuvio) {
|
|
1042
|
-
this.draftQueue = draftQueue;
|
|
1043
|
-
this.networkAdapter = networkAdapter;
|
|
1044
|
-
this.getLuvio = getLuvio;
|
|
1045
|
-
// NOTE[W-12567340]: This property stores in-memory mappings between draft
|
|
1046
|
-
// ids and canonical ids for the current session. Having a local copy of
|
|
1047
|
-
// these mappings is necessary to avoid a race condition between publishing
|
|
1048
|
-
// new mappings to the durable store and those mappings being loaded into
|
|
1049
|
-
// the luvio store redirect table, during which a new draft might be enqueued
|
|
1050
|
-
// which would not see a necessary mapping.
|
|
1051
|
-
this.ephemeralRedirects = {};
|
|
1052
|
-
}
|
|
1053
|
-
enqueue(data) {
|
|
1054
|
-
return this.draftQueue.enqueue(this.handlerId, data);
|
|
1055
|
-
}
|
|
1056
|
-
async handleAction(action, actionCompleted, actionErrored) {
|
|
1057
|
-
const { data: request } = action;
|
|
1058
|
-
// no context is stored in draft action
|
|
1059
|
-
try {
|
|
1060
|
-
const response = await this.networkAdapter(request, {});
|
|
1061
|
-
if (response.ok) {
|
|
1062
|
-
await actionCompleted({
|
|
1063
|
-
...action,
|
|
1064
|
-
response,
|
|
1065
|
-
status: DraftActionStatus.Completed,
|
|
1066
|
-
});
|
|
1067
|
-
return ProcessActionResult.ACTION_SUCCEEDED;
|
|
1068
|
-
}
|
|
1069
|
-
await actionErrored({
|
|
1070
|
-
...action,
|
|
1071
|
-
error: response,
|
|
1072
|
-
status: DraftActionStatus.Error,
|
|
1073
|
-
}, false);
|
|
1074
|
-
return ProcessActionResult.ACTION_ERRORED;
|
|
1075
|
-
}
|
|
1076
|
-
catch (_a) {
|
|
1077
|
-
await actionErrored(action, true);
|
|
1078
|
-
return ProcessActionResult.NETWORK_ERROR;
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
async buildPendingAction(request, queue) {
|
|
1082
|
-
const targetId = await this.getIdFromRequest(request);
|
|
1083
|
-
if (targetId === undefined) {
|
|
1084
|
-
return Promise.reject(new Error('Cannot determine target id from the resource request'));
|
|
1085
|
-
}
|
|
1086
|
-
const tag = this.buildTagForTargetId(targetId);
|
|
1087
|
-
const handlerActions = queue.filter((x) => x.handler === this.handlerId);
|
|
1088
|
-
if (request.method === 'post' && actionsForTag(tag, handlerActions).length > 0) {
|
|
1089
|
-
return Promise.reject(new Error('Cannot enqueue a POST draft action with an existing tag'));
|
|
1090
|
-
}
|
|
1091
|
-
if (deleteActionsForTag(tag, handlerActions).length > 0) {
|
|
1092
|
-
return Promise.reject(new Error('Cannot enqueue a draft action for a deleted record'));
|
|
1093
|
-
}
|
|
1094
|
-
return {
|
|
1095
|
-
handler: this.handlerId,
|
|
1096
|
-
targetId,
|
|
1097
|
-
tag,
|
|
1098
|
-
data: request,
|
|
1099
|
-
status: DraftActionStatus.Pending,
|
|
1100
|
-
id: generateUniqueDraftActionId(queue.map((x) => x.id)),
|
|
1101
|
-
timestamp: Date.now(),
|
|
1102
|
-
metadata: {},
|
|
1103
|
-
version: '242.0.0',
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
async handleActionEnqueued(action) {
|
|
1107
|
-
const { method } = action.data;
|
|
1108
|
-
// delete adapters don't get a value back to ingest so
|
|
1109
|
-
// we ingest it for them here
|
|
1110
|
-
if (method === 'delete') {
|
|
1111
|
-
await this.reingestRecord(action);
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
handleActionRemoved(action) {
|
|
1115
|
-
return this.reingestRecord(action);
|
|
1116
|
-
}
|
|
1117
|
-
getQueueOperationsForCompletingDrafts(queue, action) {
|
|
1118
|
-
const queueOperations = [];
|
|
1119
|
-
const redirects = this.getRedirectMappings(action);
|
|
1120
|
-
if (redirects !== undefined) {
|
|
1121
|
-
const { length } = queue;
|
|
1122
|
-
for (let i = 0; i < length; i++) {
|
|
1123
|
-
const queueAction = queue[i];
|
|
1124
|
-
// if this queueAction is the action that is completing we can move on,
|
|
1125
|
-
// it is about to be deleted and won't have the draft ID in it
|
|
1126
|
-
if (queueAction.id === action.id) {
|
|
1127
|
-
continue;
|
|
1128
|
-
}
|
|
1129
|
-
if (isResourceRequestAction(queueAction)) {
|
|
1130
|
-
let queueOperationMutated = false;
|
|
1131
|
-
let updatedActionTag = undefined;
|
|
1132
|
-
let updatedActionTargetId = undefined;
|
|
1133
|
-
const { tag: queueActionTag, data: queueActionRequest, id: queueActionId, } = queueAction;
|
|
1134
|
-
let { basePath, body } = queueActionRequest;
|
|
1135
|
-
let stringifiedBody = stringify(body);
|
|
1136
|
-
// for each redirected ID/key we loop over the operation to see if it needs
|
|
1137
|
-
// to be updated
|
|
1138
|
-
for (const { draftId, draftKey, canonicalId, canonicalKey } of redirects) {
|
|
1139
|
-
if (basePath.search(draftId) >= 0 || stringifiedBody.search(draftId) >= 0) {
|
|
1140
|
-
basePath = basePath.replace(draftId, canonicalId);
|
|
1141
|
-
stringifiedBody = stringifiedBody.replace(draftId, canonicalId);
|
|
1142
|
-
queueOperationMutated = true;
|
|
1143
|
-
}
|
|
1144
|
-
// if the action is performed on a previous draft id, we need to replace the action
|
|
1145
|
-
// with a new one at the updated canonical key
|
|
1146
|
-
if (queueActionTag === draftKey) {
|
|
1147
|
-
updatedActionTag = canonicalKey;
|
|
1148
|
-
updatedActionTargetId = canonicalId;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
if (queueOperationMutated) {
|
|
1152
|
-
if (updatedActionTag !== undefined && updatedActionTargetId !== undefined) {
|
|
1153
|
-
const updatedAction = {
|
|
1154
|
-
...queueAction,
|
|
1155
|
-
tag: updatedActionTag,
|
|
1156
|
-
targetId: updatedActionTargetId,
|
|
1157
|
-
data: {
|
|
1158
|
-
...queueActionRequest,
|
|
1159
|
-
basePath: basePath,
|
|
1160
|
-
body: parse(stringifiedBody),
|
|
1161
|
-
},
|
|
1162
|
-
};
|
|
1163
|
-
// item needs to be replaced with a new item at the new record key
|
|
1164
|
-
queueOperations.push({
|
|
1165
|
-
type: QueueOperationType.Delete,
|
|
1166
|
-
id: queueActionId,
|
|
1167
|
-
});
|
|
1168
|
-
queueOperations.push({
|
|
1169
|
-
type: QueueOperationType.Add,
|
|
1170
|
-
action: updatedAction,
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
else {
|
|
1174
|
-
const updatedAction = {
|
|
1175
|
-
...queueAction,
|
|
1176
|
-
data: {
|
|
1177
|
-
...queueActionRequest,
|
|
1178
|
-
basePath: basePath,
|
|
1179
|
-
body: parse(stringifiedBody),
|
|
1180
|
-
},
|
|
1181
|
-
};
|
|
1182
|
-
// item needs to be updated
|
|
1183
|
-
queueOperations.push({
|
|
1184
|
-
type: QueueOperationType.Update,
|
|
1185
|
-
id: queueActionId,
|
|
1186
|
-
action: updatedAction,
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
// delete completed action
|
|
1194
|
-
queueOperations.push({
|
|
1195
|
-
type: QueueOperationType.Delete,
|
|
1196
|
-
id: action.id,
|
|
1197
|
-
});
|
|
1198
|
-
return queueOperations;
|
|
1199
|
-
}
|
|
1200
|
-
getRedirectMappings(action) {
|
|
1201
|
-
if (action.data.method !== 'post') {
|
|
1202
|
-
return undefined;
|
|
1203
|
-
}
|
|
1204
|
-
const body = action.response.body;
|
|
1205
|
-
const canonicalId = this.getIdFromResponseBody(body);
|
|
1206
|
-
const draftId = action.targetId;
|
|
1207
|
-
if (draftId !== undefined && canonicalId !== undefined && draftId !== canonicalId) {
|
|
1208
|
-
this.ephemeralRedirects[draftId] = canonicalId;
|
|
1209
|
-
}
|
|
1210
|
-
return [
|
|
1211
|
-
{
|
|
1212
|
-
draftId,
|
|
1213
|
-
canonicalId,
|
|
1214
|
-
draftKey: this.buildTagForTargetId(draftId),
|
|
1215
|
-
canonicalKey: this.buildTagForTargetId(canonicalId),
|
|
1216
|
-
},
|
|
1217
|
-
];
|
|
1218
|
-
}
|
|
1219
|
-
async handleActionCompleted(action, queueOperations, _queue, allHandlers) {
|
|
1220
|
-
const { data: request, tag } = action;
|
|
1221
|
-
const { method } = request;
|
|
1222
|
-
if (method === 'delete') {
|
|
1223
|
-
return this.evictKey(tag);
|
|
1224
|
-
}
|
|
1225
|
-
const recordsToIngest = [];
|
|
1226
|
-
recordsToIngest.push({
|
|
1227
|
-
response: action.response.body,
|
|
1228
|
-
synchronousIngest: this.synchronousIngest.bind(this),
|
|
1229
|
-
buildCacheKeysForResponse: this.buildCacheKeysFromResponse.bind(this),
|
|
1230
|
-
});
|
|
1231
|
-
const recordsNeedingReplay = queueOperations.filter((x) => x.type === QueueOperationType.Update);
|
|
1232
|
-
for (const recordNeedingReplay of recordsNeedingReplay) {
|
|
1233
|
-
const { action } = recordNeedingReplay;
|
|
1234
|
-
if (isResourceRequestAction(action)) {
|
|
1235
|
-
// We can't assume the queue operation is for our handler, have to find the handler.
|
|
1236
|
-
const handler = allHandlers.find((h) => h.handlerId === action.handler);
|
|
1237
|
-
if (handler !== undefined) {
|
|
1238
|
-
const record = await handler.getDataForAction(action);
|
|
1239
|
-
if (record !== undefined) {
|
|
1240
|
-
recordsToIngest.push({
|
|
1241
|
-
response: record,
|
|
1242
|
-
synchronousIngest: handler.synchronousIngest.bind(handler),
|
|
1243
|
-
buildCacheKeysForResponse: handler.buildCacheKeysFromResponse.bind(handler),
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
await this.ingestResponses(recordsToIngest, action);
|
|
1250
|
-
}
|
|
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
|
-
}
|
|
1276
|
-
//reject if the action to replace is a POST action
|
|
1277
|
-
const pendingAction = actionToReplace;
|
|
1278
|
-
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');
|
|
1281
|
-
}
|
|
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
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
else {
|
|
1297
|
-
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
1298
|
-
throw new Error('Incompatible Action types to replace one another');
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
shouldDeleteActionByTagOnRemoval(action) {
|
|
1302
|
-
return action.data.method === 'post';
|
|
1303
|
-
}
|
|
1304
|
-
updateMetadata(_existingMetadata, incomingMetadata) {
|
|
1305
|
-
return incomingMetadata;
|
|
1306
|
-
}
|
|
1307
|
-
isActionOfType(action) {
|
|
1308
|
-
return action.handler === this.handlerId;
|
|
1309
|
-
}
|
|
1310
|
-
async reingestRecord(action) {
|
|
1311
|
-
const record = await this.getDataForAction(action);
|
|
1312
|
-
if (record !== undefined) {
|
|
1313
|
-
await this.ingestResponses([
|
|
1314
|
-
{
|
|
1315
|
-
response: record,
|
|
1316
|
-
synchronousIngest: this.synchronousIngest.bind(this),
|
|
1317
|
-
buildCacheKeysForResponse: this.buildCacheKeysFromResponse.bind(this),
|
|
1318
|
-
},
|
|
1319
|
-
], action);
|
|
1320
|
-
}
|
|
1321
|
-
else {
|
|
1322
|
-
await this.evictKey(action.tag);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
// Given an action for this handler this method should return the draft IDs.
|
|
1326
|
-
// Most of the time it will simply be the targetId, but certain handlers might
|
|
1327
|
-
// have multiple draft-created IDs so they would override this to return them.
|
|
1328
|
-
getDraftIdsFromAction(action) {
|
|
1329
|
-
return [action.targetId];
|
|
1330
|
-
}
|
|
1331
|
-
async ingestResponses(responses, action) {
|
|
1332
|
-
const luvio = this.getLuvio();
|
|
1333
|
-
await luvio.handleSuccessResponse(() => {
|
|
1334
|
-
for (const entry of responses) {
|
|
1335
|
-
const { response, synchronousIngest } = entry;
|
|
1336
|
-
synchronousIngest(response, action);
|
|
1337
|
-
}
|
|
1338
|
-
// must call base broadcast
|
|
1339
|
-
return luvio.storeBroadcast();
|
|
1340
|
-
},
|
|
1341
|
-
// getTypeCacheKeysRecord uses the response, not the full path factory
|
|
1342
|
-
// so 2nd parameter will be unused
|
|
1343
|
-
() => {
|
|
1344
|
-
const keySet = new StoreKeyMap();
|
|
1345
|
-
for (const entry of responses) {
|
|
1346
|
-
const { response, buildCacheKeysForResponse } = entry;
|
|
1347
|
-
const set = buildCacheKeysForResponse(response);
|
|
1348
|
-
for (const key of set.keys()) {
|
|
1349
|
-
const value = set.get(key);
|
|
1350
|
-
if (value !== undefined) {
|
|
1351
|
-
keySet.set(key, value);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
return keySet;
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
async evictKey(key) {
|
|
1359
|
-
const luvio = this.getLuvio();
|
|
1360
|
-
await luvio.handleSuccessResponse(() => {
|
|
1361
|
-
luvio.storeEvict(key);
|
|
1362
|
-
return luvio.storeBroadcast();
|
|
1363
|
-
}, () => {
|
|
1364
|
-
return new StoreKeyMap();
|
|
1365
|
-
});
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
function actionsForTag(tag, queue) {
|
|
1369
|
-
return queue.filter((action) => action.tag === tag);
|
|
1370
|
-
}
|
|
1371
|
-
function deleteActionsForTag(tag, queue) {
|
|
1372
|
-
return queue.filter((action) => action.tag === tag && action.data.method === 'delete');
|
|
1373
|
-
}
|
|
1374
|
-
function isResourceRequestAction(action) {
|
|
1375
|
-
const dataAsAny = action.data;
|
|
1376
|
-
return (dataAsAny !== undefined && dataAsAny.method !== undefined && dataAsAny.body !== undefined);
|
|
1040
|
+
class AbstractResourceRequestActionHandler {
|
|
1041
|
+
constructor(draftQueue, networkAdapter, getLuvio) {
|
|
1042
|
+
this.draftQueue = draftQueue;
|
|
1043
|
+
this.networkAdapter = networkAdapter;
|
|
1044
|
+
this.getLuvio = getLuvio;
|
|
1045
|
+
// NOTE[W-12567340]: This property stores in-memory mappings between draft
|
|
1046
|
+
// ids and canonical ids for the current session. Having a local copy of
|
|
1047
|
+
// these mappings is necessary to avoid a race condition between publishing
|
|
1048
|
+
// new mappings to the durable store and those mappings being loaded into
|
|
1049
|
+
// the luvio store redirect table, during which a new draft might be enqueued
|
|
1050
|
+
// which would not see a necessary mapping.
|
|
1051
|
+
this.ephemeralRedirects = {};
|
|
1052
|
+
}
|
|
1053
|
+
enqueue(data) {
|
|
1054
|
+
return this.draftQueue.enqueue(this.handlerId, data);
|
|
1055
|
+
}
|
|
1056
|
+
async handleAction(action, actionCompleted, actionErrored) {
|
|
1057
|
+
const { data: request } = action;
|
|
1058
|
+
// no context is stored in draft action
|
|
1059
|
+
try {
|
|
1060
|
+
const response = await this.networkAdapter(request, {});
|
|
1061
|
+
if (response.ok) {
|
|
1062
|
+
await actionCompleted({
|
|
1063
|
+
...action,
|
|
1064
|
+
response,
|
|
1065
|
+
status: DraftActionStatus.Completed,
|
|
1066
|
+
});
|
|
1067
|
+
return ProcessActionResult.ACTION_SUCCEEDED;
|
|
1068
|
+
}
|
|
1069
|
+
await actionErrored({
|
|
1070
|
+
...action,
|
|
1071
|
+
error: response,
|
|
1072
|
+
status: DraftActionStatus.Error,
|
|
1073
|
+
}, false);
|
|
1074
|
+
return ProcessActionResult.ACTION_ERRORED;
|
|
1075
|
+
}
|
|
1076
|
+
catch (_a) {
|
|
1077
|
+
await actionErrored(action, true);
|
|
1078
|
+
return ProcessActionResult.NETWORK_ERROR;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async buildPendingAction(request, queue) {
|
|
1082
|
+
const targetId = await this.getIdFromRequest(request);
|
|
1083
|
+
if (targetId === undefined) {
|
|
1084
|
+
return Promise.reject(new Error('Cannot determine target id from the resource request'));
|
|
1085
|
+
}
|
|
1086
|
+
const tag = this.buildTagForTargetId(targetId);
|
|
1087
|
+
const handlerActions = queue.filter((x) => x.handler === this.handlerId);
|
|
1088
|
+
if (request.method === 'post' && actionsForTag(tag, handlerActions).length > 0) {
|
|
1089
|
+
return Promise.reject(new Error('Cannot enqueue a POST draft action with an existing tag'));
|
|
1090
|
+
}
|
|
1091
|
+
if (deleteActionsForTag(tag, handlerActions).length > 0) {
|
|
1092
|
+
return Promise.reject(new Error('Cannot enqueue a draft action for a deleted record'));
|
|
1093
|
+
}
|
|
1094
|
+
return {
|
|
1095
|
+
handler: this.handlerId,
|
|
1096
|
+
targetId,
|
|
1097
|
+
tag,
|
|
1098
|
+
data: request,
|
|
1099
|
+
status: DraftActionStatus.Pending,
|
|
1100
|
+
id: generateUniqueDraftActionId(queue.map((x) => x.id)),
|
|
1101
|
+
timestamp: Date.now(),
|
|
1102
|
+
metadata: {},
|
|
1103
|
+
version: '242.0.0',
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
async handleActionEnqueued(action) {
|
|
1107
|
+
const { method } = action.data;
|
|
1108
|
+
// delete adapters don't get a value back to ingest so
|
|
1109
|
+
// we ingest it for them here
|
|
1110
|
+
if (method === 'delete') {
|
|
1111
|
+
await this.reingestRecord(action);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
handleActionRemoved(action) {
|
|
1115
|
+
return this.reingestRecord(action);
|
|
1116
|
+
}
|
|
1117
|
+
getQueueOperationsForCompletingDrafts(queue, action) {
|
|
1118
|
+
const queueOperations = [];
|
|
1119
|
+
const redirects = this.getRedirectMappings(action);
|
|
1120
|
+
if (redirects !== undefined) {
|
|
1121
|
+
const { length } = queue;
|
|
1122
|
+
for (let i = 0; i < length; i++) {
|
|
1123
|
+
const queueAction = queue[i];
|
|
1124
|
+
// if this queueAction is the action that is completing we can move on,
|
|
1125
|
+
// it is about to be deleted and won't have the draft ID in it
|
|
1126
|
+
if (queueAction.id === action.id) {
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (isResourceRequestAction(queueAction)) {
|
|
1130
|
+
let queueOperationMutated = false;
|
|
1131
|
+
let updatedActionTag = undefined;
|
|
1132
|
+
let updatedActionTargetId = undefined;
|
|
1133
|
+
const { tag: queueActionTag, data: queueActionRequest, id: queueActionId, } = queueAction;
|
|
1134
|
+
let { basePath, body } = queueActionRequest;
|
|
1135
|
+
let stringifiedBody = stringify(body);
|
|
1136
|
+
// for each redirected ID/key we loop over the operation to see if it needs
|
|
1137
|
+
// to be updated
|
|
1138
|
+
for (const { draftId, draftKey, canonicalId, canonicalKey } of redirects) {
|
|
1139
|
+
if (basePath.search(draftId) >= 0 || stringifiedBody.search(draftId) >= 0) {
|
|
1140
|
+
basePath = basePath.replace(draftId, canonicalId);
|
|
1141
|
+
stringifiedBody = stringifiedBody.replace(draftId, canonicalId);
|
|
1142
|
+
queueOperationMutated = true;
|
|
1143
|
+
}
|
|
1144
|
+
// if the action is performed on a previous draft id, we need to replace the action
|
|
1145
|
+
// with a new one at the updated canonical key
|
|
1146
|
+
if (queueActionTag === draftKey) {
|
|
1147
|
+
updatedActionTag = canonicalKey;
|
|
1148
|
+
updatedActionTargetId = canonicalId;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (queueOperationMutated) {
|
|
1152
|
+
if (updatedActionTag !== undefined && updatedActionTargetId !== undefined) {
|
|
1153
|
+
const updatedAction = {
|
|
1154
|
+
...queueAction,
|
|
1155
|
+
tag: updatedActionTag,
|
|
1156
|
+
targetId: updatedActionTargetId,
|
|
1157
|
+
data: {
|
|
1158
|
+
...queueActionRequest,
|
|
1159
|
+
basePath: basePath,
|
|
1160
|
+
body: parse(stringifiedBody),
|
|
1161
|
+
},
|
|
1162
|
+
};
|
|
1163
|
+
// item needs to be replaced with a new item at the new record key
|
|
1164
|
+
queueOperations.push({
|
|
1165
|
+
type: QueueOperationType.Delete,
|
|
1166
|
+
id: queueActionId,
|
|
1167
|
+
});
|
|
1168
|
+
queueOperations.push({
|
|
1169
|
+
type: QueueOperationType.Add,
|
|
1170
|
+
action: updatedAction,
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
const updatedAction = {
|
|
1175
|
+
...queueAction,
|
|
1176
|
+
data: {
|
|
1177
|
+
...queueActionRequest,
|
|
1178
|
+
basePath: basePath,
|
|
1179
|
+
body: parse(stringifiedBody),
|
|
1180
|
+
},
|
|
1181
|
+
};
|
|
1182
|
+
// item needs to be updated
|
|
1183
|
+
queueOperations.push({
|
|
1184
|
+
type: QueueOperationType.Update,
|
|
1185
|
+
id: queueActionId,
|
|
1186
|
+
action: updatedAction,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
// delete completed action
|
|
1194
|
+
queueOperations.push({
|
|
1195
|
+
type: QueueOperationType.Delete,
|
|
1196
|
+
id: action.id,
|
|
1197
|
+
});
|
|
1198
|
+
return queueOperations;
|
|
1199
|
+
}
|
|
1200
|
+
getRedirectMappings(action) {
|
|
1201
|
+
if (action.data.method !== 'post') {
|
|
1202
|
+
return undefined;
|
|
1203
|
+
}
|
|
1204
|
+
const body = action.response.body;
|
|
1205
|
+
const canonicalId = this.getIdFromResponseBody(body);
|
|
1206
|
+
const draftId = action.targetId;
|
|
1207
|
+
if (draftId !== undefined && canonicalId !== undefined && draftId !== canonicalId) {
|
|
1208
|
+
this.ephemeralRedirects[draftId] = canonicalId;
|
|
1209
|
+
}
|
|
1210
|
+
return [
|
|
1211
|
+
{
|
|
1212
|
+
draftId,
|
|
1213
|
+
canonicalId,
|
|
1214
|
+
draftKey: this.buildTagForTargetId(draftId),
|
|
1215
|
+
canonicalKey: this.buildTagForTargetId(canonicalId),
|
|
1216
|
+
},
|
|
1217
|
+
];
|
|
1218
|
+
}
|
|
1219
|
+
async handleActionCompleted(action, queueOperations, _queue, allHandlers) {
|
|
1220
|
+
const { data: request, tag } = action;
|
|
1221
|
+
const { method } = request;
|
|
1222
|
+
if (method === 'delete') {
|
|
1223
|
+
return this.evictKey(tag);
|
|
1224
|
+
}
|
|
1225
|
+
const recordsToIngest = [];
|
|
1226
|
+
recordsToIngest.push({
|
|
1227
|
+
response: action.response.body,
|
|
1228
|
+
synchronousIngest: this.synchronousIngest.bind(this),
|
|
1229
|
+
buildCacheKeysForResponse: this.buildCacheKeysFromResponse.bind(this),
|
|
1230
|
+
});
|
|
1231
|
+
const recordsNeedingReplay = queueOperations.filter((x) => x.type === QueueOperationType.Update);
|
|
1232
|
+
for (const recordNeedingReplay of recordsNeedingReplay) {
|
|
1233
|
+
const { action } = recordNeedingReplay;
|
|
1234
|
+
if (isResourceRequestAction(action)) {
|
|
1235
|
+
// We can't assume the queue operation is for our handler, have to find the handler.
|
|
1236
|
+
const handler = allHandlers.find((h) => h.handlerId === action.handler);
|
|
1237
|
+
if (handler !== undefined) {
|
|
1238
|
+
const record = await handler.getDataForAction(action);
|
|
1239
|
+
if (record !== undefined) {
|
|
1240
|
+
recordsToIngest.push({
|
|
1241
|
+
response: record,
|
|
1242
|
+
synchronousIngest: handler.synchronousIngest.bind(handler),
|
|
1243
|
+
buildCacheKeysForResponse: handler.buildCacheKeysFromResponse.bind(handler),
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
await this.ingestResponses(recordsToIngest, action);
|
|
1250
|
+
}
|
|
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
|
+
}
|
|
1276
|
+
//reject if the action to replace is a POST action
|
|
1277
|
+
const pendingAction = actionToReplace;
|
|
1278
|
+
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');
|
|
1281
|
+
}
|
|
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
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
1298
|
+
throw new Error('Incompatible Action types to replace one another');
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
shouldDeleteActionByTagOnRemoval(action) {
|
|
1302
|
+
return action.data.method === 'post';
|
|
1303
|
+
}
|
|
1304
|
+
updateMetadata(_existingMetadata, incomingMetadata) {
|
|
1305
|
+
return incomingMetadata;
|
|
1306
|
+
}
|
|
1307
|
+
isActionOfType(action) {
|
|
1308
|
+
return action.handler === this.handlerId;
|
|
1309
|
+
}
|
|
1310
|
+
async reingestRecord(action) {
|
|
1311
|
+
const record = await this.getDataForAction(action);
|
|
1312
|
+
if (record !== undefined) {
|
|
1313
|
+
await this.ingestResponses([
|
|
1314
|
+
{
|
|
1315
|
+
response: record,
|
|
1316
|
+
synchronousIngest: this.synchronousIngest.bind(this),
|
|
1317
|
+
buildCacheKeysForResponse: this.buildCacheKeysFromResponse.bind(this),
|
|
1318
|
+
},
|
|
1319
|
+
], action);
|
|
1320
|
+
}
|
|
1321
|
+
else {
|
|
1322
|
+
await this.evictKey(action.tag);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
// Given an action for this handler this method should return the draft IDs.
|
|
1326
|
+
// Most of the time it will simply be the targetId, but certain handlers might
|
|
1327
|
+
// have multiple draft-created IDs so they would override this to return them.
|
|
1328
|
+
getDraftIdsFromAction(action) {
|
|
1329
|
+
return [action.targetId];
|
|
1330
|
+
}
|
|
1331
|
+
async ingestResponses(responses, action) {
|
|
1332
|
+
const luvio = this.getLuvio();
|
|
1333
|
+
await luvio.handleSuccessResponse(() => {
|
|
1334
|
+
for (const entry of responses) {
|
|
1335
|
+
const { response, synchronousIngest } = entry;
|
|
1336
|
+
synchronousIngest(response, action);
|
|
1337
|
+
}
|
|
1338
|
+
// must call base broadcast
|
|
1339
|
+
return luvio.storeBroadcast();
|
|
1340
|
+
},
|
|
1341
|
+
// getTypeCacheKeysRecord uses the response, not the full path factory
|
|
1342
|
+
// so 2nd parameter will be unused
|
|
1343
|
+
() => {
|
|
1344
|
+
const keySet = new StoreKeyMap();
|
|
1345
|
+
for (const entry of responses) {
|
|
1346
|
+
const { response, buildCacheKeysForResponse } = entry;
|
|
1347
|
+
const set = buildCacheKeysForResponse(response);
|
|
1348
|
+
for (const key of set.keys()) {
|
|
1349
|
+
const value = set.get(key);
|
|
1350
|
+
if (value !== undefined) {
|
|
1351
|
+
keySet.set(key, value);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return keySet;
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
async evictKey(key) {
|
|
1359
|
+
const luvio = this.getLuvio();
|
|
1360
|
+
await luvio.handleSuccessResponse(() => {
|
|
1361
|
+
luvio.storeEvict(key);
|
|
1362
|
+
return luvio.storeBroadcast();
|
|
1363
|
+
}, () => {
|
|
1364
|
+
return new StoreKeyMap();
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function actionsForTag(tag, queue) {
|
|
1369
|
+
return queue.filter((action) => action.tag === tag);
|
|
1370
|
+
}
|
|
1371
|
+
function deleteActionsForTag(tag, queue) {
|
|
1372
|
+
return queue.filter((action) => action.tag === tag && action.data.method === 'delete');
|
|
1373
|
+
}
|
|
1374
|
+
function isResourceRequestAction(action) {
|
|
1375
|
+
const dataAsAny = action.data;
|
|
1376
|
+
return (dataAsAny !== undefined && dataAsAny.method !== undefined && dataAsAny.body !== undefined);
|
|
1377
1377
|
}
|
|
1378
1378
|
|
|
1379
|
-
/**
|
|
1380
|
-
* Denotes what kind of operation a DraftQueueItem represents.
|
|
1381
|
-
*/
|
|
1382
|
-
var DraftActionOperationType;
|
|
1383
|
-
(function (DraftActionOperationType) {
|
|
1384
|
-
DraftActionOperationType["Create"] = "create";
|
|
1385
|
-
DraftActionOperationType["Update"] = "update";
|
|
1386
|
-
DraftActionOperationType["Delete"] = "delete";
|
|
1387
|
-
DraftActionOperationType["Custom"] = "custom";
|
|
1388
|
-
})(DraftActionOperationType || (DraftActionOperationType = {}));
|
|
1389
|
-
var DraftQueueOperationType;
|
|
1390
|
-
(function (DraftQueueOperationType) {
|
|
1391
|
-
DraftQueueOperationType["ItemAdded"] = "added";
|
|
1392
|
-
DraftQueueOperationType["ItemDeleted"] = "deleted";
|
|
1393
|
-
DraftQueueOperationType["ItemCompleted"] = "completed";
|
|
1394
|
-
DraftQueueOperationType["ItemFailed"] = "failed";
|
|
1395
|
-
DraftQueueOperationType["ItemUpdated"] = "updated";
|
|
1396
|
-
DraftQueueOperationType["QueueStarted"] = "started";
|
|
1397
|
-
DraftQueueOperationType["QueueStopped"] = "stopped";
|
|
1398
|
-
})(DraftQueueOperationType || (DraftQueueOperationType = {}));
|
|
1399
|
-
/**
|
|
1400
|
-
* Converts the internal DraftAction's ResourceRequest into
|
|
1401
|
-
* a DraftActionOperationType.
|
|
1402
|
-
* Returns a DraftActionOperationType as long as the http request is a
|
|
1403
|
-
* valid method type for DraftQueue or else it is undefined.
|
|
1404
|
-
* @param action
|
|
1405
|
-
*/
|
|
1406
|
-
function getOperationTypeFrom(action) {
|
|
1407
|
-
if (isResourceRequestAction(action)) {
|
|
1408
|
-
if (action.data !== undefined && action.data.method !== undefined) {
|
|
1409
|
-
switch (action.data.method) {
|
|
1410
|
-
case 'put':
|
|
1411
|
-
case 'patch':
|
|
1412
|
-
return DraftActionOperationType.Update;
|
|
1413
|
-
case 'post':
|
|
1414
|
-
return DraftActionOperationType.Create;
|
|
1415
|
-
case 'delete':
|
|
1416
|
-
return DraftActionOperationType.Delete;
|
|
1417
|
-
default:
|
|
1418
|
-
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
1419
|
-
throw new Error(`${action.data.method} is an unsupported request method type for DraftQueue.`);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
else {
|
|
1423
|
-
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
1424
|
-
throw new Error(`action has no data found`);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
else {
|
|
1428
|
-
return DraftActionOperationType.Custom;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
function toQueueState(queue) {
|
|
1432
|
-
return (states) => {
|
|
1433
|
-
return {
|
|
1434
|
-
queueState: queue.getQueueState(),
|
|
1435
|
-
items: states,
|
|
1436
|
-
};
|
|
1437
|
-
};
|
|
1438
|
-
}
|
|
1439
|
-
class DraftManager {
|
|
1440
|
-
constructor(draftQueue) {
|
|
1441
|
-
this.listeners = [];
|
|
1442
|
-
this.draftQueue = draftQueue;
|
|
1443
|
-
draftQueue.registerOnChangedListener((event) => {
|
|
1444
|
-
if (this.shouldEmitDraftEvent(event)) {
|
|
1445
|
-
return this.callListeners(event);
|
|
1446
|
-
}
|
|
1447
|
-
return Promise.resolve();
|
|
1448
|
-
});
|
|
1449
|
-
}
|
|
1450
|
-
shouldEmitDraftEvent(event) {
|
|
1451
|
-
const { type } = event;
|
|
1452
|
-
return [
|
|
1453
|
-
DraftQueueEventType.ActionAdded,
|
|
1454
|
-
DraftQueueEventType.ActionCompleted,
|
|
1455
|
-
DraftQueueEventType.ActionDeleted,
|
|
1456
|
-
DraftQueueEventType.ActionFailed,
|
|
1457
|
-
DraftQueueEventType.ActionUpdated,
|
|
1458
|
-
DraftQueueEventType.QueueStateChanged,
|
|
1459
|
-
].includes(type);
|
|
1460
|
-
}
|
|
1461
|
-
draftQueueEventTypeToOperationType(type) {
|
|
1462
|
-
switch (type) {
|
|
1463
|
-
case DraftQueueEventType.ActionAdded:
|
|
1464
|
-
return DraftQueueOperationType.ItemAdded;
|
|
1465
|
-
case DraftQueueEventType.ActionCompleted:
|
|
1466
|
-
return DraftQueueOperationType.ItemCompleted;
|
|
1467
|
-
case DraftQueueEventType.ActionDeleted:
|
|
1468
|
-
return DraftQueueOperationType.ItemDeleted;
|
|
1469
|
-
case DraftQueueEventType.ActionFailed:
|
|
1470
|
-
return DraftQueueOperationType.ItemFailed;
|
|
1471
|
-
case DraftQueueEventType.ActionUpdated:
|
|
1472
|
-
return DraftQueueOperationType.ItemUpdated;
|
|
1473
|
-
default:
|
|
1474
|
-
throw Error('Unsupported event type');
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
draftQueueStateToOperationType(state) {
|
|
1478
|
-
switch (state) {
|
|
1479
|
-
case DraftQueueState.Started:
|
|
1480
|
-
return DraftQueueOperationType.QueueStarted;
|
|
1481
|
-
case DraftQueueState.Stopped:
|
|
1482
|
-
return DraftQueueOperationType.QueueStopped;
|
|
1483
|
-
default:
|
|
1484
|
-
throw Error('Unsupported event type');
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
/**
|
|
1488
|
-
* Enqueue a custom action on the DraftQueue for a handler
|
|
1489
|
-
* @param handler the handler's id
|
|
1490
|
-
* @param targetId
|
|
1491
|
-
* @param tag - the key to group with in durable store
|
|
1492
|
-
* @param metadata
|
|
1493
|
-
* @returns
|
|
1494
|
-
*/
|
|
1495
|
-
addCustomAction(handler, targetId, tag, metadata) {
|
|
1496
|
-
return this.draftQueue
|
|
1497
|
-
.enqueue(handler, {
|
|
1498
|
-
data: metadata,
|
|
1499
|
-
handler,
|
|
1500
|
-
targetId,
|
|
1501
|
-
tag,
|
|
1502
|
-
})
|
|
1503
|
-
.then((result) => {
|
|
1504
|
-
return this.buildDraftQueueItem(result.action);
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
/**
|
|
1508
|
-
* Get the current state of each of the DraftActions in the DraftQueue
|
|
1509
|
-
* @returns A promise of an array of the state of each item in the DraftQueue
|
|
1510
|
-
*/
|
|
1511
|
-
getQueue() {
|
|
1512
|
-
return this.draftQueue
|
|
1513
|
-
.getQueueActions()
|
|
1514
|
-
.then((queueActions) => {
|
|
1515
|
-
return queueActions.map(this.buildDraftQueueItem);
|
|
1516
|
-
})
|
|
1517
|
-
.then(toQueueState(this.draftQueue));
|
|
1518
|
-
}
|
|
1519
|
-
/**
|
|
1520
|
-
* Starts the draft queue and begins processing the first item in the queue.
|
|
1521
|
-
*/
|
|
1522
|
-
startQueue() {
|
|
1523
|
-
return this.draftQueue.startQueue();
|
|
1524
|
-
}
|
|
1525
|
-
/**
|
|
1526
|
-
* Stops the draft queue from processing more draft items after any current
|
|
1527
|
-
* in progress items are finished.
|
|
1528
|
-
*/
|
|
1529
|
-
stopQueue() {
|
|
1530
|
-
return this.draftQueue.stopQueue();
|
|
1531
|
-
}
|
|
1532
|
-
/**
|
|
1533
|
-
* Subscribes the listener to changes to the draft queue.
|
|
1534
|
-
*
|
|
1535
|
-
* Returns a closure to invoke in order to unsubscribe the listener
|
|
1536
|
-
* from changes to the draft queue.
|
|
1537
|
-
*
|
|
1538
|
-
* @param listener The listener closure to subscribe to changes
|
|
1539
|
-
*/
|
|
1540
|
-
registerDraftQueueChangedListener(listener) {
|
|
1541
|
-
this.listeners.push(listener);
|
|
1542
|
-
return () => {
|
|
1543
|
-
this.listeners = this.listeners.filter((l) => {
|
|
1544
|
-
return l !== listener;
|
|
1545
|
-
});
|
|
1546
|
-
return Promise.resolve();
|
|
1547
|
-
};
|
|
1548
|
-
}
|
|
1549
|
-
/**
|
|
1550
|
-
* Creates a custom action handler for the given handler
|
|
1551
|
-
* @param handlerId
|
|
1552
|
-
* @param executor
|
|
1553
|
-
* @returns
|
|
1554
|
-
*/
|
|
1555
|
-
setCustomActionExecutor(handlerId, executor) {
|
|
1556
|
-
return this.draftQueue
|
|
1557
|
-
.addCustomHandler(handlerId, (action, completed) => {
|
|
1558
|
-
executor(this.buildDraftQueueItem(action), completed);
|
|
1559
|
-
})
|
|
1560
|
-
.then(() => {
|
|
1561
|
-
return () => {
|
|
1562
|
-
this.draftQueue.removeHandler(handlerId);
|
|
1563
|
-
return Promise.resolve();
|
|
1564
|
-
};
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
|
-
buildDraftQueueItem(action) {
|
|
1568
|
-
const operationType = getOperationTypeFrom(action);
|
|
1569
|
-
const { id, status, timestamp, targetId, metadata } = action;
|
|
1570
|
-
const item = {
|
|
1571
|
-
id,
|
|
1572
|
-
targetId,
|
|
1573
|
-
state: status,
|
|
1574
|
-
timestamp,
|
|
1575
|
-
operationType,
|
|
1576
|
-
metadata,
|
|
1577
|
-
};
|
|
1578
|
-
if (isDraftError(action)) {
|
|
1579
|
-
// We should always return an array, if the body is just a dictionary,
|
|
1580
|
-
// stick it in an array
|
|
1581
|
-
const body = isArray(action.error.body) ? action.error.body : [action.error.body];
|
|
1582
|
-
const bodyString = stringify(body);
|
|
1583
|
-
item.error = {
|
|
1584
|
-
status: action.error.status || 0,
|
|
1585
|
-
ok: action.error.ok || false,
|
|
1586
|
-
headers: action.error.headers || {},
|
|
1587
|
-
statusText: action.error.statusText || '',
|
|
1588
|
-
bodyString,
|
|
1589
|
-
};
|
|
1590
|
-
}
|
|
1591
|
-
return item;
|
|
1592
|
-
}
|
|
1593
|
-
async callListeners(event) {
|
|
1594
|
-
if (this.listeners.length < 1) {
|
|
1595
|
-
return;
|
|
1596
|
-
}
|
|
1597
|
-
const managerState = await this.getQueue();
|
|
1598
|
-
let operationType, item;
|
|
1599
|
-
if (isDraftQueueStateChangeEvent(event)) {
|
|
1600
|
-
operationType = this.draftQueueStateToOperationType(event.state);
|
|
1601
|
-
}
|
|
1602
|
-
else {
|
|
1603
|
-
const { action, type } = event;
|
|
1604
|
-
item = this.buildDraftQueueItem(action);
|
|
1605
|
-
operationType = this.draftQueueEventTypeToOperationType(type);
|
|
1606
|
-
}
|
|
1607
|
-
for (let i = 0, len = this.listeners.length; i < len; i++) {
|
|
1608
|
-
const listener = this.listeners[i];
|
|
1609
|
-
listener(managerState, operationType, item);
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
/**
|
|
1613
|
-
* Removes the draft action identified by actionId from the draft queue.
|
|
1614
|
-
*
|
|
1615
|
-
* @param actionId The action identifier
|
|
1616
|
-
*
|
|
1617
|
-
* @returns The current state of the draft queue
|
|
1618
|
-
*/
|
|
1619
|
-
removeDraftAction(actionId) {
|
|
1620
|
-
return this.draftQueue.removeDraftAction(actionId).then(() => this.getQueue());
|
|
1621
|
-
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Replaces the resource request of `withAction` for the resource request
|
|
1624
|
-
* of `actionId`. Action ids cannot be equal. Both actions must be acting
|
|
1625
|
-
* on the same target object, and neither can currently be in progress.
|
|
1626
|
-
*
|
|
1627
|
-
* @param actionId The id of the draft action to replace
|
|
1628
|
-
* @param withActionId The id of the draft action that will replace the other
|
|
1629
|
-
*/
|
|
1630
|
-
replaceAction(actionId, withActionId) {
|
|
1631
|
-
return this.draftQueue.replaceAction(actionId, withActionId).then((replaced) => {
|
|
1632
|
-
return this.buildDraftQueueItem(replaced);
|
|
1633
|
-
});
|
|
1634
|
-
}
|
|
1635
|
-
/**
|
|
1636
|
-
* Merges two actions into a single target action. The target action maintains
|
|
1637
|
-
* its position in the queue, while the source action is removed from the queue.
|
|
1638
|
-
* Action ids cannot be equal. Both actions must be acting on the same target
|
|
1639
|
-
* object, and neither can currently be in progress.
|
|
1640
|
-
*
|
|
1641
|
-
* @param targetActionId The draft action id of the target action. This action
|
|
1642
|
-
* will be replaced with the merged result.
|
|
1643
|
-
* @param sourceActionId The draft action id to merge onto the target. This
|
|
1644
|
-
* action will be removed after the merge.
|
|
1645
|
-
*/
|
|
1646
|
-
mergeActions(targetActionId, sourceActionId) {
|
|
1647
|
-
return this.draftQueue.mergeActions(targetActionId, sourceActionId).then((merged) => {
|
|
1648
|
-
return this.buildDraftQueueItem(merged);
|
|
1649
|
-
});
|
|
1650
|
-
}
|
|
1651
|
-
/**
|
|
1652
|
-
* Sets the metadata object of the specified action to the
|
|
1653
|
-
* provided metadata
|
|
1654
|
-
* @param actionId The id of the action to set the metadata on
|
|
1655
|
-
* @param metadata The metadata to set on the specified action
|
|
1656
|
-
*/
|
|
1657
|
-
setMetadata(actionId, metadata) {
|
|
1658
|
-
return this.draftQueue.setMetadata(actionId, metadata).then((updatedAction) => {
|
|
1659
|
-
return this.buildDraftQueueItem(updatedAction);
|
|
1660
|
-
});
|
|
1661
|
-
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Denotes what kind of operation a DraftQueueItem represents.
|
|
1381
|
+
*/
|
|
1382
|
+
var DraftActionOperationType;
|
|
1383
|
+
(function (DraftActionOperationType) {
|
|
1384
|
+
DraftActionOperationType["Create"] = "create";
|
|
1385
|
+
DraftActionOperationType["Update"] = "update";
|
|
1386
|
+
DraftActionOperationType["Delete"] = "delete";
|
|
1387
|
+
DraftActionOperationType["Custom"] = "custom";
|
|
1388
|
+
})(DraftActionOperationType || (DraftActionOperationType = {}));
|
|
1389
|
+
var DraftQueueOperationType;
|
|
1390
|
+
(function (DraftQueueOperationType) {
|
|
1391
|
+
DraftQueueOperationType["ItemAdded"] = "added";
|
|
1392
|
+
DraftQueueOperationType["ItemDeleted"] = "deleted";
|
|
1393
|
+
DraftQueueOperationType["ItemCompleted"] = "completed";
|
|
1394
|
+
DraftQueueOperationType["ItemFailed"] = "failed";
|
|
1395
|
+
DraftQueueOperationType["ItemUpdated"] = "updated";
|
|
1396
|
+
DraftQueueOperationType["QueueStarted"] = "started";
|
|
1397
|
+
DraftQueueOperationType["QueueStopped"] = "stopped";
|
|
1398
|
+
})(DraftQueueOperationType || (DraftQueueOperationType = {}));
|
|
1399
|
+
/**
|
|
1400
|
+
* Converts the internal DraftAction's ResourceRequest into
|
|
1401
|
+
* a DraftActionOperationType.
|
|
1402
|
+
* Returns a DraftActionOperationType as long as the http request is a
|
|
1403
|
+
* valid method type for DraftQueue or else it is undefined.
|
|
1404
|
+
* @param action
|
|
1405
|
+
*/
|
|
1406
|
+
function getOperationTypeFrom(action) {
|
|
1407
|
+
if (isResourceRequestAction(action)) {
|
|
1408
|
+
if (action.data !== undefined && action.data.method !== undefined) {
|
|
1409
|
+
switch (action.data.method) {
|
|
1410
|
+
case 'put':
|
|
1411
|
+
case 'patch':
|
|
1412
|
+
return DraftActionOperationType.Update;
|
|
1413
|
+
case 'post':
|
|
1414
|
+
return DraftActionOperationType.Create;
|
|
1415
|
+
case 'delete':
|
|
1416
|
+
return DraftActionOperationType.Delete;
|
|
1417
|
+
default:
|
|
1418
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
1419
|
+
throw new Error(`${action.data.method} is an unsupported request method type for DraftQueue.`);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
1424
|
+
throw new Error(`action has no data found`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
return DraftActionOperationType.Custom;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
function toQueueState(queue) {
|
|
1432
|
+
return (states) => {
|
|
1433
|
+
return {
|
|
1434
|
+
queueState: queue.getQueueState(),
|
|
1435
|
+
items: states,
|
|
1436
|
+
};
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
class DraftManager {
|
|
1440
|
+
constructor(draftQueue) {
|
|
1441
|
+
this.listeners = [];
|
|
1442
|
+
this.draftQueue = draftQueue;
|
|
1443
|
+
draftQueue.registerOnChangedListener((event) => {
|
|
1444
|
+
if (this.shouldEmitDraftEvent(event)) {
|
|
1445
|
+
return this.callListeners(event);
|
|
1446
|
+
}
|
|
1447
|
+
return Promise.resolve();
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
shouldEmitDraftEvent(event) {
|
|
1451
|
+
const { type } = event;
|
|
1452
|
+
return [
|
|
1453
|
+
DraftQueueEventType.ActionAdded,
|
|
1454
|
+
DraftQueueEventType.ActionCompleted,
|
|
1455
|
+
DraftQueueEventType.ActionDeleted,
|
|
1456
|
+
DraftQueueEventType.ActionFailed,
|
|
1457
|
+
DraftQueueEventType.ActionUpdated,
|
|
1458
|
+
DraftQueueEventType.QueueStateChanged,
|
|
1459
|
+
].includes(type);
|
|
1460
|
+
}
|
|
1461
|
+
draftQueueEventTypeToOperationType(type) {
|
|
1462
|
+
switch (type) {
|
|
1463
|
+
case DraftQueueEventType.ActionAdded:
|
|
1464
|
+
return DraftQueueOperationType.ItemAdded;
|
|
1465
|
+
case DraftQueueEventType.ActionCompleted:
|
|
1466
|
+
return DraftQueueOperationType.ItemCompleted;
|
|
1467
|
+
case DraftQueueEventType.ActionDeleted:
|
|
1468
|
+
return DraftQueueOperationType.ItemDeleted;
|
|
1469
|
+
case DraftQueueEventType.ActionFailed:
|
|
1470
|
+
return DraftQueueOperationType.ItemFailed;
|
|
1471
|
+
case DraftQueueEventType.ActionUpdated:
|
|
1472
|
+
return DraftQueueOperationType.ItemUpdated;
|
|
1473
|
+
default:
|
|
1474
|
+
throw Error('Unsupported event type');
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
draftQueueStateToOperationType(state) {
|
|
1478
|
+
switch (state) {
|
|
1479
|
+
case DraftQueueState.Started:
|
|
1480
|
+
return DraftQueueOperationType.QueueStarted;
|
|
1481
|
+
case DraftQueueState.Stopped:
|
|
1482
|
+
return DraftQueueOperationType.QueueStopped;
|
|
1483
|
+
default:
|
|
1484
|
+
throw Error('Unsupported event type');
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Enqueue a custom action on the DraftQueue for a handler
|
|
1489
|
+
* @param handler the handler's id
|
|
1490
|
+
* @param targetId
|
|
1491
|
+
* @param tag - the key to group with in durable store
|
|
1492
|
+
* @param metadata
|
|
1493
|
+
* @returns
|
|
1494
|
+
*/
|
|
1495
|
+
addCustomAction(handler, targetId, tag, metadata) {
|
|
1496
|
+
return this.draftQueue
|
|
1497
|
+
.enqueue(handler, {
|
|
1498
|
+
data: metadata,
|
|
1499
|
+
handler,
|
|
1500
|
+
targetId,
|
|
1501
|
+
tag,
|
|
1502
|
+
})
|
|
1503
|
+
.then((result) => {
|
|
1504
|
+
return this.buildDraftQueueItem(result.action);
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Get the current state of each of the DraftActions in the DraftQueue
|
|
1509
|
+
* @returns A promise of an array of the state of each item in the DraftQueue
|
|
1510
|
+
*/
|
|
1511
|
+
getQueue() {
|
|
1512
|
+
return this.draftQueue
|
|
1513
|
+
.getQueueActions()
|
|
1514
|
+
.then((queueActions) => {
|
|
1515
|
+
return queueActions.map(this.buildDraftQueueItem);
|
|
1516
|
+
})
|
|
1517
|
+
.then(toQueueState(this.draftQueue));
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Starts the draft queue and begins processing the first item in the queue.
|
|
1521
|
+
*/
|
|
1522
|
+
startQueue() {
|
|
1523
|
+
return this.draftQueue.startQueue();
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Stops the draft queue from processing more draft items after any current
|
|
1527
|
+
* in progress items are finished.
|
|
1528
|
+
*/
|
|
1529
|
+
stopQueue() {
|
|
1530
|
+
return this.draftQueue.stopQueue();
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Subscribes the listener to changes to the draft queue.
|
|
1534
|
+
*
|
|
1535
|
+
* Returns a closure to invoke in order to unsubscribe the listener
|
|
1536
|
+
* from changes to the draft queue.
|
|
1537
|
+
*
|
|
1538
|
+
* @param listener The listener closure to subscribe to changes
|
|
1539
|
+
*/
|
|
1540
|
+
registerDraftQueueChangedListener(listener) {
|
|
1541
|
+
this.listeners.push(listener);
|
|
1542
|
+
return () => {
|
|
1543
|
+
this.listeners = this.listeners.filter((l) => {
|
|
1544
|
+
return l !== listener;
|
|
1545
|
+
});
|
|
1546
|
+
return Promise.resolve();
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Creates a custom action handler for the given handler
|
|
1551
|
+
* @param handlerId
|
|
1552
|
+
* @param executor
|
|
1553
|
+
* @returns
|
|
1554
|
+
*/
|
|
1555
|
+
setCustomActionExecutor(handlerId, executor) {
|
|
1556
|
+
return this.draftQueue
|
|
1557
|
+
.addCustomHandler(handlerId, (action, completed) => {
|
|
1558
|
+
executor(this.buildDraftQueueItem(action), completed);
|
|
1559
|
+
})
|
|
1560
|
+
.then(() => {
|
|
1561
|
+
return () => {
|
|
1562
|
+
this.draftQueue.removeHandler(handlerId);
|
|
1563
|
+
return Promise.resolve();
|
|
1564
|
+
};
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
buildDraftQueueItem(action) {
|
|
1568
|
+
const operationType = getOperationTypeFrom(action);
|
|
1569
|
+
const { id, status, timestamp, targetId, metadata } = action;
|
|
1570
|
+
const item = {
|
|
1571
|
+
id,
|
|
1572
|
+
targetId,
|
|
1573
|
+
state: status,
|
|
1574
|
+
timestamp,
|
|
1575
|
+
operationType,
|
|
1576
|
+
metadata,
|
|
1577
|
+
};
|
|
1578
|
+
if (isDraftError(action)) {
|
|
1579
|
+
// We should always return an array, if the body is just a dictionary,
|
|
1580
|
+
// stick it in an array
|
|
1581
|
+
const body = isArray(action.error.body) ? action.error.body : [action.error.body];
|
|
1582
|
+
const bodyString = stringify(body);
|
|
1583
|
+
item.error = {
|
|
1584
|
+
status: action.error.status || 0,
|
|
1585
|
+
ok: action.error.ok || false,
|
|
1586
|
+
headers: action.error.headers || {},
|
|
1587
|
+
statusText: action.error.statusText || '',
|
|
1588
|
+
bodyString,
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
return item;
|
|
1592
|
+
}
|
|
1593
|
+
async callListeners(event) {
|
|
1594
|
+
if (this.listeners.length < 1) {
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const managerState = await this.getQueue();
|
|
1598
|
+
let operationType, item;
|
|
1599
|
+
if (isDraftQueueStateChangeEvent(event)) {
|
|
1600
|
+
operationType = this.draftQueueStateToOperationType(event.state);
|
|
1601
|
+
}
|
|
1602
|
+
else {
|
|
1603
|
+
const { action, type } = event;
|
|
1604
|
+
item = this.buildDraftQueueItem(action);
|
|
1605
|
+
operationType = this.draftQueueEventTypeToOperationType(type);
|
|
1606
|
+
}
|
|
1607
|
+
for (let i = 0, len = this.listeners.length; i < len; i++) {
|
|
1608
|
+
const listener = this.listeners[i];
|
|
1609
|
+
listener(managerState, operationType, item);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Removes the draft action identified by actionId from the draft queue.
|
|
1614
|
+
*
|
|
1615
|
+
* @param actionId The action identifier
|
|
1616
|
+
*
|
|
1617
|
+
* @returns The current state of the draft queue
|
|
1618
|
+
*/
|
|
1619
|
+
removeDraftAction(actionId) {
|
|
1620
|
+
return this.draftQueue.removeDraftAction(actionId).then(() => this.getQueue());
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Replaces the resource request of `withAction` for the resource request
|
|
1624
|
+
* of `actionId`. Action ids cannot be equal. Both actions must be acting
|
|
1625
|
+
* on the same target object, and neither can currently be in progress.
|
|
1626
|
+
*
|
|
1627
|
+
* @param actionId The id of the draft action to replace
|
|
1628
|
+
* @param withActionId The id of the draft action that will replace the other
|
|
1629
|
+
*/
|
|
1630
|
+
replaceAction(actionId, withActionId) {
|
|
1631
|
+
return this.draftQueue.replaceAction(actionId, withActionId).then((replaced) => {
|
|
1632
|
+
return this.buildDraftQueueItem(replaced);
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Merges two actions into a single target action. The target action maintains
|
|
1637
|
+
* its position in the queue, while the source action is removed from the queue.
|
|
1638
|
+
* Action ids cannot be equal. Both actions must be acting on the same target
|
|
1639
|
+
* object, and neither can currently be in progress.
|
|
1640
|
+
*
|
|
1641
|
+
* @param targetActionId The draft action id of the target action. This action
|
|
1642
|
+
* will be replaced with the merged result.
|
|
1643
|
+
* @param sourceActionId The draft action id to merge onto the target. This
|
|
1644
|
+
* action will be removed after the merge.
|
|
1645
|
+
*/
|
|
1646
|
+
mergeActions(targetActionId, sourceActionId) {
|
|
1647
|
+
return this.draftQueue.mergeActions(targetActionId, sourceActionId).then((merged) => {
|
|
1648
|
+
return this.buildDraftQueueItem(merged);
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Sets the metadata object of the specified action to the
|
|
1653
|
+
* provided metadata
|
|
1654
|
+
* @param actionId The id of the action to set the metadata on
|
|
1655
|
+
* @param metadata The metadata to set on the specified action
|
|
1656
|
+
*/
|
|
1657
|
+
setMetadata(actionId, metadata) {
|
|
1658
|
+
return this.draftQueue.setMetadata(actionId, metadata).then((updatedAction) => {
|
|
1659
|
+
return this.buildDraftQueueItem(updatedAction);
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
1662
|
}
|
|
1663
1663
|
|
|
1664
|
-
function makeEnvironmentDraftAware(luvio, env, durableStore, handlers, draftQueue) {
|
|
1665
|
-
const draftMetadata = {};
|
|
1666
|
-
// setup existing store redirects when bootstrapping the environment
|
|
1667
|
-
(async () => {
|
|
1668
|
-
const mappings = await getDraftIdMappings(durableStore);
|
|
1669
|
-
mappings.forEach((mapping) => {
|
|
1670
|
-
const { draftKey, canonicalKey } = mapping;
|
|
1671
|
-
env.storeRedirect(draftKey, canonicalKey);
|
|
1672
|
-
});
|
|
1673
|
-
})();
|
|
1674
|
-
durableStore.registerOnChangedListener(async (changes) => {
|
|
1675
|
-
const draftIdMappingsIds = [];
|
|
1676
|
-
for (let i = 0, len = changes.length; i < len; i++) {
|
|
1677
|
-
const change = changes[i];
|
|
1678
|
-
if (change.segment === DRAFT_ID_MAPPINGS_SEGMENT) {
|
|
1679
|
-
draftIdMappingsIds.push(...change.ids);
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
if (draftIdMappingsIds.length > 0) {
|
|
1683
|
-
const mappings = await getDraftIdMappings(durableStore, draftIdMappingsIds);
|
|
1684
|
-
mappings.forEach((mapping) => {
|
|
1685
|
-
const { draftKey, canonicalKey } = mapping;
|
|
1686
|
-
env.storeRedirect(draftKey, canonicalKey);
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
});
|
|
1690
|
-
const handleSuccessResponse = async function (ingestAndBroadcastFunc, getResponseCacheKeysFunc) {
|
|
1691
|
-
const queue = await draftQueue.getQueueActions();
|
|
1692
|
-
if (queue.length === 0) {
|
|
1693
|
-
return env.handleSuccessResponse(ingestAndBroadcastFunc, getResponseCacheKeysFunc);
|
|
1694
|
-
}
|
|
1695
|
-
const cacheKeySet = getResponseCacheKeysFunc();
|
|
1696
|
-
for (const possibleKey of cacheKeySet.keys()) {
|
|
1697
|
-
const key = typeof possibleKey === 'string' ? possibleKey : '';
|
|
1698
|
-
const cacheKey = cacheKeySet.get(key);
|
|
1699
|
-
for (const handler of handlers) {
|
|
1700
|
-
if (cacheKey !== undefined &&
|
|
1701
|
-
handler.canRepresentationContainDraftMetadata(cacheKey.representationName)) {
|
|
1702
|
-
const metadata = await handler.getDraftMetadata(key);
|
|
1703
|
-
if (metadata !== undefined) {
|
|
1704
|
-
// if this key is related to a draft then mark it mergeable so
|
|
1705
|
-
// base environment revives it to staging store before ingestion
|
|
1706
|
-
cacheKey.mergeable = true;
|
|
1707
|
-
const existing = draftMetadata[key];
|
|
1708
|
-
if (existing === undefined) {
|
|
1709
|
-
draftMetadata[key] = { metadata, refCount: 1 };
|
|
1710
|
-
}
|
|
1711
|
-
else {
|
|
1712
|
-
draftMetadata[key] = { metadata, refCount: existing.refCount + 1 };
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
break;
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
return env.handleSuccessResponse(ingestAndBroadcastFunc, () => cacheKeySet);
|
|
1720
|
-
};
|
|
1721
|
-
const storePublish = function (key, data) {
|
|
1722
|
-
const draftMetadataForKey = draftMetadata[key];
|
|
1723
|
-
for (const handler of handlers) {
|
|
1724
|
-
if (handler.canHandlePublish(key)) {
|
|
1725
|
-
handler.applyDraftsToIncomingData(key, data, draftMetadataForKey && draftMetadataForKey.metadata, env.storePublish);
|
|
1726
|
-
return;
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
if (draftMetadataForKey !== undefined) {
|
|
1730
|
-
// WARNING: this code depends on the fact that RecordRepresentation
|
|
1731
|
-
// fields get published before the root record.
|
|
1732
|
-
if (draftMetadataForKey.refCount === 1) {
|
|
1733
|
-
delete draftMetadata[key];
|
|
1734
|
-
}
|
|
1735
|
-
else {
|
|
1736
|
-
draftMetadataForKey.refCount--;
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
// no handler could handle it so publish
|
|
1740
|
-
env.storePublish(key, data);
|
|
1741
|
-
};
|
|
1742
|
-
// note the makeEnvironmentUiApiRecordDraftAware will eventually go away once the adapters become draft aware
|
|
1743
|
-
return create(env, {
|
|
1744
|
-
storePublish: { value: storePublish },
|
|
1745
|
-
handleSuccessResponse: { value: handleSuccessResponse },
|
|
1746
|
-
});
|
|
1664
|
+
function makeEnvironmentDraftAware(luvio, env, durableStore, handlers, draftQueue) {
|
|
1665
|
+
const draftMetadata = {};
|
|
1666
|
+
// setup existing store redirects when bootstrapping the environment
|
|
1667
|
+
(async () => {
|
|
1668
|
+
const mappings = await getDraftIdMappings(durableStore);
|
|
1669
|
+
mappings.forEach((mapping) => {
|
|
1670
|
+
const { draftKey, canonicalKey } = mapping;
|
|
1671
|
+
env.storeRedirect(draftKey, canonicalKey);
|
|
1672
|
+
});
|
|
1673
|
+
})();
|
|
1674
|
+
durableStore.registerOnChangedListener(async (changes) => {
|
|
1675
|
+
const draftIdMappingsIds = [];
|
|
1676
|
+
for (let i = 0, len = changes.length; i < len; i++) {
|
|
1677
|
+
const change = changes[i];
|
|
1678
|
+
if (change.segment === DRAFT_ID_MAPPINGS_SEGMENT) {
|
|
1679
|
+
draftIdMappingsIds.push(...change.ids);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
if (draftIdMappingsIds.length > 0) {
|
|
1683
|
+
const mappings = await getDraftIdMappings(durableStore, draftIdMappingsIds);
|
|
1684
|
+
mappings.forEach((mapping) => {
|
|
1685
|
+
const { draftKey, canonicalKey } = mapping;
|
|
1686
|
+
env.storeRedirect(draftKey, canonicalKey);
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
const handleSuccessResponse = async function (ingestAndBroadcastFunc, getResponseCacheKeysFunc) {
|
|
1691
|
+
const queue = await draftQueue.getQueueActions();
|
|
1692
|
+
if (queue.length === 0) {
|
|
1693
|
+
return env.handleSuccessResponse(ingestAndBroadcastFunc, getResponseCacheKeysFunc);
|
|
1694
|
+
}
|
|
1695
|
+
const cacheKeySet = getResponseCacheKeysFunc();
|
|
1696
|
+
for (const possibleKey of cacheKeySet.keys()) {
|
|
1697
|
+
const key = typeof possibleKey === 'string' ? possibleKey : '';
|
|
1698
|
+
const cacheKey = cacheKeySet.get(key);
|
|
1699
|
+
for (const handler of handlers) {
|
|
1700
|
+
if (cacheKey !== undefined &&
|
|
1701
|
+
handler.canRepresentationContainDraftMetadata(cacheKey.representationName)) {
|
|
1702
|
+
const metadata = await handler.getDraftMetadata(key);
|
|
1703
|
+
if (metadata !== undefined) {
|
|
1704
|
+
// if this key is related to a draft then mark it mergeable so
|
|
1705
|
+
// base environment revives it to staging store before ingestion
|
|
1706
|
+
cacheKey.mergeable = true;
|
|
1707
|
+
const existing = draftMetadata[key];
|
|
1708
|
+
if (existing === undefined) {
|
|
1709
|
+
draftMetadata[key] = { metadata, refCount: 1 };
|
|
1710
|
+
}
|
|
1711
|
+
else {
|
|
1712
|
+
draftMetadata[key] = { metadata, refCount: existing.refCount + 1 };
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
break;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return env.handleSuccessResponse(ingestAndBroadcastFunc, () => cacheKeySet);
|
|
1720
|
+
};
|
|
1721
|
+
const storePublish = function (key, data) {
|
|
1722
|
+
const draftMetadataForKey = draftMetadata[key];
|
|
1723
|
+
for (const handler of handlers) {
|
|
1724
|
+
if (handler.canHandlePublish(key)) {
|
|
1725
|
+
handler.applyDraftsToIncomingData(key, data, draftMetadataForKey && draftMetadataForKey.metadata, env.storePublish);
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (draftMetadataForKey !== undefined) {
|
|
1730
|
+
// WARNING: this code depends on the fact that RecordRepresentation
|
|
1731
|
+
// fields get published before the root record.
|
|
1732
|
+
if (draftMetadataForKey.refCount === 1) {
|
|
1733
|
+
delete draftMetadata[key];
|
|
1734
|
+
}
|
|
1735
|
+
else {
|
|
1736
|
+
draftMetadataForKey.refCount--;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
// no handler could handle it so publish
|
|
1740
|
+
env.storePublish(key, data);
|
|
1741
|
+
};
|
|
1742
|
+
// note the makeEnvironmentUiApiRecordDraftAware will eventually go away once the adapters become draft aware
|
|
1743
|
+
return create(env, {
|
|
1744
|
+
storePublish: { value: storePublish },
|
|
1745
|
+
handleSuccessResponse: { value: handleSuccessResponse },
|
|
1746
|
+
});
|
|
1747
1747
|
}
|
|
1748
1748
|
|
|
1749
1749
|
export { AbstractResourceRequestActionHandler, CustomActionResultType, DRAFT_ERROR_CODE, DRAFT_ID_MAPPINGS_SEGMENT, DRAFT_SEGMENT, DraftActionOperationType, DraftActionStatus, DraftErrorFetchResponse, DraftFetchResponse, DraftManager, DraftQueueEventType, DraftQueueState, DraftSynthesisError, DurableDraftQueue, DurableDraftStore, ProcessActionResult, buildLuvioOverrideForDraftAdapters, createBadRequestResponse, createDeletedResponse, createDraftSynthesisErrorResponse, createInternalErrorResponse, createNotFoundResponse, createOkResponse, generateUniqueDraftActionId, isDraftSynthesisError, makeEnvironmentDraftAware, transformErrorToDraftSynthesisError };
|