/** * API Client for Chrono Manager * Provides a centralized way to make API calls with envelope wrapping/unwrapping */ import { createFetchWrapper } from '@KTXC'; import type { ApiRequest, ApiResponse, ApiStreamResponse } from '../types/common'; const fetchWrapper = createFetchWrapper(); const API_URL = '/m/chrono_manager/v1'; const API_VERSION = 1; /** * Generate a unique transaction ID */ export function generateTransactionId(): string { return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } /** * Make an API call with automatic envelope wrapping and unwrapping * * @param operation - Operation name (e.g., 'provider.list', 'service.autodiscover') * @param data - Operation-specific request data * @param user - Optional user identifier override * @returns Promise with unwrapped response data * @throws Error if the API returns an error status */ export async function transceivePost( operation: string, data: TRequest, user?: string ): Promise { const request: ApiRequest = { version: API_VERSION, transaction: generateTransactionId(), operation, data, user }; const response: ApiResponse = await fetchWrapper.post(API_URL, request); if (response.status === 'error') { const errorMessage = `[${operation}] ${response.data.message}${response.data.code ? ` (code: ${response.data.code})` : ''}`; throw new Error(errorMessage); } return response.data; } /** * Stream an NDJSON API response, unwrapping data frames for the caller. * * @param operation - Operation name, e.g. 'entity.listStream' * @param data - Operation-specific request data * @param onData - Synchronous callback invoked for every unwrapped data payload. * @param options - Optional `user` override and an `onStart` hook. * @returns Promise resolving to the final stream total from the control/end frame */ export async function transceiveStream( operation: string, data: TRequest, onData: (data: TData) => void, options?: { user?: string; onStart?: (expected?: number) => void } ): Promise<{ total: number }> { const request: ApiRequest = { version: API_VERSION, transaction: generateTransactionId(), operation, data, user: options?.user, }; let total = 0; const dispatch = (line: string): void => { const message = JSON.parse(line) as ApiStreamResponse; if (message.type === 'control') { if (message.status === 'start') { options?.onStart?.(message.total); } else if (message.status === 'end') { total = message.total; } return; } if (message.type === 'error') { throw new Error(`[${operation}] ${message.message}`); } onData(message.data); }; await fetchWrapper.post(API_URL, request, { headers: { 'Accept': 'application/json' }, onStream: async (response: Response) => { if (!response.body) { throw new Error(`[${operation}] Response body is not readable`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop()!; for (const line of lines) { if (line.trim()) dispatch(line); } } if (buffer.trim()) dispatch(buffer); } finally { reader.releaseLock(); } }, }); return { total }; }