Files
Project_Velocity/app/src/oracle/lib/oracleApiClient.ts

223 lines
5.9 KiB
TypeScript

/**
* Oracle API Client — production-only client for the Oracle v1 backend.
*/
import type {
CanvasPage,
PromptSubmitRequest,
PromptSubmitResponse,
ForkCreateRequest,
ForkCreateResponse,
MergeRequestCreateRequest,
MergeRequest,
MergeReviewRequest,
ComponentTemplate,
UserProfile,
OracleWSMessage,
OracleEnvelope,
CanvasPageRevision,
} from '../types/canvas';
import { VELOCITY_TOKEN_KEY } from '@/lib/velocityPlatformClient';
function getBrowserOrigin(): string {
return typeof window !== 'undefined' ? window.location.origin : '';
}
function resolveBaseUrl(): string {
const configured = (import.meta.env.VITE_ORACLE_API_URL as string | undefined)?.trim();
if (configured) {
return configured.replace(/\/$/, '');
}
return getBrowserOrigin();
}
function resolveWsUrl(): string {
const configured = (import.meta.env.VITE_ORACLE_WS_URL as string | undefined)?.trim();
if (configured) {
return configured.replace(/\/$/, '');
}
const origin = getBrowserOrigin();
return origin ? origin.replace(/^http/, 'ws') : '';
}
const BASE_URL = resolveBaseUrl();
const WS_URL = resolveWsUrl();
function apiUrl(path: string): string {
return `${BASE_URL}/api/oracle/v1${path}`;
}
async function apiFetch<T>(
path: string,
options?: RequestInit & { idempotencyKey?: string },
): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Oracle-Contract-Version': 'v1',
...(options?.idempotencyKey ? { 'Idempotency-Key': options.idempotencyKey } : {}),
};
const token = localStorage.getItem('oracle_jwt') ?? localStorage.getItem(VELOCITY_TOKEN_KEY);
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(apiUrl(path), { ...options, headers });
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw Object.assign(new Error(body?.error?.message ?? body?.detail?.errors?.join?.(', ') ?? `HTTP ${res.status}`), { apiError: body });
}
const body = await res.json() as OracleEnvelope<T> | T;
if (typeof body === 'object' && body !== null && 'data' in body) {
return (body as OracleEnvelope<T>).data;
}
return body as T;
}
export async function fetchMe(): Promise<UserProfile> {
return apiFetch<UserProfile>('/me');
}
export async function fetchCanvasPage(pageId: string): Promise<CanvasPage> {
return apiFetch<CanvasPage>(`/canvas-pages/${pageId}`);
}
export async function submitPrompt(
pageId: string,
payload: PromptSubmitRequest,
): Promise<PromptSubmitResponse> {
return apiFetch<PromptSubmitResponse>(`/canvas-pages/${pageId}/prompts`, {
method: 'POST',
body: JSON.stringify(payload),
idempotencyKey: payload.clientRequestId,
});
}
export async function rollbackPage(
pageId: string,
targetRevision: number,
clientRequestId: string,
): Promise<{ headRevision: number; pageId: string; components: CanvasPage['components'] }> {
return apiFetch<{ headRevision: number; pageId: string; components: CanvasPage['components'] }>(
`/canvas-pages/${pageId}/rollback`,
{
method: 'POST',
body: JSON.stringify({ targetRevision, clientRequestId }),
idempotencyKey: clientRequestId,
},
);
}
export async function listRevisions(pageId: string): Promise<CanvasPageRevision[]> {
return apiFetch<CanvasPageRevision[]>(`/canvas-pages/${pageId}/revisions`);
}
export async function createFork(
pageId: string,
payload: ForkCreateRequest,
): Promise<ForkCreateResponse> {
return apiFetch<ForkCreateResponse>(`/canvas-pages/${pageId}/forks`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function openMergeRequest(
payload: MergeRequestCreateRequest,
): Promise<MergeRequest> {
return apiFetch<MergeRequest>('/merge-requests', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function reviewMergeRequest(
mrId: string,
payload: MergeReviewRequest,
): Promise<MergeRequest> {
return apiFetch<MergeRequest>(`/merge-requests/${mrId}/review`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function listMergeRequests(pageId: string): Promise<MergeRequest[]> {
return apiFetch<MergeRequest[]>(`/merge-requests?targetPageId=${pageId}`);
}
export async function listComponentTemplates(filters?: {
category?: string;
status?: string;
}): Promise<ComponentTemplate[]> {
const qs = new URLSearchParams(filters as Record<string, string>).toString();
return apiFetch<ComponentTemplate[]>(`/component-templates${qs ? `?${qs}` : ''}`);
}
export async function synthesizeTemplate(params: {
prompt: string;
dataShape: string[];
styleSignatureRef?: string;
}): Promise<ComponentTemplate> {
return apiFetch<ComponentTemplate>('/component-templates/synthesize', {
method: 'POST',
body: JSON.stringify(params),
});
}
export function connectPageSocket(
pageId: string,
handlers: {
onMessage: (msg: OracleWSMessage) => void;
onReconnect?: () => void;
onOpen?: () => void;
onClose: () => void;
},
): () => void {
if (!WS_URL && !BASE_URL) {
handlers.onClose();
return () => undefined;
}
const wsBase = WS_URL || BASE_URL.replace(/^http/, 'ws');
let ws: WebSocket;
let stopped = false;
let retryTimeout: ReturnType<typeof setTimeout> | undefined;
function connect() {
ws = new WebSocket(`${wsBase}/ws/oracle/canvas/${pageId}`);
ws.onopen = () => {
handlers.onOpen?.();
};
ws.onmessage = (event) => {
try {
handlers.onMessage(JSON.parse(event.data as string) as OracleWSMessage);
} catch {
// Ignore malformed messages from the transport.
}
};
ws.onclose = () => {
handlers.onClose();
if (!stopped) {
retryTimeout = setTimeout(() => {
handlers.onReconnect?.();
connect();
}, 3000);
}
};
ws.onerror = () => {
ws.close();
};
}
connect();
return () => {
stopped = true;
if (retryTimeout) clearTimeout(retryTimeout);
ws?.close();
};
}