@atlaskit/editor-synced-block-provider 6.6.12 → 6.7.0
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 +16 -0
- package/dist/cjs/store-manager/referenceSyncBlockStoreManager.js +207 -25
- package/dist/cjs/store-manager/syncBlockBatchFetcher.js +35 -0
- package/dist/cjs/store-manager/syncBlockStoreManager.js +19 -0
- package/dist/cjs/store-manager/syncBlockSubscriptionManager.js +33 -15
- package/dist/cjs/utils/errorHandling.js +44 -1
- package/dist/es2019/store-manager/referenceSyncBlockStoreManager.js +180 -15
- package/dist/es2019/store-manager/syncBlockBatchFetcher.js +29 -0
- package/dist/es2019/store-manager/syncBlockStoreManager.js +21 -1
- package/dist/es2019/store-manager/syncBlockSubscriptionManager.js +34 -15
- package/dist/es2019/utils/errorHandling.js +46 -0
- package/dist/esm/store-manager/referenceSyncBlockStoreManager.js +208 -26
- package/dist/esm/store-manager/syncBlockBatchFetcher.js +35 -0
- package/dist/esm/store-manager/syncBlockStoreManager.js +21 -1
- package/dist/esm/store-manager/syncBlockSubscriptionManager.js +34 -15
- package/dist/esm/utils/errorHandling.js +43 -0
- package/dist/types/store-manager/referenceSyncBlockStoreManager.d.ts +25 -0
- package/dist/types/store-manager/syncBlockBatchFetcher.d.ts +8 -0
- package/dist/types/store-manager/syncBlockSubscriptionManager.d.ts +4 -0
- package/dist/types/utils/errorHandling.d.ts +15 -0
- package/dist/types-ts4.5/store-manager/referenceSyncBlockStoreManager.d.ts +25 -0
- package/dist/types-ts4.5/store-manager/syncBlockBatchFetcher.d.ts +8 -0
- package/dist/types-ts4.5/store-manager/syncBlockSubscriptionManager.d.ts +4 -0
- package/dist/types-ts4.5/utils/errorHandling.d.ts +15 -0
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @atlaskit/editor-synced-block-provider
|
|
2
2
|
|
|
3
|
+
## 6.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`608189fcbdca7`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/608189fcbdca7) -
|
|
8
|
+
Harden synced block cache deletion lifecycle: replace the legacy 1-second eager deletion with a
|
|
9
|
+
guard-checked 30-second grace period that protects against premature cache invalidation when
|
|
10
|
+
blocks unmount/remount during editor open, block moves, or other React subscribe/unsubscribe
|
|
11
|
+
churn. Fixes an intermittent issue where the 'Edit at source' button could become disabled and the
|
|
12
|
+
source link could disappear from the synced locations dropdown. Gated behind
|
|
13
|
+
platform_synced_block_patch_14.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
|
|
3
19
|
## 6.6.12
|
|
4
20
|
|
|
5
21
|
### 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
|
-
|
|
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
|
-
|
|
875
|
+
_this7.entityNotFoundRetryTimers.delete(resourceId);
|
|
711
876
|
|
|
712
877
|
// If no active subscriptions remain for this block, clean up and skip
|
|
713
|
-
var subscriptions =
|
|
878
|
+
var subscriptions = _this7._subscriptionManager.getSubscriptions().get(resourceId);
|
|
714
879
|
if (!subscriptions || Object.keys(subscriptions).length === 0) {
|
|
715
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
890
|
+
_this7.deleteFromCache(resourceId);
|
|
726
891
|
}
|
|
727
892
|
|
|
728
893
|
// Trigger a re-fetch via the batch fetcher
|
|
729
|
-
|
|
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
|
|
906
|
+
var _this8 = this;
|
|
742
907
|
if (!resourceIds || resourceIds.length === 0) {
|
|
743
908
|
return;
|
|
744
909
|
}
|
|
745
910
|
resourceIds.forEach(function (resourceId) {
|
|
746
|
-
|
|
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$
|
|
940
|
+
var _this$fireAnalyticsEv7;
|
|
776
941
|
(0, _monitoring.logException)(error, {
|
|
777
942
|
location: 'editor-synced-block-provider/referenceSyncBlockStoreManager'
|
|
778
943
|
});
|
|
779
|
-
(_this$
|
|
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
|
|
793
|
-
|
|
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
|
|
820
|
-
var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
962
|
-
void
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
};
|