Skip to content

Commit 0de8572

Browse files
feat(web): align client with Hono API runtime config + chat signaling
- Resolve runtime API base URL per-request (vortex-config.json + env fallback). - Preserve HTTP status in API errors for consistent UI surfacing. - Fix WebRTC signaling payloads to use `toPeerId` (matches server). - Keep API access centralized in apiClient with deploy-time overrides.
1 parent aee1c0c commit 0de8572

File tree

5 files changed

+108
-24
lines changed

5 files changed

+108
-24
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ Docs live separately in `humanode-network/vortex-simulator-docs`.
2424

2525
- `dist/` is generated build output.
2626
- UI expects the API at `/api/*`. During local dev, Rsbuild proxies `/api/*` to `http://127.0.0.1:8788` by default (override with `API_PROXY_TARGET`).
27+
- To point the UI at a different API host, set `RSBUILD_PUBLIC_API_BASE_URL` at build time or serve `public/vortex-config.json` with `{"apiBaseUrl":"https://api.example.com"}`.

public/vortex-config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"apiBaseUrl": ""
3+
}

src/index.tsx

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,45 @@ import "./styles/global.css";
88
import "./styles/base.css";
99
import { initTheme } from "./lib/theme";
1010

11-
initTheme("sky");
11+
type RuntimeConfig = {
12+
apiBaseUrl?: string;
13+
apiHeaders?: Record<string, string>;
14+
apiCredentials?: RequestCredentials;
15+
};
1216

13-
const rootEl = document.getElementById("root");
14-
if (rootEl === null) {
15-
throw new Error("no root");
17+
async function loadRuntimeConfig(): Promise<void> {
18+
if (typeof window === "undefined") return;
19+
const target = window as typeof window & {
20+
__VORTEX_CONFIG__?: RuntimeConfig;
21+
};
22+
if (target.__VORTEX_CONFIG__) return;
23+
try {
24+
const res = await fetch("/vortex-config.json", { cache: "no-store" });
25+
if (!res.ok) return;
26+
const json = (await res.json()) as unknown;
27+
if (json && typeof json === "object") {
28+
target.__VORTEX_CONFIG__ = json as RuntimeConfig;
29+
}
30+
} catch {
31+
// Ignore missing runtime config.
32+
}
1633
}
1734

18-
const root = ReactDOM.createRoot(rootEl);
19-
root.render(
20-
<React.StrictMode>
21-
<App />
22-
</React.StrictMode>,
23-
);
35+
async function bootstrap(): Promise<void> {
36+
await loadRuntimeConfig();
37+
initTheme("sky");
38+
39+
const rootEl = document.getElementById("root");
40+
if (rootEl === null) {
41+
throw new Error("no root");
42+
}
43+
44+
const root = ReactDOM.createRoot(rootEl);
45+
root.render(
46+
<React.StrictMode>
47+
<App />
48+
</React.StrictMode>,
49+
);
50+
}
51+
52+
void bootstrap();

src/lib/apiClient.ts

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ import type {
2727
PoolProposalPageDto,
2828
} from "@/types/api";
2929

30+
type ApiClientRuntimeConfig = {
31+
apiBaseUrl?: string;
32+
apiHeaders?: Record<string, string>;
33+
apiCredentials?: RequestCredentials;
34+
};
35+
36+
declare global {
37+
interface Window {
38+
__VORTEX_CONFIG__?: ApiClientRuntimeConfig;
39+
}
40+
}
41+
3042
export type ApiErrorPayload = {
3143
error?: {
3244
message?: string;
@@ -47,16 +59,51 @@ export function getApiErrorPayload(error: unknown): ApiErrorPayload | null {
4759
return data as ApiErrorPayload;
4860
}
4961

62+
const envApiBaseUrl =
63+
import.meta.env.RSBUILD_PUBLIC_API_BASE_URL ??
64+
import.meta.env.VITE_API_BASE_URL ??
65+
"";
66+
67+
function getRuntimeConfig(): ApiClientRuntimeConfig | undefined {
68+
if (typeof window === "undefined") return undefined;
69+
return window.__VORTEX_CONFIG__;
70+
}
71+
72+
function getApiBaseUrl(): string {
73+
const runtimeConfig = getRuntimeConfig();
74+
return runtimeConfig?.apiBaseUrl ?? envApiBaseUrl ?? "";
75+
}
76+
77+
function getApiCredentials(): RequestCredentials {
78+
const runtimeConfig = getRuntimeConfig();
79+
return runtimeConfig?.apiCredentials ?? "include";
80+
}
81+
82+
function getApiHeaders(): Record<string, string> {
83+
const runtimeConfig = getRuntimeConfig();
84+
return runtimeConfig?.apiHeaders ?? {};
85+
}
86+
87+
function resolveApiUrl(path: string): string {
88+
if (/^https?:\/\//i.test(path)) return path;
89+
const apiBaseUrl = getApiBaseUrl();
90+
if (!apiBaseUrl) return path;
91+
const base = apiBaseUrl.replace(/\/$/, "");
92+
const suffix = path.startsWith("/") ? path : `/${path}`;
93+
return `${base}${suffix}`;
94+
}
95+
5096
async function readJsonResponse<T>(res: Response): Promise<T> {
5197
const contentType = res.headers.get("content-type") ?? "";
5298
const isJson = contentType.toLowerCase().includes("application/json");
53-
const body = isJson ? ((await res.json()) as unknown) : null;
99+
const body = isJson ? ((await res.json()) as unknown) : await res.text();
54100
if (!res.ok) {
55101
const payload = (body as ApiErrorPayload | null) ?? null;
56-
const message =
102+
const rawMessage =
57103
payload?.error?.message ??
58-
(typeof body === "string" ? body : null) ??
59-
`HTTP ${res.status}`;
104+
(typeof body === "string" && body.trim() ? body : null) ??
105+
res.statusText;
106+
const message = `HTTP ${res.status}${rawMessage ? `: ${rawMessage}` : ""}`;
60107
const error = new Error(message) as ApiError;
61108
if (payload) error.data = payload;
62109
error.status = res.status;
@@ -66,7 +113,10 @@ async function readJsonResponse<T>(res: Response): Promise<T> {
66113
}
67114

68115
export async function apiGet<T>(path: string): Promise<T> {
69-
const res = await fetch(path, { credentials: "include" });
116+
const res = await fetch(resolveApiUrl(path), {
117+
credentials: getApiCredentials(),
118+
headers: getApiHeaders(),
119+
});
70120
return await readJsonResponse<T>(res);
71121
}
72122

@@ -75,10 +125,11 @@ export async function apiPost<T>(
75125
body: unknown,
76126
init?: { headers?: HeadersInit },
77127
): Promise<T> {
78-
const res = await fetch(path, {
128+
const res = await fetch(resolveApiUrl(path), {
79129
method: "POST",
80-
credentials: "include",
130+
credentials: getApiCredentials(),
81131
headers: {
132+
...getApiHeaders(),
82133
"content-type": "application/json",
83134
...(init?.headers ?? {}),
84135
},
@@ -159,14 +210,14 @@ export async function apiChamberChatSignalPost(
159210
input: {
160211
peerId: string;
161212
kind: "offer" | "answer" | "candidate";
162-
targetPeerId?: string;
213+
toPeerId?: string;
163214
payload: Record<string, unknown>;
164215
},
165216
): Promise<{ ok: true }> {
166217
return await apiPost<{ ok: true }>(`/api/chambers/${chamberId}/chat/signal`, {
167218
peerId: input.peerId,
168219
kind: input.kind,
169-
targetPeerId: input.targetPeerId,
220+
toPeerId: input.toPeerId,
170221
payload: input.payload,
171222
});
172223
}

src/pages/chambers/Chamber.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,14 @@ const Chamber: React.FC = () => {
249249
const sendSignal = useCallback(
250250
async (input: {
251251
kind: "offer" | "answer" | "candidate";
252-
targetPeerId: string;
252+
toPeerId: string;
253253
payload: Record<string, unknown>;
254254
}) => {
255255
if (!id) return;
256256
await apiChamberChatSignalPost(id, {
257257
peerId,
258258
kind: input.kind,
259-
targetPeerId: input.targetPeerId,
259+
toPeerId: input.toPeerId,
260260
payload: input.payload,
261261
});
262262
},
@@ -273,7 +273,7 @@ const Chamber: React.FC = () => {
273273
if (!event.candidate) return;
274274
void sendSignal({
275275
kind: "candidate",
276-
targetPeerId: remotePeerId,
276+
toPeerId: remotePeerId,
277277
payload: event.candidate.toJSON() as Record<string, unknown>,
278278
});
279279
};
@@ -321,7 +321,7 @@ const Chamber: React.FC = () => {
321321
if (pc.localDescription) {
322322
await sendSignal({
323323
kind: "answer",
324-
targetPeerId: remotePeerId,
324+
toPeerId: remotePeerId,
325325
payload: pc.localDescription.toJSON() as unknown as Record<
326326
string,
327327
unknown
@@ -404,7 +404,7 @@ const Chamber: React.FC = () => {
404404
if (pc.localDescription) {
405405
await sendSignal({
406406
kind: "offer",
407-
targetPeerId: peer.peerId,
407+
toPeerId: peer.peerId,
408408
payload: pc.localDescription.toJSON() as unknown as Record<
409409
string,
410410
unknown

0 commit comments

Comments
 (0)