Fixes 31156: release TipTap editors on unmount and load the ELK layout engine on demand - #31157
Conversation
…ulating `useCustomEditor` creates a ProseMirror `Editor` in an effect but its cleanup only flipped an `isMounted` flag — `instance.destroy()` was never called (grep finds it only in test files). The editor's view, plugin state, contenteditable DOM and MutationObserver therefore outlive the component. This compounds because the editor is mounted per table row: the description column renders `RichTextEditorPreviewerNew` for every row, so a page of descriptions is a page of editors. Measured live on release-2-0: 16 live `.ProseMirror` instances for 16 rows on `/tags/General`, 7 for 7 on a glossary terms table. Every pagination click, search keystroke or tab switch abandoned that many editors for the rest of the session. The `isDestroyed` guard mirrors `@tiptap/react`'s own `useEditor` cleanup (node_modules/@tiptap/react/dist/index.js:1013 and :1039) — StrictMode double-invokes effects, so the cleanup can run twice. Co-Authored-By: Claude <noreply@anthropic.com>
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
✅ Playwright Results — workflow succeededValidated commit ✅ 550 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky PerformanceBlocking targets: ✅ met · Optimization targets: 🟡 in progress Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting. 🕒 Full workflow signal wall (to summary) 50m 59s ⏱️ Max setup 3m 52s · max shard execution 17m 50s · max shard-job elapsed before upload 21m 13s · reporting 4s 🌐 199.86 requests/attempt · 2.83 app boots/UI scenario · 21.03% common-shard skew Optimization targets still in progress:
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
…demand `EntityLineageLayoutUtils` mixed two concerns: five pure geometry helpers (`centerNodePosition`, `getNodesBoundsReactFlow`, `getViewportForBoundsReactFlow`, ...) and two ELK-backed layout functions. Because `ELKUtil` was imported at module scope, importing any of the pure helpers dragged elkjs — 1.37MB built. `LineageControlButtons` imports only `centerNodePosition`, and `LineageProvider` is statically imported by eight lineage components. That was enough to land elkjs in the shared chunk which 476 route chunks statically import, so every authenticated page downloaded and parsed the lineage layout engine. Glossary pages paid for it while rendering with G6/antv-dagre, never calling ELK at all. `getELKLayoutedElements` and `positionNodesUsingElk` now live in `utils/Lineage/Layout/ElkLayoutUtils.ts`, alongside a named `loadElkLayout()` lazy boundary. `EntityLineageLayoutUtils` no longer references ELK in any form, so the pure helpers cannot re-acquire the dependency by accident. A plain file split would not have been enough on its own: `LineageProvider` is the sole caller of the ELK functions and is itself reachable from the shared cluster, so the engine needs the dynamic boundary regardless. Making it a named function in an ELK-specific module keeps that explicit rather than burying an `await import()` in the middle of layout logic. Measured on a production build: ELKUtil chunk importers 1 static -> 0 static, 1 dynamic glossary terms tab 12.57 MB -> 11.20 MB (-1.37 MB, runtime) Verified against a live backend: the glossary terms tab makes zero elk-related requests and still renders, and /lineage fetches ELKUtil + vendor-elk on demand and draws 83 nodes with no console errors. Co-Authored-By: Claude <noreply@anthropic.com>
1def900 to
9a9143e
Compare
Destroying the TipTap instance on unmount exposed two setTimeout callbacks in BlockEditor that touch the editor after the effect that scheduled them has been cleaned up. They previously wrote into an orphaned-but-alive editor; now they reach a torn-down ProseMirror view and throw "Cannot read properties of null (reading 'updateOuterDeco')". Clear the timers on cleanup and re-check isDestroyed inside the callback. Clearing also drops a stale content write that could overwrite a newer one. Cover the unmount path in useCustomEditor's own suite: MockEditor had no destroy()/isDestroyed, so the new branch threw on every unmount and React swallowed it while the suite still reported green.
organize-imports-cli places the ElkLayoutUtils import after ExplorePureUtils. Mechanical reformat only, from the same organize-imports + prettier sequence CI runs.
|
| Count | Rule |
|---|---|
| 36 | react-hooks/exhaustive-deps |
| 5 | sonarjs/cyclomatic-complexity |
| 4 | sonarjs/no-duplicate-string |
| 2 | sonarjs/no-nested-functions |
| 2 | sonarjs/cognitive-complexity |
| 1 | jsx-a11y/no-static-element-interactions |
| 1 | sonarjs/no-nested-conditional |
All findings
| Location | Rule | Message | |
|---|---|---|---|
| 🟡 | src/components/BlockEditor/BlockEditor.tsx:145:11 |
sonarjs/no-duplicate-string |
Define a constant instead of duplicating this literal 3 times. |
| 🟡 | src/components/BlockEditor/BlockEditor.tsx:243:8 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'isPopoverOpenRef'. Either include it or remove the dependency array. |
| 🟡 | src/components/BlockEditor/BlockEditor.tsx:277:7 |
jsx-a11y/no-static-element-interactions |
Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs |
| 🟡 | src/components/BlockEditor/hooks/useCustomEditor.ts:128:34 |
sonarjs/no-nested-functions |
Refactor this code to not nest functions more than 4 levels deep. |
| 🟡 | src/components/BlockEditor/hooks/useCustomEditor.ts:148:6 |
react-hooks/exhaustive-deps |
React Hook useEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependen |
| 🟡 | src/components/BlockEditor/hooks/useCustomEditor.ts:148:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has missing dependencies: 'forceUpdate' and 'options'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:205:62 |
sonarjs/cyclomatic-complexity |
{"message":"Function has a complexity of 13 which is greater than 10 authorized.","cost":3,"secondaryLocations":[{"line":205,"column":61,"endLine":205,"endColum |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:336:9 |
sonarjs/no-nested-conditional |
Extract this nested ternary operation into an independent statement. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:470:6 |
react-hooks/exhaustive-deps |
React Hook useMemo has an unnecessary dependency: 'isEditMode'. Either exclude it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:503:7 |
sonarjs/cognitive-complexity |
Refactor this function to reduce its Cognitive Complexity from 20 to the 15 allowed. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:503:7 |
sonarjs/cyclomatic-complexity |
{"message":"Function has a complexity of 19 which is greater than 10 authorized.","cost":9,"secondaryLocations":[{"line":503,"column":6,"endLine":503,"endColumn |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:622:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'setColumnsHavingLineage', 'setEdges', 'setIsRepositioning', and 'setNodes'. Either include them or remove the |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:681:13 |
sonarjs/no-duplicate-string |
Define a constant instead of duplicating this literal 4 times. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:682:23 |
sonarjs/no-duplicate-string |
Define a constant instead of duplicating this literal 4 times. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:690:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has a missing dependency: 't'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:749:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'isTourOpen', 'setEdges', 'setNodes', 't', and 'timeFilter'. Either include them or remove the dependency array |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:778:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'navigate', 'setActiveLayer', and 'setPlatformView'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:819:26 |
sonarjs/no-duplicate-string |
Define a constant instead of duplicating this literal 4 times. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:850:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'exportLineageData', 'showModal', and 't'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:862:75 |
sonarjs/cyclomatic-complexity |
{"message":"Function has a complexity of 20 which is greater than 10 authorized.","cost":10,"secondaryLocations":[{"line":862,"column":74,"endLine":862,"endColu |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:984:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'lineageData?.downstreamEdges', 'lineageData?.upstreamEdges', 't', and 'updateLineageData'. Either include them |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1036:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'setTracedColumns' and 'setTracedNodes'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1057:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'onPlatformViewChange' and 'setIsPlatformLineage'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1073:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has missing dependencies: 'setSelectedEdge', 'setTracedColumns', and 'setTracedNodes'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1083:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has a missing dependency: 'setTracedColumns'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1241:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'edges', 'removeEdgesBySourceTarget', and 'removeNodeById'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1433:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has a missing dependency: 'setSelectedNode'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1456:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'selectLoadMoreNode', 'setActiveNode', 'setSelectedEdge', and 'setSelectedNode'. Either include them or remove |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1466:6 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'setActiveNode', 'setSelectedColumn', 'setSelectedNode', 'setTracedColumns', and 'setTracedNodes'. Either inclu |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1478:6 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'setActiveNode', 'setSelectedEdge', 'setSelectedNode', 'setTracedColumns', and 'setTracedNodes'. Either include |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1526:6 |
react-hooks/exhaustive-deps |
React Hook useCallback has a missing dependency: 'setSelectedEdge'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1529:33 |
sonarjs/cyclomatic-complexity |
{"message":"Function has a complexity of 14 which is greater than 10 authorized.","cost":4,"secondaryLocations":[{"line":1529,"column":32,"endLine":1529,"endCol |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1626:31 |
sonarjs/no-nested-functions |
Refactor this code to not nest functions more than 4 levels deep. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1653:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'addTracedColumns', 'setColumnsHavingLineage', and 'setTracedNodes'. Either include them or remove the dependen |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1672:6 |
react-hooks/exhaustive-deps |
React Hook useCallback has a missing dependency: 'setSelectedEdge'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1752:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'handleModalCancel' and 'updateEdge'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1815:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'setSelectedEdge' and 'updateEdge'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1878:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'setActiveNode' and 'updateLineageData'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1883:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'redraw'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1885:47 |
sonarjs/cognitive-complexity |
Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1885:47 |
sonarjs/cyclomatic-complexity |
{"message":"Function has a complexity of 21 which is greater than 10 authorized.","cost":11,"secondaryLocations":[{"line":1885,"column":46,"endLine":1885,"endCo |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1938:6 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'fetchLineageData', 'fetchPlatformLineage', and 'timeFilter'. Either include them or remove the dependency arra |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:1965:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has missing dependencies: 'setActiveLayer' and 'setLineageConfig'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:2004:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has missing dependencies: 'removeEdgeHandler' and 'removeNodeHandler'. Either include them or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:2010:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'redraw'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:2014:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'onPlatformViewUpdate'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:2062:6 |
react-hooks/exhaustive-deps |
React Hook useMemo has a missing dependency: 'queryFilter'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:2116:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'updateActiveLayer'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:2122:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'fetchDataQualityLineage'. Either include it or remove the dependency array. |
| 🟡 | src/context/LineageProvider/LineageProvider.tsx:2164:6 |
react-hooks/exhaustive-deps |
React Hook useMemo has missing dependencies: 'platformView' and 't'. Either include them or remove the dependency array. |
… and 1 more. Run make ui-checkstyle-changed locally for the full list.
Fix locally (fast - only checks files changed in this branch):
make ui-checkstyle-changed
|
|
Changes have been cherry-picked to the 2.0 branch. |
…t engine on demand (#31157) * fix(ui): destroy the TipTap editor on unmount so instances stop accumulating `useCustomEditor` creates a ProseMirror `Editor` in an effect but its cleanup only flipped an `isMounted` flag — `instance.destroy()` was never called (grep finds it only in test files). The editor's view, plugin state, contenteditable DOM and MutationObserver therefore outlive the component. This compounds because the editor is mounted per table row: the description column renders `RichTextEditorPreviewerNew` for every row, so a page of descriptions is a page of editors. Measured live on release-2-0: 16 live `.ProseMirror` instances for 16 rows on `/tags/General`, 7 for 7 on a glossary terms table. Every pagination click, search keystroke or tab switch abandoned that many editors for the rest of the session. The `isDestroyed` guard mirrors `@tiptap/react`'s own `useEditor` cleanup (node_modules/@tiptap/react/dist/index.js:1013 and :1039) — StrictMode double-invokes effects, so the cleanup can run twice. Co-Authored-By: Claude <noreply@anthropic.com> * perf(ui): move ELK layout into its own module and load the engine on demand `EntityLineageLayoutUtils` mixed two concerns: five pure geometry helpers (`centerNodePosition`, `getNodesBoundsReactFlow`, `getViewportForBoundsReactFlow`, ...) and two ELK-backed layout functions. Because `ELKUtil` was imported at module scope, importing any of the pure helpers dragged elkjs — 1.37MB built. `LineageControlButtons` imports only `centerNodePosition`, and `LineageProvider` is statically imported by eight lineage components. That was enough to land elkjs in the shared chunk which 476 route chunks statically import, so every authenticated page downloaded and parsed the lineage layout engine. Glossary pages paid for it while rendering with G6/antv-dagre, never calling ELK at all. `getELKLayoutedElements` and `positionNodesUsingElk` now live in `utils/Lineage/Layout/ElkLayoutUtils.ts`, alongside a named `loadElkLayout()` lazy boundary. `EntityLineageLayoutUtils` no longer references ELK in any form, so the pure helpers cannot re-acquire the dependency by accident. A plain file split would not have been enough on its own: `LineageProvider` is the sole caller of the ELK functions and is itself reachable from the shared cluster, so the engine needs the dynamic boundary regardless. Making it a named function in an ELK-specific module keeps that explicit rather than burying an `await import()` in the middle of layout logic. Measured on a production build: ELKUtil chunk importers 1 static -> 0 static, 1 dynamic glossary terms tab 12.57 MB -> 11.20 MB (-1.37 MB, runtime) Verified against a live backend: the glossary terms tab makes zero elk-related requests and still renders, and /lineage fetches ELKUtil + vendor-elk on demand and draws 83 nodes with no console errors. Co-Authored-By: Claude <noreply@anthropic.com> * fix(ui): guard deferred BlockEditor timers against the destroyed editor Destroying the TipTap instance on unmount exposed two setTimeout callbacks in BlockEditor that touch the editor after the effect that scheduled them has been cleaned up. They previously wrote into an orphaned-but-alive editor; now they reach a torn-down ProseMirror view and throw "Cannot read properties of null (reading 'updateOuterDeco')". Clear the timers on cleanup and re-check isDestroyed inside the callback. Clearing also drops a stale content write that could overwrite a newer one. Cover the unmount path in useCustomEditor's own suite: MockEditor had no destroy()/isDestroyed, so the new branch threw on every unmount and React swallowed it while the suite still reported green. * Fix UI checkstyle organize-imports-cli places the ElkLayoutUtils import after ExplorePureUtils. Mechanical reformat only, from the same organize-imports + prettier sequence CI runs. --------- Co-authored-by: Claude <noreply@anthropic.com> (cherry picked from commit 2064dce)
|
Failed to cherry-pick changes to the 1.13 branch. |
Code Review ✅ ApprovedReleases TipTap editor instances on unmount to prevent DOM accumulation and moves the ELK layout engine to an on-demand dynamic import, reducing bundle size by 1.37MB across authenticated routes. No issues found. OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |



Fixes #31156
Two independent main-thread fixes found while investigating Sentry
ui.long-animation-framespans ("Main UI thread blocked") — 826 spans over two weeks, p99 1461ms, max 4304ms, 153.4 s of total blocking.1. TipTap editors were never destroyed
useCustomEditorcreates a ProseMirrorEditorin an effect, but cleanup only flipped a localisMountedflag —instance.destroy()was never called (a grep acrosssrc/matches only test files). The view, plugin stack, contenteditable DOM andMutationObserveroutlived the component.It compounds because the editor mounts once per table row — the shared description column (
utils/TableColumn.util.tsx) rendersRichTextEditorPreviewerNew, which mounts aBlockEditor.The
isDestroyedguard mirrors@tiptap/react's ownuseEditorcleanup (node_modules/@tiptap/react/dist/index.js:1013,:1039) —<StrictMode>double-invokes effects, so cleanup can run twice.Verified at runtime
Production build served against a live backend, driving SPA navigation (
pushState+popstate) so components unmount without a page reload — a full reload resets the count regardless and proves nothing:.ProseMirrorcount returns to exactly the row count every round and drops 16 → 9 on leaving. No compounding.2. ELK layout engine loaded on every authenticated page
EntityLineageLayoutUtilsimportedELKUtilat module scope. That module also exports five pure geometry helpers, so importing any one of them dragged elkjs — 1.37 MB built.LineageControlButtonsimports onlycenterNodePosition, and is reached statically fromLineage.component.tsx. That was enough to pull elkjs into the shared chunk that 476 route chunks statically import. Every authenticated page downloaded and parsed the lineage layout engine — including glossary, which uses G6/antv-dagre and never calls ELK (OntologyExplorerchunk: 0 occurrences oforg.eclipse.elk).ELKLayoutis referenced in exactly one place, insidegetELKLayoutedElements, which is alreadyasyncand already awaited at every call site. Moving the import inside it is behaviour-preserving; the ELK engine itself is untouched.Measured on a production build
ServicesPagemust-load closureELKUtilchunk importers−1.37 MB on every authenticated route.
Verified against a live backend
/lineage: fetchesELKUtil+vendor-elkon demand, renders 83 nodes, 0 console errors3. Destroying the editor exposed two unguarded deferred writes
Fix 1 above turned a latent bug into a live one.
BlockEditordefers two pieces ofwork onto the editor with
setTimeout(to dodge a known TipTapflushSyncwarning), with no
clearTimeoutand no re-check inside the callback:setEditorContent)editabletoggle (setEditable)Both effects check
editor.isDestroyedat the top, but the timer fires later.Before this PR the editor was never destroyed, so a stale timer harmlessly wrote
into an orphaned-but-alive editor. Once the instance is destroyed on unmount, the
same timer reaches a torn-down ProseMirror view:
This is reachable in the app, not just in tests: any description cell whose
contentoreditableprop changes and then unmounts within the same tick —pagination click, tab switch, search keystroke, autosave round-trip — throws from
a timer callback, outside React, where no error boundary catches it. That is
exactly the high-churn table this PR targets.
Both sites now clear the timer on cleanup and re-check
isDestroyedinside thecallback. Clearing also drops a stale content write that could otherwise land
after a newer one.
Caught by CI, confirmed by A/B
ui-coverage-testswent red onEntitySummaryPanelandTestDefinitionList,both with that identical stack, while the same job is green on every recent
mainrun. A/B on one stack, changing only the
destroy()hunk:TestDefinitionListdestroy(), without the guarddestroy()reverteddestroy()+ the guardA sweep of the UI for other post-destroy access paths found none: only three files
combine a TipTap editor with deferred work —
BlockEditor(fixed),useCustomEditor(its rAF is alreadyisMounted-guarded), andFeedEditor(Quill, not TipTap). No extension NodeView defers editor access.
Type of change
Tests
128 tests pass across the BlockEditor tree and the two suites that caught the
regression (
TestDefinitionList,BlockEditor/**), plus the lineage areas(
EntityLineageLayoutUtils,Lineage,EntityLineage,LineageProvider).useCustomEditor's own suite now covers the unmount path. ItsMockEditorhadneither
destroy()norisDestroyed, so the new branch threwTypeError: instance.destroy is not a functionon every unmount — React swallowedit and the suite still reported green, which is why the first pass of this PR
missed the regression above. The mock now implements both, and a new test asserts
the instance is destroyed on unmount; it is non-vacuous (it fails against
main'shook and passes against this branch's).
Suites that fail to run locally with "Jest encountered an unexpected token" are a
pre-existing environment issue with the
ui-core-componentsprebuilt dist; theyrun fine on CI. I A/B'd them with the change removed and they fail identically.
tsc --noEmit: 0 errors in all changed files.prettier --check: clean.Not verified: ESLint could not run locally — this branch's
eslint.config.mjsrequireseslint-plugin-sonarjs, absent from the checkouts available here. Relying on CI.Manual test steps
Editor leak: open a table of descriptions (
/tags/General, a glossary terms tab), rundocument.querySelectorAll('.ProseMirror').length, navigate away and back within the app (don't reload), re-run. Before: grows. After: returns to the row count.ELK: open a glossary page with DevTools Network filtered to
elk— no requests. Then open/lineage—ELKUtilandvendor-elkload, and the graph renders.Checklist
🤖 Generated with Claude Code
Summary by Gitar
ELKUtilinEntityLineageLayoutUtilsto prevent bundlingelkjs(~1.37MB) across all authenticated pagesThis will update automatically on new commits.