Files
Project_Velocity/app/src/oracle/lib/oracleApiClient.ts
sayan f78655debc feat: Built the Oracle Tab1 (#14)
#13 Built the complete Oracle Tab with all the functionalities.

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: #14
2026-04-11 19:35:45 +05:30

200 lines
5.4 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';
const BASE_URL = (import.meta.env.VITE_ORACLE_API_URL as string | undefined) ?? '';
const WS_URL = (import.meta.env.VITE_ORACLE_WS_URL as string | undefined) ?? '';
function apiUrl(path: string): string {
return `${BASE_URL}/api/oracle/v1${path}`;
}
async function apiFetch<T>(
path: string,
options?: RequestInit & { idempotencyKey?: string },
): Promise<T> {
if (!BASE_URL) {
throw new Error('Oracle API is not configured. Set VITE_ORACLE_API_URL to a live backend.');
}
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');
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;
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.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();
};
}