import { isReady } from "../../compat/stub"; import { t } from "../../i18n/runtime"; import { API } from "../ws/connect"; import type { ArtifactRow, ArtifactVersionRow } from "./types"; export { API }; export type FetchFn = (input: string, init?: RequestInit) => Promise; let fetchImpl: FetchFn | null = null; export function setArtifactsFetch(fn: FetchFn | null): void { fetchImpl = fn; } export function artifactsFetch(input: string, init?: RequestInit): Promise { const impl = fetchImpl ?? (globalThis as { fetch?: FetchFn }).fetch; if (!impl) return Promise.reject(new Error("fetch is not available")); return impl(input, init); } /** app.js:93-101 */ export class ApiError extends Error { code: string; status: number; requestId: string; body: unknown; constructor(body: unknown, httpStatus: number) { const rec = body && typeof body === "object" ? (body as Record) : null; super(String((rec && (rec.error || rec.detail)) || "HTTP " + httpStatus)); this.name = "ApiError"; this.code = rec && rec.code != null ? String(rec.code) : ""; this.status = rec && rec.status != null ? Number(rec.status) : httpStatus; this.requestId = rec && rec.request_id != null ? String(rec.request_id) : ""; this.body = body; } } /** app.js:107-109 */ export function apiErrorText(e: unknown): string { const err = e as { message?: unknown; requestId?: unknown } | null; const msg = err && err.message != null ? String(err.message) : String(e); return err && err.requestId ? `${msg} [${err.requestId}]` : msg; } /** * app.js:111-118. `p` must be an internal same-origin API path. */ export async function api(p: string, o: RequestInit = {}): Promise { if (typeof p !== "string" || p[0] !== "/" || p[1] === "/") { throw new Error("invalid api path"); } const r = await artifactsFetch(API + p, { headers: { "content-type": "application/json" }, ...o, }); const text = await r.text(); let j: unknown = null; try { j = text ? JSON.parse(text) : null; } catch { j = text; } if (!r.ok) throw new ApiError(j, r.status); return j; } export function asArtifactList(value: unknown): ArtifactRow[] { if (!Array.isArray(value)) return []; const rows: ArtifactRow[] = []; for (const item of value) { if (!item || typeof item !== "object") continue; const rec = item as Record; const id = rec.id || rec.artifact_id; if (id == null || id === "") continue; rows.push({ ...rec, id: String(id) } as ArtifactRow); } return rows; } export function asVersionList(value: unknown): ArtifactVersionRow[] { if (!value || typeof value !== "object") return []; const raw = (value as { versions?: unknown }).versions; if (!Array.isArray(raw)) return []; const rows: ArtifactVersionRow[] = []; for (const item of raw) { if (!item || typeof item !== "object") continue; const rec = item as Record; if (typeof rec.version_id !== "string" || !rec.version_id) continue; rows.push(rec as ArtifactVersionRow); } return rows; } /** app.js:12919 */ export function bytes(b: number | null | undefined): string { b = b || 0; if (b < 1024) return b + " B"; if (b < 1048576) return (b / 1024).toFixed(1) + " KB"; return (b / 1048576).toFixed(1) + " MB"; } /** * app.js:6055-6066. Heuristic: raw binary / giant base64|hex blob. */ export function looksBinary(s: string | null | undefined): boolean { if (!s) return false; const sample = s.slice(0, 4096); let ctrl = 0; for (let i = 0; i < sample.length; i++) { const c = sample.charCodeAt(i); if (c === 9 || c === 10 || c === 13) continue; if (c < 32 || c === 127 || c === 0xfffd) ctrl++; } if (sample.length && ctrl / sample.length > 0.12) return true; return /[A-Za-z0-9+/=]{1200,}/.test(s) || /(?:\\x[0-9a-fA-F]{2}){400,}/.test(s); } export function el( tag: K, className?: string | null, text?: string | null, ): HTMLElementTagNameMap[K] { const node = document.createElement(tag); if (className) node.className = className; if (text != null) node.textContent = text; return node; } export function svgElement(name: string, attrs?: Record): SVGElement { const node = document.createElementNS("http://www.w3.org/2000/svg", name); if (attrs) { for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value)); } return node; } const ICONS: Record = { file: '', clock: '', files: '', notebook: '', x: '', "file-text": '', table: '', type: '', atom: '', package: '', "alert-triangle": '', loader: '', }; export function icon(name: string, size = 16): string { const path = ICONS[name] || ICONS.file || ""; return ``; } export function iconEl(name: string, size = 16, extraClass?: string): HTMLElement { const span = el("span", extraClass ? "ic " + extraClass : "ic"); span.innerHTML = icon(name, size); return span; } type HostWindow = Record & { t?: (key: string, ...args: unknown[]) => string; }; export function hostWindow(): HostWindow { return globalThis as unknown as HostWindow; } /** Call a later-lane window export. Stubs are functions; `isReady` is the only gate. */ export function callWindow(name: string, ...args: unknown[]): unknown { const fn = hostWindow()[name]; if (!isReady(fn)) return undefined; return (fn as (...a: unknown[]) => unknown)(...args); } export function translate(key: string, ...args: unknown[]): string { const fromWindow = hostWindow().t; if (isReady(fromWindow)) return fromWindow(key, ...args); return t(key, ...args); } export function fetchArtifactText(url: string): Promise { return artifactsFetch(url).then((response) => { if (!response.ok) throw new ApiError(null, response.status); return response.text(); }); } export function isApiStatus(e: unknown, status: number, code?: string): boolean { if (!(e instanceof ApiError)) return false; if (e.status !== status) return false; if (code && e.code !== code) return false; return true; }