Files
ComfyAssets-ComfyUI_PromptM…/web/js/api-client.js
T
Vito ecd21165a7 feat(history): add chronological runs and exact output inspection (#227)
* feat(history): add bounded chronological read projections (#209)

* fix(history): validate internal continuation positions (#209)

* feat(history): expose fenced history windows and run details (#209)

* fix(history): release retired projection connections (#209)

* feat(history): resolve reported outputs through current authorization (#209)

* fix(history): reject invalid inspection targets (#209)

* feat(history): add scoped history state and current-file adapter (#209)

* fix(history): preserve slideshow and clear stale reentry actions (#209)

* feat(history): render recorded executions and frozen run details (#209)

* fix(history): recover receipts and refresh capture evidence (#209)

* test(history): verify chronological history across restore and host modes (#209)

* fix(history): enforce chronological marker visual parity (#209)

* fix(history): resolve final chronological review findings

Address #209 recovery, bounded visit restoration, exact receipt availability, and portable regression coverage.

* test(history): repair Windows fixtures and review feedback

Address #209 and PR #227 native path, cleanup, media and review-comment expectations. Document Windows portability preflight without changing production behavior.

* ci: raise the viewerjs job budget for the larger browser suite

The cross-browser workflows suite grew from 396 to 496 tests on this
branch, which pushed the whole viewerjs job to about 25.5 minutes
against the 25 minute cap set in #181. Two exact-head runs on this
branch timed out without any test failing: run 39173019283 was killed
on test 474 of 496, and run 39175125474 printed "496 passed (12.8m)"
and was then cancelled during Playwright shutdown.

Raise only the job budget. Browser projects, workers and per-test
timeouts are unchanged. Splitting the browsers into parallel jobs is
tracked separately.
2026-10-18 17:17:05 -07:00

119 lines
5.1 KiB
JavaScript

/**
* 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 } = {}) {
const init = { method, headers: {} };
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 {
get: (url, { signal } = {}) => request("GET", url, { signal }),
post: (url, body, { signal } = {}) => request("POST", url, { body, signal }),
put: (url, body, { signal } = {}) => request("PUT", url, { body, signal }),
del: (url, { signal } = {}) => request("DELETE", url, { 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);