mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-08 18:05:10 +00:00
fix: memory leak in SSE log accumulation and infinite reload loop in NodeContext
- Cap GlobalObservabilityView SSE log array at 10,000 entries to prevent unbounded memory growth during long dev-mode sessions - Break NodeContext infinite re-fetch loop by replacing the activeNode closure dependency in refreshNodes with a useRef read, making the callback stable and preventing the useEffect -> setState -> useCallback -> useEffect cycle
This commit is contained in:
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
- **Fixed:** Memory leak in `GlobalObservabilityView` SSE mode — log array now capped at 10,000 entries (`.slice(-10000)`) to prevent unbounded accumulation across long sessions.
|
||||
- **Fixed:** Infinite re-fetch loop in `NodeContext` — `refreshNodes` useCallback no longer depends on `activeNode` state; replaced with a `useRef` to read current node inside the callback without being a reactive dependency.
|
||||
- **Removed:** SSH/SFTP file adapters and remote Docker TCP connections (net negative ~500 lines of code).
|
||||
- **Added:** Distributed API proxying using http-proxy-middleware for HTTP and WebSockets.
|
||||
- **Added:** Long-lived JWT generation for Sencho-to-Sencho API authentication (`POST /api/auth/generate-node-token`).
|
||||
|
||||
@@ -93,9 +93,8 @@ export function GlobalObservabilityView() {
|
||||
const batch = bufferRef.current.splice(0);
|
||||
setLogs(prev => {
|
||||
const merged = [...prev, ...batch];
|
||||
// Infinite scroll: no slice limit in dev mode
|
||||
merged.sort((a, b) => a.timestampMs - b.timestampMs);
|
||||
return merged;
|
||||
return merged.slice(-10000);
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
|
||||
export interface Node {
|
||||
@@ -27,6 +27,9 @@ export function NodeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [activeNode, setActiveNodeState] = useState<Node | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
// Ref lets refreshNodes read current activeNode without being a dep (breaks infinite loop)
|
||||
const activeNodeRef = useRef<Node | null>(null);
|
||||
activeNodeRef.current = activeNode;
|
||||
|
||||
const refreshNodes = useCallback(async () => {
|
||||
try {
|
||||
@@ -35,7 +38,8 @@ export function NodeProvider({ children }: { children: React.ReactNode }) {
|
||||
const data = await res.json();
|
||||
setNodes(data);
|
||||
|
||||
if (!activeNode) {
|
||||
const currentActive = activeNodeRef.current;
|
||||
if (!currentActive) {
|
||||
const defaultNode = data.find((n: Node) => n.is_default);
|
||||
if (defaultNode) {
|
||||
setActiveNodeState(defaultNode);
|
||||
@@ -43,7 +47,7 @@ export function NodeProvider({ children }: { children: React.ReactNode }) {
|
||||
setActiveNodeState(data[0]);
|
||||
}
|
||||
} else {
|
||||
const updatedActive = data.find((n: Node) => n.id === activeNode.id);
|
||||
const updatedActive = data.find((n: Node) => n.id === currentActive.id);
|
||||
if (updatedActive) {
|
||||
setActiveNodeState(updatedActive);
|
||||
} else {
|
||||
@@ -63,7 +67,7 @@ export function NodeProvider({ children }: { children: React.ReactNode }) {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [activeNode]);
|
||||
}, []); // stable — reads activeNode via ref, not closure capture
|
||||
|
||||
const setActiveNode = useCallback((node: Node) => {
|
||||
setActiveNodeState(node);
|
||||
|
||||
Reference in New Issue
Block a user