@atlaskit/editor-synced-block-provider 6.6.12 → 6.7.1

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 (25) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/store-manager/referenceSyncBlockStoreManager.js +207 -25
  3. package/dist/cjs/store-manager/syncBlockBatchFetcher.js +35 -0
  4. package/dist/cjs/store-manager/syncBlockStoreManager.js +19 -0
  5. package/dist/cjs/store-manager/syncBlockSubscriptionManager.js +33 -15
  6. package/dist/cjs/utils/errorHandling.js +44 -1
  7. package/dist/es2019/store-manager/referenceSyncBlockStoreManager.js +180 -15
  8. package/dist/es2019/store-manager/syncBlockBatchFetcher.js +29 -0
  9. package/dist/es2019/store-manager/syncBlockStoreManager.js +21 -1
  10. package/dist/es2019/store-manager/syncBlockSubscriptionManager.js +34 -15
  11. package/dist/es2019/utils/errorHandling.js +46 -0
  12. package/dist/esm/store-manager/referenceSyncBlockStoreManager.js +208 -26
  13. package/dist/esm/store-manager/syncBlockBatchFetcher.js +35 -0
  14. package/dist/esm/store-manager/syncBlockStoreManager.js +21 -1
  15. package/dist/esm/store-manager/syncBlockSubscriptionManager.js +34 -15
  16. package/dist/esm/utils/errorHandling.js +43 -0
  17. package/dist/types/store-manager/referenceSyncBlockStoreManager.d.ts +25 -0
  18. package/dist/types/store-manager/syncBlockBatchFetcher.d.ts +8 -0
  19. package/dist/types/store-manager/syncBlockSubscriptionManager.d.ts +4 -0
  20. package/dist/types/utils/errorHandling.d.ts +15 -0
  21. package/dist/types-ts4.5/store-manager/referenceSyncBlockStoreManager.d.ts +25 -0
  22. package/dist/types-ts4.5/store-manager/syncBlockBatchFetcher.d.ts +8 -0
  23. package/dist/types-ts4.5/store-manager/syncBlockSubscriptionManager.d.ts +4 -0
  24. package/dist/types-ts4.5/utils/errorHandling.d.ts +15 -0
  25. package/package.json +6 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @atlaskit/editor-synced-block-provider
2
2
 
3
+ ## 6.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 6.7.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`608189fcbdca7`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/608189fcbdca7) -
14
+ Harden synced block cache deletion lifecycle: replace the legacy 1-second eager deletion with a
15
+ guard-checked 30-second grace period that protects against premature cache invalidation when
16
+ blocks unmount/remount during editor open, block moves, or other React subscribe/unsubscribe
17
+ churn. Fixes an intermittent issue where the 'Edit at source' button could become disabled and the
18
+ source link could disappear from the synced locations dropdown. Gated behind
19
+ platform_synced_block_patch_14.
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies
24
+
3
25
  ## 6.6.12
4
26
 
5
27
  ### Patch Changes
@@ -32,6 +32,13 @@ var CACHE_KEY_PREFIX = 'sync-block-data-';
32
32
  var ENTITY_NOT_FOUND_MAX_RETRIES = 3;
33
33
  var ENTITY_NOT_FOUND_INITIAL_DELAY_MS = 2000;
34
34
 
35
+ // Grace period before a cache entry is removed after the last subscriber
36
+ // unsubscribes (gated by `platform_synced_block_patch_14`). Guards are
37
+ // re-checked at fire time; if any are positive, the timer is rescheduled.
38
+ var CACHE_DELETION_GRACE_PERIOD_MS = 30000;
39
+ // Max reschedules before force-deleting with an analytics event (~5 min).
40
+ var CACHE_DELETION_MAX_RESCHEDULES = 10;
41
+
35
42
  // A store manager responsible for the lifecycle and state management of reference sync blocks in an editor instance.
36
43
  // Designed to manage local in-memory state and synchronize with an external data provider.
37
44
  // Supports fetch, cache, and subscription for sync block data.
@@ -55,6 +62,13 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
55
62
  // Track retry attempts for EntityNotFound errors (block may be in the process of being created)
56
63
  (0, _defineProperty2.default)(this, "entityNotFoundRetryCount", new Map());
57
64
  (0, _defineProperty2.default)(this, "entityNotFoundRetryTimers", new Map());
65
+ // Pending cache deletion timers keyed by resourceId (gated by
66
+ // `platform_synced_block_patch_14`). Cancelled when a subscriber re-attaches.
67
+ (0, _defineProperty2.default)(this, "pendingCacheDeletions", new Map());
68
+ // Reschedule counter per resource — reset on actual deletion or re-subscribe.
69
+ (0, _defineProperty2.default)(this, "cacheDeletionRescheduleCounts", new Map());
70
+ // Set by destroy() so in-flight timer callbacks can early-return.
71
+ (0, _defineProperty2.default)(this, "isDestroyed", false);
58
72
  this.dataProvider = dataProvider;
59
73
  this.viewMode = viewMode;
60
74
  this.syncBlockFetchDataRequests = new Map();
@@ -84,6 +98,14 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
84
98
  },
85
99
  markCacheDirty: function markCacheDirty() {
86
100
  _this.isCacheDirty = true;
101
+ },
102
+ // Delegate cache lifecycle to the store manager so guards can be
103
+ // checked atomically (gated by `platform_synced_block_patch_14`).
104
+ scheduleCacheDeletion: function scheduleCacheDeletion(rid) {
105
+ return _this.scheduleCacheDeletion(rid);
106
+ },
107
+ cancelPendingCacheDeletion: function cancelPendingCacheDeletion(rid) {
108
+ return _this.cancelPendingCacheDeletion(rid);
87
109
  }
88
110
  });
89
111
  this._providerFactoryManager = new _syncBlockProviderFactoryManager.SyncBlockProviderFactoryManager({
@@ -637,6 +659,19 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
637
659
  key: "updateCacheWithSourceInfo",
638
660
  value: function updateCacheWithSourceInfo(resourceId, sourceInfo) {
639
661
  var existingSyncBlock = this.getFromCache(resourceId);
662
+ // If the cache entry was deleted while the source-info request was
663
+ // in flight, fire an analytics event so the race is observable.
664
+ if (!existingSyncBlock && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
665
+ var _this$fireAnalyticsEv5;
666
+ (_this$fireAnalyticsEv5 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv5 === void 0 || _this$fireAnalyticsEv5.call(this, (0, _errorHandling.sourceInfoOrphanedPayload)(resourceId, (0, _utils.getSourceProductFromResourceIdSafe)(resourceId), {
667
+ hasPendingDeletion: this.pendingCacheDeletions.has(resourceId),
668
+ hasSubscribers: this._subscriptionManager.getSubscriptions().has(resourceId)
669
+ }));
670
+ (0, _monitoring.logException)(new Error('updateCacheWithSourceInfo: cache entry missing for resource'), {
671
+ location: 'editor-synced-block-provider/referenceSyncBlockStoreManager/orphaned-source-info'
672
+ });
673
+ return;
674
+ }
640
675
  if (existingSyncBlock && existingSyncBlock.data) {
641
676
  existingSyncBlock.data.sourceURL = sourceInfo === null || sourceInfo === void 0 ? void 0 : sourceInfo.url;
642
677
  existingSyncBlock.data = _objectSpread(_objectSpread({}, existingSyncBlock.data), {}, {
@@ -677,6 +712,136 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
677
712
  var _this$dataProvider5;
678
713
  (_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 || _this$dataProvider5.removeFromCache([resourceId]);
679
714
  this._providerFactoryManager.deleteFactory(resourceId);
715
+ // Evict in-flight source-info promise and reset reschedule counter
716
+ // so a stale resolution can't silently merge into a re-fetched entry.
717
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
718
+ this.syncBlockSourceInfoRequests.delete(resourceId);
719
+ this.cacheDeletionRescheduleCounts.delete(resourceId);
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Returns true if the cache entry for `resourceId` is safe to delete:
725
+ * no active subscribers, no in-flight source-info request, and no
726
+ * queued/in-flight batch fetch (gated by `platform_synced_block_patch_14`).
727
+ */
728
+ }, {
729
+ key: "canDeleteCache",
730
+ value: function canDeleteCache(resourceId) {
731
+ if (this._subscriptionManager.getSubscriptions().has(resourceId)) {
732
+ return false;
733
+ }
734
+ if (this.syncBlockSourceInfoRequests.has(resourceId)) {
735
+ return false;
736
+ }
737
+ if (this._batchFetcher.hasPendingFetch(resourceId)) {
738
+ return false;
739
+ }
740
+ return true;
741
+ }
742
+
743
+ /**
744
+ * Schedules cache deletion for `resourceId` after the grace period
745
+ * (gated by `platform_synced_block_patch_14`). Called when the last
746
+ * subscriber unsubscribes. Guards are re-checked at fire time; if any
747
+ * are positive the timer is rescheduled up to MAX_RESCHEDULES times.
748
+ */
749
+ }, {
750
+ key: "scheduleCacheDeletion",
751
+ value: function scheduleCacheDeletion(resourceId) {
752
+ var _this6 = this;
753
+ if (!(0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
754
+ return;
755
+ }
756
+ if (this.isDestroyed) {
757
+ return;
758
+ }
759
+ // Cancel any existing timer \u2014 restart the grace period \u2014 but DO NOT
760
+ // reset the reschedule counter. The counter is reset only by
761
+ // `cancelPendingCacheDeletion` (called when a real subscriber returns)
762
+ // or when the cache is actually deleted.
763
+ var existing = this.pendingCacheDeletions.get(resourceId);
764
+ if (existing) {
765
+ clearTimeout(existing);
766
+ this.pendingCacheDeletions.delete(resourceId);
767
+ }
768
+ var timer = setTimeout(function () {
769
+ // Guard against timer callback running after destroy. clearTimeout
770
+ // is synchronous so this should be unreachable in practice, but
771
+ // belt-and-braces.
772
+ if (_this6.isDestroyed) {
773
+ return;
774
+ }
775
+ _this6.pendingCacheDeletions.delete(resourceId);
776
+ _this6.onCacheDeletionTimerFire(resourceId);
777
+ }, CACHE_DELETION_GRACE_PERIOD_MS);
778
+ this.pendingCacheDeletions.set(resourceId, timer);
779
+ }
780
+
781
+ /**
782
+ * Cancels any pending cache deletion timer for `resourceId` and resets the
783
+ * reschedule counter (gated by `platform_synced_block_patch_14`). Called
784
+ * when a new subscriber arrives.
785
+ */
786
+ }, {
787
+ key: "cancelPendingCacheDeletion",
788
+ value: function cancelPendingCacheDeletion(resourceId) {
789
+ if (!(0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
790
+ return;
791
+ }
792
+ var existing = this.pendingCacheDeletions.get(resourceId);
793
+ if (existing) {
794
+ clearTimeout(existing);
795
+ this.pendingCacheDeletions.delete(resourceId);
796
+ }
797
+ // Subscribers returning resets the reschedule counter \u2014 the resource is
798
+ // active again.
799
+ this.cacheDeletionRescheduleCounts.delete(resourceId);
800
+ }
801
+
802
+ /** Returns whether a cache deletion timer is pending for `resourceId`. */
803
+ }, {
804
+ key: "hasPendingCacheDeletion",
805
+ value: function hasPendingCacheDeletion(resourceId) {
806
+ return this.pendingCacheDeletions.has(resourceId);
807
+ }
808
+ }, {
809
+ key: "onCacheDeletionTimerFire",
810
+ value: function onCacheDeletionTimerFire(resourceId) {
811
+ var _this$cacheDeletionRe;
812
+ if (this.canDeleteCache(resourceId)) {
813
+ // `deleteFromCache` resets the reschedule counter under the flag.
814
+ this.deleteFromCache(resourceId);
815
+ return;
816
+ }
817
+ var currentCount = (_this$cacheDeletionRe = this.cacheDeletionRescheduleCounts.get(resourceId)) !== null && _this$cacheDeletionRe !== void 0 ? _this$cacheDeletionRe : 0;
818
+ if (currentCount >= CACHE_DELETION_MAX_RESCHEDULES) {
819
+ var _this$fireAnalyticsEv6;
820
+ // Stuck guard — force deletion to prevent unbounded memory growth and
821
+ // fire analytics so the stuck state is visible in production telemetry.
822
+ //
823
+ // NOTE: If active React subscribers still exist at force-delete time
824
+ // (e.g. an in-flight batch fetch never settled), the cache entry is
825
+ // removed without notifying subscribers. Those components will
826
+ // continue to render with stale data until their next re-render
827
+ // triggers a new batch fetch — typically within ~1 frame. We accept
828
+ // this brief stale window in exchange for bounded memory growth.
829
+ (_this$fireAnalyticsEv6 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv6 === void 0 || _this$fireAnalyticsEv6.call(this, (0, _errorHandling.cacheDeletionForcedPayload)(currentCount, resourceId, (0, _utils.getSourceProductFromResourceIdSafe)(resourceId)));
830
+ (0, _monitoring.logException)(new Error("Cache deletion forced after ".concat(currentCount, " reschedules \u2014 stuck in-flight guard")), {
831
+ location: 'editor-synced-block-provider/referenceSyncBlockStoreManager/cache-deletion-forced'
832
+ });
833
+ // `deleteFromCache` resets the reschedule counter under the flag.
834
+ this.deleteFromCache(resourceId);
835
+ // If subscribers still exist, kick off a fresh fetch so they get
836
+ // fresh data on the next batch tick instead of holding stale data
837
+ // indefinitely.
838
+ if (this._subscriptionManager.getSubscriptions().has(resourceId)) {
839
+ this.debouncedBatchedFetchSyncBlocks(resourceId);
840
+ }
841
+ return;
842
+ }
843
+ this.cacheDeletionRescheduleCounts.set(resourceId, currentCount + 1);
844
+ this.scheduleCacheDeletion(resourceId);
680
845
  }
681
846
 
682
847
  /**
@@ -689,7 +854,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
689
854
  key: "scheduleEntityNotFoundRetry",
690
855
  value: function scheduleEntityNotFoundRetry(resourceId) {
691
856
  var _this$entityNotFoundR,
692
- _this6 = this;
857
+ _this7 = this;
693
858
  var currentRetries = (_this$entityNotFoundR = this.entityNotFoundRetryCount.get(resourceId)) !== null && _this$entityNotFoundR !== void 0 ? _this$entityNotFoundR : 0;
694
859
  if (currentRetries >= ENTITY_NOT_FOUND_MAX_RETRIES) {
695
860
  // Max retries exceeded — keep count at max so future calls immediately exit
@@ -707,26 +872,26 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
707
872
  var delay = ENTITY_NOT_FOUND_INITIAL_DELAY_MS * Math.pow(2, currentRetries);
708
873
  var timer = setTimeout(function () {
709
874
  var _cached$error;
710
- _this6.entityNotFoundRetryTimers.delete(resourceId);
875
+ _this7.entityNotFoundRetryTimers.delete(resourceId);
711
876
 
712
877
  // If no active subscriptions remain for this block, clean up and skip
713
- var subscriptions = _this6._subscriptionManager.getSubscriptions().get(resourceId);
878
+ var subscriptions = _this7._subscriptionManager.getSubscriptions().get(resourceId);
714
879
  if (!subscriptions || Object.keys(subscriptions).length === 0) {
715
- _this6.entityNotFoundRetryCount.delete(resourceId);
880
+ _this7.entityNotFoundRetryCount.delete(resourceId);
716
881
  return;
717
882
  }
718
883
 
719
884
  // Increment count only when the timer fires, not when scheduled
720
- _this6.entityNotFoundRetryCount.set(resourceId, currentRetries + 1);
885
+ _this7.entityNotFoundRetryCount.set(resourceId, currentRetries + 1);
721
886
 
722
887
  // Clear the error from cache so fetchSyncBlocksData doesn't skip it
723
- var cached = _this6.getFromCache(resourceId);
888
+ var cached = _this7.getFromCache(resourceId);
724
889
  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);
890
+ _this7.deleteFromCache(resourceId);
726
891
  }
727
892
 
728
893
  // Trigger a re-fetch via the batch fetcher
729
- _this6.debouncedBatchedFetchSyncBlocks(resourceId);
894
+ _this7.debouncedBatchedFetchSyncBlocks(resourceId);
730
895
  }, delay);
731
896
  this.entityNotFoundRetryTimers.set(resourceId, timer);
732
897
  }
@@ -738,12 +903,12 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
738
903
  }, {
739
904
  key: "setSSRDataInSessionCache",
740
905
  value: function setSSRDataInSessionCache(resourceIds) {
741
- var _this7 = this;
906
+ var _this8 = this;
742
907
  if (!resourceIds || resourceIds.length === 0) {
743
908
  return;
744
909
  }
745
910
  resourceIds.forEach(function (resourceId) {
746
- _this7.updateSessionCache(resourceId);
911
+ _this8.updateSessionCache(resourceId);
747
912
  });
748
913
  }
749
914
  }, {
@@ -772,11 +937,11 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
772
937
  }
773
938
  return this._subscriptionManager.subscribeToSyncBlock(resourceId, localId, callback);
774
939
  } catch (error) {
775
- var _this$fireAnalyticsEv5;
940
+ var _this$fireAnalyticsEv7;
776
941
  (0, _monitoring.logException)(error, {
777
942
  location: 'editor-synced-block-provider/referenceSyncBlockStoreManager'
778
943
  });
779
- (_this$fireAnalyticsEv5 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv5 === void 0 || _this$fireAnalyticsEv5.call(this, (0, _errorHandling.fetchErrorPayload)(error.message));
944
+ (_this$fireAnalyticsEv7 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv7 === void 0 || _this$fireAnalyticsEv7.call(this, (0, _errorHandling.fetchErrorPayload)(error.message));
780
945
  return function () {};
781
946
  }
782
947
  }
@@ -789,12 +954,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
789
954
  }, {
790
955
  key: "getSyncBlockURL",
791
956
  value: function getSyncBlockURL(resourceId) {
792
- var _syncBlock$data2;
793
- var syncBlock = this.getFromCache(resourceId);
794
- if (!syncBlock) {
795
- return undefined;
796
- }
797
- return (_syncBlock$data2 = syncBlock.data) === null || _syncBlock$data2 === void 0 ? void 0 : _syncBlock$data2.sourceURL;
957
+ var _this$getFromCache;
958
+ return (_this$getFromCache = this.getFromCache(resourceId)) === null || _this$getFromCache === void 0 || (_this$getFromCache = _this$getFromCache.data) === null || _this$getFromCache === void 0 ? void 0 : _this$getFromCache.sourceURL;
798
959
  }
799
960
  }, {
800
961
  key: "getProviderFactory",
@@ -816,8 +977,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
816
977
  key: "flush",
817
978
  value: (function () {
818
979
  var _flush = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3() {
819
- var _this8 = this;
820
- var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$fireAnalyticsEv6, _this$saveExperience3, _this$fireAnalyticsEv7, _this$saveExperience4, _t2, _t3;
980
+ var _this9 = this;
981
+ var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$fireAnalyticsEv8, _this$saveExperience3, _this$fireAnalyticsEv9, _this$saveExperience4, _t2, _t3;
821
982
  return _regenerator.default.wrap(function (_context4) {
822
983
  while (1) switch (_context4.prev = _context4.next) {
823
984
  case 0:
@@ -923,7 +1084,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
923
1084
  (_this$saveExperience2 = this.saveExperience) === null || _this$saveExperience2 === void 0 || _this$saveExperience2.failure({
924
1085
  reason: updateResult.error || 'Failed to update reference synced blocks on the document'
925
1086
  });
926
- (_this$fireAnalyticsEv6 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv6 === void 0 || _this$fireAnalyticsEv6.call(this, (0, _errorHandling.updateReferenceErrorPayload)(updateResult.error || 'Failed to update reference synced blocks on the document'));
1087
+ (_this$fireAnalyticsEv8 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv8 === void 0 || _this$fireAnalyticsEv8.call(this, (0, _errorHandling.updateReferenceErrorPayload)(updateResult.error || 'Failed to update reference synced blocks on the document'));
927
1088
  }
928
1089
  _context4.next = 17;
929
1090
  break;
@@ -938,7 +1099,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
938
1099
  reason: _t3.message
939
1100
  });
940
1101
  // No `resourceId` available in this catch — sourceProduct is intentionally omitted.
941
- (_this$fireAnalyticsEv7 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv7 === void 0 || _this$fireAnalyticsEv7.call(this, (0, _errorHandling.updateReferenceErrorPayload)(_t3.message));
1102
+ (_this$fireAnalyticsEv9 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv9 === void 0 || _this$fireAnalyticsEv9.call(this, (0, _errorHandling.updateReferenceErrorPayload)(_t3.message));
942
1103
  case 17:
943
1104
  _context4.prev = 17;
944
1105
  if (!success) {
@@ -958,8 +1119,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
958
1119
  // Use setTimeout to avoid deep recursion and run queued flush asynchronously
959
1120
  // Note: flush() handles all exceptions internally and never rejects
960
1121
  this.queuedFlushTimeout = setTimeout(function () {
961
- _this8.queuedFlushTimeout = undefined;
962
- void _this8.flush();
1122
+ _this9.queuedFlushTimeout = undefined;
1123
+ void _this9.flush();
963
1124
  }, 0);
964
1125
  }
965
1126
  return _context4.finish(17);
@@ -980,6 +1141,9 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
980
1141
  key: "destroy",
981
1142
  value: function destroy() {
982
1143
  var _this$dataProvider6, _this$saveExperience5, _this$fetchExperience0, _this$fetchSourceInfo2;
1144
+ // Mark destroyed first so in-flight timer callbacks can early-return.
1145
+ this.isDestroyed = true;
1146
+
983
1147
  // Cancel any queued flush to prevent it from running after destroy
984
1148
  if (this.queuedFlushTimeout) {
985
1149
  clearTimeout(this.queuedFlushTimeout);
@@ -992,6 +1156,13 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
992
1156
  });
993
1157
  this.entityNotFoundRetryTimers.clear();
994
1158
  this.entityNotFoundRetryCount.clear();
1159
+
1160
+ // Cancel pending cache deletion timers.
1161
+ this.pendingCacheDeletions.forEach(function (timer) {
1162
+ return clearTimeout(timer);
1163
+ });
1164
+ this.pendingCacheDeletions.clear();
1165
+ this.cacheDeletionRescheduleCounts.clear();
995
1166
  this._subscriptionManager.destroy();
996
1167
  this._providerFactoryManager.destroy();
997
1168
  this._batchFetcher.destroy();
@@ -1009,7 +1180,18 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1009
1180
  reason: 'editorDestroyed'
1010
1181
  });
1011
1182
  this.fireAnalyticsEvent = undefined;
1012
- _syncBlockInMemorySessionCache.syncBlockInMemorySessionCache.clear();
1183
+
1184
+ // Under `platform_synced_block_patch_14`, `destroy()` is now wired to
1185
+ // React component unmount via `useMemoizedSyncBlockStoreManager`.
1186
+ // Clearing the module-level singleton on unmount would wipe SSR session
1187
+ // cache data that a sibling/successor manager (e.g. the editor
1188
+ // instance that mounts immediately after the renderer unmounts during
1189
+ // the view-mode transition) is about to read.
1190
+ // Let entries age out naturally instead — the in-memory cache is
1191
+ // naturally bounded by `maxSize` (LRU) and cleared on hard navigation.
1192
+ if (!(0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
1193
+ _syncBlockInMemorySessionCache.syncBlockInMemorySessionCache.clear();
1194
+ }
1013
1195
  }
1014
1196
  }]);
1015
1197
  }();
@@ -22,6 +22,11 @@ var SyncBlockBatchFetcher = exports.SyncBlockBatchFetcher = /*#__PURE__*/functio
22
22
  var _this = this;
23
23
  (0, _classCallCheck2.default)(this, SyncBlockBatchFetcher);
24
24
  (0, _defineProperty2.default)(this, "pendingFetchRequests", new Set());
25
+ // Tracks resourceIds whose batched fetch is in flight (after RAF drains
26
+ // pendingFetchRequests, before the promise settles). Ensures
27
+ // `hasPendingFetch` remains true during the network window.
28
+ (0, _defineProperty2.default)(this, "inFlightFetches", new Set());
29
+ (0, _defineProperty2.default)(this, "isDestroyed", false);
25
30
  this.deps = deps;
26
31
  this.scheduledBatchFetch = (0, _rafSchd.default)(function () {
27
32
  if (_this.pendingFetchRequests.size === 0) {
@@ -34,6 +39,11 @@ var SyncBlockBatchFetcher = exports.SyncBlockBatchFetcher = /*#__PURE__*/functio
34
39
  return (0, _utils.createSyncBlockNode)(firstLocalId, resId);
35
40
  });
36
41
  _this.pendingFetchRequests.clear();
42
+
43
+ // Track in-flight before the fetch so guards remain positive.
44
+ resourceIds.forEach(function (resId) {
45
+ return _this.inFlightFetches.add(resId);
46
+ });
37
47
  _this.deps.fetchSyncBlocksData(syncBlockNodes).catch(function (error) {
38
48
  (0, _monitoring.logException)(error, {
39
49
  location: 'editor-synced-block-provider/syncBlockBatchFetcher/batchedFetchSyncBlocks'
@@ -42,6 +52,17 @@ var SyncBlockBatchFetcher = exports.SyncBlockBatchFetcher = /*#__PURE__*/functio
42
52
  var _this$deps$getFireAna;
43
53
  (_this$deps$getFireAna = _this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 || _this$deps$getFireAna((0, _errorHandling.fetchErrorPayload)(error.message, resId, (0, _utils.getSourceProductFromResourceIdSafe)(resId)));
44
54
  });
55
+ }).finally(function () {
56
+ // If the fetcher was destroyed while the request was in flight,
57
+ // skip cleanup — `destroy()` already cleared `inFlightFetches`
58
+ // and there's nothing observable to update.
59
+ if (_this.isDestroyed) {
60
+ return;
61
+ }
62
+ // Clear in-flight tracking once the fetch settles.
63
+ resourceIds.forEach(function (resId) {
64
+ return _this.inFlightFetches.delete(resId);
65
+ });
45
66
  });
46
67
  });
47
68
  }
@@ -56,6 +77,17 @@ var SyncBlockBatchFetcher = exports.SyncBlockBatchFetcher = /*#__PURE__*/functio
56
77
  this.pendingFetchRequests.delete(resourceId);
57
78
  }
58
79
  }
80
+
81
+ /**
82
+ * Returns true if a batched fetch is queued or in flight for `resourceId`.
83
+ * Used by cache deletion guards to prevent deleting while a fetch is
84
+ * about to populate the entry.
85
+ */
86
+ }, {
87
+ key: "hasPendingFetch",
88
+ value: function hasPendingFetch(resourceId) {
89
+ return this.pendingFetchRequests.has(resourceId) || this.inFlightFetches.has(resourceId);
90
+ }
59
91
  }, {
60
92
  key: "cancel",
61
93
  value: function cancel() {
@@ -69,8 +101,11 @@ var SyncBlockBatchFetcher = exports.SyncBlockBatchFetcher = /*#__PURE__*/functio
69
101
  }, {
70
102
  key: "destroy",
71
103
  value: function destroy() {
104
+ this.isDestroyed = true;
72
105
  this.cancel();
73
106
  this.clearPending();
107
+ // Clear in-flight tracking to prevent stale entries after teardown.
108
+ this.inFlightFetches.clear();
74
109
  }
75
110
  }]);
76
111
  }();
@@ -12,6 +12,7 @@ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/cl
12
12
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
13
13
  var _react = require("react");
14
14
  var _monitoring = require("@atlaskit/editor-common/monitoring");
15
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
15
16
  var _ari = require("../clients/block-service/ari");
16
17
  var _types = require("../common/types");
17
18
  var _errorHandling = require("../utils/errorHandling");
@@ -306,5 +307,23 @@ var useMemoizedSyncBlockStoreManager = exports.useMemoizedSyncBlockStoreManager
306
307
  prevFireAnalyticsEventRef.current = fireAnalyticsEvent;
307
308
  syncBlockStoreManager.setFireAnalyticsEvent(fireAnalyticsEvent);
308
309
  }
310
+
311
+ // Gated by platform_synced_block_patch_14:
312
+ // Destroy the SyncBlockStoreManager when:
313
+ // (a) the component unmounts — manager is fully cleaned up, or
314
+ // (b) dataProvider changes — the old manager (now orphaned by the
315
+ // useMemo recalculation) is destroyed before the new one takes over.
316
+ //
317
+ // Without this, orphaned managers leak timers, GQL subscriptions, and
318
+ // in-flight fetches indefinitely. The effect dep is `syncBlockStoreManager`
319
+ // (the useMemo result) — it changes identity precisely when dataProvider
320
+ // changes, triggering the cleanup for the old instance.
321
+ (0, _react.useEffect)(function () {
322
+ return function () {
323
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
324
+ syncBlockStoreManager.destroy();
325
+ }
326
+ };
327
+ }, [syncBlockStoreManager]);
309
328
  return syncBlockStoreManager;
310
329
  };
@@ -9,6 +9,7 @@ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/cl
9
9
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
10
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
11
  var _monitoring = require("@atlaskit/editor-common/monitoring");
12
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
12
13
  var _errorHandling = require("../utils/errorHandling");
13
14
  var _resolveSyncBlockInstance = require("../utils/resolveSyncBlockInstance");
14
15
  var _utils = require("../utils/utils");
@@ -119,8 +120,13 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
119
120
  // This handles the case where a block is moved - the old component unmounts
120
121
  // (scheduling deletion) but the new component mounts and subscribes before
121
122
  // the deletion timeout fires.
123
+ //
124
+ // Under the flag, cache deletion is owned by the store manager.
125
+ // With the flag off, the legacy 1s timer path is preserved.
122
126
  var pendingDeletion = this.pendingCacheDeletions.get(resourceId);
123
- if (pendingDeletion) {
127
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
128
+ this.deps.cancelPendingCacheDeletion(resourceId);
129
+ } else if (pendingDeletion) {
124
130
  clearTimeout(pendingDeletion);
125
131
  this.pendingCacheDeletions.delete(resourceId);
126
132
  }
@@ -154,7 +160,8 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
154
160
  // Unsubscription means a reference synced block is removed from the document
155
161
  _this3.deps.markCacheDirty();
156
162
  delete resourceSubscriptions[localId];
157
- if (Object.keys(resourceSubscriptions).length === 0) {
163
+ var remainingIds = Object.keys(resourceSubscriptions);
164
+ if (remainingIds.length === 0) {
158
165
  _this3.subscriptions.delete(resourceId);
159
166
 
160
167
  // Clean up GraphQL subscription when no more local subscribers
@@ -163,19 +170,30 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
163
170
  // Notify listeners that subscription was removed
164
171
  _this3.notifySubscriptionChangeListeners();
165
172
 
166
- // Delay cache deletion to handle block moves (unmount/remount).
167
- // When a block is moved, the old component unmounts before the new one mounts.
168
- // By delaying deletion, we give the new component time to subscribe and
169
- // cancel this pending deletion, preserving the cached data.
170
- // TODO: EDITOR-4152 - Rework this logic
171
- var deletionTimeout = setTimeout(function () {
172
- // Only delete if still no subscribers (wasn't re-subscribed)
173
- if (!_this3.subscriptions.has(resourceId)) {
174
- _this3.deps.deleteFromCache(resourceId);
175
- }
176
- _this3.pendingCacheDeletions.delete(resourceId);
177
- }, 1000);
178
- _this3.pendingCacheDeletions.set(resourceId, deletionTimeout);
173
+ // Under the flag, delegate cache deletion to the store manager
174
+ // which uses a 30s grace period with guard re-checks.
175
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
176
+ _this3.deps.scheduleCacheDeletion(resourceId);
177
+ } else {
178
+ // Legacy path (unchanged): delay cache deletion to handle
179
+ // block moves (unmount/remount). When a block is moved, the
180
+ // old component unmounts before the new one mounts. By
181
+ // delaying deletion, we give the new component time to
182
+ // subscribe and cancel this pending deletion, preserving
183
+ // the cached data.
184
+ // TODO: EDITOR-4152 - Rework this logic (superseded by
185
+ // `platform_synced_block_patch_14`).
186
+ var deletionTimeout = setTimeout(function () {
187
+ var hasSubscribers = _this3.subscriptions.has(resourceId);
188
+
189
+ // Only delete if still no subscribers (wasn't re-subscribed)
190
+ if (!hasSubscribers) {
191
+ _this3.deps.deleteFromCache(resourceId);
192
+ }
193
+ _this3.pendingCacheDeletions.delete(resourceId);
194
+ }, 1000);
195
+ _this3.pendingCacheDeletions.set(resourceId, deletionTimeout);
196
+ }
179
197
  } else {
180
198
  _this3.subscriptions.set(resourceId, resourceSubscriptions);
181
199
  }
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.updateSuccessPayload = exports.updateReferenceErrorPayload = exports.updateErrorPayload = exports.updateCacheErrorPayload = exports.stringifyError = exports.getSourceInfoErrorPayload = exports.getErrorPayload = exports.fetchSuccessPayload = exports.fetchReferencesErrorPayload = exports.fetchErrorPayload = exports.deleteSuccessPayload = exports.deleteErrorPayload = exports.createSuccessPayloadNew = exports.createSuccessPayload = exports.createErrorPayload = void 0;
7
+ exports.updateSuccessPayload = exports.updateReferenceErrorPayload = exports.updateErrorPayload = exports.updateCacheErrorPayload = exports.stringifyError = exports.sourceInfoOrphanedPayload = exports.getSourceInfoErrorPayload = exports.getErrorPayload = exports.fetchSuccessPayload = exports.fetchReferencesErrorPayload = exports.fetchErrorPayload = exports.deleteSuccessPayload = exports.deleteErrorPayload = exports.createSuccessPayloadNew = exports.createSuccessPayload = exports.createErrorPayload = exports.cacheDeletionForcedPayload = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _analytics = require("@atlaskit/editor-common/analytics");
10
10
  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; }
@@ -59,6 +59,49 @@ var deleteErrorPayload = exports.deleteErrorPayload = function deleteErrorPayloa
59
59
  var updateCacheErrorPayload = exports.updateCacheErrorPayload = function updateCacheErrorPayload(error, resourceId, sourceProduct) {
60
60
  return getErrorPayload(_analytics.ACTION_SUBJECT_ID.SYNCED_BLOCK_UPDATE_CACHE, error, resourceId, sourceProduct);
61
61
  };
62
+ /**
63
+ * Payload for `SYNCED_BLOCK_SOURCE_INFO_ORPHANED`. Fired when source-info
64
+ * resolves into a cache that has already been deleted — should be unreachable
65
+ * under `platform_synced_block_patch_14`.
66
+ */
67
+ var sourceInfoOrphanedPayload = exports.sourceInfoOrphanedPayload = function sourceInfoOrphanedPayload(resourceId, sourceProduct, context) {
68
+ return {
69
+ action: _analytics.ACTION.ERROR,
70
+ actionSubject: _analytics.ACTION_SUBJECT.SYNCED_BLOCK,
71
+ actionSubjectId: _analytics.ACTION_SUBJECT_ID.SYNCED_BLOCK_SOURCE_INFO_ORPHANED,
72
+ eventType: _analytics.EVENT_TYPE.OPERATIONAL,
73
+ attributes: _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, resourceId && {
74
+ resourceId: resourceId
75
+ }), sourceProduct && {
76
+ sourceProduct: sourceProduct
77
+ }), (context === null || context === void 0 ? void 0 : context.hasPendingDeletion) !== undefined && {
78
+ hasPendingDeletion: context.hasPendingDeletion
79
+ }), (context === null || context === void 0 ? void 0 : context.hasSubscribers) !== undefined && {
80
+ hasSubscribers: context.hasSubscribers
81
+ })
82
+ };
83
+ };
84
+
85
+ /**
86
+ * Payload for `SYNCED_BLOCK_CACHE_DELETION_FORCED`. Fired when the cache
87
+ * deletion timer has been rescheduled `MAX_RESCHEDULE_COUNT` times and we force
88
+ * the deletion to avoid leaking memory. Indicates a stuck in-flight flag.
89
+ */
90
+ var cacheDeletionForcedPayload = exports.cacheDeletionForcedPayload = function cacheDeletionForcedPayload(rescheduleCount, resourceId, sourceProduct) {
91
+ return {
92
+ action: _analytics.ACTION.ERROR,
93
+ actionSubject: _analytics.ACTION_SUBJECT.SYNCED_BLOCK,
94
+ actionSubjectId: _analytics.ACTION_SUBJECT_ID.SYNCED_BLOCK_CACHE_DELETION_FORCED,
95
+ eventType: _analytics.EVENT_TYPE.OPERATIONAL,
96
+ attributes: _objectSpread(_objectSpread({
97
+ rescheduleCount: rescheduleCount
98
+ }, resourceId && {
99
+ resourceId: resourceId
100
+ }), sourceProduct && {
101
+ sourceProduct: sourceProduct
102
+ })
103
+ };
104
+ };
62
105
  var fetchReferencesErrorPayload = exports.fetchReferencesErrorPayload = function fetchReferencesErrorPayload(error, resourceId, sourceProduct) {
63
106
  return getErrorPayload(_analytics.ACTION_SUBJECT_ID.SYNCED_BLOCK_FETCH_REFERENCES, error, resourceId, sourceProduct);
64
107
  };