@atlaskit/editor-plugin-synced-block 8.2.9 → 8.2.11

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/AGENTS.md CHANGED
@@ -65,24 +65,24 @@ src/
65
65
 
66
66
  ### Editor Actions
67
67
 
68
- This package exposes top-level **editor actions** (in `editor-actions/index.ts`)
69
- that products call from outside the plugin lifecycle:
68
+ This package exposes top-level **editor actions** (in `editor-actions/index.ts`) that products call
69
+ from outside the plugin lifecycle:
70
70
 
71
71
  - `flushBodiedSyncBlocks(store)` — flush all dirty source blocks
72
72
  - `flushSyncBlocks(store)` — flush reference manager (e.g. on save)
73
- - `discardUnpublishedSyncBlocks(store)` — delete unpublished blocks on cancel
74
- (added in EDITOR-6473; used by Confluence's editor cancel flow)
73
+ - `discardUnpublishedSyncBlocks(store)` — delete unpublished blocks on cancel (added in EDITOR-6473;
74
+ used by Confluence's editor cancel flow)
75
75
 
76
76
  ### Lazy Init & Perf (EDITOR-6928 / EDITOR-6930)
77
77
 
78
78
  Behind the `editor_synced_block_perf` experiment, `main.ts`:
79
- - Skips creating synced-block plugin state and node-views for documents
80
- with no synced blocks (`hasSyncedBlocks(doc)`).
81
- - Computes `statusDecorationSet` inside `apply()` and stores it on plugin
82
- state, then exposes it via an O(1) `decorations` prop instead of an
83
- O(n) `doc.descendants()` walk on every transaction.
84
- - Uses `sourceSyncBlockStoreManager.hasPendingCreations()` for an O(1)
85
- pending-creation early return in `buildStatusDecorations()`.
79
+
80
+ - Skips creating synced-block plugin state and node-views for documents with no synced blocks
81
+ (`hasSyncedBlocks(doc)`).
82
+ - Computes `statusDecorationSet` inside `apply()` and stores it on plugin state, then exposes it via
83
+ an O(1) `decorations` prop instead of an O(n) `doc.descendants()` walk on every transaction.
84
+ - Uses `sourceSyncBlockStoreManager.hasPendingCreations()` for an O(1) pending-creation early return
85
+ in `buildStatusDecorations()`.
86
86
 
87
87
  ### Key Code Patterns
88
88
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 8.2.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [`a160344820ea5`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/a160344820ea5) -
8
+ EDITOR-6929: Fix React re-render cascade by (1) returning same plugin state reference from apply()
9
+ when nothing changed, (2) memoizing getSharedState to return a stable reference, and (3) guarding
10
+ contentComponent to skip rendering when hasSyncedBlocks is false. All gated behind
11
+ editor_synced_block_perf experiment.
12
+ - Updated dependencies
13
+
14
+ ## 8.2.10
15
+
16
+ ### Patch Changes
17
+
18
+ - [`0a702a2b251d1`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/0a702a2b251d1) -
19
+ Guard menuAndToolbarExperiencesPlugin DOM listeners and floatingToolbar config behind
20
+ hasSyncedBlocks to avoid unnecessary work on pages without synced blocks
21
+ - Updated dependencies
22
+
3
23
  ## 8.2.9
4
24
 
5
25
  ### Patch Changes
@@ -357,7 +357,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
357
357
  // When the perf gate is ON and the doc has synced blocks we do a
358
358
  // single traversal here; afterwards `apply()` will map or rebuild
359
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;
360
+ var initStatusDecorationSet = docHasSyncedBlocks && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true) ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : _view.DecorationSet.empty;
361
361
  return {
362
362
  selectionDecorationSet: (0, _selectionDecorations.calculateDecorations)(instance.doc, instance.selection, instance.schema),
363
363
  activeFlag: false,
@@ -374,6 +374,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
374
374
  apply: function apply(tr, currentPluginState, oldEditorState) {
375
375
  var _meta$activeFlag, _meta$bodiedSyncBlock;
376
376
  var meta = tr.getMeta(syncedBlockPluginKey);
377
+ var isPerfExperimentOn = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true);
377
378
  var activeFlag = currentPluginState.activeFlag,
378
379
  selectionDecorationSet = currentPluginState.selectionDecorationSet,
379
380
  bodiedSyncBlockDeletionStatus = currentPluginState.bodiedSyncBlockDeletionStatus,
@@ -387,11 +388,19 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
387
388
  // Lazy-init bookkeeping: once a synced block enters the document we
388
389
  // flip `hasSyncedBlocks` to `true` for the lifetime of this editor
389
390
  var nextHasSyncedBlocks = prevHasSyncedBlocks;
390
- if (!prevHasSyncedBlocks && tr.docChanged && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
391
+ if (!prevHasSyncedBlocks && tr.docChanged && isPerfExperimentOn) {
391
392
  if ((0, _transactionInsertsSyncedBlock.transactionInsertsSyncedBlock)(tr)) {
392
393
  nextHasSyncedBlocks = true;
393
394
  }
394
395
  }
396
+
397
+ // --- Fast path (EDITOR-6929): when `hasSyncedBlocks` is false,
398
+ // no meta is set, and the selection/doc haven't changed in a way
399
+ // that affects our state, return the SAME object reference so
400
+ // SharedStateAPI skips notifying subscribers. ---
401
+ if (!nextHasSyncedBlocks && !meta && !tr.docChanged && tr.selection.eq(oldEditorState.selection) && isPerfExperimentOn) {
402
+ return currentPluginState;
403
+ }
395
404
  var newDecorationSet = tr.docChanged ? selectionDecorationSet.map(tr.mapping, tr.doc) // only map if document changed
396
405
  : selectionDecorationSet;
397
406
  if (!tr.selection.eq(oldEditorState.selection)) {
@@ -415,7 +424,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
415
424
  var nextIsOffline = prevOffline;
416
425
  var nextIsViewMode = prevViewMode;
417
426
  var nextIsDragging = prevDragging;
418
- if ((0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
427
+ if (isPerfExperimentOn) {
419
428
  if (!nextHasSyncedBlocks) {
420
429
  // No synced blocks → keep empty status decorations
421
430
  nextStatusDecorationSet = _view.DecorationSet.empty;
@@ -444,14 +453,24 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
444
453
  }
445
454
  var newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
446
455
  var newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
456
+ var nextActiveFlag = (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag;
457
+ var nextBodiedSyncBlockDeletionStatus = (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus;
458
+ var nextHasUnsavedBodiedSyncBlockChanges = syncBlockStore.sourceManager.hasUnsavedChanges();
459
+
460
+ // --- Reference equality (EDITOR-6929): return the same object
461
+ // when ALL fields are reference-equal to avoid SharedStateAPI
462
+ // notifying subscribers and triggering React re-renders. ---
463
+ if (nextActiveFlag === activeFlag && newDecorationSet === selectionDecorationSet && newRetryCreationPosMap === retryCreationPosMap && nextHasSyncedBlocks === prevHasSyncedBlocks && nextBodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && nextHasUnsavedBodiedSyncBlockChanges === currentPluginState.hasUnsavedBodiedSyncBlockChanges && nextStatusDecorationSet === prevStatusDecorationSet && nextIsOffline === prevOffline && nextIsViewMode === prevViewMode && nextIsDragging === prevDragging && isPerfExperimentOn) {
464
+ return currentPluginState;
465
+ }
447
466
  return {
448
- activeFlag: (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag,
467
+ activeFlag: nextActiveFlag,
449
468
  selectionDecorationSet: newDecorationSet,
450
469
  syncBlockStore: syncBlockStore,
451
470
  retryCreationPosMap: newRetryCreationPosMap,
452
471
  hasSyncedBlocks: nextHasSyncedBlocks,
453
- bodiedSyncBlockDeletionStatus: (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus,
454
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
472
+ bodiedSyncBlockDeletionStatus: nextBodiedSyncBlockDeletionStatus,
473
+ hasUnsavedBodiedSyncBlockChanges: nextHasUnsavedBodiedSyncBlockChanges,
455
474
  statusDecorationSet: nextStatusDecorationSet,
456
475
  prevIsOffline: nextIsOffline,
457
476
  prevIsViewMode: nextIsViewMode,
@@ -12,7 +12,9 @@ var _experiences = require("@atlaskit/editor-common/experiences");
12
12
  var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
13
13
  var _state = require("@atlaskit/editor-prosemirror/state");
14
14
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
15
+ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
15
16
  var _types = require("../types");
17
+ var _main = require("./main");
16
18
  var TIMEOUT_DURATION = 30000;
17
19
  var pluginKey = new _state.PluginKey('syncedBlockMenuAndToolbarExperience');
18
20
  var SYNCED_BLOCK_BUTTON_TEST_IDS = Object.values(_types.SYNCED_BLOCK_BUTTON_TEST_ID);
@@ -96,69 +98,106 @@ var getMenuAndToolbarExperiencesPlugin = exports.getMenuAndToolbarExperiencesPlu
96
98
  durationMs: TIMEOUT_DURATION
97
99
  }), syncedLocationsDropdownOpenedCheck()]
98
100
  });
99
- var unbindClickListener = (0, _bindEventListener.bind)(document, {
100
- type: 'click',
101
- listener: function listener(event) {
102
- var target = event.target;
103
- if (!target) {
104
- return;
105
- }
106
- var button = target.closest('button[data-testid]');
107
- if (!button || !(button instanceof HTMLButtonElement)) {
108
- return;
109
- }
110
- var testId = button.dataset.testid;
111
- if (!isSyncedBlockButtonId(testId)) {
112
- return;
113
- }
114
- if (button.disabled) {
115
- return;
116
- }
117
- handleButtonClick({
118
- testId: testId,
119
- button: button,
120
- createSourcePrimaryToolbarExperience: createSourcePrimaryToolbarExperience,
121
- createSourceBlockMenuExperience: createSourceBlockMenuExperience,
122
- createSourceQuickInsertMenuExperience: createSourceQuickInsertMenuExperience,
123
- deleteReferenceSyncedBlockExperience: deleteReferenceSyncedBlockExperience,
124
- unsyncReferenceSyncedBlockExperience: unsyncReferenceSyncedBlockExperience,
125
- unsyncSourceSyncedBlockExperience: unsyncSourceSyncedBlockExperience,
126
- deleteSourceSyncedBlockExperience: deleteSourceSyncedBlockExperience,
127
- syncedLocationsExperience: syncedLocationsExperience
128
- });
129
- },
130
- options: {
131
- capture: true
132
- }
133
- });
134
- var unbindKeydownListener = (0, _bindEventListener.bind)(document, {
135
- type: 'keydown',
136
- listener: function listener(event) {
137
- if (isEnterKey(event.key)) {
138
- var typeaheadPopup = (0, _experiences.popupWithNestedElement)(getPopupsTarget(), '.fabric-editor-typeahead');
139
- if (!typeaheadPopup || !(typeaheadPopup instanceof HTMLElement)) {
101
+ var bindListeners = function bindListeners() {
102
+ var unbindClickListener = (0, _bindEventListener.bind)(document, {
103
+ type: 'click',
104
+ listener: function listener(event) {
105
+ var target = event.target;
106
+ if (!target) {
107
+ return;
108
+ }
109
+ var button = target.closest('button[data-testid]');
110
+ if (!button || !(button instanceof HTMLButtonElement)) {
140
111
  return;
141
112
  }
142
- var targetElement = (0, _platformFeatureFlags.fg)('platform_synced_block_fix_experience_tracking') ? typeaheadPopup.querySelector('[role="option"][aria-selected="true"]') : typeaheadPopup.querySelector('[role="option"]');
143
- if (!targetElement || !(targetElement instanceof HTMLElement)) {
113
+ var testId = button.dataset.testid;
114
+ if (!isSyncedBlockButtonId(testId)) {
144
115
  return;
145
116
  }
146
- var testId = targetElement.dataset.testid;
147
- if (testId === _types.SYNCED_BLOCK_BUTTON_TEST_ID.quickInsertCreate) {
148
- createSourceQuickInsertMenuExperience.start();
117
+ if (button.disabled) {
118
+ return;
149
119
  }
120
+ handleButtonClick({
121
+ testId: testId,
122
+ button: button,
123
+ createSourcePrimaryToolbarExperience: createSourcePrimaryToolbarExperience,
124
+ createSourceBlockMenuExperience: createSourceBlockMenuExperience,
125
+ createSourceQuickInsertMenuExperience: createSourceQuickInsertMenuExperience,
126
+ deleteReferenceSyncedBlockExperience: deleteReferenceSyncedBlockExperience,
127
+ unsyncReferenceSyncedBlockExperience: unsyncReferenceSyncedBlockExperience,
128
+ unsyncSourceSyncedBlockExperience: unsyncSourceSyncedBlockExperience,
129
+ deleteSourceSyncedBlockExperience: deleteSourceSyncedBlockExperience,
130
+ syncedLocationsExperience: syncedLocationsExperience
131
+ });
132
+ },
133
+ options: {
134
+ capture: true
150
135
  }
151
- },
152
- options: {
153
- capture: true
154
- }
155
- });
136
+ });
137
+ var unbindKeydownListener = (0, _bindEventListener.bind)(document, {
138
+ type: 'keydown',
139
+ listener: function listener(event) {
140
+ if (isEnterKey(event.key)) {
141
+ var typeaheadPopup = (0, _experiences.popupWithNestedElement)(getPopupsTarget(), '.fabric-editor-typeahead');
142
+ if (!typeaheadPopup || !(typeaheadPopup instanceof HTMLElement)) {
143
+ return;
144
+ }
145
+ var targetElement = (0, _platformFeatureFlags.fg)('platform_synced_block_fix_experience_tracking') ? typeaheadPopup.querySelector('[role="option"][aria-selected="true"]') : typeaheadPopup.querySelector('[role="option"]');
146
+ if (!targetElement || !(targetElement instanceof HTMLElement)) {
147
+ return;
148
+ }
149
+ var testId = targetElement.dataset.testid;
150
+ if (testId === _types.SYNCED_BLOCK_BUTTON_TEST_ID.quickInsertCreate) {
151
+ createSourceQuickInsertMenuExperience.start();
152
+ }
153
+ }
154
+ },
155
+ options: {
156
+ capture: true
157
+ }
158
+ });
159
+ return {
160
+ unbindClickListener: unbindClickListener,
161
+ unbindKeydownListener: unbindKeydownListener
162
+ };
163
+ };
156
164
  return new _safePlugin.SafePlugin({
157
165
  key: pluginKey,
158
166
  view: function view(_view) {
167
+ var _syncedBlockPluginKey;
159
168
  editorViewRef.current = _view;
169
+
170
+ // Track whether listeners have been bound. When the experiment is
171
+ // ON and the document initially has no synced blocks, we defer
172
+ // binding until `update()` detects that `hasSyncedBlocks` has
173
+ // flipped to `true` (e.g. via paste or collab insert). This avoids
174
+ // the ~2-5 ms TBT cost of capture-phase click/keydown handlers on
175
+ // the ~99.97 % of pages that never use synced blocks (EDITOR-6931).
176
+ var listenersBound = false;
177
+ var unbindClickListener;
178
+ var unbindKeydownListener;
179
+ var ensureListenersBound = function ensureListenersBound() {
180
+ if (listenersBound) {
181
+ return;
182
+ }
183
+ listenersBound = true;
184
+ var unbinders = bindListeners();
185
+ unbindClickListener = unbinders.unbindClickListener;
186
+ unbindKeydownListener = unbinders.unbindKeydownListener;
187
+ };
188
+ if ((_syncedBlockPluginKey = _main.syncedBlockPluginKey.getState(_view.state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks || !(0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
189
+ ensureListenersBound();
190
+ }
160
191
  return {
192
+ update: function update(view, prevState) {
193
+ var _syncedBlockPluginKey2;
194
+ if (!listenersBound && view.state.doc !== prevState.doc && (_syncedBlockPluginKey2 = _main.syncedBlockPluginKey.getState(view.state)) !== null && _syncedBlockPluginKey2 !== void 0 && _syncedBlockPluginKey2.hasSyncedBlocks && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
195
+ // Bind listeners now that synced blocks are present.
196
+ ensureListenersBound();
197
+ }
198
+ },
161
199
  destroy: function destroy() {
200
+ var _unbindClickListener, _unbindKeydownListene;
162
201
  createSourcePrimaryToolbarExperience.abort({
163
202
  reason: 'editorDestroyed'
164
203
  });
@@ -183,8 +222,8 @@ var getMenuAndToolbarExperiencesPlugin = exports.getMenuAndToolbarExperiencesPlu
183
222
  syncedLocationsExperience === null || syncedLocationsExperience === void 0 || syncedLocationsExperience.abort({
184
223
  reason: 'editorDestroyed'
185
224
  });
186
- unbindClickListener();
187
- unbindKeydownListener();
225
+ (_unbindClickListener = unbindClickListener) === null || _unbindClickListener === void 0 || _unbindClickListener();
226
+ (_unbindKeydownListene = unbindKeydownListener) === null || _unbindKeydownListene === void 0 || _unbindKeydownListene();
188
227
  }
189
228
  };
190
229
  }
@@ -7,7 +7,9 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.syncedBlockPlugin = void 0;
8
8
  var _react = _interopRequireDefault(require("react"));
9
9
  var _adfSchema = require("@atlaskit/adf-schema");
10
+ var _hooks = require("@atlaskit/editor-common/hooks");
10
11
  var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider");
12
+ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
11
13
  var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
12
14
  var _editorActions = require("./editor-actions");
13
15
  var _editorCommands = require("./editor-commands");
@@ -21,14 +23,50 @@ var _floatingToolbar = require("./ui/floating-toolbar");
21
23
  var _quickInsert = require("./ui/quick-insert");
22
24
  var _SyncBlockRefresher = require("./ui/SyncBlockRefresher");
23
25
  var _toolbarComponents = require("./ui/toolbar-components");
24
- var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_ref) {
25
- var _api$editorViewMode, _api$analytics, _api$blockMenu, _config$enableSourceC, _api$toolbar, _config$enableSourceC2;
26
- var config = _ref.config,
26
+ /**
27
+ * EDITOR-6929 / PR-G: Guard contentComponent rendering.
28
+ * When `hasSyncedBlocks` is false return null
29
+ * to avoid mounting SyncBlockRefresher, DeleteConfirmationModal, and Flag —
30
+ * their hooks (useSharedPluginStateWithSelector) would execute selectors on
31
+ * every transaction for no benefit on the ~99.98% of pages with zero synced
32
+ * blocks.
33
+ */
34
+ var LazySyncedBlockUI = function LazySyncedBlockUI(_ref) {
35
+ var syncBlockStoreManager = _ref.syncBlockStore,
27
36
  api = _ref.api;
37
+ var hasSyncBlocks = (0, _hooks.useSharedPluginStateWithSelector)(api, ['syncedBlock'], function (states) {
38
+ var _states$syncedBlockSt;
39
+ return (_states$syncedBlockSt = states.syncedBlockState) === null || _states$syncedBlockSt === void 0 ? void 0 : _states$syncedBlockSt.hasSyncedBlocks;
40
+ });
41
+ if (!hasSyncBlocks && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
42
+ return null;
43
+ }
44
+ return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_SyncBlockRefresher.SyncBlockRefresher, {
45
+ syncBlockStoreManager: syncBlockStoreManager,
46
+ api: api
47
+ }), /*#__PURE__*/_react.default.createElement(_DeleteConfirmationModal.DeleteConfirmationModal, {
48
+ syncBlockStoreManager: syncBlockStoreManager,
49
+ api: api
50
+ }), /*#__PURE__*/_react.default.createElement(_Flag.Flag, {
51
+ api: api
52
+ }));
53
+ };
54
+ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_ref2) {
55
+ var _api$editorViewMode, _api$analytics, _api$blockMenu, _config$enableSourceC, _api$toolbar, _config$enableSourceC2;
56
+ var config = _ref2.config,
57
+ api = _ref2.api;
28
58
  var refs = {};
29
59
  var viewMode = 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;
30
60
  var syncBlockStore = new _editorSyncedBlockProvider.SyncBlockStoreManager(config === null || config === void 0 ? void 0 : config.syncBlockDataProvider, viewMode, config === null || config === void 0 ? void 0 : config.__livePage);
61
+ var isPerfExperimentOn = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true);
31
62
  syncBlockStore.setFireAnalyticsEvent(api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 ? void 0 : _api$analytics.fireAnalyticsEvent);
63
+
64
+ // --- Memoized getSharedState (EDITOR-6929 / PR-F) ---
65
+ // Cache the last returned shared state object. On each call, perform a
66
+ // shallow comparison of all fields against the cached value. If nothing
67
+ // changed, return the cached reference so SharedStateAPI subscribers
68
+ // (React components) skip re-rendering.
69
+ var cachedSharedState;
32
70
  api === null || api === void 0 || (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === void 0 || _api$blockMenu.actions.registerBlockMenuComponents((0, _blockMenuComponents.getBlockMenuComponents)(api, (_config$enableSourceC = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC !== void 0 ? _config$enableSourceC : false));
33
71
  api === null || api === void 0 || (_api$toolbar = api.toolbar) === null || _api$toolbar === void 0 || _api$toolbar.actions.registerComponents((0, _toolbarComponents.getToolbarComponents)(api, (_config$enableSourceC2 = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC2 !== void 0 ? _config$enableSourceC2 : false));
34
72
  return {
@@ -70,9 +108,9 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
70
108
  return (0, _editorCommands.copySyncedBlockReferenceToClipboardEditorCommand)(syncBlockStore, inputMethod, api);
71
109
  },
72
110
  insertSyncedBlock: function insertSyncedBlock() {
73
- return function (_ref2) {
111
+ return function (_ref3) {
74
112
  var _api$analytics3;
75
- var tr = _ref2.tr;
113
+ var tr = _ref3.tr;
76
114
  if (!(config !== null && config !== void 0 && config.enableSourceCreation)) {
77
115
  return null;
78
116
  }
@@ -98,38 +136,49 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
98
136
  pluginsOptions: {
99
137
  quickInsert: (0, _quickInsert.getQuickInsertConfig)(config, api, syncBlockStore),
100
138
  floatingToolbar: function floatingToolbar(state, intl) {
139
+ var _syncedBlockPluginKey;
140
+ // When the experiment is ON and the document has no synced blocks,
141
+ // skip the toolbar config entirely to avoid the per-selection-change
142
+ // cost of findSyncBlockOrBodiedSyncBlock (EDITOR-6931).
143
+ // Save the expValEquals('editor_synced_block_perf', 'isEnabled', true) in a const
144
+ // because floatingToolbar is called on every selection change.
145
+ // computing it once at plugin initialisation is more efficient.
146
+ if (!((_syncedBlockPluginKey = _main.syncedBlockPluginKey.getState(state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks) && isPerfExperimentOn) {
147
+ return undefined;
148
+ }
101
149
  return (0, _floatingToolbar.getToolbarConfig)(state, intl, api, syncBlockStore);
102
150
  }
103
151
  },
104
- contentComponent: function contentComponent(_ref3) {
105
- var containerElement = _ref3.containerElement,
106
- wrapperElement = _ref3.wrapperElement,
107
- popupsMountPoint = _ref3.popupsMountPoint;
152
+ contentComponent: function contentComponent(_ref4) {
153
+ var containerElement = _ref4.containerElement,
154
+ wrapperElement = _ref4.wrapperElement,
155
+ popupsMountPoint = _ref4.popupsMountPoint;
108
156
  refs.containerElement = containerElement || undefined;
109
157
  refs.popupsMountPoint = popupsMountPoint || undefined;
110
158
  refs.wrapperElement = wrapperElement || undefined;
111
- return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_SyncBlockRefresher.SyncBlockRefresher, {
112
- syncBlockStoreManager: syncBlockStore,
159
+ return /*#__PURE__*/_react.default.createElement(LazySyncedBlockUI, {
160
+ syncBlockStore: syncBlockStore,
113
161
  api: api
114
- }), /*#__PURE__*/_react.default.createElement(_DeleteConfirmationModal.DeleteConfirmationModal, {
115
- syncBlockStoreManager: syncBlockStore,
116
- api: api
117
- }), /*#__PURE__*/_react.default.createElement(_Flag.Flag, {
118
- api: api
119
- }));
162
+ });
120
163
  },
121
164
  getSharedState: function getSharedState(editorState) {
122
165
  if (!editorState) {
123
166
  return;
124
167
  }
125
- var _syncedBlockPluginKey = _main.syncedBlockPluginKey.getState(editorState),
126
- activeFlag = _syncedBlockPluginKey.activeFlag,
127
- currentSyncBlockStore = _syncedBlockPluginKey.syncBlockStore,
128
- bodiedSyncBlockDeletionStatus = _syncedBlockPluginKey.bodiedSyncBlockDeletionStatus,
129
- retryCreationPosMap = _syncedBlockPluginKey.retryCreationPosMap,
130
- hasSyncedBlocks = _syncedBlockPluginKey.hasSyncedBlocks,
131
- hasUnsavedBodiedSyncBlockChanges = _syncedBlockPluginKey.hasUnsavedBodiedSyncBlockChanges;
132
- return {
168
+ var pluginState = _main.syncedBlockPluginKey.getState(editorState);
169
+ var activeFlag = pluginState.activeFlag,
170
+ currentSyncBlockStore = pluginState.syncBlockStore,
171
+ bodiedSyncBlockDeletionStatus = pluginState.bodiedSyncBlockDeletionStatus,
172
+ retryCreationPosMap = pluginState.retryCreationPosMap,
173
+ hasSyncedBlocks = pluginState.hasSyncedBlocks,
174
+ hasUnsavedBodiedSyncBlockChanges = pluginState.hasUnsavedBodiedSyncBlockChanges;
175
+
176
+ // --- EDITOR-6929 / PR-F: return a stable reference when all
177
+ // fields are unchanged to prevent unnecessary React re-renders. ---
178
+ if (cachedSharedState !== undefined && cachedSharedState.activeFlag === activeFlag && cachedSharedState.syncBlockStore === currentSyncBlockStore && cachedSharedState.bodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && cachedSharedState.retryCreationPosMap === retryCreationPosMap && cachedSharedState.hasSyncedBlocks === hasSyncedBlocks && cachedSharedState.hasUnsavedBodiedSyncBlockChanges === hasUnsavedBodiedSyncBlockChanges && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
179
+ return cachedSharedState;
180
+ }
181
+ var nextSharedState = {
133
182
  activeFlag: activeFlag,
134
183
  syncBlockStore: currentSyncBlockStore,
135
184
  bodiedSyncBlockDeletionStatus: bodiedSyncBlockDeletionStatus,
@@ -137,6 +186,8 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
137
186
  hasSyncedBlocks: hasSyncedBlocks,
138
187
  hasUnsavedBodiedSyncBlockChanges: hasUnsavedBodiedSyncBlockChanges
139
188
  };
189
+ cachedSharedState = nextSharedState;
190
+ return nextSharedState;
140
191
  }
141
192
  };
142
193
  };
@@ -324,7 +324,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
324
324
  // When the perf gate is ON and the doc has synced blocks we do a
325
325
  // single traversal here; afterwards `apply()` will map or rebuild
326
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;
327
+ const initStatusDecorationSet = docHasSyncedBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true) ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : DecorationSet.empty;
328
328
  return {
329
329
  selectionDecorationSet: calculateDecorations(instance.doc, instance.selection, instance.schema),
330
330
  activeFlag: false,
@@ -341,6 +341,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
341
341
  apply: (tr, currentPluginState, oldEditorState) => {
342
342
  var _meta$activeFlag, _meta$bodiedSyncBlock;
343
343
  const meta = tr.getMeta(syncedBlockPluginKey);
344
+ const isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
344
345
  const {
345
346
  activeFlag,
346
347
  selectionDecorationSet,
@@ -356,11 +357,19 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
356
357
  // Lazy-init bookkeeping: once a synced block enters the document we
357
358
  // flip `hasSyncedBlocks` to `true` for the lifetime of this editor
358
359
  let nextHasSyncedBlocks = prevHasSyncedBlocks;
359
- if (!prevHasSyncedBlocks && tr.docChanged && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
360
+ if (!prevHasSyncedBlocks && tr.docChanged && isPerfExperimentOn) {
360
361
  if (transactionInsertsSyncedBlock(tr)) {
361
362
  nextHasSyncedBlocks = true;
362
363
  }
363
364
  }
365
+
366
+ // --- Fast path (EDITOR-6929): when `hasSyncedBlocks` is false,
367
+ // no meta is set, and the selection/doc haven't changed in a way
368
+ // that affects our state, return the SAME object reference so
369
+ // SharedStateAPI skips notifying subscribers. ---
370
+ if (!nextHasSyncedBlocks && !meta && !tr.docChanged && tr.selection.eq(oldEditorState.selection) && isPerfExperimentOn) {
371
+ return currentPluginState;
372
+ }
364
373
  let newDecorationSet = tr.docChanged ? selectionDecorationSet.map(tr.mapping, tr.doc) // only map if document changed
365
374
  : selectionDecorationSet;
366
375
  if (!tr.selection.eq(oldEditorState.selection)) {
@@ -384,7 +393,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
384
393
  let nextIsOffline = prevOffline;
385
394
  let nextIsViewMode = prevViewMode;
386
395
  let nextIsDragging = prevDragging;
387
- if (expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
396
+ if (isPerfExperimentOn) {
388
397
  if (!nextHasSyncedBlocks) {
389
398
  // No synced blocks → keep empty status decorations
390
399
  nextStatusDecorationSet = DecorationSet.empty;
@@ -413,14 +422,24 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
413
422
  }
414
423
  const newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
415
424
  const newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
425
+ const nextActiveFlag = (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag;
426
+ const nextBodiedSyncBlockDeletionStatus = (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus;
427
+ const nextHasUnsavedBodiedSyncBlockChanges = syncBlockStore.sourceManager.hasUnsavedChanges();
428
+
429
+ // --- Reference equality (EDITOR-6929): return the same object
430
+ // when ALL fields are reference-equal to avoid SharedStateAPI
431
+ // notifying subscribers and triggering React re-renders. ---
432
+ if (nextActiveFlag === activeFlag && newDecorationSet === selectionDecorationSet && newRetryCreationPosMap === retryCreationPosMap && nextHasSyncedBlocks === prevHasSyncedBlocks && nextBodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && nextHasUnsavedBodiedSyncBlockChanges === currentPluginState.hasUnsavedBodiedSyncBlockChanges && nextStatusDecorationSet === prevStatusDecorationSet && nextIsOffline === prevOffline && nextIsViewMode === prevViewMode && nextIsDragging === prevDragging && isPerfExperimentOn) {
433
+ return currentPluginState;
434
+ }
416
435
  return {
417
- activeFlag: (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag,
436
+ activeFlag: nextActiveFlag,
418
437
  selectionDecorationSet: newDecorationSet,
419
438
  syncBlockStore: syncBlockStore,
420
439
  retryCreationPosMap: newRetryCreationPosMap,
421
440
  hasSyncedBlocks: nextHasSyncedBlocks,
422
- bodiedSyncBlockDeletionStatus: (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus,
423
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
441
+ bodiedSyncBlockDeletionStatus: nextBodiedSyncBlockDeletionStatus,
442
+ hasUnsavedBodiedSyncBlockChanges: nextHasUnsavedBodiedSyncBlockChanges,
424
443
  statusDecorationSet: nextStatusDecorationSet,
425
444
  prevIsOffline: nextIsOffline,
426
445
  prevIsViewMode: nextIsViewMode,