Add ApiResult<T> discriminated union, unwrapOr utility, handleApiResponse, and parseApiJsonResponse. Legacy handleResponse and parseJsonResponse are kept as @deprecated to allow incremental migration.
163 lines
4.8 KiB
TypeScript
163 lines
4.8 KiB
TypeScript
import { api } from "/scripts/api.js";
|
|
|
|
(window as unknown as { api: unknown }).api = api;
|
|
|
|
export interface ApiResponse<T = unknown> {
|
|
success: boolean;
|
|
data?: T;
|
|
error?: string;
|
|
message?: string;
|
|
count?: number;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Result type: replaces throwing patterns with explicit ok/error branching.
|
|
// All new API functions must return Promise<ApiResult<T>> instead of
|
|
// throwing on non-2xx or success=false responses.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };
|
|
|
|
/** Extract data or return fallback without throwing (for silent-catch patterns). */
|
|
export function unwrapOr<T>(result: ApiResult<T>, fallback: T): T {
|
|
return result.ok ? result.data : fallback;
|
|
}
|
|
|
|
// Internal helpers shared by all response parsers.
|
|
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|
typeof value === "object" && value !== null;
|
|
|
|
const extractMessage = (value: unknown): string | undefined => {
|
|
if (typeof value === "string") {
|
|
return value;
|
|
}
|
|
if (isObjectRecord(value) && typeof value.message === "string") {
|
|
return value.message;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
/**
|
|
* Result-returning helper for ApiResponse-wrapped endpoints.
|
|
* Does not throw; callers must check result.ok.
|
|
*/
|
|
export async function handleApiResponse<T>(res: Response): Promise<ApiResult<T>> {
|
|
if (!res.ok) {
|
|
let errorMsg = `API error: ${res.status} ${res.statusText}`;
|
|
try {
|
|
const errorJson = await res.json();
|
|
if (errorJson?.error) {
|
|
errorMsg = errorJson.error;
|
|
}
|
|
} catch (_e) {
|
|
// ignore parse error
|
|
}
|
|
return { ok: false, error: errorMsg };
|
|
}
|
|
|
|
const result: ApiResponse<T> = await res.json();
|
|
if (!result.success) {
|
|
return { ok: false, error: result.error || result.message || "Unknown error" };
|
|
}
|
|
return { ok: true, data: result.data as T };
|
|
}
|
|
|
|
/**
|
|
* Result-returning helper for raw JSON endpoints (ComfyUI core: /upload/image, /prompt).
|
|
* Does not throw; callers must check result.ok.
|
|
*/
|
|
export async function parseApiJsonResponse<T>(res: Response): Promise<ApiResult<T>> {
|
|
let data: unknown;
|
|
try {
|
|
data = await res.json();
|
|
} catch (_e) {
|
|
if (!res.ok) {
|
|
return { ok: false, error: `${res.status} ${res.statusText}` };
|
|
}
|
|
return { ok: false, error: "Failed to parse JSON response" };
|
|
}
|
|
|
|
if (!res.ok) {
|
|
let msg = `${res.status} ${res.statusText}`;
|
|
if (isObjectRecord(data)) {
|
|
const err = "error" in data ? data.error : undefined;
|
|
const errMsg = extractMessage(err);
|
|
const message = "message" in data ? extractMessage(data.message) : undefined;
|
|
msg = errMsg ?? message ?? msg;
|
|
}
|
|
return { ok: false, error: msg };
|
|
}
|
|
return { ok: true, data: data as T };
|
|
}
|
|
|
|
/**
|
|
* Generic helper to handle wrapped API responses.
|
|
* @deprecated Use handleApiResponse instead. Kept for backward compatibility during migration.
|
|
*/
|
|
export async function handleResponse<T>(res: Response): Promise<T> {
|
|
if (!res.ok) {
|
|
let errorMsg = `API error: ${res.status} ${res.statusText}`;
|
|
try {
|
|
const errorJson = await res.json();
|
|
if (errorJson?.error) {
|
|
errorMsg = errorJson.error;
|
|
}
|
|
} catch (_e) {
|
|
// ignore parse error
|
|
}
|
|
throw new Error(errorMsg);
|
|
}
|
|
|
|
const result: ApiResponse<T> = await res.json();
|
|
if (!result.success) {
|
|
throw new Error(result.error || result.message || "Unknown error");
|
|
}
|
|
|
|
// For some endpoints, the data itself might be the response,
|
|
// but according to our new rule, it should be in .data
|
|
return result.data as T;
|
|
}
|
|
|
|
/**
|
|
* Helper for endpoints that return raw JSON (not ApiResponse format).
|
|
* Use for ComfyUI core endpoints like /upload/image, /prompt.
|
|
* @deprecated Use parseApiJsonResponse instead. Kept for backward compatibility during migration.
|
|
*/
|
|
export async function parseJsonResponse<T>(res: Response): Promise<T> {
|
|
let data: unknown;
|
|
try {
|
|
data = await res.json();
|
|
} catch (_e) {
|
|
if (!res.ok) {
|
|
throw new Error(`${res.status} ${res.statusText}`);
|
|
}
|
|
throw new Error("Failed to parse JSON response");
|
|
}
|
|
|
|
if (!res.ok) {
|
|
let msg = `${res.status} ${res.statusText}`;
|
|
if (isObjectRecord(data)) {
|
|
const err = "error" in data ? data.error : undefined;
|
|
const errMsg = extractMessage(err);
|
|
const message = "message" in data ? extractMessage(data.message) : undefined;
|
|
msg = errMsg ?? message ?? msg;
|
|
}
|
|
throw new Error(msg);
|
|
}
|
|
return data as T;
|
|
}
|
|
|
|
export const fetchHomeDir = async (): Promise<string> => {
|
|
const res = await api.fetchApi("/meld/home-dir");
|
|
const data = await handleResponse<{ home: string }>(res);
|
|
return data.home;
|
|
};
|
|
|
|
export const cleanupDatabase = async (): Promise<{ count: number }> => {
|
|
const res = await api.fetchApi("/meld/cleanup", {
|
|
method: "POST",
|
|
});
|
|
const data = await handleResponse<{ count: number }>(res);
|
|
return data;
|
|
};
|