import type { BackendEvent, BackendService } from "./api"; export type ConnectionState = "connecting" | "online" | "offline"; export type ServiceStatus = "running" | "stopped" | "faulted"; export type EventTone = "success" | "warning" | "danger" | "neutral"; export type Service = { id: string; revision: string; artifact: string; world: string; status: ServiceStatus; updatedAt: string; memoryMb: number; fuel: string; deadlineMs: number; mailbox: number; maxInputKb: number; capabilities: BackendService["capabilities"]; calls: number; errors: number; latencyMs: number | null; active: boolean; }; export type RuntimeEvent = { id: number; time: string; title: string; detail: string; tone: EventTone; }; export type FormattedInvocationOutput = { text: string; format: "UTF-8" | "HEX" | "U64 LE"; automatic: boolean; }; export const STATUS_META: Record = { running: { label: "运行中" }, stopped: { label: "已停止" }, faulted: { label: "故障" }, }; export function serviceKey(service: Pick): string { 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, world: service.world, status: service.status, updatedAt: new Date(service.updated_at_ms).toLocaleString("zh-CN", { hour12: false }), 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 = { 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, time: new Date(event.timestamp_ms).toLocaleTimeString("zh-CN", { hour12: false }), title: `${identity} ${meta[event.kind].label}`, detail: event.message, tone: meta[event.kind].tone, }; } export function compactNumber(value: number): string { if (value >= 1_000_000) return `${value / 1_000_000}M`; if (value >= 1_000) return `${value / 1_000}K`; return value.toString(); } export function formatDuration(milliseconds: number): string { 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`; } export function parseInvocationInput(value: string, format: "utf8" | "hex"): Uint8Array { 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)) ?? []); } export function invocationInputSize(value: string, format: "utf8" | "hex"): number { try { return parseInvocationInput(value, format).byteLength; } catch { return 0; } } export function formatInvocationOutput( service: Service, output: Uint8Array, format: "utf8" | "hex", ): FormattedInvocationOutput { 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, }; } if (format === "hex") { return { text: formatHex(output) || "(empty)", format: "HEX", automatic: false }; } const text = decodeReadableUtf8(output); if (text !== null) { return { text: text || "(empty)", format: "UTF-8", automatic: false }; } 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; }