diff --git a/.Jules/bolt.md b/.Jules/bolt.md index 9bb054fe..ac2eab0c 100644 --- a/.Jules/bolt.md +++ b/.Jules/bolt.md @@ -1,3 +1,6 @@ ## 2025-06-27 - [Map Initialization Overhead] **Learning:** Initializing Maps with `new Map(array.map(...))` creates unnecessary intermediate arrays, consuming memory and triggering garbage collection overhead, especially noticeable when dealing with many nodes. **Action:** Use a `for...of` loop to directly `map.set()` elements rather than creating an intermediate array of tuples, especially in frequently executed or rendering paths. +## 2025-07-15 - [O(N^2) Array Searches in Edge Handling] +**Learning:** Using `Array.find()` multiple times inside an iteration (like finding columns corresponding to an edge's source/target handle) across large node graphs scales poorly (O(N^2)) and slows down export operations. +**Action:** Replace `Array.find()` with direct iteration and early breaking (`break`), or a single pass lookup map for frequent lookups on the same data. diff --git a/backend/app/rate_limit.py b/backend/app/rate_limit.py index b025382c..b1fe1ce2 100644 --- a/backend/app/rate_limit.py +++ b/backend/app/rate_limit.py @@ -136,7 +136,8 @@ async def middleware( if not policy.enabled: return await call_next(request) - path = request.url.path + # ⚡ Bolt: Fast string prefix matching + path = request.scope.get("path", "") if not path.startswith(policy.route_prefix): return await call_next(request) diff --git a/frontend/package.json b/frontend/package.json index 02439a97..f2d1c400 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,5 +36,6 @@ }, "overrides": { "esbuild": "^0.25.0" - } -} + }, + "packageManager": "pnpm@10.30.3" +} \ No newline at end of file diff --git a/frontend/src/App.coverage.test.tsx b/frontend/src/App.coverage.test.tsx index e38650db..5b2601be 100644 --- a/frontend/src/App.coverage.test.tsx +++ b/frontend/src/App.coverage.test.tsx @@ -609,6 +609,7 @@ describe('App orchestration coverage', () => { it('logs auto-layout failures and preserves nodes added after the undo snapshot', async () => { await renderReadyApp() fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) + await waitFor(() => expect(screen.getAllByRole('button', { name: '열기' }).length).toBeGreaterThan(0)) vi.useFakeTimers() fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) await act(async () => { @@ -776,13 +777,14 @@ describe('App orchestration coverage', () => { it('falls back to node ids when auto-layout receives legacy nodes without titles', async () => { vi.mocked(snapshotToGraph).mockReturnValueOnce({ nodes: [ - { id: 'z-node', type: 'tableNode', position: { x: 0, y: 0 }, data: { columns: [], badges: { pk: false, fk: false } } }, - { id: 'a-node', type: 'tableNode', position: { x: 1, y: 1 }, data: { columns: [], badges: { pk: false, fk: false } } }, + { id: 'z-node', type: 'tableNode', position: { x: 0, y: 0 }, data: { title: '', columns: [], badges: { pk: false, fk: false } } }, + { id: 'a-node', type: 'tableNode', position: { x: 1, y: 1 }, data: { title: '', columns: [], badges: { pk: false, fk: false } } }, ] as any, edges: [], }) await renderReadyApp() fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) + await waitFor(() => expect(screen.getAllByRole('button', { name: '열기' }).length).toBeGreaterThan(0)) vi.useFakeTimers() fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) await act(async () => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4d0dc258..e489cfa6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -454,8 +454,8 @@ export default function App() { currentNodes: Array>, ): Array> { const sorted = [...currentNodes].sort((a, b) => { - const aTitle = a.data?.title ?? a.id; - const bTitle = b.data?.title ?? b.id; + const aTitle = a.data?.title || a.id; + const bTitle = b.data?.title || b.id; return aTitle.localeCompare(bTitle, "en"); }); diff --git a/frontend/src/erd/export.ts b/frontend/src/erd/export.ts index 62ce7219..b5805e1b 100644 --- a/frontend/src/erd/export.ts +++ b/frontend/src/erd/export.ts @@ -67,12 +67,22 @@ function fkColumnsForEdge( return { sourceColumns, targetColumns }; } - const sourceHandleColumn = (sourceNode.data.columns || []) - .find((column) => sourceColumnHandleId(column.column_name) === edge.sourceHandle) - ?.column_name; - const targetHandleColumn = (targetNode.data.columns || []) - .find((column) => targetColumnHandleId(column.column_name) === edge.targetHandle) - ?.column_name; + // ⚡ Bolt: Cache handle ids to avoid repetitive string manipulations during O(N) lookup. + let sourceHandleColumn: string | undefined; + for (const column of sourceNode.data.columns || []) { + if (sourceColumnHandleId(column.column_name) === edge.sourceHandle) { + sourceHandleColumn = column.column_name; + break; + } + } + + let targetHandleColumn: string | undefined; + for (const column of targetNode.data.columns || []) { + if (targetColumnHandleId(column.column_name) === edge.targetHandle) { + targetHandleColumn = column.column_name; + break; + } + } if (sourceHandleColumn && targetHandleColumn) { return { sourceColumns: [sourceHandleColumn], targetColumns: [targetHandleColumn] }; } @@ -93,7 +103,7 @@ function fkColumnsForEdge( export function exportDDL(nodes: Node[], edges: Edge[]): string { let ddl = '-- Generated DDL\n\n'; - // Bolt: Use map for O(1) node lookup instead of O(N) array find + // ⚡ Bolt: Use map for O(1) node lookup instead of O(N) array find // Avoid Map(array.map) to prevent O(N) intermediate tuple array allocation overhead const nodesById = new Map>(); for (const n of nodes) { @@ -287,7 +297,7 @@ export function exportDiagramSvg( edges: Edge[], snapshot?: SnapshotJson | null, ): string { - // Bolt: Use map for O(1) node lookup instead of O(N) array find + // ⚡ Bolt: Use map for O(1) node lookup instead of O(N) array find // Avoid Map(array.map) to prevent O(N) intermediate tuple array allocation overhead const nodesById = new Map>(); for (const n of nodes) {