@atlaskit/editor-synced-block-provider 8.6.0 → 8.6.2

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @atlaskit/editor-synced-block-provider
2
2
 
3
+ ## 8.6.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 8.6.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [`f543995529d9d`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/f543995529d9d) -
14
+ [ux] Synced blocks now recover automatically from realtime connection drops that happen while a
15
+ tab is hidden or the network is offline. Instead of showing a fetch error when reconnection
16
+ attempts are exhausted in the background, the block silently re-arms and reconnects when the tab
17
+ becomes visible again or the network comes back online.
18
+ - Updated dependencies
19
+
3
20
  ## 8.6.0
4
21
 
5
22
  ### Minor Changes
@@ -1020,7 +1020,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1020
1020
  _context4.next = 1;
1021
1021
  break;
1022
1022
  }
1023
- return _context4.abrupt("return", (0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_2'));
1023
+ return _context4.abrupt("return", true);
1024
1024
  case 1:
1025
1025
  if (this.isCacheDirty) {
1026
1026
  _context4.next = 2;
@@ -8,6 +8,8 @@ exports.SyncBlockSubscriptionManager = void 0;
8
8
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
9
9
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
10
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
+ var _bindEventListener = require("bind-event-listener");
12
+ var _browserApis = require("@atlaskit/browser-apis");
11
13
  var _monitoring = require("@atlaskit/editor-common/monitoring");
12
14
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
13
15
  var _errorHandling = require("../utils/errorHandling");
@@ -26,6 +28,7 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
26
28
  */
27
29
  var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__PURE__*/function () {
28
30
  function SyncBlockSubscriptionManager(deps) {
31
+ var _this = this;
29
32
  (0, _classCallCheck2.default)(this, SyncBlockSubscriptionManager);
30
33
  (0, _defineProperty2.default)(this, "subscriptions", new Map());
31
34
  (0, _defineProperty2.default)(this, "titleSubscriptions", new Map());
@@ -40,6 +43,24 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
40
43
  // backoff cap (gate ON)
41
44
  (0, _defineProperty2.default)(this, "retryAttempts", new Map());
42
45
  (0, _defineProperty2.default)(this, "pendingRetries", new Map());
46
+ // Resources whose reconnection exhausted while the tab was hidden: parked here and
47
+ // re-armed on the next online / visibilitychange→visible instead of surfacing a
48
+ // terminal error. Only a re-armed attempt that also exhausts while visible fails.
49
+ (0, _defineProperty2.default)(this, "deferredExhausted", new Set());
50
+ (0, _defineProperty2.default)(this, "wakeDebounceTimer", null);
51
+ // `online` always sweeps (network is back regardless of tab visibility); a
52
+ // `visibilitychange` only sweeps when the tab became visible.
53
+ (0, _defineProperty2.default)(this, "onOnline", function () {
54
+ return _this.scheduleWakeSweep();
55
+ });
56
+ (0, _defineProperty2.default)(this, "onVisibilityChange", function () {
57
+ if (!_this.isDocumentHidden()) {
58
+ _this.scheduleWakeSweep();
59
+ }
60
+ });
61
+ // Cleanup fns returned by `bind` for the registered wake listeners; a non-empty
62
+ // array means listeners are currently registered. Emptied on teardown.
63
+ (0, _defineProperty2.default)(this, "wakeListenerCleanups", []);
43
64
  this.deps = deps;
44
65
  }
45
66
 
@@ -102,25 +123,25 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
102
123
  }, {
103
124
  key: "onSubscriptionsChanged",
104
125
  value: function onSubscriptionsChanged(listener) {
105
- var _this = this;
126
+ var _this2 = this;
106
127
  this.subscriptionChangeListeners.add(listener);
107
128
  return function () {
108
- _this.subscriptionChangeListeners.delete(listener);
129
+ _this2.subscriptionChangeListeners.delete(listener);
109
130
  };
110
131
  }
111
132
  }, {
112
133
  key: "notifySubscriptionChangeListeners",
113
134
  value: function notifySubscriptionChangeListeners() {
114
- var _this2 = this;
135
+ var _this3 = this;
115
136
  this.subscriptionChangeListeners.forEach(function (listener) {
116
137
  try {
117
138
  listener();
118
139
  } catch (error) {
119
- var _this2$deps$getFireAn;
140
+ var _this3$deps$getFireAn;
120
141
  (0, _monitoring.logException)(error, {
121
142
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/notifySubscriptionChangeListeners'
122
143
  });
123
- (_this2$deps$getFireAn = _this2.deps.getFireAnalyticsEvent()) === null || _this2$deps$getFireAn === void 0 || _this2$deps$getFireAn((0, _errorHandling.fetchErrorPayload)(error.message, undefined, undefined, (0, _errorHandling.buildFetchErrorAttribution)((0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_3'), error.message)));
144
+ (_this3$deps$getFireAn = _this3.deps.getFireAnalyticsEvent()) === null || _this3$deps$getFireAn === void 0 || _this3$deps$getFireAn((0, _errorHandling.fetchErrorPayload)(error.message, undefined, undefined, (0, _errorHandling.buildFetchErrorAttribution)((0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_3'), error.message)));
124
145
  }
125
146
  });
126
147
  }
@@ -140,7 +161,7 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
140
161
  }, {
141
162
  key: "subscribeToSyncBlock",
142
163
  value: function subscribeToSyncBlock(resourceId, localId, callback) {
143
- var _this3 = this;
164
+ var _this4 = this;
144
165
  // Cancel any pending cache deletion for this resourceId.
145
166
  // This handles the case where a block is moved - the old component unmounts
146
167
  // (scheduling deletion) but the new component mounts and subscribes before
@@ -180,25 +201,25 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
180
201
  this.setupSubscription(resourceId);
181
202
  }
182
203
  return function () {
183
- var resourceSubscriptions = _this3.subscriptions.get(resourceId);
204
+ var resourceSubscriptions = _this4.subscriptions.get(resourceId);
184
205
  if (resourceSubscriptions) {
185
206
  // Unsubscription means a reference synced block is removed from the document
186
- _this3.deps.markCacheDirty();
207
+ _this4.deps.markCacheDirty();
187
208
  delete resourceSubscriptions[localId];
188
209
  var remainingIds = Object.keys(resourceSubscriptions);
189
210
  if (remainingIds.length === 0) {
190
- _this3.subscriptions.delete(resourceId);
211
+ _this4.subscriptions.delete(resourceId);
191
212
 
192
213
  // Clean up GraphQL subscription when no more local subscribers
193
- _this3.cleanupSubscription(resourceId);
214
+ _this4.cleanupSubscription(resourceId);
194
215
 
195
216
  // Notify listeners that subscription was removed
196
- _this3.notifySubscriptionChangeListeners();
217
+ _this4.notifySubscriptionChangeListeners();
197
218
 
198
219
  // Under the flag, delegate cache deletion to the store manager
199
220
  // which uses a 30s grace period with guard re-checks.
200
221
  if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_14')) {
201
- _this3.deps.scheduleCacheDeletion(resourceId);
222
+ _this4.deps.scheduleCacheDeletion(resourceId);
202
223
  } else {
203
224
  // Legacy path (unchanged): delay cache deletion to handle
204
225
  // block moves (unmount/remount). When a block is moved, the
@@ -209,18 +230,18 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
209
230
  // TODO: EDITOR-4152 - Rework this logic (superseded by
210
231
  // `platform_synced_block_patch_14`).
211
232
  var deletionTimeout = setTimeout(function () {
212
- var hasSubscribers = _this3.subscriptions.has(resourceId);
233
+ var hasSubscribers = _this4.subscriptions.has(resourceId);
213
234
 
214
235
  // Only delete if still no subscribers (wasn't re-subscribed)
215
236
  if (!hasSubscribers) {
216
- _this3.deps.deleteFromCache(resourceId);
237
+ _this4.deps.deleteFromCache(resourceId);
217
238
  }
218
- _this3.pendingCacheDeletions.delete(resourceId);
239
+ _this4.pendingCacheDeletions.delete(resourceId);
219
240
  }, 1000);
220
- _this3.pendingCacheDeletions.set(resourceId, deletionTimeout);
241
+ _this4.pendingCacheDeletions.set(resourceId, deletionTimeout);
221
242
  }
222
243
  } else {
223
- _this3.subscriptions.set(resourceId, resourceSubscriptions);
244
+ _this4.subscriptions.set(resourceId, resourceSubscriptions);
224
245
  }
225
246
  }
226
247
  };
@@ -229,7 +250,7 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
229
250
  key: "subscribeToSourceTitle",
230
251
  value: function subscribeToSourceTitle(node, callback) {
231
252
  var _cachedData$data,
232
- _this4 = this;
253
+ _this5 = this;
233
254
  // check node is a sync block, as we only support sync block subscriptions
234
255
  if (node.type.name !== 'syncBlock') {
235
256
  return function () {};
@@ -249,13 +270,13 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
249
270
  var resourceSubscriptions = this.titleSubscriptions.get(resourceId) || {};
250
271
  this.titleSubscriptions.set(resourceId, _objectSpread(_objectSpread({}, resourceSubscriptions), {}, (0, _defineProperty2.default)({}, localId, callback)));
251
272
  return function () {
252
- var resourceSubscriptions = _this4.titleSubscriptions.get(resourceId);
273
+ var resourceSubscriptions = _this5.titleSubscriptions.get(resourceId);
253
274
  if (resourceSubscriptions) {
254
275
  delete resourceSubscriptions[localId];
255
276
  if (Object.keys(resourceSubscriptions).length === 0) {
256
- _this4.titleSubscriptions.delete(resourceId);
277
+ _this5.titleSubscriptions.delete(resourceId);
257
278
  } else {
258
- _this4.titleSubscriptions.set(resourceId, resourceSubscriptions);
279
+ _this5.titleSubscriptions.set(resourceId, resourceSubscriptions);
259
280
  }
260
281
  }
261
282
  };
@@ -287,7 +308,7 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
287
308
  }, {
288
309
  key: "setupSubscription",
289
310
  value: function setupSubscription(resourceId) {
290
- var _this5 = this;
311
+ var _this6 = this;
291
312
  if (this.graphqlSubscriptions.has(resourceId)) {
292
313
  return;
293
314
  }
@@ -296,7 +317,7 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
296
317
  return;
297
318
  }
298
319
  var unsubscribe = dataProvider.subscribeToBlockUpdates(resourceId, function (syncBlockInstance) {
299
- _this5.handleGraphQLUpdate(syncBlockInstance);
320
+ _this6.handleGraphQLUpdate(syncBlockInstance);
300
321
  }, function (error) {
301
322
  (0, _monitoring.logException)(error, {
302
323
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/graphql-subscription'
@@ -309,12 +330,12 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
309
330
  // would return undefined; the structured attribution (EDITOR-7862) is therefore
310
331
  // applied at the gate-ON exhaustion site in scheduleReconnection instead.
311
332
  if (!(0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_3')) {
312
- var _this5$deps$getFireAn;
313
- (_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)));
333
+ var _this6$deps$getFireAn;
334
+ (_this6$deps$getFireAn = _this6.deps.getFireAnalyticsEvent()) === null || _this6$deps$getFireAn === void 0 || _this6$deps$getFireAn((0, _errorHandling.fetchErrorPayload)(error.message, resourceId, (0, _utils.getSourceProductFromResourceIdSafe)(resourceId)));
314
335
  }
315
- _this5.handleSubscriptionTerminated(resourceId);
336
+ _this6.handleSubscriptionTerminated(resourceId);
316
337
  }, function () {
317
- _this5.handleSubscriptionTerminated(resourceId);
338
+ _this6.handleSubscriptionTerminated(resourceId);
318
339
  });
319
340
  if (unsubscribe) {
320
341
  this.graphqlSubscriptions.set(resourceId, unsubscribe);
@@ -348,36 +369,48 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
348
369
  key: "scheduleReconnection",
349
370
  value: function scheduleReconnection(resourceId) {
350
371
  var _this$retryAttempts$g,
351
- _this6 = this;
372
+ _this7 = this;
352
373
  var attempts = (_this$retryAttempts$g = this.retryAttempts.get(resourceId)) !== null && _this$retryAttempts$g !== void 0 ? _this$retryAttempts$g : 0;
353
374
  var maxAttempts = this.getMaxRetryAttempts();
354
375
  if (attempts >= maxAttempts) {
355
- var _this$deps$getFireAna;
376
+ var _this$deps$getFireAna2;
356
377
  // Exhausted all attempts — the only place a WS drop surfaces as a
357
378
  // fetch error under the gate (EDITOR-7861).
358
379
  var errorMessage = "Subscription reconnection failed after ".concat(attempts, " attempts");
380
+
381
+ // Tab hidden at exhaustion: don't surface a terminal failure (user isn't
382
+ // looking, and most exhaustions self-recover once foregrounded). Park + re-arm
383
+ // on wake, emitting a benign `deferred` signal so suppression stays auditable.
384
+ var shouldDefer = (0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_3') && this.isDocumentHidden();
385
+ if (shouldDefer) {
386
+ var _this$deps$getFireAna;
387
+ this.deferredExhausted.add(resourceId);
388
+ this.registerWakeListeners();
389
+ (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 || _this$deps$getFireAna((0, _errorHandling.fetchErrorPayload)(errorMessage, resourceId, (0, _utils.getSourceProductFromResourceIdSafe)(resourceId), (0, _errorHandling.buildFetchErrorAttribution)(true, errorMessage, undefined, /* deferred */true)));
390
+ return;
391
+ }
359
392
  (0, _monitoring.logException)(new Error(errorMessage), {
360
393
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/max-retries-exhausted'
361
394
  });
362
- (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 || _this$deps$getFireAna((0, _errorHandling.fetchErrorPayload)(errorMessage, resourceId, (0, _utils.getSourceProductFromResourceIdSafe)(resourceId), (0, _errorHandling.buildFetchErrorAttribution)((0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_3'), errorMessage)));
395
+ (_this$deps$getFireAna2 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna2 === void 0 || _this$deps$getFireAna2((0, _errorHandling.fetchErrorPayload)(errorMessage, resourceId, (0, _utils.getSourceProductFromResourceIdSafe)(resourceId), (0, _errorHandling.buildFetchErrorAttribution)((0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_3'), errorMessage)));
363
396
  return;
364
397
  }
365
398
  var delay = this.getReconnectionDelay(attempts);
366
399
  var timer = setTimeout(function () {
367
- _this6.pendingRetries.delete(resourceId);
400
+ _this7.pendingRetries.delete(resourceId);
368
401
 
369
402
  // Only re-subscribe if still relevant
370
- if (_this6.subscriptions.has(resourceId) && _this6.shouldUseRealTime()) {
371
- _this6.setupSubscription(resourceId);
403
+ if (_this7.subscriptions.has(resourceId) && _this7.shouldUseRealTime()) {
404
+ _this7.setupSubscription(resourceId);
372
405
 
373
406
  // Only increment if the subscription was actually established
374
407
  // (setupSubscription may be a no-op if another code path already re-established it)
375
- if (_this6.graphqlSubscriptions.has(resourceId)) {
376
- _this6.retryAttempts.set(resourceId, attempts + 1);
408
+ if (_this7.graphqlSubscriptions.has(resourceId)) {
409
+ _this7.retryAttempts.set(resourceId, attempts + 1);
377
410
  }
378
411
  } else {
379
412
  // Conditions no longer met — clean up stale retry state
380
- _this6.retryAttempts.delete(resourceId);
413
+ _this7.retryAttempts.delete(resourceId);
381
414
  }
382
415
  }, delay);
383
416
  this.pendingRetries.set(resourceId, timer);
@@ -392,6 +425,106 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
392
425
  value: function resetRetryCount(resourceId) {
393
426
  this.retryAttempts.delete(resourceId);
394
427
  }
428
+
429
+ /**
430
+ * Whether the tab is currently hidden. SSR/non-browser is treated as "not hidden" so
431
+ * we never defer where wake events can't fire (realtime never subscribes there anyway).
432
+ */
433
+ }, {
434
+ key: "isDocumentHidden",
435
+ value: function isDocumentHidden() {
436
+ var _getDocument;
437
+ return ((_getDocument = (0, _browserApis.getDocument)()) === null || _getDocument === void 0 ? void 0 : _getDocument.hidden) === true;
438
+ }
439
+
440
+ /**
441
+ * Lazily register shared `online` / `visibilitychange` listeners on first deferral.
442
+ * Torn down by `unregisterWakeListeners` (via `handleWake`). No-op in SSR / non-browser.
443
+ */
444
+ }, {
445
+ key: "registerWakeListeners",
446
+ value: function registerWakeListeners() {
447
+ if (this.wakeListenerCleanups.length > 0 || typeof window === 'undefined') {
448
+ return;
449
+ }
450
+ this.wakeListenerCleanups.push((0, _bindEventListener.bind)(window, {
451
+ type: 'online',
452
+ listener: this.onOnline
453
+ }));
454
+ var doc = (0, _browserApis.getDocument)();
455
+ if (doc) {
456
+ this.wakeListenerCleanups.push((0, _bindEventListener.bind)(doc, {
457
+ type: 'visibilitychange',
458
+ listener: this.onVisibilityChange
459
+ }));
460
+ }
461
+ }
462
+ }, {
463
+ key: "unregisterWakeListeners",
464
+ value: function unregisterWakeListeners() {
465
+ if (this.wakeDebounceTimer !== null) {
466
+ clearTimeout(this.wakeDebounceTimer);
467
+ this.wakeDebounceTimer = null;
468
+ }
469
+ var _iterator = _createForOfIteratorHelper(this.wakeListenerCleanups),
470
+ _step;
471
+ try {
472
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
473
+ var cleanup = _step.value;
474
+ cleanup();
475
+ }
476
+ } catch (err) {
477
+ _iterator.e(err);
478
+ } finally {
479
+ _iterator.f();
480
+ }
481
+ this.wakeListenerCleanups = [];
482
+ }
483
+
484
+ /**
485
+ * Coalesce a burst of wake events into one debounced re-arm sweep. Callers decide
486
+ * eligibility: `onOnline` always sweeps; `onVisibilityChange` only sweeps when visible.
487
+ */
488
+ }, {
489
+ key: "scheduleWakeSweep",
490
+ value: function scheduleWakeSweep() {
491
+ var _this8 = this;
492
+ if (this.wakeDebounceTimer !== null) {
493
+ return;
494
+ }
495
+ this.wakeDebounceTimer = setTimeout(function () {
496
+ _this8.wakeDebounceTimer = null;
497
+ _this8.handleWake();
498
+ }, SyncBlockSubscriptionManager.WAKE_DEBOUNCE_MS);
499
+ }
500
+
501
+ /**
502
+ * Re-arm every deferred resource: full reset (attempts→0) + immediate reconnect now
503
+ * the tab is visible / online. Healthy subscriptions are untouched. A re-armed cycle
504
+ * that later exhausts while visible fires the terminal error normally.
505
+ */
506
+ }, {
507
+ key: "handleWake",
508
+ value: function handleWake() {
509
+ if (this.deferredExhausted.size === 0) {
510
+ this.unregisterWakeListeners();
511
+ return;
512
+ }
513
+ var toRearm = Array.from(this.deferredExhausted);
514
+ this.deferredExhausted.clear();
515
+ for (var _i = 0, _toRearm = toRearm; _i < _toRearm.length; _i++) {
516
+ var resourceId = _toRearm[_i];
517
+ // Only re-arm resources that still have active subscribers and realtime on.
518
+ if (!this.subscriptions.has(resourceId) || !this.shouldUseRealTime()) {
519
+ continue;
520
+ }
521
+ this.resetRetryCount(resourceId);
522
+ this.cancelPendingRetry(resourceId);
523
+ this.setupSubscription(resourceId);
524
+ }
525
+ // No deferred resources remain until another hidden exhaustion re-adds one.
526
+ this.unregisterWakeListeners();
527
+ }
395
528
  }, {
396
529
  key: "cancelPendingRetry",
397
530
  value: function cancelPendingRetry(resourceId) {
@@ -404,20 +537,23 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
404
537
  }, {
405
538
  key: "cancelAllPendingRetries",
406
539
  value: function cancelAllPendingRetries() {
407
- var _iterator = _createForOfIteratorHelper(this.pendingRetries.values()),
408
- _step;
540
+ var _iterator2 = _createForOfIteratorHelper(this.pendingRetries.values()),
541
+ _step2;
409
542
  try {
410
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
411
- var timer = _step.value;
543
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
544
+ var timer = _step2.value;
412
545
  clearTimeout(timer);
413
546
  }
414
547
  } catch (err) {
415
- _iterator.e(err);
548
+ _iterator2.e(err);
416
549
  } finally {
417
- _iterator.f();
550
+ _iterator2.f();
418
551
  }
419
552
  this.pendingRetries.clear();
420
553
  this.retryAttempts.clear();
554
+ // Nothing left to re-arm: drop parked exhaustions and tear down wake listeners.
555
+ this.deferredExhausted.clear();
556
+ this.unregisterWakeListeners();
421
557
  }
422
558
  }, {
423
559
  key: "cleanupSubscription",
@@ -429,37 +565,42 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
429
565
  }
430
566
  this.cancelPendingRetry(resourceId);
431
567
  this.retryAttempts.delete(resourceId);
568
+ // No subscribers left, so this resource can't be re-armed.
569
+ this.deferredExhausted.delete(resourceId);
570
+ if (this.deferredExhausted.size === 0) {
571
+ this.unregisterWakeListeners();
572
+ }
432
573
  }
433
574
  }, {
434
575
  key: "setupSubscriptionsForAllBlocks",
435
576
  value: function setupSubscriptionsForAllBlocks() {
436
- var _iterator2 = _createForOfIteratorHelper(this.subscriptions.keys()),
437
- _step2;
577
+ var _iterator3 = _createForOfIteratorHelper(this.subscriptions.keys()),
578
+ _step3;
438
579
  try {
439
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
440
- var resourceId = _step2.value;
580
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
581
+ var resourceId = _step3.value;
441
582
  this.setupSubscription(resourceId);
442
583
  }
443
584
  } catch (err) {
444
- _iterator2.e(err);
585
+ _iterator3.e(err);
445
586
  } finally {
446
- _iterator2.f();
587
+ _iterator3.f();
447
588
  }
448
589
  }
449
590
  }, {
450
591
  key: "cleanupAll",
451
592
  value: function cleanupAll() {
452
- var _iterator3 = _createForOfIteratorHelper(this.graphqlSubscriptions.values()),
453
- _step3;
593
+ var _iterator4 = _createForOfIteratorHelper(this.graphqlSubscriptions.values()),
594
+ _step4;
454
595
  try {
455
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
456
- var unsubscribe = _step3.value;
596
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
597
+ var unsubscribe = _step4.value;
457
598
  unsubscribe();
458
599
  }
459
600
  } catch (err) {
460
- _iterator3.e(err);
601
+ _iterator4.e(err);
461
602
  } finally {
462
- _iterator3.f();
603
+ _iterator4.f();
463
604
  }
464
605
  this.graphqlSubscriptions.clear();
465
606
  this.cancelAllPendingRetries();
@@ -474,17 +615,17 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
474
615
  this.useRealTimeSubscriptions = false;
475
616
 
476
617
  // Clear any pending cache deletions
477
- var _iterator4 = _createForOfIteratorHelper(this.pendingCacheDeletions.values()),
478
- _step4;
618
+ var _iterator5 = _createForOfIteratorHelper(this.pendingCacheDeletions.values()),
619
+ _step5;
479
620
  try {
480
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
481
- var timeout = _step4.value;
621
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
622
+ var timeout = _step5.value;
482
623
  clearTimeout(timeout);
483
624
  }
484
625
  } catch (err) {
485
- _iterator4.e(err);
626
+ _iterator5.e(err);
486
627
  } finally {
487
- _iterator4.f();
628
+ _iterator5.f();
488
629
  }
489
630
  this.pendingCacheDeletions.clear();
490
631
  }
@@ -496,7 +637,7 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
496
637
  }, {
497
638
  key: "handleGraphQLUpdate",
498
639
  value: function handleGraphQLUpdate(syncBlockInstance) {
499
- var _this7 = this;
640
+ var _this9 = this;
500
641
  if (!syncBlockInstance.resourceId) {
501
642
  return;
502
643
  }
@@ -505,21 +646,28 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
505
646
  this.deps.updateCache(resolved);
506
647
  if (!syncBlockInstance.error) {
507
648
  this.resetRetryCount(syncBlockInstance.resourceId);
649
+ // A healthy update means a parked exhaustion recovered on its own before any
650
+ // wake sweep — drop it and tear down listeners if nothing else is parked.
651
+ if (this.deferredExhausted.delete(syncBlockInstance.resourceId)) {
652
+ if (this.deferredExhausted.size === 0) {
653
+ this.unregisterWakeListeners();
654
+ }
655
+ }
508
656
  var callbacks = this.subscriptions.get(syncBlockInstance.resourceId);
509
657
  var localIds = callbacks ? Object.keys(callbacks) : [];
510
658
  localIds.forEach(function (localId) {
511
- var _this7$deps$getFireAn, _syncBlockInstance$da, _syncBlockInstance$da2;
512
- (_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)));
659
+ var _this9$deps$getFireAn, _syncBlockInstance$da, _syncBlockInstance$da2;
660
+ (_this9$deps$getFireAn = _this9.deps.getFireAnalyticsEvent()) === null || _this9$deps$getFireAn === void 0 || _this9$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)));
513
661
  });
514
662
  this.deps.fetchSyncBlockSourceInfo(resolved.resourceId);
515
663
  } else {
516
- var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna2, _syncBlockInstance$da3, _syncBlockInstance$da4, _syncBlockInstance$er3, _syncBlockInstance$er4, _syncBlockInstance$er5;
664
+ var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna3, _syncBlockInstance$da3, _syncBlockInstance$da4, _syncBlockInstance$er3, _syncBlockInstance$er4, _syncBlockInstance$er5;
517
665
  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);
518
666
 
519
667
  // Prefer the structured `type` (a `SyncBlockError` enum value) for classification
520
668
  // and fall back to the free-text `reason` so source-state/permission strings are
521
669
  // still bucketed (EDITOR-7862). The emitted free-text `error` attribute is unchanged.
522
- (_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), (0, _errorHandling.buildFetchErrorAttribution)((0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_3'), ((_syncBlockInstance$er3 = syncBlockInstance.error) === null || _syncBlockInstance$er3 === void 0 ? void 0 : _syncBlockInstance$er3.type) || ((_syncBlockInstance$er4 = syncBlockInstance.error) === null || _syncBlockInstance$er4 === void 0 ? void 0 : _syncBlockInstance$er4.reason), (_syncBlockInstance$er5 = syncBlockInstance.error) === null || _syncBlockInstance$er5 === void 0 ? void 0 : _syncBlockInstance$er5.statusCode)));
670
+ (_this$deps$getFireAna3 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna3 === void 0 || _this$deps$getFireAna3((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), (0, _errorHandling.buildFetchErrorAttribution)((0, _platformFeatureFlags.fg)('platform_editor_blocks_patch_3'), ((_syncBlockInstance$er3 = syncBlockInstance.error) === null || _syncBlockInstance$er3 === void 0 ? void 0 : _syncBlockInstance$er3.type) || ((_syncBlockInstance$er4 = syncBlockInstance.error) === null || _syncBlockInstance$er4 === void 0 ? void 0 : _syncBlockInstance$er4.reason), (_syncBlockInstance$er5 = syncBlockInstance.error) === null || _syncBlockInstance$er5 === void 0 ? void 0 : _syncBlockInstance$er5.statusCode)));
523
671
  }
524
672
  }
525
673
  }]);
@@ -531,4 +679,6 @@ var SyncBlockSubscriptionManager = exports.SyncBlockSubscriptionManager = /*#__P
531
679
  // legacy (gate OFF)
532
680
  (0, _defineProperty2.default)(SyncBlockSubscriptionManager, "MAX_RETRY_ATTEMPTS_HARDENED", 8);
533
681
  // gate ON (EDITOR-7861)
534
- (0, _defineProperty2.default)(SyncBlockSubscriptionManager, "MAX_RETRY_DELAY_MS", 30000);
682
+ (0, _defineProperty2.default)(SyncBlockSubscriptionManager, "MAX_RETRY_DELAY_MS", 30000);
683
+ // Coalesce wake-event bursts into one re-arm sweep to avoid a reconnection storm.
684
+ (0, _defineProperty2.default)(SyncBlockSubscriptionManager, "WAKE_DEBOUNCE_MS", 1000);
@@ -202,16 +202,19 @@ var classifyFetchErrorReason = exports.classifyFetchErrorReason = function class
202
202
  * `gateEnabled` is injected by the caller (the store managers evaluate `fg(...)`) so this
203
203
  * helper stays pure and trivially unit-testable for both gate states.
204
204
  */
205
- var buildFetchErrorAttribution = exports.buildFetchErrorAttribution = function buildFetchErrorAttribution(gateEnabled, error, statusCode) {
205
+ var buildFetchErrorAttribution = exports.buildFetchErrorAttribution = function buildFetchErrorAttribution(gateEnabled, error, statusCode, deferred) {
206
206
  if (!gateEnabled) {
207
207
  return undefined;
208
208
  }
209
209
  var reason = classifyFetchErrorReason(error);
210
- return _objectSpread({
210
+ return _objectSpread(_objectSpread({
211
211
  reason: reason,
212
- benign: FETCH_BENIGN_REASONS.has(reason)
212
+ // A deferred (tab-hidden) exhaustion is benign regardless of reason bucket.
213
+ benign: deferred === true || FETCH_BENIGN_REASONS.has(reason)
213
214
  }, statusCode !== undefined && {
214
215
  statusCode: statusCode
216
+ }), deferred === true && {
217
+ deferred: true
215
218
  });
216
219
  };
217
220
 
@@ -239,7 +242,7 @@ function getErrorPayload(actionSubjectId, error, resourceId, sourceProduct, attr
239
242
  actionSubject: _analytics.ACTION_SUBJECT.SYNCED_BLOCK,
240
243
  actionSubjectId: actionSubjectId,
241
244
  eventType: _analytics.EVENT_TYPE.OPERATIONAL,
242
- attributes: _objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread({
245
+ attributes: _objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread({
243
246
  error: error
244
247
  }, resourceId && {
245
248
  resourceId: resourceId
@@ -251,6 +254,8 @@ function getErrorPayload(actionSubjectId, error, resourceId, sourceProduct, attr
251
254
  statusCode: attribution.statusCode
252
255
  }), attribution && 'benign' in attribution && {
253
256
  benign: attribution.benign
257
+ }), attribution && 'deferred' in attribution && attribution.deferred === true && {
258
+ deferred: true
254
259
  })
255
260
  };
256
261
  }
@@ -841,7 +841,7 @@ export class ReferenceSyncBlockStoreManager {
841
841
  if (this.viewMode === 'view') {
842
842
  // Reference flushes are only meaningful while editing. Treat view-mode flush attempts as
843
843
  // gated no-op successes so lifecycle/teardown calls do not report false save failures.
844
- return fg('platform_editor_blocks_patch_2');
844
+ return true;
845
845
  }
846
846
  if (!this.isCacheDirty) {
847
847
  // we use the isCacheDirty flag as a quick check.