@atlaskit/editor-synced-block-provider 6.6.8 → 6.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/cjs/store-manager/referenceSyncBlockStoreManager.js +97 -9
- package/dist/es2019/store-manager/referenceSyncBlockStoreManager.js +87 -4
- package/dist/esm/store-manager/referenceSyncBlockStoreManager.js +97 -9
- package/dist/types/store-manager/referenceSyncBlockStoreManager.d.ts +9 -0
- package/dist/types-ts4.5/store-manager/referenceSyncBlockStoreManager.d.ts +9 -0
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @atlaskit/editor-synced-block-provider
|
|
2
2
|
|
|
3
|
+
## 6.6.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
|
|
9
|
+
## 6.6.9
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [`085a281306c03`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/085a281306c03) -
|
|
14
|
+
Add defensive mechanisms for synced block EntityNotFound errors:
|
|
15
|
+
- Add retry with exponential backoff when fetching synced block references returns EntityNotFound
|
|
16
|
+
(up to 3 retries with 2s/4s/8s delays)
|
|
17
|
+
- Add transformPasted handler to convert any bodiedSyncBlock nodes arriving via paste into
|
|
18
|
+
syncBlock references, preventing createBlock from being called with the wrong parentId
|
|
19
|
+
|
|
20
|
+
Both changes are gated behind `platform_synced_block_patch_13`.
|
|
21
|
+
|
|
3
22
|
## 6.6.8
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
|
@@ -29,6 +29,8 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
29
29
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
30
30
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
31
31
|
var CACHE_KEY_PREFIX = 'sync-block-data-';
|
|
32
|
+
var ENTITY_NOT_FOUND_MAX_RETRIES = 3;
|
|
33
|
+
var ENTITY_NOT_FOUND_INITIAL_DELAY_MS = 2000;
|
|
32
34
|
|
|
33
35
|
// A store manager responsible for the lifecycle and state management of reference sync blocks in an editor instance.
|
|
34
36
|
// Designed to manage local in-memory state and synchronize with an external data provider.
|
|
@@ -36,8 +38,6 @@ var CACHE_KEY_PREFIX = 'sync-block-data-';
|
|
|
36
38
|
// Handles fetching source URL and title for sync blocks.
|
|
37
39
|
// Can be used in both editor and renderer contexts.
|
|
38
40
|
var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
|
|
39
|
-
// Track the setTimeout handle for queued flush so we can cancel it on destroy
|
|
40
|
-
|
|
41
41
|
function ReferenceSyncBlockStoreManager(dataProvider, viewMode) {
|
|
42
42
|
var _this = this,
|
|
43
43
|
_this$dataProvider;
|
|
@@ -52,6 +52,9 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
|
|
|
52
52
|
(0, _defineProperty2.default)(this, "isFlushInProgress", false);
|
|
53
53
|
// Track if another flush is needed after the current one completes
|
|
54
54
|
(0, _defineProperty2.default)(this, "flushNeededAfterCurrent", false);
|
|
55
|
+
// Track retry attempts for EntityNotFound errors (block may be in the process of being created)
|
|
56
|
+
(0, _defineProperty2.default)(this, "entityNotFoundRetryCount", new Map());
|
|
57
|
+
(0, _defineProperty2.default)(this, "entityNotFoundRetryTimers", new Map());
|
|
55
58
|
this.dataProvider = dataProvider;
|
|
56
59
|
this.viewMode = viewMode;
|
|
57
60
|
this.syncBlockFetchDataRequests = new Map();
|
|
@@ -581,11 +584,37 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
|
|
|
581
584
|
// Remove from newly added set even if not unpublished (to clean up)
|
|
582
585
|
_this5.newlyAddedSyncBlocks.delete(syncBlockInstance.resourceId);
|
|
583
586
|
}
|
|
587
|
+
|
|
588
|
+
// Clear retry tracking on successful fetch — block has been created
|
|
589
|
+
if (!syncBlockInstance.error && _this5.entityNotFoundRetryCount.has(syncBlockInstance.resourceId)) {
|
|
590
|
+
var timer = _this5.entityNotFoundRetryTimers.get(syncBlockInstance.resourceId);
|
|
591
|
+
if (timer) {
|
|
592
|
+
clearTimeout(timer);
|
|
593
|
+
_this5.entityNotFoundRetryTimers.delete(syncBlockInstance.resourceId);
|
|
594
|
+
}
|
|
595
|
+
_this5.entityNotFoundRetryCount.delete(syncBlockInstance.resourceId);
|
|
596
|
+
}
|
|
584
597
|
if (syncBlockInstance.error) {
|
|
585
|
-
var _this5$
|
|
586
|
-
|
|
598
|
+
var _this5$entityNotFound;
|
|
599
|
+
// Skip error analytics when EntityNotFound will be retried, to avoid
|
|
600
|
+
// inflating error-rate metrics with expected transient failures
|
|
601
|
+
var isRetryingEntityNotFound = syncBlockInstance.error.type === _types.SyncBlockError.EntityNotFound && ((_this5$entityNotFound = _this5.entityNotFoundRetryCount.get(syncBlockInstance.resourceId)) !== null && _this5$entityNotFound !== void 0 ? _this5$entityNotFound : 0) < ENTITY_NOT_FOUND_MAX_RETRIES && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_13');
|
|
602
|
+
if (!isRetryingEntityNotFound) {
|
|
603
|
+
var _this5$fireAnalyticsE2, _syncBlockInstance$da, _syncBlockInstance$da2;
|
|
604
|
+
(_this5$fireAnalyticsE2 = _this5.fireAnalyticsEvent) === null || _this5$fireAnalyticsE2 === void 0 || _this5$fireAnalyticsE2.call(_this5, (0, _errorHandling.fetchErrorPayload)(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : (0, _utils.getSourceProductFromResourceIdSafe)(syncBlockInstance.resourceId)));
|
|
605
|
+
}
|
|
587
606
|
if (syncBlockInstance.error.type === _types.SyncBlockError.NotFound || syncBlockInstance.error.type === _types.SyncBlockError.Forbidden) {
|
|
588
607
|
hasExpectedError = true;
|
|
608
|
+
} else if (syncBlockInstance.error.type === _types.SyncBlockError.EntityNotFound) {
|
|
609
|
+
// Schedule a retry for EntityNotFound — the source block may be in
|
|
610
|
+
// the process of being created by a collaborator (race condition
|
|
611
|
+
// between NCS propagation and Block Service createBlock call).
|
|
612
|
+
if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_13')) {
|
|
613
|
+
_this5.scheduleEntityNotFoundRetry(syncBlockInstance.resourceId);
|
|
614
|
+
}
|
|
615
|
+
if (!isRetryingEntityNotFound) {
|
|
616
|
+
hasUnexpectedError = true;
|
|
617
|
+
}
|
|
589
618
|
} else if (syncBlockInstance.error) {
|
|
590
619
|
hasUnexpectedError = true;
|
|
591
620
|
}
|
|
@@ -649,6 +678,58 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
|
|
|
649
678
|
(_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 || _this$dataProvider5.removeFromCache([resourceId]);
|
|
650
679
|
this._providerFactoryManager.deleteFactory(resourceId);
|
|
651
680
|
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Schedules a delayed retry for a block that returned EntityNotFound.
|
|
684
|
+
* The block may be in the process of being created by a collaborator —
|
|
685
|
+
* the NCS transaction propagates the bodiedSyncBlock ADF node before
|
|
686
|
+
* the Block Service createBlock call completes.
|
|
687
|
+
*/
|
|
688
|
+
}, {
|
|
689
|
+
key: "scheduleEntityNotFoundRetry",
|
|
690
|
+
value: function scheduleEntityNotFoundRetry(resourceId) {
|
|
691
|
+
var _this$entityNotFoundR,
|
|
692
|
+
_this6 = this;
|
|
693
|
+
var currentRetries = (_this$entityNotFoundR = this.entityNotFoundRetryCount.get(resourceId)) !== null && _this$entityNotFoundR !== void 0 ? _this$entityNotFoundR : 0;
|
|
694
|
+
if (currentRetries >= ENTITY_NOT_FOUND_MAX_RETRIES) {
|
|
695
|
+
// Max retries exceeded — keep count at max so future calls immediately exit
|
|
696
|
+
// (don't delete — that would reset the counter and allow unbounded retry waves)
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// If a timer is already pending, don't schedule another one — let the
|
|
701
|
+
// existing timer fire. This prevents rapid EntityNotFound responses from
|
|
702
|
+
// exhausting the retry budget through cancellations without any actual
|
|
703
|
+
// fetch completing.
|
|
704
|
+
if (this.entityNotFoundRetryTimers.has(resourceId)) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
var delay = ENTITY_NOT_FOUND_INITIAL_DELAY_MS * Math.pow(2, currentRetries);
|
|
708
|
+
var timer = setTimeout(function () {
|
|
709
|
+
var _cached$error;
|
|
710
|
+
_this6.entityNotFoundRetryTimers.delete(resourceId);
|
|
711
|
+
|
|
712
|
+
// If no active subscriptions remain for this block, clean up and skip
|
|
713
|
+
var subscriptions = _this6._subscriptionManager.getSubscriptions().get(resourceId);
|
|
714
|
+
if (!subscriptions || Object.keys(subscriptions).length === 0) {
|
|
715
|
+
_this6.entityNotFoundRetryCount.delete(resourceId);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Increment count only when the timer fires, not when scheduled
|
|
720
|
+
_this6.entityNotFoundRetryCount.set(resourceId, currentRetries + 1);
|
|
721
|
+
|
|
722
|
+
// Clear the error from cache so fetchSyncBlocksData doesn't skip it
|
|
723
|
+
var cached = _this6.getFromCache(resourceId);
|
|
724
|
+
if ((cached === null || cached === void 0 || (_cached$error = cached.error) === null || _cached$error === void 0 ? void 0 : _cached$error.type) === _types.SyncBlockError.EntityNotFound) {
|
|
725
|
+
_this6.deleteFromCache(resourceId);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Trigger a re-fetch via the batch fetcher
|
|
729
|
+
_this6.debouncedBatchedFetchSyncBlocks(resourceId);
|
|
730
|
+
}, delay);
|
|
731
|
+
this.entityNotFoundRetryTimers.set(resourceId, timer);
|
|
732
|
+
}
|
|
652
733
|
}, {
|
|
653
734
|
key: "debouncedBatchedFetchSyncBlocks",
|
|
654
735
|
value: function debouncedBatchedFetchSyncBlocks(resourceId) {
|
|
@@ -657,12 +738,12 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
|
|
|
657
738
|
}, {
|
|
658
739
|
key: "setSSRDataInSessionCache",
|
|
659
740
|
value: function setSSRDataInSessionCache(resourceIds) {
|
|
660
|
-
var
|
|
741
|
+
var _this7 = this;
|
|
661
742
|
if (!resourceIds || resourceIds.length === 0) {
|
|
662
743
|
return;
|
|
663
744
|
}
|
|
664
745
|
resourceIds.forEach(function (resourceId) {
|
|
665
|
-
|
|
746
|
+
_this7.updateSessionCache(resourceId);
|
|
666
747
|
});
|
|
667
748
|
}
|
|
668
749
|
}, {
|
|
@@ -735,7 +816,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
|
|
|
735
816
|
key: "flush",
|
|
736
817
|
value: (function () {
|
|
737
818
|
var _flush = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3() {
|
|
738
|
-
var
|
|
819
|
+
var _this8 = this;
|
|
739
820
|
var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$fireAnalyticsEv6, _this$saveExperience3, _this$fireAnalyticsEv7, _this$saveExperience4;
|
|
740
821
|
return _regenerator.default.wrap(function _callee3$(_context4) {
|
|
741
822
|
while (1) switch (_context4.prev = _context4.next) {
|
|
@@ -877,8 +958,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
|
|
|
877
958
|
// Use setTimeout to avoid deep recursion and run queued flush asynchronously
|
|
878
959
|
// Note: flush() handles all exceptions internally and never rejects
|
|
879
960
|
this.queuedFlushTimeout = setTimeout(function () {
|
|
880
|
-
|
|
881
|
-
void
|
|
961
|
+
_this8.queuedFlushTimeout = undefined;
|
|
962
|
+
void _this8.flush();
|
|
882
963
|
}, 0);
|
|
883
964
|
}
|
|
884
965
|
return _context4.finish(49);
|
|
@@ -904,6 +985,13 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
|
|
|
904
985
|
clearTimeout(this.queuedFlushTimeout);
|
|
905
986
|
this.queuedFlushTimeout = undefined;
|
|
906
987
|
}
|
|
988
|
+
|
|
989
|
+
// Cancel any pending EntityNotFound retry timers
|
|
990
|
+
this.entityNotFoundRetryTimers.forEach(function (timer) {
|
|
991
|
+
return clearTimeout(timer);
|
|
992
|
+
});
|
|
993
|
+
this.entityNotFoundRetryTimers.clear();
|
|
994
|
+
this.entityNotFoundRetryCount.clear();
|
|
907
995
|
this._subscriptionManager.destroy();
|
|
908
996
|
this._providerFactoryManager.destroy();
|
|
909
997
|
this._batchFetcher.destroy();
|
|
@@ -12,6 +12,8 @@ import { syncBlockInMemorySessionCache } from './syncBlockInMemorySessionCache';
|
|
|
12
12
|
import { SyncBlockProviderFactoryManager } from './syncBlockProviderFactoryManager';
|
|
13
13
|
import { SyncBlockSubscriptionManager } from './syncBlockSubscriptionManager';
|
|
14
14
|
const CACHE_KEY_PREFIX = 'sync-block-data-';
|
|
15
|
+
const ENTITY_NOT_FOUND_MAX_RETRIES = 3;
|
|
16
|
+
const ENTITY_NOT_FOUND_INITIAL_DELAY_MS = 2000;
|
|
15
17
|
|
|
16
18
|
// A store manager responsible for the lifecycle and state management of reference sync blocks in an editor instance.
|
|
17
19
|
// Designed to manage local in-memory state and synchronize with an external data provider.
|
|
@@ -19,8 +21,6 @@ const CACHE_KEY_PREFIX = 'sync-block-data-';
|
|
|
19
21
|
// Handles fetching source URL and title for sync blocks.
|
|
20
22
|
// Can be used in both editor and renderer contexts.
|
|
21
23
|
export class ReferenceSyncBlockStoreManager {
|
|
22
|
-
// Track the setTimeout handle for queued flush so we can cancel it on destroy
|
|
23
|
-
|
|
24
24
|
constructor(dataProvider, viewMode) {
|
|
25
25
|
var _this$dataProvider;
|
|
26
26
|
// Keeps track of addition and deletion of reference synced blocks on the document
|
|
@@ -33,6 +33,9 @@ export class ReferenceSyncBlockStoreManager {
|
|
|
33
33
|
_defineProperty(this, "isFlushInProgress", false);
|
|
34
34
|
// Track if another flush is needed after the current one completes
|
|
35
35
|
_defineProperty(this, "flushNeededAfterCurrent", false);
|
|
36
|
+
// Track retry attempts for EntityNotFound errors (block may be in the process of being created)
|
|
37
|
+
_defineProperty(this, "entityNotFoundRetryCount", new Map());
|
|
38
|
+
_defineProperty(this, "entityNotFoundRetryTimers", new Map());
|
|
36
39
|
this.dataProvider = dataProvider;
|
|
37
40
|
this.viewMode = viewMode;
|
|
38
41
|
this.syncBlockFetchDataRequests = new Map();
|
|
@@ -453,11 +456,37 @@ export class ReferenceSyncBlockStoreManager {
|
|
|
453
456
|
// Remove from newly added set even if not unpublished (to clean up)
|
|
454
457
|
this.newlyAddedSyncBlocks.delete(syncBlockInstance.resourceId);
|
|
455
458
|
}
|
|
459
|
+
|
|
460
|
+
// Clear retry tracking on successful fetch — block has been created
|
|
461
|
+
if (!syncBlockInstance.error && this.entityNotFoundRetryCount.has(syncBlockInstance.resourceId)) {
|
|
462
|
+
const timer = this.entityNotFoundRetryTimers.get(syncBlockInstance.resourceId);
|
|
463
|
+
if (timer) {
|
|
464
|
+
clearTimeout(timer);
|
|
465
|
+
this.entityNotFoundRetryTimers.delete(syncBlockInstance.resourceId);
|
|
466
|
+
}
|
|
467
|
+
this.entityNotFoundRetryCount.delete(syncBlockInstance.resourceId);
|
|
468
|
+
}
|
|
456
469
|
if (syncBlockInstance.error) {
|
|
457
|
-
var _this$
|
|
458
|
-
|
|
470
|
+
var _this$entityNotFoundR;
|
|
471
|
+
// Skip error analytics when EntityNotFound will be retried, to avoid
|
|
472
|
+
// inflating error-rate metrics with expected transient failures
|
|
473
|
+
const isRetryingEntityNotFound = syncBlockInstance.error.type === SyncBlockError.EntityNotFound && ((_this$entityNotFoundR = this.entityNotFoundRetryCount.get(syncBlockInstance.resourceId)) !== null && _this$entityNotFoundR !== void 0 ? _this$entityNotFoundR : 0) < ENTITY_NOT_FOUND_MAX_RETRIES && fg('platform_synced_block_patch_13');
|
|
474
|
+
if (!isRetryingEntityNotFound) {
|
|
475
|
+
var _this$fireAnalyticsEv9, _syncBlockInstance$da, _syncBlockInstance$da2;
|
|
476
|
+
(_this$fireAnalyticsEv9 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv9 === void 0 ? void 0 : _this$fireAnalyticsEv9.call(this, fetchErrorPayload(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
|
|
477
|
+
}
|
|
459
478
|
if (syncBlockInstance.error.type === SyncBlockError.NotFound || syncBlockInstance.error.type === SyncBlockError.Forbidden) {
|
|
460
479
|
hasExpectedError = true;
|
|
480
|
+
} else if (syncBlockInstance.error.type === SyncBlockError.EntityNotFound) {
|
|
481
|
+
// Schedule a retry for EntityNotFound — the source block may be in
|
|
482
|
+
// the process of being created by a collaborator (race condition
|
|
483
|
+
// between NCS propagation and Block Service createBlock call).
|
|
484
|
+
if (fg('platform_synced_block_patch_13')) {
|
|
485
|
+
this.scheduleEntityNotFoundRetry(syncBlockInstance.resourceId);
|
|
486
|
+
}
|
|
487
|
+
if (!isRetryingEntityNotFound) {
|
|
488
|
+
hasUnexpectedError = true;
|
|
489
|
+
}
|
|
461
490
|
} else if (syncBlockInstance.error) {
|
|
462
491
|
hasUnexpectedError = true;
|
|
463
492
|
}
|
|
@@ -518,6 +547,55 @@ export class ReferenceSyncBlockStoreManager {
|
|
|
518
547
|
(_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 ? void 0 : _this$dataProvider5.removeFromCache([resourceId]);
|
|
519
548
|
this._providerFactoryManager.deleteFactory(resourceId);
|
|
520
549
|
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Schedules a delayed retry for a block that returned EntityNotFound.
|
|
553
|
+
* The block may be in the process of being created by a collaborator —
|
|
554
|
+
* the NCS transaction propagates the bodiedSyncBlock ADF node before
|
|
555
|
+
* the Block Service createBlock call completes.
|
|
556
|
+
*/
|
|
557
|
+
scheduleEntityNotFoundRetry(resourceId) {
|
|
558
|
+
var _this$entityNotFoundR2;
|
|
559
|
+
const currentRetries = (_this$entityNotFoundR2 = this.entityNotFoundRetryCount.get(resourceId)) !== null && _this$entityNotFoundR2 !== void 0 ? _this$entityNotFoundR2 : 0;
|
|
560
|
+
if (currentRetries >= ENTITY_NOT_FOUND_MAX_RETRIES) {
|
|
561
|
+
// Max retries exceeded — keep count at max so future calls immediately exit
|
|
562
|
+
// (don't delete — that would reset the counter and allow unbounded retry waves)
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// If a timer is already pending, don't schedule another one — let the
|
|
567
|
+
// existing timer fire. This prevents rapid EntityNotFound responses from
|
|
568
|
+
// exhausting the retry budget through cancellations without any actual
|
|
569
|
+
// fetch completing.
|
|
570
|
+
if (this.entityNotFoundRetryTimers.has(resourceId)) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const delay = ENTITY_NOT_FOUND_INITIAL_DELAY_MS * Math.pow(2, currentRetries);
|
|
574
|
+
const timer = setTimeout(() => {
|
|
575
|
+
var _cached$error;
|
|
576
|
+
this.entityNotFoundRetryTimers.delete(resourceId);
|
|
577
|
+
|
|
578
|
+
// If no active subscriptions remain for this block, clean up and skip
|
|
579
|
+
const subscriptions = this._subscriptionManager.getSubscriptions().get(resourceId);
|
|
580
|
+
if (!subscriptions || Object.keys(subscriptions).length === 0) {
|
|
581
|
+
this.entityNotFoundRetryCount.delete(resourceId);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Increment count only when the timer fires, not when scheduled
|
|
586
|
+
this.entityNotFoundRetryCount.set(resourceId, currentRetries + 1);
|
|
587
|
+
|
|
588
|
+
// Clear the error from cache so fetchSyncBlocksData doesn't skip it
|
|
589
|
+
const cached = this.getFromCache(resourceId);
|
|
590
|
+
if ((cached === null || cached === void 0 ? void 0 : (_cached$error = cached.error) === null || _cached$error === void 0 ? void 0 : _cached$error.type) === SyncBlockError.EntityNotFound) {
|
|
591
|
+
this.deleteFromCache(resourceId);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Trigger a re-fetch via the batch fetcher
|
|
595
|
+
this.debouncedBatchedFetchSyncBlocks(resourceId);
|
|
596
|
+
}, delay);
|
|
597
|
+
this.entityNotFoundRetryTimers.set(resourceId, timer);
|
|
598
|
+
}
|
|
521
599
|
debouncedBatchedFetchSyncBlocks(resourceId) {
|
|
522
600
|
this._batchFetcher.queueFetch(resourceId);
|
|
523
601
|
}
|
|
@@ -695,6 +773,11 @@ export class ReferenceSyncBlockStoreManager {
|
|
|
695
773
|
clearTimeout(this.queuedFlushTimeout);
|
|
696
774
|
this.queuedFlushTimeout = undefined;
|
|
697
775
|
}
|
|
776
|
+
|
|
777
|
+
// Cancel any pending EntityNotFound retry timers
|
|
778
|
+
this.entityNotFoundRetryTimers.forEach(timer => clearTimeout(timer));
|
|
779
|
+
this.entityNotFoundRetryTimers.clear();
|
|
780
|
+
this.entityNotFoundRetryCount.clear();
|
|
698
781
|
this._subscriptionManager.destroy();
|
|
699
782
|
this._providerFactoryManager.destroy();
|
|
700
783
|
this._batchFetcher.destroy();
|
|
@@ -22,6 +22,8 @@ import { syncBlockInMemorySessionCache } from './syncBlockInMemorySessionCache';
|
|
|
22
22
|
import { SyncBlockProviderFactoryManager } from './syncBlockProviderFactoryManager';
|
|
23
23
|
import { SyncBlockSubscriptionManager } from './syncBlockSubscriptionManager';
|
|
24
24
|
var CACHE_KEY_PREFIX = 'sync-block-data-';
|
|
25
|
+
var ENTITY_NOT_FOUND_MAX_RETRIES = 3;
|
|
26
|
+
var ENTITY_NOT_FOUND_INITIAL_DELAY_MS = 2000;
|
|
25
27
|
|
|
26
28
|
// A store manager responsible for the lifecycle and state management of reference sync blocks in an editor instance.
|
|
27
29
|
// Designed to manage local in-memory state and synchronize with an external data provider.
|
|
@@ -29,8 +31,6 @@ var CACHE_KEY_PREFIX = 'sync-block-data-';
|
|
|
29
31
|
// Handles fetching source URL and title for sync blocks.
|
|
30
32
|
// Can be used in both editor and renderer contexts.
|
|
31
33
|
export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
|
|
32
|
-
// Track the setTimeout handle for queued flush so we can cancel it on destroy
|
|
33
|
-
|
|
34
34
|
function ReferenceSyncBlockStoreManager(dataProvider, viewMode) {
|
|
35
35
|
var _this = this,
|
|
36
36
|
_this$dataProvider;
|
|
@@ -45,6 +45,9 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
|
|
|
45
45
|
_defineProperty(this, "isFlushInProgress", false);
|
|
46
46
|
// Track if another flush is needed after the current one completes
|
|
47
47
|
_defineProperty(this, "flushNeededAfterCurrent", false);
|
|
48
|
+
// Track retry attempts for EntityNotFound errors (block may be in the process of being created)
|
|
49
|
+
_defineProperty(this, "entityNotFoundRetryCount", new Map());
|
|
50
|
+
_defineProperty(this, "entityNotFoundRetryTimers", new Map());
|
|
48
51
|
this.dataProvider = dataProvider;
|
|
49
52
|
this.viewMode = viewMode;
|
|
50
53
|
this.syncBlockFetchDataRequests = new Map();
|
|
@@ -574,11 +577,37 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
|
|
|
574
577
|
// Remove from newly added set even if not unpublished (to clean up)
|
|
575
578
|
_this5.newlyAddedSyncBlocks.delete(syncBlockInstance.resourceId);
|
|
576
579
|
}
|
|
580
|
+
|
|
581
|
+
// Clear retry tracking on successful fetch — block has been created
|
|
582
|
+
if (!syncBlockInstance.error && _this5.entityNotFoundRetryCount.has(syncBlockInstance.resourceId)) {
|
|
583
|
+
var timer = _this5.entityNotFoundRetryTimers.get(syncBlockInstance.resourceId);
|
|
584
|
+
if (timer) {
|
|
585
|
+
clearTimeout(timer);
|
|
586
|
+
_this5.entityNotFoundRetryTimers.delete(syncBlockInstance.resourceId);
|
|
587
|
+
}
|
|
588
|
+
_this5.entityNotFoundRetryCount.delete(syncBlockInstance.resourceId);
|
|
589
|
+
}
|
|
577
590
|
if (syncBlockInstance.error) {
|
|
578
|
-
var _this5$
|
|
579
|
-
|
|
591
|
+
var _this5$entityNotFound;
|
|
592
|
+
// Skip error analytics when EntityNotFound will be retried, to avoid
|
|
593
|
+
// inflating error-rate metrics with expected transient failures
|
|
594
|
+
var isRetryingEntityNotFound = syncBlockInstance.error.type === SyncBlockError.EntityNotFound && ((_this5$entityNotFound = _this5.entityNotFoundRetryCount.get(syncBlockInstance.resourceId)) !== null && _this5$entityNotFound !== void 0 ? _this5$entityNotFound : 0) < ENTITY_NOT_FOUND_MAX_RETRIES && fg('platform_synced_block_patch_13');
|
|
595
|
+
if (!isRetryingEntityNotFound) {
|
|
596
|
+
var _this5$fireAnalyticsE2, _syncBlockInstance$da, _syncBlockInstance$da2;
|
|
597
|
+
(_this5$fireAnalyticsE2 = _this5.fireAnalyticsEvent) === null || _this5$fireAnalyticsE2 === void 0 || _this5$fireAnalyticsE2.call(_this5, fetchErrorPayload(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
|
|
598
|
+
}
|
|
580
599
|
if (syncBlockInstance.error.type === SyncBlockError.NotFound || syncBlockInstance.error.type === SyncBlockError.Forbidden) {
|
|
581
600
|
hasExpectedError = true;
|
|
601
|
+
} else if (syncBlockInstance.error.type === SyncBlockError.EntityNotFound) {
|
|
602
|
+
// Schedule a retry for EntityNotFound — the source block may be in
|
|
603
|
+
// the process of being created by a collaborator (race condition
|
|
604
|
+
// between NCS propagation and Block Service createBlock call).
|
|
605
|
+
if (fg('platform_synced_block_patch_13')) {
|
|
606
|
+
_this5.scheduleEntityNotFoundRetry(syncBlockInstance.resourceId);
|
|
607
|
+
}
|
|
608
|
+
if (!isRetryingEntityNotFound) {
|
|
609
|
+
hasUnexpectedError = true;
|
|
610
|
+
}
|
|
582
611
|
} else if (syncBlockInstance.error) {
|
|
583
612
|
hasUnexpectedError = true;
|
|
584
613
|
}
|
|
@@ -642,6 +671,58 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
|
|
|
642
671
|
(_this$dataProvider5 = this.dataProvider) === null || _this$dataProvider5 === void 0 || _this$dataProvider5.removeFromCache([resourceId]);
|
|
643
672
|
this._providerFactoryManager.deleteFactory(resourceId);
|
|
644
673
|
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Schedules a delayed retry for a block that returned EntityNotFound.
|
|
677
|
+
* The block may be in the process of being created by a collaborator —
|
|
678
|
+
* the NCS transaction propagates the bodiedSyncBlock ADF node before
|
|
679
|
+
* the Block Service createBlock call completes.
|
|
680
|
+
*/
|
|
681
|
+
}, {
|
|
682
|
+
key: "scheduleEntityNotFoundRetry",
|
|
683
|
+
value: function scheduleEntityNotFoundRetry(resourceId) {
|
|
684
|
+
var _this$entityNotFoundR,
|
|
685
|
+
_this6 = this;
|
|
686
|
+
var currentRetries = (_this$entityNotFoundR = this.entityNotFoundRetryCount.get(resourceId)) !== null && _this$entityNotFoundR !== void 0 ? _this$entityNotFoundR : 0;
|
|
687
|
+
if (currentRetries >= ENTITY_NOT_FOUND_MAX_RETRIES) {
|
|
688
|
+
// Max retries exceeded — keep count at max so future calls immediately exit
|
|
689
|
+
// (don't delete — that would reset the counter and allow unbounded retry waves)
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// If a timer is already pending, don't schedule another one — let the
|
|
694
|
+
// existing timer fire. This prevents rapid EntityNotFound responses from
|
|
695
|
+
// exhausting the retry budget through cancellations without any actual
|
|
696
|
+
// fetch completing.
|
|
697
|
+
if (this.entityNotFoundRetryTimers.has(resourceId)) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
var delay = ENTITY_NOT_FOUND_INITIAL_DELAY_MS * Math.pow(2, currentRetries);
|
|
701
|
+
var timer = setTimeout(function () {
|
|
702
|
+
var _cached$error;
|
|
703
|
+
_this6.entityNotFoundRetryTimers.delete(resourceId);
|
|
704
|
+
|
|
705
|
+
// If no active subscriptions remain for this block, clean up and skip
|
|
706
|
+
var subscriptions = _this6._subscriptionManager.getSubscriptions().get(resourceId);
|
|
707
|
+
if (!subscriptions || Object.keys(subscriptions).length === 0) {
|
|
708
|
+
_this6.entityNotFoundRetryCount.delete(resourceId);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Increment count only when the timer fires, not when scheduled
|
|
713
|
+
_this6.entityNotFoundRetryCount.set(resourceId, currentRetries + 1);
|
|
714
|
+
|
|
715
|
+
// Clear the error from cache so fetchSyncBlocksData doesn't skip it
|
|
716
|
+
var cached = _this6.getFromCache(resourceId);
|
|
717
|
+
if ((cached === null || cached === void 0 || (_cached$error = cached.error) === null || _cached$error === void 0 ? void 0 : _cached$error.type) === SyncBlockError.EntityNotFound) {
|
|
718
|
+
_this6.deleteFromCache(resourceId);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Trigger a re-fetch via the batch fetcher
|
|
722
|
+
_this6.debouncedBatchedFetchSyncBlocks(resourceId);
|
|
723
|
+
}, delay);
|
|
724
|
+
this.entityNotFoundRetryTimers.set(resourceId, timer);
|
|
725
|
+
}
|
|
645
726
|
}, {
|
|
646
727
|
key: "debouncedBatchedFetchSyncBlocks",
|
|
647
728
|
value: function debouncedBatchedFetchSyncBlocks(resourceId) {
|
|
@@ -650,12 +731,12 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
|
|
|
650
731
|
}, {
|
|
651
732
|
key: "setSSRDataInSessionCache",
|
|
652
733
|
value: function setSSRDataInSessionCache(resourceIds) {
|
|
653
|
-
var
|
|
734
|
+
var _this7 = this;
|
|
654
735
|
if (!resourceIds || resourceIds.length === 0) {
|
|
655
736
|
return;
|
|
656
737
|
}
|
|
657
738
|
resourceIds.forEach(function (resourceId) {
|
|
658
|
-
|
|
739
|
+
_this7.updateSessionCache(resourceId);
|
|
659
740
|
});
|
|
660
741
|
}
|
|
661
742
|
}, {
|
|
@@ -728,7 +809,7 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
|
|
|
728
809
|
key: "flush",
|
|
729
810
|
value: (function () {
|
|
730
811
|
var _flush = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3() {
|
|
731
|
-
var
|
|
812
|
+
var _this8 = this;
|
|
732
813
|
var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator, _step, _loop, updateResult, _this$saveExperience2, _this$fireAnalyticsEv6, _this$saveExperience3, _this$fireAnalyticsEv7, _this$saveExperience4;
|
|
733
814
|
return _regeneratorRuntime.wrap(function _callee3$(_context4) {
|
|
734
815
|
while (1) switch (_context4.prev = _context4.next) {
|
|
@@ -870,8 +951,8 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
|
|
|
870
951
|
// Use setTimeout to avoid deep recursion and run queued flush asynchronously
|
|
871
952
|
// Note: flush() handles all exceptions internally and never rejects
|
|
872
953
|
this.queuedFlushTimeout = setTimeout(function () {
|
|
873
|
-
|
|
874
|
-
void
|
|
954
|
+
_this8.queuedFlushTimeout = undefined;
|
|
955
|
+
void _this8.flush();
|
|
875
956
|
}, 0);
|
|
876
957
|
}
|
|
877
958
|
return _context4.finish(49);
|
|
@@ -897,6 +978,13 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
|
|
|
897
978
|
clearTimeout(this.queuedFlushTimeout);
|
|
898
979
|
this.queuedFlushTimeout = undefined;
|
|
899
980
|
}
|
|
981
|
+
|
|
982
|
+
// Cancel any pending EntityNotFound retry timers
|
|
983
|
+
this.entityNotFoundRetryTimers.forEach(function (timer) {
|
|
984
|
+
return clearTimeout(timer);
|
|
985
|
+
});
|
|
986
|
+
this.entityNotFoundRetryTimers.clear();
|
|
987
|
+
this.entityNotFoundRetryCount.clear();
|
|
900
988
|
this._subscriptionManager.destroy();
|
|
901
989
|
this._providerFactoryManager.destroy();
|
|
902
990
|
this._batchFetcher.destroy();
|
|
@@ -18,6 +18,8 @@ export declare class ReferenceSyncBlockStoreManager {
|
|
|
18
18
|
private isFlushInProgress;
|
|
19
19
|
private flushNeededAfterCurrent;
|
|
20
20
|
private queuedFlushTimeout?;
|
|
21
|
+
private entityNotFoundRetryCount;
|
|
22
|
+
private entityNotFoundRetryTimers;
|
|
21
23
|
fetchExperience: Experience | undefined;
|
|
22
24
|
private fetchSourceInfoExperience;
|
|
23
25
|
private saveExperience;
|
|
@@ -88,6 +90,13 @@ export declare class ReferenceSyncBlockStoreManager {
|
|
|
88
90
|
private updateCache;
|
|
89
91
|
getFromCache(resourceId: ResourceId): SyncBlockInstance | undefined;
|
|
90
92
|
private deleteFromCache;
|
|
93
|
+
/**
|
|
94
|
+
* Schedules a delayed retry for a block that returned EntityNotFound.
|
|
95
|
+
* The block may be in the process of being created by a collaborator —
|
|
96
|
+
* the NCS transaction propagates the bodiedSyncBlock ADF node before
|
|
97
|
+
* the Block Service createBlock call completes.
|
|
98
|
+
*/
|
|
99
|
+
private scheduleEntityNotFoundRetry;
|
|
91
100
|
private debouncedBatchedFetchSyncBlocks;
|
|
92
101
|
private setSSRDataInSessionCache;
|
|
93
102
|
subscribeToSyncBlock(resourceId: string, localId: string, callback: SubscriptionCallback): () => void;
|
|
@@ -18,6 +18,8 @@ export declare class ReferenceSyncBlockStoreManager {
|
|
|
18
18
|
private isFlushInProgress;
|
|
19
19
|
private flushNeededAfterCurrent;
|
|
20
20
|
private queuedFlushTimeout?;
|
|
21
|
+
private entityNotFoundRetryCount;
|
|
22
|
+
private entityNotFoundRetryTimers;
|
|
21
23
|
fetchExperience: Experience | undefined;
|
|
22
24
|
private fetchSourceInfoExperience;
|
|
23
25
|
private saveExperience;
|
|
@@ -88,6 +90,13 @@ export declare class ReferenceSyncBlockStoreManager {
|
|
|
88
90
|
private updateCache;
|
|
89
91
|
getFromCache(resourceId: ResourceId): SyncBlockInstance | undefined;
|
|
90
92
|
private deleteFromCache;
|
|
93
|
+
/**
|
|
94
|
+
* Schedules a delayed retry for a block that returned EntityNotFound.
|
|
95
|
+
* The block may be in the process of being created by a collaborator —
|
|
96
|
+
* the NCS transaction propagates the bodiedSyncBlock ADF node before
|
|
97
|
+
* the Block Service createBlock call completes.
|
|
98
|
+
*/
|
|
99
|
+
private scheduleEntityNotFoundRetry;
|
|
91
100
|
private debouncedBatchedFetchSyncBlocks;
|
|
92
101
|
private setSSRDataInSessionCache;
|
|
93
102
|
subscribeToSyncBlock(resourceId: string, localId: string, callback: SubscriptionCallback): () => void;
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@atlaskit/editor-prosemirror": "^7.3.0",
|
|
30
30
|
"@atlaskit/node-data-provider": "^11.1.0",
|
|
31
31
|
"@atlaskit/platform-feature-flags": "^1.1.0",
|
|
32
|
-
"@atlaskit/tmp-editor-statsig": "^
|
|
32
|
+
"@atlaskit/tmp-editor-statsig": "^84.0.0",
|
|
33
33
|
"@babel/runtime": "^7.0.0",
|
|
34
34
|
"@compiled/react": "^0.20.0",
|
|
35
35
|
"graphql-ws": "^5.14.2",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"uuid": "^3.1.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@atlaskit/editor-common": "^114.
|
|
41
|
+
"@atlaskit/editor-common": "^114.47.0",
|
|
42
42
|
"react": "^18.2.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
84
|
"name": "@atlaskit/editor-synced-block-provider",
|
|
85
|
-
"version": "6.6.
|
|
85
|
+
"version": "6.6.10",
|
|
86
86
|
"description": "Synced Block Provider for @atlaskit/editor-plugin-synced-block",
|
|
87
87
|
"author": "Atlassian Pty Ltd",
|
|
88
88
|
"license": "Apache-2.0",
|
|
@@ -92,6 +92,9 @@
|
|
|
92
92
|
"platform-feature-flags": {
|
|
93
93
|
"platform_synced_block_patch_12": {
|
|
94
94
|
"type": "boolean"
|
|
95
|
+
},
|
|
96
|
+
"platform_synced_block_patch_13": {
|
|
97
|
+
"type": "boolean"
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
}
|