@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.
@@ -2,7 +2,7 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { logException } from '@atlaskit/editor-common/monitoring';
3
3
  import { fg } from '@atlaskit/platform-feature-flags';
4
4
  import { SyncBlockError } from '../common/types';
5
- import { updateErrorPayload, createErrorPayload, deleteErrorPayload, updateCacheErrorPayload, getSourceInfoErrorPayload, updateSuccessPayload, createSuccessPayload, createSuccessOperationalPayload, deleteSuccessPayload, fetchReferencesErrorPayload, buildErrorAttribution } from '../utils/errorHandling';
5
+ import { updateErrorPayload, createErrorPayload, deleteErrorPayload, updateCacheErrorPayload, getSourceInfoErrorPayload, updateSuccessPayload, createSuccessPayload, createSuccessOperationalPayload, addContentSuccessPayload, deleteSuccessPayload, fetchReferencesErrorPayload, buildErrorAttribution } from '../utils/errorHandling';
6
6
  import { getCreateSourceExperience, getDeleteSourceExperience, getSaveSourceExperience, getFetchSourceInfoExperience } from '../utils/experienceTracking';
7
7
  import { convertSyncBlockPMNodeToSyncBlockData, getSourceProductFromResourceIdSafe } from '../utils/utils';
8
8
  /** Maximum time (ms) flush() will wait for in-flight block creations before proceeding. */
@@ -40,6 +40,20 @@ export class SourceSyncBlockStoreManager {
40
40
  * multiple blocks are created concurrently. See EDITOR-7112.
41
41
  */
42
42
  _defineProperty(this, "creationsTimedOutDuringFlush", new Set());
43
+ /**
44
+ * Creation-type signals captured at `createBodiedSyncBlockNode` time and read
45
+ * back in `commitPendingCreation` to enrich the `syncedBlockCreate` event.
46
+ * Kept out of the flushed cache record so it never leaks into Block Service /
47
+ * ADF; removed once consumed.
48
+ */
49
+ _defineProperty(this, "creationEnrichment", new Map());
50
+ /**
51
+ * Resource IDs of blocks created empty this session that have not yet fired
52
+ * the first-content-added event. Added on empty creation, removed when the
53
+ * block first gains content (the event fires once). Blocks created with
54
+ * content or not created this session are never tracked.
55
+ */
56
+ _defineProperty(this, "awaitingFirstContent", new Set());
43
57
  /**
44
58
  * resourceId -> timestamp (ms) of the last `syncedBlockDelete` emission, used
45
59
  * to suppress duplicates within {@link DELETE_DEDUPE_WINDOW_MS}. Only consulted
@@ -338,17 +352,46 @@ export class SourceSyncBlockStoreManager {
338
352
  var _this$fireAnalyticsEv6;
339
353
  const sourceProduct = getSourceProductFromResourceIdSafe(resourceId);
340
354
  (_this$fireAnalyticsEv6 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv6 === void 0 ? void 0 : _this$fireAnalyticsEv6.call(this, createSuccessPayload(resourceId || '', sourceProduct));
341
- // Operational create-success event with the join key.
355
+ // Operational create-success event with the join key + creation-type
356
+ // signals (inputMethod / createdEmpty) captured at the command layer.
342
357
  if (fg('platform_editor_blocks_patch_4')) {
343
358
  var _this$fireAnalyticsEv7, _this$syncBlockCache$;
344
- (_this$fireAnalyticsEv7 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv7 === void 0 ? void 0 : _this$fireAnalyticsEv7.call(this, createSuccessOperationalPayload(resourceId || '', (_this$syncBlockCache$ = this.syncBlockCache.get(resourceId)) === null || _this$syncBlockCache$ === void 0 ? void 0 : _this$syncBlockCache$.blockInstanceId, sourceProduct));
359
+ (_this$fireAnalyticsEv7 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv7 === void 0 ? void 0 : _this$fireAnalyticsEv7.call(this, createSuccessOperationalPayload(resourceId || '', (_this$syncBlockCache$ = this.syncBlockCache.get(resourceId)) === null || _this$syncBlockCache$ === void 0 ? void 0 : _this$syncBlockCache$.blockInstanceId, sourceProduct, this.creationEnrichment.get(resourceId)));
345
360
  }
346
361
  } else {
347
362
  var _this$fireAnalyticsEv8;
348
363
  // Delete the node from cache if fail to create so it's not flushed to BE
349
364
  this.syncBlockCache.delete(resourceId || '');
365
+ // Creation failed, so there is no block to add content to — drop the
366
+ // first-content tracking entry so a later unrelated edit cannot fire it.
367
+ this.awaitingFirstContent.delete(resourceId || '');
350
368
  (_this$fireAnalyticsEv8 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv8 === void 0 ? void 0 : _this$fireAnalyticsEv8.call(this, createErrorPayload('Fail to create bodied sync block', resourceId, getSourceProductFromResourceIdSafe(resourceId)));
351
369
  }
370
+ // Enrichment is single-use — drop it once the create has resolved either way.
371
+ this.creationEnrichment.delete(resourceId || '');
372
+ }
373
+
374
+ /**
375
+ * Fire the first-content-added event once when a block created empty this
376
+ * session first gains content. The caller detects the in-block edit; this owns
377
+ * dedupe + emission (fires at most once per block, only for empty→content).
378
+ * Gated behind `platform_editor_blocks_patch_4`.
379
+ */
380
+ maybeEmitFirstContentAdded(resourceId, blockInstanceId) {
381
+ var _this$fireAnalyticsEv9;
382
+ if (this.viewMode === 'view') {
383
+ return;
384
+ }
385
+ if (!fg('platform_editor_blocks_patch_4')) {
386
+ return;
387
+ }
388
+ // `delete` returns false when the id was not tracked (already emitted, or
389
+ // the block was not created empty this session), so this both dedupes and
390
+ // scopes emission to genuine first-content transitions in one check.
391
+ if (!this.awaitingFirstContent.delete(resourceId)) {
392
+ return;
393
+ }
394
+ (_this$fireAnalyticsEv9 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv9 === void 0 ? void 0 : _this$fireAnalyticsEv9.call(this, addContentSuccessPayload(resourceId, blockInstanceId, getSourceProductFromResourceIdSafe(resourceId)));
352
395
  }
353
396
  registerConfirmationCallback(callback) {
354
397
  this.confirmationCallback = callback;
@@ -382,8 +425,12 @@ export class SourceSyncBlockStoreManager {
382
425
  * @param attrs attributes Ids of the node
383
426
  * @param node the ProseMirror node to cache
384
427
  * @param onCompletion callback invoked when creation completes
428
+ * @param enrichment optional creation-type signals stashed and attached to the
429
+ * `syncedBlockCreate` event when the async create resolves. When
430
+ * `createdEmpty` is true the block is also registered for the
431
+ * first-content-added event.
385
432
  */
386
- createBodiedSyncBlockNode(attrs, node, onCompletion) {
433
+ createBodiedSyncBlockNode(attrs, node, onCompletion, enrichment) {
387
434
  if (this.viewMode === 'view') {
388
435
  return;
389
436
  }
@@ -391,6 +438,14 @@ export class SourceSyncBlockStoreManager {
391
438
  resourceId,
392
439
  localId: blockInstanceId
393
440
  } = attrs;
441
+ // Stash creation-type signals for commitPendingCreation to read.
442
+ if (enrichment) {
443
+ this.creationEnrichment.set(resourceId, enrichment);
444
+ // Only empty-created blocks can produce an empty→content transition.
445
+ if (enrichment.createdEmpty) {
446
+ this.awaitingFirstContent.add(resourceId);
447
+ }
448
+ }
394
449
  try {
395
450
  var _this$createExperienc;
396
451
  if (!this.dataProvider) {
@@ -418,15 +473,15 @@ export class SourceSyncBlockStoreManager {
418
473
  this.commitPendingCreation(true, resourceId);
419
474
  (_this$createExperienc2 = this.createExperience) === null || _this$createExperienc2 === void 0 ? void 0 : _this$createExperienc2.success();
420
475
  } else {
421
- var _this$createExperienc3, _this$fireAnalyticsEv9;
476
+ var _this$createExperienc3, _this$fireAnalyticsEv0;
422
477
  this.commitPendingCreation(false, resourceId);
423
478
  (_this$createExperienc3 = this.createExperience) === null || _this$createExperienc3 === void 0 ? void 0 : _this$createExperienc3.failure({
424
479
  reason: result.error || 'Failed to create bodied sync block'
425
480
  });
426
- (_this$fireAnalyticsEv9 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv9 === void 0 ? void 0 : _this$fireAnalyticsEv9.call(this, createErrorPayload(result.error || 'Failed to create bodied sync block', resourceId, getSourceProductFromResourceIdSafe(resourceId), buildErrorAttribution(fg('platform_editor_blocks_patch_3'), result.error, result.statusCode)));
481
+ (_this$fireAnalyticsEv0 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv0 === void 0 ? void 0 : _this$fireAnalyticsEv0.call(this, createErrorPayload(result.error || 'Failed to create bodied sync block', resourceId, getSourceProductFromResourceIdSafe(resourceId), buildErrorAttribution(fg('platform_editor_blocks_patch_3'), result.error, result.statusCode)));
427
482
  }
428
483
  }).catch(error => {
429
- var _this$createExperienc4, _this$fireAnalyticsEv0;
484
+ var _this$createExperienc4, _this$fireAnalyticsEv1;
430
485
  this.commitPendingCreation(false, resourceId);
431
486
  logException(error, {
432
487
  location: 'editor-synced-block-provider/sourceSyncBlockStoreManager'
@@ -434,20 +489,20 @@ export class SourceSyncBlockStoreManager {
434
489
  (_this$createExperienc4 = this.createExperience) === null || _this$createExperienc4 === void 0 ? void 0 : _this$createExperienc4.failure({
435
490
  reason: error.message
436
491
  });
437
- (_this$fireAnalyticsEv0 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv0 === void 0 ? void 0 : _this$fireAnalyticsEv0.call(this, createErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
492
+ (_this$fireAnalyticsEv1 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv1 === void 0 ? void 0 : _this$fireAnalyticsEv1.call(this, createErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
438
493
  }).finally(() => {
439
494
  this.pendingCreationPromises.delete(resourceId);
440
495
  });
441
496
  this.pendingCreationPromises.set(resourceId, creationPromise);
442
497
  } catch (error) {
443
- var _this$fireAnalyticsEv1;
498
+ var _this$fireAnalyticsEv10;
444
499
  if (this.isPendingCreation(resourceId)) {
445
500
  this.commitPendingCreation(false, resourceId);
446
501
  }
447
502
  logException(error, {
448
503
  location: 'editor-synced-block-provider/sourceSyncBlockStoreManager'
449
504
  });
450
- (_this$fireAnalyticsEv1 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv1 === void 0 ? void 0 : _this$fireAnalyticsEv1.call(this, createErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
505
+ (_this$fireAnalyticsEv10 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv10 === void 0 ? void 0 : _this$fireAnalyticsEv10.call(this, createErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
451
506
  }
452
507
  }
453
508
  /**
@@ -469,11 +524,11 @@ export class SourceSyncBlockStoreManager {
469
524
  * while the cache entry still exists so `blockInstanceId` is available.
470
525
  */
471
526
  emitDeleteSuccess(resourceId, reason, mechanism) {
472
- var _this$syncBlockCache$2, _this$fireAnalyticsEv11;
527
+ var _this$syncBlockCache$2, _this$fireAnalyticsEv12;
473
528
  const sourceProduct = getSourceProductFromResourceIdSafe(resourceId);
474
529
  if (!fg('platform_editor_blocks_patch_4')) {
475
- var _this$fireAnalyticsEv10;
476
- (_this$fireAnalyticsEv10 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv10 === void 0 ? void 0 : _this$fireAnalyticsEv10.call(this, deleteSuccessPayload(resourceId, sourceProduct));
530
+ var _this$fireAnalyticsEv11;
531
+ (_this$fireAnalyticsEv11 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv11 === void 0 ? void 0 : _this$fireAnalyticsEv11.call(this, deleteSuccessPayload(resourceId, sourceProduct));
477
532
  return;
478
533
  }
479
534
  const now = Date.now();
@@ -488,7 +543,7 @@ export class SourceSyncBlockStoreManager {
488
543
  deletionReason: reason,
489
544
  mechanism
490
545
  };
491
- (_this$fireAnalyticsEv11 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv11 === void 0 ? void 0 : _this$fireAnalyticsEv11.call(this, deleteSuccessPayload(resourceId, sourceProduct, enrichment));
546
+ (_this$fireAnalyticsEv12 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv12 === void 0 ? void 0 : _this$fireAnalyticsEv12.call(this, deleteSuccessPayload(resourceId, sourceProduct, enrichment));
492
547
  }
493
548
  async delete(syncBlockIds, onDelete, onDeleteCompleted, reason, mechanism) {
494
549
  try {
@@ -529,8 +584,8 @@ export class SourceSyncBlockStoreManager {
529
584
  if (result.success) {
530
585
  this.emitDeleteSuccess(result.resourceId, reason, mechanism);
531
586
  } else {
532
- var _this$fireAnalyticsEv12;
533
- (_this$fireAnalyticsEv12 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv12 === void 0 ? void 0 : _this$fireAnalyticsEv12.call(this, deleteErrorPayload(result.error || 'Failed to delete synced block', result.resourceId, getSourceProductFromResourceIdSafe(result.resourceId), buildErrorAttribution(attributionEnabled, result.error, result.statusCode)));
587
+ var _this$fireAnalyticsEv13;
588
+ (_this$fireAnalyticsEv13 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv13 === void 0 ? void 0 : _this$fireAnalyticsEv13.call(this, deleteErrorPayload(result.error || 'Failed to delete synced block', result.resourceId, getSourceProductFromResourceIdSafe(result.resourceId), buildErrorAttribution(attributionEnabled, result.error, result.statusCode)));
534
589
  }
535
590
  });
536
591
  }
@@ -541,9 +596,9 @@ export class SourceSyncBlockStoreManager {
541
596
  // so the attribution `reason` falls back to `unknown` when the gate is on.
542
597
  const attribution = buildErrorAttribution(fg('platform_editor_blocks_patch_3'));
543
598
  syncBlockIds.forEach(Ids => {
544
- var _this$fireAnalyticsEv13;
599
+ var _this$fireAnalyticsEv14;
545
600
  this.setPendingDeletion(Ids, false);
546
- (_this$fireAnalyticsEv13 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv13 === void 0 ? void 0 : _this$fireAnalyticsEv13.call(this, deleteErrorPayload(error.message, Ids.resourceId, getSourceProductFromResourceIdSafe(Ids.resourceId), attribution));
601
+ (_this$fireAnalyticsEv14 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv14 === void 0 ? void 0 : _this$fireAnalyticsEv14.call(this, deleteErrorPayload(error.message, Ids.resourceId, getSourceProductFromResourceIdSafe(Ids.resourceId), attribution));
547
602
  });
548
603
  logException(error, {
549
604
  location: 'editor-synced-block-provider/sourceSyncBlockStoreManager'
@@ -703,11 +758,11 @@ export class SourceSyncBlockStoreManager {
703
758
  return sourceInfo;
704
759
  });
705
760
  } catch (error) {
706
- var _this$fireAnalyticsEv14;
761
+ var _this$fireAnalyticsEv15;
707
762
  logException(error, {
708
763
  location: 'editor-synced-block-provider/sourceSyncBlockStoreManager'
709
764
  });
710
- (_this$fireAnalyticsEv14 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv14 === void 0 ? void 0 : _this$fireAnalyticsEv14.call(this, getSourceInfoErrorPayload(error.message));
765
+ (_this$fireAnalyticsEv15 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv15 === void 0 ? void 0 : _this$fireAnalyticsEv15.call(this, getSourceInfoErrorPayload(error.message));
711
766
  return Promise.resolve(undefined);
712
767
  }
713
768
  }
@@ -718,11 +773,11 @@ export class SourceSyncBlockStoreManager {
718
773
  }
719
774
  return this.dataProvider.fetchReferences(resourceId, true);
720
775
  } catch (error) {
721
- var _this$fireAnalyticsEv15;
776
+ var _this$fireAnalyticsEv16;
722
777
  logException(error, {
723
778
  location: 'editor-synced-block-provider/sourceSyncBlockStoreManager'
724
779
  });
725
- (_this$fireAnalyticsEv15 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv15 === void 0 ? void 0 : _this$fireAnalyticsEv15.call(this, fetchReferencesErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
780
+ (_this$fireAnalyticsEv16 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv16 === void 0 ? void 0 : _this$fireAnalyticsEv16.call(this, fetchReferencesErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
726
781
  return Promise.resolve({
727
782
  error: SyncBlockError.Errored
728
783
  });
@@ -737,6 +792,8 @@ export class SourceSyncBlockStoreManager {
737
792
  this.postCreationFlushCallback = undefined;
738
793
  this.pendingCreationPromises.clear();
739
794
  this.creationsTimedOutDuringFlush.clear();
795
+ this.creationEnrichment.clear();
796
+ this.awaitingFirstContent.clear();
740
797
  this.recentDeleteEmissions.clear();
741
798
  this.dataProvider = undefined;
742
799
  (_this$saveExperience4 = this.saveExperience) === null || _this$saveExperience4 === void 0 ? void 0 : _this$saveExperience4.abort({
@@ -1,4 +1,6 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import { bind } from 'bind-event-listener';
3
+ import { getDocument } from '@atlaskit/browser-apis';
2
4
  import { logException } from '@atlaskit/editor-common/monitoring';
3
5
  import { fg } from '@atlaskit/platform-feature-flags';
4
6
  import { buildFetchErrorAttribution, fetchErrorPayload, fetchSuccessPayload } from '../utils/errorHandling';
@@ -45,6 +47,22 @@ export class SyncBlockSubscriptionManager {
45
47
  // backoff cap (gate ON)
46
48
  _defineProperty(this, "retryAttempts", new Map());
47
49
  _defineProperty(this, "pendingRetries", new Map());
50
+ // Resources whose reconnection exhausted while the tab was hidden: parked here and
51
+ // re-armed on the next online / visibilitychange→visible instead of surfacing a
52
+ // terminal error. Only a re-armed attempt that also exhausts while visible fails.
53
+ _defineProperty(this, "deferredExhausted", new Set());
54
+ _defineProperty(this, "wakeDebounceTimer", null);
55
+ // `online` always sweeps (network is back regardless of tab visibility); a
56
+ // `visibilitychange` only sweeps when the tab became visible.
57
+ _defineProperty(this, "onOnline", () => this.scheduleWakeSweep());
58
+ _defineProperty(this, "onVisibilityChange", () => {
59
+ if (!this.isDocumentHidden()) {
60
+ this.scheduleWakeSweep();
61
+ }
62
+ });
63
+ // Cleanup fns returned by `bind` for the registered wake listeners; a non-empty
64
+ // array means listeners are currently registered. Emptied on teardown.
65
+ _defineProperty(this, "wakeListenerCleanups", []);
48
66
  this.deps = deps;
49
67
  }
50
68
 
@@ -306,14 +324,26 @@ export class SyncBlockSubscriptionManager {
306
324
  const attempts = (_this$retryAttempts$g = this.retryAttempts.get(resourceId)) !== null && _this$retryAttempts$g !== void 0 ? _this$retryAttempts$g : 0;
307
325
  const maxAttempts = this.getMaxRetryAttempts();
308
326
  if (attempts >= maxAttempts) {
309
- var _this$deps$getFireAna3;
327
+ var _this$deps$getFireAna4;
310
328
  // Exhausted all attempts — the only place a WS drop surfaces as a
311
329
  // fetch error under the gate (EDITOR-7861).
312
330
  const errorMessage = `Subscription reconnection failed after ${attempts} attempts`;
331
+
332
+ // Tab hidden at exhaustion: don't surface a terminal failure (user isn't
333
+ // looking, and most exhaustions self-recover once foregrounded). Park + re-arm
334
+ // on wake, emitting a benign `deferred` signal so suppression stays auditable.
335
+ const shouldDefer = fg('platform_editor_blocks_patch_3') && this.isDocumentHidden();
336
+ if (shouldDefer) {
337
+ var _this$deps$getFireAna3;
338
+ this.deferredExhausted.add(resourceId);
339
+ this.registerWakeListeners();
340
+ (_this$deps$getFireAna3 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna3 === void 0 ? void 0 : _this$deps$getFireAna3(fetchErrorPayload(errorMessage, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(true, errorMessage, undefined, /* deferred */true)));
341
+ return;
342
+ }
313
343
  logException(new Error(errorMessage), {
314
344
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/max-retries-exhausted'
315
345
  });
316
- (_this$deps$getFireAna3 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna3 === void 0 ? void 0 : _this$deps$getFireAna3(fetchErrorPayload(errorMessage, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), errorMessage)));
346
+ (_this$deps$getFireAna4 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna4 === void 0 ? void 0 : _this$deps$getFireAna4(fetchErrorPayload(errorMessage, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), errorMessage)));
317
347
  return;
318
348
  }
319
349
  const delay = this.getReconnectionDelay(attempts);
@@ -344,6 +374,85 @@ export class SyncBlockSubscriptionManager {
344
374
  resetRetryCount(resourceId) {
345
375
  this.retryAttempts.delete(resourceId);
346
376
  }
377
+
378
+ /**
379
+ * Whether the tab is currently hidden. SSR/non-browser is treated as "not hidden" so
380
+ * we never defer where wake events can't fire (realtime never subscribes there anyway).
381
+ */
382
+ isDocumentHidden() {
383
+ var _getDocument;
384
+ return ((_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.hidden) === true;
385
+ }
386
+
387
+ /**
388
+ * Lazily register shared `online` / `visibilitychange` listeners on first deferral.
389
+ * Torn down by `unregisterWakeListeners` (via `handleWake`). No-op in SSR / non-browser.
390
+ */
391
+ registerWakeListeners() {
392
+ if (this.wakeListenerCleanups.length > 0 || typeof window === 'undefined') {
393
+ return;
394
+ }
395
+ this.wakeListenerCleanups.push(bind(window, {
396
+ type: 'online',
397
+ listener: this.onOnline
398
+ }));
399
+ const doc = getDocument();
400
+ if (doc) {
401
+ this.wakeListenerCleanups.push(bind(doc, {
402
+ type: 'visibilitychange',
403
+ listener: this.onVisibilityChange
404
+ }));
405
+ }
406
+ }
407
+ unregisterWakeListeners() {
408
+ if (this.wakeDebounceTimer !== null) {
409
+ clearTimeout(this.wakeDebounceTimer);
410
+ this.wakeDebounceTimer = null;
411
+ }
412
+ for (const cleanup of this.wakeListenerCleanups) {
413
+ cleanup();
414
+ }
415
+ this.wakeListenerCleanups = [];
416
+ }
417
+
418
+ /**
419
+ * Coalesce a burst of wake events into one debounced re-arm sweep. Callers decide
420
+ * eligibility: `onOnline` always sweeps; `onVisibilityChange` only sweeps when visible.
421
+ */
422
+ scheduleWakeSweep() {
423
+ if (this.wakeDebounceTimer !== null) {
424
+ return;
425
+ }
426
+ this.wakeDebounceTimer = setTimeout(() => {
427
+ this.wakeDebounceTimer = null;
428
+ this.handleWake();
429
+ }, SyncBlockSubscriptionManager.WAKE_DEBOUNCE_MS);
430
+ }
431
+
432
+ /**
433
+ * Re-arm every deferred resource: full reset (attempts→0) + immediate reconnect now
434
+ * the tab is visible / online. Healthy subscriptions are untouched. A re-armed cycle
435
+ * that later exhausts while visible fires the terminal error normally.
436
+ */
437
+ handleWake() {
438
+ if (this.deferredExhausted.size === 0) {
439
+ this.unregisterWakeListeners();
440
+ return;
441
+ }
442
+ const toRearm = Array.from(this.deferredExhausted);
443
+ this.deferredExhausted.clear();
444
+ for (const resourceId of toRearm) {
445
+ // Only re-arm resources that still have active subscribers and realtime on.
446
+ if (!this.subscriptions.has(resourceId) || !this.shouldUseRealTime()) {
447
+ continue;
448
+ }
449
+ this.resetRetryCount(resourceId);
450
+ this.cancelPendingRetry(resourceId);
451
+ this.setupSubscription(resourceId);
452
+ }
453
+ // No deferred resources remain until another hidden exhaustion re-adds one.
454
+ this.unregisterWakeListeners();
455
+ }
347
456
  cancelPendingRetry(resourceId) {
348
457
  const timer = this.pendingRetries.get(resourceId);
349
458
  if (timer) {
@@ -357,6 +466,9 @@ export class SyncBlockSubscriptionManager {
357
466
  }
358
467
  this.pendingRetries.clear();
359
468
  this.retryAttempts.clear();
469
+ // Nothing left to re-arm: drop parked exhaustions and tear down wake listeners.
470
+ this.deferredExhausted.clear();
471
+ this.unregisterWakeListeners();
360
472
  }
361
473
  cleanupSubscription(resourceId) {
362
474
  const unsubscribe = this.graphqlSubscriptions.get(resourceId);
@@ -366,6 +478,11 @@ export class SyncBlockSubscriptionManager {
366
478
  }
367
479
  this.cancelPendingRetry(resourceId);
368
480
  this.retryAttempts.delete(resourceId);
481
+ // No subscribers left, so this resource can't be re-armed.
482
+ this.deferredExhausted.delete(resourceId);
483
+ if (this.deferredExhausted.size === 0) {
484
+ this.unregisterWakeListeners();
485
+ }
369
486
  }
370
487
  setupSubscriptionsForAllBlocks() {
371
488
  for (const resourceId of this.subscriptions.keys()) {
@@ -404,21 +521,28 @@ export class SyncBlockSubscriptionManager {
404
521
  this.deps.updateCache(resolved);
405
522
  if (!syncBlockInstance.error) {
406
523
  this.resetRetryCount(syncBlockInstance.resourceId);
524
+ // A healthy update means a parked exhaustion recovered on its own before any
525
+ // wake sweep — drop it and tear down listeners if nothing else is parked.
526
+ if (this.deferredExhausted.delete(syncBlockInstance.resourceId)) {
527
+ if (this.deferredExhausted.size === 0) {
528
+ this.unregisterWakeListeners();
529
+ }
530
+ }
407
531
  const callbacks = this.subscriptions.get(syncBlockInstance.resourceId);
408
532
  const localIds = callbacks ? Object.keys(callbacks) : [];
409
533
  localIds.forEach(localId => {
410
- var _this$deps$getFireAna4, _syncBlockInstance$da, _syncBlockInstance$da2;
411
- (_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)));
534
+ var _this$deps$getFireAna5, _syncBlockInstance$da, _syncBlockInstance$da2;
535
+ (_this$deps$getFireAna5 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna5 === void 0 ? void 0 : _this$deps$getFireAna5(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)));
412
536
  });
413
537
  this.deps.fetchSyncBlockSourceInfo(resolved.resourceId);
414
538
  } else {
415
- var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna5, _syncBlockInstance$da3, _syncBlockInstance$da4, _syncBlockInstance$er3, _syncBlockInstance$er4, _syncBlockInstance$er5;
539
+ var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna6, _syncBlockInstance$da3, _syncBlockInstance$da4, _syncBlockInstance$er3, _syncBlockInstance$er4, _syncBlockInstance$er5;
416
540
  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);
417
541
 
418
542
  // Prefer the structured `type` (a `SyncBlockError` enum value) for classification
419
543
  // and fall back to the free-text `reason` so source-state/permission strings are
420
544
  // still bucketed (EDITOR-7862). The emitted free-text `error` attribute is unchanged.
421
- (_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), 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)));
545
+ (_this$deps$getFireAna6 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna6 === void 0 ? void 0 : _this$deps$getFireAna6(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)));
422
546
  }
423
547
  }
424
548
  }
@@ -429,4 +553,6 @@ _defineProperty(SyncBlockSubscriptionManager, "MAX_RETRY_ATTEMPTS", 5);
429
553
  // legacy (gate OFF)
430
554
  _defineProperty(SyncBlockSubscriptionManager, "MAX_RETRY_ATTEMPTS_HARDENED", 8);
431
555
  // gate ON (EDITOR-7861)
432
- _defineProperty(SyncBlockSubscriptionManager, "MAX_RETRY_DELAY_MS", 30000);
556
+ _defineProperty(SyncBlockSubscriptionManager, "MAX_RETRY_DELAY_MS", 30000);
557
+ // Coalesce wake-event bursts into one re-arm sweep to avoid a reconnection storm.
558
+ _defineProperty(SyncBlockSubscriptionManager, "WAKE_DEBOUNCE_MS", 1000);
@@ -188,16 +188,20 @@ export const classifyFetchErrorReason = error => {
188
188
  * `gateEnabled` is injected by the caller (the store managers evaluate `fg(...)`) so this
189
189
  * helper stays pure and trivially unit-testable for both gate states.
190
190
  */
191
- export const buildFetchErrorAttribution = (gateEnabled, error, statusCode) => {
191
+ export const buildFetchErrorAttribution = (gateEnabled, error, statusCode, deferred) => {
192
192
  if (!gateEnabled) {
193
193
  return undefined;
194
194
  }
195
195
  const reason = classifyFetchErrorReason(error);
196
196
  return {
197
197
  reason,
198
- benign: FETCH_BENIGN_REASONS.has(reason),
198
+ // A deferred (tab-hidden) exhaustion is benign regardless of reason bucket.
199
+ benign: deferred === true || FETCH_BENIGN_REASONS.has(reason),
199
200
  ...(statusCode !== undefined && {
200
201
  statusCode
202
+ }),
203
+ ...(deferred === true && {
204
+ deferred: true
201
205
  })
202
206
  };
203
207
  };
@@ -246,6 +250,11 @@ export function getErrorPayload(actionSubjectId, error, resourceId, sourceProduc
246
250
  // `attribution.benign` is accessible.
247
251
  ...(attribution && 'benign' in attribution && {
248
252
  benign: attribution.benign
253
+ }),
254
+ // `deferred` is only present on fetch/subscribe attribution, and only when a
255
+ // tab-hidden exhaustion was deferred rather than surfaced. Boolean, never UGC.
256
+ ...(attribution && 'deferred' in attribution && attribution.deferred === true && {
257
+ deferred: true
249
258
  })
250
259
  }
251
260
  };
@@ -339,15 +348,50 @@ export const createSuccessPayload = (resourceId, sourceProduct) => {
339
348
  };
340
349
 
341
350
  /**
342
- * Operational `syncedBlockCreate` success event (previously only error rows
343
- * existed, so dashboards had no create denominator). Fired behind
344
- * `platform_editor_blocks_patch_4` with the `blockInstanceId` join key.
351
+ * Optional enrichment for the `syncedBlockCreate` success event. All fields
352
+ * optional so gate-off/legacy payloads are unchanged. `inputMethod`: creating
353
+ * surface (enum, PII-safe). `createdEmpty`: true from an empty selection, false
354
+ * when content was converted.
355
+ */
356
+
357
+ /**
358
+ * Operational `syncedBlockCreate` success event, behind
359
+ * `platform_editor_blocks_patch_4`, with the `blockInstanceId` join key and,
360
+ * when available, the `inputMethod` + `createdEmpty` creation-type signals.
345
361
  */
346
- export const createSuccessOperationalPayload = (resourceId, blockInstanceId, sourceProduct) => ({
362
+ export const createSuccessOperationalPayload = (resourceId, blockInstanceId, sourceProduct, enrichment) => ({
347
363
  action: ACTION.INSERTED,
348
364
  actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
349
365
  actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_CREATE,
350
366
  eventType: EVENT_TYPE.OPERATIONAL,
367
+ attributes: {
368
+ resourceId,
369
+ ...(blockInstanceId && {
370
+ blockInstanceId
371
+ }),
372
+ ...(sourceProduct && {
373
+ sourceProduct
374
+ }),
375
+ ...((enrichment === null || enrichment === void 0 ? void 0 : enrichment.inputMethod) && {
376
+ inputMethod: enrichment.inputMethod
377
+ }),
378
+ ...((enrichment === null || enrichment === void 0 ? void 0 : enrichment.createdEmpty) !== undefined && {
379
+ createdEmpty: enrichment.createdEmpty
380
+ })
381
+ }
382
+ });
383
+
384
+ /**
385
+ * Operational first-content-added event, behind
386
+ * `platform_editor_blocks_patch_4`. Fired once when a block created empty first
387
+ * gains user content. Join keys only (`resourceId` + `blockInstanceId`), no user
388
+ * content (PII-safe).
389
+ */
390
+ export const addContentSuccessPayload = (resourceId, blockInstanceId, sourceProduct) => ({
391
+ action: ACTION.ADDED,
392
+ actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
393
+ actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK_ADD_CONTENT,
394
+ eventType: EVENT_TYPE.OPERATIONAL,
351
395
  attributes: {
352
396
  resourceId,
353
397
  ...(blockInstanceId && {
@@ -1013,7 +1013,7 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1013
1013
  _context4.next = 1;
1014
1014
  break;
1015
1015
  }
1016
- return _context4.abrupt("return", fg('platform_editor_blocks_patch_2'));
1016
+ return _context4.abrupt("return", true);
1017
1017
  case 1:
1018
1018
  if (this.isCacheDirty) {
1019
1019
  _context4.next = 2;