/** * 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, ApiStreamResponse } 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, unwrapping data frames for the caller. * * The server emits one JSON object per line with a transport-level `type` * discriminant. This helper consumes control and error frames, forwards only * unwrapped `data` payloads to the caller, and returns the final stream total. * * @param operation - Operation name, e.g. 'entity.stream' * @param data - Operation-specific request data * @param onData - Synchronous callback invoked for every unwrapped data payload. * May throw to abort the stream. * @param user - Optional user identifier override * @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, user?: string ): Promise<{ total: number }> { const request: ApiRequest = { version: API_VERSION, transaction: generateTransactionId(), operation, data, user, }; let total = 0; await fetchWrapper.post(API_URL, request, { //headers: { 'Accept': 'application/x-ndjson' }, 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()!; // retain any incomplete trailing chunk for (const line of lines) { if (!line.trim()) continue; const message = JSON.parse(line) as ApiStreamResponse; if (message.type === 'control') { if (message.status === 'end') { total = message.total; } continue; } if (message.type === 'error') { throw new Error(`[${operation}] ${message.message}`); } onData(message.data); } } // flush any remaining bytes still in the buffer if (buffer.trim()) { const message = JSON.parse(buffer) as ApiStreamResponse; if (message.type === 'control') { if (message.status === 'end') { total = message.total; } } else if (message.type === 'error') { throw new Error(`[${operation}] ${message.message}`); } else { onData(message.data); } } } finally { reader.releaseLock(); } }, }); return { total }; }