@atlaskit/editor-plugin-synced-block 8.2.6 → 8.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 8.2.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [`27f53bba7e425`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/27f53bba7e425) -
8
+ EDITOR-6930: Refactor decorations prop to map status decorations in apply() instead of rebuilding
9
+ via doc.descendants() every transaction. Behind editor_synced_block_perf experiment.
10
+ - Updated dependencies
11
+
12
+ ## 8.2.7
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies
17
+
3
18
  ## 8.2.6
4
19
 
5
20
  ### Patch Changes
@@ -99,9 +99,7 @@ var showExtensionInSyncBlockWarningIfNeeded = function showExtensionInSyncBlockW
99
99
  var tr = _ref2.tr;
100
100
  return tr.setMeta(syncedBlockPluginKey, {
101
101
  activeFlag: {
102
- id: (0, _experiments.editorExperiment)('platform_synced_block_patch_6', true, {
103
- exposure: true
104
- }) ? _types.FLAG_ID.EXTENSION_IN_SYNC_BLOCK : _types.FLAG_ID.INLINE_EXTENSION_IN_SYNC_BLOCK
102
+ id: _types.FLAG_ID.EXTENSION_IN_SYNC_BLOCK
105
103
  }
106
104
  });
107
105
  });
@@ -206,6 +204,48 @@ var filterTransactionOffline = function filterTransactionOffline(_ref4) {
206
204
  return true;
207
205
  };
208
206
 
207
+ /**
208
+ * Build the status decoration set for sync-block nodes. This performs a full
209
+ * `doc.descendants()` walk so it must only be called when a status signal
210
+ * actually changes (offline, view-mode, dragging, pending-creation). Between
211
+ * status changes the caller should use `decorationSet.map(tr.mapping, tr.doc)`
212
+ * instead (see EDITOR-6930).
213
+ */
214
+ var buildStatusDecorations = function buildStatusDecorations(doc, syncBlockStore, isOffline, isViewMode, isDragging) {
215
+ // Fast path: when all status flags are off and no creations are in flight,
216
+ // no node can produce a decoration — skip the full doc traversal.
217
+ if (!isOffline && !isViewMode && !isDragging && !syncBlockStore.sourceManager.hasPendingCreations()) {
218
+ return _view.DecorationSet.empty;
219
+ }
220
+ var offlineDecorations = [];
221
+ var viewModeDecorations = [];
222
+ var loadingDecorations = [];
223
+ var dragDecorations = [];
224
+ doc.descendants(function (node, pos) {
225
+ if (node.type.name === 'bodiedSyncBlock' && isOffline) {
226
+ offlineDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
227
+ class: _syncBlock.SyncBlockStateCssClassName.disabledClassName
228
+ }));
229
+ }
230
+ if (syncBlockStore.isSyncBlock(node) && isViewMode) {
231
+ viewModeDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
232
+ class: _syncBlock.SyncBlockStateCssClassName.viewModeClassName
233
+ }));
234
+ }
235
+ if (node.type.name === 'bodiedSyncBlock' && syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
236
+ loadingDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
237
+ class: _syncBlock.SyncBlockStateCssClassName.creationLoadingClassName
238
+ }));
239
+ }
240
+ if (isDragging && syncBlockStore.isSyncBlock(node)) {
241
+ dragDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
242
+ class: _syncBlock.SyncBlockStateCssClassName.draggingClassName
243
+ }));
244
+ }
245
+ });
246
+ return _view.DecorationSet.create(doc, [].concat(offlineDecorations, viewModeDecorations, loadingDecorations, dragDecorations));
247
+ };
248
+
209
249
  /**
210
250
  * Encapsulates mutable state that persists across transactions in the
211
251
  * synced block plugin. Replaces module-level closure variables so state
@@ -282,6 +322,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
282
322
  key: syncedBlockPluginKey,
283
323
  state: {
284
324
  init: function init(_, instance) {
325
+ var _api$connectivity2, _api$editorViewMode, _api$userIntent;
285
326
  // When `editor_synced_block_perf` is ON and the document has no
286
327
  // synced blocks, we skip the eager fetch + cache walks. They will be
287
328
  // re-run lazily by `apply` the first time a synced block enters the
@@ -306,13 +347,28 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
306
347
  }
307
348
  }
308
349
  }
350
+
351
+ // Read initial shared-state signals for status decorations
352
+ var initIsOffline = (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity2 = api.connectivity) === null || _api$connectivity2 === void 0 || (_api$connectivity2 = _api$connectivity2.sharedState.currentState()) === null || _api$connectivity2 === void 0 ? void 0 : _api$connectivity2.mode);
353
+ var initIsViewMode = (api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode) === 'view';
354
+ var initIsDragging = (api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 || (_api$userIntent = _api$userIntent.sharedState.currentState()) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.currentUserIntent) === 'dragging';
355
+
356
+ // Build initial status decoration set (EDITOR-6930).
357
+ // When the perf gate is ON and the doc has synced blocks we do a
358
+ // single traversal here; afterwards `apply()` will map or rebuild
359
+ // only when a status signal changes.
360
+ var initStatusDecorationSet = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true) && docHasSyncedBlocks ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : _view.DecorationSet.empty;
309
361
  return {
310
362
  selectionDecorationSet: (0, _selectionDecorations.calculateDecorations)(instance.doc, instance.selection, instance.schema),
311
363
  activeFlag: false,
312
364
  syncBlockStore: syncBlockStore,
313
365
  retryCreationPosMap: new Map(),
314
366
  hasSyncedBlocks: docHasSyncedBlocks,
315
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges()
367
+ hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
368
+ statusDecorationSet: initStatusDecorationSet,
369
+ prevIsOffline: initIsOffline,
370
+ prevIsViewMode: initIsViewMode,
371
+ prevIsDragging: initIsDragging
316
372
  };
317
373
  },
318
374
  apply: function apply(tr, currentPluginState, oldEditorState) {
@@ -322,7 +378,11 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
322
378
  selectionDecorationSet = currentPluginState.selectionDecorationSet,
323
379
  bodiedSyncBlockDeletionStatus = currentPluginState.bodiedSyncBlockDeletionStatus,
324
380
  retryCreationPosMap = currentPluginState.retryCreationPosMap,
325
- prevHasSyncedBlocks = currentPluginState.hasSyncedBlocks;
381
+ prevHasSyncedBlocks = currentPluginState.hasSyncedBlocks,
382
+ prevStatusDecorationSet = currentPluginState.statusDecorationSet,
383
+ prevOffline = currentPluginState.prevIsOffline,
384
+ prevViewMode = currentPluginState.prevIsViewMode,
385
+ prevDragging = currentPluginState.prevIsDragging;
326
386
 
327
387
  // Lazy-init bookkeeping: once a synced block enters the document we
328
388
  // flip `hasSyncedBlocks` to `true` for the lifetime of this editor
@@ -347,6 +407,41 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
347
407
  newDecorationSet = (0, _selectionDecorations.calculateDecorations)(tr.doc, tr.selection, tr.doc.type.schema);
348
408
  }
349
409
  }
410
+
411
+ // --- Status decoration set (EDITOR-6930) ---
412
+ // When the perf gate is ON we maintain `statusDecorationSet` in
413
+ // plugin state so the `decorations` prop becomes an O(1) lookup.
414
+ var nextStatusDecorationSet = prevStatusDecorationSet;
415
+ var nextIsOffline = prevOffline;
416
+ var nextIsViewMode = prevViewMode;
417
+ var nextIsDragging = prevDragging;
418
+ if ((0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
419
+ if (!nextHasSyncedBlocks) {
420
+ // No synced blocks → keep empty status decorations
421
+ nextStatusDecorationSet = _view.DecorationSet.empty;
422
+ } else {
423
+ var _api$connectivity3, _api$editorViewMode2, _api$userIntent2;
424
+ // Read current shared-state signals
425
+ nextIsOffline = (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity3 = api.connectivity) === null || _api$connectivity3 === void 0 || (_api$connectivity3 = _api$connectivity3.sharedState.currentState()) === null || _api$connectivity3 === void 0 ? void 0 : _api$connectivity3.mode);
426
+ nextIsViewMode = (api === null || api === void 0 || (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 || (_api$editorViewMode2 = _api$editorViewMode2.sharedState.currentState()) === null || _api$editorViewMode2 === void 0 ? void 0 : _api$editorViewMode2.mode) === 'view';
427
+ nextIsDragging = (api === null || api === void 0 || (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 || (_api$userIntent2 = _api$userIntent2.sharedState.currentState()) === null || _api$userIntent2 === void 0 ? void 0 : _api$userIntent2.currentUserIntent) === 'dragging';
428
+
429
+ // Determine whether we need a full rebuild or a cheap map
430
+ var hasSyncedBlocksJustFlipped = nextHasSyncedBlocks && !prevHasSyncedBlocks;
431
+ var statusSignalChanged = nextIsOffline !== prevOffline || nextIsViewMode !== prevViewMode || nextIsDragging !== prevDragging;
432
+ // Meta-driven status changes (e.g. pending creation
433
+ // completed, retry creation pos updated)
434
+ var hasMetaStatusChange = !!(meta !== null && meta !== void 0 && meta.retryCreationPos) || !!(meta !== null && meta !== void 0 && meta.activeFlag);
435
+ if (hasSyncedBlocksJustFlipped || statusSignalChanged || hasMetaStatusChange) {
436
+ // Full rebuild — a status signal changed
437
+ nextStatusDecorationSet = buildStatusDecorations(tr.doc, syncBlockStore, nextIsOffline, nextIsViewMode, nextIsDragging);
438
+ } else if (tr.docChanged) {
439
+ // Cheap map — positions shifted but status unchanged
440
+ nextStatusDecorationSet = prevStatusDecorationSet.map(tr.mapping, tr.doc);
441
+ }
442
+ // else: nothing changed, keep same reference
443
+ }
444
+ }
350
445
  var newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
351
446
  var newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
352
447
  return {
@@ -356,7 +451,11 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
356
451
  retryCreationPosMap: newRetryCreationPosMap,
357
452
  hasSyncedBlocks: nextHasSyncedBlocks,
358
453
  bodiedSyncBlockDeletionStatus: (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus,
359
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges()
454
+ hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
455
+ statusDecorationSet: nextStatusDecorationSet,
456
+ prevIsOffline: nextIsOffline,
457
+ prevIsViewMode: nextIsViewMode,
458
+ prevIsDragging: nextIsDragging
360
459
  };
361
460
  }
362
461
  },
@@ -395,57 +494,81 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
395
494
  })
396
495
  },
397
496
  decorations: function decorations(state) {
398
- var _currentPluginState$s, _api$connectivity2, _api$editorViewMode, _api$userIntent, _api$focus;
399
497
  var currentPluginState = syncedBlockPluginKey.getState(state);
400
- var selectionDecorationSet = (_currentPluginState$s = currentPluginState === null || currentPluginState === void 0 ? void 0 : currentPluginState.selectionDecorationSet) !== null && _currentPluginState$s !== void 0 ? _currentPluginState$s : _view.DecorationSet.empty;
401
- var syncBlockStore = currentPluginState === null || currentPluginState === void 0 ? void 0 : currentPluginState.syncBlockStore;
402
- var doc = state.doc;
403
-
404
- // Lazy-init: when no synced block exists in the doc, skip the
405
- // `descendants` walk and the 4 shared-state reads below. The
406
- // selection decoration set is the only thing that can be
407
- // non-empty in this state.
408
- if (currentPluginState && !currentPluginState.hasSyncedBlocks && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
409
- return selectionDecorationSet;
498
+ if (!currentPluginState) {
499
+ return _view.DecorationSet.empty;
410
500
  }
411
- var isOffline = (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity2 = api.connectivity) === null || _api$connectivity2 === void 0 || (_api$connectivity2 = _api$connectivity2.sharedState.currentState()) === null || _api$connectivity2 === void 0 ? void 0 : _api$connectivity2.mode);
412
- var isViewMode = (api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode) === 'view';
413
- var isDragging = (api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 || (_api$userIntent = _api$userIntent.sharedState.currentState()) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.currentUserIntent) === 'dragging';
414
- var offlineDecorations = [];
415
- var viewModeDecorations = [];
416
- var loadingDecorations = [];
417
- var dragDecorations = [];
418
- state.doc.descendants(function (node, pos) {
419
- if (node.type.name === 'bodiedSyncBlock' && isOffline) {
420
- offlineDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
421
- class: _syncBlock.SyncBlockStateCssClassName.disabledClassName
422
- }));
423
- }
424
- if (syncBlockStore.isSyncBlock(node) && isViewMode) {
425
- viewModeDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
426
- class: _syncBlock.SyncBlockStateCssClassName.viewModeClassName
427
- }));
428
- }
429
- if (node.type.name === 'bodiedSyncBlock' && syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
430
- loadingDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
431
- class: _syncBlock.SyncBlockStateCssClassName.creationLoadingClassName
432
- }));
501
+ var selectionDecorationSet = currentPluginState.selectionDecorationSet,
502
+ statusDecorationSet = currentPluginState.statusDecorationSet,
503
+ docHasSyncedBlocks = currentPluginState.hasSyncedBlocks;
504
+
505
+ // When the perf gate is ON, both `selectionDecorationSet` and
506
+ // `statusDecorationSet` are maintained in plugin state by
507
+ // `apply()`. The `decorations` prop is now an O(1) merge of
508
+ // the two cached sets — no `doc.descendants()` walk, no
509
+ // shared-state reads.
510
+ if ((0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
511
+ var _api$focus$sharedStat, _api$focus;
512
+ if (!docHasSyncedBlocks) {
513
+ return selectionDecorationSet;
433
514
  }
434
515
 
435
- // Show sync block border while the user is dragging
436
- if (isDragging && syncBlockStore.isSyncBlock(node)) {
437
- dragDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
438
- class: _syncBlock.SyncBlockStateCssClassName.draggingClassName
439
- }));
516
+ // Focus state is read live here (single cheap read) because
517
+ // it only gates whether selection decorations are included —
518
+ // it does not affect the status decoration set and can change
519
+ // within the same transaction cycle.
520
+ var hasFocus = (_api$focus$sharedStat = api === null || api === void 0 || (_api$focus = api.focus) === null || _api$focus === void 0 || (_api$focus = _api$focus.sharedState) === null || _api$focus === void 0 || (_api$focus = _api$focus.currentState()) === null || _api$focus === void 0 ? void 0 : _api$focus.hasFocus) !== null && _api$focus$sharedStat !== void 0 ? _api$focus$sharedStat : true;
521
+
522
+ // Merge selection + status decorations.
523
+ // When the editor is unfocused,
524
+ // omit selection decorations (matches old behaviour).
525
+ var statusDecorations = statusDecorationSet.find();
526
+ if (statusDecorations.length === 0) {
527
+ return hasFocus ? selectionDecorationSet : _view.DecorationSet.empty;
528
+ } else {
529
+ return hasFocus ? selectionDecorationSet.add(state.doc, statusDecorations) : statusDecorationSet;
440
530
  }
441
- });
442
- if (api !== null && api !== void 0 && (_api$focus = api.focus) !== null && _api$focus !== void 0 && (_api$focus = _api$focus.sharedState) !== null && _api$focus !== void 0 && (_api$focus = _api$focus.currentState()) !== null && _api$focus !== void 0 && _api$focus.hasFocus || !(0, _experiments.editorExperiment)('platform_synced_block_patch_6', true, {
443
- exposure: true
444
- })) {
445
- // Don't show decorations if the editor is not focused
446
- return selectionDecorationSet.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
447
531
  } else {
448
- return _view.DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
532
+ var _api$connectivity4, _api$editorViewMode3, _api$userIntent3, _api$focus2;
533
+ // --- Legacy path (perf gate OFF) ---
534
+ // Full `doc.descendants()` walk every transaction. Preserved
535
+ // for safe rollback.
536
+ var _syncBlockStore = currentPluginState.syncBlockStore;
537
+ var doc = state.doc;
538
+ var isOffline = (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity4 = api.connectivity) === null || _api$connectivity4 === void 0 || (_api$connectivity4 = _api$connectivity4.sharedState.currentState()) === null || _api$connectivity4 === void 0 ? void 0 : _api$connectivity4.mode);
539
+ var isViewMode = (api === null || api === void 0 || (_api$editorViewMode3 = api.editorViewMode) === null || _api$editorViewMode3 === void 0 || (_api$editorViewMode3 = _api$editorViewMode3.sharedState.currentState()) === null || _api$editorViewMode3 === void 0 ? void 0 : _api$editorViewMode3.mode) === 'view';
540
+ var isDragging = (api === null || api === void 0 || (_api$userIntent3 = api.userIntent) === null || _api$userIntent3 === void 0 || (_api$userIntent3 = _api$userIntent3.sharedState.currentState()) === null || _api$userIntent3 === void 0 ? void 0 : _api$userIntent3.currentUserIntent) === 'dragging';
541
+ var offlineDecorations = [];
542
+ var viewModeDecorations = [];
543
+ var loadingDecorations = [];
544
+ var dragDecorations = [];
545
+ state.doc.descendants(function (node, pos) {
546
+ if (node.type.name === 'bodiedSyncBlock' && isOffline) {
547
+ offlineDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
548
+ class: _syncBlock.SyncBlockStateCssClassName.disabledClassName
549
+ }));
550
+ }
551
+ if (_syncBlockStore.isSyncBlock(node) && isViewMode) {
552
+ viewModeDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
553
+ class: _syncBlock.SyncBlockStateCssClassName.viewModeClassName
554
+ }));
555
+ }
556
+ if (node.type.name === 'bodiedSyncBlock' && _syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
557
+ loadingDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
558
+ class: _syncBlock.SyncBlockStateCssClassName.creationLoadingClassName
559
+ }));
560
+ }
561
+ if (isDragging && _syncBlockStore.isSyncBlock(node)) {
562
+ dragDecorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {
563
+ class: _syncBlock.SyncBlockStateCssClassName.draggingClassName
564
+ }));
565
+ }
566
+ });
567
+ if (api !== null && api !== void 0 && (_api$focus2 = api.focus) !== null && _api$focus2 !== void 0 && (_api$focus2 = _api$focus2.sharedState) !== null && _api$focus2 !== void 0 && (_api$focus2 = _api$focus2.currentState()) !== null && _api$focus2 !== void 0 && _api$focus2.hasFocus) {
568
+ return selectionDecorationSet.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
569
+ } else {
570
+ return _view.DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
571
+ }
449
572
  }
450
573
  },
451
574
  handleClickOn: (0, _selection.createSelectionClickHandler)(['bodiedSyncBlock'], function (target) {
@@ -503,7 +626,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
503
626
  }
504
627
  },
505
628
  filterTransaction: function filterTransaction(tr, state) {
506
- var _api$editorViewMode2, _api$connectivity3;
629
+ var _api$editorViewMode4, _api$connectivity5;
507
630
  // Lazy-init: when no synced block currently exists in the doc and the
508
631
  // transaction does not insert one, all downstream filter logic is a
509
632
  // no-op. Avoid both the shared-state reads and the `trackSyncBlocks`
@@ -514,11 +637,11 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
514
637
  return true;
515
638
  }
516
639
  }
517
- var viewMode = api === null || api === void 0 || (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 || (_api$editorViewMode2 = _api$editorViewMode2.sharedState.currentState()) === null || _api$editorViewMode2 === void 0 ? void 0 : _api$editorViewMode2.mode;
640
+ var viewMode = api === null || api === void 0 || (_api$editorViewMode4 = api.editorViewMode) === null || _api$editorViewMode4 === void 0 || (_api$editorViewMode4 = _api$editorViewMode4.sharedState.currentState()) === null || _api$editorViewMode4 === void 0 ? void 0 : _api$editorViewMode4.mode;
518
641
  if (viewMode === 'view' && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_8')) {
519
642
  return true;
520
643
  }
521
- var isOffline = (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity3 = api.connectivity) === null || _api$connectivity3 === void 0 || (_api$connectivity3 = _api$connectivity3.sharedState.currentState()) === null || _api$connectivity3 === void 0 ? void 0 : _api$connectivity3.mode);
644
+ var isOffline = (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity5 = api.connectivity) === null || _api$connectivity5 === void 0 || (_api$connectivity5 = _api$connectivity5.sharedState.currentState()) === null || _api$connectivity5 === void 0 ? void 0 : _api$connectivity5.mode);
522
645
  var isConfirmedSyncBlockDeletion = Boolean(tr.getMeta('isConfirmedSyncBlockDeletion'));
523
646
 
524
647
  // Track newly added reference sync blocks before processing the transaction
@@ -593,7 +716,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
593
716
  });
594
717
  },
595
718
  appendTransaction: function appendTransaction(trs, oldState, newState) {
596
- var _api$editorViewMode3;
719
+ var _api$editorViewMode5;
597
720
  // Lazy-init: when neither the previous nor the new state contains a
598
721
  // synced block (and none of the dispatched transactions inserts one),
599
722
  // skip all downstream work. This is the hot path on the ~99.97% of
@@ -606,7 +729,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
606
729
  return null;
607
730
  }
608
731
  }
609
- var viewMode = api === null || api === void 0 || (_api$editorViewMode3 = api.editorViewMode) === null || _api$editorViewMode3 === void 0 || (_api$editorViewMode3 = _api$editorViewMode3.sharedState.currentState()) === null || _api$editorViewMode3 === void 0 ? void 0 : _api$editorViewMode3.mode;
732
+ var viewMode = api === null || api === void 0 || (_api$editorViewMode5 = api.editorViewMode) === null || _api$editorViewMode5 === void 0 || (_api$editorViewMode5 = _api$editorViewMode5.sharedState.currentState()) === null || _api$editorViewMode5 === void 0 ? void 0 : _api$editorViewMode5.mode;
610
733
  if (viewMode === 'view' && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_8')) {
611
734
  return null;
612
735
  }
@@ -74,9 +74,7 @@ const showExtensionInSyncBlockWarningIfNeeded = (tr, state, api, extensionFlagSh
74
74
  tr
75
75
  }) => tr.setMeta(syncedBlockPluginKey, {
76
76
  activeFlag: {
77
- id: editorExperiment('platform_synced_block_patch_6', true, {
78
- exposure: true
79
- }) ? FLAG_ID.EXTENSION_IN_SYNC_BLOCK : FLAG_ID.INLINE_EXTENSION_IN_SYNC_BLOCK
77
+ id: FLAG_ID.EXTENSION_IN_SYNC_BLOCK
80
78
  }
81
79
  }));
82
80
  });
@@ -183,6 +181,48 @@ const filterTransactionOffline = ({
183
181
  return true;
184
182
  };
185
183
 
184
+ /**
185
+ * Build the status decoration set for sync-block nodes. This performs a full
186
+ * `doc.descendants()` walk so it must only be called when a status signal
187
+ * actually changes (offline, view-mode, dragging, pending-creation). Between
188
+ * status changes the caller should use `decorationSet.map(tr.mapping, tr.doc)`
189
+ * instead (see EDITOR-6930).
190
+ */
191
+ const buildStatusDecorations = (doc, syncBlockStore, isOffline, isViewMode, isDragging) => {
192
+ // Fast path: when all status flags are off and no creations are in flight,
193
+ // no node can produce a decoration — skip the full doc traversal.
194
+ if (!isOffline && !isViewMode && !isDragging && !syncBlockStore.sourceManager.hasPendingCreations()) {
195
+ return DecorationSet.empty;
196
+ }
197
+ const offlineDecorations = [];
198
+ const viewModeDecorations = [];
199
+ const loadingDecorations = [];
200
+ const dragDecorations = [];
201
+ doc.descendants((node, pos) => {
202
+ if (node.type.name === 'bodiedSyncBlock' && isOffline) {
203
+ offlineDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
204
+ class: SyncBlockStateCssClassName.disabledClassName
205
+ }));
206
+ }
207
+ if (syncBlockStore.isSyncBlock(node) && isViewMode) {
208
+ viewModeDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
209
+ class: SyncBlockStateCssClassName.viewModeClassName
210
+ }));
211
+ }
212
+ if (node.type.name === 'bodiedSyncBlock' && syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
213
+ loadingDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
214
+ class: SyncBlockStateCssClassName.creationLoadingClassName
215
+ }));
216
+ }
217
+ if (isDragging && syncBlockStore.isSyncBlock(node)) {
218
+ dragDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
219
+ class: SyncBlockStateCssClassName.draggingClassName
220
+ }));
221
+ }
222
+ });
223
+ return DecorationSet.create(doc, [...offlineDecorations, ...viewModeDecorations, ...loadingDecorations, ...dragDecorations]);
224
+ };
225
+
186
226
  /**
187
227
  * Encapsulates mutable state that persists across transactions in the
188
228
  * synced block plugin. Replaces module-level closure variables so state
@@ -249,6 +289,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
249
289
  key: syncedBlockPluginKey,
250
290
  state: {
251
291
  init(_, instance) {
292
+ var _api$connectivity2, _api$connectivity2$sh, _api$editorViewMode, _api$editorViewMode$s, _api$userIntent, _api$userIntent$share;
252
293
  // When `editor_synced_block_perf` is ON and the document has no
253
294
  // synced blocks, we skip the eager fetch + cache walks. They will be
254
295
  // re-run lazily by `apply` the first time a synced block enters the
@@ -273,13 +314,28 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
273
314
  }
274
315
  }
275
316
  }
317
+
318
+ // Read initial shared-state signals for status decorations
319
+ const initIsOffline = isOfflineMode(api === null || api === void 0 ? void 0 : (_api$connectivity2 = api.connectivity) === null || _api$connectivity2 === void 0 ? void 0 : (_api$connectivity2$sh = _api$connectivity2.sharedState.currentState()) === null || _api$connectivity2$sh === void 0 ? void 0 : _api$connectivity2$sh.mode);
320
+ const initIsViewMode = (api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode) === 'view';
321
+ const initIsDragging = (api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : (_api$userIntent$share = _api$userIntent.sharedState.currentState()) === null || _api$userIntent$share === void 0 ? void 0 : _api$userIntent$share.currentUserIntent) === 'dragging';
322
+
323
+ // Build initial status decoration set (EDITOR-6930).
324
+ // When the perf gate is ON and the doc has synced blocks we do a
325
+ // single traversal here; afterwards `apply()` will map or rebuild
326
+ // only when a status signal changes.
327
+ const initStatusDecorationSet = expValEquals('editor_synced_block_perf', 'isEnabled', true) && docHasSyncedBlocks ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : DecorationSet.empty;
276
328
  return {
277
329
  selectionDecorationSet: calculateDecorations(instance.doc, instance.selection, instance.schema),
278
330
  activeFlag: false,
279
331
  syncBlockStore: syncBlockStore,
280
332
  retryCreationPosMap: new Map(),
281
333
  hasSyncedBlocks: docHasSyncedBlocks,
282
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges()
334
+ hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
335
+ statusDecorationSet: initStatusDecorationSet,
336
+ prevIsOffline: initIsOffline,
337
+ prevIsViewMode: initIsViewMode,
338
+ prevIsDragging: initIsDragging
283
339
  };
284
340
  },
285
341
  apply: (tr, currentPluginState, oldEditorState) => {
@@ -290,7 +346,11 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
290
346
  selectionDecorationSet,
291
347
  bodiedSyncBlockDeletionStatus,
292
348
  retryCreationPosMap,
293
- hasSyncedBlocks: prevHasSyncedBlocks
349
+ hasSyncedBlocks: prevHasSyncedBlocks,
350
+ statusDecorationSet: prevStatusDecorationSet,
351
+ prevIsOffline: prevOffline,
352
+ prevIsViewMode: prevViewMode,
353
+ prevIsDragging: prevDragging
294
354
  } = currentPluginState;
295
355
 
296
356
  // Lazy-init bookkeeping: once a synced block enters the document we
@@ -316,6 +376,41 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
316
376
  newDecorationSet = calculateDecorations(tr.doc, tr.selection, tr.doc.type.schema);
317
377
  }
318
378
  }
379
+
380
+ // --- Status decoration set (EDITOR-6930) ---
381
+ // When the perf gate is ON we maintain `statusDecorationSet` in
382
+ // plugin state so the `decorations` prop becomes an O(1) lookup.
383
+ let nextStatusDecorationSet = prevStatusDecorationSet;
384
+ let nextIsOffline = prevOffline;
385
+ let nextIsViewMode = prevViewMode;
386
+ let nextIsDragging = prevDragging;
387
+ if (expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
388
+ if (!nextHasSyncedBlocks) {
389
+ // No synced blocks → keep empty status decorations
390
+ nextStatusDecorationSet = DecorationSet.empty;
391
+ } else {
392
+ var _api$connectivity3, _api$connectivity3$sh, _api$editorViewMode2, _api$editorViewMode2$, _api$userIntent2, _api$userIntent2$shar;
393
+ // Read current shared-state signals
394
+ nextIsOffline = isOfflineMode(api === null || api === void 0 ? void 0 : (_api$connectivity3 = api.connectivity) === null || _api$connectivity3 === void 0 ? void 0 : (_api$connectivity3$sh = _api$connectivity3.sharedState.currentState()) === null || _api$connectivity3$sh === void 0 ? void 0 : _api$connectivity3$sh.mode);
395
+ nextIsViewMode = (api === null || api === void 0 ? void 0 : (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 ? void 0 : (_api$editorViewMode2$ = _api$editorViewMode2.sharedState.currentState()) === null || _api$editorViewMode2$ === void 0 ? void 0 : _api$editorViewMode2$.mode) === 'view';
396
+ nextIsDragging = (api === null || api === void 0 ? void 0 : (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 ? void 0 : (_api$userIntent2$shar = _api$userIntent2.sharedState.currentState()) === null || _api$userIntent2$shar === void 0 ? void 0 : _api$userIntent2$shar.currentUserIntent) === 'dragging';
397
+
398
+ // Determine whether we need a full rebuild or a cheap map
399
+ const hasSyncedBlocksJustFlipped = nextHasSyncedBlocks && !prevHasSyncedBlocks;
400
+ const statusSignalChanged = nextIsOffline !== prevOffline || nextIsViewMode !== prevViewMode || nextIsDragging !== prevDragging;
401
+ // Meta-driven status changes (e.g. pending creation
402
+ // completed, retry creation pos updated)
403
+ const hasMetaStatusChange = !!(meta !== null && meta !== void 0 && meta.retryCreationPos) || !!(meta !== null && meta !== void 0 && meta.activeFlag);
404
+ if (hasSyncedBlocksJustFlipped || statusSignalChanged || hasMetaStatusChange) {
405
+ // Full rebuild — a status signal changed
406
+ nextStatusDecorationSet = buildStatusDecorations(tr.doc, syncBlockStore, nextIsOffline, nextIsViewMode, nextIsDragging);
407
+ } else if (tr.docChanged) {
408
+ // Cheap map — positions shifted but status unchanged
409
+ nextStatusDecorationSet = prevStatusDecorationSet.map(tr.mapping, tr.doc);
410
+ }
411
+ // else: nothing changed, keep same reference
412
+ }
413
+ }
319
414
  const newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
320
415
  const newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
321
416
  return {
@@ -325,7 +420,11 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
325
420
  retryCreationPosMap: newRetryCreationPosMap,
326
421
  hasSyncedBlocks: nextHasSyncedBlocks,
327
422
  bodiedSyncBlockDeletionStatus: (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus,
328
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges()
423
+ hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
424
+ statusDecorationSet: nextStatusDecorationSet,
425
+ prevIsOffline: nextIsOffline,
426
+ prevIsViewMode: nextIsViewMode,
427
+ prevIsDragging: nextIsDragging
329
428
  };
330
429
  }
331
430
  },
@@ -361,59 +460,85 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
361
460
  })
362
461
  },
363
462
  decorations: state => {
364
- var _currentPluginState$s, _api$connectivity2, _api$connectivity2$sh, _api$editorViewMode, _api$editorViewMode$s, _api$userIntent, _api$userIntent$share, _api$focus, _api$focus$sharedStat, _api$focus$sharedStat2;
365
463
  const currentPluginState = syncedBlockPluginKey.getState(state);
366
- const selectionDecorationSet = (_currentPluginState$s = currentPluginState === null || currentPluginState === void 0 ? void 0 : currentPluginState.selectionDecorationSet) !== null && _currentPluginState$s !== void 0 ? _currentPluginState$s : DecorationSet.empty;
367
- const syncBlockStore = currentPluginState === null || currentPluginState === void 0 ? void 0 : currentPluginState.syncBlockStore;
464
+ if (!currentPluginState) {
465
+ return DecorationSet.empty;
466
+ }
368
467
  const {
369
- doc
370
- } = state;
468
+ selectionDecorationSet,
469
+ statusDecorationSet,
470
+ hasSyncedBlocks: docHasSyncedBlocks
471
+ } = currentPluginState;
371
472
 
372
- // Lazy-init: when no synced block exists in the doc, skip the
373
- // `descendants` walk and the 4 shared-state reads below. The
374
- // selection decoration set is the only thing that can be
375
- // non-empty in this state.
376
- if (currentPluginState && !currentPluginState.hasSyncedBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
377
- return selectionDecorationSet;
378
- }
379
- const isOffline = isOfflineMode(api === null || api === void 0 ? void 0 : (_api$connectivity2 = api.connectivity) === null || _api$connectivity2 === void 0 ? void 0 : (_api$connectivity2$sh = _api$connectivity2.sharedState.currentState()) === null || _api$connectivity2$sh === void 0 ? void 0 : _api$connectivity2$sh.mode);
380
- const isViewMode = (api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode) === 'view';
381
- const isDragging = (api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : (_api$userIntent$share = _api$userIntent.sharedState.currentState()) === null || _api$userIntent$share === void 0 ? void 0 : _api$userIntent$share.currentUserIntent) === 'dragging';
382
- const offlineDecorations = [];
383
- const viewModeDecorations = [];
384
- const loadingDecorations = [];
385
- const dragDecorations = [];
386
- state.doc.descendants((node, pos) => {
387
- if (node.type.name === 'bodiedSyncBlock' && isOffline) {
388
- offlineDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
389
- class: SyncBlockStateCssClassName.disabledClassName
390
- }));
391
- }
392
- if (syncBlockStore.isSyncBlock(node) && isViewMode) {
393
- viewModeDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
394
- class: SyncBlockStateCssClassName.viewModeClassName
395
- }));
396
- }
397
- if (node.type.name === 'bodiedSyncBlock' && syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
398
- loadingDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
399
- class: SyncBlockStateCssClassName.creationLoadingClassName
400
- }));
473
+ // When the perf gate is ON, both `selectionDecorationSet` and
474
+ // `statusDecorationSet` are maintained in plugin state by
475
+ // `apply()`. The `decorations` prop is now an O(1) merge of
476
+ // the two cached sets — no `doc.descendants()` walk, no
477
+ // shared-state reads.
478
+ if (expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
479
+ var _api$focus$sharedStat, _api$focus, _api$focus$sharedStat2, _api$focus$sharedStat3;
480
+ if (!docHasSyncedBlocks) {
481
+ return selectionDecorationSet;
401
482
  }
402
483
 
403
- // Show sync block border while the user is dragging
404
- if (isDragging && syncBlockStore.isSyncBlock(node)) {
405
- dragDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
406
- class: SyncBlockStateCssClassName.draggingClassName
407
- }));
484
+ // Focus state is read live here (single cheap read) because
485
+ // it only gates whether selection decorations are included —
486
+ // it does not affect the status decoration set and can change
487
+ // within the same transaction cycle.
488
+ const hasFocus = (_api$focus$sharedStat = api === null || api === void 0 ? void 0 : (_api$focus = api.focus) === null || _api$focus === void 0 ? void 0 : (_api$focus$sharedStat2 = _api$focus.sharedState) === null || _api$focus$sharedStat2 === void 0 ? void 0 : (_api$focus$sharedStat3 = _api$focus$sharedStat2.currentState()) === null || _api$focus$sharedStat3 === void 0 ? void 0 : _api$focus$sharedStat3.hasFocus) !== null && _api$focus$sharedStat !== void 0 ? _api$focus$sharedStat : true;
489
+
490
+ // Merge selection + status decorations.
491
+ // When the editor is unfocused,
492
+ // omit selection decorations (matches old behaviour).
493
+ const statusDecorations = statusDecorationSet.find();
494
+ if (statusDecorations.length === 0) {
495
+ return hasFocus ? selectionDecorationSet : DecorationSet.empty;
496
+ } else {
497
+ return hasFocus ? selectionDecorationSet.add(state.doc, statusDecorations) : statusDecorationSet;
408
498
  }
409
- });
410
- if (api !== null && api !== void 0 && (_api$focus = api.focus) !== null && _api$focus !== void 0 && (_api$focus$sharedStat = _api$focus.sharedState) !== null && _api$focus$sharedStat !== void 0 && (_api$focus$sharedStat2 = _api$focus$sharedStat.currentState()) !== null && _api$focus$sharedStat2 !== void 0 && _api$focus$sharedStat2.hasFocus || !editorExperiment('platform_synced_block_patch_6', true, {
411
- exposure: true
412
- })) {
413
- // Don't show decorations if the editor is not focused
414
- return selectionDecorationSet.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
415
499
  } else {
416
- return DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
500
+ var _api$connectivity4, _api$connectivity4$sh, _api$editorViewMode3, _api$editorViewMode3$, _api$userIntent3, _api$userIntent3$shar, _api$focus2, _api$focus2$sharedSta, _api$focus2$sharedSta2;
501
+ // --- Legacy path (perf gate OFF) ---
502
+ // Full `doc.descendants()` walk every transaction. Preserved
503
+ // for safe rollback.
504
+ const syncBlockStore = currentPluginState.syncBlockStore;
505
+ const {
506
+ doc
507
+ } = state;
508
+ const isOffline = isOfflineMode(api === null || api === void 0 ? void 0 : (_api$connectivity4 = api.connectivity) === null || _api$connectivity4 === void 0 ? void 0 : (_api$connectivity4$sh = _api$connectivity4.sharedState.currentState()) === null || _api$connectivity4$sh === void 0 ? void 0 : _api$connectivity4$sh.mode);
509
+ const isViewMode = (api === null || api === void 0 ? void 0 : (_api$editorViewMode3 = api.editorViewMode) === null || _api$editorViewMode3 === void 0 ? void 0 : (_api$editorViewMode3$ = _api$editorViewMode3.sharedState.currentState()) === null || _api$editorViewMode3$ === void 0 ? void 0 : _api$editorViewMode3$.mode) === 'view';
510
+ const isDragging = (api === null || api === void 0 ? void 0 : (_api$userIntent3 = api.userIntent) === null || _api$userIntent3 === void 0 ? void 0 : (_api$userIntent3$shar = _api$userIntent3.sharedState.currentState()) === null || _api$userIntent3$shar === void 0 ? void 0 : _api$userIntent3$shar.currentUserIntent) === 'dragging';
511
+ const offlineDecorations = [];
512
+ const viewModeDecorations = [];
513
+ const loadingDecorations = [];
514
+ const dragDecorations = [];
515
+ state.doc.descendants((node, pos) => {
516
+ if (node.type.name === 'bodiedSyncBlock' && isOffline) {
517
+ offlineDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
518
+ class: SyncBlockStateCssClassName.disabledClassName
519
+ }));
520
+ }
521
+ if (syncBlockStore.isSyncBlock(node) && isViewMode) {
522
+ viewModeDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
523
+ class: SyncBlockStateCssClassName.viewModeClassName
524
+ }));
525
+ }
526
+ if (node.type.name === 'bodiedSyncBlock' && syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
527
+ loadingDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
528
+ class: SyncBlockStateCssClassName.creationLoadingClassName
529
+ }));
530
+ }
531
+ if (isDragging && syncBlockStore.isSyncBlock(node)) {
532
+ dragDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
533
+ class: SyncBlockStateCssClassName.draggingClassName
534
+ }));
535
+ }
536
+ });
537
+ if (api !== null && api !== void 0 && (_api$focus2 = api.focus) !== null && _api$focus2 !== void 0 && (_api$focus2$sharedSta = _api$focus2.sharedState) !== null && _api$focus2$sharedSta !== void 0 && (_api$focus2$sharedSta2 = _api$focus2$sharedSta.currentState()) !== null && _api$focus2$sharedSta2 !== void 0 && _api$focus2$sharedSta2.hasFocus) {
538
+ return selectionDecorationSet.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
539
+ } else {
540
+ return DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
541
+ }
417
542
  }
418
543
  },
419
544
  handleClickOn: createSelectionClickHandler(['bodiedSyncBlock'], target => !!target.closest(`.${BodiedSyncBlockSharedCssClassName.prefix}`), {
@@ -473,7 +598,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
473
598
  }
474
599
  },
475
600
  filterTransaction: (tr, state) => {
476
- var _api$editorViewMode2, _api$editorViewMode2$, _api$connectivity3, _api$connectivity3$sh;
601
+ var _api$editorViewMode4, _api$editorViewMode4$, _api$connectivity5, _api$connectivity5$sh;
477
602
  // Lazy-init: when no synced block currently exists in the doc and the
478
603
  // transaction does not insert one, all downstream filter logic is a
479
604
  // no-op. Avoid both the shared-state reads and the `trackSyncBlocks`
@@ -484,11 +609,11 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
484
609
  return true;
485
610
  }
486
611
  }
487
- const viewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 ? void 0 : (_api$editorViewMode2$ = _api$editorViewMode2.sharedState.currentState()) === null || _api$editorViewMode2$ === void 0 ? void 0 : _api$editorViewMode2$.mode;
612
+ const viewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode4 = api.editorViewMode) === null || _api$editorViewMode4 === void 0 ? void 0 : (_api$editorViewMode4$ = _api$editorViewMode4.sharedState.currentState()) === null || _api$editorViewMode4$ === void 0 ? void 0 : _api$editorViewMode4$.mode;
488
613
  if (viewMode === 'view' && fg('platform_synced_block_patch_8')) {
489
614
  return true;
490
615
  }
491
- const isOffline = isOfflineMode(api === null || api === void 0 ? void 0 : (_api$connectivity3 = api.connectivity) === null || _api$connectivity3 === void 0 ? void 0 : (_api$connectivity3$sh = _api$connectivity3.sharedState.currentState()) === null || _api$connectivity3$sh === void 0 ? void 0 : _api$connectivity3$sh.mode);
616
+ const isOffline = isOfflineMode(api === null || api === void 0 ? void 0 : (_api$connectivity5 = api.connectivity) === null || _api$connectivity5 === void 0 ? void 0 : (_api$connectivity5$sh = _api$connectivity5.sharedState.currentState()) === null || _api$connectivity5$sh === void 0 ? void 0 : _api$connectivity5$sh.mode);
492
617
  const isConfirmedSyncBlockDeletion = Boolean(tr.getMeta('isConfirmedSyncBlockDeletion'));
493
618
 
494
619
  // Track newly added reference sync blocks before processing the transaction
@@ -556,7 +681,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
556
681
  });
557
682
  },
558
683
  appendTransaction: (trs, oldState, newState) => {
559
- var _api$editorViewMode3, _api$editorViewMode3$;
684
+ var _api$editorViewMode5, _api$editorViewMode5$;
560
685
  // Lazy-init: when neither the previous nor the new state contains a
561
686
  // synced block (and none of the dispatched transactions inserts one),
562
687
  // skip all downstream work. This is the hot path on the ~99.97% of
@@ -569,7 +694,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
569
694
  return null;
570
695
  }
571
696
  }
572
- const viewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode3 = api.editorViewMode) === null || _api$editorViewMode3 === void 0 ? void 0 : (_api$editorViewMode3$ = _api$editorViewMode3.sharedState.currentState()) === null || _api$editorViewMode3$ === void 0 ? void 0 : _api$editorViewMode3$.mode;
697
+ const viewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode5 = api.editorViewMode) === null || _api$editorViewMode5 === void 0 ? void 0 : (_api$editorViewMode5$ = _api$editorViewMode5.sharedState.currentState()) === null || _api$editorViewMode5$ === void 0 ? void 0 : _api$editorViewMode5$.mode;
573
698
  if (viewMode === 'view' && fg('platform_synced_block_patch_8')) {
574
699
  return null;
575
700
  }
@@ -92,9 +92,7 @@ var showExtensionInSyncBlockWarningIfNeeded = function showExtensionInSyncBlockW
92
92
  var tr = _ref2.tr;
93
93
  return tr.setMeta(syncedBlockPluginKey, {
94
94
  activeFlag: {
95
- id: editorExperiment('platform_synced_block_patch_6', true, {
96
- exposure: true
97
- }) ? FLAG_ID.EXTENSION_IN_SYNC_BLOCK : FLAG_ID.INLINE_EXTENSION_IN_SYNC_BLOCK
95
+ id: FLAG_ID.EXTENSION_IN_SYNC_BLOCK
98
96
  }
99
97
  });
100
98
  });
@@ -199,6 +197,48 @@ var filterTransactionOffline = function filterTransactionOffline(_ref4) {
199
197
  return true;
200
198
  };
201
199
 
200
+ /**
201
+ * Build the status decoration set for sync-block nodes. This performs a full
202
+ * `doc.descendants()` walk so it must only be called when a status signal
203
+ * actually changes (offline, view-mode, dragging, pending-creation). Between
204
+ * status changes the caller should use `decorationSet.map(tr.mapping, tr.doc)`
205
+ * instead (see EDITOR-6930).
206
+ */
207
+ var buildStatusDecorations = function buildStatusDecorations(doc, syncBlockStore, isOffline, isViewMode, isDragging) {
208
+ // Fast path: when all status flags are off and no creations are in flight,
209
+ // no node can produce a decoration — skip the full doc traversal.
210
+ if (!isOffline && !isViewMode && !isDragging && !syncBlockStore.sourceManager.hasPendingCreations()) {
211
+ return DecorationSet.empty;
212
+ }
213
+ var offlineDecorations = [];
214
+ var viewModeDecorations = [];
215
+ var loadingDecorations = [];
216
+ var dragDecorations = [];
217
+ doc.descendants(function (node, pos) {
218
+ if (node.type.name === 'bodiedSyncBlock' && isOffline) {
219
+ offlineDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
220
+ class: SyncBlockStateCssClassName.disabledClassName
221
+ }));
222
+ }
223
+ if (syncBlockStore.isSyncBlock(node) && isViewMode) {
224
+ viewModeDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
225
+ class: SyncBlockStateCssClassName.viewModeClassName
226
+ }));
227
+ }
228
+ if (node.type.name === 'bodiedSyncBlock' && syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
229
+ loadingDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
230
+ class: SyncBlockStateCssClassName.creationLoadingClassName
231
+ }));
232
+ }
233
+ if (isDragging && syncBlockStore.isSyncBlock(node)) {
234
+ dragDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
235
+ class: SyncBlockStateCssClassName.draggingClassName
236
+ }));
237
+ }
238
+ });
239
+ return DecorationSet.create(doc, [].concat(offlineDecorations, viewModeDecorations, loadingDecorations, dragDecorations));
240
+ };
241
+
202
242
  /**
203
243
  * Encapsulates mutable state that persists across transactions in the
204
244
  * synced block plugin. Replaces module-level closure variables so state
@@ -275,6 +315,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
275
315
  key: syncedBlockPluginKey,
276
316
  state: {
277
317
  init: function init(_, instance) {
318
+ var _api$connectivity2, _api$editorViewMode, _api$userIntent;
278
319
  // When `editor_synced_block_perf` is ON and the document has no
279
320
  // synced blocks, we skip the eager fetch + cache walks. They will be
280
321
  // re-run lazily by `apply` the first time a synced block enters the
@@ -299,13 +340,28 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
299
340
  }
300
341
  }
301
342
  }
343
+
344
+ // Read initial shared-state signals for status decorations
345
+ var initIsOffline = isOfflineMode(api === null || api === void 0 || (_api$connectivity2 = api.connectivity) === null || _api$connectivity2 === void 0 || (_api$connectivity2 = _api$connectivity2.sharedState.currentState()) === null || _api$connectivity2 === void 0 ? void 0 : _api$connectivity2.mode);
346
+ var initIsViewMode = (api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode) === 'view';
347
+ var initIsDragging = (api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 || (_api$userIntent = _api$userIntent.sharedState.currentState()) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.currentUserIntent) === 'dragging';
348
+
349
+ // Build initial status decoration set (EDITOR-6930).
350
+ // When the perf gate is ON and the doc has synced blocks we do a
351
+ // single traversal here; afterwards `apply()` will map or rebuild
352
+ // only when a status signal changes.
353
+ var initStatusDecorationSet = expValEquals('editor_synced_block_perf', 'isEnabled', true) && docHasSyncedBlocks ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : DecorationSet.empty;
302
354
  return {
303
355
  selectionDecorationSet: calculateDecorations(instance.doc, instance.selection, instance.schema),
304
356
  activeFlag: false,
305
357
  syncBlockStore: syncBlockStore,
306
358
  retryCreationPosMap: new Map(),
307
359
  hasSyncedBlocks: docHasSyncedBlocks,
308
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges()
360
+ hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
361
+ statusDecorationSet: initStatusDecorationSet,
362
+ prevIsOffline: initIsOffline,
363
+ prevIsViewMode: initIsViewMode,
364
+ prevIsDragging: initIsDragging
309
365
  };
310
366
  },
311
367
  apply: function apply(tr, currentPluginState, oldEditorState) {
@@ -315,7 +371,11 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
315
371
  selectionDecorationSet = currentPluginState.selectionDecorationSet,
316
372
  bodiedSyncBlockDeletionStatus = currentPluginState.bodiedSyncBlockDeletionStatus,
317
373
  retryCreationPosMap = currentPluginState.retryCreationPosMap,
318
- prevHasSyncedBlocks = currentPluginState.hasSyncedBlocks;
374
+ prevHasSyncedBlocks = currentPluginState.hasSyncedBlocks,
375
+ prevStatusDecorationSet = currentPluginState.statusDecorationSet,
376
+ prevOffline = currentPluginState.prevIsOffline,
377
+ prevViewMode = currentPluginState.prevIsViewMode,
378
+ prevDragging = currentPluginState.prevIsDragging;
319
379
 
320
380
  // Lazy-init bookkeeping: once a synced block enters the document we
321
381
  // flip `hasSyncedBlocks` to `true` for the lifetime of this editor
@@ -340,6 +400,41 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
340
400
  newDecorationSet = calculateDecorations(tr.doc, tr.selection, tr.doc.type.schema);
341
401
  }
342
402
  }
403
+
404
+ // --- Status decoration set (EDITOR-6930) ---
405
+ // When the perf gate is ON we maintain `statusDecorationSet` in
406
+ // plugin state so the `decorations` prop becomes an O(1) lookup.
407
+ var nextStatusDecorationSet = prevStatusDecorationSet;
408
+ var nextIsOffline = prevOffline;
409
+ var nextIsViewMode = prevViewMode;
410
+ var nextIsDragging = prevDragging;
411
+ if (expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
412
+ if (!nextHasSyncedBlocks) {
413
+ // No synced blocks → keep empty status decorations
414
+ nextStatusDecorationSet = DecorationSet.empty;
415
+ } else {
416
+ var _api$connectivity3, _api$editorViewMode2, _api$userIntent2;
417
+ // Read current shared-state signals
418
+ nextIsOffline = isOfflineMode(api === null || api === void 0 || (_api$connectivity3 = api.connectivity) === null || _api$connectivity3 === void 0 || (_api$connectivity3 = _api$connectivity3.sharedState.currentState()) === null || _api$connectivity3 === void 0 ? void 0 : _api$connectivity3.mode);
419
+ nextIsViewMode = (api === null || api === void 0 || (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 || (_api$editorViewMode2 = _api$editorViewMode2.sharedState.currentState()) === null || _api$editorViewMode2 === void 0 ? void 0 : _api$editorViewMode2.mode) === 'view';
420
+ nextIsDragging = (api === null || api === void 0 || (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 || (_api$userIntent2 = _api$userIntent2.sharedState.currentState()) === null || _api$userIntent2 === void 0 ? void 0 : _api$userIntent2.currentUserIntent) === 'dragging';
421
+
422
+ // Determine whether we need a full rebuild or a cheap map
423
+ var hasSyncedBlocksJustFlipped = nextHasSyncedBlocks && !prevHasSyncedBlocks;
424
+ var statusSignalChanged = nextIsOffline !== prevOffline || nextIsViewMode !== prevViewMode || nextIsDragging !== prevDragging;
425
+ // Meta-driven status changes (e.g. pending creation
426
+ // completed, retry creation pos updated)
427
+ var hasMetaStatusChange = !!(meta !== null && meta !== void 0 && meta.retryCreationPos) || !!(meta !== null && meta !== void 0 && meta.activeFlag);
428
+ if (hasSyncedBlocksJustFlipped || statusSignalChanged || hasMetaStatusChange) {
429
+ // Full rebuild — a status signal changed
430
+ nextStatusDecorationSet = buildStatusDecorations(tr.doc, syncBlockStore, nextIsOffline, nextIsViewMode, nextIsDragging);
431
+ } else if (tr.docChanged) {
432
+ // Cheap map — positions shifted but status unchanged
433
+ nextStatusDecorationSet = prevStatusDecorationSet.map(tr.mapping, tr.doc);
434
+ }
435
+ // else: nothing changed, keep same reference
436
+ }
437
+ }
343
438
  var newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
344
439
  var newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
345
440
  return {
@@ -349,7 +444,11 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
349
444
  retryCreationPosMap: newRetryCreationPosMap,
350
445
  hasSyncedBlocks: nextHasSyncedBlocks,
351
446
  bodiedSyncBlockDeletionStatus: (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus,
352
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges()
447
+ hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
448
+ statusDecorationSet: nextStatusDecorationSet,
449
+ prevIsOffline: nextIsOffline,
450
+ prevIsViewMode: nextIsViewMode,
451
+ prevIsDragging: nextIsDragging
353
452
  };
354
453
  }
355
454
  },
@@ -388,57 +487,81 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
388
487
  })
389
488
  },
390
489
  decorations: function decorations(state) {
391
- var _currentPluginState$s, _api$connectivity2, _api$editorViewMode, _api$userIntent, _api$focus;
392
490
  var currentPluginState = syncedBlockPluginKey.getState(state);
393
- var selectionDecorationSet = (_currentPluginState$s = currentPluginState === null || currentPluginState === void 0 ? void 0 : currentPluginState.selectionDecorationSet) !== null && _currentPluginState$s !== void 0 ? _currentPluginState$s : DecorationSet.empty;
394
- var syncBlockStore = currentPluginState === null || currentPluginState === void 0 ? void 0 : currentPluginState.syncBlockStore;
395
- var doc = state.doc;
396
-
397
- // Lazy-init: when no synced block exists in the doc, skip the
398
- // `descendants` walk and the 4 shared-state reads below. The
399
- // selection decoration set is the only thing that can be
400
- // non-empty in this state.
401
- if (currentPluginState && !currentPluginState.hasSyncedBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
402
- return selectionDecorationSet;
491
+ if (!currentPluginState) {
492
+ return DecorationSet.empty;
403
493
  }
404
- var isOffline = isOfflineMode(api === null || api === void 0 || (_api$connectivity2 = api.connectivity) === null || _api$connectivity2 === void 0 || (_api$connectivity2 = _api$connectivity2.sharedState.currentState()) === null || _api$connectivity2 === void 0 ? void 0 : _api$connectivity2.mode);
405
- var isViewMode = (api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode) === 'view';
406
- var isDragging = (api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 || (_api$userIntent = _api$userIntent.sharedState.currentState()) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.currentUserIntent) === 'dragging';
407
- var offlineDecorations = [];
408
- var viewModeDecorations = [];
409
- var loadingDecorations = [];
410
- var dragDecorations = [];
411
- state.doc.descendants(function (node, pos) {
412
- if (node.type.name === 'bodiedSyncBlock' && isOffline) {
413
- offlineDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
414
- class: SyncBlockStateCssClassName.disabledClassName
415
- }));
416
- }
417
- if (syncBlockStore.isSyncBlock(node) && isViewMode) {
418
- viewModeDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
419
- class: SyncBlockStateCssClassName.viewModeClassName
420
- }));
421
- }
422
- if (node.type.name === 'bodiedSyncBlock' && syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
423
- loadingDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
424
- class: SyncBlockStateCssClassName.creationLoadingClassName
425
- }));
494
+ var selectionDecorationSet = currentPluginState.selectionDecorationSet,
495
+ statusDecorationSet = currentPluginState.statusDecorationSet,
496
+ docHasSyncedBlocks = currentPluginState.hasSyncedBlocks;
497
+
498
+ // When the perf gate is ON, both `selectionDecorationSet` and
499
+ // `statusDecorationSet` are maintained in plugin state by
500
+ // `apply()`. The `decorations` prop is now an O(1) merge of
501
+ // the two cached sets — no `doc.descendants()` walk, no
502
+ // shared-state reads.
503
+ if (expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
504
+ var _api$focus$sharedStat, _api$focus;
505
+ if (!docHasSyncedBlocks) {
506
+ return selectionDecorationSet;
426
507
  }
427
508
 
428
- // Show sync block border while the user is dragging
429
- if (isDragging && syncBlockStore.isSyncBlock(node)) {
430
- dragDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
431
- class: SyncBlockStateCssClassName.draggingClassName
432
- }));
509
+ // Focus state is read live here (single cheap read) because
510
+ // it only gates whether selection decorations are included —
511
+ // it does not affect the status decoration set and can change
512
+ // within the same transaction cycle.
513
+ var hasFocus = (_api$focus$sharedStat = api === null || api === void 0 || (_api$focus = api.focus) === null || _api$focus === void 0 || (_api$focus = _api$focus.sharedState) === null || _api$focus === void 0 || (_api$focus = _api$focus.currentState()) === null || _api$focus === void 0 ? void 0 : _api$focus.hasFocus) !== null && _api$focus$sharedStat !== void 0 ? _api$focus$sharedStat : true;
514
+
515
+ // Merge selection + status decorations.
516
+ // When the editor is unfocused,
517
+ // omit selection decorations (matches old behaviour).
518
+ var statusDecorations = statusDecorationSet.find();
519
+ if (statusDecorations.length === 0) {
520
+ return hasFocus ? selectionDecorationSet : DecorationSet.empty;
521
+ } else {
522
+ return hasFocus ? selectionDecorationSet.add(state.doc, statusDecorations) : statusDecorationSet;
433
523
  }
434
- });
435
- if (api !== null && api !== void 0 && (_api$focus = api.focus) !== null && _api$focus !== void 0 && (_api$focus = _api$focus.sharedState) !== null && _api$focus !== void 0 && (_api$focus = _api$focus.currentState()) !== null && _api$focus !== void 0 && _api$focus.hasFocus || !editorExperiment('platform_synced_block_patch_6', true, {
436
- exposure: true
437
- })) {
438
- // Don't show decorations if the editor is not focused
439
- return selectionDecorationSet.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
440
524
  } else {
441
- return DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
525
+ var _api$connectivity4, _api$editorViewMode3, _api$userIntent3, _api$focus2;
526
+ // --- Legacy path (perf gate OFF) ---
527
+ // Full `doc.descendants()` walk every transaction. Preserved
528
+ // for safe rollback.
529
+ var _syncBlockStore = currentPluginState.syncBlockStore;
530
+ var doc = state.doc;
531
+ var isOffline = isOfflineMode(api === null || api === void 0 || (_api$connectivity4 = api.connectivity) === null || _api$connectivity4 === void 0 || (_api$connectivity4 = _api$connectivity4.sharedState.currentState()) === null || _api$connectivity4 === void 0 ? void 0 : _api$connectivity4.mode);
532
+ var isViewMode = (api === null || api === void 0 || (_api$editorViewMode3 = api.editorViewMode) === null || _api$editorViewMode3 === void 0 || (_api$editorViewMode3 = _api$editorViewMode3.sharedState.currentState()) === null || _api$editorViewMode3 === void 0 ? void 0 : _api$editorViewMode3.mode) === 'view';
533
+ var isDragging = (api === null || api === void 0 || (_api$userIntent3 = api.userIntent) === null || _api$userIntent3 === void 0 || (_api$userIntent3 = _api$userIntent3.sharedState.currentState()) === null || _api$userIntent3 === void 0 ? void 0 : _api$userIntent3.currentUserIntent) === 'dragging';
534
+ var offlineDecorations = [];
535
+ var viewModeDecorations = [];
536
+ var loadingDecorations = [];
537
+ var dragDecorations = [];
538
+ state.doc.descendants(function (node, pos) {
539
+ if (node.type.name === 'bodiedSyncBlock' && isOffline) {
540
+ offlineDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
541
+ class: SyncBlockStateCssClassName.disabledClassName
542
+ }));
543
+ }
544
+ if (_syncBlockStore.isSyncBlock(node) && isViewMode) {
545
+ viewModeDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
546
+ class: SyncBlockStateCssClassName.viewModeClassName
547
+ }));
548
+ }
549
+ if (node.type.name === 'bodiedSyncBlock' && _syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
550
+ loadingDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
551
+ class: SyncBlockStateCssClassName.creationLoadingClassName
552
+ }));
553
+ }
554
+ if (isDragging && _syncBlockStore.isSyncBlock(node)) {
555
+ dragDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
556
+ class: SyncBlockStateCssClassName.draggingClassName
557
+ }));
558
+ }
559
+ });
560
+ if (api !== null && api !== void 0 && (_api$focus2 = api.focus) !== null && _api$focus2 !== void 0 && (_api$focus2 = _api$focus2.sharedState) !== null && _api$focus2 !== void 0 && (_api$focus2 = _api$focus2.currentState()) !== null && _api$focus2 !== void 0 && _api$focus2.hasFocus) {
561
+ return selectionDecorationSet.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
562
+ } else {
563
+ return DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
564
+ }
442
565
  }
443
566
  },
444
567
  handleClickOn: createSelectionClickHandler(['bodiedSyncBlock'], function (target) {
@@ -496,7 +619,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
496
619
  }
497
620
  },
498
621
  filterTransaction: function filterTransaction(tr, state) {
499
- var _api$editorViewMode2, _api$connectivity3;
622
+ var _api$editorViewMode4, _api$connectivity5;
500
623
  // Lazy-init: when no synced block currently exists in the doc and the
501
624
  // transaction does not insert one, all downstream filter logic is a
502
625
  // no-op. Avoid both the shared-state reads and the `trackSyncBlocks`
@@ -507,11 +630,11 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
507
630
  return true;
508
631
  }
509
632
  }
510
- var viewMode = api === null || api === void 0 || (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 || (_api$editorViewMode2 = _api$editorViewMode2.sharedState.currentState()) === null || _api$editorViewMode2 === void 0 ? void 0 : _api$editorViewMode2.mode;
633
+ var viewMode = api === null || api === void 0 || (_api$editorViewMode4 = api.editorViewMode) === null || _api$editorViewMode4 === void 0 || (_api$editorViewMode4 = _api$editorViewMode4.sharedState.currentState()) === null || _api$editorViewMode4 === void 0 ? void 0 : _api$editorViewMode4.mode;
511
634
  if (viewMode === 'view' && fg('platform_synced_block_patch_8')) {
512
635
  return true;
513
636
  }
514
- var isOffline = isOfflineMode(api === null || api === void 0 || (_api$connectivity3 = api.connectivity) === null || _api$connectivity3 === void 0 || (_api$connectivity3 = _api$connectivity3.sharedState.currentState()) === null || _api$connectivity3 === void 0 ? void 0 : _api$connectivity3.mode);
637
+ var isOffline = isOfflineMode(api === null || api === void 0 || (_api$connectivity5 = api.connectivity) === null || _api$connectivity5 === void 0 || (_api$connectivity5 = _api$connectivity5.sharedState.currentState()) === null || _api$connectivity5 === void 0 ? void 0 : _api$connectivity5.mode);
515
638
  var isConfirmedSyncBlockDeletion = Boolean(tr.getMeta('isConfirmedSyncBlockDeletion'));
516
639
 
517
640
  // Track newly added reference sync blocks before processing the transaction
@@ -586,7 +709,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
586
709
  });
587
710
  },
588
711
  appendTransaction: function appendTransaction(trs, oldState, newState) {
589
- var _api$editorViewMode3;
712
+ var _api$editorViewMode5;
590
713
  // Lazy-init: when neither the previous nor the new state contains a
591
714
  // synced block (and none of the dispatched transactions inserts one),
592
715
  // skip all downstream work. This is the hot path on the ~99.97% of
@@ -599,7 +722,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
599
722
  return null;
600
723
  }
601
724
  }
602
- var viewMode = api === null || api === void 0 || (_api$editorViewMode3 = api.editorViewMode) === null || _api$editorViewMode3 === void 0 || (_api$editorViewMode3 = _api$editorViewMode3.sharedState.currentState()) === null || _api$editorViewMode3 === void 0 ? void 0 : _api$editorViewMode3.mode;
725
+ var viewMode = api === null || api === void 0 || (_api$editorViewMode5 = api.editorViewMode) === null || _api$editorViewMode5 === void 0 || (_api$editorViewMode5 = _api$editorViewMode5.sharedState.currentState()) === null || _api$editorViewMode5 === void 0 ? void 0 : _api$editorViewMode5.mode;
603
726
  if (viewMode === 'view' && fg('platform_synced_block_patch_8')) {
604
727
  return null;
605
728
  }
@@ -21,8 +21,25 @@ type SyncedBlockPluginState = {
21
21
  */
22
22
  hasSyncedBlocks: boolean;
23
23
  hasUnsavedBodiedSyncBlockChanges?: boolean;
24
+ /**
25
+ * Cached previous values for shared-state signals. Used inside `apply()` to
26
+ * detect when a status change requires a full rebuild of `statusDecorationSet`
27
+ * instead of a cheap `map()` call. Only meaningful when
28
+ * `editor_synced_block_perf` is ON.
29
+ */
30
+ prevIsDragging: boolean;
31
+ prevIsOffline: boolean;
32
+ prevIsViewMode: boolean;
24
33
  retryCreationPosMap: RetryCreationPosMap;
25
34
  selectionDecorationSet: DecorationSet;
35
+ /**
36
+ * Cached decoration set for sync-block status decorations (offline overlay,
37
+ * view-mode class, creation-loading spinner, drag border). When the perf
38
+ * gate is ON this is computed in `apply()` and mapped through edits so the
39
+ * `decorations` prop becomes an O(1) lookup instead of a full
40
+ * `doc.descendants()` walk every transaction (see EDITOR-6930).
41
+ */
42
+ statusDecorationSet: DecorationSet;
26
43
  syncBlockStore: SyncBlockStoreManager;
27
44
  };
28
45
  export declare const createPlugin: (options: SyncedBlockPluginOptions | undefined, pmPluginFactoryParams: PMPluginFactoryParams, syncBlockStore: SyncBlockStoreManager, api?: ExtractInjectionAPI<SyncedBlockPlugin>) => SafePlugin<SyncedBlockPluginState>;
@@ -21,8 +21,25 @@ type SyncedBlockPluginState = {
21
21
  */
22
22
  hasSyncedBlocks: boolean;
23
23
  hasUnsavedBodiedSyncBlockChanges?: boolean;
24
+ /**
25
+ * Cached previous values for shared-state signals. Used inside `apply()` to
26
+ * detect when a status change requires a full rebuild of `statusDecorationSet`
27
+ * instead of a cheap `map()` call. Only meaningful when
28
+ * `editor_synced_block_perf` is ON.
29
+ */
30
+ prevIsDragging: boolean;
31
+ prevIsOffline: boolean;
32
+ prevIsViewMode: boolean;
24
33
  retryCreationPosMap: RetryCreationPosMap;
25
34
  selectionDecorationSet: DecorationSet;
35
+ /**
36
+ * Cached decoration set for sync-block status decorations (offline overlay,
37
+ * view-mode class, creation-loading spinner, drag border). When the perf
38
+ * gate is ON this is computed in `apply()` and mapped through edits so the
39
+ * `decorations` prop becomes an O(1) lookup instead of a full
40
+ * `doc.descendants()` walk every transaction (see EDITOR-6930).
41
+ */
42
+ statusDecorationSet: DecorationSet;
26
43
  syncBlockStore: SyncBlockStoreManager;
27
44
  };
28
45
  export declare const createPlugin: (options: SyncedBlockPluginOptions | undefined, pmPluginFactoryParams: PMPluginFactoryParams, syncBlockStore: SyncBlockStoreManager, api?: ExtractInjectionAPI<SyncedBlockPlugin>) => SafePlugin<SyncedBlockPluginState>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-synced-block",
3
- "version": "8.2.6",
3
+ "version": "8.2.8",
4
4
  "description": "SyncedBlock plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -54,7 +54,7 @@
54
54
  "@atlaskit/platform-feature-flags": "^1.1.0",
55
55
  "@atlaskit/primitives": "^19.0.0",
56
56
  "@atlaskit/spinner": "19.1.2",
57
- "@atlaskit/tmp-editor-statsig": "^76.0.0",
57
+ "@atlaskit/tmp-editor-statsig": "^77.1.0",
58
58
  "@atlaskit/tokens": "13.0.3",
59
59
  "@atlaskit/tooltip": "^22.0.0",
60
60
  "@atlaskit/visually-hidden": "^3.1.0",
@@ -64,7 +64,7 @@
64
64
  "date-fns": "^2.17.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "@atlaskit/editor-common": "^114.20.0",
67
+ "@atlaskit/editor-common": "^114.21.0",
68
68
  "react": "^18.2.0",
69
69
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
70
70
  },
@@ -1,120 +0,0 @@
1
- {
2
- "extends": "../../../../tsconfig.local-consumption.json",
3
- "compilerOptions": {
4
- "target": "es5",
5
- "outDir": "../../../../../jira/tsDist/@atlaskit__editor-plugin-synced-block/app",
6
- "rootDir": "../",
7
- "composite": true,
8
- "noCheck": true
9
- },
10
- "include": [
11
- "../src/**/*.ts",
12
- "../src/**/*.tsx"
13
- ],
14
- "exclude": [
15
- "../src/**/__tests__/*",
16
- "../src/**/*.test.*",
17
- "../src/**/test.*",
18
- "../src/**/examples.*",
19
- "../src/**/examples/*",
20
- "../src/**/examples/**/*",
21
- "../src/**/*.stories.*",
22
- "../src/**/stories/*",
23
- "../src/**/stories/**/*"
24
- ],
25
- "references": [
26
- {
27
- "path": "../../adf-schema/afm-jira/tsconfig.json"
28
- },
29
- {
30
- "path": "../../../design-system/button/afm-jira/tsconfig.json"
31
- },
32
- {
33
- "path": "../../../design-system/dropdown-menu/afm-jira/tsconfig.json"
34
- },
35
- {
36
- "path": "../../editor-json-transformer/afm-jira/tsconfig.json"
37
- },
38
- {
39
- "path": "../../editor-plugin-analytics/afm-jira/tsconfig.json"
40
- },
41
- {
42
- "path": "../../editor-plugin-block-menu/afm-jira/tsconfig.json"
43
- },
44
- {
45
- "path": "../../editor-plugin-connectivity/afm-jira/tsconfig.json"
46
- },
47
- {
48
- "path": "../../editor-plugin-content-format/afm-jira/tsconfig.json"
49
- },
50
- {
51
- "path": "../../editor-plugin-decorations/afm-jira/tsconfig.json"
52
- },
53
- {
54
- "path": "../../editor-plugin-floating-toolbar/afm-jira/tsconfig.json"
55
- },
56
- {
57
- "path": "../../editor-plugin-focus/afm-jira/tsconfig.json"
58
- },
59
- {
60
- "path": "../../editor-plugin-selection/afm-jira/tsconfig.json"
61
- },
62
- {
63
- "path": "../../editor-plugin-user-intent/afm-jira/tsconfig.json"
64
- },
65
- {
66
- "path": "../../editor-prosemirror/afm-jira/tsconfig.json"
67
- },
68
- {
69
- "path": "../../editor-shared-styles/afm-jira/tsconfig.json"
70
- },
71
- {
72
- "path": "../../editor-synced-block-provider/afm-jira/tsconfig.json"
73
- },
74
- {
75
- "path": "../../editor-toolbar/afm-jira/tsconfig.json"
76
- },
77
- {
78
- "path": "../../../design-system/flag/afm-jira/tsconfig.json"
79
- },
80
- {
81
- "path": "../../../design-system/icon/afm-jira/tsconfig.json"
82
- },
83
- {
84
- "path": "../../../design-system/icon-lab/afm-jira/tsconfig.json"
85
- },
86
- {
87
- "path": "../../../design-system/logo/afm-jira/tsconfig.json"
88
- },
89
- {
90
- "path": "../../../design-system/lozenge/afm-jira/tsconfig.json"
91
- },
92
- {
93
- "path": "../../../design-system/modal-dialog/afm-jira/tsconfig.json"
94
- },
95
- {
96
- "path": "../../../platform/feature-flags/afm-jira/tsconfig.json"
97
- },
98
- {
99
- "path": "../../../design-system/primitives/afm-jira/tsconfig.json"
100
- },
101
- {
102
- "path": "../../../design-system/spinner/afm-jira/tsconfig.json"
103
- },
104
- {
105
- "path": "../../tmp-editor-statsig/afm-jira/tsconfig.json"
106
- },
107
- {
108
- "path": "../../../design-system/tokens/afm-jira/tsconfig.json"
109
- },
110
- {
111
- "path": "../../../design-system/tooltip/afm-jira/tsconfig.json"
112
- },
113
- {
114
- "path": "../../../design-system/visually-hidden/afm-jira/tsconfig.json"
115
- },
116
- {
117
- "path": "../../editor-common/afm-jira/tsconfig.json"
118
- }
119
- ]
120
- }