diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f31816f..b8bcef6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "rbush": "^4.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "y-indexeddb": "^9.0.12", "y-websocket": "^2.0.0", "yjs": "^13.6.0" }, @@ -3097,6 +3098,25 @@ "node": ">=0.4" } }, + "node_modules/y-indexeddb": { + "version": "9.0.12", + "resolved": "https://registry.npmjs.org/y-indexeddb/-/y-indexeddb-9.0.12.tgz", + "integrity": "sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==", + "dependencies": { + "lib0": "^0.2.74" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/y-leveldb": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index b421297..b9bf462 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "rbush": "^4.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "y-indexeddb": "^9.0.12", "y-websocket": "^2.0.0", "yjs": "^13.6.0" }, diff --git a/frontend/src/crdt/useYjsBinding.ts b/frontend/src/crdt/useYjsBinding.ts index 65d4080..38155a0 100644 --- a/frontend/src/crdt/useYjsBinding.ts +++ b/frontend/src/crdt/useYjsBinding.ts @@ -4,6 +4,7 @@ */ import { useEffect, useRef, useState, useCallback } from 'react'; import * as Y from 'yjs'; +import { IndexeddbPersistence } from 'y-indexeddb'; import { YjsDocument } from './YjsDocument'; import { WebSocketProvider, type ConnectionStatus } from './WebSocketProvider'; import { AwarenessManager, type UserCursor, type UserSelection } from './AwarenessManager'; @@ -90,6 +91,22 @@ export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { const doc = new YjsDocument(); yjsDocRef.current = doc; + // IndexedDB offline persistence — allows editing without network connection + let indexedDbProvider: IndexeddbPersistence | null = null; + try { + indexedDbProvider = new IndexeddbPersistence(`webcad-${docName}`, doc.doc); + indexedDbProvider.whenSynced.then(() => { + // Sync state from IndexedDB once loaded + setElements(doc.getElements()); + setLayers(doc.getLayers()); + setBlocks(doc.getBlocks()); + setGroups(doc.getGroups()); + setBgConfigs(doc.getBgConfigs()); + }); + } catch (err) { + console.warn('[useYjsBinding] IndexedDB persistence unavailable:', err); + } + const awareness = new AwarenessManager(doc, userId); awarenessRef.current = awareness; @@ -150,6 +167,10 @@ export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { awareness.removeSelf(); awareness.destroy(); provider.disconnect(); + if (indexedDbProvider) { + indexedDbProvider.destroy(); + indexedDbProvider = null; + } doc.destroy(); yjsDocRef.current = null; providerRef.current = null; diff --git a/frontend/tests/PerformanceBenchmark.test.ts b/frontend/tests/PerformanceBenchmark.test.ts new file mode 100644 index 0000000..1c5036b --- /dev/null +++ b/frontend/tests/PerformanceBenchmark.test.ts @@ -0,0 +1,98 @@ +/** + * Performance Benchmark: SpatialIndex with 50,000 elements + * Verifies that the R-Tree spatial index can handle large datasets efficiently. + */ +import { describe, it, expect, beforeAll } from 'vitest'; +import { SpatialIndex } from '../src/canvas/SpatialIndex'; +import type { CADElement } from '../src/types/cad.types'; + +const N = 50_000; + +describe('Performance Benchmark: 50.000 Elemente', () => { + let spatialIndex: SpatialIndex; + let elements: CADElement[]; + + beforeAll(() => { + spatialIndex = new SpatialIndex(); + elements = []; + + // Generate 50k elements in a grid pattern + for (let i = 0; i < N; i++) { + const col = i % 200; + const row = Math.floor(i / 200); + elements.push({ + id: `perf-el-${i}`, + type: 'rect', + layerId: 'layer-perf', + x: col * 10, + y: row * 10, + width: 8, + height: 8, + properties: { stroke: '#000', fill: '#ccc' }, + }); + } + }); + + it('should bulk-insert 50k elements in under 2s', () => { + const start = performance.now(); + spatialIndex.bulkInsert(elements); + const elapsed = performance.now() - start; + console.log(`Bulk insert ${N} elements: ${elapsed.toFixed(1)}ms`); + expect(elapsed).toBeLessThan(2000); + }); + + it('should search viewport with ~100 visible elements in under 10ms', () => { + // Query a small viewport that contains ~100 elements + const start = performance.now(); + const results = spatialIndex.search({ minX: 0, minY: 0, maxX: 1000, maxY: 100 }); + const elapsed = performance.now() - start; + console.log(`Viewport search (~100 elements): ${elapsed.toFixed(2)}ms, found=${results.length}`); + expect(results.length).toBeGreaterThan(50); + expect(results.length).toBeLessThan(2000); + expect(elapsed).toBeLessThan(10); + }); + + it('should search large viewport (all elements) in under 100ms', () => { + const start = performance.now(); + const results = spatialIndex.search({ minX: 0, minY: 0, maxX: 20000, maxY: 2500 }); + const elapsed = performance.now() - start; + console.log(`Full viewport search: ${elapsed.toFixed(2)}ms, found=${results.length}`); + expect(results.length).toBe(N); + expect(elapsed).toBeLessThan(100); + }); + + it('should handle 100 sequential viewport searches in under 500ms', () => { + const start = performance.now(); + for (let i = 0; i < 100; i++) { + const x = (i * 20) % 2000; + const y = (i * 5) % 500; + spatialIndex.search({ minX: x, minY: y, maxX: x + 200, maxY: y + 100 }); + } + const elapsed = performance.now() - start; + console.log(`100 sequential searches: ${elapsed.toFixed(2)}ms`); + expect(elapsed).toBeLessThan(500); + }); + + it('should remove 1000 elements in under 200ms', () => { + const start = performance.now(); + for (let i = 0; i < 1000; i++) { + spatialIndex.remove(elements[i]); + } + const elapsed = performance.now() - start; + console.log(`Remove 1000 elements: ${elapsed.toFixed(2)}ms`); + expect(elapsed).toBeLessThan(200); + + // Verify count + const results = spatialIndex.search({ minX: 0, minY: 0, maxX: 20000, maxY: 2500 }); + expect(results.length).toBe(N - 1000); + }); + + it('should clear all elements in under 50ms', () => { + const start = performance.now(); + spatialIndex.clear(); + const elapsed = performance.now() - start; + console.log(`Clear all: ${elapsed.toFixed(2)}ms`); + expect(elapsed).toBeLessThan(50); + expect(spatialIndex.search({ minX: 0, minY: 0, maxX: 20000, maxY: 2500 }).length).toBe(0); + }); +});