@atlaskit/editor-synced-block-provider 6.6.7 → 6.6.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @atlaskit/editor-synced-block-provider
2
2
 
3
+ ## 6.6.9
4
+
5
+ ### Patch Changes
6
+
7
+ - [`085a281306c03`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/085a281306c03) -
8
+ Add defensive mechanisms for synced block EntityNotFound errors:
9
+ - Add retry with exponential backoff when fetching synced block references returns EntityNotFound
10
+ (up to 3 retries with 2s/4s/8s delays)
11
+ - Add transformPasted handler to convert any bodiedSyncBlock nodes arriving via paste into
12
+ syncBlock references, preventing createBlock from being called with the wrong parentId
13
+
14
+ Both changes are gated behind `platform_synced_block_patch_13`.
15
+
16
+ ## 6.6.8
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies
21
+
3
22
  ## 6.6.7
4
23
 
5
24
  ### Patch Changes
@@ -29,6 +29,8 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
29
29
  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; }
30
30
  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) { (0, _defineProperty2.default)(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; }
31
31
  var CACHE_KEY_PREFIX = 'sync-block-data-';
32
+ var ENTITY_NOT_FOUND_MAX_RETRIES = 3;
33
+ var ENTITY_NOT_FOUND_INITIAL_DELAY_MS = 2000;
32
34
 
33
35
  // A store manager responsible for the lifecycle and state management of reference sync blocks in an editor instance.
34
36
  // Designed to manage local in-memory state and synchronize with an external data provider.
@@ -36,8 +38,6 @@ var CACHE_KEY_PREFIX = 'sync-block-data-';
36
38
  // Handles fetching source URL and title for sync blocks.
37
39
  // Can be used in both editor and renderer contexts.
38
40
  var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
39
- // Track the setTimeout handle for queued flush so we can cancel it on destroy
40
-
41
41
  function ReferenceSyncBlockStoreManager(dataProvider, viewMode) {
42
42
  var _this = this,
43
43
  _this$dataProvider;
@@ -52,6 +52,9 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
52
52
  (0, _defineProperty2.default)(this, "isFlushInProgress", false);
53
53
  // Track if another flush is needed after the current one completes
54
54
  (0, _defineProperty2.default)(this, "flushNeededAfterCurrent", false);
55
+ // Track retry attempts for EntityNotFound errors (block may be in the process of being created)
56
+ (0, _defineProperty2.default)(this, "entityNotFoundRetryCount", new Map());
57
+ (0, _defineProperty2.default)(this, "entityNotFoundRetryTimers", new Map());
55
58
  this.dataProvider = dataProvider;
56
59
  this.viewMode = viewMode;
57
60
  this.syncBlockFetchDataRequests = new Map();
@@ -211,6 +214,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
211
214
  var syncBlockNode = (0, _utils.createSyncBlockNode)('', resourceId);
212
215
  var providerData = (_this$dataProvider2 = this.dataProvider) === null || _this$dataProvider2 === void 0 || (_this$dataProvider2 = _this$dataProvider2.getNodeDataFromCache(syncBlockNode)) === null || _this$dataProvider2 === void 0 ? void 0 : _this$dataProvider2.data;
213
216
  if (providerData) {
217
+ // Initial provider cache data can come from SSR/prefetch and bypass updateCache(),
218
+ // so strip annotations here before references render existing synced block payloads.
214
219
  return this.stripAnnotationMarksFromReferenceData(providerData);
215
220
  }
216
221
  return this.getFromSessionCache(resourceId);
@@ -248,6 +253,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
248
253
  if (!raw) {
249
254
  return undefined;
250
255
  }
256
+ // Session cache entries written before this sanitizer existed may still include
257
+ // source annotation marks, so keep this read-time safety net for legacy data.
251
258
  return this.stripAnnotationMarksFromReferenceData(JSON.parse(raw));
252
259
  } catch (error) {
253
260
  (0, _monitoring.logException)(error, {
@@ -577,11 +584,37 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
577
584
  // Remove from newly added set even if not unpublished (to clean up)
578
585
  _this5.newlyAddedSyncBlocks.delete(syncBlockInstance.resourceId);
579
586
  }
587
+
588
+ // Clear retry tracking on successful fetch — block has been created
589
+ if (!syncBlockInstance.error && _this5.entityNotFoundRetryCount.has(syncBlockInstance.resourceId)) {
590
+ var timer = _this5.entityNotFoundRetryTimers.get(syncBlockInstance.resourceId);
591
+ if (timer) {
592
+ clearTimeout(timer);
593
+ _this5.entityNotFoundRetryTimers.delete(syncBlockInstance.resourceId);
594
+ }
595
+ _this5.entityNotFoundRetryCount.delete(syncBlockInstance.resourceId);
596
+ }
580
597
  if (syncBlockInstance.error) {
581
- var _this5$fireAnalyticsE2, _syncBlockInstance$da, _syncBlockInstance$da2;
582
- (_this5$fireAnalyticsE2 = _this5.fireAnalyticsEvent) === null || _this5$fireAnalyticsE2 === void 0 || _this5$fireAnalyticsE2.call(_this5, (0, _errorHandling.fetchErrorPayload)(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : (0, _utils.getSourceProductFromResourceIdSafe)(syncBlockInstance.resourceId)));
598
+ var _this5$entityNotFound;
599
+ // Skip error analytics when EntityNotFound will be retried, to avoid
600
+ // inflating error-rate metrics with expected transient failures
601
+ var isRetryingEntityNotFound = syncBlockInstance.error.type === _types.SyncBlockError.EntityNotFound && ((_this5$entityNotFound = _this5.entityNotFoundRetryCount.get(syncBlockInstance.resourceId)) !== null && _this5$entityNotFound !== void 0 ? _this5$entityNotFound : 0) < ENTITY_NOT_FOUND_MAX_RETRIES && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_13');
602
+ if (!isRetryingEntityNotFound) {
603
+ var _this5$fireAnalyticsE2, _syncBlockInstance$da, _syncBlockInstance$da2;
604
+ (_this5$fireAnalyticsE2 = _this5.fireAnalyticsEvent) === null || _this5$fireAnalyticsE2 === void 0 || _this5$fireAnalyticsE2.call(_this5, (0, _errorHandling.fetchErrorPayload)(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : (0, _utils.getSourceProductFromResourceIdSafe)(syncBlockInstance.resourceId)));
605
+ }
583
606
  if (syncBlockInstance.error.type === _types.SyncBlockError.NotFound || syncBlockInstance.error.type === _types.SyncBlockError.Forbidden) {
584
607
  hasExpectedError = true;
608
+ } else if (syncBlockInstance.error.type === _types.SyncBlockError.EntityNotFound) {
609
+ // Schedule a retry for EntityNotFound — the source block may be in
610
+ // the process of being created by a collaborator (race condition
611
+ // between NCS propagation and Block Service createBlock call).
612
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_13')) {
613
+ _this5.scheduleEntityNotFoundRetry(syncBlockInstance.resourceId);
614
+ }
615
+ if (!isRetryingEntityNotFound) {
616
+ hasUnexpectedError = true;
617
+ }
585
618
  } else if (syncBlockInstance.error) {
586
619
  hasUnexpectedError = true;
587
620
  }
@@ -645,6 +678,58 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
645
678
  (_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 || _this$dataProvider5.removeFromCache([resourceId]);
646
679
  this._providerFactoryManager.deleteFactory(resourceId);
647
680
  }
681
+
682
+ /**
683
+ * Schedules a delayed retry for a block that returned EntityNotFound.
684
+ * The block may be in the process of being created by a collaborator —
685
+ * the NCS transaction propagates the bodiedSyncBlock ADF node before
686
+ * the Block Service createBlock call completes.
687
+ */
688
+ }, {
689
+ key: "scheduleEntityNotFoundRetry",
690
+ value: function scheduleEntityNotFoundRetry(resourceId) {
691
+ var _this$entityNotFoundR,
692
+ _this6 = this;
693
+ var currentRetries = (_this$entityNotFoundR = this.entityNotFoundRetryCount.get(resourceId)) !== null && _this$entityNotFoundR !== void 0 ? _this$entityNotFoundR : 0;
694
+ if (currentRetries >= ENTITY_NOT_FOUND_MAX_RETRIES) {
695
+ // Max retries exceeded — keep count at max so future calls immediately exit
696
+ // (don't delete — that would reset the counter and allow unbounded retry waves)
697
+ return;
698
+ }
699
+
700
+ // If a timer is already pending, don't schedule another one — let the
701
+ // existing timer fire. This prevents rapid EntityNotFound responses from
702
+ // exhausting the retry budget through cancellations without any actual
703
+ // fetch completing.
704
+ if (this.entityNotFoundRetryTimers.has(resourceId)) {
705
+ return;
706
+ }
707
+ var delay = ENTITY_NOT_FOUND_INITIAL_DELAY_MS * Math.pow(2, currentRetries);
708
+ var timer = setTimeout(function () {
709
+ var _cached$error;
710
+ _this6.entityNotFoundRetryTimers.delete(resourceId);
711
+
712
+ // If no active subscriptions remain for this block, clean up and skip
713
+ var subscriptions = _this6._subscriptionManager.getSubscriptions().get(resourceId);
714
+ if (!subscriptions || Object.keys(subscriptions).length === 0) {
715
+ _this6.entityNotFoundRetryCount.delete(resourceId);
716
+ return;
717
+ }
718
+
719
+ // Increment count only when the timer fires, not when scheduled
720
+ _this6.entityNotFoundRetryCount.set(resourceId, currentRetries + 1);
721
+
722
+ // Clear the error from cache so fetchSyncBlocksData doesn't skip it
723
+ var cached = _this6.getFromCache(resourceId);
724
+ if ((cached === null || cached === void 0 || (_cached$error = cached.error) === null || _cached$error === void 0 ? void 0 : _cached$error.type) === _types.SyncBlockError.EntityNotFound) {
725
+ _this6.deleteFromCache(resourceId);
726
+ }
727
+
728
+ // Trigger a re-fetch via the batch fetcher
729
+ _this6.debouncedBatchedFetchSyncBlocks(resourceId);
730
+ }, delay);
731
+ this.entityNotFoundRetryTimers.set(resourceId, timer);
732
+ }
648
733
  }, {
649
734
  key: "debouncedBatchedFetchSyncBlocks",
650
735
  value: function debouncedBatchedFetchSyncBlocks(resourceId) {
@@ -653,12 +738,12 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
653
738
  }, {
654
739
  key: "setSSRDataInSessionCache",
655
740
  value: function setSSRDataInSessionCache(resourceIds) {
656
- var _this6 = this;
741
+ var _this7 = this;
657
742
  if (!resourceIds || resourceIds.length === 0) {
658
743
  return;
659
744
  }
660
745
  resourceIds.forEach(function (resourceId) {
661
- _this6.updateSessionCache(resourceId);
746
+ _this7.updateSessionCache(resourceId);
662
747
  });
663
748
  }
664
749
  }, {
@@ -731,7 +816,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
731
816
  key: "flush",
732
817
  value: (function () {
733
818
  var _flush = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3() {
734
- var _this7 = this;
819
+ var _this8 = this;
735
820
  var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$fireAnalyticsEv6, _this$saveExperience3, _this$fireAnalyticsEv7, _this$saveExperience4;
736
821
  return _regenerator.default.wrap(function _callee3$(_context4) {
737
822
  while (1) switch (_context4.prev = _context4.next) {
@@ -873,8 +958,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
873
958
  // Use setTimeout to avoid deep recursion and run queued flush asynchronously
874
959
  // Note: flush() handles all exceptions internally and never rejects
875
960
  this.queuedFlushTimeout = setTimeout(function () {
876
- _this7.queuedFlushTimeout = undefined;
877
- void _this7.flush();
961
+ _this8.queuedFlushTimeout = undefined;
962
+ void _this8.flush();
878
963
  }, 0);
879
964
  }
880
965
  return _context4.finish(49);
@@ -900,6 +985,13 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
900
985
  clearTimeout(this.queuedFlushTimeout);
901
986
  this.queuedFlushTimeout = undefined;
902
987
  }
988
+
989
+ // Cancel any pending EntityNotFound retry timers
990
+ this.entityNotFoundRetryTimers.forEach(function (timer) {
991
+ return clearTimeout(timer);
992
+ });
993
+ this.entityNotFoundRetryTimers.clear();
994
+ this.entityNotFoundRetryCount.clear();
903
995
  this._subscriptionManager.destroy();
904
996
  this._providerFactoryManager.destroy();
905
997
  this._batchFetcher.destroy();
@@ -12,6 +12,8 @@ import { syncBlockInMemorySessionCache } from './syncBlockInMemorySessionCache';
12
12
  import { SyncBlockProviderFactoryManager } from './syncBlockProviderFactoryManager';
13
13
  import { SyncBlockSubscriptionManager } from './syncBlockSubscriptionManager';
14
14
  const CACHE_KEY_PREFIX = 'sync-block-data-';
15
+ const ENTITY_NOT_FOUND_MAX_RETRIES = 3;
16
+ const ENTITY_NOT_FOUND_INITIAL_DELAY_MS = 2000;
15
17
 
16
18
  // A store manager responsible for the lifecycle and state management of reference sync blocks in an editor instance.
17
19
  // Designed to manage local in-memory state and synchronize with an external data provider.
@@ -19,8 +21,6 @@ const CACHE_KEY_PREFIX = 'sync-block-data-';
19
21
  // Handles fetching source URL and title for sync blocks.
20
22
  // Can be used in both editor and renderer contexts.
21
23
  export class ReferenceSyncBlockStoreManager {
22
- // Track the setTimeout handle for queued flush so we can cancel it on destroy
23
-
24
24
  constructor(dataProvider, viewMode) {
25
25
  var _this$dataProvider;
26
26
  // Keeps track of addition and deletion of reference synced blocks on the document
@@ -33,6 +33,9 @@ export class ReferenceSyncBlockStoreManager {
33
33
  _defineProperty(this, "isFlushInProgress", false);
34
34
  // Track if another flush is needed after the current one completes
35
35
  _defineProperty(this, "flushNeededAfterCurrent", false);
36
+ // Track retry attempts for EntityNotFound errors (block may be in the process of being created)
37
+ _defineProperty(this, "entityNotFoundRetryCount", new Map());
38
+ _defineProperty(this, "entityNotFoundRetryTimers", new Map());
36
39
  this.dataProvider = dataProvider;
37
40
  this.viewMode = viewMode;
38
41
  this.syncBlockFetchDataRequests = new Map();
@@ -142,6 +145,8 @@ export class ReferenceSyncBlockStoreManager {
142
145
  const syncBlockNode = createSyncBlockNode('', resourceId);
143
146
  const providerData = (_this$dataProvider2 = this.dataProvider) === null || _this$dataProvider2 === void 0 ? void 0 : (_this$dataProvider2$g = _this$dataProvider2.getNodeDataFromCache(syncBlockNode)) === null || _this$dataProvider2$g === void 0 ? void 0 : _this$dataProvider2$g.data;
144
147
  if (providerData) {
148
+ // Initial provider cache data can come from SSR/prefetch and bypass updateCache(),
149
+ // so strip annotations here before references render existing synced block payloads.
145
150
  return this.stripAnnotationMarksFromReferenceData(providerData);
146
151
  }
147
152
  return this.getFromSessionCache(resourceId);
@@ -175,6 +180,8 @@ export class ReferenceSyncBlockStoreManager {
175
180
  if (!raw) {
176
181
  return undefined;
177
182
  }
183
+ // Session cache entries written before this sanitizer existed may still include
184
+ // source annotation marks, so keep this read-time safety net for legacy data.
178
185
  return this.stripAnnotationMarksFromReferenceData(JSON.parse(raw));
179
186
  } catch (error) {
180
187
  logException(error, {
@@ -449,11 +456,37 @@ export class ReferenceSyncBlockStoreManager {
449
456
  // Remove from newly added set even if not unpublished (to clean up)
450
457
  this.newlyAddedSyncBlocks.delete(syncBlockInstance.resourceId);
451
458
  }
459
+
460
+ // Clear retry tracking on successful fetch — block has been created
461
+ if (!syncBlockInstance.error && this.entityNotFoundRetryCount.has(syncBlockInstance.resourceId)) {
462
+ const timer = this.entityNotFoundRetryTimers.get(syncBlockInstance.resourceId);
463
+ if (timer) {
464
+ clearTimeout(timer);
465
+ this.entityNotFoundRetryTimers.delete(syncBlockInstance.resourceId);
466
+ }
467
+ this.entityNotFoundRetryCount.delete(syncBlockInstance.resourceId);
468
+ }
452
469
  if (syncBlockInstance.error) {
453
- var _this$fireAnalyticsEv9, _syncBlockInstance$da, _syncBlockInstance$da2;
454
- (_this$fireAnalyticsEv9 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv9 === void 0 ? void 0 : _this$fireAnalyticsEv9.call(this, fetchErrorPayload(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
470
+ var _this$entityNotFoundR;
471
+ // Skip error analytics when EntityNotFound will be retried, to avoid
472
+ // inflating error-rate metrics with expected transient failures
473
+ const isRetryingEntityNotFound = syncBlockInstance.error.type === SyncBlockError.EntityNotFound && ((_this$entityNotFoundR = this.entityNotFoundRetryCount.get(syncBlockInstance.resourceId)) !== null && _this$entityNotFoundR !== void 0 ? _this$entityNotFoundR : 0) < ENTITY_NOT_FOUND_MAX_RETRIES && fg('platform_synced_block_patch_13');
474
+ if (!isRetryingEntityNotFound) {
475
+ var _this$fireAnalyticsEv9, _syncBlockInstance$da, _syncBlockInstance$da2;
476
+ (_this$fireAnalyticsEv9 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv9 === void 0 ? void 0 : _this$fireAnalyticsEv9.call(this, fetchErrorPayload(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
477
+ }
455
478
  if (syncBlockInstance.error.type === SyncBlockError.NotFound || syncBlockInstance.error.type === SyncBlockError.Forbidden) {
456
479
  hasExpectedError = true;
480
+ } else if (syncBlockInstance.error.type === SyncBlockError.EntityNotFound) {
481
+ // Schedule a retry for EntityNotFound — the source block may be in
482
+ // the process of being created by a collaborator (race condition
483
+ // between NCS propagation and Block Service createBlock call).
484
+ if (fg('platform_synced_block_patch_13')) {
485
+ this.scheduleEntityNotFoundRetry(syncBlockInstance.resourceId);
486
+ }
487
+ if (!isRetryingEntityNotFound) {
488
+ hasUnexpectedError = true;
489
+ }
457
490
  } else if (syncBlockInstance.error) {
458
491
  hasUnexpectedError = true;
459
492
  }
@@ -514,6 +547,55 @@ export class ReferenceSyncBlockStoreManager {
514
547
  (_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 ? void 0 : _this$dataProvider5.removeFromCache([resourceId]);
515
548
  this._providerFactoryManager.deleteFactory(resourceId);
516
549
  }
550
+
551
+ /**
552
+ * Schedules a delayed retry for a block that returned EntityNotFound.
553
+ * The block may be in the process of being created by a collaborator —
554
+ * the NCS transaction propagates the bodiedSyncBlock ADF node before
555
+ * the Block Service createBlock call completes.
556
+ */
557
+ scheduleEntityNotFoundRetry(resourceId) {
558
+ var _this$entityNotFoundR2;
559
+ const currentRetries = (_this$entityNotFoundR2 = this.entityNotFoundRetryCount.get(resourceId)) !== null && _this$entityNotFoundR2 !== void 0 ? _this$entityNotFoundR2 : 0;
560
+ if (currentRetries >= ENTITY_NOT_FOUND_MAX_RETRIES) {
561
+ // Max retries exceeded — keep count at max so future calls immediately exit
562
+ // (don't delete — that would reset the counter and allow unbounded retry waves)
563
+ return;
564
+ }
565
+
566
+ // If a timer is already pending, don't schedule another one — let the
567
+ // existing timer fire. This prevents rapid EntityNotFound responses from
568
+ // exhausting the retry budget through cancellations without any actual
569
+ // fetch completing.
570
+ if (this.entityNotFoundRetryTimers.has(resourceId)) {
571
+ return;
572
+ }
573
+ const delay = ENTITY_NOT_FOUND_INITIAL_DELAY_MS * Math.pow(2, currentRetries);
574
+ const timer = setTimeout(() => {
575
+ var _cached$error;
576
+ this.entityNotFoundRetryTimers.delete(resourceId);
577
+
578
+ // If no active subscriptions remain for this block, clean up and skip
579
+ const subscriptions = this._subscriptionManager.getSubscriptions().get(resourceId);
580
+ if (!subscriptions || Object.keys(subscriptions).length === 0) {
581
+ this.entityNotFoundRetryCount.delete(resourceId);
582
+ return;
583
+ }
584
+
585
+ // Increment count only when the timer fires, not when scheduled
586
+ this.entityNotFoundRetryCount.set(resourceId, currentRetries + 1);
587
+
588
+ // Clear the error from cache so fetchSyncBlocksData doesn't skip it
589
+ const cached = this.getFromCache(resourceId);
590
+ if ((cached === null || cached === void 0 ? void 0 : (_cached$error = cached.error) === null || _cached$error === void 0 ? void 0 : _cached$error.type) === SyncBlockError.EntityNotFound) {
591
+ this.deleteFromCache(resourceId);
592
+ }
593
+
594
+ // Trigger a re-fetch via the batch fetcher
595
+ this.debouncedBatchedFetchSyncBlocks(resourceId);
596
+ }, delay);
597
+ this.entityNotFoundRetryTimers.set(resourceId, timer);
598
+ }
517
599
  debouncedBatchedFetchSyncBlocks(resourceId) {
518
600
  this._batchFetcher.queueFetch(resourceId);
519
601
  }
@@ -691,6 +773,11 @@ export class ReferenceSyncBlockStoreManager {
691
773
  clearTimeout(this.queuedFlushTimeout);
692
774
  this.queuedFlushTimeout = undefined;
693
775
  }
776
+
777
+ // Cancel any pending EntityNotFound retry timers
778
+ this.entityNotFoundRetryTimers.forEach(timer => clearTimeout(timer));
779
+ this.entityNotFoundRetryTimers.clear();
780
+ this.entityNotFoundRetryCount.clear();
694
781
  this._subscriptionManager.destroy();
695
782
  this._providerFactoryManager.destroy();
696
783
  this._batchFetcher.destroy();
@@ -22,6 +22,8 @@ import { syncBlockInMemorySessionCache } from './syncBlockInMemorySessionCache';
22
22
  import { SyncBlockProviderFactoryManager } from './syncBlockProviderFactoryManager';
23
23
  import { SyncBlockSubscriptionManager } from './syncBlockSubscriptionManager';
24
24
  var CACHE_KEY_PREFIX = 'sync-block-data-';
25
+ var ENTITY_NOT_FOUND_MAX_RETRIES = 3;
26
+ var ENTITY_NOT_FOUND_INITIAL_DELAY_MS = 2000;
25
27
 
26
28
  // A store manager responsible for the lifecycle and state management of reference sync blocks in an editor instance.
27
29
  // Designed to manage local in-memory state and synchronize with an external data provider.
@@ -29,8 +31,6 @@ var CACHE_KEY_PREFIX = 'sync-block-data-';
29
31
  // Handles fetching source URL and title for sync blocks.
30
32
  // Can be used in both editor and renderer contexts.
31
33
  export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
32
- // Track the setTimeout handle for queued flush so we can cancel it on destroy
33
-
34
34
  function ReferenceSyncBlockStoreManager(dataProvider, viewMode) {
35
35
  var _this = this,
36
36
  _this$dataProvider;
@@ -45,6 +45,9 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
45
45
  _defineProperty(this, "isFlushInProgress", false);
46
46
  // Track if another flush is needed after the current one completes
47
47
  _defineProperty(this, "flushNeededAfterCurrent", false);
48
+ // Track retry attempts for EntityNotFound errors (block may be in the process of being created)
49
+ _defineProperty(this, "entityNotFoundRetryCount", new Map());
50
+ _defineProperty(this, "entityNotFoundRetryTimers", new Map());
48
51
  this.dataProvider = dataProvider;
49
52
  this.viewMode = viewMode;
50
53
  this.syncBlockFetchDataRequests = new Map();
@@ -204,6 +207,8 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
204
207
  var syncBlockNode = createSyncBlockNode('', resourceId);
205
208
  var providerData = (_this$dataProvider2 = this.dataProvider) === null || _this$dataProvider2 === void 0 || (_this$dataProvider2 = _this$dataProvider2.getNodeDataFromCache(syncBlockNode)) === null || _this$dataProvider2 === void 0 ? void 0 : _this$dataProvider2.data;
206
209
  if (providerData) {
210
+ // Initial provider cache data can come from SSR/prefetch and bypass updateCache(),
211
+ // so strip annotations here before references render existing synced block payloads.
207
212
  return this.stripAnnotationMarksFromReferenceData(providerData);
208
213
  }
209
214
  return this.getFromSessionCache(resourceId);
@@ -241,6 +246,8 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
241
246
  if (!raw) {
242
247
  return undefined;
243
248
  }
249
+ // Session cache entries written before this sanitizer existed may still include
250
+ // source annotation marks, so keep this read-time safety net for legacy data.
244
251
  return this.stripAnnotationMarksFromReferenceData(JSON.parse(raw));
245
252
  } catch (error) {
246
253
  logException(error, {
@@ -570,11 +577,37 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
570
577
  // Remove from newly added set even if not unpublished (to clean up)
571
578
  _this5.newlyAddedSyncBlocks.delete(syncBlockInstance.resourceId);
572
579
  }
580
+
581
+ // Clear retry tracking on successful fetch — block has been created
582
+ if (!syncBlockInstance.error && _this5.entityNotFoundRetryCount.has(syncBlockInstance.resourceId)) {
583
+ var timer = _this5.entityNotFoundRetryTimers.get(syncBlockInstance.resourceId);
584
+ if (timer) {
585
+ clearTimeout(timer);
586
+ _this5.entityNotFoundRetryTimers.delete(syncBlockInstance.resourceId);
587
+ }
588
+ _this5.entityNotFoundRetryCount.delete(syncBlockInstance.resourceId);
589
+ }
573
590
  if (syncBlockInstance.error) {
574
- var _this5$fireAnalyticsE2, _syncBlockInstance$da, _syncBlockInstance$da2;
575
- (_this5$fireAnalyticsE2 = _this5.fireAnalyticsEvent) === null || _this5$fireAnalyticsE2 === void 0 || _this5$fireAnalyticsE2.call(_this5, fetchErrorPayload(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
591
+ var _this5$entityNotFound;
592
+ // Skip error analytics when EntityNotFound will be retried, to avoid
593
+ // inflating error-rate metrics with expected transient failures
594
+ var isRetryingEntityNotFound = syncBlockInstance.error.type === SyncBlockError.EntityNotFound && ((_this5$entityNotFound = _this5.entityNotFoundRetryCount.get(syncBlockInstance.resourceId)) !== null && _this5$entityNotFound !== void 0 ? _this5$entityNotFound : 0) < ENTITY_NOT_FOUND_MAX_RETRIES && fg('platform_synced_block_patch_13');
595
+ if (!isRetryingEntityNotFound) {
596
+ var _this5$fireAnalyticsE2, _syncBlockInstance$da, _syncBlockInstance$da2;
597
+ (_this5$fireAnalyticsE2 = _this5.fireAnalyticsEvent) === null || _this5$fireAnalyticsE2 === void 0 || _this5$fireAnalyticsE2.call(_this5, fetchErrorPayload(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
598
+ }
576
599
  if (syncBlockInstance.error.type === SyncBlockError.NotFound || syncBlockInstance.error.type === SyncBlockError.Forbidden) {
577
600
  hasExpectedError = true;
601
+ } else if (syncBlockInstance.error.type === SyncBlockError.EntityNotFound) {
602
+ // Schedule a retry for EntityNotFound — the source block may be in
603
+ // the process of being created by a collaborator (race condition
604
+ // between NCS propagation and Block Service createBlock call).
605
+ if (fg('platform_synced_block_patch_13')) {
606
+ _this5.scheduleEntityNotFoundRetry(syncBlockInstance.resourceId);
607
+ }
608
+ if (!isRetryingEntityNotFound) {
609
+ hasUnexpectedError = true;
610
+ }
578
611
  } else if (syncBlockInstance.error) {
579
612
  hasUnexpectedError = true;
580
613
  }
@@ -638,6 +671,58 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
638
671
  (_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 || _this$dataProvider5.removeFromCache([resourceId]);
639
672
  this._providerFactoryManager.deleteFactory(resourceId);
640
673
  }
674
+
675
+ /**
676
+ * Schedules a delayed retry for a block that returned EntityNotFound.
677
+ * The block may be in the process of being created by a collaborator —
678
+ * the NCS transaction propagates the bodiedSyncBlock ADF node before
679
+ * the Block Service createBlock call completes.
680
+ */
681
+ }, {
682
+ key: "scheduleEntityNotFoundRetry",
683
+ value: function scheduleEntityNotFoundRetry(resourceId) {
684
+ var _this$entityNotFoundR,
685
+ _this6 = this;
686
+ var currentRetries = (_this$entityNotFoundR = this.entityNotFoundRetryCount.get(resourceId)) !== null && _this$entityNotFoundR !== void 0 ? _this$entityNotFoundR : 0;
687
+ if (currentRetries >= ENTITY_NOT_FOUND_MAX_RETRIES) {
688
+ // Max retries exceeded — keep count at max so future calls immediately exit
689
+ // (don't delete — that would reset the counter and allow unbounded retry waves)
690
+ return;
691
+ }
692
+
693
+ // If a timer is already pending, don't schedule another one — let the
694
+ // existing timer fire. This prevents rapid EntityNotFound responses from
695
+ // exhausting the retry budget through cancellations without any actual
696
+ // fetch completing.
697
+ if (this.entityNotFoundRetryTimers.has(resourceId)) {
698
+ return;
699
+ }
700
+ var delay = ENTITY_NOT_FOUND_INITIAL_DELAY_MS * Math.pow(2, currentRetries);
701
+ var timer = setTimeout(function () {
702
+ var _cached$error;
703
+ _this6.entityNotFoundRetryTimers.delete(resourceId);
704
+
705
+ // If no active subscriptions remain for this block, clean up and skip
706
+ var subscriptions = _this6._subscriptionManager.getSubscriptions().get(resourceId);
707
+ if (!subscriptions || Object.keys(subscriptions).length === 0) {
708
+ _this6.entityNotFoundRetryCount.delete(resourceId);
709
+ return;
710
+ }
711
+
712
+ // Increment count only when the timer fires, not when scheduled
713
+ _this6.entityNotFoundRetryCount.set(resourceId, currentRetries + 1);
714
+
715
+ // Clear the error from cache so fetchSyncBlocksData doesn't skip it
716
+ var cached = _this6.getFromCache(resourceId);
717
+ if ((cached === null || cached === void 0 || (_cached$error = cached.error) === null || _cached$error === void 0 ? void 0 : _cached$error.type) === SyncBlockError.EntityNotFound) {
718
+ _this6.deleteFromCache(resourceId);
719
+ }
720
+
721
+ // Trigger a re-fetch via the batch fetcher
722
+ _this6.debouncedBatchedFetchSyncBlocks(resourceId);
723
+ }, delay);
724
+ this.entityNotFoundRetryTimers.set(resourceId, timer);
725
+ }
641
726
  }, {
642
727
  key: "debouncedBatchedFetchSyncBlocks",
643
728
  value: function debouncedBatchedFetchSyncBlocks(resourceId) {
@@ -646,12 +731,12 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
646
731
  }, {
647
732
  key: "setSSRDataInSessionCache",
648
733
  value: function setSSRDataInSessionCache(resourceIds) {
649
- var _this6 = this;
734
+ var _this7 = this;
650
735
  if (!resourceIds || resourceIds.length === 0) {
651
736
  return;
652
737
  }
653
738
  resourceIds.forEach(function (resourceId) {
654
- _this6.updateSessionCache(resourceId);
739
+ _this7.updateSessionCache(resourceId);
655
740
  });
656
741
  }
657
742
  }, {
@@ -724,7 +809,7 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
724
809
  key: "flush",
725
810
  value: (function () {
726
811
  var _flush = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3() {
727
- var _this7 = this;
812
+ var _this8 = this;
728
813
  var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$fireAnalyticsEv6, _this$saveExperience3, _this$fireAnalyticsEv7, _this$saveExperience4;
729
814
  return _regeneratorRuntime.wrap(function _callee3$(_context4) {
730
815
  while (1) switch (_context4.prev = _context4.next) {
@@ -866,8 +951,8 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
866
951
  // Use setTimeout to avoid deep recursion and run queued flush asynchronously
867
952
  // Note: flush() handles all exceptions internally and never rejects
868
953
  this.queuedFlushTimeout = setTimeout(function () {
869
- _this7.queuedFlushTimeout = undefined;
870
- void _this7.flush();
954
+ _this8.queuedFlushTimeout = undefined;
955
+ void _this8.flush();
871
956
  }, 0);
872
957
  }
873
958
  return _context4.finish(49);
@@ -893,6 +978,13 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
893
978
  clearTimeout(this.queuedFlushTimeout);
894
979
  this.queuedFlushTimeout = undefined;
895
980
  }
981
+
982
+ // Cancel any pending EntityNotFound retry timers
983
+ this.entityNotFoundRetryTimers.forEach(function (timer) {
984
+ return clearTimeout(timer);
985
+ });
986
+ this.entityNotFoundRetryTimers.clear();
987
+ this.entityNotFoundRetryCount.clear();
896
988
  this._subscriptionManager.destroy();
897
989
  this._providerFactoryManager.destroy();
898
990
  this._batchFetcher.destroy();
@@ -18,6 +18,8 @@ export declare class ReferenceSyncBlockStoreManager {
18
18
  private isFlushInProgress;
19
19
  private flushNeededAfterCurrent;
20
20
  private queuedFlushTimeout?;
21
+ private entityNotFoundRetryCount;
22
+ private entityNotFoundRetryTimers;
21
23
  fetchExperience: Experience | undefined;
22
24
  private fetchSourceInfoExperience;
23
25
  private saveExperience;
@@ -88,6 +90,13 @@ export declare class ReferenceSyncBlockStoreManager {
88
90
  private updateCache;
89
91
  getFromCache(resourceId: ResourceId): SyncBlockInstance | undefined;
90
92
  private deleteFromCache;
93
+ /**
94
+ * Schedules a delayed retry for a block that returned EntityNotFound.
95
+ * The block may be in the process of being created by a collaborator —
96
+ * the NCS transaction propagates the bodiedSyncBlock ADF node before
97
+ * the Block Service createBlock call completes.
98
+ */
99
+ private scheduleEntityNotFoundRetry;
91
100
  private debouncedBatchedFetchSyncBlocks;
92
101
  private setSSRDataInSessionCache;
93
102
  subscribeToSyncBlock(resourceId: string, localId: string, callback: SubscriptionCallback): () => void;
@@ -18,6 +18,8 @@ export declare class ReferenceSyncBlockStoreManager {
18
18
  private isFlushInProgress;
19
19
  private flushNeededAfterCurrent;
20
20
  private queuedFlushTimeout?;
21
+ private entityNotFoundRetryCount;
22
+ private entityNotFoundRetryTimers;
21
23
  fetchExperience: Experience | undefined;
22
24
  private fetchSourceInfoExperience;
23
25
  private saveExperience;
@@ -88,6 +90,13 @@ export declare class ReferenceSyncBlockStoreManager {
88
90
  private updateCache;
89
91
  getFromCache(resourceId: ResourceId): SyncBlockInstance | undefined;
90
92
  private deleteFromCache;
93
+ /**
94
+ * Schedules a delayed retry for a block that returned EntityNotFound.
95
+ * The block may be in the process of being created by a collaborator —
96
+ * the NCS transaction propagates the bodiedSyncBlock ADF node before
97
+ * the Block Service createBlock call completes.
98
+ */
99
+ private scheduleEntityNotFoundRetry;
91
100
  private debouncedBatchedFetchSyncBlocks;
92
101
  private setSSRDataInSessionCache;
93
102
  subscribeToSyncBlock(resourceId: string, localId: string, callback: SubscriptionCallback): () => void;
package/package.json CHANGED
@@ -24,12 +24,12 @@
24
24
  ],
25
25
  "atlaskit:src": "src/index.ts",
26
26
  "dependencies": {
27
- "@atlaskit/adf-utils": "^19.30.0",
27
+ "@atlaskit/adf-utils": "^19.31.0",
28
28
  "@atlaskit/editor-json-transformer": "^8.32.0",
29
29
  "@atlaskit/editor-prosemirror": "^7.3.0",
30
30
  "@atlaskit/node-data-provider": "^11.1.0",
31
31
  "@atlaskit/platform-feature-flags": "^1.1.0",
32
- "@atlaskit/tmp-editor-statsig": "^82.1.0",
32
+ "@atlaskit/tmp-editor-statsig": "^83.0.0",
33
33
  "@babel/runtime": "^7.0.0",
34
34
  "@compiled/react": "^0.20.0",
35
35
  "graphql-ws": "^5.14.2",
@@ -38,11 +38,12 @@
38
38
  "uuid": "^3.1.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@atlaskit/editor-common": "^114.39.0",
41
+ "@atlaskit/editor-common": "^114.46.0",
42
42
  "react": "^18.2.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@testing-library/react": "^16.3.0",
46
+ "react": "^18.2.0",
46
47
  "react-dom": "^18.2.0"
47
48
  },
48
49
  "techstack": {
@@ -81,7 +82,7 @@
81
82
  }
82
83
  },
83
84
  "name": "@atlaskit/editor-synced-block-provider",
84
- "version": "6.6.7",
85
+ "version": "6.6.9",
85
86
  "description": "Synced Block Provider for @atlaskit/editor-plugin-synced-block",
86
87
  "author": "Atlassian Pty Ltd",
87
88
  "license": "Apache-2.0",
@@ -91,6 +92,9 @@
91
92
  "platform-feature-flags": {
92
93
  "platform_synced_block_patch_12": {
93
94
  "type": "boolean"
95
+ },
96
+ "platform_synced_block_patch_13": {
97
+ "type": "boolean"
94
98
  }
95
99
  }
96
100
  }