import { ProxyServerUnavailableError } from '#core/errors/proxy_server_unavailable_error' import { ApiHttpClient } from '#core/http_clients/api_http_client' import { FetchHttpClient } from '#core/http_clients/fetch_http_client' import { isServerErrorResponse } from '#core/modules/error/error_utility' import { ApplicationRuntimeConfigurationService } from '#core/modules/runtime/configurations/application_runtime_configuration' import { getApplicationRuntimeConfiguration } from '#core/modules/runtime/utils/runtime_configuration_utility' import { HttpClient, HttpClientRequest } from '@effect/platform' import { Effect, Either, Schedule } from 'effect' import { isEqual, joinURL, parsePath, withLeadingSlash } from 'ufo' import { EventEmitterIdentifier } from '~/constants/event_emitter_identifier_enum' import { emitter } from '~/core/emittery' export class UpsilonHttpClient extends Effect.Service()('client:http:upsilon', { dependencies: [ApplicationRuntimeConfigurationService.Default, FetchHttpClient.Default, ApiHttpClient.Default], effect: Effect.gen(function * () { const { BaseHttpClient } = yield * FetchHttpClient const api = yield * ApiHttpClient const applicationConfig = yield * getApplicationRuntimeConfiguration(config => ({ application_url: config.application_url, upsilon_base_path: config.upsilon.base_path, })) const upsilonClient = BaseHttpClient.pipe( HttpClient.mapRequest(HttpClientRequest.updateUrl(withLeadingSlash)), HttpClient.mapRequest(HttpClientRequest.prependUrl(joinURL(applicationConfig.application_url, applicationConfig.upsilon_base_path))), HttpClient.followRedirects(3), ) function json(request: HttpClientRequest.HttpClientRequest) { return Effect.gen(function * () { const result = yield * api.json(request, upsilonClient).pipe(Effect.either) if (Either.isLeft(result)) { if (isServerErrorResponse(result.left) && [502, 503, 504].includes(result.left.status)) { return yield * new ProxyServerUnavailableError({ name: 'upsilon' }, 'Upsilon proxy server is unavailable at the moment. Please try again later.') } if (!isEqual(parsePath(request.url).pathname, '/health') && result.left._tag !== 'error:app:client_network_offline') { emitter.emit(EventEmitterIdentifier.UPSILON_AVAILABILITY_CHANGED, 'available') } return yield * Effect.fail(result.left) } if (!isEqual(parsePath(request.url).pathname, '/health')) { emitter.emit(EventEmitterIdentifier.UPSILON_AVAILABILITY_CHANGED, 'available') } return yield * Effect.succeed(result.right) }).pipe( Effect.tapErrorTag('error:app:proxy_server_unavailable', error => Effect.sync(() => { if (error.name === 'upsilon') { emitter.emit(EventEmitterIdentifier.UPSILON_AVAILABILITY_CHANGED, 'unavailable') } })), Effect.tapErrorTag('error:server_error_response:unauthorized', () => Effect.sync(() => { emitter.emit(EventEmitterIdentifier.AUTHENTICATION_STATUS_CHANGED, { authenticated: false, user: null }) })), Effect.retry({ while: error => error._tag === 'error:app:proxy_server_unavailable' && error.name === 'upsilon' && import.meta.client, schedule: Schedule.exponential('800 millis'), times: 5, }), ) } return { /** * Perform a request with the JSON accept header and return the JSON response. * If the response is an error, it will be decoded and returned as an error. * * - It will also handle the client network offline error and retry the request if the client is offline. * - It will also handle the proxy server unavailable error and retry the request if the proxy server is unavailable. * * @param request The request to perform. */ json, } }), }) {}