/** * Small JSON API client for the admin and gallery pages. * * Every method resolves with the parsed body of a successful response and * throws ApiError(status, message) for a non-2xx status, a `success: false` * body or a body that is not JSON, so callers have one failure path. * * latestOnly() guards list requests against out-of-order responses: each new * call aborts the previous in-flight request and a superseded call resolves * to STALE instead of its (outdated) result. * * Loadable in the browser (window.ApiClient) and in Node for tests. */ (function (root) { "use strict"; class ApiError extends Error { constructor(status, message, code = null, recovery = null) { super(message); this.name = "ApiError"; this.status = status; this.code = typeof code === "string" && /^[a-z0-9-]{1,64}$/.test(code) ? code : null; this.recovery = null; if (recovery && typeof recovery === "object" && !Array.isArray(recovery)) { const keys = Object.keys(recovery); if (keys.length === 1 && keys[0] === "action" && ["refresh-history", "reload-outputs", "retry-read"].includes(recovery.action)) { this.recovery = { action: recovery.action }; } else if (keys.length === 2 && keys.includes("action") && keys.includes("limit") && recovery.action === "smaller-limit" && Number.isInteger(recovery.limit) && recovery.limit >= 1 && recovery.limit <= 100) { this.recovery = { action: "smaller-limit", limit: recovery.limit }; } } } } /** Result of a latestOnly() call that was superseded by a newer one. */ const STALE = Symbol("stale response"); /** Keep Prompt Manager requests under the prefix that serves its page. */ function resolveUrl(url, pathname = root.location && root.location.pathname) { if (typeof url !== "string" || !url.startsWith("/prompt_manager/")) return url; const match = (pathname || "").match(/^(.*)\/prompt_manager(?:\/|$)/); return match && match[1] ? match[1] + url : url; } /** Like fetch(), but keep Prompt Manager URLs under the page's proxy mount. */ function fetchUrl(url, init) { return globalThis.fetch(resolveUrl(url), init); } /** * @param {typeof fetch} [fetchImpl] - defaults to the global fetch (bound to globalThis) */ function createApiClient(fetchImpl) { const doFetch = fetchImpl || ((...args) => globalThis.fetch(...args)); async function request(method, url, { body, signal, cache } = {}) { const init = { method, headers: {} }; if (cache !== undefined) init.cache = cache; if (body !== undefined) { init.headers["Content-Type"] = "application/json"; init.body = JSON.stringify(body); } if (signal) init.signal = signal; const response = await doFetch(resolveUrl(url), init); let data = null; let parsed = true; try { data = await response.json(); } catch (_) { parsed = false; } const serverMessage = data && typeof data === "object" ? data.error || data.message : undefined; if (!response.ok) { throw new ApiError(response.status, serverMessage || response.statusText || `HTTP ${response.status}`, data?.code, data?.recovery); } if (!parsed) throw new ApiError(response.status, "Invalid JSON response"); if (data && typeof data === "object" && data.success === false) { throw new ApiError(response.status, serverMessage || "Request failed", data.code, data.recovery); } return data; } return { // cache is a fetch RequestCache mode such as "no-store"; omitted keeps the default. get: (url, { signal, cache } = {}) => request("GET", url, { signal, cache }), post: (url, body, { signal } = {}) => request("POST", url, { body, signal }), put: (url, body, { signal } = {}) => request("PUT", url, { body, signal }), patch: (url, body, { signal } = {}) => request("PATCH", url, { body, signal }), // A DELETE body is optional; the favorites store names its file in one. del: (url, { signal, body } = {}) => request("DELETE", url, { body, signal }), }; } /** * @returns {(run: (signal: AbortSignal) => Promise<*>) => Promise<*>} wrapper that * aborts the previous call's signal and resolves superseded calls to STALE */ function latestOnly() { let current = null; return async function (run) { if (current) current.abort(); const controller = new AbortController(); current = controller; try { const result = await run(controller.signal); return controller === current ? result : STALE; } catch (error) { if (controller !== current) return STALE; throw error; } }; } const api = { createApiClient, latestOnly, resolveUrl, fetchUrl, ApiError, STALE }; if (typeof module !== "undefined" && module.exports) module.exports = api; else root.ApiClient = api; })(typeof window !== "undefined" ? window : globalThis);