@atlaskit/editor-synced-block-provider 8.5.7 → 8.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,8 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
6
6
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
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
+ import { bind } from 'bind-event-listener';
10
+ import { getDocument } from '@atlaskit/browser-apis';
9
11
  import { logException } from '@atlaskit/editor-common/monitoring';
10
12
  import { fg } from '@atlaskit/platform-feature-flags';
11
13
  import { buildFetchErrorAttribution, fetchErrorPayload, fetchSuccessPayload } from '../utils/errorHandling';
@@ -20,6 +22,7 @@ import { getSourceProductFromResourceIdSafe } from '../utils/utils';
20
22
 
21
23
  export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
22
24
  function SyncBlockSubscriptionManager(deps) {
25
+ var _this = this;
23
26
  _classCallCheck(this, SyncBlockSubscriptionManager);
24
27
  _defineProperty(this, "subscriptions", new Map());
25
28
  _defineProperty(this, "titleSubscriptions", new Map());
@@ -34,6 +37,24 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
34
37
  // backoff cap (gate ON)
35
38
  _defineProperty(this, "retryAttempts", new Map());
36
39
  _defineProperty(this, "pendingRetries", new Map());
40
+ // Resources whose reconnection exhausted while the tab was hidden: parked here and
41
+ // re-armed on the next online / visibilitychange→visible instead of surfacing a
42
+ // terminal error. Only a re-armed attempt that also exhausts while visible fails.
43
+ _defineProperty(this, "deferredExhausted", new Set());
44
+ _defineProperty(this, "wakeDebounceTimer", null);
45
+ // `online` always sweeps (network is back regardless of tab visibility); a
46
+ // `visibilitychange` only sweeps when the tab became visible.
47
+ _defineProperty(this, "onOnline", function () {
48
+ return _this.scheduleWakeSweep();
49
+ });
50
+ _defineProperty(this, "onVisibilityChange", function () {
51
+ if (!_this.isDocumentHidden()) {
52
+ _this.scheduleWakeSweep();
53
+ }
54
+ });
55
+ // Cleanup fns returned by `bind` for the registered wake listeners; a non-empty
56
+ // array means listeners are currently registered. Emptied on teardown.
57
+ _defineProperty(this, "wakeListenerCleanups", []);
37
58
  this.deps = deps;
38
59
  }
39
60
 
@@ -96,25 +117,25 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
96
117
  }, {
97
118
  key: "onSubscriptionsChanged",
98
119
  value: function onSubscriptionsChanged(listener) {
99
- var _this = this;
120
+ var _this2 = this;
100
121
  this.subscriptionChangeListeners.add(listener);
101
122
  return function () {
102
- _this.subscriptionChangeListeners.delete(listener);
123
+ _this2.subscriptionChangeListeners.delete(listener);
103
124
  };
104
125
  }
105
126
  }, {
106
127
  key: "notifySubscriptionChangeListeners",
107
128
  value: function notifySubscriptionChangeListeners() {
108
- var _this2 = this;
129
+ var _this3 = this;
109
130
  this.subscriptionChangeListeners.forEach(function (listener) {
110
131
  try {
111
132
  listener();
112
133
  } catch (error) {
113
- var _this2$deps$getFireAn;
134
+ var _this3$deps$getFireAn;
114
135
  logException(error, {
115
136
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/notifySubscriptionChangeListeners'
116
137
  });
117
- (_this2$deps$getFireAn = _this2.deps.getFireAnalyticsEvent()) === null || _this2$deps$getFireAn === void 0 || _this2$deps$getFireAn(fetchErrorPayload(error.message, undefined, undefined, buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), error.message)));
138
+ (_this3$deps$getFireAn = _this3.deps.getFireAnalyticsEvent()) === null || _this3$deps$getFireAn === void 0 || _this3$deps$getFireAn(fetchErrorPayload(error.message, undefined, undefined, buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), error.message)));
118
139
  }
119
140
  });
120
141
  }
@@ -134,7 +155,7 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
134
155
  }, {
135
156
  key: "subscribeToSyncBlock",
136
157
  value: function subscribeToSyncBlock(resourceId, localId, callback) {
137
- var _this3 = this;
158
+ var _this4 = this;
138
159
  // Cancel any pending cache deletion for this resourceId.
139
160
  // This handles the case where a block is moved - the old component unmounts
140
161
  // (scheduling deletion) but the new component mounts and subscribes before
@@ -174,25 +195,25 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
174
195
  this.setupSubscription(resourceId);
175
196
  }
176
197
  return function () {
177
- var resourceSubscriptions = _this3.subscriptions.get(resourceId);
198
+ var resourceSubscriptions = _this4.subscriptions.get(resourceId);
178
199
  if (resourceSubscriptions) {
179
200
  // Unsubscription means a reference synced block is removed from the document
180
- _this3.deps.markCacheDirty();
201
+ _this4.deps.markCacheDirty();
181
202
  delete resourceSubscriptions[localId];
182
203
  var remainingIds = Object.keys(resourceSubscriptions);
183
204
  if (remainingIds.length === 0) {
184
- _this3.subscriptions.delete(resourceId);
205
+ _this4.subscriptions.delete(resourceId);
185
206
 
186
207
  // Clean up GraphQL subscription when no more local subscribers
187
- _this3.cleanupSubscription(resourceId);
208
+ _this4.cleanupSubscription(resourceId);
188
209
 
189
210
  // Notify listeners that subscription was removed
190
- _this3.notifySubscriptionChangeListeners();
211
+ _this4.notifySubscriptionChangeListeners();
191
212
 
192
213
  // Under the flag, delegate cache deletion to the store manager
193
214
  // which uses a 30s grace period with guard re-checks.
194
215
  if (fg('platform_synced_block_patch_14')) {
195
- _this3.deps.scheduleCacheDeletion(resourceId);
216
+ _this4.deps.scheduleCacheDeletion(resourceId);
196
217
  } else {
197
218
  // Legacy path (unchanged): delay cache deletion to handle
198
219
  // block moves (unmount/remount). When a block is moved, the
@@ -203,18 +224,18 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
203
224
  // TODO: EDITOR-4152 - Rework this logic (superseded by
204
225
  // `platform_synced_block_patch_14`).
205
226
  var deletionTimeout = setTimeout(function () {
206
- var hasSubscribers = _this3.subscriptions.has(resourceId);
227
+ var hasSubscribers = _this4.subscriptions.has(resourceId);
207
228
 
208
229
  // Only delete if still no subscribers (wasn't re-subscribed)
209
230
  if (!hasSubscribers) {
210
- _this3.deps.deleteFromCache(resourceId);
231
+ _this4.deps.deleteFromCache(resourceId);
211
232
  }
212
- _this3.pendingCacheDeletions.delete(resourceId);
233
+ _this4.pendingCacheDeletions.delete(resourceId);
213
234
  }, 1000);
214
- _this3.pendingCacheDeletions.set(resourceId, deletionTimeout);
235
+ _this4.pendingCacheDeletions.set(resourceId, deletionTimeout);
215
236
  }
216
237
  } else {
217
- _this3.subscriptions.set(resourceId, resourceSubscriptions);
238
+ _this4.subscriptions.set(resourceId, resourceSubscriptions);
218
239
  }
219
240
  }
220
241
  };
@@ -223,7 +244,7 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
223
244
  key: "subscribeToSourceTitle",
224
245
  value: function subscribeToSourceTitle(node, callback) {
225
246
  var _cachedData$data,
226
- _this4 = this;
247
+ _this5 = this;
227
248
  // check node is a sync block, as we only support sync block subscriptions
228
249
  if (node.type.name !== 'syncBlock') {
229
250
  return function () {};
@@ -243,13 +264,13 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
243
264
  var resourceSubscriptions = this.titleSubscriptions.get(resourceId) || {};
244
265
  this.titleSubscriptions.set(resourceId, _objectSpread(_objectSpread({}, resourceSubscriptions), {}, _defineProperty({}, localId, callback)));
245
266
  return function () {
246
- var resourceSubscriptions = _this4.titleSubscriptions.get(resourceId);
267
+ var resourceSubscriptions = _this5.titleSubscriptions.get(resourceId);
247
268
  if (resourceSubscriptions) {
248
269
  delete resourceSubscriptions[localId];
249
270
  if (Object.keys(resourceSubscriptions).length === 0) {
250
- _this4.titleSubscriptions.delete(resourceId);
271
+ _this5.titleSubscriptions.delete(resourceId);
251
272
  } else {
252
- _this4.titleSubscriptions.set(resourceId, resourceSubscriptions);
273
+ _this5.titleSubscriptions.set(resourceId, resourceSubscriptions);
253
274
  }
254
275
  }
255
276
  };
@@ -281,7 +302,7 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
281
302
  }, {
282
303
  key: "setupSubscription",
283
304
  value: function setupSubscription(resourceId) {
284
- var _this5 = this;
305
+ var _this6 = this;
285
306
  if (this.graphqlSubscriptions.has(resourceId)) {
286
307
  return;
287
308
  }
@@ -290,7 +311,7 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
290
311
  return;
291
312
  }
292
313
  var unsubscribe = dataProvider.subscribeToBlockUpdates(resourceId, function (syncBlockInstance) {
293
- _this5.handleGraphQLUpdate(syncBlockInstance);
314
+ _this6.handleGraphQLUpdate(syncBlockInstance);
294
315
  }, function (error) {
295
316
  logException(error, {
296
317
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/graphql-subscription'
@@ -303,12 +324,12 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
303
324
  // would return undefined; the structured attribution (EDITOR-7862) is therefore
304
325
  // applied at the gate-ON exhaustion site in scheduleReconnection instead.
305
326
  if (!fg('platform_editor_blocks_patch_3')) {
306
- var _this5$deps$getFireAn;
307
- (_this5$deps$getFireAn = _this5.deps.getFireAnalyticsEvent()) === null || _this5$deps$getFireAn === void 0 || _this5$deps$getFireAn(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
327
+ var _this6$deps$getFireAn;
328
+ (_this6$deps$getFireAn = _this6.deps.getFireAnalyticsEvent()) === null || _this6$deps$getFireAn === void 0 || _this6$deps$getFireAn(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
308
329
  }
309
- _this5.handleSubscriptionTerminated(resourceId);
330
+ _this6.handleSubscriptionTerminated(resourceId);
310
331
  }, function () {
311
- _this5.handleSubscriptionTerminated(resourceId);
332
+ _this6.handleSubscriptionTerminated(resourceId);
312
333
  });
313
334
  if (unsubscribe) {
314
335
  this.graphqlSubscriptions.set(resourceId, unsubscribe);
@@ -342,36 +363,48 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
342
363
  key: "scheduleReconnection",
343
364
  value: function scheduleReconnection(resourceId) {
344
365
  var _this$retryAttempts$g,
345
- _this6 = this;
366
+ _this7 = this;
346
367
  var attempts = (_this$retryAttempts$g = this.retryAttempts.get(resourceId)) !== null && _this$retryAttempts$g !== void 0 ? _this$retryAttempts$g : 0;
347
368
  var maxAttempts = this.getMaxRetryAttempts();
348
369
  if (attempts >= maxAttempts) {
349
- var _this$deps$getFireAna;
370
+ var _this$deps$getFireAna2;
350
371
  // Exhausted all attempts — the only place a WS drop surfaces as a
351
372
  // fetch error under the gate (EDITOR-7861).
352
373
  var errorMessage = "Subscription reconnection failed after ".concat(attempts, " attempts");
374
+
375
+ // Tab hidden at exhaustion: don't surface a terminal failure (user isn't
376
+ // looking, and most exhaustions self-recover once foregrounded). Park + re-arm
377
+ // on wake, emitting a benign `deferred` signal so suppression stays auditable.
378
+ var shouldDefer = fg('platform_editor_blocks_patch_3') && this.isDocumentHidden();
379
+ if (shouldDefer) {
380
+ var _this$deps$getFireAna;
381
+ this.deferredExhausted.add(resourceId);
382
+ this.registerWakeListeners();
383
+ (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 || _this$deps$getFireAna(fetchErrorPayload(errorMessage, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(true, errorMessage, undefined, /* deferred */true)));
384
+ return;
385
+ }
353
386
  logException(new Error(errorMessage), {
354
387
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/max-retries-exhausted'
355
388
  });
356
- (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 || _this$deps$getFireAna(fetchErrorPayload(errorMessage, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), errorMessage)));
389
+ (_this$deps$getFireAna2 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna2 === void 0 || _this$deps$getFireAna2(fetchErrorPayload(errorMessage, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), errorMessage)));
357
390
  return;
358
391
  }
359
392
  var delay = this.getReconnectionDelay(attempts);
360
393
  var timer = setTimeout(function () {
361
- _this6.pendingRetries.delete(resourceId);
394
+ _this7.pendingRetries.delete(resourceId);
362
395
 
363
396
  // Only re-subscribe if still relevant
364
- if (_this6.subscriptions.has(resourceId) && _this6.shouldUseRealTime()) {
365
- _this6.setupSubscription(resourceId);
397
+ if (_this7.subscriptions.has(resourceId) && _this7.shouldUseRealTime()) {
398
+ _this7.setupSubscription(resourceId);
366
399
 
367
400
  // Only increment if the subscription was actually established
368
401
  // (setupSubscription may be a no-op if another code path already re-established it)
369
- if (_this6.graphqlSubscriptions.has(resourceId)) {
370
- _this6.retryAttempts.set(resourceId, attempts + 1);
402
+ if (_this7.graphqlSubscriptions.has(resourceId)) {
403
+ _this7.retryAttempts.set(resourceId, attempts + 1);
371
404
  }
372
405
  } else {
373
406
  // Conditions no longer met — clean up stale retry state
374
- _this6.retryAttempts.delete(resourceId);
407
+ _this7.retryAttempts.delete(resourceId);
375
408
  }
376
409
  }, delay);
377
410
  this.pendingRetries.set(resourceId, timer);
@@ -386,6 +419,106 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
386
419
  value: function resetRetryCount(resourceId) {
387
420
  this.retryAttempts.delete(resourceId);
388
421
  }
422
+
423
+ /**
424
+ * Whether the tab is currently hidden. SSR/non-browser is treated as "not hidden" so
425
+ * we never defer where wake events can't fire (realtime never subscribes there anyway).
426
+ */
427
+ }, {
428
+ key: "isDocumentHidden",
429
+ value: function isDocumentHidden() {
430
+ var _getDocument;
431
+ return ((_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.hidden) === true;
432
+ }
433
+
434
+ /**
435
+ * Lazily register shared `online` / `visibilitychange` listeners on first deferral.
436
+ * Torn down by `unregisterWakeListeners` (via `handleWake`). No-op in SSR / non-browser.
437
+ */
438
+ }, {
439
+ key: "registerWakeListeners",
440
+ value: function registerWakeListeners() {
441
+ if (this.wakeListenerCleanups.length > 0 || typeof window === 'undefined') {
442
+ return;
443
+ }
444
+ this.wakeListenerCleanups.push(bind(window, {
445
+ type: 'online',
446
+ listener: this.onOnline
447
+ }));
448
+ var doc = getDocument();
449
+ if (doc) {
450
+ this.wakeListenerCleanups.push(bind(doc, {
451
+ type: 'visibilitychange',
452
+ listener: this.onVisibilityChange
453
+ }));
454
+ }
455
+ }
456
+ }, {
457
+ key: "unregisterWakeListeners",
458
+ value: function unregisterWakeListeners() {
459
+ if (this.wakeDebounceTimer !== null) {
460
+ clearTimeout(this.wakeDebounceTimer);
461
+ this.wakeDebounceTimer = null;
462
+ }
463
+ var _iterator = _createForOfIteratorHelper(this.wakeListenerCleanups),
464
+ _step;
465
+ try {
466
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
467
+ var cleanup = _step.value;
468
+ cleanup();
469
+ }
470
+ } catch (err) {
471
+ _iterator.e(err);
472
+ } finally {
473
+ _iterator.f();
474
+ }
475
+ this.wakeListenerCleanups = [];
476
+ }
477
+
478
+ /**
479
+ * Coalesce a burst of wake events into one debounced re-arm sweep. Callers decide
480
+ * eligibility: `onOnline` always sweeps; `onVisibilityChange` only sweeps when visible.
481
+ */
482
+ }, {
483
+ key: "scheduleWakeSweep",
484
+ value: function scheduleWakeSweep() {
485
+ var _this8 = this;
486
+ if (this.wakeDebounceTimer !== null) {
487
+ return;
488
+ }
489
+ this.wakeDebounceTimer = setTimeout(function () {
490
+ _this8.wakeDebounceTimer = null;
491
+ _this8.handleWake();
492
+ }, SyncBlockSubscriptionManager.WAKE_DEBOUNCE_MS);
493
+ }
494
+
495
+ /**
496
+ * Re-arm every deferred resource: full reset (attempts→0) + immediate reconnect now
497
+ * the tab is visible / online. Healthy subscriptions are untouched. A re-armed cycle
498
+ * that later exhausts while visible fires the terminal error normally.
499
+ */
500
+ }, {
501
+ key: "handleWake",
502
+ value: function handleWake() {
503
+ if (this.deferredExhausted.size === 0) {
504
+ this.unregisterWakeListeners();
505
+ return;
506
+ }
507
+ var toRearm = Array.from(this.deferredExhausted);
508
+ this.deferredExhausted.clear();
509
+ for (var _i = 0, _toRearm = toRearm; _i < _toRearm.length; _i++) {
510
+ var resourceId = _toRearm[_i];
511
+ // Only re-arm resources that still have active subscribers and realtime on.
512
+ if (!this.subscriptions.has(resourceId) || !this.shouldUseRealTime()) {
513
+ continue;
514
+ }
515
+ this.resetRetryCount(resourceId);
516
+ this.cancelPendingRetry(resourceId);
517
+ this.setupSubscription(resourceId);
518
+ }
519
+ // No deferred resources remain until another hidden exhaustion re-adds one.
520
+ this.unregisterWakeListeners();
521
+ }
389
522
  }, {
390
523
  key: "cancelPendingRetry",
391
524
  value: function cancelPendingRetry(resourceId) {
@@ -398,20 +531,23 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
398
531
  }, {
399
532
  key: "cancelAllPendingRetries",
400
533
  value: function cancelAllPendingRetries() {
401
- var _iterator = _createForOfIteratorHelper(this.pendingRetries.values()),
402
- _step;
534
+ var _iterator2 = _createForOfIteratorHelper(this.pendingRetries.values()),
535
+ _step2;
403
536
  try {
404
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
405
- var timer = _step.value;
537
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
538
+ var timer = _step2.value;
406
539
  clearTimeout(timer);
407
540
  }
408
541
  } catch (err) {
409
- _iterator.e(err);
542
+ _iterator2.e(err);
410
543
  } finally {
411
- _iterator.f();
544
+ _iterator2.f();
412
545
  }
413
546
  this.pendingRetries.clear();
414
547
  this.retryAttempts.clear();
548
+ // Nothing left to re-arm: drop parked exhaustions and tear down wake listeners.
549
+ this.deferredExhausted.clear();
550
+ this.unregisterWakeListeners();
415
551
  }
416
552
  }, {
417
553
  key: "cleanupSubscription",
@@ -423,37 +559,42 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
423
559
  }
424
560
  this.cancelPendingRetry(resourceId);
425
561
  this.retryAttempts.delete(resourceId);
562
+ // No subscribers left, so this resource can't be re-armed.
563
+ this.deferredExhausted.delete(resourceId);
564
+ if (this.deferredExhausted.size === 0) {
565
+ this.unregisterWakeListeners();
566
+ }
426
567
  }
427
568
  }, {
428
569
  key: "setupSubscriptionsForAllBlocks",
429
570
  value: function setupSubscriptionsForAllBlocks() {
430
- var _iterator2 = _createForOfIteratorHelper(this.subscriptions.keys()),
431
- _step2;
571
+ var _iterator3 = _createForOfIteratorHelper(this.subscriptions.keys()),
572
+ _step3;
432
573
  try {
433
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
434
- var resourceId = _step2.value;
574
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
575
+ var resourceId = _step3.value;
435
576
  this.setupSubscription(resourceId);
436
577
  }
437
578
  } catch (err) {
438
- _iterator2.e(err);
579
+ _iterator3.e(err);
439
580
  } finally {
440
- _iterator2.f();
581
+ _iterator3.f();
441
582
  }
442
583
  }
443
584
  }, {
444
585
  key: "cleanupAll",
445
586
  value: function cleanupAll() {
446
- var _iterator3 = _createForOfIteratorHelper(this.graphqlSubscriptions.values()),
447
- _step3;
587
+ var _iterator4 = _createForOfIteratorHelper(this.graphqlSubscriptions.values()),
588
+ _step4;
448
589
  try {
449
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
450
- var unsubscribe = _step3.value;
590
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
591
+ var unsubscribe = _step4.value;
451
592
  unsubscribe();
452
593
  }
453
594
  } catch (err) {
454
- _iterator3.e(err);
595
+ _iterator4.e(err);
455
596
  } finally {
456
- _iterator3.f();
597
+ _iterator4.f();
457
598
  }
458
599
  this.graphqlSubscriptions.clear();
459
600
  this.cancelAllPendingRetries();
@@ -468,17 +609,17 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
468
609
  this.useRealTimeSubscriptions = false;
469
610
 
470
611
  // Clear any pending cache deletions
471
- var _iterator4 = _createForOfIteratorHelper(this.pendingCacheDeletions.values()),
472
- _step4;
612
+ var _iterator5 = _createForOfIteratorHelper(this.pendingCacheDeletions.values()),
613
+ _step5;
473
614
  try {
474
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
475
- var timeout = _step4.value;
615
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
616
+ var timeout = _step5.value;
476
617
  clearTimeout(timeout);
477
618
  }
478
619
  } catch (err) {
479
- _iterator4.e(err);
620
+ _iterator5.e(err);
480
621
  } finally {
481
- _iterator4.f();
622
+ _iterator5.f();
482
623
  }
483
624
  this.pendingCacheDeletions.clear();
484
625
  }
@@ -490,7 +631,7 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
490
631
  }, {
491
632
  key: "handleGraphQLUpdate",
492
633
  value: function handleGraphQLUpdate(syncBlockInstance) {
493
- var _this7 = this;
634
+ var _this9 = this;
494
635
  if (!syncBlockInstance.resourceId) {
495
636
  return;
496
637
  }
@@ -499,21 +640,28 @@ export var SyncBlockSubscriptionManager = /*#__PURE__*/function () {
499
640
  this.deps.updateCache(resolved);
500
641
  if (!syncBlockInstance.error) {
501
642
  this.resetRetryCount(syncBlockInstance.resourceId);
643
+ // A healthy update means a parked exhaustion recovered on its own before any
644
+ // wake sweep — drop it and tear down listeners if nothing else is parked.
645
+ if (this.deferredExhausted.delete(syncBlockInstance.resourceId)) {
646
+ if (this.deferredExhausted.size === 0) {
647
+ this.unregisterWakeListeners();
648
+ }
649
+ }
502
650
  var callbacks = this.subscriptions.get(syncBlockInstance.resourceId);
503
651
  var localIds = callbacks ? Object.keys(callbacks) : [];
504
652
  localIds.forEach(function (localId) {
505
- var _this7$deps$getFireAn, _syncBlockInstance$da, _syncBlockInstance$da2;
506
- (_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)));
653
+ var _this9$deps$getFireAn, _syncBlockInstance$da, _syncBlockInstance$da2;
654
+ (_this9$deps$getFireAn = _this9.deps.getFireAnalyticsEvent()) === null || _this9$deps$getFireAn === void 0 || _this9$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)));
507
655
  });
508
656
  this.deps.fetchSyncBlockSourceInfo(resolved.resourceId);
509
657
  } else {
510
- var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna2, _syncBlockInstance$da3, _syncBlockInstance$da4, _syncBlockInstance$er3, _syncBlockInstance$er4, _syncBlockInstance$er5;
658
+ var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna3, _syncBlockInstance$da3, _syncBlockInstance$da4, _syncBlockInstance$er3, _syncBlockInstance$er4, _syncBlockInstance$er5;
511
659
  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);
512
660
 
513
661
  // Prefer the structured `type` (a `SyncBlockError` enum value) for classification
514
662
  // and fall back to the free-text `reason` so source-state/permission strings are
515
663
  // still bucketed (EDITOR-7862). The emitted free-text `error` attribute is unchanged.
516
- (_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), buildFetchErrorAttribution(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)));
664
+ (_this$deps$getFireAna3 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna3 === void 0 || _this$deps$getFireAna3(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), buildFetchErrorAttribution(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)));
517
665
  }
518
666
  }
519
667
  }]);
@@ -525,4 +673,6 @@ _defineProperty(SyncBlockSubscriptionManager, "MAX_RETRY_ATTEMPTS", 5);
525
673
  // legacy (gate OFF)
526
674
  _defineProperty(SyncBlockSubscriptionManager, "MAX_RETRY_ATTEMPTS_HARDENED", 8);
527
675
  // gate ON (EDITOR-7861)
528
- _defineProperty(SyncBlockSubscriptionManager, "MAX_RETRY_DELAY_MS", 30000);
676
+ _defineProperty(SyncBlockSubscriptionManager, "MAX_RETRY_DELAY_MS", 30000);
677
+ // Coalesce wake-event bursts into one re-arm sweep to avoid a reconnection storm.
678
+ _defineProperty(SyncBlockSubscriptionManager, "WAKE_DEBOUNCE_MS", 1000);
@@ -193,16 +193,19 @@ export var classifyFetchErrorReason = function classifyFetchErrorReason(error) {
193
193
  * `gateEnabled` is injected by the caller (the store managers evaluate `fg(...)`) so this
194
194
  * helper stays pure and trivially unit-testable for both gate states.
195
195
  */
196
- export var buildFetchErrorAttribution = function buildFetchErrorAttribution(gateEnabled, error, statusCode) {
196
+ export var buildFetchErrorAttribution = function buildFetchErrorAttribution(gateEnabled, error, statusCode, deferred) {
197
197
  if (!gateEnabled) {
198
198
  return undefined;
199
199
  }
200
200
  var reason = classifyFetchErrorReason(error);
201
- return _objectSpread({
201
+ return _objectSpread(_objectSpread({
202
202
  reason: reason,
203
- benign: FETCH_BENIGN_REASONS.has(reason)
203
+ // A deferred (tab-hidden) exhaustion is benign regardless of reason bucket.
204
+ benign: deferred === true || FETCH_BENIGN_REASONS.has(reason)
204
205
  }, statusCode !== undefined && {
205
206
  statusCode: statusCode
207
+ }), deferred === true && {
208
+ deferred: true
206
209
  });
207
210
  };
208
211
 
@@ -230,7 +233,7 @@ export function getErrorPayload(actionSubjectId, error, resourceId, sourceProduc
230
233
  actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
231
234
  actionSubjectId: actionSubjectId,
232
235
  eventType: EVENT_TYPE.OPERATIONAL,
233
- attributes: _objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread({
236
+ attributes: _objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread({
234
237
  error: error
235
238
  }, resourceId && {
236
239
  resourceId: resourceId
@@ -242,6 +245,8 @@ export function getErrorPayload(actionSubjectId, error, resourceId, sourceProduc
242
245
  statusCode: attribution.statusCode
243
246
  }), attribution && 'benign' in attribution && {
244
247
  benign: attribution.benign
248
+ }), attribution && 'deferred' in attribution && attribution.deferred === true && {
249
+ deferred: true
245
250
  })
246
251
  };
247
252
  }
@@ -348,16 +353,49 @@ export var createSuccessPayload = function createSuccessPayload(resourceId, sour
348
353
  };
349
354
 
350
355
  /**
351
- * Operational `syncedBlockCreate` success event (previously only error rows
352
- * existed, so dashboards had no create denominator). Fired behind
353
- * `platform_editor_blocks_patch_4` with the `blockInstanceId` join key.
356
+ * Optional enrichment for the `syncedBlockCreate` success event. All fields
357
+ * optional so gate-off/legacy payloads are unchanged. `inputMethod`: creating
358
+ * surface (enum, PII-safe). `createdEmpty`: true from an empty selection, false
359
+ * when content was converted.
354
360
  */
355
- export var createSuccessOperationalPayload = function createSuccessOperationalPayload(resourceId, blockInstanceId, sourceProduct) {
361
+
362
+ /**
363
+ * Operational `syncedBlockCreate` success event, behind
364
+ * `platform_editor_blocks_patch_4`, with the `blockInstanceId` join key and,
365
+ * when available, the `inputMethod` + `createdEmpty` creation-type signals.
366
+ */
367
+ export var createSuccessOperationalPayload = function createSuccessOperationalPayload(resourceId, blockInstanceId, sourceProduct, enrichment) {
356
368
  return {
357
369
  action: ACTION.INSERTED,
358
370
  actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
359
371
  actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_CREATE,
360
372
  eventType: EVENT_TYPE.OPERATIONAL,
373
+ attributes: _objectSpread(_objectSpread(_objectSpread(_objectSpread({
374
+ resourceId: resourceId
375
+ }, blockInstanceId && {
376
+ blockInstanceId: blockInstanceId
377
+ }), sourceProduct && {
378
+ sourceProduct: sourceProduct
379
+ }), (enrichment === null || enrichment === void 0 ? void 0 : enrichment.inputMethod) && {
380
+ inputMethod: enrichment.inputMethod
381
+ }), (enrichment === null || enrichment === void 0 ? void 0 : enrichment.createdEmpty) !== undefined && {
382
+ createdEmpty: enrichment.createdEmpty
383
+ })
384
+ };
385
+ };
386
+
387
+ /**
388
+ * Operational first-content-added event, behind
389
+ * `platform_editor_blocks_patch_4`. Fired once when a block created empty first
390
+ * gains user content. Join keys only (`resourceId` + `blockInstanceId`), no user
391
+ * content (PII-safe).
392
+ */
393
+ export var addContentSuccessPayload = function addContentSuccessPayload(resourceId, blockInstanceId, sourceProduct) {
394
+ return {
395
+ action: ACTION.ADDED,
396
+ actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
397
+ actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_ADD_CONTENT,
398
+ eventType: EVENT_TYPE.OPERATIONAL,
361
399
  attributes: _objectSpread(_objectSpread({
362
400
  resourceId: resourceId
363
401
  }, blockInstanceId && {
@@ -1 +1,2 @@
1
1
  export { buildFetchErrorAttribution, fetchErrorPayload, getPiiSafeOriginalError, } from '../utils/errorHandling';
2
+ export type { CreateSuccessEnrichment } from '../utils/errorHandling';