Files
pikasTech-HWLAB/internal/cloud/workbench-runtime-client.ts
T

96 lines
4.3 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;
const traceparent = normalizeTraceparent(options.traceparent ?? options.traceParent);
return {
serviceId: "hwlab-workbench-runtime",
queryWorkbenchFacts: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path: "/v1/workbench-runtime/facts", body: params }),
queryAgentTraceEvents: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path: "/v1/workbench-runtime/trace-events", body: params }),
listWorkbenchSessions: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path: "/v1/workbench-runtime/sessions", body: params }),
readAtomicWorkbenchProjectionSync: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path: "/v1/workbench-runtime/sync", body: params }),
readWorkbenchProjectionOutbox: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path: "/v1/workbench-runtime/projection-outbox", body: params }).then((result) => Array.isArray(result?.rows) ? result.rows : [])
};
}
async function postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, 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 {
const headers = { "content-type": "application/json", accept: "application/json" };
if (traceparent) headers.traceparent = traceparent;
response = await fetchFn(new URL(path, baseUrl), {
method: "POST",
headers,
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 normalizeTraceparent(value) {
const text = String(value ?? "").trim().toLowerCase();
return /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/u.test(text) ? text : "";
}
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)));
}