@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.
- package/CHANGELOG.md +24 -0
- package/dist/cjs/store-manager/referenceSyncBlockStoreManager.js +1 -1
- package/dist/cjs/store-manager/sourceSyncBlockStoreManager.js +72 -13
- package/dist/cjs/store-manager/syncBlockSubscriptionManager.js +216 -66
- package/dist/cjs/utils/errorHandling.js +47 -9
- package/dist/es2019/store-manager/referenceSyncBlockStoreManager.js +1 -1
- package/dist/es2019/store-manager/sourceSyncBlockStoreManager.js +79 -22
- package/dist/es2019/store-manager/syncBlockSubscriptionManager.js +133 -7
- package/dist/es2019/utils/errorHandling.js +50 -6
- package/dist/esm/store-manager/referenceSyncBlockStoreManager.js +1 -1
- package/dist/esm/store-manager/sourceSyncBlockStoreManager.js +73 -14
- package/dist/esm/store-manager/syncBlockSubscriptionManager.js +216 -66
- package/dist/esm/utils/errorHandling.js +46 -8
- package/dist/types/entry-points/errorHandling.d.ts +1 -0
- package/dist/types/store-manager/sourceSyncBlockStoreManager.d.ts +27 -1
- package/dist/types/store-manager/syncBlockSubscriptionManager.d.ts +28 -0
- package/dist/types/utils/errorHandling.d.ts +31 -6
- package/package.json +5 -6
|
@@ -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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
476
|
-
(_this$
|
|
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$
|
|
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$
|
|
533
|
-
(_this$
|
|
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$
|
|
599
|
+
var _this$fireAnalyticsEv14;
|
|
545
600
|
this.setPendingDeletion(Ids, false);
|
|
546
|
-
(_this$
|
|
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$
|
|
761
|
+
var _this$fireAnalyticsEv15;
|
|
707
762
|
logException(error, {
|
|
708
763
|
location: 'editor-synced-block-provider/sourceSyncBlockStoreManager'
|
|
709
764
|
});
|
|
710
|
-
(_this$
|
|
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$
|
|
776
|
+
var _this$fireAnalyticsEv16;
|
|
722
777
|
logException(error, {
|
|
723
778
|
location: 'editor-synced-block-provider/sourceSyncBlockStoreManager'
|
|
724
779
|
});
|
|
725
|
-
(_this$
|
|
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$
|
|
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$
|
|
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$
|
|
411
|
-
(_this$deps$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
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",
|
|
1016
|
+
return _context4.abrupt("return", true);
|
|
1017
1017
|
case 1:
|
|
1018
1018
|
if (this.isCacheDirty) {
|
|
1019
1019
|
_context4.next = 2;
|