import type { NuxtApp } from '#app' import type { R } from '#application/core/effect/types/application_runtime' import type UnknownError from '#shared/core/error/errors/unknown_error' import type { Cause } from 'effect' import { GracefulExecutionStatus } from '#application/core/effect/constants/graceful_execition_status' import { GracefulExecutionStrategy } from '#application/core/effect/constants/graceful_execution_strategy' import { GracefulLoadingStrategy } from '#application/core/effect/constants/graceful_loading_strategy' import ApplicationRuntime from '#application/core/effect/runtime/application_runtime' import ApplicationRuntimeService from '#application/core/effect/services/application_runtime_service' import NuxtApplication from '#application/core/nuxt/contexts/nuxt_application_context' import { tryOnScopeDispose } from '@vueuse/core' import defu from 'defu' import { Effect, Exit } from 'effect' export interface ExecuteEffectGracefullyOptions { /** * Nuxt application instance, used to provide the Nuxt context to the effect. * * This is useful for executing Nuxt-specific features or services * within the effect. */ nuxtApp?: NuxtApp; /** * Signal to abort the execution of the effect. * This can be used to cancel the effect if needed. * * It is useful for scenarios where the effect might take a long time to complete * or if the user navigates away from the page before the effect completes. */ signal?: AbortSignal; /** * The loading strategy to use for the effect execution. * * @defaultValue `GracefulLoadingStrategy.ON_DEMAND` */ loadingStrategy?: GracefulLoadingStrategy; /** * The execution strategy to use for the effect execution. * * @defaultValue `GracefulExecutionStrategy.KEEP_STATE` */ executionStrategy?: GracefulExecutionStrategy; /** * The callback function to handle the error cause of the effect program * if it fails during execution. * * @param cause - The cause of the error that occurred during the effect program execution. */ onError?: (cause: Cause.Cause) => void; /** * The callback function to handle the successful value of the effect program * if it completes successfully. * * @param value - The successful value returned by the effect program. */ onSuccess?: (value: A) => void; /** * The callback function to handle the status change of the effect execution * as it progresses through different states. * * @param status - The current status of the effect execution. */ onStatusChange?: (status: GracefulExecutionStatus) => void; /** * The callback function to execute when the effect execution is finally completed, * regardless of whether it succeeded or failed. */ onFinally?: () => void; } export function executeEffectGracefully(effect: (...args: P) => Effect.Effect, options?: ExecuteEffectGracefullyOptions) { const resolvedOptions = defu( options, { loadingStrategy: GracefulLoadingStrategy.ON_DEMAND, executionStrategy: GracefulExecutionStrategy.KEEP_STATE, onError: () => {}, onSuccess: () => {}, onStatusChange: () => {}, onFinally: () => {}, } satisfies ExecuteEffectGracefullyOptions, ) const data = ref() const error = ref>() const pending = ref(resolvedOptions.loadingStrategy === GracefulLoadingStrategy.PRELOAD) const status = ref(GracefulExecutionStatus.IDLE) const ensuredFailureLoading = ref(resolvedOptions.loadingStrategy === GracefulLoadingStrategy.PRELOAD) async function execute(...args: P) { pending.value = true ensuredFailureLoading.value = true status.value = GracefulExecutionStatus.PENDING if (resolvedOptions.executionStrategy === GracefulExecutionStrategy.RESET_STATE) { data.value = undefined error.value = undefined } await ApplicationRuntime.runPromiseExit( Effect.gen(function* () { const runtime = yield* ApplicationRuntimeService return yield* runtime.managedRuntime(effect(...args)).pipe( Effect.provide(NuxtApplication.layer(resolvedOptions.nuxtApp)), ) }), { signal: resolvedOptions.signal, }, ).then( Exit.match({ onSuccess: (value) => { data.value = value status.value = GracefulExecutionStatus.SUCCESS resolvedOptions.onSuccess(value) }, onFailure: (cause) => { error.value = cause status.value = GracefulExecutionStatus.ERROR resolvedOptions.onError(cause) ensuredFailureLoading.value = false }, }), ).finally(() => { if (resolvedOptions.executionStrategy === GracefulExecutionStrategy.RESET_STATE) { status.value = GracefulExecutionStatus.IDLE } pending.value = false resolvedOptions.onFinally() }) return data.value } const stop = watch(status, (status) => { resolvedOptions.onStatusChange(status) }) tryOnScopeDispose(() => { stop() }) return { data, error, pending, status, ensuredFailureLoading, execute, } }