@atlaskit/editor-synced-block-provider 8.6.0 → 8.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/cjs/store-manager/referenceSyncBlockStoreManager.js +1 -1
- package/dist/cjs/store-manager/syncBlockSubscriptionManager.js +216 -66
- package/dist/cjs/utils/errorHandling.js +9 -4
- package/dist/es2019/store-manager/referenceSyncBlockStoreManager.js +1 -1
- package/dist/es2019/store-manager/syncBlockSubscriptionManager.js +133 -7
- package/dist/es2019/utils/errorHandling.js +11 -2
- package/dist/esm/store-manager/referenceSyncBlockStoreManager.js +1 -1
- package/dist/esm/store-manager/syncBlockSubscriptionManager.js +216 -66
- package/dist/esm/utils/errorHandling.js +9 -4
- package/dist/types/store-manager/syncBlockSubscriptionManager.d.ts +28 -0
- package/dist/types/utils/errorHandling.d.ts +9 -1
- package/package.json +5 -6
|
@@ -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
|
};
|
|
@@ -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;
|