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<HttpClient.HttpClient> => 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<never, RemoteEnvironmentRequestError> => { 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 = <A, E, R>( requestUrl: string, timeoutMs: number, request: Effect.Effect<A, E, R>,): Effect.Effect<A, RemoteEnvironmentRequestError, R> => 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)), );
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<HttpClient.HttpClient> =>
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<never, RemoteEnvironmentRequestError> => {
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 = <A, E, R>(
requestUrl: string,
timeoutMs: number,
request: Effect.Effect<A, E, R>,
): Effect.Effect<A, RemoteEnvironmentRequestError, R> =>
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)),
);