@atlaskit/editor-synced-block-provider 2.12.1 → 2.12.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cjs/clients/block-service/blockService.js +6 -5
  3. package/dist/cjs/clients/confluence/contentProperty.js +4 -3
  4. package/dist/cjs/clients/confluence/sourceInfo.js +2 -1
  5. package/dist/cjs/common/types.js +5 -0
  6. package/dist/cjs/providers/block-service/blockServiceAPI.js +81 -37
  7. package/dist/cjs/providers/confluence/confluenceContentAPI.js +80 -42
  8. package/dist/cjs/providers/syncBlockProvider.js +13 -2
  9. package/dist/cjs/store-manager/referenceSyncBlockStoreManager.js +4 -6
  10. package/dist/cjs/store-manager/sourceSyncBlockStoreManager.js +10 -12
  11. package/dist/cjs/utils/resolveSyncBlockInstance.js +8 -4
  12. package/dist/cjs/utils/retry.js +66 -0
  13. package/dist/cjs/utils/utils.js +1 -10
  14. package/dist/es2019/clients/block-service/blockService.js +6 -5
  15. package/dist/es2019/clients/confluence/contentProperty.js +4 -3
  16. package/dist/es2019/clients/confluence/sourceInfo.js +2 -1
  17. package/dist/es2019/common/types.js +5 -0
  18. package/dist/es2019/providers/block-service/blockServiceAPI.js +43 -15
  19. package/dist/es2019/providers/confluence/confluenceContentAPI.js +35 -10
  20. package/dist/es2019/providers/syncBlockProvider.js +5 -0
  21. package/dist/es2019/store-manager/referenceSyncBlockStoreManager.js +4 -6
  22. package/dist/es2019/store-manager/sourceSyncBlockStoreManager.js +11 -13
  23. package/dist/es2019/utils/resolveSyncBlockInstance.js +8 -4
  24. package/dist/es2019/utils/retry.js +26 -0
  25. package/dist/es2019/utils/utils.js +0 -9
  26. package/dist/esm/clients/block-service/blockService.js +6 -5
  27. package/dist/esm/clients/confluence/contentProperty.js +4 -3
  28. package/dist/esm/clients/confluence/sourceInfo.js +2 -1
  29. package/dist/esm/common/types.js +5 -0
  30. package/dist/esm/providers/block-service/blockServiceAPI.js +81 -37
  31. package/dist/esm/providers/confluence/confluenceContentAPI.js +80 -42
  32. package/dist/esm/providers/syncBlockProvider.js +13 -2
  33. package/dist/esm/store-manager/referenceSyncBlockStoreManager.js +4 -6
  34. package/dist/esm/store-manager/sourceSyncBlockStoreManager.js +11 -13
  35. package/dist/esm/utils/resolveSyncBlockInstance.js +8 -4
  36. package/dist/esm/utils/retry.js +60 -0
  37. package/dist/esm/utils/utils.js +0 -9
  38. package/dist/types/common/types.d.ts +4 -0
  39. package/dist/types/providers/block-service/blockServiceAPI.d.ts +1 -0
  40. package/dist/types/providers/confluence/confluenceContentAPI.d.ts +1 -0
  41. package/dist/types/providers/syncBlockProvider.d.ts +1 -0
  42. package/dist/types/providers/types.d.ts +2 -0
  43. package/dist/types/utils/retry.d.ts +1 -0
  44. package/dist/types/utils/utils.d.ts +0 -1
  45. package/dist/types-ts4.5/common/types.d.ts +4 -0
  46. package/dist/types-ts4.5/providers/block-service/blockServiceAPI.d.ts +1 -0
  47. package/dist/types-ts4.5/providers/confluence/confluenceContentAPI.d.ts +1 -0
  48. package/dist/types-ts4.5/providers/syncBlockProvider.d.ts +1 -0
  49. package/dist/types-ts4.5/providers/types.d.ts +2 -0
  50. package/dist/types-ts4.5/utils/retry.d.ts +1 -0
  51. package/dist/types-ts4.5/utils/utils.d.ts +0 -1
  52. package/package.json +2 -2
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.fetchWithRetry = void 0;
8
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
9
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
10
+ var parseRetryAfter = function parseRetryAfter(retryAfter) {
11
+ var newDelay;
12
+
13
+ // retryAfter can either be in ms or HTTP date
14
+ var parsedRetryAfter = parseInt(retryAfter);
15
+ if (!isNaN(parsedRetryAfter)) {
16
+ newDelay = parsedRetryAfter * 1000;
17
+ } else {
18
+ var retryDate = new Date(retryAfter);
19
+ var delayFromDate = retryDate.getTime() - Date.now();
20
+ if (delayFromDate > 0) {
21
+ newDelay = delayFromDate;
22
+ }
23
+ }
24
+ return newDelay;
25
+ };
26
+ var _fetchWithRetry = exports.fetchWithRetry = /*#__PURE__*/function () {
27
+ var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(url, options) {
28
+ var retriesRemaining,
29
+ delay,
30
+ response,
31
+ shouldRetry,
32
+ retryAfter,
33
+ _args = arguments;
34
+ return _regenerator.default.wrap(function _callee$(_context) {
35
+ while (1) switch (_context.prev = _context.next) {
36
+ case 0:
37
+ retriesRemaining = _args.length > 2 && _args[2] !== undefined ? _args[2] : 3;
38
+ delay = _args.length > 3 && _args[3] !== undefined ? _args[3] : 1000;
39
+ _context.next = 4;
40
+ return fetch(url, options);
41
+ case 4:
42
+ response = _context.sent;
43
+ shouldRetry = !response.ok && response.status === 429 && retriesRemaining > 1;
44
+ if (shouldRetry) {
45
+ _context.next = 8;
46
+ break;
47
+ }
48
+ return _context.abrupt("return", response);
49
+ case 8:
50
+ retryAfter = response.headers.get('Retry-After');
51
+ _context.next = 11;
52
+ return new Promise(function (resolve) {
53
+ return setTimeout(resolve, retryAfter ? parseRetryAfter(retryAfter) : delay);
54
+ });
55
+ case 11:
56
+ return _context.abrupt("return", _fetchWithRetry(url, options, retriesRemaining - 1, delay * 2));
57
+ case 12:
58
+ case "end":
59
+ return _context.stop();
60
+ }
61
+ }, _callee);
62
+ }));
63
+ return function fetchWithRetry(_x, _x2) {
64
+ return _ref.apply(this, arguments);
65
+ };
66
+ }();
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.createSyncBlockNode = exports.createBodiedSyncBlockNode = exports.convertSyncBlockPMNodeToSyncBlockData = exports.convertSyncBlockJSONNodeToSyncBlockNode = exports.convertPMNodesToSyncBlockNodes = exports.convertPMNodeToSyncBlockNode = void 0;
6
+ exports.createSyncBlockNode = exports.convertSyncBlockPMNodeToSyncBlockData = exports.convertSyncBlockJSONNodeToSyncBlockNode = exports.convertPMNodesToSyncBlockNodes = exports.convertPMNodeToSyncBlockNode = void 0;
7
7
  var convertSyncBlockPMNodeToSyncBlockData = exports.convertSyncBlockPMNodeToSyncBlockData = function convertSyncBlockPMNodeToSyncBlockData(node) {
8
8
  return {
9
9
  blockInstanceId: node.attrs.localId,
@@ -20,15 +20,6 @@ var createSyncBlockNode = exports.createSyncBlockNode = function createSyncBlock
20
20
  }
21
21
  };
22
22
  };
23
- var createBodiedSyncBlockNode = exports.createBodiedSyncBlockNode = function createBodiedSyncBlockNode(localId, resourceId) {
24
- return {
25
- type: 'bodiedSyncBlock',
26
- attrs: {
27
- localId: localId,
28
- resourceId: resourceId
29
- }
30
- };
31
- };
32
23
  var convertSyncBlockJSONNodeToSyncBlockNode = exports.convertSyncBlockJSONNodeToSyncBlockNode = function convertSyncBlockJSONNodeToSyncBlockNode(node) {
33
24
  if (node.type !== 'syncBlock' || !node.attrs || !('localId' in node.attrs) || !('resourceId' in node.attrs) || typeof node.attrs.localId !== 'string' || typeof node.attrs.resourceId !== 'string') {
34
25
  return undefined;
@@ -1,3 +1,4 @@
1
+ import { fetchWithRetry } from '../../utils/retry';
1
2
  export const isBlockContentResponse = response => {
2
3
  const content = response.content;
3
4
  return typeof content === 'string';
@@ -46,7 +47,7 @@ export const isBlockContentResponse = response => {
46
47
  * Check https://block-service.dev.atl-paas.net/ for latest API documentation.
47
48
  */
48
49
  export const getReferenceSyncedBlocks = async documentAri => {
49
- const response = await fetch(`${BLOCK_SERVICE_API_URL}/block/document/reference/${encodeURIComponent(documentAri)}`, {
50
+ const response = await fetchWithRetry(`${BLOCK_SERVICE_API_URL}/block/document/reference/${encodeURIComponent(documentAri)}`, {
50
51
  method: 'GET',
51
52
  headers: COMMON_HEADERS
52
53
  });
@@ -69,7 +70,7 @@ export class BlockError extends Error {
69
70
  export const getSyncedBlockContent = async ({
70
71
  blockAri
71
72
  }) => {
72
- const response = await fetch(`${BLOCK_SERVICE_API_URL}/block/${encodeURIComponent(blockAri)}`, {
73
+ const response = await fetchWithRetry(`${BLOCK_SERVICE_API_URL}/block/${encodeURIComponent(blockAri)}`, {
73
74
  method: 'GET',
74
75
  headers: COMMON_HEADERS
75
76
  });
@@ -81,7 +82,7 @@ export const getSyncedBlockContent = async ({
81
82
  export const deleteSyncedBlock = async ({
82
83
  blockAri
83
84
  }) => {
84
- const response = await fetch(`${BLOCK_SERVICE_API_URL}/block/${encodeURIComponent(blockAri)}`, {
85
+ const response = await fetchWithRetry(`${BLOCK_SERVICE_API_URL}/block/${encodeURIComponent(blockAri)}`, {
85
86
  method: 'DELETE',
86
87
  headers: COMMON_HEADERS
87
88
  });
@@ -93,7 +94,7 @@ export const updateSyncedBlock = async ({
93
94
  blockAri,
94
95
  content
95
96
  }) => {
96
- const response = await fetch(`${BLOCK_SERVICE_API_URL}/block/${encodeURIComponent(blockAri)}`, {
97
+ const response = await fetchWithRetry(`${BLOCK_SERVICE_API_URL}/block/${encodeURIComponent(blockAri)}`, {
97
98
  method: 'PUT',
98
99
  headers: COMMON_HEADERS,
99
100
  body: JSON.stringify({
@@ -111,7 +112,7 @@ export const createSyncedBlock = async ({
111
112
  product,
112
113
  content
113
114
  }) => {
114
- const response = await fetch(`${BLOCK_SERVICE_API_URL}/block`, {
115
+ const response = await fetchWithRetry(`${BLOCK_SERVICE_API_URL}/block`, {
115
116
  method: 'POST',
116
117
  headers: COMMON_HEADERS,
117
118
  body: JSON.stringify({
@@ -1,3 +1,4 @@
1
+ import { fetchWithRetry } from '../../utils/retry';
1
2
  import { getConfluencePageAri } from './ari';
2
3
  import { isBlogPageType } from './utils';
3
4
  const COMMON_HEADERS = {
@@ -151,7 +152,7 @@ export const getContentProperty = async ({
151
152
  keys: [key]
152
153
  }
153
154
  };
154
- const response = await fetch(GRAPHQL_ENDPOINT, {
155
+ const response = await fetchWithRetry(GRAPHQL_ENDPOINT, {
155
156
  method: 'POST',
156
157
  headers: {
157
158
  ...COMMON_HEADERS,
@@ -237,7 +238,7 @@ export const createContentProperty = async ({
237
238
  }
238
239
  }
239
240
  };
240
- const response = await fetch(GRAPHQL_ENDPOINT, {
241
+ const response = await fetchWithRetry(GRAPHQL_ENDPOINT, {
241
242
  method: 'POST',
242
243
  headers: {
243
244
  ...COMMON_HEADERS,
@@ -272,7 +273,7 @@ export const deleteContentProperty = async ({
272
273
  }
273
274
  }
274
275
  };
275
- const response = await fetch(GRAPHQL_ENDPOINT, {
276
+ const response = await fetchWithRetry(GRAPHQL_ENDPOINT, {
276
277
  method: 'POST',
277
278
  headers: {
278
279
  ...COMMON_HEADERS,
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { logException } from '@atlaskit/editor-common/monitoring';
4
4
  import { getSourceInfoErrorPayload } from '../../utils/errorHandling';
5
+ import { fetchWithRetry } from '../../utils/retry';
5
6
  import { getPageIdAndTypeFromConfluencePageAri } from './ari';
6
7
  import { isBlogPageType } from './utils';
7
8
  const COMMON_HEADERS = {
@@ -41,7 +42,7 @@ const getConfluenceSourceInfo = async ari => {
41
42
  id: ari
42
43
  }
43
44
  };
44
- const response = await fetch(GRAPHQL_ENDPOINT, {
45
+ const response = await fetchWithRetry(GRAPHQL_ENDPOINT, {
45
46
  method: 'POST',
46
47
  headers: {
47
48
  ...COMMON_HEADERS,
@@ -2,6 +2,11 @@ export let SyncBlockError = /*#__PURE__*/function (SyncBlockError) {
2
2
  SyncBlockError["Errored"] = "errored";
3
3
  SyncBlockError["NotFound"] = "not_found";
4
4
  SyncBlockError["Forbidden"] = "forbidden";
5
+ SyncBlockError["InvalidRequest"] = "invalid_request";
6
+ SyncBlockError["RateLimited"] = "rate_limited";
7
+ SyncBlockError["Conflict"] = "conflict";
8
+ // attempt to create block that already exists
9
+ SyncBlockError["ServerError"] = "server_error";
5
10
  SyncBlockError["InvalidContent"] = "invalid_content"; // content is not a valid JSON
6
11
  return SyncBlockError;
7
12
  }({});
@@ -5,10 +5,21 @@ import { SyncBlockError } from '../../common/types';
5
5
  import { stringifyError } from '../../utils/errorHandling';
6
6
  const mapBlockError = error => {
7
7
  switch (error.status) {
8
+ case 400:
9
+ case 401:
10
+ return SyncBlockError.InvalidRequest;
8
11
  case 403:
9
12
  return SyncBlockError.Forbidden;
10
13
  case 404:
11
14
  return SyncBlockError.NotFound;
15
+ case 409:
16
+ return SyncBlockError.Conflict;
17
+ case 429:
18
+ return SyncBlockError.RateLimited;
19
+ case 500:
20
+ case 503:
21
+ case 504:
22
+ return SyncBlockError.ServerError;
12
23
  }
13
24
  return SyncBlockError.Errored;
14
25
  };
@@ -138,21 +149,38 @@ class BlockServiceADFWriteProvider {
138
149
  };
139
150
  } catch (error) {
140
151
  if (error instanceof BlockError) {
141
- if (error.status === 404) {
142
- // Create the block
143
- await createSyncedBlock({
144
- blockAri: resourceId,
145
- blockInstanceId: data.blockInstanceId,
146
- sourceAri: this.sourceAri,
147
- product: this.product,
148
- content: JSON.stringify(data.content)
149
- });
150
- } else {
151
- return {
152
- error: mapBlockError(error),
153
- resourceId
154
- };
155
- }
152
+ return {
153
+ error: mapBlockError(error),
154
+ resourceId
155
+ };
156
+ }
157
+ return {
158
+ error: stringifyError(error),
159
+ resourceId
160
+ };
161
+ }
162
+ }
163
+ async createData(data) {
164
+ const {
165
+ resourceId
166
+ } = data;
167
+ try {
168
+ await createSyncedBlock({
169
+ blockAri: resourceId,
170
+ blockInstanceId: data.blockInstanceId,
171
+ sourceAri: this.sourceAri,
172
+ product: this.product,
173
+ content: JSON.stringify(data.content)
174
+ });
175
+ return {
176
+ resourceId
177
+ };
178
+ } catch (error) {
179
+ if (error instanceof BlockError) {
180
+ return {
181
+ error: mapBlockError(error),
182
+ resourceId
183
+ };
156
184
  }
157
185
  return {
158
186
  error: stringifyError(error),
@@ -182,16 +182,6 @@ class ConfluenceADFWriteProvider {
182
182
  return {
183
183
  resourceId
184
184
  };
185
- } else if (!updateResult) {
186
- return this.createNewContentProperty(pageId, key, syncBlockDataWithSourceDocumentAri, pageType).then(() => {
187
- return {
188
- resourceId
189
- };
190
- }, error => {
191
- return {
192
- error
193
- };
194
- });
195
185
  } else {
196
186
  return {
197
187
  error: `Failed to update ${pageType} content property`
@@ -203,6 +193,41 @@ class ConfluenceADFWriteProvider {
203
193
  };
204
194
  }
205
195
  }
196
+ async createData(syncBlockData) {
197
+ let match;
198
+ const {
199
+ resourceId
200
+ } = syncBlockData;
201
+ try {
202
+ match = getPageIdAndTypeFromConfluencePageAri(resourceId);
203
+ } catch (error) {
204
+ return {
205
+ error: stringifyError(error)
206
+ };
207
+ }
208
+ const {
209
+ id: pageId,
210
+ type: pageType
211
+ } = match;
212
+ try {
213
+ const localId = getLocalIdFromConfluencePageAri(resourceId);
214
+ const key = getContentPropertyKey(this.config.contentPropertyKey, localId);
215
+ const sourceAri = getConfluencePageAri(pageId, this.config.cloudId, pageType);
216
+ const syncBlockDataWithSourceDocumentAri = {
217
+ ...syncBlockData,
218
+ product: 'confluence-page',
219
+ sourceAri
220
+ };
221
+ await this.createNewContentProperty(pageId, key, syncBlockDataWithSourceDocumentAri, pageType);
222
+ return {
223
+ resourceId
224
+ };
225
+ } catch (error) {
226
+ return Promise.resolve({
227
+ error: stringifyError(error)
228
+ });
229
+ }
230
+ }
206
231
  async deleteData(resourceId) {
207
232
  let deletePayload, deleteResult, match;
208
233
  try {
@@ -98,6 +98,11 @@ export class SyncBlockProvider extends SyncBlockDataProvider {
98
98
  }
99
99
  });
100
100
  }
101
+ createNodeData(data) {
102
+ return this.writeProvider.createData(data).then(result => result, error => ({
103
+ error
104
+ }));
105
+ }
101
106
 
102
107
  /**
103
108
  * Delete the data from the write provider
@@ -139,17 +139,15 @@ export class ReferenceSyncBlockStoreManager {
139
139
  (_this$fireAnalyticsEv5 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv5 === void 0 ? void 0 : _this$fireAnalyticsEv5.call(this, fetchErrorPayload(syncBlockInstance.error || 'Returned sync block instance does not have resource id'));
140
140
  return;
141
141
  }
142
+ const existingSyncBlock = this.getFromCache(syncBlockInstance.resourceId);
143
+ const resolvedSyncBlockInstance = existingSyncBlock ? resolveSyncBlockInstance(existingSyncBlock, syncBlockInstance) : syncBlockInstance;
144
+ this.updateCache(resolvedSyncBlockInstance);
145
+ resolvedData.push(resolvedSyncBlockInstance);
142
146
  if (syncBlockInstance.error) {
143
147
  var _this$fireAnalyticsEv6;
144
148
  (_this$fireAnalyticsEv6 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv6 === void 0 ? void 0 : _this$fireAnalyticsEv6.call(this, fetchErrorPayload(syncBlockInstance.error));
145
- this.updateCache(syncBlockInstance);
146
- resolvedData.push(syncBlockInstance);
147
149
  return;
148
150
  }
149
- const existingSyncBlock = this.getFromCache(syncBlockInstance.resourceId);
150
- const resolvedSyncBlockInstance = existingSyncBlock ? resolveSyncBlockInstance(existingSyncBlock, syncBlockInstance) : syncBlockInstance;
151
- this.updateCache(resolvedSyncBlockInstance);
152
- resolvedData.push(resolvedSyncBlockInstance);
153
151
  this.fetchSyncBlockSourceInfo(resolvedSyncBlockInstance.resourceId);
154
152
  });
155
153
  return [...resolvedData, ...blocksWithNotFoundError];
@@ -3,7 +3,7 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
3
  import uuid from 'uuid';
4
4
  import { logException } from '@atlaskit/editor-common/monitoring';
5
5
  import { updateErrorPayload, createErrorPayload, deleteErrorPayload, updateCacheErrorPayload } from '../utils/errorHandling';
6
- import { convertSyncBlockPMNodeToSyncBlockData, createBodiedSyncBlockNode } from '../utils/utils';
6
+ import { convertSyncBlockPMNodeToSyncBlockData } from '../utils/utils';
7
7
  // A store manager responsible for the lifecycle and state management of source sync blocks in an editor instance.
8
8
  // Designed to manage local in-memory state and synchronize with an external data provider.
9
9
  // Supports create, flush, and delete operations for source sync blocks.
@@ -172,21 +172,19 @@ export class SourceSyncBlockStoreManager {
172
172
  resourceId,
173
173
  localId: blockInstanceId
174
174
  } = attrs;
175
- this.dataProvider.writeNodesData([createBodiedSyncBlockNode(blockInstanceId, resourceId)], [{
175
+ this.dataProvider.createNodeData({
176
176
  content: [],
177
177
  blockInstanceId,
178
178
  resourceId: resourceId
179
- }]).then(results => {
180
- results.forEach(result => {
181
- const resourceId = result.resourceId;
182
- if (resourceId) {
183
- this.commitPendingCreation(true);
184
- } else {
185
- var _this$fireAnalyticsEv4;
186
- this.commitPendingCreation(false);
187
- (_this$fireAnalyticsEv4 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv4 === void 0 ? void 0 : _this$fireAnalyticsEv4.call(this, createErrorPayload(result.error || 'Failed to create bodied sync block'));
188
- }
189
- });
179
+ }).then(result => {
180
+ const resourceId = result.resourceId;
181
+ if (resourceId) {
182
+ this.commitPendingCreation(true);
183
+ } else {
184
+ var _this$fireAnalyticsEv4;
185
+ this.commitPendingCreation(false);
186
+ (_this$fireAnalyticsEv4 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv4 === void 0 ? void 0 : _this$fireAnalyticsEv4.call(this, createErrorPayload(result.error || 'Failed to create bodied sync block'));
187
+ }
190
188
  }).catch(error => {
191
189
  var _this$fireAnalyticsEv5;
192
190
  this.commitPendingCreation(false);
@@ -1,3 +1,4 @@
1
+ import { SyncBlockError } from '../common/types';
1
2
  /**
2
3
  * Merges two SyncBlockInstance objects,
3
4
  * currently it only preserves the sourceURL from the old result,
@@ -10,13 +11,16 @@
10
11
  */
11
12
  export const resolveSyncBlockInstance = (oldResult, newResult) => {
12
13
  var _newResult$data, _oldResult$data, _newResult$data2, _oldResult$data2;
13
- // if the old result has no data, we simple return the new result
14
+ // if the old result has no data, we simply return the new result
14
15
  if (!oldResult.data) {
15
16
  return newResult;
16
17
  } else if (!newResult.data) {
17
- // if the new result has no data, we simply return the old result
18
- // TODO: EDITOR-2533 - handle this case based on the error type and whether we should keep old data or not
19
- return oldResult;
18
+ // return the old result if there was an error, e.g. network error, but not if not found or forbidden
19
+ if (newResult.error === SyncBlockError.NotFound || newResult.error === SyncBlockError.Forbidden) {
20
+ return newResult;
21
+ } else {
22
+ return oldResult;
23
+ }
20
24
  }
21
25
 
22
26
  // otherwise, we merge the two results, preserving the sourceURL and sourceTitle from the old result if it exists
@@ -0,0 +1,26 @@
1
+ const parseRetryAfter = retryAfter => {
2
+ let newDelay;
3
+
4
+ // retryAfter can either be in ms or HTTP date
5
+ const parsedRetryAfter = parseInt(retryAfter);
6
+ if (!isNaN(parsedRetryAfter)) {
7
+ newDelay = parsedRetryAfter * 1000;
8
+ } else {
9
+ const retryDate = new Date(retryAfter);
10
+ const delayFromDate = retryDate.getTime() - Date.now();
11
+ if (delayFromDate > 0) {
12
+ newDelay = delayFromDate;
13
+ }
14
+ }
15
+ return newDelay;
16
+ };
17
+ export const fetchWithRetry = async (url, options, retriesRemaining = 3, delay = 1000) => {
18
+ const response = await fetch(url, options);
19
+ const shouldRetry = !response.ok && response.status === 429 && retriesRemaining > 1;
20
+ if (!shouldRetry) {
21
+ return response;
22
+ }
23
+ const retryAfter = response.headers.get('Retry-After');
24
+ await new Promise(resolve => setTimeout(resolve, retryAfter ? parseRetryAfter(retryAfter) : delay));
25
+ return fetchWithRetry(url, options, retriesRemaining - 1, delay * 2);
26
+ };
@@ -14,15 +14,6 @@ export const createSyncBlockNode = (localId, resourceId) => {
14
14
  }
15
15
  };
16
16
  };
17
- export const createBodiedSyncBlockNode = (localId, resourceId) => {
18
- return {
19
- type: 'bodiedSyncBlock',
20
- attrs: {
21
- localId,
22
- resourceId
23
- }
24
- };
25
- };
26
17
  export const convertSyncBlockJSONNodeToSyncBlockNode = node => {
27
18
  if (node.type !== 'syncBlock' || !node.attrs || !('localId' in node.attrs) || !('resourceId' in node.attrs) || typeof node.attrs.localId !== 'string' || typeof node.attrs.resourceId !== 'string') {
28
19
  return undefined;
@@ -8,6 +8,7 @@ import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
8
8
  function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
9
9
  function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
10
10
  import _regeneratorRuntime from "@babel/runtime/regenerator";
11
+ import { fetchWithRetry } from '../../utils/retry';
11
12
  export var isBlockContentResponse = function isBlockContentResponse(response) {
12
13
  var content = response.content;
13
14
  return typeof content === 'string';
@@ -62,7 +63,7 @@ export var getReferenceSyncedBlocks = /*#__PURE__*/function () {
62
63
  while (1) switch (_context.prev = _context.next) {
63
64
  case 0:
64
65
  _context.next = 2;
65
- return fetch("".concat(BLOCK_SERVICE_API_URL, "/block/document/reference/").concat(encodeURIComponent(documentAri)), {
66
+ return fetchWithRetry("".concat(BLOCK_SERVICE_API_URL, "/block/document/reference/").concat(encodeURIComponent(documentAri)), {
66
67
  method: 'GET',
67
68
  headers: COMMON_HEADERS
68
69
  });
@@ -112,7 +113,7 @@ export var getSyncedBlockContent = /*#__PURE__*/function () {
112
113
  case 0:
113
114
  blockAri = _ref2.blockAri;
114
115
  _context2.next = 3;
115
- return fetch("".concat(BLOCK_SERVICE_API_URL, "/block/").concat(encodeURIComponent(blockAri)), {
116
+ return fetchWithRetry("".concat(BLOCK_SERVICE_API_URL, "/block/").concat(encodeURIComponent(blockAri)), {
116
117
  method: 'GET',
117
118
  headers: COMMON_HEADERS
118
119
  });
@@ -146,7 +147,7 @@ export var deleteSyncedBlock = /*#__PURE__*/function () {
146
147
  case 0:
147
148
  blockAri = _ref4.blockAri;
148
149
  _context3.next = 3;
149
- return fetch("".concat(BLOCK_SERVICE_API_URL, "/block/").concat(encodeURIComponent(blockAri)), {
150
+ return fetchWithRetry("".concat(BLOCK_SERVICE_API_URL, "/block/").concat(encodeURIComponent(blockAri)), {
150
151
  method: 'DELETE',
151
152
  headers: COMMON_HEADERS
152
153
  });
@@ -175,7 +176,7 @@ export var updateSyncedBlock = /*#__PURE__*/function () {
175
176
  case 0:
176
177
  blockAri = _ref6.blockAri, content = _ref6.content;
177
178
  _context4.next = 3;
178
- return fetch("".concat(BLOCK_SERVICE_API_URL, "/block/").concat(encodeURIComponent(blockAri)), {
179
+ return fetchWithRetry("".concat(BLOCK_SERVICE_API_URL, "/block/").concat(encodeURIComponent(blockAri)), {
179
180
  method: 'PUT',
180
181
  headers: COMMON_HEADERS,
181
182
  body: JSON.stringify({
@@ -207,7 +208,7 @@ export var createSyncedBlock = /*#__PURE__*/function () {
207
208
  case 0:
208
209
  blockAri = _ref8.blockAri, blockInstanceId = _ref8.blockInstanceId, sourceAri = _ref8.sourceAri, product = _ref8.product, content = _ref8.content;
209
210
  _context5.next = 3;
210
- return fetch("".concat(BLOCK_SERVICE_API_URL, "/block"), {
211
+ return fetchWithRetry("".concat(BLOCK_SERVICE_API_URL, "/block"), {
211
212
  method: 'POST',
212
213
  headers: COMMON_HEADERS,
213
214
  body: JSON.stringify({
@@ -3,6 +3,7 @@ import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
3
3
  import _regeneratorRuntime from "@babel/runtime/regenerator";
4
4
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
5
5
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
6
+ import { fetchWithRetry } from '../../utils/retry';
6
7
  import { getConfluencePageAri } from './ari';
7
8
  import { isBlogPageType } from './utils';
8
9
  var COMMON_HEADERS = {
@@ -88,7 +89,7 @@ export var getContentProperty = /*#__PURE__*/function () {
88
89
  }
89
90
  };
90
91
  _context.next = 6;
91
- return fetch(GRAPHQL_ENDPOINT, {
92
+ return fetchWithRetry(GRAPHQL_ENDPOINT, {
92
93
  method: 'POST',
93
94
  headers: _objectSpread(_objectSpread({}, COMMON_HEADERS), AGG_HEADERS),
94
95
  body: JSON.stringify(bodyData)
@@ -197,7 +198,7 @@ export var createContentProperty = /*#__PURE__*/function () {
197
198
  }
198
199
  };
199
200
  _context3.next = 6;
200
- return fetch(GRAPHQL_ENDPOINT, {
201
+ return fetchWithRetry(GRAPHQL_ENDPOINT, {
201
202
  method: 'POST',
202
203
  headers: _objectSpread(_objectSpread({}, COMMON_HEADERS), AGG_HEADERS),
203
204
  body: JSON.stringify(bodyData)
@@ -247,7 +248,7 @@ export var deleteContentProperty = /*#__PURE__*/function () {
247
248
  }
248
249
  };
249
250
  _context4.next = 6;
250
- return fetch(GRAPHQL_ENDPOINT, {
251
+ return fetchWithRetry(GRAPHQL_ENDPOINT, {
251
252
  method: 'POST',
252
253
  headers: _objectSpread(_objectSpread({}, COMMON_HEADERS), AGG_HEADERS),
253
254
  body: JSON.stringify(bodyData)
@@ -7,6 +7,7 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
7
7
 
8
8
  import { logException } from '@atlaskit/editor-common/monitoring';
9
9
  import { getSourceInfoErrorPayload } from '../../utils/errorHandling';
10
+ import { fetchWithRetry } from '../../utils/retry';
10
11
  import { getPageIdAndTypeFromConfluencePageAri } from './ari';
11
12
  import { isBlogPageType } from './utils';
12
13
  var COMMON_HEADERS = {
@@ -38,7 +39,7 @@ var getConfluenceSourceInfo = /*#__PURE__*/function () {
38
39
  }
39
40
  };
40
41
  _context.next = 3;
41
- return fetch(GRAPHQL_ENDPOINT, {
42
+ return fetchWithRetry(GRAPHQL_ENDPOINT, {
42
43
  method: 'POST',
43
44
  headers: _objectSpread(_objectSpread({}, COMMON_HEADERS), AGG_HEADERS),
44
45
  body: JSON.stringify(bodyData)
@@ -2,6 +2,11 @@ export var SyncBlockError = /*#__PURE__*/function (SyncBlockError) {
2
2
  SyncBlockError["Errored"] = "errored";
3
3
  SyncBlockError["NotFound"] = "not_found";
4
4
  SyncBlockError["Forbidden"] = "forbidden";
5
+ SyncBlockError["InvalidRequest"] = "invalid_request";
6
+ SyncBlockError["RateLimited"] = "rate_limited";
7
+ SyncBlockError["Conflict"] = "conflict";
8
+ // attempt to create block that already exists
9
+ SyncBlockError["ServerError"] = "server_error";
5
10
  SyncBlockError["InvalidContent"] = "invalid_content"; // content is not a valid JSON
6
11
  return SyncBlockError;
7
12
  }({});