/** * API Client for Mail Manager * Provides a centralized way to make API calls with envelope wrapping/unwrapping */ import { createFetchWrapper } from '@KTXC'; import type { ApiRequest, ApiResponse } from '../types/common'; const fetchWrapper = createFetchWrapper(); const API_URL = '/m/mail_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, calling onLine for each parsed line. * * The server emits one JSON object per line with a `type` discriminant * (meta / entity / done / error). The caller interprets each line via onLine. * Throwing inside onLine aborts the stream. * * @param operation - Operation name, e.g. 'entity.stream' * @param data - Operation-specific request data * @param onLine - Synchronous callback invoked for every parsed line. * May throw to abort the stream. * @param user - Optional user identifier override */ export async function transceiveStream( operation: string, data: TRequest, onLine: (line: TLine) => void, user?: string ): Promise { const request: ApiRequest = { version: API_VERSION, transaction: generateTransactionId(), operation, data, user, }; await fetchWrapper.post(API_URL, request, { //headers: { 'Accept': 'application/x-ndjson' }, headers: { 'Accept': 'application/json' }, onStream: async (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()!; // retain any incomplete trailing chunk for (const line of lines) { if (!line.trim()) continue; onLine(JSON.parse(line) as TLine); } } // flush any remaining bytes still in the buffer if (buffer.trim()) { onLine(JSON.parse(buffer) as TLine); } } finally { reader.releaseLock(); } }, }); }