import { EnvironmentHttpApi, EnvironmentHttpCommonError, type EnvironmentAuthInvalidError, type EnvironmentInternalError, type EnvironmentOperationForbiddenError, type EnvironmentRequestInvalidError, type EnvironmentResourceNotFoundError, type EnvironmentScopeRequiredError, } from "@t3tools/contracts"; import { httpHeaderRedactionLayer } from "@t3tools/shared/httpObservability"; import * as Data from "effect/Data"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { FetchHttpClient, HttpClient, HttpClientError } from "effect/unstable/http"; import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); export class RemoteEnvironmentAuthFetchError extends Data.TaggedError( "RemoteEnvironmentAuthFetchError", )<{ readonly message: string; readonly cause: unknown; }> {} export class RemoteEnvironmentAuthInvalidJsonError extends Data.TaggedError( "RemoteEnvironmentAuthInvalidJsonError", )<{ readonly message: string; readonly cause: unknown; }> {} export class RemoteEnvironmentAuthUndeclaredStatusError extends Data.TaggedError( "RemoteEnvironmentAuthUndeclaredStatusError", )<{ readonly message: string; readonly status: number; readonly requestUrl: string; }> { constructor(requestUrl: string, status: number) { super({ message: `Remote environment endpoint ${requestUrl} returned undeclared status ${status}.`, requestUrl, status, }); } } export class RemoteEnvironmentAuthTimeoutError extends Data.TaggedError( "RemoteEnvironmentAuthTimeoutError", )<{ readonly message: string; readonly requestUrl: string; readonly timeoutMs: number; }> { constructor(requestUrl: string, timeoutMs: number) { super({ message: `Remote environment endpoint ${requestUrl} timed out after ${timeoutMs}ms.`, requestUrl, timeoutMs, }); } } export type RemoteEnvironmentRequestError = | EnvironmentRequestInvalidError | EnvironmentAuthInvalidError | EnvironmentScopeRequiredError | EnvironmentOperationForbiddenError | EnvironmentResourceNotFoundError | EnvironmentInternalError | RemoteEnvironmentAuthFetchError | RemoteEnvironmentAuthInvalidJsonError | RemoteEnvironmentAuthUndeclaredStatusError | RemoteEnvironmentAuthTimeoutError; export const remoteHttpClientLayer = ( fetchFn: typeof globalThis.fetch, ): Layer.Layer => Layer.merge( FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn))), httpHeaderRedactionLayer, ); const remoteApiBaseUrl = (httpBaseUrl: string): string => { const url = new URL(httpBaseUrl); url.pathname = "/"; url.search = ""; url.hash = ""; return url.toString(); }; export const makeEnvironmentHttpApiClient = (httpBaseUrl: string) => HttpApiClient.make(EnvironmentHttpApi, { baseUrl: remoteApiBaseUrl(httpBaseUrl), }); const failRemoteRequest = ( requestUrl: string, cause: unknown, ): Effect.Effect => { if (cause instanceof RemoteEnvironmentAuthTimeoutError) { return Effect.fail(cause); } if (isEnvironmentHttpCommonError(cause)) { return Effect.fail(cause); } if (Schema.isSchemaError(cause)) { return Effect.fail( new RemoteEnvironmentAuthInvalidJsonError({ message: `Remote environment endpoint returned an invalid response from ${requestUrl}.`, cause, }), ); } if (HttpClientError.isHttpClientError(cause) && cause.response !== undefined) { const response = cause.response; if (response.status < 200 || response.status >= 300) { return Effect.fail( new RemoteEnvironmentAuthUndeclaredStatusError(requestUrl, response.status), ); } return Effect.fail( new RemoteEnvironmentAuthInvalidJsonError({ message: `Remote environment endpoint returned an invalid response from ${requestUrl}.`, cause, }), ); } return Effect.fail( new RemoteEnvironmentAuthFetchError({ message: `Failed to fetch remote environment endpoint ${requestUrl} (${String(cause)}).`, cause, }), ); }; export const executeEnvironmentHttpRequest = ( requestUrl: string, timeoutMs: number, request: Effect.Effect, ): Effect.Effect => request.pipe( Effect.timeoutOption(Duration.millis(timeoutMs)), Effect.flatMap( Option.match({ onNone: () => Effect.fail(new RemoteEnvironmentAuthTimeoutError(requestUrl, timeoutMs)), onSome: Effect.succeed, }), ), Effect.catch((cause) => failRemoteRequest(requestUrl, cause)), );