feat: add WebSocket hook and API client for frontend
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
import { useEffect, useRef, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
interface ForgeEvent {
|
||||||
|
type: string;
|
||||||
|
action: string;
|
||||||
|
data: unknown;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventHandler = (event: ForgeEvent) => void;
|
||||||
|
|
||||||
|
export function useWebSocket() {
|
||||||
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
const handlersRef = useRef<Map<string, Set<EventHandler>>>(new Map());
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const url = `${protocol}//${window.location.host}/ws`;
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
const ws = new WebSocket(url);
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
ws.onopen = () => setConnected(true);
|
||||||
|
ws.onclose = () => {
|
||||||
|
setConnected(false);
|
||||||
|
setTimeout(connect, 2000);
|
||||||
|
};
|
||||||
|
ws.onerror = () => ws.close();
|
||||||
|
|
||||||
|
ws.onmessage = (msg) => {
|
||||||
|
try {
|
||||||
|
const event: ForgeEvent = JSON.parse(msg.data);
|
||||||
|
const key = `${event.type}:${event.action}`;
|
||||||
|
const wildcard = `${event.type}:*`;
|
||||||
|
|
||||||
|
for (const k of [key, wildcard]) {
|
||||||
|
const handlers = handlersRef.current.get(k);
|
||||||
|
if (handlers) {
|
||||||
|
for (const handler of handlers) handler(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore malformed messages
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
connect();
|
||||||
|
return () => wsRef.current?.close();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const subscribe = useCallback((typeAction: string, handler: EventHandler) => {
|
||||||
|
if (!handlersRef.current.has(typeAction)) {
|
||||||
|
handlersRef.current.set(typeAction, new Set());
|
||||||
|
}
|
||||||
|
handlersRef.current.get(typeAction)!.add(handler);
|
||||||
|
return () => {
|
||||||
|
handlersRef.current.get(typeAction)?.delete(handler);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { connected, subscribe };
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const BASE = "/api";
|
||||||
|
|
||||||
|
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||||
|
const res = await fetch(`${BASE}${path}`, {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
throw new Error(body.error || res.statusText);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
health: () => request<{ status: string; wsClients: number }>("/health"),
|
||||||
|
|
||||||
|
workspace: {
|
||||||
|
getConfig: () => request<Record<string, unknown>>("/workspace/config"),
|
||||||
|
updateConfig: (data: Record<string, unknown>) =>
|
||||||
|
request("/workspace/config", { method: "PUT", body: JSON.stringify(data) }),
|
||||||
|
init: () => request("/workspace/init", { method: "POST" }),
|
||||||
|
},
|
||||||
|
|
||||||
|
docker: {
|
||||||
|
containers: () => request<Array<Record<string, string>>>("/docker/containers"),
|
||||||
|
pull: (image: string) =>
|
||||||
|
request("/docker/pull", { method: "POST", body: JSON.stringify({ image }) }),
|
||||||
|
start: (name: string) =>
|
||||||
|
request(`/docker/containers/${name}/start`, { method: "POST" }),
|
||||||
|
stop: (name: string) =>
|
||||||
|
request(`/docker/containers/${name}/stop`, { method: "POST" }),
|
||||||
|
restart: (name: string) =>
|
||||||
|
request(`/docker/containers/${name}/restart`, { method: "POST" }),
|
||||||
|
logs: (name: string, tail = 100) =>
|
||||||
|
request<{ logs: string }>(`/docker/containers/${name}/logs?tail=${tail}`),
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user