Upsilon Http Client

🧩 Syntax:
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<UpsilonHttpClient>()('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,
    }
  }),
}) {}