goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
/**
* 차트·실시간 전략용 STOMP 단일 연결 (SockJS 1회).
* 훅마다 StompClient 를 만들면 "closed before connection is established" 가 반복된다.
*/
import { Client as StompClient, IMessage, StompSubscription } from '@stomp/stompjs';
import SockJS from 'sockjs-client';
import { getStompSockJsUrl } from './backendApi';
type MessageHandler = (msg: IMessage) => void;
interface TopicEntry {
handlers: Set<MessageHandler>;
stompSub?: StompSubscription;
}
let client: StompClient | null = null;
let connected = false;
const topics = new Map<string, TopicEntry>();
let disconnectTimer: ReturnType<typeof setTimeout> | null = null;
function handlerCount(): number {
let n = 0;
for (const entry of topics.values()) n += entry.handlers.size;
return n;
}
function cancelPendingDisconnect(): void {
if (disconnectTimer) {
clearTimeout(disconnectTimer);
disconnectTimer = null;
}
}
function resubscribeAll(): void {
if (!client?.connected) return;
for (const [topic, entry] of topics) {
if (entry.handlers.size === 0) continue;
entry.stompSub?.unsubscribe();
entry.stompSub = client.subscribe(topic, msg => {
entry.handlers.forEach(h => h(msg));
});
}
}
function getOrCreateClient(): StompClient {
if (client) return client;
client = new StompClient({
webSocketFactory: () => new SockJS(getStompSockJsUrl()),
reconnectDelay: 5000,
heartbeatIncoming: 10_000,
heartbeatOutgoing: 10_000,
onConnect: () => {
connected = true;
cancelPendingDisconnect();
resubscribeAll();
},
onDisconnect: () => {
connected = false;
for (const entry of topics.values()) {
entry.stompSub = undefined;
}
},
onStompError: frame => {
console.warn('[stompChartBroker] STOMP error', frame.headers?.message ?? frame);
},
onWebSocketError: ev => {
console.warn('[stompChartBroker] WebSocket error', getStompSockJsUrl(), ev);
},
onWebSocketClose: () => {
connected = false;
for (const entry of topics.values()) {
entry.stompSub = undefined;
}
},
});
return client;
}
function ensureActive(): void {
cancelPendingDisconnect();
const c = getOrCreateClient();
if (!c.active) c.activate();
}
function scheduleDisconnect(): void {
cancelPendingDisconnect();
disconnectTimer = setTimeout(() => {
disconnectTimer = null;
if (handlerCount() > 0) return;
if (!client?.active) return;
// 연결 수립 중 deactivate 하면 "closed before connection is established" 발생
if (!connected) {
scheduleDisconnect();
return;
}
client.deactivate();
connected = false;
}, 5000);
}
/**
* STOMP 토픽 구독 (참조 카운트). 반환 함수로 해제.
*/
export function subscribeStompTopic(topic: string, handler: MessageHandler): () => void {
let entry = topics.get(topic);
if (!entry) {
entry = { handlers: new Set() };
topics.set(topic, entry);
}
entry.handlers.add(handler);
if (connected && client && entry.handlers.size === 1) {
entry.stompSub = client.subscribe(topic, msg => {
entry!.handlers.forEach(h => h(msg));
});
}
ensureActive();
return () => {
const e = topics.get(topic);
if (!e) return;
e.handlers.delete(handler);
if (e.handlers.size === 0) {
e.stompSub?.unsubscribe();
topics.delete(topic);
scheduleDisconnect();
}
};
}
export function isStompChartConnected(): boolean {
return connected && Boolean(client?.connected);
}