diff --git a/backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java.orig b/backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java.orig
deleted file mode 100644
index 6f63911..0000000
--- a/backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java.orig
+++ /dev/null
@@ -1,156 +0,0 @@
-package com.algorithmrace.visualizer;
-
-import jakarta.servlet.*;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.core.Ordered;
-import org.springframework.core.annotation.Order;
-import org.springframework.http.HttpStatus;
-import org.springframework.stereotype.Component;
-
-/**
- * IP-based sliding-window rate limiter to prevent DoS attacks on computationally expensive
- * simulation endpoints.
- *
- *
Limits: - /api/simulations/* → 30 requests per 60 seconds per IP - /api/catalog → 120 requests
- * per 60 seconds per IP - All other /api/* → 60 requests per 60 seconds per IP
- */
-@Component
-@Order(Ordered.HIGHEST_PRECEDENCE + 1)
-public class RateLimitFilter implements Filter {
-
- private static final Logger log = LoggerFactory.getLogger(RateLimitFilter.class);
-
- private static final int SIMULATION_LIMIT = 30;
- private static final int CATALOG_LIMIT = 120;
- private static final int DEFAULT_LIMIT = 60;
- private static final long WINDOW_MS = 60_000L;
-
- // Stores: clientIP -> bucket -> [timestamps]
- private final Map> clients = new ConcurrentHashMap<>();
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- if (!(request instanceof HttpServletRequest httpRequest)
- || !(response instanceof HttpServletResponse httpResponse)) {
- chain.doFilter(request, response);
- return;
- }
-
- String path = httpRequest.getRequestURI();
-
- // Only rate-limit API paths
- if (!path.startsWith("/api/")) {
- chain.doFilter(request, response);
- return;
- }
-
- String clientIp = resolveClientIp(httpRequest);
- String bucket = resolveBucket(path);
- int limit = resolveLimit(path);
-
- SlidingWindow window =
- clients
- .computeIfAbsent(clientIp, k -> new ConcurrentHashMap<>())
- .computeIfAbsent(bucket, k -> new SlidingWindow());
-
- if (!window.tryAcquire(limit)) {
- log.warn("Rate limit exceeded for IP={} bucket={}", clientIp, bucket);
- httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
- httpResponse.setContentType("application/json");
- httpResponse
- .getWriter()
- .write(
- "{\"error\":\"Too Many Requests\",\"message\":\"Rate limit exceeded. Please try again"
- + " later.\"}");
- return;
- }
-
- chain.doFilter(request, response);
- }
-
- private String resolveClientIp(HttpServletRequest request) {
- String remoteAddr = request.getRemoteAddr();
-
- // If the direct connection is NOT from a trusted internal IP, we cannot trust headers.
- if (!isInternalIp(remoteAddr)) {
- return remoteAddr;
- }
-
- String forwarded = request.getHeader("X-Forwarded-For");
- if (forwarded != null && !forwarded.isBlank()) {
- // Parse from right-to-left. The rightmost IP is the one added by the last proxy.
- // We skip internal proxies to find the true client IP.
- String[] ips = forwarded.split(",");
- for (int i = ips.length - 1; i >= 0; i--) {
- String ip = ips[i].trim();
- if (!isInternalIp(ip)) {
- if (ip.matches("[0-9a-fA-F.:]+")) {
- return ip;
- }
- }
- }
- }
- return remoteAddr;
- }
-
- private boolean isInternalIp(String ip) {
- if (ip == null) return false;
- // IPv4 localhost
- if (ip.startsWith("127.")) return true;
- // IPv6 localhost
- if (ip.equals("0:0:0:0:0:0:0:1") || ip.equals("::1")) return true;
- // 10.0.0.0/8
- if (ip.startsWith("10.")) return true;
- // 172.16.0.0/12
- if (ip.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..+")) return true;
- // 192.168.0.0/16
- if (ip.startsWith("192.168.")) return true;
-
- return false;
- }
-
- private String resolveBucket(String path) {
- if (path.startsWith("/api/simulations")) return "simulation";
- if (path.startsWith("/api/catalog")) return "catalog";
- return "default";
- }
-
- private int resolveLimit(String path) {
- if (path.startsWith("/api/simulations")) return SIMULATION_LIMIT;
- if (path.startsWith("/api/catalog")) return CATALOG_LIMIT;
- return DEFAULT_LIMIT;
- }
-
- /**
- * Thread-safe sliding window counter. Keeps timestamps of recent requests and evicts expired
- * ones.
- */
- private static class SlidingWindow {
- private final java.util.Deque timestamps =
- new java.util.concurrent.ConcurrentLinkedDeque<>();
-
- boolean tryAcquire(int limit) {
- long now = System.currentTimeMillis();
- long cutoff = now - WINDOW_MS;
-
- // Evict expired timestamps
- while (!timestamps.isEmpty() && timestamps.peekFirst() < cutoff) {
- timestamps.pollFirst();
- }
-
- if (timestamps.size() >= limit) {
- return false;
- }
-
- timestamps.addLast(now);
- return true;
- }
- }
-}
diff --git a/backend/src/main/java/com/algorithmrace/visualizer/dto/SimulationFrame.java b/backend/src/main/java/com/algorithmrace/visualizer/dto/SimulationFrame.java
index afcaa10..e26cdb9 100644
--- a/backend/src/main/java/com/algorithmrace/visualizer/dto/SimulationFrame.java
+++ b/backend/src/main/java/com/algorithmrace/visualizer/dto/SimulationFrame.java
@@ -21,6 +21,4 @@ public record SimulationFrame(
String[][] grid,
List path,
int steps,
- boolean pathFound,
- Integer nodesVisited,
- Integer frontierSize) {}
+ boolean pathFound) {}
diff --git a/backend/src/main/java/com/algorithmrace/visualizer/service/SimulationService.java b/backend/src/main/java/com/algorithmrace/visualizer/service/SimulationService.java
index 19401a9..54746ec 100644
--- a/backend/src/main/java/com/algorithmrace/visualizer/service/SimulationService.java
+++ b/backend/src/main/java/com/algorithmrace/visualizer/service/SimulationService.java
@@ -20,7 +20,6 @@
import com.algorithmrace.visualizer.utils.ComplexityCatalog;
import com.algorithmrace.visualizer.utils.MazeGenerator;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
@@ -28,12 +27,6 @@
@Service
public class SimulationService {
- private static class FrameState {
- int[] lastArray = null;
- int[] lastHighlight = null;
- int[] lastSearchPath = null;
- }
-
private static final int MAX_FRAMES = 5000;
private static final int MAX_ARRAY_SIZE = 100;
private static final int MAX_LANES = 6;
@@ -138,13 +131,12 @@ private RaceLaneResponse simulateSortingLane(String name, int[] dataset) {
AlgorithmModel model = SortingAlgorithmFactory.create(name);
model.resetState(dataset);
List frames = new ArrayList<>();
- FrameState state = new FrameState();
- frames.add(sortFrame(0, model, state));
+ frames.add(sortFrame(0, model));
int frame = 1;
while (!model.isDone() && frame < MAX_FRAMES) {
model.step();
model.setTimeMs((long) frame * SORT_FRAME_MS);
- frames.add(sortFrame(frame, model, state));
+ frames.add(sortFrame(frame, model));
frame++;
}
LaneStats stats =
@@ -158,13 +150,12 @@ private RaceLaneResponse simulateSearchLane(String name, int[] dataset, int targ
model.resetState(dataset);
model.setTarget(target);
List frames = new ArrayList<>();
- FrameState state = new FrameState();
- frames.add(searchFrame(0, model, state));
+ frames.add(searchFrame(0, model));
int frame = 1;
while (!model.isDone() && frame < MAX_FRAMES) {
model.step();
model.setTimeMs((long) frame * SEARCH_FRAME_MS);
- frames.add(searchFrame(frame, model, state));
+ frames.add(searchFrame(frame, model));
frame++;
}
LaneStats stats =
@@ -245,17 +236,11 @@ private int[] resolveSearchingDataset(SearchingSimulationRequest request) {
return ArrayGenerator.generate(size, ArrayGenerator.ArrayType.RANDOM);
}
- private SimulationFrame sortFrame(int frame, AlgorithmModel model, FrameState state) {
- if (state.lastArray == null || !Arrays.equals(state.lastArray, model.getArray())) {
- state.lastArray = model.getArray().clone();
- }
- if (state.lastHighlight == null || !Arrays.equals(state.lastHighlight, model.getHighlight())) {
- state.lastHighlight = model.getHighlight().clone();
- }
+ private SimulationFrame sortFrame(int frame, AlgorithmModel model) {
return new SimulationFrame(
frame,
- state.lastArray,
- state.lastHighlight,
+ model.getArray().clone(),
+ model.getHighlight().clone(),
model.getSortedBoundary(),
model.getPivotIndex(),
model.getMergeRegionStart(),
@@ -271,26 +256,14 @@ private SimulationFrame sortFrame(int frame, AlgorithmModel model, FrameState st
null,
List.of(),
0,
- false,
- null,
- null);
+ false);
}
- private SimulationFrame searchFrame(int frame, SearchModel model, FrameState state) {
- if (state.lastArray == null || !Arrays.equals(state.lastArray, model.getArray())) {
- state.lastArray = model.getArray().clone();
- }
- if (state.lastHighlight == null || !Arrays.equals(state.lastHighlight, model.getHighlight())) {
- state.lastHighlight = model.getHighlight().clone();
- }
- if (state.lastSearchPath == null
- || !Arrays.equals(state.lastSearchPath, model.getSearchPath())) {
- state.lastSearchPath = model.getSearchPath().clone();
- }
+ private SimulationFrame searchFrame(int frame, SearchModel model) {
return new SimulationFrame(
frame,
- state.lastArray,
- state.lastHighlight,
+ model.getArray().clone(),
+ model.getHighlight().clone(),
model.getSortedBoundary(),
model.getPivotIndex(),
model.getMergeRegionStart(),
@@ -302,34 +275,14 @@ private SimulationFrame searchFrame(int frame, SearchModel model, FrameState sta
model.isDone(),
model.getStatus(),
model.getFoundIndex(),
- state.lastSearchPath,
+ model.getSearchPath().clone(),
null,
List.of(),
0,
- false,
- null,
- null);
+ false);
}
private SimulationFrame pathFrame(int frame, PathfindingModel model, long timeMs) {
- GridCell[][] grid = model.getGrid();
- String[][] states = new String[grid.length][grid[0].length];
- int nodesVisited = 0;
- int frontierSize = 0;
-
- for (int r = 0; r < grid.length; r++) {
- for (int c = 0; c < grid[r].length; c++) {
- CellState cellState = grid[r][c].state;
- states[r][c] = cellState.name();
-
- if (cellState == CellState.VISITED || cellState == CellState.PATH) {
- nodesVisited++;
- } else if (cellState == CellState.FRONTIER) {
- frontierSize++;
- }
- }
- }
-
return new SimulationFrame(
frame,
new int[0],
@@ -346,12 +299,20 @@ private SimulationFrame pathFrame(int frame, PathfindingModel model, long timeMs
model.isDone() ? "Done" : "Running",
null,
new int[0],
- states,
+ gridState(model.getGrid()),
model.getPath().stream().map(cell -> new PointDto(cell.row, cell.col)).toList(),
model.getSteps(),
- model.isPathFound(),
- nodesVisited,
- frontierSize);
+ model.isPathFound());
+ }
+
+ private String[][] gridState(GridCell[][] grid) {
+ String[][] states = new String[grid.length][grid[0].length];
+ for (int r = 0; r < grid.length; r++) {
+ for (int c = 0; c < grid[r].length; c++) {
+ states[r][c] = grid[r][c].state.name();
+ }
+ }
+ return states;
}
private void markPath(PathfindingModel model) {
diff --git a/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java b/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java
index 4c91b94..485d85b 100644
--- a/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java
+++ b/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java
@@ -1,121 +1,90 @@
package com.algorithmrace.visualizer;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
import jakarta.servlet.FilterChain;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import java.lang.reflect.Method;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
class RateLimitFilterTest {
- private RateLimitFilter filter;
- private Method resolveClientIpMethod;
- private HttpServletRequest mockRequest;
- private HttpServletResponse mockResponse;
- private FilterChain mockFilterChain;
-
- @BeforeEach
- void setUp() throws Exception {
- filter = new RateLimitFilter();
- resolveClientIpMethod =
- RateLimitFilter.class.getDeclaredMethod("resolveClientIp", HttpServletRequest.class);
- resolveClientIpMethod.setAccessible(true);
- mockRequest = mock(HttpServletRequest.class);
- mockResponse = mock(HttpServletResponse.class);
- mockFilterChain = mock(FilterChain.class);
- }
-
- private String invokeResolveClientIp(HttpServletRequest request) throws Exception {
- return (String) resolveClientIpMethod.invoke(filter, request);
- }
-
@Test
- void testDirectRequest() throws Exception {
+ public void testDirectConnectionSpoofingIgnored() throws Exception {
+ RateLimitFilter filter = new RateLimitFilter();
MockHttpServletRequest request = new MockHttpServletRequest();
- request.setRemoteAddr("203.0.113.1");
-
- assertEquals("203.0.113.1", invokeResolveClientIp(request));
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain chain = mock(FilterChain.class);
+
+ request.setRequestURI("/api/simulations");
+ // Direct connection from attacker's public IP
+ request.setRemoteAddr("203.0.113.5");
+ // Attacker tries to spoof someone else's IP
+ request.addHeader("X-Forwarded-For", "198.51.100.10");
+
+ // Hit the limit
+ for (int i = 0; i < 30; i++) {
+ filter.doFilter(request, response, chain);
+ response = new MockHttpServletResponse();
+ }
+ // 31st request should be blocked
+ filter.doFilter(request, response, chain);
+ assertEquals(429, response.getStatus());
+
+ // Now attacker tries to use another spoofed IP but same real remote IP
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ request.setRequestURI("/api/simulations");
+ request.setRemoteAddr("203.0.113.5");
+ request.addHeader("X-Forwarded-For", "198.51.100.11");
+
+ filter.doFilter(request, response, chain);
+ // Should still be blocked because it should ignore the spoofed header and use the remote addr
+ assertEquals(429, response.getStatus());
}
@Test
- void testDirectRequestWithSpoofedHeader() throws Exception {
+ public void testBehindProxySpoofingIgnored() throws Exception {
+ RateLimitFilter filter = new RateLimitFilter();
MockHttpServletRequest request = new MockHttpServletRequest();
- request.setRemoteAddr("203.0.113.1");
- // This is a direct connection from a public IP that has spoofed the X-Forwarded-For header
- request.addHeader("X-Forwarded-For", "198.51.100.1");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain chain = mock(FilterChain.class);
- assertEquals("203.0.113.1", invokeResolveClientIp(request));
- }
-
- @Test
- void testRequestFromInternalProxy() throws Exception {
- MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setRequestURI("/api/simulations");
+ // Connection from trusted internal proxy
request.setRemoteAddr("10.0.0.5");
- request.addHeader("X-Forwarded-For", "203.0.113.1");
-
- assertEquals("203.0.113.1", invokeResolveClientIp(request));
- }
-
- @Test
- void testRequestFromInternalProxySpoofedChain() throws Exception {
- MockHttpServletRequest request = new MockHttpServletRequest();
- request.setRemoteAddr("10.0.0.5");
- // Client sent spoofed IP 1.2.3.4, real IP is 203.0.113.1
- request.addHeader("X-Forwarded-For", "1.2.3.4, 203.0.113.1");
-
- assertEquals("203.0.113.1", invokeResolveClientIp(request));
- }
-
- @Test
- void testRequestFromMultipleInternalProxies() throws Exception {
- MockHttpServletRequest request = new MockHttpServletRequest();
+ // Attacker spoofed IP and proxy appended real attacker IP
+ request.addHeader("X-Forwarded-For", "1.2.3.4, 203.0.113.5");
+
+ // Hit the limit for attacker real IP
+ for (int i = 0; i < 30; i++) {
+ filter.doFilter(request, response, chain);
+ response = new MockHttpServletResponse();
+ }
+ // 31st request should be blocked
+ filter.doFilter(request, response, chain);
+ assertEquals(429, response.getStatus());
+
+ // Attacker changes spoofed IP, but proxy still appends real IP
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ request.setRequestURI("/api/simulations");
request.setRemoteAddr("10.0.0.5");
- // Client IP 203.0.113.1, passed through internal proxy 192.168.1.1, then to 10.0.0.5
- request.addHeader("X-Forwarded-For", "203.0.113.1, 192.168.1.1");
+ request.addHeader("X-Forwarded-For", "5.6.7.8, 203.0.113.5");
- assertEquals("203.0.113.1", invokeResolveClientIp(request));
- }
-
- @Test
- void testRequestFromMultipleInternalProxiesSpoofed() throws Exception {
- MockHttpServletRequest request = new MockHttpServletRequest();
- request.setRemoteAddr("10.0.0.5");
- // Client spoofed 1.2.3.4, real IP 203.0.113.1, internal proxy 192.168.1.1
- request.addHeader("X-Forwarded-For", "1.2.3.4, 203.0.113.1, 192.168.1.1");
-
- assertEquals("203.0.113.1", invokeResolveClientIp(request));
- }
-
- @Test
- void testRequestFromInternalProxyNoValidIp() throws Exception {
- MockHttpServletRequest request = new MockHttpServletRequest();
- request.setRemoteAddr("10.0.0.5");
- request.addHeader("X-Forwarded-For", "invalid_ip");
-
- // Should fallback to remote addr if no valid IP found in header
- assertEquals("10.0.0.5", invokeResolveClientIp(request));
- }
-
- @Test
- void testRequestFromInternalClientViaInternalProxy() throws Exception {
- MockHttpServletRequest request = new MockHttpServletRequest();
- request.setRemoteAddr("10.0.0.5");
- request.addHeader("X-Forwarded-For", "192.168.1.50");
-
- assertEquals("192.168.1.50", invokeResolveClientIp(request));
+ filter.doFilter(request, response, chain);
+ // Should still be blocked because it correctly identifies 203.0.113.5
+ assertEquals(429, response.getStatus());
}
@Test
void doFilter_nonApiRoot_bypassesFilter() throws Exception {
+ RateLimitFilter filter = new RateLimitFilter();
+ MockHttpServletRequest mockRequest = mock(MockHttpServletRequest.class);
+ MockHttpServletResponse mockResponse = mock(MockHttpServletResponse.class);
+ FilterChain mockFilterChain = mock(FilterChain.class);
+
when(mockRequest.getRequestURI()).thenReturn("/");
filter.doFilter(mockRequest, mockResponse, mockFilterChain);
@@ -126,6 +95,11 @@ void doFilter_nonApiRoot_bypassesFilter() throws Exception {
@Test
void doFilter_untrustedProxyHeader_ignoresSpoofedHeader() throws Exception {
+ RateLimitFilter filter = new RateLimitFilter();
+ MockHttpServletRequest mockRequest = mock(MockHttpServletRequest.class);
+ MockHttpServletResponse mockResponse = mock(MockHttpServletResponse.class);
+ FilterChain mockFilterChain = mock(FilterChain.class);
+
when(mockRequest.getRequestURI()).thenReturn("/api/simulations/sorting");
when(mockRequest.getRemoteAddr()).thenReturn("203.0.113.195"); // Public IP (not proxy)
when(mockRequest.getHeader("X-Forwarded-For")).thenReturn("198.51.100.10"); // Spoofed IP
@@ -137,6 +111,11 @@ void doFilter_untrustedProxyHeader_ignoresSpoofedHeader() throws Exception {
@Test
void doFilter_trustedProxyHeader_usesForwardedHeader() throws Exception {
+ RateLimitFilter filter = new RateLimitFilter();
+ MockHttpServletRequest mockRequest = mock(MockHttpServletRequest.class);
+ MockHttpServletResponse mockResponse = mock(MockHttpServletResponse.class);
+ FilterChain mockFilterChain = mock(FilterChain.class);
+
when(mockRequest.getRequestURI()).thenReturn("/api/simulations/sorting");
when(mockRequest.getRemoteAddr()).thenReturn("127.0.0.1"); // Trusted local proxy
when(mockRequest.getHeader("X-Forwarded-For")).thenReturn("198.51.100.10");
diff --git a/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java.patch b/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java.patch
deleted file mode 100644
index 230cc67..0000000
--- a/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java
-+++ backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java
-@@ -83,4 +83,12 @@
- // Should fallback to remote addr if no valid IP found in header
- assertEquals("10.0.0.5", invokeResolveClientIp(request));
- }
-+
-+ @Test
-+ public void testRequestFromInternalClientViaInternalProxy() throws Exception {
-+ MockHttpServletRequest request = new MockHttpServletRequest();
-+ request.setRemoteAddr("10.0.0.5");
-+ request.addHeader("X-Forwarded-For", "192.168.1.50");
-+
-+ assertEquals("192.168.1.50", invokeResolveClientIp(request));
-+ }
diff --git a/frontend/src/components/HeroMiniCanvas.tsx b/frontend/src/components/HeroMiniCanvas.tsx
index b6d2513..c4112ca 100644
--- a/frontend/src/components/HeroMiniCanvas.tsx
+++ b/frontend/src/components/HeroMiniCanvas.tsx
@@ -107,7 +107,7 @@ export function HeroMiniCanvas() {
array: [...qArr],
comparing: [j, high],
swapping: [],
- sorted: getSortedIndices(low, high, qArr.length),
+ sorted: getSortedIndices(low, high, qArr),
pivot: high,
});
if (qArr[j] < pivotVal) {
@@ -121,7 +121,7 @@ export function HeroMiniCanvas() {
array: [...qArr],
comparing: [],
swapping: [i, j],
- sorted: getSortedIndices(low, high, qArr.length),
+ sorted: getSortedIndices(low, high, qArr),
pivot: high,
});
}
@@ -136,7 +136,7 @@ export function HeroMiniCanvas() {
array: [...qArr],
comparing: [],
swapping: [i + 1, high],
- sorted: getSortedIndices(low, high, qArr.length),
+ sorted: getSortedIndices(low, high, qArr),
pivot: pIndex,
});
@@ -145,15 +145,10 @@ export function HeroMiniCanvas() {
}
};
- const getSortedIndices = (currentLow: number, currentHigh: number, length: number) => {
- const sortedCount = length - (currentHigh - currentLow + 1);
- const sorted: number[] = new Array(sortedCount);
- let idx = 0;
- for (let k = 0; k < currentLow; k++) {
- sorted[idx++] = k;
- }
- for (let k = currentHigh + 1; k < length; k++) {
- sorted[idx++] = k;
+ const getSortedIndices = (currentLow: number, currentHigh: number, currentArr: number[]) => {
+ const sorted: number[] = [];
+ for (let k = 0; k < currentArr.length; k++) {
+ if (k < currentLow || k > currentHigh) sorted.push(k);
}
return sorted;
};
diff --git a/frontend/src/components/PerformanceComparison.tsx b/frontend/src/components/PerformanceComparison.tsx
index 1e32e54..85a995d 100644
--- a/frontend/src/components/PerformanceComparison.tsx
+++ b/frontend/src/components/PerformanceComparison.tsx
@@ -164,9 +164,17 @@ export function PerformanceComparison({
let nodesVisited = 0;
let frontierSize = 0;
- if (type === 'pathfinding') {
- nodesVisited = frame?.nodesVisited ?? 0;
- frontierSize = frame?.frontierSize ?? 0;
+ if (type === 'pathfinding' && frame?.grid) {
+ for (let r = 0; r < frame.grid.length; r++) {
+ for (let c = 0; c < frame.grid[r].length; c++) {
+ const cellState = frame.grid[r][c];
+ if (cellState === 'VISITED' || cellState === 'PATH') {
+ nodesVisited++;
+ } else if (cellState === 'FRONTIER') {
+ frontierSize++;
+ }
+ }
+ }
}
const pathLength = (type === 'pathfinding' && frame?.path) ? frame.path.length : 0;
diff --git a/frontend/src/models/types.ts b/frontend/src/models/types.ts
index 584c4bc..362498c 100644
--- a/frontend/src/models/types.ts
+++ b/frontend/src/models/types.ts
@@ -38,8 +38,6 @@ export type SimulationFrame = {
path: PointDto[];
steps: number;
pathFound: boolean;
- nodesVisited?: number;
- frontierSize?: number;
};
export type LaneStats = {
diff --git a/frontend/src/utils/arrayParser.test.ts b/frontend/src/utils/arrayParser.test.ts
index 6cce14a..5ee5108 100644
--- a/frontend/src/utils/arrayParser.test.ts
+++ b/frontend/src/utils/arrayParser.test.ts
@@ -2,51 +2,48 @@ import { describe, it, expect } from 'vitest';
import { parseCustomArrayInput } from './arrayParser';
describe('parseCustomArrayInput', () => {
- it('should parse standard comma-separated integers', () => {
- expect(parseCustomArrayInput('5, 8, 10')).toEqual([5, 8, 10]);
+ it('should parse a typical comma-separated input', () => {
+ expect(parseCustomArrayInput("1, 2, 3")).toEqual([1, 2, 3]);
});
it('should handle trailing commas', () => {
- expect(parseCustomArrayInput('5, 8,')).toEqual([5, 8]);
+ expect(parseCustomArrayInput("5, 8,")).toEqual([5, 8]);
});
it('should handle empty middle commas', () => {
- expect(parseCustomArrayInput('5,, 8')).toEqual([5, 8]);
+ expect(parseCustomArrayInput("5,, 8")).toEqual([5, 8]);
});
- it('should parse a single element', () => {
- expect(parseCustomArrayInput('5')).toEqual([5]);
+ it('should handle a single element', () => {
+ expect(parseCustomArrayInput("5")).toEqual([5]);
});
- it('should parse a zero element', () => {
- expect(parseCustomArrayInput('0')).toEqual([0]);
+ it('should handle a zero element', () => {
+ expect(parseCustomArrayInput("0")).toEqual([0]);
});
it('should handle duplicate elements', () => {
- expect(parseCustomArrayInput('5, 5, 5')).toEqual([5, 5, 5]);
+ expect(parseCustomArrayInput("5, 5, 5")).toEqual([5, 5, 5]);
});
it('should ignore invalid non-numeric entries', () => {
- expect(parseCustomArrayInput('5, abc, 8')).toEqual([5, 8]);
- expect(parseCustomArrayInput('abc, def')).toEqual([]);
- expect(parseCustomArrayInput('1, 2.5, 3')).toEqual([1, 3]); // ignores floats based on regex /^-?\d+$/
+ expect(parseCustomArrayInput("5, abc, 8")).toEqual([5, 8]);
});
- it('should handle negative numbers', () => {
- expect(parseCustomArrayInput('-5, 0, 10')).toEqual([-5, 0, 10]);
+ it('should parse negative numbers', () => {
+ expect(parseCustomArrayInput("-5, 0, 10")).toEqual([-5, 0, 10]);
});
- it('should return an empty array for an empty string or all non-numeric input', () => {
+ it('should return an empty array for an empty string', () => {
expect(parseCustomArrayInput("")).toEqual([]);
- expect(parseCustomArrayInput("abc, def")).toEqual([]);
});
- it('should return empty array for non-string inputs', () => {
- // @ts-expect-error Testing invalid input type
+ it('should return an empty array for null/undefined/non-string input', () => {
+ // @ts-expect-error testing invalid input types
expect(parseCustomArrayInput(null)).toEqual([]);
- // @ts-expect-error Testing invalid input type
+ // @ts-expect-error testing invalid input types
expect(parseCustomArrayInput(undefined)).toEqual([]);
- // @ts-expect-error Testing invalid input type
+ // @ts-expect-error testing invalid input types
expect(parseCustomArrayInput(123)).toEqual([]);
});
});
diff --git a/patch.diff b/patch.diff
deleted file mode 100644
index df68d2d..0000000
--- a/patch.diff
+++ /dev/null
@@ -1,26 +0,0 @@
---- backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java
-+++ backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java
-@@ -87,19 +87,22 @@
- return remoteAddr;
- }
-
-+ String lastTrustedIp = remoteAddr;
- String forwarded = request.getHeader("X-Forwarded-For");
- if (forwarded != null && !forwarded.isBlank()) {
- // Parse from right-to-left. The rightmost IP is the one added by the last proxy.
- // We skip internal proxies to find the true client IP.
- String[] ips = forwarded.split(",");
- for (int i = ips.length - 1; i >= 0; i--) {
- String ip = ips[i].trim();
- if (!isInternalIp(ip)) {
- if (ip.matches("[0-9a-fA-F.:]+")) {
- return ip;
- }
-+ } else {
-+ lastTrustedIp = ip;
- }
- }
- }
-- return remoteAddr;
-+ return lastTrustedIp;
- }