@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.
- package/CHANGELOG.md +22 -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 +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
|
-
|
|
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
|
-
|
|
868
|
+
_this7.entityNotFoundRetryTimers.delete(resourceId);
|
|
704
869
|
|
|
705
870
|
// If no active subscriptions remain for this block, clean up and skip
|
|
706
|
-
var subscriptions =
|
|
871
|
+
var subscriptions = _this7._subscriptionManager.getSubscriptions().get(resourceId);
|
|
707
872
|
if (!subscriptions || Object.keys(subscriptions).length === 0) {
|
|
708
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
883
|
+
_this7.deleteFromCache(resourceId);
|
|
719
884
|
}
|
|
720
885
|
|
|
721
886
|
// Trigger a re-fetch via the batch fetcher
|
|
722
|
-
|
|
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
|
|
899
|
+
var _this8 = this;
|
|
735
900
|
if (!resourceIds || resourceIds.length === 0) {
|
|
736
901
|
return;
|
|
737
902
|
}
|
|
738
903
|
resourceIds.forEach(function (resourceId) {
|
|
739
|
-
|
|
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$
|
|
933
|
+
var _this$fireAnalyticsEv7;
|
|
769
934
|
logException(error, {
|
|
770
935
|
location: 'editor-synced-block-provider/referenceSyncBlockStoreManager'
|
|
771
936
|
});
|
|
772
|
-
(_this$
|
|
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
|
|
786
|
-
|
|
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
|
|
813
|
-
var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
955
|
-
void
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
};
|