86 lines
3.6 KiB
TypeScript
86 lines
3.6 KiB
TypeScript
/*
|
|
* SPEC: PJ2026-0104010803 Workbench唯一投影; HWLAB#1832 Workbench runtime Go 微服务拆分.
|
|
* 职责: Cloud API 到 hwlab-workbench-runtime 的单一路径只读客户端;配置后不回退本地 PG。
|
|
*/
|
|
|
|
const DEFAULT_WORKBENCH_RUNTIME_TIMEOUT_MS = 10000;
|
|
|
|
export function createWorkbenchRuntimeClient(options = {}) {
|
|
const env = options.env ?? process.env;
|
|
const baseUrl = normalizeBaseUrl(env.HWLAB_WORKBENCH_RUNTIME_URL);
|
|
if (!baseUrl) return null;
|
|
const timeoutMs = boundedInteger(env.HWLAB_WORKBENCH_RUNTIME_TIMEOUT_MS, DEFAULT_WORKBENCH_RUNTIME_TIMEOUT_MS, 1000, 60000);
|
|
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
return {
|
|
serviceId: "hwlab-workbench-runtime",
|
|
queryWorkbenchFacts: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, path: "/v1/workbench-runtime/facts", body: params }),
|
|
queryAgentTraceEvents: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, path: "/v1/workbench-runtime/trace-events", body: params }),
|
|
readWorkbenchProjectionOutbox: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, path: "/v1/workbench-runtime/projection-outbox", body: params }).then((result) => Array.isArray(result?.rows) ? result.rows : [])
|
|
};
|
|
}
|
|
|
|
async function postRuntime({ fetchFn, baseUrl, timeoutMs, path, body }) {
|
|
if (typeof fetchFn !== "function") {
|
|
throw runtimeDependencyError({ code: "workbench_runtime_fetch_unavailable", message: "fetch is unavailable for workbench runtime dependency", retryable: false });
|
|
}
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
let response;
|
|
try {
|
|
response = await fetchFn(new URL(path, baseUrl), {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
body: JSON.stringify(body ?? {}),
|
|
signal: controller.signal
|
|
});
|
|
} catch (error) {
|
|
throw runtimeDependencyError({ code: error?.name === "AbortError" ? "workbench_runtime_timeout" : "workbench_runtime_unreachable", message: error?.message ?? "workbench runtime request failed", retryable: true, cause: error });
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
const payload = await responseJson(response);
|
|
if (!response.ok) {
|
|
const error = payload?.error ?? {};
|
|
throw runtimeDependencyError({
|
|
code: error.code ?? `workbench_runtime_http_${response.status}`,
|
|
message: error.message ?? `workbench runtime request failed with HTTP ${response.status}`,
|
|
retryable: response.status >= 500 || error.retryable === true,
|
|
status: response.status,
|
|
payload
|
|
});
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
async function responseJson(response) {
|
|
try { return await response.json(); } catch { return null; }
|
|
}
|
|
|
|
function runtimeDependencyError({ code, message, retryable, status = null, payload = null, cause = null }) {
|
|
const error = new Error(message || code || "workbench runtime dependency failed");
|
|
error.name = "WorkbenchRuntimeDependencyError";
|
|
error.code = code ?? "workbench_runtime_dependency_failed";
|
|
if (cause) error.cause = cause;
|
|
error.data = {
|
|
serviceId: "hwlab-workbench-runtime",
|
|
status,
|
|
retryable: retryable === true,
|
|
transient: retryable === true,
|
|
payload,
|
|
valuesRedacted: true
|
|
};
|
|
return error;
|
|
}
|
|
|
|
function normalizeBaseUrl(value) {
|
|
const text = String(value ?? "").trim();
|
|
if (!text) return null;
|
|
return text.endsWith("/") ? text : `${text}/`;
|
|
}
|
|
|
|
function boundedInteger(value, fallback, min, max) {
|
|
const parsed = Number(value);
|
|
if (!Number.isFinite(parsed)) return fallback;
|
|
return Math.max(min, Math.min(max, Math.trunc(parsed)));
|
|
}
|