docs(console-ui): explain data and byte transformations
This commit is contained in:
@@ -1,3 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Typed management-plane client for `wasmeld-console`.
|
||||||
|
*
|
||||||
|
* These DTOs intentionally preserve the Rust API's snake_case fields; view
|
||||||
|
* formatting belongs in `console-model.ts`. Management invocation bytes use
|
||||||
|
* base64 inside JSON for inspectability. Public clients should call the
|
||||||
|
* gateway's `application/octet-stream` endpoint instead.
|
||||||
|
*/
|
||||||
|
|
||||||
export const DEFAULT_API_BASE = import.meta.env.VITE_WASMELD_API_URL ?? "http://127.0.0.1:8080";
|
export const DEFAULT_API_BASE = import.meta.env.VITE_WASMELD_API_URL ?? "http://127.0.0.1:8080";
|
||||||
|
|
||||||
const API_BASE_STORAGE_KEY = "wasmeld-api-base";
|
const API_BASE_STORAGE_KEY = "wasmeld-api-base";
|
||||||
@@ -80,6 +89,8 @@ export type InvokeResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function getApiBase(): string {
|
export function getApiBase(): string {
|
||||||
|
// TanStack Start can evaluate modules during SSR, where localStorage does
|
||||||
|
// not exist. The environment default keeps server rendering deterministic.
|
||||||
if (typeof window === "undefined") return DEFAULT_API_BASE;
|
if (typeof window === "undefined") return DEFAULT_API_BASE;
|
||||||
return window.localStorage.getItem(API_BASE_STORAGE_KEY) ?? DEFAULT_API_BASE;
|
return window.localStorage.getItem(API_BASE_STORAGE_KEY) ?? DEFAULT_API_BASE;
|
||||||
}
|
}
|
||||||
@@ -95,6 +106,9 @@ export function saveApiBase(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSnapshot(apiBase: string) {
|
export async function fetchSnapshot(apiBase: string) {
|
||||||
|
// Parallel reads minimize refresh latency but are not a database
|
||||||
|
// transaction. A deployment can change between responses; the five-second
|
||||||
|
// poll in the route reconciles that short-lived inconsistency.
|
||||||
const [services, deployments, events, runtime, witPackages] = await Promise.all([
|
const [services, deployments, events, runtime, witPackages] = await Promise.all([
|
||||||
request<{ services: BackendService[] }>(apiBase, "/api/v1/services"),
|
request<{ services: BackendService[] }>(apiBase, "/api/v1/services"),
|
||||||
request<{ deployments: BackendDeployment[] }>(apiBase, "/api/v1/deployments"),
|
request<{ deployments: BackendDeployment[] }>(apiBase, "/api/v1/deployments"),
|
||||||
@@ -181,6 +195,9 @@ export async function invokeComponent(
|
|||||||
service: Pick<BackendService, "id" | "revision">,
|
service: Pick<BackendService, "id" | "revision">,
|
||||||
input: Uint8Array,
|
input: Uint8Array,
|
||||||
): Promise<InvokeResult> {
|
): Promise<InvokeResult> {
|
||||||
|
// Example: `invokeComponent(base, service, new TextEncoder().encode("ping"))`.
|
||||||
|
// The management endpoint targets an exact revision and is intended for the
|
||||||
|
// operator console, not public application traffic.
|
||||||
const response = await request<{
|
const response = await request<{
|
||||||
output_base64: string;
|
output_base64: string;
|
||||||
output_bytes: number;
|
output_bytes: number;
|
||||||
@@ -216,6 +233,9 @@ async function request<T>(apiBase: string, path: string, init?: RequestInit): Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
// Both management and gateway errors use `{ error: { code, message } }`.
|
||||||
|
// The UI displays only the server-safe message and falls back to status
|
||||||
|
// when a proxy returns HTML or an otherwise unrelated response.
|
||||||
const body = (await response.json().catch(() => null)) as {
|
const body = (await response.json().catch(() => null)) as {
|
||||||
error?: { message?: string };
|
error?: { message?: string };
|
||||||
} | null;
|
} | null;
|
||||||
@@ -225,6 +245,8 @@ async function request<T>(apiBase: string, path: string, init?: RequestInit): Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bytesToBase64(bytes: Uint8Array): string {
|
function bytesToBase64(bytes: Uint8Array): string {
|
||||||
|
// Invocation input is bounded by the Runtime, so constructing this binary
|
||||||
|
// string cannot grow beyond the configured management request limit.
|
||||||
let binary = "";
|
let binary = "";
|
||||||
for (const byte of bytes) binary += String.fromCharCode(byte);
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
||||||
return window.btoa(binary);
|
return window.btoa(binary);
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Pure conversion and presentation helpers for the management UI.
|
||||||
|
*
|
||||||
|
* No function in this module performs I/O or changes backend state. Keeping
|
||||||
|
* byte decoding here prevents React views from silently treating arbitrary
|
||||||
|
* Component output as readable text.
|
||||||
|
*/
|
||||||
|
|
||||||
import { FileCode2, History, LayoutDashboard, Package, Server, Settings } from "lucide-react";
|
import { FileCode2, History, LayoutDashboard, Package, Server, Settings } from "lucide-react";
|
||||||
import { BackendCapability, BackendEvent, BackendService } from "./api";
|
import { BackendCapability, BackendEvent, BackendService } from "./api";
|
||||||
|
|
||||||
@@ -160,6 +168,9 @@ export function decodeReadableUtf8(bytes: Uint8Array) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Valid UTF-8 can still contain terminal controls. Reject them so the output
|
||||||
|
// panel cannot render invisible or misleading content; tab/newline/CR remain
|
||||||
|
// useful for ordinary textual responses.
|
||||||
for (const character of text) {
|
for (const character of text) {
|
||||||
const codePoint = character.codePointAt(0) ?? 0;
|
const codePoint = character.codePointAt(0) ?? 0;
|
||||||
const allowedWhitespace = codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d;
|
const allowedWhitespace = codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d;
|
||||||
@@ -175,6 +186,9 @@ export function formatInvocationOutput(
|
|||||||
output: Uint8Array,
|
output: Uint8Array,
|
||||||
format: "utf8" | "hex",
|
format: "utf8" | "hex",
|
||||||
) {
|
) {
|
||||||
|
// Counter is the one built-in example with a documented numeric wire
|
||||||
|
// format. Other eight-byte services remain generic bytes and are not
|
||||||
|
// guessed as integers.
|
||||||
if (service.id === "counter" && output.byteLength === 8) {
|
if (service.id === "counter" && output.byteLength === 8) {
|
||||||
return {
|
return {
|
||||||
text: new DataView(output.buffer, output.byteOffset, output.byteLength)
|
text: new DataView(output.buffer, output.byteOffset, output.byteLength)
|
||||||
@@ -192,6 +206,8 @@ export function formatInvocationOutput(
|
|||||||
} satisfies FormattedInvocationOutput;
|
} satisfies FormattedInvocationOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UTF-8 decoding is fatal rather than replacement-based. If one byte is
|
||||||
|
// invalid or contains a disallowed control, show the exact bytes as HEX.
|
||||||
const text = decodeReadableUtf8(output);
|
const text = decodeReadableUtf8(output);
|
||||||
if (text !== null) {
|
if (text !== null) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ function Home() {
|
|||||||
setEvents(snapshot.events.map(toEvent));
|
setEvents(snapshot.events.map(toEvent));
|
||||||
setRuntime(snapshot.runtime);
|
setRuntime(snapshot.runtime);
|
||||||
setWitPackages(snapshot.witPackages);
|
setWitPackages(snapshot.witPackages);
|
||||||
|
// Preserve operator context when possible. Registrations and cleanup can
|
||||||
|
// remove the selected revision between polls, in which case the first
|
||||||
|
// remaining service becomes the deterministic fallback.
|
||||||
setSelectedKey((current) =>
|
setSelectedKey((current) =>
|
||||||
nextServices.some((service) => serviceKey(service) === current)
|
nextServices.some((service) => serviceKey(service) === current)
|
||||||
? current
|
? current
|
||||||
@@ -128,6 +131,9 @@ function Home() {
|
|||||||
);
|
);
|
||||||
setConnection("online");
|
setConnection("online");
|
||||||
} catch {
|
} catch {
|
||||||
|
// Clear the complete snapshot on any request failure. Mixing stale
|
||||||
|
// services with live deployments or Runtime state would present actions
|
||||||
|
// against identities that may no longer exist.
|
||||||
setServices([]);
|
setServices([]);
|
||||||
setDeployments([]);
|
setDeployments([]);
|
||||||
setEvents([]);
|
setEvents([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user