@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
@@ -13,7 +13,7 @@ import isEqual from 'lodash/isEqual';
13
13
  import { logException } from '@atlaskit/editor-common/monitoring';
14
14
  import { fg } from '@atlaskit/platform-feature-flags';
15
15
  import { SyncBlockError } from '../common/types';
16
- import { fetchErrorPayload, fetchSuccessPayload, getSourceInfoErrorPayload, updateReferenceErrorPayload } from '../utils/errorHandling';
16
+ import { cacheDeletionForcedPayload, fetchErrorPayload, fetchSuccessPayload, getSourceInfoErrorPayload, sourceInfoOrphanedPayload, updateReferenceErrorPayload } from '../utils/errorHandling';
17
17
  import { getFetchExperience, getFetchSourceInfoExperience, getSaveReferenceExperience } from '../utils/experienceTracking';
18
18
  import { resolveSyncBlockInstance } from '../utils/resolveSyncBlockInstance';
19
19
  import { createSyncBlockNode, getSourceProductFromResourceIdSafe, stripAnnotationMarksFromJSONContent } from '../utils/utils';
@@ -25,6 +25,13 @@ var CACHE_KEY_PREFIX = 'sync-block-data-';
25
25
  var ENTITY_NOT_FOUND_MAX_RETRIES = 3;
26
26
  var ENTITY_NOT_FOUND_INITIAL_DELAY_MS = 2000;
27
27
 
28
+ // Grace period before a cache entry is removed after the last subscriber
29
+ // unsubscribes (gated by `platform_synced_block_patch_14`). Guards are
30
+ // re-checked at fire time; if any are positive, the timer is rescheduled.
31
+ var CACHE_DELETION_GRACE_PERIOD_MS = 30000;
32
+ // Max reschedules before force-deleting with an analytics event (~5 min).
33
+ var CACHE_DELETION_MAX_RESCHEDULES = 10;
34
+
28
35
  // A store manager responsible for the lifecycle and state management of reference sync blocks in an editor instance.
29
36
  // Designed to manage local in-memory state and synchronize with an external data provider.
30
37
  // Supports fetch, cache, and subscription for sync block data.
@@ -48,6 +55,13 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
48
55
  // Track retry attempts for EntityNotFound errors (block may be in the process of being created)
49
56
  _defineProperty(this, "entityNotFoundRetryCount", new Map());
50
57
  _defineProperty(this, "entityNotFoundRetryTimers", new Map());
58
+ // Pending cache deletion timers keyed by resourceId (gated by
59
+ // `platform_synced_block_patch_14`). Cancelled when a subscriber re-attaches.
60
+ _defineProperty(this, "pendingCacheDeletions", new Map());
61
+ // Reschedule counter per resource — reset on actual deletion or re-subscribe.
62
+ _defineProperty(this, "cacheDeletionRescheduleCounts", new Map());
63
+ // Set by destroy() so in-flight timer callbacks can early-return.
64
+ _defineProperty(this, "isDestroyed", false);
51
65
  this.dataProvider = dataProvider;
52
66
  this.viewMode = viewMode;
53
67
  this.syncBlockFetchDataRequests = new Map();
@@ -77,6 +91,14 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
77
91
  },
78
92
  markCacheDirty: function markCacheDirty() {
79
93
  _this.isCacheDirty = true;
94
+ },
95
+ // Delegate cache lifecycle to the store manager so guards can be
96
+ // checked atomically (gated by `platform_synced_block_patch_14`).
97
+ scheduleCacheDeletion: function scheduleCacheDeletion(rid) {
98
+ return _this.scheduleCacheDeletion(rid);
99
+ },
100
+ cancelPendingCacheDeletion: function cancelPendingCacheDeletion(rid) {
101
+ return _this.cancelPendingCacheDeletion(rid);
80
102
  }
81
103
  });
82
104
  this._providerFactoryManager = new SyncBlockProviderFactoryManager({
@@ -630,6 +652,19 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
630
652
  key: "updateCacheWithSourceInfo",
631
653
  value: function updateCacheWithSourceInfo(resourceId, sourceInfo) {
632
654
  var existingSyncBlock = this.getFromCache(resourceId);
655
+ // If the cache entry was deleted while the source-info request was
656
+ // in flight, fire an analytics event so the race is observable.
657
+ if (!existingSyncBlock && fg('platform_synced_block_patch_14')) {
658
+ var _this$fireAnalyticsEv5;
659
+ (_this$fireAnalyticsEv5 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv5 === void 0 || _this$fireAnalyticsEv5.call(this, sourceInfoOrphanedPayload(resourceId, getSourceProductFromResourceIdSafe(resourceId), {
660
+ hasPendingDeletion: this.pendingCacheDeletions.has(resourceId),
661
+ hasSubscribers: this._subscriptionManager.getSubscriptions().has(resourceId)
662
+ }));
663
+ logException(new Error('updateCacheWithSourceInfo: cache entry missing for resource'), {
664
+ location: 'editor-synced-block-provider/referenceSyncBlockStoreManager/orphaned-source-info'
665
+ });
666
+ return;
667
+ }
633
668
  if (existingSyncBlock && existingSyncBlock.data) {
634
669
  existingSyncBlock.data.sourceURL = sourceInfo === null || sourceInfo === void 0 ? void 0 : sourceInfo.url;
635
670
  existingSyncBlock.data = _objectSpread(_objectSpread({}, existingSyncBlock.data), {}, {
@@ -670,6 +705,136 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
670
705
  var _this$dataProvider5;
671
706
  (_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 || _this$dataProvider5.removeFromCache([resourceId]);
672
707
  this._providerFactoryManager.deleteFactory(resourceId);
708
+ // Evict in-flight source-info promise and reset reschedule counter
709
+ // so a stale resolution can't silently merge into a re-fetched entry.
710
+ if (fg('platform_synced_block_patch_14')) {
711
+ this.syncBlockSourceInfoRequests.delete(resourceId);
712
+ this.cacheDeletionRescheduleCounts.delete(resourceId);
713
+ }
714
+ }
715
+
716
+ /**
717
+ * Returns true if the cache entry for `resourceId` is safe to delete:
718
+ * no active subscribers, no in-flight source-info request, and no
719
+ * queued/in-flight batch fetch (gated by `platform_synced_block_patch_14`).
720
+ */
721
+ }, {
722
+ key: "canDeleteCache",
723
+ value: function canDeleteCache(resourceId) {
724
+ if (this._subscriptionManager.getSubscriptions().has(resourceId)) {
725
+ return false;
726
+ }
727
+ if (this.syncBlockSourceInfoRequests.has(resourceId)) {
728
+ return false;
729
+ }
730
+ if (this._batchFetcher.hasPendingFetch(resourceId)) {
731
+ return false;
732
+ }
733
+ return true;
734
+ }
735
+
736
+ /**
737
+ * Schedules cache deletion for `resourceId` after the grace period
738
+ * (gated by `platform_synced_block_patch_14`). Called when the last
739
+ * subscriber unsubscribes. Guards are re-checked at fire time; if any
740
+ * are positive the timer is rescheduled up to MAX_RESCHEDULES times.
741
+ */
742
+ }, {
743
+ key: "scheduleCacheDeletion",
744
+ value: function scheduleCacheDeletion(resourceId) {
745
+ var _this6 = this;
746
+ if (!fg('platform_synced_block_patch_14')) {
747
+ return;
748
+ }
749
+ if (this.isDestroyed) {
750
+ return;
751
+ }
752
+ // Cancel any existing timer \u2014 restart the grace period \u2014 but DO NOT
753
+ // reset the reschedule counter. The counter is reset only by
754
+ // `cancelPendingCacheDeletion` (called when a real subscriber returns)
755
+ // or when the cache is actually deleted.
756
+ var existing = this.pendingCacheDeletions.get(resourceId);
757
+ if (existing) {
758
+ clearTimeout(existing);
759
+ this.pendingCacheDeletions.delete(resourceId);
760
+ }
761
+ var timer = setTimeout(function () {
762
+ // Guard against timer callback running after destroy. clearTimeout
763
+ // is synchronous so this should be unreachable in practice, but
764
+ // belt-and-braces.
765
+ if (_this6.isDestroyed) {
766
+ return;
767
+ }
768
+ _this6.pendingCacheDeletions.delete(resourceId);
769
+ _this6.onCacheDeletionTimerFire(resourceId);
770
+ }, CACHE_DELETION_GRACE_PERIOD_MS);
771
+ this.pendingCacheDeletions.set(resourceId, timer);
772
+ }
773
+
774
+ /**
775
+ * Cancels any pending cache deletion timer for `resourceId` and resets the
776
+ * reschedule counter (gated by `platform_synced_block_patch_14`). Called
777
+ * when a new subscriber arrives.
778
+ */
779
+ }, {
780
+ key: "cancelPendingCacheDeletion",
781
+ value: function cancelPendingCacheDeletion(resourceId) {
782
+ if (!fg('platform_synced_block_patch_14')) {
783
+ return;
784
+ }
785
+ var existing = this.pendingCacheDeletions.get(resourceId);
786
+ if (existing) {
787
+ clearTimeout(existing);
788
+ this.pendingCacheDeletions.delete(resourceId);
789
+ }
790
+ // Subscribers returning resets the reschedule counter \u2014 the resource is
791
+ // active again.
792
+ this.cacheDeletionRescheduleCounts.delete(resourceId);
793
+ }
794
+
795
+ /** Returns whether a cache deletion timer is pending for `resourceId`. */
796
+ }, {
797
+ key: "hasPendingCacheDeletion",
798
+ value: function hasPendingCacheDeletion(resourceId) {
799
+ return this.pendingCacheDeletions.has(resourceId);
800
+ }
801
+ }, {
802
+ key: "onCacheDeletionTimerFire",
803
+ value: function onCacheDeletionTimerFire(resourceId) {
804
+ var _this$cacheDeletionRe;
805
+ if (this.canDeleteCache(resourceId)) {
806
+ // `deleteFromCache` resets the reschedule counter under the flag.
807
+ this.deleteFromCache(resourceId);
808
+ return;
809
+ }
810
+ var currentCount = (_this$cacheDeletionRe = this.cacheDeletionRescheduleCounts.get(resourceId)) !== null && _this$cacheDeletionRe !== void 0 ? _this$cacheDeletionRe : 0;
811
+ if (currentCount >= CACHE_DELETION_MAX_RESCHEDULES) {
812
+ var _this$fireAnalyticsEv6;
813
+ // Stuck guard — force deletion to prevent unbounded memory growth and
814
+ // fire analytics so the stuck state is visible in production telemetry.
815
+ //
816
+ // NOTE: If active React subscribers still exist at force-delete time
817
+ // (e.g. an in-flight batch fetch never settled), the cache entry is
818
+ // removed without notifying subscribers. Those components will
819
+ // continue to render with stale data until their next re-render
820
+ // triggers a new batch fetch — typically within ~1 frame. We accept
821
+ // this brief stale window in exchange for bounded memory growth.
822
+ (_this$fireAnalyticsEv6 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv6 === void 0 || _this$fireAnalyticsEv6.call(this, cacheDeletionForcedPayload(currentCount, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
823
+ logException(new Error("Cache deletion forced after ".concat(currentCount, " reschedules \u2014 stuck in-flight guard")), {
824
+ location: 'editor-synced-block-provider/referenceSyncBlockStoreManager/cache-deletion-forced'
825
+ });
826
+ // `deleteFromCache` resets the reschedule counter under the flag.
827
+ this.deleteFromCache(resourceId);
828
+ // If subscribers still exist, kick off a fresh fetch so they get
829
+ // fresh data on the next batch tick instead of holding stale data
830
+ // indefinitely.
831
+ if (this._subscriptionManager.getSubscriptions().has(resourceId)) {
832
+ this.debouncedBatchedFetchSyncBlocks(resourceId);
833
+ }
834
+ return;
835
+ }
836
+ this.cacheDeletionRescheduleCounts.set(resourceId, currentCount + 1);
837
+ this.scheduleCacheDeletion(resourceId);
673
838
  }
674
839
 
675
840
  /**
@@ -682,7 +847,7 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
682
847
  key: "scheduleEntityNotFoundRetry",
683
848
  value: function scheduleEntityNotFoundRetry(resourceId) {
684
849
  var _this$entityNotFoundR,
685
- _this6 = this;
850
+ _this7 = this;
686
851
  var currentRetries = (_this$entityNotFoundR = this.entityNotFoundRetryCount.get(resourceId)) !== null && _this$entityNotFoundR !== void 0 ? _this$entityNotFoundR : 0;
687
852
  if (currentRetries >= ENTITY_NOT_FOUND_MAX_RETRIES) {
688
853
  // Max retries exceeded — keep count at max so future calls immediately exit
@@ -700,26 +865,26 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
700
865
  var delay = ENTITY_NOT_FOUND_INITIAL_DELAY_MS * Math.pow(2, currentRetries);
701
866
  var timer = setTimeout(function () {
702
867
  var _cached$error;
703
- _this6.entityNotFoundRetryTimers.delete(resourceId);
868
+ _this7.entityNotFoundRetryTimers.delete(resourceId);
704
869
 
705
870
  // If no active subscriptions remain for this block, clean up and skip
706
- var subscriptions = _this6._subscriptionManager.getSubscriptions().get(resourceId);
871
+ var subscriptions = _this7._subscriptionManager.getSubscriptions().get(resourceId);
707
872
  if (!subscriptions || Object.keys(subscriptions).length === 0) {
708
- _this6.entityNotFoundRetryCount.delete(resourceId);
873
+ _this7.entityNotFoundRetryCount.delete(resourceId);
709
874
  return;
710
875
  }
711
876
 
712
877
  // Increment count only when the timer fires, not when scheduled
713
- _this6.entityNotFoundRetryCount.set(resourceId, currentRetries + 1);
878
+ _this7.entityNotFoundRetryCount.set(resourceId, currentRetries + 1);
714
879
 
715
880
  // Clear the error from cache so fetchSyncBlocksData doesn't skip it
716
- var cached = _this6.getFromCache(resourceId);
881
+ var cached = _this7.getFromCache(resourceId);
717
882
  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);
883
+ _this7.deleteFromCache(resourceId);
719
884
  }
720
885
 
721
886
  // Trigger a re-fetch via the batch fetcher
722
- _this6.debouncedBatchedFetchSyncBlocks(resourceId);
887
+ _this7.debouncedBatchedFetchSyncBlocks(resourceId);
723
888
  }, delay);
724
889
  this.entityNotFoundRetryTimers.set(resourceId, timer);
725
890
  }
@@ -731,12 +896,12 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
731
896
  }, {
732
897
  key: "setSSRDataInSessionCache",
733
898
  value: function setSSRDataInSessionCache(resourceIds) {
734
- var _this7 = this;
899
+ var _this8 = this;
735
900
  if (!resourceIds || resourceIds.length === 0) {
736
901
  return;
737
902
  }
738
903
  resourceIds.forEach(function (resourceId) {
739
- _this7.updateSessionCache(resourceId);
904
+ _this8.updateSessionCache(resourceId);
740
905
  });
741
906
  }
742
907
  }, {
@@ -765,11 +930,11 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
765
930
  }
766
931
  return this._subscriptionManager.subscribeToSyncBlock(resourceId, localId, callback);
767
932
  } catch (error) {
768
- var _this$fireAnalyticsEv5;
933
+ var _this$fireAnalyticsEv7;
769
934
  logException(error, {
770
935
  location: 'editor-synced-block-provider/referenceSyncBlockStoreManager'
771
936
  });
772
- (_this$fireAnalyticsEv5 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv5 === void 0 || _this$fireAnalyticsEv5.call(this, fetchErrorPayload(error.message));
937
+ (_this$fireAnalyticsEv7 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv7 === void 0 || _this$fireAnalyticsEv7.call(this, fetchErrorPayload(error.message));
773
938
  return function () {};
774
939
  }
775
940
  }
@@ -782,12 +947,8 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
782
947
  }, {
783
948
  key: "getSyncBlockURL",
784
949
  value: function getSyncBlockURL(resourceId) {
785
- var _syncBlock$data2;
786
- var syncBlock = this.getFromCache(resourceId);
787
- if (!syncBlock) {
788
- return undefined;
789
- }
790
- return (_syncBlock$data2 = syncBlock.data) === null || _syncBlock$data2 === void 0 ? void 0 : _syncBlock$data2.sourceURL;
950
+ var _this$getFromCache;
951
+ 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;
791
952
  }
792
953
  }, {
793
954
  key: "getProviderFactory",
@@ -809,8 +970,8 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
809
970
  key: "flush",
810
971
  value: (function () {
811
972
  var _flush = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3() {
812
- var _this8 = this;
813
- var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$fireAnalyticsEv6, _this$saveExperience3, _this$fireAnalyticsEv7, _this$saveExperience4, _t2, _t3;
973
+ var _this9 = this;
974
+ var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$fireAnalyticsEv8, _this$saveExperience3, _this$fireAnalyticsEv9, _this$saveExperience4, _t2, _t3;
814
975
  return _regeneratorRuntime.wrap(function (_context4) {
815
976
  while (1) switch (_context4.prev = _context4.next) {
816
977
  case 0:
@@ -916,7 +1077,7 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
916
1077
  (_this$saveExperience2 = this.saveExperience) === null || _this$saveExperience2 === void 0 || _this$saveExperience2.failure({
917
1078
  reason: updateResult.error || 'Failed to update reference synced blocks on the document'
918
1079
  });
919
- (_this$fireAnalyticsEv6 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv6 === void 0 || _this$fireAnalyticsEv6.call(this, updateReferenceErrorPayload(updateResult.error || 'Failed to update reference synced blocks on the document'));
1080
+ (_this$fireAnalyticsEv8 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv8 === void 0 || _this$fireAnalyticsEv8.call(this, updateReferenceErrorPayload(updateResult.error || 'Failed to update reference synced blocks on the document'));
920
1081
  }
921
1082
  _context4.next = 17;
922
1083
  break;
@@ -931,7 +1092,7 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
931
1092
  reason: _t3.message
932
1093
  });
933
1094
  // No `resourceId` available in this catch — sourceProduct is intentionally omitted.
934
- (_this$fireAnalyticsEv7 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv7 === void 0 || _this$fireAnalyticsEv7.call(this, updateReferenceErrorPayload(_t3.message));
1095
+ (_this$fireAnalyticsEv9 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv9 === void 0 || _this$fireAnalyticsEv9.call(this, updateReferenceErrorPayload(_t3.message));
935
1096
  case 17:
936
1097
  _context4.prev = 17;
937
1098
  if (!success) {
@@ -951,8 +1112,8 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
951
1112
  // Use setTimeout to avoid deep recursion and run queued flush asynchronously
952
1113
  // Note: flush() handles all exceptions internally and never rejects
953
1114
  this.queuedFlushTimeout = setTimeout(function () {
954
- _this8.queuedFlushTimeout = undefined;
955
- void _this8.flush();
1115
+ _this9.queuedFlushTimeout = undefined;
1116
+ void _this9.flush();
956
1117
  }, 0);
957
1118
  }
958
1119
  return _context4.finish(17);
@@ -973,6 +1134,9 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
973
1134
  key: "destroy",
974
1135
  value: function destroy() {
975
1136
  var _this$dataProvider6, _this$saveExperience5, _this$fetchExperience0, _this$fetchSourceInfo2;
1137
+ // Mark destroyed first so in-flight timer callbacks can early-return.
1138
+ this.isDestroyed = true;
1139
+
976
1140
  // Cancel any queued flush to prevent it from running after destroy
977
1141
  if (this.queuedFlushTimeout) {
978
1142
  clearTimeout(this.queuedFlushTimeout);
@@ -985,6 +1149,13 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
985
1149
  });
986
1150
  this.entityNotFoundRetryTimers.clear();
987
1151
  this.entityNotFoundRetryCount.clear();
1152
+
1153
+ // Cancel pending cache deletion timers.
1154
+ this.pendingCacheDeletions.forEach(function (timer) {
1155
+ return clearTimeout(timer);
1156
+ });
1157
+ this.pendingCacheDeletions.clear();
1158
+ this.cacheDeletionRescheduleCounts.clear();
988
1159
  this._subscriptionManager.destroy();
989
1160
  this._providerFactoryManager.destroy();
990
1161
  this._batchFetcher.destroy();
@@ -1002,7 +1173,18 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1002
1173
  reason: 'editorDestroyed'
1003
1174
  });
1004
1175
  this.fireAnalyticsEvent = undefined;
1005
- syncBlockInMemorySessionCache.clear();
1176
+
1177
+ // Under `platform_synced_block_patch_14`, `destroy()` is now wired to
1178
+ // React component unmount via `useMemoizedSyncBlockStoreManager`.
1179
+ // Clearing the module-level singleton on unmount would wipe SSR session
1180
+ // cache data that a sibling/successor manager (e.g. the editor
1181
+ // instance that mounts immediately after the renderer unmounts during
1182
+ // the view-mode transition) is about to read.
1183
+ // Let entries age out naturally instead — the in-memory cache is
1184
+ // naturally bounded by `maxSize` (LRU) and cleared on hard navigation.
1185
+ if (!fg('platform_synced_block_patch_14')) {
1186
+ syncBlockInMemorySessionCache.clear();
1187
+ }
1006
1188
  }
1007
1189
  }]);
1008
1190
  }();
@@ -15,6 +15,11 @@ export var SyncBlockBatchFetcher = /*#__PURE__*/function () {
15
15
  var _this = this;
16
16
  _classCallCheck(this, SyncBlockBatchFetcher);
17
17
  _defineProperty(this, "pendingFetchRequests", new Set());
18
+ // Tracks resourceIds whose batched fetch is in flight (after RAF drains
19
+ // pendingFetchRequests, before the promise settles). Ensures
20
+ // `hasPendingFetch` remains true during the network window.
21
+ _defineProperty(this, "inFlightFetches", new Set());
22
+ _defineProperty(this, "isDestroyed", false);
18
23
  this.deps = deps;
19
24
  this.scheduledBatchFetch = rafSchedule(function () {
20
25
  if (_this.pendingFetchRequests.size === 0) {
@@ -27,6 +32,11 @@ export var SyncBlockBatchFetcher = /*#__PURE__*/function () {
27
32
  return createSyncBlockNode(firstLocalId, resId);
28
33
  });
29
34
  _this.pendingFetchRequests.clear();
35
+
36
+ // Track in-flight before the fetch so guards remain positive.
37
+ resourceIds.forEach(function (resId) {
38
+ return _this.inFlightFetches.add(resId);
39
+ });
30
40
  _this.deps.fetchSyncBlocksData(syncBlockNodes).catch(function (error) {
31
41
  logException(error, {
32
42
  location: 'editor-synced-block-provider/syncBlockBatchFetcher/batchedFetchSyncBlocks'
@@ -35,6 +45,17 @@ export var SyncBlockBatchFetcher = /*#__PURE__*/function () {
35
45
  var _this$deps$getFireAna;
36
46
  (_this$deps$getFireAna = _this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 || _this$deps$getFireAna(fetchErrorPayload(error.message, resId, getSourceProductFromResourceIdSafe(resId)));
37
47
  });
48
+ }).finally(function () {
49
+ // If the fetcher was destroyed while the request was in flight,
50
+ // skip cleanup — `destroy()` already cleared `inFlightFetches`
51
+ // and there's nothing observable to update.
52
+ if (_this.isDestroyed) {
53
+ return;
54
+ }
55
+ // Clear in-flight tracking once the fetch settles.
56
+ resourceIds.forEach(function (resId) {
57
+ return _this.inFlightFetches.delete(resId);
58
+ });
38
59
  });
39
60
  });
40
61
  }
@@ -49,6 +70,17 @@ export var SyncBlockBatchFetcher = /*#__PURE__*/function () {
49
70
  this.pendingFetchRequests.delete(resourceId);
50
71
  }
51
72
  }
73
+
74
+ /**
75
+ * Returns true if a batched fetch is queued or in flight for `resourceId`.
76
+ * Used by cache deletion guards to prevent deleting while a fetch is
77
+ * about to populate the entry.
78
+ */
79
+ }, {
80
+ key: "hasPendingFetch",
81
+ value: function hasPendingFetch(resourceId) {
82
+ return this.pendingFetchRequests.has(resourceId) || this.inFlightFetches.has(resourceId);
83
+ }
52
84
  }, {
53
85
  key: "cancel",
54
86
  value: function cancel() {
@@ -62,8 +94,11 @@ export var SyncBlockBatchFetcher = /*#__PURE__*/function () {
62
94
  }, {
63
95
  key: "destroy",
64
96
  value: function destroy() {
97
+ this.isDestroyed = true;
65
98
  this.cancel();
66
99
  this.clearPending();
100
+ // Clear in-flight tracking to prevent stale entries after teardown.
101
+ this.inFlightFetches.clear();
67
102
  }
68
103
  }]);
69
104
  }();
@@ -5,8 +5,9 @@ import _createClass from "@babel/runtime/helpers/createClass";
5
5
  import _regeneratorRuntime from "@babel/runtime/regenerator";
6
6
  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; }
7
7
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
8
- import { useMemo, useRef } from 'react';
8
+ import { useEffect, useMemo, useRef } from 'react';
9
9
  import { logException } from '@atlaskit/editor-common/monitoring';
10
+ import { fg } from '@atlaskit/platform-feature-flags';
10
11
  import { getProductFromSourceAri } from '../clients/block-service/ari';
11
12
  import { SyncBlockError } from '../common/types';
12
13
  import { fetchReferencesErrorPayload } from '../utils/errorHandling';
@@ -20,6 +21,7 @@ import { SourceSyncBlockStoreManager } from './sourceSyncBlockStoreManager';
20
21
  // ReferenceSyncBlockStoreManager is responsible for the lifecycle and state management of reference sync blocks in an editor instance.
21
22
  // SourceSyncBlockStoreManager is responsible for the lifecycle and state management of source sync blocks in an editor instance.
22
23
  // Can be used in both editor and renderer contexts.
24
+
23
25
  export var SyncBlockStoreManager = /*#__PURE__*/function () {
24
26
  function SyncBlockStoreManager(dataProvider, viewMode, isLivePage) {
25
27
  _classCallCheck(this, SyncBlockStoreManager);
@@ -300,5 +302,23 @@ export var useMemoizedSyncBlockStoreManager = function useMemoizedSyncBlockStore
300
302
  prevFireAnalyticsEventRef.current = fireAnalyticsEvent;
301
303
  syncBlockStoreManager.setFireAnalyticsEvent(fireAnalyticsEvent);
302
304
  }
305
+
306
+ // Gated by platform_synced_block_patch_14:
307
+ // Destroy the SyncBlockStoreManager when:
308
+ // (a) the component unmounts — manager is fully cleaned up, or
309
+ // (b) dataProvider changes — the old manager (now orphaned by the
310
+ // useMemo recalculation) is destroyed before the new one takes over.
311
+ //
312
+ // Without this, orphaned managers leak timers, GQL subscriptions, and
313
+ // in-flight fetches indefinitely. The effect dep is `syncBlockStoreManager`
314
+ // (the useMemo result) — it changes identity precisely when dataProvider
315
+ // changes, triggering the cleanup for the old instance.
316
+ useEffect(function () {
317
+ return function () {
318
+ if (fg('platform_synced_block_patch_14')) {
319
+ syncBlockStoreManager.destroy();
320
+ }
321
+ };
322
+ }, [syncBlockStoreManager]);
303
323
  return syncBlockStoreManager;
304
324
  };
@@ -7,6 +7,7 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
7
7
  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; }
8
8
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
9
9
  import { logException } from '@atlaskit/editor-common/monitoring';
10
+ import { fg } from '@atlaskit/platform-feature-flags';
10
11
  import { fetchErrorPayload, fetchSuccessPayload } from '../utils/errorHandling';
11
12
  import { resolveSyncBlockInstance } from '../utils/resolveSyncBlockInstance';
12
13
  import { getSourceProductFromResourceIdSafe } from '../utils/utils';
@@ -16,6 +17,7 @@ import { getSourceProductFromResourceIdSafe } from '../utils/utils';
16
17
  * and provides a listener API so React components can react when the set
17
18
  * of subscribed resource IDs changes.
18
19
  */
20
+
19
21
  export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
20
22
  function SyncBlockSubscriptionManager(deps) {
21
23
  _classCallCheck(this, SyncBlockSubscriptionManager);
@@ -112,8 +114,13 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
112
114
  // This handles the case where a block is moved - the old component unmounts
113
115
  // (scheduling deletion) but the new component mounts and subscribes before
114
116
  // the deletion timeout fires.
117
+ //
118
+ // Under the flag, cache deletion is owned by the store manager.
119
+ // With the flag off, the legacy 1s timer path is preserved.
115
120
  var pendingDeletion = this.pendingCacheDeletions.get(resourceId);
116
- if (pendingDeletion) {
121
+ if (fg('platform_synced_block_patch_14')) {
122
+ this.deps.cancelPendingCacheDeletion(resourceId);
123
+ } else if (pendingDeletion) {
117
124
  clearTimeout(pendingDeletion);
118
125
  this.pendingCacheDeletions.delete(resourceId);
119
126
  }
@@ -147,7 +154,8 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
147
154
  // Unsubscription means a reference synced block is removed from the document
148
155
  _this3.deps.markCacheDirty();
149
156
  delete resourceSubscriptions[localId];
150
- if (Object.keys(resourceSubscriptions).length === 0) {
157
+ var remainingIds = Object.keys(resourceSubscriptions);
158
+ if (remainingIds.length === 0) {
151
159
  _this3.subscriptions.delete(resourceId);
152
160
 
153
161
  // Clean up GraphQL subscription when no more local subscribers
@@ -156,19 +164,30 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
156
164
  // Notify listeners that subscription was removed
157
165
  _this3.notifySubscriptionChangeListeners();
158
166
 
159
- // Delay cache deletion to handle block moves (unmount/remount).
160
- // When a block is moved, the old component unmounts before the new one mounts.
161
- // By delaying deletion, we give the new component time to subscribe and
162
- // cancel this pending deletion, preserving the cached data.
163
- // TODO: EDITOR-4152 - Rework this logic
164
- var deletionTimeout = setTimeout(function () {
165
- // Only delete if still no subscribers (wasn't re-subscribed)
166
- if (!_this3.subscriptions.has(resourceId)) {
167
- _this3.deps.deleteFromCache(resourceId);
168
- }
169
- _this3.pendingCacheDeletions.delete(resourceId);
170
- }, 1000);
171
- _this3.pendingCacheDeletions.set(resourceId, deletionTimeout);
167
+ // Under the flag, delegate cache deletion to the store manager
168
+ // which uses a 30s grace period with guard re-checks.
169
+ if (fg('platform_synced_block_patch_14')) {
170
+ _this3.deps.scheduleCacheDeletion(resourceId);
171
+ } else {
172
+ // Legacy path (unchanged): delay cache deletion to handle
173
+ // block moves (unmount/remount). When a block is moved, the
174
+ // old component unmounts before the new one mounts. By
175
+ // delaying deletion, we give the new component time to
176
+ // subscribe and cancel this pending deletion, preserving
177
+ // the cached data.
178
+ // TODO: EDITOR-4152 - Rework this logic (superseded by
179
+ // `platform_synced_block_patch_14`).
180
+ var deletionTimeout = setTimeout(function () {
181
+ var hasSubscribers = _this3.subscriptions.has(resourceId);
182
+
183
+ // Only delete if still no subscribers (wasn't re-subscribed)
184
+ if (!hasSubscribers) {
185
+ _this3.deps.deleteFromCache(resourceId);
186
+ }
187
+ _this3.pendingCacheDeletions.delete(resourceId);
188
+ }, 1000);
189
+ _this3.pendingCacheDeletions.set(resourceId, deletionTimeout);
190
+ }
172
191
  } else {
173
192
  _this3.subscriptions.set(resourceId, resourceSubscriptions);
174
193
  }
@@ -52,6 +52,49 @@ export var deleteErrorPayload = function deleteErrorPayload(error, resourceId, s
52
52
  export var updateCacheErrorPayload = function updateCacheErrorPayload(error, resourceId, sourceProduct) {
53
53
  return getErrorPayload(ACTION_SUBJECT_ID.SYNCED_BLOCK_UPDATE_CACHE, error, resourceId, sourceProduct);
54
54
  };
55
+ /**
56
+ * Payload for `SYNCED_BLOCK_SOURCE_INFO_ORPHANED`. Fired when source-info
57
+ * resolves into a cache that has already been deleted — should be unreachable
58
+ * under `platform_synced_block_patch_14`.
59
+ */
60
+ export var sourceInfoOrphanedPayload = function sourceInfoOrphanedPayload(resourceId, sourceProduct, context) {
61
+ return {
62
+ action: ACTION.ERROR,
63
+ actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
64
+ actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_SOURCE_INFO_ORPHANED,
65
+ eventType: EVENT_TYPE.OPERATIONAL,
66
+ attributes: _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, resourceId && {
67
+ resourceId: resourceId
68
+ }), sourceProduct && {
69
+ sourceProduct: sourceProduct
70
+ }), (context === null || context === void 0 ? void 0 : context.hasPendingDeletion) !== undefined && {
71
+ hasPendingDeletion: context.hasPendingDeletion
72
+ }), (context === null || context === void 0 ? void 0 : context.hasSubscribers) !== undefined && {
73
+ hasSubscribers: context.hasSubscribers
74
+ })
75
+ };
76
+ };
77
+
78
+ /**
79
+ * Payload for `SYNCED_BLOCK_CACHE_DELETION_FORCED`. Fired when the cache
80
+ * deletion timer has been rescheduled `MAX_RESCHEDULE_COUNT` times and we force
81
+ * the deletion to avoid leaking memory. Indicates a stuck in-flight flag.
82
+ */
83
+ export var cacheDeletionForcedPayload = function cacheDeletionForcedPayload(rescheduleCount, resourceId, sourceProduct) {
84
+ return {
85
+ action: ACTION.ERROR,
86
+ actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
87
+ actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_CACHE_DELETION_FORCED,
88
+ eventType: EVENT_TYPE.OPERATIONAL,
89
+ attributes: _objectSpread(_objectSpread({
90
+ rescheduleCount: rescheduleCount
91
+ }, resourceId && {
92
+ resourceId: resourceId
93
+ }), sourceProduct && {
94
+ sourceProduct: sourceProduct
95
+ })
96
+ };
97
+ };
55
98
  export var fetchReferencesErrorPayload = function fetchReferencesErrorPayload(error, resourceId, sourceProduct) {
56
99
  return getErrorPayload(ACTION_SUBJECT_ID.SYNCED_BLOCK_FETCH_REFERENCES, error, resourceId, sourceProduct);
57
100
  };