2026-07-30 09:21:55 +08:00
|
|
|
import type { BackendEvent, BackendService } from "./api";
|
|
|
|
|
|
|
|
|
|
export type ConnectionState = "connecting" | "online" | "offline";
|
2026-07-30 07:18:29 +08:00
|
|
|
export type ServiceStatus = "running" | "stopped" | "faulted";
|
|
|
|
|
export type EventTone = "success" | "warning" | "danger" | "neutral";
|
|
|
|
|
|
|
|
|
|
export type Service = {
|
|
|
|
|
id: string;
|
|
|
|
|
revision: string;
|
|
|
|
|
artifact: string;
|
2026-07-30 09:21:55 +08:00
|
|
|
world: string;
|
2026-07-30 07:18:29 +08:00
|
|
|
status: ServiceStatus;
|
|
|
|
|
updatedAt: string;
|
|
|
|
|
memoryMb: number;
|
|
|
|
|
fuel: string;
|
|
|
|
|
deadlineMs: number;
|
|
|
|
|
mailbox: number;
|
|
|
|
|
maxInputKb: number;
|
2026-07-30 09:21:55 +08:00
|
|
|
capabilities: BackendService["capabilities"];
|
2026-07-30 07:18:29 +08:00
|
|
|
calls: number;
|
|
|
|
|
errors: number;
|
|
|
|
|
latencyMs: number | null;
|
|
|
|
|
active: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type RuntimeEvent = {
|
|
|
|
|
id: number;
|
|
|
|
|
time: string;
|
|
|
|
|
title: string;
|
|
|
|
|
detail: string;
|
|
|
|
|
tone: EventTone;
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-30 09:21:55 +08:00
|
|
|
export type FormattedInvocationOutput = {
|
|
|
|
|
text: string;
|
|
|
|
|
format: "UTF-8" | "HEX" | "U64 LE";
|
|
|
|
|
automatic: boolean;
|
|
|
|
|
};
|
2026-07-30 07:18:29 +08:00
|
|
|
|
2026-07-30 15:16:02 +08:00
|
|
|
export const STATUS_META: Record<ServiceStatus, { label: string }> = {
|
|
|
|
|
running: { label: "运行中" },
|
|
|
|
|
stopped: { label: "已停止" },
|
|
|
|
|
faulted: { label: "故障" },
|
2026-07-30 07:18:29 +08:00
|
|
|
};
|
|
|
|
|
|
2026-07-30 09:21:55 +08:00
|
|
|
export function serviceKey(service: Pick<Service, "id" | "revision">): string {
|
2026-07-30 07:18:29 +08:00
|
|
|
return `${service.id}@${service.revision}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function toService(service: BackendService, activeRevision?: string): Service {
|
|
|
|
|
return {
|
|
|
|
|
id: service.id,
|
|
|
|
|
revision: service.revision,
|
|
|
|
|
artifact: service.artifact.split(/[\\/]/).pop() ?? service.artifact,
|
2026-07-30 09:21:55 +08:00
|
|
|
world: service.world,
|
2026-07-30 07:18:29 +08:00
|
|
|
status: service.status,
|
2026-07-30 09:21:55 +08:00
|
|
|
updatedAt: new Date(service.updated_at_ms).toLocaleString("zh-CN", { hour12: false }),
|
2026-07-30 07:18:29 +08:00
|
|
|
memoryMb: Math.round(service.limits.memory_bytes / 1024 / 1024),
|
|
|
|
|
fuel: compactNumber(service.limits.fuel_per_call),
|
|
|
|
|
deadlineMs: service.limits.deadline_ms,
|
|
|
|
|
mailbox: service.limits.mailbox_capacity,
|
|
|
|
|
maxInputKb: Math.round(service.limits.max_input_bytes / 1024),
|
|
|
|
|
capabilities: service.capabilities,
|
|
|
|
|
calls: service.calls,
|
|
|
|
|
errors: service.errors,
|
|
|
|
|
latencyMs: service.last_latency_ms,
|
|
|
|
|
active: service.revision === activeRevision,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function toEvent(event: BackendEvent): RuntimeEvent {
|
|
|
|
|
const identity =
|
|
|
|
|
event.service_id && event.revision ? `${event.service_id}@${event.revision}` : "Wasmeld";
|
|
|
|
|
const meta: Record<BackendEvent["kind"], { label: string; tone: EventTone }> = {
|
|
|
|
|
registered: { label: "已注册", tone: "success" },
|
|
|
|
|
unregistered: { label: "已注销", tone: "neutral" },
|
|
|
|
|
deployed: { label: "部署切换", tone: "success" },
|
|
|
|
|
started: { label: "已启动", tone: "success" },
|
|
|
|
|
stopped: { label: "已停止", tone: "neutral" },
|
|
|
|
|
invoked: { label: "调用完成", tone: "success" },
|
|
|
|
|
failed: { label: "操作失败", tone: "danger" },
|
|
|
|
|
};
|
|
|
|
|
return {
|
|
|
|
|
id: event.id,
|
2026-07-30 09:21:55 +08:00
|
|
|
time: new Date(event.timestamp_ms).toLocaleTimeString("zh-CN", { hour12: false }),
|
2026-07-30 07:18:29 +08:00
|
|
|
title: `${identity} ${meta[event.kind].label}`,
|
|
|
|
|
detail: event.message,
|
|
|
|
|
tone: meta[event.kind].tone,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 09:21:55 +08:00
|
|
|
export function compactNumber(value: number): string {
|
2026-07-30 07:18:29 +08:00
|
|
|
if (value >= 1_000_000) return `${value / 1_000_000}M`;
|
|
|
|
|
if (value >= 1_000) return `${value / 1_000}K`;
|
|
|
|
|
return value.toString();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 09:21:55 +08:00
|
|
|
export function formatDuration(milliseconds: number): string {
|
2026-07-30 07:18:29 +08:00
|
|
|
const totalSeconds = Math.floor(milliseconds / 1000);
|
|
|
|
|
const hours = Math.floor(totalSeconds / 3600);
|
|
|
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
|
|
|
const seconds = totalSeconds % 60;
|
|
|
|
|
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m ${seconds}s`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 09:21:55 +08:00
|
|
|
export function parseInvocationInput(value: string, format: "utf8" | "hex"): Uint8Array {
|
2026-07-30 07:18:29 +08:00
|
|
|
if (format === "utf8") return new TextEncoder().encode(value);
|
|
|
|
|
const compact = value.replace(/\s+/g, "");
|
|
|
|
|
if (compact.length % 2 !== 0 || /[^0-9a-f]/i.test(compact)) {
|
|
|
|
|
throw new Error("HEX 输入必须由成对的十六进制字符组成");
|
|
|
|
|
}
|
|
|
|
|
return Uint8Array.from(compact.match(/.{2}/g)?.map((byte) => Number.parseInt(byte, 16)) ?? []);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 09:21:55 +08:00
|
|
|
export function invocationInputSize(value: string, format: "utf8" | "hex"): number {
|
2026-07-30 07:18:29 +08:00
|
|
|
try {
|
|
|
|
|
return parseInvocationInput(value, format).byteLength;
|
|
|
|
|
} catch {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatInvocationOutput(
|
|
|
|
|
service: Service,
|
|
|
|
|
output: Uint8Array,
|
|
|
|
|
format: "utf8" | "hex",
|
2026-07-30 09:21:55 +08:00
|
|
|
): FormattedInvocationOutput {
|
2026-07-30 07:18:29 +08:00
|
|
|
if (service.id === "counter" && output.byteLength === 8) {
|
|
|
|
|
return {
|
|
|
|
|
text: new DataView(output.buffer, output.byteOffset, output.byteLength)
|
|
|
|
|
.getBigUint64(0, true)
|
|
|
|
|
.toString(),
|
|
|
|
|
format: "U64 LE",
|
|
|
|
|
automatic: false,
|
2026-07-30 09:21:55 +08:00
|
|
|
};
|
2026-07-30 07:18:29 +08:00
|
|
|
}
|
|
|
|
|
if (format === "hex") {
|
2026-07-30 09:21:55 +08:00
|
|
|
return { text: formatHex(output) || "(empty)", format: "HEX", automatic: false };
|
2026-07-30 07:18:29 +08:00
|
|
|
}
|
|
|
|
|
const text = decodeReadableUtf8(output);
|
|
|
|
|
if (text !== null) {
|
2026-07-30 09:21:55 +08:00
|
|
|
return { text: text || "(empty)", format: "UTF-8", automatic: false };
|
2026-07-30 07:18:29 +08:00
|
|
|
}
|
2026-07-30 09:21:55 +08:00
|
|
|
return { text: formatHex(output), format: "HEX", automatic: true };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatHex(bytes: Uint8Array): string {
|
|
|
|
|
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function decodeReadableUtf8(bytes: Uint8Array): string | null {
|
|
|
|
|
let text: string;
|
|
|
|
|
try {
|
|
|
|
|
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
for (const character of text) {
|
|
|
|
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
|
|
|
const allowedWhitespace = codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d;
|
|
|
|
|
if ((codePoint < 0x20 && !allowedWhitespace) || codePoint === 0x7f) return null;
|
|
|
|
|
}
|
|
|
|
|
return text;
|
2026-07-30 07:18:29 +08:00
|
|
|
}
|