@atlaskit/editor-synced-block-provider 6.6.6 → 6.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/clients/block-service/blockSubscription.js +2 -2
  3. package/dist/cjs/providers/block-service/blockServiceAPI.js +2 -2
  4. package/dist/cjs/providers/syncBlockProvider.js +2 -2
  5. package/dist/cjs/store-manager/referenceSyncBlockStoreManager.js +4 -0
  6. package/dist/cjs/store-manager/syncBlockSubscriptionManager.js +148 -25
  7. package/dist/es2019/clients/block-service/blockSubscription.js +2 -2
  8. package/dist/es2019/providers/block-service/blockServiceAPI.js +2 -2
  9. package/dist/es2019/providers/syncBlockProvider.js +2 -2
  10. package/dist/es2019/store-manager/referenceSyncBlockStoreManager.js +4 -0
  11. package/dist/es2019/store-manager/syncBlockSubscriptionManager.js +109 -6
  12. package/dist/esm/clients/block-service/blockSubscription.js +2 -2
  13. package/dist/esm/providers/block-service/blockServiceAPI.js +2 -2
  14. package/dist/esm/providers/syncBlockProvider.js +2 -2
  15. package/dist/esm/store-manager/referenceSyncBlockStoreManager.js +4 -0
  16. package/dist/esm/store-manager/syncBlockSubscriptionManager.js +148 -25
  17. package/dist/types/clients/block-service/blockSubscription.d.ts +2 -1
  18. package/dist/types/entry-points/providers-types.d.ts +1 -1
  19. package/dist/types/providers/block-service/blockServiceAPI.d.ts +1 -1
  20. package/dist/types/providers/syncBlockProvider.d.ts +1 -1
  21. package/dist/types/providers/types.d.ts +4 -2
  22. package/dist/types/store-manager/syncBlockSubscriptionManager.d.ts +23 -0
  23. package/dist/types-ts4.5/clients/block-service/blockSubscription.d.ts +2 -1
  24. package/dist/types-ts4.5/entry-points/providers-types.d.ts +1 -1
  25. package/dist/types-ts4.5/providers/block-service/blockServiceAPI.d.ts +1 -1
  26. package/dist/types-ts4.5/providers/syncBlockProvider.d.ts +1 -1
  27. package/dist/types-ts4.5/providers/types.d.ts +4 -2
  28. package/dist/types-ts4.5/store-manager/syncBlockSubscriptionManager.d.ts +23 -0
  29. package/package.json +5 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @atlaskit/editor-synced-block-provider
2
2
 
3
+ ## 6.6.8
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 6.6.7
10
+
11
+ ### Patch Changes
12
+
13
+ - [`bfba4158ff047`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/bfba4158ff047) -
14
+ Fix subscription reconnection for synced blocks when WebSocket or Relay subscriptions complete or
15
+ error silently
16
+
3
17
  ## 6.6.6
4
18
 
5
19
  ### Patch Changes
@@ -174,7 +174,7 @@ var parseSubscriptionPayload = function parseSubscriptionPayload(payload) {
174
174
  * @param onError - Optional callback function invoked on subscription errors
175
175
  * @returns Unsubscribe function to close the subscription
176
176
  */
177
- var subscribeToBlockUpdates = exports.subscribeToBlockUpdates = function subscribeToBlockUpdates(blockAri, onData, onError) {
177
+ var subscribeToBlockUpdates = exports.subscribeToBlockUpdates = function subscribeToBlockUpdates(blockAri, onData, onError, onComplete) {
178
178
  var client = getBlockServiceClient();
179
179
  if (!client) {
180
180
  // Return a no-op unsubscribe if client is not available (e.g., SSR)
@@ -210,7 +210,7 @@ var subscribeToBlockUpdates = exports.subscribeToBlockUpdates = function subscri
210
210
  onError === null || onError === void 0 || onError(new Error(extractGraphQLWSErrorMessage(error)));
211
211
  }),
212
212
  complete: function complete() {
213
- // Subscription completed
213
+ onComplete === null || onComplete === void 0 || onComplete();
214
214
  }
215
215
  });
216
216
  return unsubscribe;
@@ -793,7 +793,7 @@ var BlockServiceADFFetchProvider = /*#__PURE__*/function () {
793
793
  )
794
794
  }, {
795
795
  key: "subscribeToBlockUpdates",
796
- value: function subscribeToBlockUpdates(resourceId, onUpdate, onError) {
796
+ value: function subscribeToBlockUpdates(resourceId, onUpdate, onError, onComplete) {
797
797
  var blockAri = (0, _ari.generateBlockAriFromReference)({
798
798
  cloudId: this.cloudId,
799
799
  resourceId: resourceId
@@ -829,7 +829,7 @@ var BlockServiceADFFetchProvider = /*#__PURE__*/function () {
829
829
  resourceId: parsedData.resourceId
830
830
  };
831
831
  onUpdate(syncBlockInstance);
832
- }, onError);
832
+ }, onError, onComplete);
833
833
  }).catch(function (err) {
834
834
  if (cancelled) {
835
835
  return;
@@ -469,9 +469,9 @@ var SyncedBlockProvider = exports.SyncedBlockProvider = /*#__PURE__*/function (_
469
469
  */
470
470
  }, {
471
471
  key: "subscribeToBlockUpdates",
472
- value: function subscribeToBlockUpdates(resourceId, onUpdate, onError) {
472
+ value: function subscribeToBlockUpdates(resourceId, onUpdate, onError, onComplete) {
473
473
  if (this.fetchProvider.subscribeToBlockUpdates) {
474
- return this.fetchProvider.subscribeToBlockUpdates(resourceId, onUpdate, onError);
474
+ return this.fetchProvider.subscribeToBlockUpdates(resourceId, onUpdate, onError, onComplete);
475
475
  }
476
476
  return undefined;
477
477
  }
@@ -211,6 +211,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
211
211
  var syncBlockNode = (0, _utils.createSyncBlockNode)('', resourceId);
212
212
  var providerData = (_this$dataProvider2 = this.dataProvider) === null || _this$dataProvider2 === void 0 || (_this$dataProvider2 = _this$dataProvider2.getNodeDataFromCache(syncBlockNode)) === null || _this$dataProvider2 === void 0 ? void 0 : _this$dataProvider2.data;
213
213
  if (providerData) {
214
+ // Initial provider cache data can come from SSR/prefetch and bypass updateCache(),
215
+ // so strip annotations here before references render existing synced block payloads.
214
216
  return this.stripAnnotationMarksFromReferenceData(providerData);
215
217
  }
216
218
  return this.getFromSessionCache(resourceId);
@@ -248,6 +250,8 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
248
250
  if (!raw) {
249
251
  return undefined;
250
252
  }
253
+ // Session cache entries written before this sanitizer existed may still include
254
+ // source annotation marks, so keep this read-time safety net for legacy data.
251
255
  return this.stripAnnotationMarksFromReferenceData(JSON.parse(raw));
252
256
  } catch (error) {
253
257
  (0, _monitoring.logException)(error, {
@@ -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");
@@ -36,6 +37,8 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
36
37
  // causing the cache to be deleted prematurely. We delay deletion to allow
37
38
  // the new component to subscribe and cancel the pending deletion.
38
39
  (0, _defineProperty2.default)(this, "pendingCacheDeletions", new Map());
40
+ (0, _defineProperty2.default)(this, "retryAttempts", new Map());
41
+ (0, _defineProperty2.default)(this, "pendingRetries", new Map());
39
42
  this.deps = deps;
40
43
  }
41
44
 
@@ -250,6 +253,7 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
250
253
  if (!(dataProvider !== null && dataProvider !== void 0 && dataProvider.subscribeToBlockUpdates)) {
251
254
  return;
252
255
  }
256
+ var reconnectEnabled = (0, _platformFeatureFlags.fg)('platform_synced_block_patch_12');
253
257
  var unsubscribe = dataProvider.subscribeToBlockUpdates(resourceId, function (syncBlockInstance) {
254
258
  _this5.handleGraphQLUpdate(syncBlockInstance);
255
259
  }, function (error) {
@@ -258,11 +262,115 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
258
262
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/graphql-subscription'
259
263
  });
260
264
  (_this5$deps$getFireAn = _this5.deps.getFireAnalyticsEvent()) === null || _this5$deps$getFireAn === void 0 || _this5$deps$getFireAn((0, _errorHandling.fetchErrorPayload)(error.message, resourceId, (0, _utils.getSourceProductFromResourceIdSafe)(resourceId)));
261
- });
265
+ if (reconnectEnabled) {
266
+ _this5.handleSubscriptionTerminated(resourceId);
267
+ }
268
+ }, reconnectEnabled ? function () {
269
+ _this5.handleSubscriptionTerminated(resourceId);
270
+ } : undefined);
262
271
  if (unsubscribe) {
263
272
  this.graphqlSubscriptions.set(resourceId, unsubscribe);
264
273
  }
265
274
  }
275
+
276
+ /**
277
+ * Handles subscription termination (complete or error) by cleaning up the
278
+ * stale entry and scheduling a reconnection with exponential backoff.
279
+ */
280
+ }, {
281
+ key: "handleSubscriptionTerminated",
282
+ value: function handleSubscriptionTerminated(resourceId) {
283
+ // Remove the stale subscription entry so setupSubscription won't early-return
284
+ this.graphqlSubscriptions.delete(resourceId);
285
+
286
+ // Guard against duplicate calls (graphql-ws can fire both error and complete)
287
+ if (this.pendingRetries.has(resourceId)) {
288
+ return;
289
+ }
290
+
291
+ // Only reconnect if there are still active subscribers for this resource
292
+ if (this.subscriptions.has(resourceId) && this.shouldUseRealTime()) {
293
+ this.scheduleReconnection(resourceId);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Schedules a reconnection attempt with exponential backoff.
299
+ * Delay = INITIAL_RETRY_DELAY_MS * (RETRY_BACKOFF_MULTIPLIER ^ attempts)
300
+ * e.g. 1s, 2s, 4s, 8s, 16s
301
+ */
302
+ }, {
303
+ key: "scheduleReconnection",
304
+ value: function scheduleReconnection(resourceId) {
305
+ var _this$retryAttempts$g,
306
+ _this6 = this;
307
+ var attempts = (_this$retryAttempts$g = this.retryAttempts.get(resourceId)) !== null && _this$retryAttempts$g !== void 0 ? _this$retryAttempts$g : 0;
308
+ if (attempts >= SyncBlockSubscriptionManager.MAX_RETRY_ATTEMPTS) {
309
+ var _this$deps$getFireAna;
310
+ var errorMessage = "Subscription reconnection failed after ".concat(attempts, " attempts");
311
+ (0, _monitoring.logException)(new Error(errorMessage), {
312
+ location: 'editor-synced-block-provider/syncBlockSubscriptionManager/max-retries-exhausted'
313
+ });
314
+ (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 || _this$deps$getFireAna((0, _errorHandling.fetchErrorPayload)(errorMessage, resourceId, (0, _utils.getSourceProductFromResourceIdSafe)(resourceId)));
315
+ return;
316
+ }
317
+ var delay = SyncBlockSubscriptionManager.INITIAL_RETRY_DELAY_MS * Math.pow(SyncBlockSubscriptionManager.RETRY_BACKOFF_MULTIPLIER, attempts);
318
+ var timer = setTimeout(function () {
319
+ _this6.pendingRetries.delete(resourceId);
320
+
321
+ // Only re-subscribe if still relevant
322
+ if (_this6.subscriptions.has(resourceId) && _this6.shouldUseRealTime()) {
323
+ _this6.setupSubscription(resourceId);
324
+
325
+ // Only increment if the subscription was actually established
326
+ // (setupSubscription may be a no-op if another code path already re-established it)
327
+ if (_this6.graphqlSubscriptions.has(resourceId)) {
328
+ _this6.retryAttempts.set(resourceId, attempts + 1);
329
+ }
330
+ } else {
331
+ // Conditions no longer met — clean up stale retry state
332
+ _this6.retryAttempts.delete(resourceId);
333
+ }
334
+ }, delay);
335
+ this.pendingRetries.set(resourceId, timer);
336
+ }
337
+
338
+ /**
339
+ * Resets the retry counter for a resource. Called when a successful
340
+ * update is received, indicating the subscription is healthy.
341
+ */
342
+ }, {
343
+ key: "resetRetryCount",
344
+ value: function resetRetryCount(resourceId) {
345
+ this.retryAttempts.delete(resourceId);
346
+ }
347
+ }, {
348
+ key: "cancelPendingRetry",
349
+ value: function cancelPendingRetry(resourceId) {
350
+ var timer = this.pendingRetries.get(resourceId);
351
+ if (timer) {
352
+ clearTimeout(timer);
353
+ this.pendingRetries.delete(resourceId);
354
+ }
355
+ }
356
+ }, {
357
+ key: "cancelAllPendingRetries",
358
+ value: function cancelAllPendingRetries() {
359
+ var _iterator = _createForOfIteratorHelper(this.pendingRetries.values()),
360
+ _step;
361
+ try {
362
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
363
+ var timer = _step.value;
364
+ clearTimeout(timer);
365
+ }
366
+ } catch (err) {
367
+ _iterator.e(err);
368
+ } finally {
369
+ _iterator.f();
370
+ }
371
+ this.pendingRetries.clear();
372
+ this.retryAttempts.clear();
373
+ }
266
374
  }, {
267
375
  key: "cleanupSubscription",
268
376
  value: function cleanupSubscription(resourceId) {
@@ -271,39 +379,46 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
271
379
  unsubscribe();
272
380
  this.graphqlSubscriptions.delete(resourceId);
273
381
  }
382
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_12')) {
383
+ this.cancelPendingRetry(resourceId);
384
+ this.retryAttempts.delete(resourceId);
385
+ }
274
386
  }
275
387
  }, {
276
388
  key: "setupSubscriptionsForAllBlocks",
277
389
  value: function setupSubscriptionsForAllBlocks() {
278
- var _iterator = _createForOfIteratorHelper(this.subscriptions.keys()),
279
- _step;
390
+ var _iterator2 = _createForOfIteratorHelper(this.subscriptions.keys()),
391
+ _step2;
280
392
  try {
281
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
282
- var resourceId = _step.value;
393
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
394
+ var resourceId = _step2.value;
283
395
  this.setupSubscription(resourceId);
284
396
  }
285
397
  } catch (err) {
286
- _iterator.e(err);
398
+ _iterator2.e(err);
287
399
  } finally {
288
- _iterator.f();
400
+ _iterator2.f();
289
401
  }
290
402
  }
291
403
  }, {
292
404
  key: "cleanupAll",
293
405
  value: function cleanupAll() {
294
- var _iterator2 = _createForOfIteratorHelper(this.graphqlSubscriptions.values()),
295
- _step2;
406
+ var _iterator3 = _createForOfIteratorHelper(this.graphqlSubscriptions.values()),
407
+ _step3;
296
408
  try {
297
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
298
- var unsubscribe = _step2.value;
409
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
410
+ var unsubscribe = _step3.value;
299
411
  unsubscribe();
300
412
  }
301
413
  } catch (err) {
302
- _iterator2.e(err);
414
+ _iterator3.e(err);
303
415
  } finally {
304
- _iterator2.f();
416
+ _iterator3.f();
305
417
  }
306
418
  this.graphqlSubscriptions.clear();
419
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_12')) {
420
+ this.cancelAllPendingRetries();
421
+ }
307
422
  }
308
423
  }, {
309
424
  key: "destroy",
@@ -315,17 +430,17 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
315
430
  this.useRealTimeSubscriptions = false;
316
431
 
317
432
  // Clear any pending cache deletions
318
- var _iterator3 = _createForOfIteratorHelper(this.pendingCacheDeletions.values()),
319
- _step3;
433
+ var _iterator4 = _createForOfIteratorHelper(this.pendingCacheDeletions.values()),
434
+ _step4;
320
435
  try {
321
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
322
- var timeout = _step3.value;
436
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
437
+ var timeout = _step4.value;
323
438
  clearTimeout(timeout);
324
439
  }
325
440
  } catch (err) {
326
- _iterator3.e(err);
441
+ _iterator4.e(err);
327
442
  } finally {
328
- _iterator3.f();
443
+ _iterator4.f();
329
444
  }
330
445
  this.pendingCacheDeletions.clear();
331
446
  }
@@ -337,7 +452,7 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
337
452
  }, {
338
453
  key: "handleGraphQLUpdate",
339
454
  value: function handleGraphQLUpdate(syncBlockInstance) {
340
- var _this6 = this;
455
+ var _this7 = this;
341
456
  if (!syncBlockInstance.resourceId) {
342
457
  return;
343
458
  }
@@ -345,18 +460,26 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
345
460
  var resolved = existing ? (0, _resolveSyncBlockInstance.resolveSyncBlockInstance)(existing, syncBlockInstance) : syncBlockInstance;
346
461
  this.deps.updateCache(resolved);
347
462
  if (!syncBlockInstance.error) {
463
+ // Successful data delivery means the subscription is healthy — reset retry counter
464
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_12')) {
465
+ this.resetRetryCount(syncBlockInstance.resourceId);
466
+ }
348
467
  var callbacks = this.subscriptions.get(syncBlockInstance.resourceId);
349
468
  var localIds = callbacks ? Object.keys(callbacks) : [];
350
469
  localIds.forEach(function (localId) {
351
- var _this6$deps$getFireAn, _syncBlockInstance$da, _syncBlockInstance$da2;
352
- (_this6$deps$getFireAn = _this6.deps.getFireAnalyticsEvent()) === null || _this6$deps$getFireAn === void 0 || _this6$deps$getFireAn((0, _errorHandling.fetchSuccessPayload)(syncBlockInstance.resourceId, localId, (_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)));
470
+ var _this7$deps$getFireAn, _syncBlockInstance$da, _syncBlockInstance$da2;
471
+ (_this7$deps$getFireAn = _this7.deps.getFireAnalyticsEvent()) === null || _this7$deps$getFireAn === void 0 || _this7$deps$getFireAn((0, _errorHandling.fetchSuccessPayload)(syncBlockInstance.resourceId, localId, (_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)));
353
472
  });
354
473
  this.deps.fetchSyncBlockSourceInfo(resolved.resourceId);
355
474
  } else {
356
- var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna, _syncBlockInstance$da3, _syncBlockInstance$da4;
475
+ var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna2, _syncBlockInstance$da3, _syncBlockInstance$da4;
357
476
  var errorMessage = ((_syncBlockInstance$er = syncBlockInstance.error) === null || _syncBlockInstance$er === void 0 ? void 0 : _syncBlockInstance$er.reason) || ((_syncBlockInstance$er2 = syncBlockInstance.error) === null || _syncBlockInstance$er2 === void 0 ? void 0 : _syncBlockInstance$er2.type);
358
- (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 || _this$deps$getFireAna((0, _errorHandling.fetchErrorPayload)(errorMessage, syncBlockInstance.resourceId, (_syncBlockInstance$da3 = (_syncBlockInstance$da4 = syncBlockInstance.data) === null || _syncBlockInstance$da4 === void 0 ? void 0 : _syncBlockInstance$da4.product) !== null && _syncBlockInstance$da3 !== void 0 ? _syncBlockInstance$da3 : (0, _utils.getSourceProductFromResourceIdSafe)(syncBlockInstance.resourceId)));
477
+ (_this$deps$getFireAna2 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna2 === void 0 || _this$deps$getFireAna2((0, _errorHandling.fetchErrorPayload)(errorMessage, syncBlockInstance.resourceId, (_syncBlockInstance$da3 = (_syncBlockInstance$da4 = syncBlockInstance.data) === null || _syncBlockInstance$da4 === void 0 ? void 0 : _syncBlockInstance$da4.product) !== null && _syncBlockInstance$da3 !== void 0 ? _syncBlockInstance$da3 : (0, _utils.getSourceProductFromResourceIdSafe)(syncBlockInstance.resourceId)));
359
478
  }
360
479
  }
361
480
  }]);
362
- }();
481
+ }();
482
+ // Reconnection with exponential backoff
483
+ (0, _defineProperty2.default)(SyncBlockSubscriptionManager, "INITIAL_RETRY_DELAY_MS", 1000);
484
+ (0, _defineProperty2.default)(SyncBlockSubscriptionManager, "RETRY_BACKOFF_MULTIPLIER", 2);
485
+ (0, _defineProperty2.default)(SyncBlockSubscriptionManager, "MAX_RETRY_ATTEMPTS", 5);
@@ -181,7 +181,7 @@ const parseSubscriptionPayload = payload => {
181
181
  * @param onError - Optional callback function invoked on subscription errors
182
182
  * @returns Unsubscribe function to close the subscription
183
183
  */
184
- export const subscribeToBlockUpdates = (blockAri, onData, onError) => {
184
+ export const subscribeToBlockUpdates = (blockAri, onData, onError, onComplete) => {
185
185
  const client = getBlockServiceClient();
186
186
  if (!client) {
187
187
  // Return a no-op unsubscribe if client is not available (e.g., SSR)
@@ -209,7 +209,7 @@ export const subscribeToBlockUpdates = (blockAri, onData, onError) => {
209
209
  onError === null || onError === void 0 ? void 0 : onError(new Error(extractGraphQLWSErrorMessage(error)));
210
210
  },
211
211
  complete: () => {
212
- // Subscription completed
212
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete();
213
213
  }
214
214
  });
215
215
  return unsubscribe;
@@ -533,7 +533,7 @@ class BlockServiceADFFetchProvider {
533
533
  * @param onError - Optional callback function invoked on subscription errors
534
534
  * @returns Unsubscribe function to stop receiving updates
535
535
  */
536
- subscribeToBlockUpdates(resourceId, onUpdate, onError) {
536
+ subscribeToBlockUpdates(resourceId, onUpdate, onError, onComplete) {
537
537
  const blockAri = generateBlockAriFromReference({
538
538
  cloudId: this.cloudId,
539
539
  resourceId
@@ -568,7 +568,7 @@ class BlockServiceADFFetchProvider {
568
568
  resourceId: parsedData.resourceId
569
569
  };
570
570
  onUpdate(syncBlockInstance);
571
- }, onError);
571
+ }, onError, onComplete);
572
572
  }).catch(err => {
573
573
  if (cancelled) {
574
574
  return;
@@ -313,9 +313,9 @@ export class SyncedBlockProvider extends SyncBlockDataProviderInterface {
313
313
  * @param onError - Optional callback function invoked on subscription errors
314
314
  * @returns Unsubscribe function to stop receiving updates, or undefined if not supported
315
315
  */
316
- subscribeToBlockUpdates(resourceId, onUpdate, onError) {
316
+ subscribeToBlockUpdates(resourceId, onUpdate, onError, onComplete) {
317
317
  if (this.fetchProvider.subscribeToBlockUpdates) {
318
- return this.fetchProvider.subscribeToBlockUpdates(resourceId, onUpdate, onError);
318
+ return this.fetchProvider.subscribeToBlockUpdates(resourceId, onUpdate, onError, onComplete);
319
319
  }
320
320
  return undefined;
321
321
  }
@@ -142,6 +142,8 @@ export class ReferenceSyncBlockStoreManager {
142
142
  const syncBlockNode = createSyncBlockNode('', resourceId);
143
143
  const providerData = (_this$dataProvider2 = this.dataProvider) === null || _this$dataProvider2 === void 0 ? void 0 : (_this$dataProvider2$g = _this$dataProvider2.getNodeDataFromCache(syncBlockNode)) === null || _this$dataProvider2$g === void 0 ? void 0 : _this$dataProvider2$g.data;
144
144
  if (providerData) {
145
+ // Initial provider cache data can come from SSR/prefetch and bypass updateCache(),
146
+ // so strip annotations here before references render existing synced block payloads.
145
147
  return this.stripAnnotationMarksFromReferenceData(providerData);
146
148
  }
147
149
  return this.getFromSessionCache(resourceId);
@@ -175,6 +177,8 @@ export class ReferenceSyncBlockStoreManager {
175
177
  if (!raw) {
176
178
  return undefined;
177
179
  }
180
+ // Session cache entries written before this sanitizer existed may still include
181
+ // source annotation marks, so keep this read-time safety net for legacy data.
178
182
  return this.stripAnnotationMarksFromReferenceData(JSON.parse(raw));
179
183
  } catch (error) {
180
184
  logException(error, {
@@ -1,5 +1,6 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { logException } from '@atlaskit/editor-common/monitoring';
3
+ import { fg } from '@atlaskit/platform-feature-flags';
3
4
  import { fetchErrorPayload, fetchSuccessPayload } from '../utils/errorHandling';
4
5
  import { resolveSyncBlockInstance } from '../utils/resolveSyncBlockInstance';
5
6
  import { getSourceProductFromResourceIdSafe } from '../utils/utils';
@@ -21,6 +22,8 @@ export class SyncBlockSubscriptionManager {
21
22
  // causing the cache to be deleted prematurely. We delay deletion to allow
22
23
  // the new component to subscribe and cancel the pending deletion.
23
24
  _defineProperty(this, "pendingCacheDeletions", new Map());
25
+ _defineProperty(this, "retryAttempts", new Map());
26
+ _defineProperty(this, "pendingRetries", new Map());
24
27
  this.deps = deps;
25
28
  }
26
29
 
@@ -213,6 +216,7 @@ export class SyncBlockSubscriptionManager {
213
216
  if (!(dataProvider !== null && dataProvider !== void 0 && dataProvider.subscribeToBlockUpdates)) {
214
217
  return;
215
218
  }
219
+ const reconnectEnabled = fg('platform_synced_block_patch_12');
216
220
  const unsubscribe = dataProvider.subscribeToBlockUpdates(resourceId, syncBlockInstance => {
217
221
  this.handleGraphQLUpdate(syncBlockInstance);
218
222
  }, error => {
@@ -221,17 +225,105 @@ export class SyncBlockSubscriptionManager {
221
225
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/graphql-subscription'
222
226
  });
223
227
  (_this$deps$getFireAna2 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna2 === void 0 ? void 0 : _this$deps$getFireAna2(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
224
- });
228
+ if (reconnectEnabled) {
229
+ this.handleSubscriptionTerminated(resourceId);
230
+ }
231
+ }, reconnectEnabled ? () => {
232
+ this.handleSubscriptionTerminated(resourceId);
233
+ } : undefined);
225
234
  if (unsubscribe) {
226
235
  this.graphqlSubscriptions.set(resourceId, unsubscribe);
227
236
  }
228
237
  }
238
+
239
+ /**
240
+ * Handles subscription termination (complete or error) by cleaning up the
241
+ * stale entry and scheduling a reconnection with exponential backoff.
242
+ */
243
+ handleSubscriptionTerminated(resourceId) {
244
+ // Remove the stale subscription entry so setupSubscription won't early-return
245
+ this.graphqlSubscriptions.delete(resourceId);
246
+
247
+ // Guard against duplicate calls (graphql-ws can fire both error and complete)
248
+ if (this.pendingRetries.has(resourceId)) {
249
+ return;
250
+ }
251
+
252
+ // Only reconnect if there are still active subscribers for this resource
253
+ if (this.subscriptions.has(resourceId) && this.shouldUseRealTime()) {
254
+ this.scheduleReconnection(resourceId);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Schedules a reconnection attempt with exponential backoff.
260
+ * Delay = INITIAL_RETRY_DELAY_MS * (RETRY_BACKOFF_MULTIPLIER ^ attempts)
261
+ * e.g. 1s, 2s, 4s, 8s, 16s
262
+ */
263
+ scheduleReconnection(resourceId) {
264
+ var _this$retryAttempts$g;
265
+ const attempts = (_this$retryAttempts$g = this.retryAttempts.get(resourceId)) !== null && _this$retryAttempts$g !== void 0 ? _this$retryAttempts$g : 0;
266
+ if (attempts >= SyncBlockSubscriptionManager.MAX_RETRY_ATTEMPTS) {
267
+ var _this$deps$getFireAna3;
268
+ const errorMessage = `Subscription reconnection failed after ${attempts} attempts`;
269
+ logException(new Error(errorMessage), {
270
+ location: 'editor-synced-block-provider/syncBlockSubscriptionManager/max-retries-exhausted'
271
+ });
272
+ (_this$deps$getFireAna3 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna3 === void 0 ? void 0 : _this$deps$getFireAna3(fetchErrorPayload(errorMessage, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
273
+ return;
274
+ }
275
+ const delay = SyncBlockSubscriptionManager.INITIAL_RETRY_DELAY_MS * Math.pow(SyncBlockSubscriptionManager.RETRY_BACKOFF_MULTIPLIER, attempts);
276
+ const timer = setTimeout(() => {
277
+ this.pendingRetries.delete(resourceId);
278
+
279
+ // Only re-subscribe if still relevant
280
+ if (this.subscriptions.has(resourceId) && this.shouldUseRealTime()) {
281
+ this.setupSubscription(resourceId);
282
+
283
+ // Only increment if the subscription was actually established
284
+ // (setupSubscription may be a no-op if another code path already re-established it)
285
+ if (this.graphqlSubscriptions.has(resourceId)) {
286
+ this.retryAttempts.set(resourceId, attempts + 1);
287
+ }
288
+ } else {
289
+ // Conditions no longer met — clean up stale retry state
290
+ this.retryAttempts.delete(resourceId);
291
+ }
292
+ }, delay);
293
+ this.pendingRetries.set(resourceId, timer);
294
+ }
295
+
296
+ /**
297
+ * Resets the retry counter for a resource. Called when a successful
298
+ * update is received, indicating the subscription is healthy.
299
+ */
300
+ resetRetryCount(resourceId) {
301
+ this.retryAttempts.delete(resourceId);
302
+ }
303
+ cancelPendingRetry(resourceId) {
304
+ const timer = this.pendingRetries.get(resourceId);
305
+ if (timer) {
306
+ clearTimeout(timer);
307
+ this.pendingRetries.delete(resourceId);
308
+ }
309
+ }
310
+ cancelAllPendingRetries() {
311
+ for (const timer of this.pendingRetries.values()) {
312
+ clearTimeout(timer);
313
+ }
314
+ this.pendingRetries.clear();
315
+ this.retryAttempts.clear();
316
+ }
229
317
  cleanupSubscription(resourceId) {
230
318
  const unsubscribe = this.graphqlSubscriptions.get(resourceId);
231
319
  if (unsubscribe) {
232
320
  unsubscribe();
233
321
  this.graphqlSubscriptions.delete(resourceId);
234
322
  }
323
+ if (fg('platform_synced_block_patch_12')) {
324
+ this.cancelPendingRetry(resourceId);
325
+ this.retryAttempts.delete(resourceId);
326
+ }
235
327
  }
236
328
  setupSubscriptionsForAllBlocks() {
237
329
  for (const resourceId of this.subscriptions.keys()) {
@@ -243,6 +335,9 @@ export class SyncBlockSubscriptionManager {
243
335
  unsubscribe();
244
336
  }
245
337
  this.graphqlSubscriptions.clear();
338
+ if (fg('platform_synced_block_patch_12')) {
339
+ this.cancelAllPendingRetries();
340
+ }
246
341
  }
247
342
  destroy() {
248
343
  this.cleanupAll();
@@ -268,17 +363,25 @@ export class SyncBlockSubscriptionManager {
268
363
  const resolved = existing ? resolveSyncBlockInstance(existing, syncBlockInstance) : syncBlockInstance;
269
364
  this.deps.updateCache(resolved);
270
365
  if (!syncBlockInstance.error) {
366
+ // Successful data delivery means the subscription is healthy — reset retry counter
367
+ if (fg('platform_synced_block_patch_12')) {
368
+ this.resetRetryCount(syncBlockInstance.resourceId);
369
+ }
271
370
  const callbacks = this.subscriptions.get(syncBlockInstance.resourceId);
272
371
  const localIds = callbacks ? Object.keys(callbacks) : [];
273
372
  localIds.forEach(localId => {
274
- var _this$deps$getFireAna3, _syncBlockInstance$da, _syncBlockInstance$da2;
275
- (_this$deps$getFireAna3 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna3 === void 0 ? void 0 : _this$deps$getFireAna3(fetchSuccessPayload(syncBlockInstance.resourceId, localId, (_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)));
373
+ var _this$deps$getFireAna4, _syncBlockInstance$da, _syncBlockInstance$da2;
374
+ (_this$deps$getFireAna4 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna4 === void 0 ? void 0 : _this$deps$getFireAna4(fetchSuccessPayload(syncBlockInstance.resourceId, localId, (_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)));
276
375
  });
277
376
  this.deps.fetchSyncBlockSourceInfo(resolved.resourceId);
278
377
  } else {
279
- var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna4, _syncBlockInstance$da3, _syncBlockInstance$da4;
378
+ var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna5, _syncBlockInstance$da3, _syncBlockInstance$da4;
280
379
  const errorMessage = ((_syncBlockInstance$er = syncBlockInstance.error) === null || _syncBlockInstance$er === void 0 ? void 0 : _syncBlockInstance$er.reason) || ((_syncBlockInstance$er2 = syncBlockInstance.error) === null || _syncBlockInstance$er2 === void 0 ? void 0 : _syncBlockInstance$er2.type);
281
- (_this$deps$getFireAna4 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna4 === void 0 ? void 0 : _this$deps$getFireAna4(fetchErrorPayload(errorMessage, syncBlockInstance.resourceId, (_syncBlockInstance$da3 = (_syncBlockInstance$da4 = syncBlockInstance.data) === null || _syncBlockInstance$da4 === void 0 ? void 0 : _syncBlockInstance$da4.product) !== null && _syncBlockInstance$da3 !== void 0 ? _syncBlockInstance$da3 : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
380
+ (_this$deps$getFireAna5 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna5 === void 0 ? void 0 : _this$deps$getFireAna5(fetchErrorPayload(errorMessage, syncBlockInstance.resourceId, (_syncBlockInstance$da3 = (_syncBlockInstance$da4 = syncBlockInstance.data) === null || _syncBlockInstance$da4 === void 0 ? void 0 : _syncBlockInstance$da4.product) !== null && _syncBlockInstance$da3 !== void 0 ? _syncBlockInstance$da3 : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
282
381
  }
283
382
  }
284
- }
383
+ }
384
+ // Reconnection with exponential backoff
385
+ _defineProperty(SyncBlockSubscriptionManager, "INITIAL_RETRY_DELAY_MS", 1000);
386
+ _defineProperty(SyncBlockSubscriptionManager, "RETRY_BACKOFF_MULTIPLIER", 2);
387
+ _defineProperty(SyncBlockSubscriptionManager, "MAX_RETRY_ATTEMPTS", 5);
@@ -167,7 +167,7 @@ var parseSubscriptionPayload = function parseSubscriptionPayload(payload) {
167
167
  * @param onError - Optional callback function invoked on subscription errors
168
168
  * @returns Unsubscribe function to close the subscription
169
169
  */
170
- export var subscribeToBlockUpdates = function subscribeToBlockUpdates(blockAri, onData, onError) {
170
+ export var subscribeToBlockUpdates = function subscribeToBlockUpdates(blockAri, onData, onError, onComplete) {
171
171
  var client = getBlockServiceClient();
172
172
  if (!client) {
173
173
  // Return a no-op unsubscribe if client is not available (e.g., SSR)
@@ -203,7 +203,7 @@ export var subscribeToBlockUpdates = function subscribeToBlockUpdates(blockAri,
203
203
  onError === null || onError === void 0 || onError(new Error(extractGraphQLWSErrorMessage(error)));
204
204
  }),
205
205
  complete: function complete() {
206
- // Subscription completed
206
+ onComplete === null || onComplete === void 0 || onComplete();
207
207
  }
208
208
  });
209
209
  return unsubscribe;
@@ -786,7 +786,7 @@ var BlockServiceADFFetchProvider = /*#__PURE__*/function () {
786
786
  )
787
787
  }, {
788
788
  key: "subscribeToBlockUpdates",
789
- value: function subscribeToBlockUpdates(resourceId, onUpdate, onError) {
789
+ value: function subscribeToBlockUpdates(resourceId, onUpdate, onError, onComplete) {
790
790
  var blockAri = generateBlockAriFromReference({
791
791
  cloudId: this.cloudId,
792
792
  resourceId: resourceId
@@ -820,7 +820,7 @@ var BlockServiceADFFetchProvider = /*#__PURE__*/function () {
820
820
  resourceId: parsedData.resourceId
821
821
  };
822
822
  onUpdate(syncBlockInstance);
823
- }, onError);
823
+ }, onError, onComplete);
824
824
  }).catch(function (err) {
825
825
  if (cancelled) {
826
826
  return;
@@ -462,9 +462,9 @@ export var SyncedBlockProvider = /*#__PURE__*/function (_SyncBlockDataProvide) {
462
462
  */
463
463
  }, {
464
464
  key: "subscribeToBlockUpdates",
465
- value: function subscribeToBlockUpdates(resourceId, onUpdate, onError) {
465
+ value: function subscribeToBlockUpdates(resourceId, onUpdate, onError, onComplete) {
466
466
  if (this.fetchProvider.subscribeToBlockUpdates) {
467
- return this.fetchProvider.subscribeToBlockUpdates(resourceId, onUpdate, onError);
467
+ return this.fetchProvider.subscribeToBlockUpdates(resourceId, onUpdate, onError, onComplete);
468
468
  }
469
469
  return undefined;
470
470
  }
@@ -204,6 +204,8 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
204
204
  var syncBlockNode = createSyncBlockNode('', resourceId);
205
205
  var providerData = (_this$dataProvider2 = this.dataProvider) === null || _this$dataProvider2 === void 0 || (_this$dataProvider2 = _this$dataProvider2.getNodeDataFromCache(syncBlockNode)) === null || _this$dataProvider2 === void 0 ? void 0 : _this$dataProvider2.data;
206
206
  if (providerData) {
207
+ // Initial provider cache data can come from SSR/prefetch and bypass updateCache(),
208
+ // so strip annotations here before references render existing synced block payloads.
207
209
  return this.stripAnnotationMarksFromReferenceData(providerData);
208
210
  }
209
211
  return this.getFromSessionCache(resourceId);
@@ -241,6 +243,8 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
241
243
  if (!raw) {
242
244
  return undefined;
243
245
  }
246
+ // Session cache entries written before this sanitizer existed may still include
247
+ // source annotation marks, so keep this read-time safety net for legacy data.
244
248
  return this.stripAnnotationMarksFromReferenceData(JSON.parse(raw));
245
249
  } catch (error) {
246
250
  logException(error, {
@@ -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';
@@ -29,6 +30,8 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
29
30
  // causing the cache to be deleted prematurely. We delay deletion to allow
30
31
  // the new component to subscribe and cancel the pending deletion.
31
32
  _defineProperty(this, "pendingCacheDeletions", new Map());
33
+ _defineProperty(this, "retryAttempts", new Map());
34
+ _defineProperty(this, "pendingRetries", new Map());
32
35
  this.deps = deps;
33
36
  }
34
37
 
@@ -243,6 +246,7 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
243
246
  if (!(dataProvider !== null && dataProvider !== void 0 && dataProvider.subscribeToBlockUpdates)) {
244
247
  return;
245
248
  }
249
+ var reconnectEnabled = fg('platform_synced_block_patch_12');
246
250
  var unsubscribe = dataProvider.subscribeToBlockUpdates(resourceId, function (syncBlockInstance) {
247
251
  _this5.handleGraphQLUpdate(syncBlockInstance);
248
252
  }, function (error) {
@@ -251,11 +255,115 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
251
255
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/graphql-subscription'
252
256
  });
253
257
  (_this5$deps$getFireAn = _this5.deps.getFireAnalyticsEvent()) === null || _this5$deps$getFireAn === void 0 || _this5$deps$getFireAn(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
254
- });
258
+ if (reconnectEnabled) {
259
+ _this5.handleSubscriptionTerminated(resourceId);
260
+ }
261
+ }, reconnectEnabled ? function () {
262
+ _this5.handleSubscriptionTerminated(resourceId);
263
+ } : undefined);
255
264
  if (unsubscribe) {
256
265
  this.graphqlSubscriptions.set(resourceId, unsubscribe);
257
266
  }
258
267
  }
268
+
269
+ /**
270
+ * Handles subscription termination (complete or error) by cleaning up the
271
+ * stale entry and scheduling a reconnection with exponential backoff.
272
+ */
273
+ }, {
274
+ key: "handleSubscriptionTerminated",
275
+ value: function handleSubscriptionTerminated(resourceId) {
276
+ // Remove the stale subscription entry so setupSubscription won't early-return
277
+ this.graphqlSubscriptions.delete(resourceId);
278
+
279
+ // Guard against duplicate calls (graphql-ws can fire both error and complete)
280
+ if (this.pendingRetries.has(resourceId)) {
281
+ return;
282
+ }
283
+
284
+ // Only reconnect if there are still active subscribers for this resource
285
+ if (this.subscriptions.has(resourceId) && this.shouldUseRealTime()) {
286
+ this.scheduleReconnection(resourceId);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Schedules a reconnection attempt with exponential backoff.
292
+ * Delay = INITIAL_RETRY_DELAY_MS * (RETRY_BACKOFF_MULTIPLIER ^ attempts)
293
+ * e.g. 1s, 2s, 4s, 8s, 16s
294
+ */
295
+ }, {
296
+ key: "scheduleReconnection",
297
+ value: function scheduleReconnection(resourceId) {
298
+ var _this$retryAttempts$g,
299
+ _this6 = this;
300
+ var attempts = (_this$retryAttempts$g = this.retryAttempts.get(resourceId)) !== null && _this$retryAttempts$g !== void 0 ? _this$retryAttempts$g : 0;
301
+ if (attempts >= SyncBlockSubscriptionManager.MAX_RETRY_ATTEMPTS) {
302
+ var _this$deps$getFireAna;
303
+ var errorMessage = "Subscription reconnection failed after ".concat(attempts, " attempts");
304
+ logException(new Error(errorMessage), {
305
+ location: 'editor-synced-block-provider/syncBlockSubscriptionManager/max-retries-exhausted'
306
+ });
307
+ (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 || _this$deps$getFireAna(fetchErrorPayload(errorMessage, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
308
+ return;
309
+ }
310
+ var delay = SyncBlockSubscriptionManager.INITIAL_RETRY_DELAY_MS * Math.pow(SyncBlockSubscriptionManager.RETRY_BACKOFF_MULTIPLIER, attempts);
311
+ var timer = setTimeout(function () {
312
+ _this6.pendingRetries.delete(resourceId);
313
+
314
+ // Only re-subscribe if still relevant
315
+ if (_this6.subscriptions.has(resourceId) && _this6.shouldUseRealTime()) {
316
+ _this6.setupSubscription(resourceId);
317
+
318
+ // Only increment if the subscription was actually established
319
+ // (setupSubscription may be a no-op if another code path already re-established it)
320
+ if (_this6.graphqlSubscriptions.has(resourceId)) {
321
+ _this6.retryAttempts.set(resourceId, attempts + 1);
322
+ }
323
+ } else {
324
+ // Conditions no longer met — clean up stale retry state
325
+ _this6.retryAttempts.delete(resourceId);
326
+ }
327
+ }, delay);
328
+ this.pendingRetries.set(resourceId, timer);
329
+ }
330
+
331
+ /**
332
+ * Resets the retry counter for a resource. Called when a successful
333
+ * update is received, indicating the subscription is healthy.
334
+ */
335
+ }, {
336
+ key: "resetRetryCount",
337
+ value: function resetRetryCount(resourceId) {
338
+ this.retryAttempts.delete(resourceId);
339
+ }
340
+ }, {
341
+ key: "cancelPendingRetry",
342
+ value: function cancelPendingRetry(resourceId) {
343
+ var timer = this.pendingRetries.get(resourceId);
344
+ if (timer) {
345
+ clearTimeout(timer);
346
+ this.pendingRetries.delete(resourceId);
347
+ }
348
+ }
349
+ }, {
350
+ key: "cancelAllPendingRetries",
351
+ value: function cancelAllPendingRetries() {
352
+ var _iterator = _createForOfIteratorHelper(this.pendingRetries.values()),
353
+ _step;
354
+ try {
355
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
356
+ var timer = _step.value;
357
+ clearTimeout(timer);
358
+ }
359
+ } catch (err) {
360
+ _iterator.e(err);
361
+ } finally {
362
+ _iterator.f();
363
+ }
364
+ this.pendingRetries.clear();
365
+ this.retryAttempts.clear();
366
+ }
259
367
  }, {
260
368
  key: "cleanupSubscription",
261
369
  value: function cleanupSubscription(resourceId) {
@@ -264,39 +372,46 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
264
372
  unsubscribe();
265
373
  this.graphqlSubscriptions.delete(resourceId);
266
374
  }
375
+ if (fg('platform_synced_block_patch_12')) {
376
+ this.cancelPendingRetry(resourceId);
377
+ this.retryAttempts.delete(resourceId);
378
+ }
267
379
  }
268
380
  }, {
269
381
  key: "setupSubscriptionsForAllBlocks",
270
382
  value: function setupSubscriptionsForAllBlocks() {
271
- var _iterator = _createForOfIteratorHelper(this.subscriptions.keys()),
272
- _step;
383
+ var _iterator2 = _createForOfIteratorHelper(this.subscriptions.keys()),
384
+ _step2;
273
385
  try {
274
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
275
- var resourceId = _step.value;
386
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
387
+ var resourceId = _step2.value;
276
388
  this.setupSubscription(resourceId);
277
389
  }
278
390
  } catch (err) {
279
- _iterator.e(err);
391
+ _iterator2.e(err);
280
392
  } finally {
281
- _iterator.f();
393
+ _iterator2.f();
282
394
  }
283
395
  }
284
396
  }, {
285
397
  key: "cleanupAll",
286
398
  value: function cleanupAll() {
287
- var _iterator2 = _createForOfIteratorHelper(this.graphqlSubscriptions.values()),
288
- _step2;
399
+ var _iterator3 = _createForOfIteratorHelper(this.graphqlSubscriptions.values()),
400
+ _step3;
289
401
  try {
290
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
291
- var unsubscribe = _step2.value;
402
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
403
+ var unsubscribe = _step3.value;
292
404
  unsubscribe();
293
405
  }
294
406
  } catch (err) {
295
- _iterator2.e(err);
407
+ _iterator3.e(err);
296
408
  } finally {
297
- _iterator2.f();
409
+ _iterator3.f();
298
410
  }
299
411
  this.graphqlSubscriptions.clear();
412
+ if (fg('platform_synced_block_patch_12')) {
413
+ this.cancelAllPendingRetries();
414
+ }
300
415
  }
301
416
  }, {
302
417
  key: "destroy",
@@ -308,17 +423,17 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
308
423
  this.useRealTimeSubscriptions = false;
309
424
 
310
425
  // Clear any pending cache deletions
311
- var _iterator3 = _createForOfIteratorHelper(this.pendingCacheDeletions.values()),
312
- _step3;
426
+ var _iterator4 = _createForOfIteratorHelper(this.pendingCacheDeletions.values()),
427
+ _step4;
313
428
  try {
314
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
315
- var timeout = _step3.value;
429
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
430
+ var timeout = _step4.value;
316
431
  clearTimeout(timeout);
317
432
  }
318
433
  } catch (err) {
319
- _iterator3.e(err);
434
+ _iterator4.e(err);
320
435
  } finally {
321
- _iterator3.f();
436
+ _iterator4.f();
322
437
  }
323
438
  this.pendingCacheDeletions.clear();
324
439
  }
@@ -330,7 +445,7 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
330
445
  }, {
331
446
  key: "handleGraphQLUpdate",
332
447
  value: function handleGraphQLUpdate(syncBlockInstance) {
333
- var _this6 = this;
448
+ var _this7 = this;
334
449
  if (!syncBlockInstance.resourceId) {
335
450
  return;
336
451
  }
@@ -338,18 +453,26 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
338
453
  var resolved = existing ? resolveSyncBlockInstance(existing, syncBlockInstance) : syncBlockInstance;
339
454
  this.deps.updateCache(resolved);
340
455
  if (!syncBlockInstance.error) {
456
+ // Successful data delivery means the subscription is healthy — reset retry counter
457
+ if (fg('platform_synced_block_patch_12')) {
458
+ this.resetRetryCount(syncBlockInstance.resourceId);
459
+ }
341
460
  var callbacks = this.subscriptions.get(syncBlockInstance.resourceId);
342
461
  var localIds = callbacks ? Object.keys(callbacks) : [];
343
462
  localIds.forEach(function (localId) {
344
- var _this6$deps$getFireAn, _syncBlockInstance$da, _syncBlockInstance$da2;
345
- (_this6$deps$getFireAn = _this6.deps.getFireAnalyticsEvent()) === null || _this6$deps$getFireAn === void 0 || _this6$deps$getFireAn(fetchSuccessPayload(syncBlockInstance.resourceId, localId, (_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)));
463
+ var _this7$deps$getFireAn, _syncBlockInstance$da, _syncBlockInstance$da2;
464
+ (_this7$deps$getFireAn = _this7.deps.getFireAnalyticsEvent()) === null || _this7$deps$getFireAn === void 0 || _this7$deps$getFireAn(fetchSuccessPayload(syncBlockInstance.resourceId, localId, (_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)));
346
465
  });
347
466
  this.deps.fetchSyncBlockSourceInfo(resolved.resourceId);
348
467
  } else {
349
- var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna, _syncBlockInstance$da3, _syncBlockInstance$da4;
468
+ var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna2, _syncBlockInstance$da3, _syncBlockInstance$da4;
350
469
  var errorMessage = ((_syncBlockInstance$er = syncBlockInstance.error) === null || _syncBlockInstance$er === void 0 ? void 0 : _syncBlockInstance$er.reason) || ((_syncBlockInstance$er2 = syncBlockInstance.error) === null || _syncBlockInstance$er2 === void 0 ? void 0 : _syncBlockInstance$er2.type);
351
- (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 || _this$deps$getFireAna(fetchErrorPayload(errorMessage, syncBlockInstance.resourceId, (_syncBlockInstance$da3 = (_syncBlockInstance$da4 = syncBlockInstance.data) === null || _syncBlockInstance$da4 === void 0 ? void 0 : _syncBlockInstance$da4.product) !== null && _syncBlockInstance$da3 !== void 0 ? _syncBlockInstance$da3 : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
470
+ (_this$deps$getFireAna2 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna2 === void 0 || _this$deps$getFireAna2(fetchErrorPayload(errorMessage, syncBlockInstance.resourceId, (_syncBlockInstance$da3 = (_syncBlockInstance$da4 = syncBlockInstance.data) === null || _syncBlockInstance$da4 === void 0 ? void 0 : _syncBlockInstance$da4.product) !== null && _syncBlockInstance$da3 !== void 0 ? _syncBlockInstance$da3 : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
352
471
  }
353
472
  }
354
473
  }]);
355
- }();
474
+ }();
475
+ // Reconnection with exponential backoff
476
+ _defineProperty(SyncBlockSubscriptionManager, "INITIAL_RETRY_DELAY_MS", 1000);
477
+ _defineProperty(SyncBlockSubscriptionManager, "RETRY_BACKOFF_MULTIPLIER", 2);
478
+ _defineProperty(SyncBlockSubscriptionManager, "MAX_RETRY_ATTEMPTS", 5);
@@ -31,6 +31,7 @@ export type ParsedBlockSubscriptionData = {
31
31
  };
32
32
  type SubscriptionCallback = (data: ParsedBlockSubscriptionData) => void;
33
33
  type ErrorCallback = (error: Error) => void;
34
+ type CompleteCallback = () => void;
34
35
  type Unsubscribe = () => void;
35
36
  /**
36
37
  * Extracts a meaningful error message from the error: GraphQL WebSocket Error: {"isTrusted":true}
@@ -47,5 +48,5 @@ export declare const extractGraphQLWSErrorMessage: (error: unknown) => string;
47
48
  * @param onError - Optional callback function invoked on subscription errors
48
49
  * @returns Unsubscribe function to close the subscription
49
50
  */
50
- export declare const subscribeToBlockUpdates: (blockAri: string, onData: SubscriptionCallback, onError?: ErrorCallback) => Unsubscribe;
51
+ export declare const subscribeToBlockUpdates: (blockAri: string, onData: SubscriptionCallback, onError?: ErrorCallback, onComplete?: CompleteCallback) => Unsubscribe;
51
52
  export {};
@@ -1 +1 @@
1
- export type { ADFFetchProvider, ADFWriteProvider, BatchFetchConfig, BlockNodeIdentifiers, BlockSubscriptionErrorCallback, BlockUpdateCallback, SyncBlockDataProviderInterface, SyncBlockInstance, MediaEmojiProviderOptions, SyncedBlockRendererProviderOptions, SyncBlockRendererProviderCreator, SyncedBlockRendererDataProviders, Unsubscribe, UpdateReferenceSyncBlockResult, WriteSyncBlockResult, SyncBlockParentInfo, SyncBlockSourceInfo, SyncBlockJiraIssueType, } from '../providers/types';
1
+ export type { ADFFetchProvider, ADFWriteProvider, BatchFetchConfig, BlockNodeIdentifiers, BlockSubscriptionCompleteCallback, BlockSubscriptionErrorCallback, BlockUpdateCallback, SyncBlockDataProviderInterface, SyncBlockInstance, MediaEmojiProviderOptions, SyncedBlockRendererProviderOptions, SyncBlockRendererProviderCreator, SyncedBlockRendererDataProviders, Unsubscribe, UpdateReferenceSyncBlockResult, WriteSyncBlockResult, SyncBlockParentInfo, SyncBlockSourceInfo, SyncBlockJiraIssueType, } from '../providers/types';
@@ -73,7 +73,7 @@ declare class BlockServiceADFFetchProvider implements ADFFetchProvider {
73
73
  * @param onError - Optional callback function invoked on subscription errors
74
74
  * @returns Unsubscribe function to stop receiving updates
75
75
  */
76
- subscribeToBlockUpdates(resourceId: ResourceId, onUpdate: (data: SyncBlockInstance) => void, onError?: (error: Error) => void): () => void;
76
+ subscribeToBlockUpdates(resourceId: ResourceId, onUpdate: (data: SyncBlockInstance) => void, onError?: (error: Error) => void, onComplete?: () => void): () => void;
77
77
  }
78
78
  interface BlockServiceADFWriteProviderProps {
79
79
  cloudId: string;
@@ -98,7 +98,7 @@ export declare class SyncedBlockProvider extends SyncBlockDataProviderInterface
98
98
  * @param onError - Optional callback function invoked on subscription errors
99
99
  * @returns Unsubscribe function to stop receiving updates, or undefined if not supported
100
100
  */
101
- subscribeToBlockUpdates(resourceId: string, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback): Unsubscribe | undefined;
101
+ subscribeToBlockUpdates(resourceId: string, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback, onComplete?: () => void): Unsubscribe | undefined;
102
102
  }
103
103
  type UseMemoizedSyncedBlockProviderProps = {
104
104
  fetchProvider: ADFFetchProvider;
@@ -92,6 +92,7 @@ export type BatchFetchConfig = {
92
92
  };
93
93
  export type BlockUpdateCallback = (data: SyncBlockInstance) => void;
94
94
  export type BlockSubscriptionErrorCallback = (error: Error) => void;
95
+ export type BlockSubscriptionCompleteCallback = () => void;
95
96
  export type Unsubscribe = () => void;
96
97
  export interface ADFFetchProvider {
97
98
  batchFetchData: (blockNodeIdentifiers: BlockNodeIdentifiers[], config?: BatchFetchConfig) => Promise<SyncBlockInstance[]>;
@@ -100,7 +101,7 @@ export interface ADFFetchProvider {
100
101
  /**
101
102
  * Subscribes to real-time updates for a specific block.
102
103
  */
103
- subscribeToBlockUpdates?: (resourceId: ResourceId, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback) => Unsubscribe;
104
+ subscribeToBlockUpdates?: (resourceId: ResourceId, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback, onComplete?: BlockSubscriptionCompleteCallback) => Unsubscribe;
104
105
  }
105
106
  export interface ADFWriteProvider {
106
107
  createData: (data: SyncBlockData) => Promise<WriteSyncBlockResult>;
@@ -174,9 +175,10 @@ export declare abstract class SyncBlockDataProviderInterface extends NodeDataPro
174
175
  * @param resourceId - The resource ID of the block to subscribe to
175
176
  * @param onUpdate - Callback function invoked when the block is updated
176
177
  * @param onError - Optional callback function invoked on subscription errors
178
+ * @param onComplete - Optional callback function invoked when the subscription completes
177
179
  * @returns Unsubscribe function to stop receiving updates, or undefined if not supported
178
180
  */
179
- subscribeToBlockUpdates?(resourceId: ResourceId, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback): Unsubscribe | undefined;
181
+ subscribeToBlockUpdates?(resourceId: ResourceId, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback, onComplete?: BlockSubscriptionCompleteCallback): Unsubscribe | undefined;
180
182
  }
181
183
  export type SubscriptionCallback = (data: SyncBlockInstance) => void;
182
184
  export type TitleSubscriptionCallback = (title: string) => void;
@@ -26,6 +26,11 @@ export declare class SyncBlockSubscriptionManager {
26
26
  private subscriptionChangeListeners;
27
27
  private useRealTimeSubscriptions;
28
28
  private pendingCacheDeletions;
29
+ private static readonly INITIAL_RETRY_DELAY_MS;
30
+ private static readonly RETRY_BACKOFF_MULTIPLIER;
31
+ private static readonly MAX_RETRY_ATTEMPTS;
32
+ private retryAttempts;
33
+ private pendingRetries;
29
34
  constructor(deps: SyncBlockSubscriptionManagerDeps);
30
35
  /**
31
36
  * Returns the subscriptions map. Used by external consumers (e.g. batch fetcher, flush)
@@ -48,6 +53,24 @@ export declare class SyncBlockSubscriptionManager {
48
53
  */
49
54
  notifySubscriptionCallbacks(resourceId: ResourceId, syncBlock: SyncBlockInstance): void;
50
55
  setupSubscription(resourceId: ResourceId): void;
56
+ /**
57
+ * Handles subscription termination (complete or error) by cleaning up the
58
+ * stale entry and scheduling a reconnection with exponential backoff.
59
+ */
60
+ private handleSubscriptionTerminated;
61
+ /**
62
+ * Schedules a reconnection attempt with exponential backoff.
63
+ * Delay = INITIAL_RETRY_DELAY_MS * (RETRY_BACKOFF_MULTIPLIER ^ attempts)
64
+ * e.g. 1s, 2s, 4s, 8s, 16s
65
+ */
66
+ private scheduleReconnection;
67
+ /**
68
+ * Resets the retry counter for a resource. Called when a successful
69
+ * update is received, indicating the subscription is healthy.
70
+ */
71
+ private resetRetryCount;
72
+ private cancelPendingRetry;
73
+ private cancelAllPendingRetries;
51
74
  cleanupSubscription(resourceId: ResourceId): void;
52
75
  setupSubscriptionsForAllBlocks(): void;
53
76
  cleanupAll(): void;
@@ -31,6 +31,7 @@ export type ParsedBlockSubscriptionData = {
31
31
  };
32
32
  type SubscriptionCallback = (data: ParsedBlockSubscriptionData) => void;
33
33
  type ErrorCallback = (error: Error) => void;
34
+ type CompleteCallback = () => void;
34
35
  type Unsubscribe = () => void;
35
36
  /**
36
37
  * Extracts a meaningful error message from the error: GraphQL WebSocket Error: {"isTrusted":true}
@@ -47,5 +48,5 @@ export declare const extractGraphQLWSErrorMessage: (error: unknown) => string;
47
48
  * @param onError - Optional callback function invoked on subscription errors
48
49
  * @returns Unsubscribe function to close the subscription
49
50
  */
50
- export declare const subscribeToBlockUpdates: (blockAri: string, onData: SubscriptionCallback, onError?: ErrorCallback) => Unsubscribe;
51
+ export declare const subscribeToBlockUpdates: (blockAri: string, onData: SubscriptionCallback, onError?: ErrorCallback, onComplete?: CompleteCallback) => Unsubscribe;
51
52
  export {};
@@ -1 +1 @@
1
- export type { ADFFetchProvider, ADFWriteProvider, BatchFetchConfig, BlockNodeIdentifiers, BlockSubscriptionErrorCallback, BlockUpdateCallback, SyncBlockDataProviderInterface, SyncBlockInstance, MediaEmojiProviderOptions, SyncedBlockRendererProviderOptions, SyncBlockRendererProviderCreator, SyncedBlockRendererDataProviders, Unsubscribe, UpdateReferenceSyncBlockResult, WriteSyncBlockResult, SyncBlockParentInfo, SyncBlockSourceInfo, SyncBlockJiraIssueType, } from '../providers/types';
1
+ export type { ADFFetchProvider, ADFWriteProvider, BatchFetchConfig, BlockNodeIdentifiers, BlockSubscriptionCompleteCallback, BlockSubscriptionErrorCallback, BlockUpdateCallback, SyncBlockDataProviderInterface, SyncBlockInstance, MediaEmojiProviderOptions, SyncedBlockRendererProviderOptions, SyncBlockRendererProviderCreator, SyncedBlockRendererDataProviders, Unsubscribe, UpdateReferenceSyncBlockResult, WriteSyncBlockResult, SyncBlockParentInfo, SyncBlockSourceInfo, SyncBlockJiraIssueType, } from '../providers/types';
@@ -73,7 +73,7 @@ declare class BlockServiceADFFetchProvider implements ADFFetchProvider {
73
73
  * @param onError - Optional callback function invoked on subscription errors
74
74
  * @returns Unsubscribe function to stop receiving updates
75
75
  */
76
- subscribeToBlockUpdates(resourceId: ResourceId, onUpdate: (data: SyncBlockInstance) => void, onError?: (error: Error) => void): () => void;
76
+ subscribeToBlockUpdates(resourceId: ResourceId, onUpdate: (data: SyncBlockInstance) => void, onError?: (error: Error) => void, onComplete?: () => void): () => void;
77
77
  }
78
78
  interface BlockServiceADFWriteProviderProps {
79
79
  cloudId: string;
@@ -98,7 +98,7 @@ export declare class SyncedBlockProvider extends SyncBlockDataProviderInterface
98
98
  * @param onError - Optional callback function invoked on subscription errors
99
99
  * @returns Unsubscribe function to stop receiving updates, or undefined if not supported
100
100
  */
101
- subscribeToBlockUpdates(resourceId: string, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback): Unsubscribe | undefined;
101
+ subscribeToBlockUpdates(resourceId: string, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback, onComplete?: () => void): Unsubscribe | undefined;
102
102
  }
103
103
  type UseMemoizedSyncedBlockProviderProps = {
104
104
  fetchProvider: ADFFetchProvider;
@@ -92,6 +92,7 @@ export type BatchFetchConfig = {
92
92
  };
93
93
  export type BlockUpdateCallback = (data: SyncBlockInstance) => void;
94
94
  export type BlockSubscriptionErrorCallback = (error: Error) => void;
95
+ export type BlockSubscriptionCompleteCallback = () => void;
95
96
  export type Unsubscribe = () => void;
96
97
  export interface ADFFetchProvider {
97
98
  batchFetchData: (blockNodeIdentifiers: BlockNodeIdentifiers[], config?: BatchFetchConfig) => Promise<SyncBlockInstance[]>;
@@ -100,7 +101,7 @@ export interface ADFFetchProvider {
100
101
  /**
101
102
  * Subscribes to real-time updates for a specific block.
102
103
  */
103
- subscribeToBlockUpdates?: (resourceId: ResourceId, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback) => Unsubscribe;
104
+ subscribeToBlockUpdates?: (resourceId: ResourceId, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback, onComplete?: BlockSubscriptionCompleteCallback) => Unsubscribe;
104
105
  }
105
106
  export interface ADFWriteProvider {
106
107
  createData: (data: SyncBlockData) => Promise<WriteSyncBlockResult>;
@@ -174,9 +175,10 @@ export declare abstract class SyncBlockDataProviderInterface extends NodeDataPro
174
175
  * @param resourceId - The resource ID of the block to subscribe to
175
176
  * @param onUpdate - Callback function invoked when the block is updated
176
177
  * @param onError - Optional callback function invoked on subscription errors
178
+ * @param onComplete - Optional callback function invoked when the subscription completes
177
179
  * @returns Unsubscribe function to stop receiving updates, or undefined if not supported
178
180
  */
179
- subscribeToBlockUpdates?(resourceId: ResourceId, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback): Unsubscribe | undefined;
181
+ subscribeToBlockUpdates?(resourceId: ResourceId, onUpdate: BlockUpdateCallback, onError?: BlockSubscriptionErrorCallback, onComplete?: BlockSubscriptionCompleteCallback): Unsubscribe | undefined;
180
182
  }
181
183
  export type SubscriptionCallback = (data: SyncBlockInstance) => void;
182
184
  export type TitleSubscriptionCallback = (title: string) => void;
@@ -26,6 +26,11 @@ export declare class SyncBlockSubscriptionManager {
26
26
  private subscriptionChangeListeners;
27
27
  private useRealTimeSubscriptions;
28
28
  private pendingCacheDeletions;
29
+ private static readonly INITIAL_RETRY_DELAY_MS;
30
+ private static readonly RETRY_BACKOFF_MULTIPLIER;
31
+ private static readonly MAX_RETRY_ATTEMPTS;
32
+ private retryAttempts;
33
+ private pendingRetries;
29
34
  constructor(deps: SyncBlockSubscriptionManagerDeps);
30
35
  /**
31
36
  * Returns the subscriptions map. Used by external consumers (e.g. batch fetcher, flush)
@@ -48,6 +53,24 @@ export declare class SyncBlockSubscriptionManager {
48
53
  */
49
54
  notifySubscriptionCallbacks(resourceId: ResourceId, syncBlock: SyncBlockInstance): void;
50
55
  setupSubscription(resourceId: ResourceId): void;
56
+ /**
57
+ * Handles subscription termination (complete or error) by cleaning up the
58
+ * stale entry and scheduling a reconnection with exponential backoff.
59
+ */
60
+ private handleSubscriptionTerminated;
61
+ /**
62
+ * Schedules a reconnection attempt with exponential backoff.
63
+ * Delay = INITIAL_RETRY_DELAY_MS * (RETRY_BACKOFF_MULTIPLIER ^ attempts)
64
+ * e.g. 1s, 2s, 4s, 8s, 16s
65
+ */
66
+ private scheduleReconnection;
67
+ /**
68
+ * Resets the retry counter for a resource. Called when a successful
69
+ * update is received, indicating the subscription is healthy.
70
+ */
71
+ private resetRetryCount;
72
+ private cancelPendingRetry;
73
+ private cancelAllPendingRetries;
51
74
  cleanupSubscription(resourceId: ResourceId): void;
52
75
  setupSubscriptionsForAllBlocks(): void;
53
76
  cleanupAll(): void;
package/package.json CHANGED
@@ -24,12 +24,12 @@
24
24
  ],
25
25
  "atlaskit:src": "src/index.ts",
26
26
  "dependencies": {
27
- "@atlaskit/adf-utils": "^19.30.0",
27
+ "@atlaskit/adf-utils": "^19.31.0",
28
28
  "@atlaskit/editor-json-transformer": "^8.32.0",
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": "^82.0.0",
32
+ "@atlaskit/tmp-editor-statsig": "^83.0.0",
33
33
  "@babel/runtime": "^7.0.0",
34
34
  "@compiled/react": "^0.20.0",
35
35
  "graphql-ws": "^5.14.2",
@@ -38,11 +38,12 @@
38
38
  "uuid": "^3.1.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@atlaskit/editor-common": "^114.37.0",
41
+ "@atlaskit/editor-common": "^114.46.0",
42
42
  "react": "^18.2.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@testing-library/react": "^16.3.0",
46
+ "react": "^18.2.0",
46
47
  "react-dom": "^18.2.0"
47
48
  },
48
49
  "techstack": {
@@ -81,7 +82,7 @@
81
82
  }
82
83
  },
83
84
  "name": "@atlaskit/editor-synced-block-provider",
84
- "version": "6.6.6",
85
+ "version": "6.6.8",
85
86
  "description": "Synced Block Provider for @atlaskit/editor-plugin-synced-block",
86
87
  "author": "Atlassian Pty Ltd",
87
88
  "license": "Apache-2.0",