Skip to content
Source
Sign inSign up

http.ts

170 lines5.3 KB

1import {
2 EnvironmentHttpApi,
3 EnvironmentHttpCommonError,
4 type EnvironmentAuthInvalidError,
5 type EnvironmentInternalError,
6 type EnvironmentOperationForbiddenError,
7 type EnvironmentRequestInvalidError,
8 type EnvironmentResourceNotFoundError,
9 type EnvironmentScopeRequiredError,
10} from "@t3tools/contracts";
11import * as HttpObservability from "@t3tools/shared/httpObservability";
12import * as Duration from "effect/Duration";
13import * as Effect from "effect/Effect";
14import * as Layer from "effect/Layer";
15import * as Option from "effect/Option";
16import * as Schema from "effect/Schema";
17import { FetchHttpClient, HttpClient, HttpClientError } from "effect/http";
18import * as HttpApiClient from "effect/http-api/HttpApiClient";
19​
20const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError);
21​
22export class RemoteEnvironmentAuthFetchError extends Schema.TaggedError<RemoteEnvironmentAuthFetchError>()(
23 "RemoteEnvironmentAuthFetchError",
24 {
25 message: Schema.String,
26 cause: Schema.Defect(),
27 },
28) {}
29​
30class RemoteEnvironmentAuthInvalidJsonError extends Schema.TaggedError<RemoteEnvironmentAuthInvalidJsonError>()(
31 "RemoteEnvironmentAuthInvalidJsonError",
32 {
33 message: Schema.String,
34 cause: Schema.Defect(),
35 },
36) {}
37​
38export class RemoteEnvironmentAuthUndeclaredStatusError extends Schema.TaggedError<RemoteEnvironmentAuthUndeclaredStatusError>()(
39 "RemoteEnvironmentAuthUndeclaredStatusError",
40 {
41 status: Schema.Number,
42 requestUrl: Schema.String,
43 },
44) {
45 override get message(): string {
46 return `Remote environment endpoint ${this.requestUrl} returned undeclared status ${this.status}.`;
47 }
49​
50export class RemoteEnvironmentAuthTimeoutError extends Schema.TaggedError<RemoteEnvironmentAuthTimeoutError>()(
51 "RemoteEnvironmentAuthTimeoutError",
52 {
53 requestUrl: Schema.String,
54 timeoutMs: Schema.Number,
55 },
56) {
57 override get message(): string {
58 return `Remote environment endpoint ${this.requestUrl} timed out after ${this.timeoutMs}ms.`;
59 }
61​
62const isRemoteEnvironmentAuthTimeoutError = Schema.is(RemoteEnvironmentAuthTimeoutError);
63​
64export type RemoteEnvironmentRequestError =
65 | EnvironmentRequestInvalidError
66 | EnvironmentAuthInvalidError
67 | EnvironmentScopeRequiredError
68 | EnvironmentOperationForbiddenError
69 | EnvironmentResourceNotFoundError
70 | EnvironmentInternalError
71 | RemoteEnvironmentAuthFetchError
72 | RemoteEnvironmentAuthInvalidJsonError
73 | RemoteEnvironmentAuthUndeclaredStatusError
74 | RemoteEnvironmentAuthTimeoutError;
75​
76export const layerRemoteHttpClient = (
77 fetchFn: typeof globalThis.fetch,
78): Layer.Layer<HttpClient.HttpClient> =>
79 Layer.merge(
80 FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn))),
81 HttpObservability.layer,
82 );
83​
84const remoteApiBaseUrl = (httpBaseUrl: string): string => {
85 const url = new URL(httpBaseUrl);
86 url.pathname = "/";
87 url.search = "";
88 url.hash = "";
89 return url.toString();
90};
91​
92export const makeEnvironmentHttpApiClient = (httpBaseUrl: string) =>
93 HttpApiClient.make(EnvironmentHttpApi, {
94 baseUrl: remoteApiBaseUrl(httpBaseUrl),
95 });
96​
97export const makeEnvironmentHttpApiGroupClient = <
98 Group extends keyof typeof EnvironmentHttpApi.groups,
99>(
100 httpBaseUrl: string,
101 group: Group,
102) =>
103 Effect.flatMap(HttpClient.HttpClient, (httpClient) =>
104 HttpApiClient.group(EnvironmentHttpApi, {
105 httpClient,
106 group,
107 baseUrl: remoteApiBaseUrl(httpBaseUrl),
108 }),
109 );
110​
111export const makeEnvironmentHttpApiUrlBuilder = (httpBaseUrl: string) =>
112 HttpApiClient.urlBuilder(EnvironmentHttpApi, {
113 baseUrl: remoteApiBaseUrl(httpBaseUrl),
114 });
115​
116const failRemoteRequest = (
117 requestUrl: string,
118 cause: unknown,
119): Effect.Effect<never, RemoteEnvironmentRequestError> => {
120 if (isRemoteEnvironmentAuthTimeoutError(cause)) {
121 return Effect.fail(cause);
122 }
123 if (isEnvironmentHttpCommonError(cause)) {
124 return Effect.fail(cause);
125 }
126 if (Schema.isSchemaError(cause)) {
127 return Effect.fail(
128 new RemoteEnvironmentAuthInvalidJsonError({
129 message: `Remote environment endpoint returned an invalid response from ${requestUrl}.`,
130 cause,
131 }),
132 );
133 }
134 if (HttpClientError.isHttpClientError(cause) && cause.response !== undefined) {
135 const response = cause.response;
136 if (response.status < 200 || response.status >= 300) {
137 return Effect.fail(
138 new RemoteEnvironmentAuthUndeclaredStatusError({ requestUrl, status: response.status }),
139 );
140 }
141 return Effect.fail(
142 new RemoteEnvironmentAuthInvalidJsonError({
143 message: `Remote environment endpoint returned an invalid response from ${requestUrl}.`,
144 cause,
145 }),
146 );
147 }
148 return Effect.fail(
149 new RemoteEnvironmentAuthFetchError({
150 message: `Failed to fetch remote environment endpoint ${requestUrl} (${String(cause)}).`,
151 cause,
152 }),
153 );
154};
155​
156export const executeEnvironmentHttpRequest = <A, E, R>(
157 requestUrl: string,
158 timeoutMs: number,
159 request: Effect.Effect<A, E, R>,
160): Effect.Effect<A, RemoteEnvironmentRequestError, R> =>
161 request.pipe(
162 Effect.timeoutOption(Duration.millis(timeoutMs)),
163 Effect.flatMap(
164 Option.match({
165 onNone: () => Effect.fail(new RemoteEnvironmentAuthTimeoutError({ requestUrl, timeoutMs })),
166 onSome: Effect.succeed,
167 }),
168 ),
169 Effect.catch((cause) => failRemoteRequest(requestUrl, cause)),
170 );