@atlaskit/editor-synced-block-provider 6.6.8 → 6.6.10

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.10
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 6.6.9
10
+
11
+ ### Patch Changes
12
+
13
+ - [`085a281306c03`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/085a281306c03) -
14
+ Add defensive mechanisms for synced block EntityNotFound errors:
15
+ - Add retry with exponential backoff when fetching synced block references returns EntityNotFound
16
+ (up to 3 retries with 2s/4s/8s delays)
17
+ - Add transformPasted handler to convert any bodiedSyncBlock nodes arriving via paste into
18
+ syncBlock references, preventing createBlock from being called with the wrong parentId
19
+
20
+ Both changes are gated behind `platform_synced_block_patch_13`.
21
+
3
22
  ## 6.6.8
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();
@@ -581,11 +584,37 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
581
584
  // Remove from newly added set even if not unpublished (to clean up)
582
585
  _this5.newlyAddedSyncBlocks.delete(syncBlockInstance.resourceId);
583
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
+ }
584
597
  if (syncBlockInstance.error) {
585
- var _this5$fireAnalyticsE2, _syncBlockInstance$da, _syncBlockInstance$da2;
586
- (_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
+ }
587
606
  if (syncBlockInstance.error.type === _types.SyncBlockError.NotFound || syncBlockInstance.error.type === _types.SyncBlockError.Forbidden) {
588
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
+ }
589
618
  } else if (syncBlockInstance.error) {
590
619
  hasUnexpectedError = true;
591
620
  }
@@ -649,6 +678,58 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
649
678
  (_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 || _this$dataProvider5.removeFromCache([resourceId]);
650
679
  this._providerFactoryManager.deleteFactory(resourceId);
651
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
+ }
652
733
  }, {
653
734
  key: "debouncedBatchedFetchSyncBlocks",
654
735
  value: function debouncedBatchedFetchSyncBlocks(resourceId) {
@@ -657,12 +738,12 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
657
738
  }, {
658
739
  key: "setSSRDataInSessionCache",
659
740
  value: function setSSRDataInSessionCache(resourceIds) {
660
- var _this6 = this;
741
+ var _this7 = this;
661
742
  if (!resourceIds || resourceIds.length === 0) {
662
743
  return;
663
744
  }
664
745
  resourceIds.forEach(function (resourceId) {
665
- _this6.updateSessionCache(resourceId);
746
+ _this7.updateSessionCache(resourceId);
666
747
  });
667
748
  }
668
749
  }, {
@@ -735,7 +816,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
735
816
  key: "flush",
736
817
  value: (function () {
737
818
  var _flush = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3() {
738
- var _this7 = this;
819
+ var _this8 = this;
739
820
  var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$fireAnalyticsEv6, _this$saveExperience3, _this$fireAnalyticsEv7, _this$saveExperience4;
740
821
  return _regenerator.default.wrap(function _callee3$(_context4) {
741
822
  while (1) switch (_context4.prev = _context4.next) {
@@ -877,8 +958,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
877
958
  // Use setTimeout to avoid deep recursion and run queued flush asynchronously
878
959
  // Note: flush() handles all exceptions internally and never rejects
879
960
  this.queuedFlushTimeout = setTimeout(function () {
880
- _this7.queuedFlushTimeout = undefined;
881
- void _this7.flush();
961
+ _this8.queuedFlushTimeout = undefined;
962
+ void _this8.flush();
882
963
  }, 0);
883
964
  }
884
965
  return _context4.finish(49);
@@ -904,6 +985,13 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
904
985
  clearTimeout(this.queuedFlushTimeout);
905
986
  this.queuedFlushTimeout = undefined;
906
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();
907
995
  this._subscriptionManager.destroy();
908
996
  this._providerFactoryManager.destroy();
909
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();
@@ -453,11 +456,37 @@ export class ReferenceSyncBlockStoreManager {
453
456
  // Remove from newly added set even if not unpublished (to clean up)
454
457
  this.newlyAddedSyncBlocks.delete(syncBlockInstance.resourceId);
455
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
+ }
456
469
  if (syncBlockInstance.error) {
457
- var _this$fireAnalyticsEv9, _syncBlockInstance$da, _syncBlockInstance$da2;
458
- (_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
+ }
459
478
  if (syncBlockInstance.error.type === SyncBlockError.NotFound || syncBlockInstance.error.type === SyncBlockError.Forbidden) {
460
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
+ }
461
490
  } else if (syncBlockInstance.error) {
462
491
  hasUnexpectedError = true;
463
492
  }
@@ -518,6 +547,55 @@ export class ReferenceSyncBlockStoreManager {
518
547
  (_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 ? void 0 : _this$dataProvider5.removeFromCache([resourceId]);
519
548
  this._providerFactoryManager.deleteFactory(resourceId);
520
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
+ }
521
599
  debouncedBatchedFetchSyncBlocks(resourceId) {
522
600
  this._batchFetcher.queueFetch(resourceId);
523
601
  }
@@ -695,6 +773,11 @@ export class ReferenceSyncBlockStoreManager {
695
773
  clearTimeout(this.queuedFlushTimeout);
696
774
  this.queuedFlushTimeout = undefined;
697
775
  }
776
+
777
+ // Cancel any pending EntityNotFound retry timers
778
+ this.entityNotFoundRetryTimers.forEach(timer => clearTimeout(timer));
779
+ this.entityNotFoundRetryTimers.clear();
780
+ this.entityNotFoundRetryCount.clear();
698
781
  this._subscriptionManager.destroy();
699
782
  this._providerFactoryManager.destroy();
700
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();
@@ -574,11 +577,37 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
574
577
  // Remove from newly added set even if not unpublished (to clean up)
575
578
  _this5.newlyAddedSyncBlocks.delete(syncBlockInstance.resourceId);
576
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
+ }
577
590
  if (syncBlockInstance.error) {
578
- var _this5$fireAnalyticsE2, _syncBlockInstance$da, _syncBlockInstance$da2;
579
- (_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
+ }
580
599
  if (syncBlockInstance.error.type === SyncBlockError.NotFound || syncBlockInstance.error.type === SyncBlockError.Forbidden) {
581
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
+ }
582
611
  } else if (syncBlockInstance.error) {
583
612
  hasUnexpectedError = true;
584
613
  }
@@ -642,6 +671,58 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
642
671
  (_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 || _this$dataProvider5.removeFromCache([resourceId]);
643
672
  this._providerFactoryManager.deleteFactory(resourceId);
644
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
+ }
645
726
  }, {
646
727
  key: "debouncedBatchedFetchSyncBlocks",
647
728
  value: function debouncedBatchedFetchSyncBlocks(resourceId) {
@@ -650,12 +731,12 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
650
731
  }, {
651
732
  key: "setSSRDataInSessionCache",
652
733
  value: function setSSRDataInSessionCache(resourceIds) {
653
- var _this6 = this;
734
+ var _this7 = this;
654
735
  if (!resourceIds || resourceIds.length === 0) {
655
736
  return;
656
737
  }
657
738
  resourceIds.forEach(function (resourceId) {
658
- _this6.updateSessionCache(resourceId);
739
+ _this7.updateSessionCache(resourceId);
659
740
  });
660
741
  }
661
742
  }, {
@@ -728,7 +809,7 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
728
809
  key: "flush",
729
810
  value: (function () {
730
811
  var _flush = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3() {
731
- var _this7 = this;
812
+ var _this8 = this;
732
813
  var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$fireAnalyticsEv6, _this$saveExperience3, _this$fireAnalyticsEv7, _this$saveExperience4;
733
814
  return _regeneratorRuntime.wrap(function _callee3$(_context4) {
734
815
  while (1) switch (_context4.prev = _context4.next) {
@@ -870,8 +951,8 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
870
951
  // Use setTimeout to avoid deep recursion and run queued flush asynchronously
871
952
  // Note: flush() handles all exceptions internally and never rejects
872
953
  this.queuedFlushTimeout = setTimeout(function () {
873
- _this7.queuedFlushTimeout = undefined;
874
- void _this7.flush();
954
+ _this8.queuedFlushTimeout = undefined;
955
+ void _this8.flush();
875
956
  }, 0);
876
957
  }
877
958
  return _context4.finish(49);
@@ -897,6 +978,13 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
897
978
  clearTimeout(this.queuedFlushTimeout);
898
979
  this.queuedFlushTimeout = undefined;
899
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();
900
988
  this._subscriptionManager.destroy();
901
989
  this._providerFactoryManager.destroy();
902
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
@@ -29,7 +29,7 @@
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": "^83.0.0",
32
+ "@atlaskit/tmp-editor-statsig": "^84.0.0",
33
33
  "@babel/runtime": "^7.0.0",
34
34
  "@compiled/react": "^0.20.0",
35
35
  "graphql-ws": "^5.14.2",
@@ -38,7 +38,7 @@
38
38
  "uuid": "^3.1.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@atlaskit/editor-common": "^114.46.0",
41
+ "@atlaskit/editor-common": "^114.47.0",
42
42
  "react": "^18.2.0"
43
43
  },
44
44
  "devDependencies": {
@@ -82,7 +82,7 @@
82
82
  }
83
83
  },
84
84
  "name": "@atlaskit/editor-synced-block-provider",
85
- "version": "6.6.8",
85
+ "version": "6.6.10",
86
86
  "description": "Synced Block Provider for @atlaskit/editor-plugin-synced-block",
87
87
  "author": "Atlassian Pty Ltd",
88
88
  "license": "Apache-2.0",
@@ -92,6 +92,9 @@
92
92
  "platform-feature-flags": {
93
93
  "platform_synced_block_patch_12": {
94
94
  "type": "boolean"
95
+ },
96
+ "platform_synced_block_patch_13": {
97
+ "type": "boolean"
95
98
  }
96
99
  }
97
100
  }