import { Cause, Context, Effect, Layer, type Option } from "effect-app" import { wrapEffect } from "effect-app/utils" import { CurrentToastId, Toast } from "./toast.js" export interface ToastOptions, WaiR, SucR, ErrR> { stableToastId?: undefined | string | ((...args: Args) => string | undefined) timeout?: number showSpanInfo?: false onWaiting: | string | ((...args: Args) => string | null) | null | (( ...args: Args ) => Effect.Effect) onSuccess: | string | ((a: A, ...args: Args) => string | null) | null | (( a: A, ...args: Args ) => Effect.Effect) onFailure: | string | (( error: Option.Option, ...args: Args ) => string | { level: "warn" | "error"; message: string }) | (( error: Option.Option, ...args: Args ) => Effect.Effect) } // @effect-diagnostics-next-line missingEffectServiceDependency:off export class WithToast extends Context.Service()("WithToast", { make: Effect.gen(function*() { const toast = yield* Toast return ( options: ToastOptions ) => Effect.fnUntraced(function*(self: Effect.Effect, ...args: Args) { const baseTimeout = options.timeout ?? 3_000 const stableToastId = typeof options.stableToastId === "function" ? options.stableToastId(...args) : options.stableToastId const t = yield* wrapEffect(options.onWaiting)(...args) const toastId = t === null ? stableToastId : yield* toast.info( t, { id: stableToastId ?? null } // TODO: timeout forever? ) return yield* self.pipe( Effect.tap(Effect.fnUntraced(function*(a) { const t = yield* wrapEffect(options.onSuccess)(a, ...args) if (t === null) { return } yield* toast.success( t, toastId !== undefined ? { id: toastId, timeout: baseTimeout } : { timeout: baseTimeout } ) })), Effect.tapCause(Effect.fnUntraced(function*(cause) { yield* Effect.logDebug( "WithToast - caught error cause: " + Cause.squash(cause), Cause.hasInterruptsOnly(cause), cause ) if (Cause.hasInterruptsOnly(cause)) { if (toastId) yield* toast.dismiss(toastId) return } const spanInfo = options.showSpanInfo !== false ? yield* Effect.currentSpan.pipe( Effect.map((span) => `\nTrace: ${span.traceId}\nSpan: ${span.spanId}`), Effect.orElseSucceed(() => "") ) : "" const t = yield* wrapEffect(options.onFailure)(Cause.findErrorOption(cause), ...args) const opts = { timeout: baseTimeout * 2 } if (typeof t === "object") { const message = t.message + spanInfo return t.level === "warn" ? yield* toast.warning(message, toastId !== undefined ? { ...opts, id: toastId } : opts) : yield* toast.error(message, toastId !== undefined ? { ...opts, id: toastId } : opts) } yield* toast.error(t + spanInfo, toastId !== undefined ? { ...opts, id: toastId } : opts) }, Effect.uninterruptible)), toastId !== undefined ? Effect.provideService(CurrentToastId, CurrentToastId.of({ toastId })) : (_) => _ ) }) }) }) { static readonly DefaultWithoutDependencies = Layer.effect(this, this.make) static readonly Default = this.DefaultWithoutDependencies static readonly handle = , R, WaiR = never, SucR = never, ErrR = never>( options: ToastOptions ): (self: Effect.Effect, ...args: Args) => Effect.Effect => (self, ...args) => this.use((_) => _(options)(self, ...args)) }