import { createLogger } from "@phnq/log"; import { Anomaly, WebSocketMessageClient } from "@phnq/message"; import type AuthApi from "../auth/AuthApi"; import { API_SERVICE_DOMAIN } from "../domains"; import type { ServiceApi } from "../Service"; import type { StandaloneClient } from "../ServiceClient"; import ServiceError from "../ServiceError"; import type { ApiRequestMessage, ApiResponseMessage } from "./ApiMessage"; const log = createLogger("ApiClient"); interface ApiRequestEvent { type: "request"; } interface ApiResponseEvent { type: "response"; req: ApiRequestEvent; } interface ApiErrorEvent { type: "error"; req: ApiRequestEvent; err: ServiceError; } type ApiClientEvent = { domain: string; method: string; payload: unknown; ts: number; } & (ApiResponseEvent | ApiRequestEvent | ApiErrorEvent); const handlers: Record void)[]> = { request: [], response: [], error: [], }; const emit = (event: T): T => { for (const handler of handlers[event.type]) { try { handler(event); } catch (err) { log.error("Error in API client event handler:", err); } } return event; }; const ApiClient = { on( eventType: T, fn: (event: ApiClientEvent & { type: T }) => void, ): (event: ApiClientEvent & { type: T }) => void { handlers[eventType].push(fn as never); return fn; }, off( eventType: T, fn: ( event: ApiClientEvent & { type: T; }, ) => void, ): void { handlers[eventType] = handlers[eventType].filter((h) => h !== fn); }, createAuthClient( url: string, ): AuthApi["handlers"] & Omit { return this.create("phnq-auth", url); }, create< T extends ServiceApi, N extends { type: string } | undefined = undefined, D extends string = T["domain"], >( domain: D, url: string, onNotify?: (msg: N) => void, ): T["handlers"] & Omit { let wsClient: WebSocketMessageClient | undefined; return new Proxy( {}, { get: (_, method: string) => { if (method === "isConnected") { return wsClient?.isOpen(); } else if (["stats", "getStats"].includes(method)) { throw new Error(`Method not available in client: ${method}`); } return async (payload: unknown) => { const req = emit({ type: "request", domain, method, payload, ts: Date.now(), }); try { if (method === "disconnect") { if (wsClient?.isOpen()) { await wsClient.close(); } return; } if (!wsClient) { wsClient = WebSocketMessageClient.create( url, ); wsClient.addReceiveHandler(async ({ domain: notifyDomain, method, payload }) => { if (onNotify && [domain, API_SERVICE_DOMAIN].includes(notifyDomain)) { switch (method) { case "notify": onNotify(payload as N); return undefined; } } return undefined; }); } if (method === "connect") { // connect is handled lazily in WebSocketMessageClient. return; } const response = await wsClient.request({ domain, method, payload }); if ( typeof response === "object" && (response as AsyncIterableIterator)[Symbol.asyncIterator] ) { const responseIter = response as AsyncIterableIterator; return (async function* () { for await (const { payload } of responseIter) { emit({ type: "response", domain, method, payload, req, ts: Date.now(), }); yield payload; } })(); } else { emit({ type: "response", domain, method, payload: (response as ApiResponseMessage).payload, req, ts: Date.now(), }); return (response as ApiResponseMessage).payload; } } catch (err) { const serviceError = err instanceof Anomaly ? err.info ? (ServiceError.fromPayload(err.info) ?? new ServiceError({ type: "anomaly", message: err.message })) : new ServiceError({ type: "anomaly", message: err.message }) : ServiceError.fromError(err); emit({ type: "error", domain, method, payload: serviceError, req, err: serviceError, ts: Date.now(), }); throw serviceError; } }; }, }, ) as T["handlers"] & StandaloneClient; }, }; export default ApiClient;