import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, Pipeable, Predicate, PubSub, pipe, Ref, type Scope, Stream, Subscribable } from "effect" export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result") export type ResultTypeId = typeof ResultTypeId export type Result = ( | Initial | Running

| Final ) // biome-ignore lint/complexity/noBannedTypes: "{}" is relevant here export type Final = (Success | Failure) & ({} | Flags

) export type Flags

= WillFetch | WillRefresh | Refreshing

export declare namespace Result { export interface Prototype extends Pipeable.Pipeable, Equal.Equal { readonly [ResultTypeId]: ResultTypeId } export type Success> = [R] extends [Result] ? A : never export type Failure> = [R] extends [Result] ? E : never export type Progress> = [R] extends [Result] ? P : never } export declare namespace Flags { export type Keys = keyof WillFetch & WillRefresh & Refreshing } export interface Initial extends Result.Prototype { readonly _tag: "Initial" } export interface Running

extends Result.Prototype { readonly _tag: "Running" readonly progress: P } export interface Success extends Result.Prototype { readonly _tag: "Success" readonly value: A } export interface Failure extends Result.Prototype { readonly _tag: "Failure" readonly cause: Cause.Cause } export interface WillFetch { readonly _flag: "WillFetch" } export interface WillRefresh { readonly _flag: "WillRefresh" } export interface Refreshing

{ readonly _flag: "Refreshing" readonly progress: P } const ResultPrototype = Object.freeze({ ...Pipeable.Prototype, [ResultTypeId]: ResultTypeId, [Equal.symbol](this: Result, that: Result): boolean { if (this._tag !== that._tag || (this as Flags)._flag !== (that as Flags)._flag) return false if (hasRefreshingFlag(this) && !Equal.equals(this.progress, (that as Refreshing).progress)) return false return Match.value(this).pipe( Match.tag("Initial", () => true), Match.tag("Running", self => Equal.equals(self.progress, (that as Running).progress)), Match.tag("Success", self => Equal.equals(self.value, (that as Success).value)), Match.tag("Failure", self => Equal.equals(self.cause, (that as Failure).cause)), Match.exhaustive, ) }, [Hash.symbol](this: Result): number { return pipe(Hash.string(this._tag), tagHash => Match.value(this).pipe( Match.tag("Initial", () => tagHash), Match.tag("Running", self => Hash.combine(Hash.hash(self.progress))(tagHash)), Match.tag("Success", self => Hash.combine(Hash.hash(self.value))(tagHash)), Match.tag("Failure", self => Hash.combine(Hash.hash(self.cause))(tagHash)), Match.exhaustive, ), Hash.combine(Hash.hash((this as Flags)._flag)), hash => hasRefreshingFlag(this) ? Hash.combine(Hash.hash(this.progress))(hash) : hash, Hash.cached(this), ) }, } as const satisfies Result.Prototype) export const isResult = (u: unknown): u is Result => Predicate.hasProperty(u, ResultTypeId) export const isFinal = (u: unknown): u is Final => isResult(u) && (isSuccess(u) || isFailure(u)) export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial" export const isRunning = (u: unknown): u is Running => isResult(u) && u._tag === "Running" export const isSuccess = (u: unknown): u is Success => isResult(u) && u._tag === "Success" export const isFailure = (u: unknown): u is Failure => isResult(u) && u._tag === "Failure" export const hasFlag = (u: unknown): u is Flags => isResult(u) && Predicate.hasProperty(u, "_flag") export const hasWillFetchFlag = (u: unknown): u is WillFetch => isResult(u) && Predicate.hasProperty(u, "_flag") && u._flag === "WillFetch" export const hasWillRefreshFlag = (u: unknown): u is WillRefresh => isResult(u) && Predicate.hasProperty(u, "_flag") && u._flag === "WillRefresh" export const hasRefreshingFlag = (u: unknown): u is Refreshing => isResult(u) && Predicate.hasProperty(u, "_flag") && u._flag === "Refreshing" export const initial: { (): Initial (): Result } = (): Initial => Object.setPrototypeOf({ _tag: "Initial" }, ResultPrototype) export const running =

(progress?: P): Running

=> Object.setPrototypeOf({ _tag: "Running", progress }, ResultPrototype) export const succeed = (value: A): Success => Object.setPrototypeOf({ _tag: "Success", value }, ResultPrototype) export const fail = (cause: Cause.Cause ): Failure => Object.setPrototypeOf({ _tag: "Failure", cause }, ResultPrototype) export const willFetch = >( result: R ): Omit & WillFetch => Object.setPrototypeOf( Object.assign({}, result, { _flag: "WillFetch" }), Object.getPrototypeOf(result), ) export const willRefresh = >( result: R ): Omit & WillRefresh => Object.setPrototypeOf( Object.assign({}, result, { _flag: "WillRefresh" }), Object.getPrototypeOf(result), ) export const refreshing = , P = never>( result: R, progress?: P, ): Omit & Refreshing

=> Object.setPrototypeOf( Object.assign({}, result, { _flag: "Refreshing", progress }), Object.getPrototypeOf(result), ) export const fromExit: { (exit: Exit.Success): Success (exit: Exit.Failure): Failure (exit: Exit.Exit): Success | Failure } = exit => (exit._tag === "Success" ? succeed(exit.value) : fail(exit.cause)) as any export const toExit: { (self: Success): Exit.Success (self: Failure): Exit.Failure (self: Final): Exit.Exit (self: Result): Exit.Exit } = (self: Result): any => { switch (self._tag) { case "Success": return Exit.succeed(self.value) case "Failure": return Exit.failCause(self.cause) default: return Exit.fail(new Cause.NoSuchElementException()) } } export interface State { readonly get: Effect.Effect> readonly set: (v: Result) => Effect.Effect } export const State = (): Context.Tag, State> => Context.GenericTag("@effect-fc/Result/State") export interface Progress

{ readonly update: ( f: (previous: P) => Effect.Effect ) => Effect.Effect } export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{ readonly previous: Result }> {} export const Progress =

(): Context.Tag, Progress

> => Context.GenericTag("@effect-fc/Result/Progress") export const makeProgressLayer = (): Layer.Layer< Progress

, never, State > => Layer.effect(Progress

(), Effect.gen(function*() { const state = yield* State() return { update: (f: (previous: P) => Effect.Effect) => Effect.Do.pipe( Effect.bind("previous", () => Effect.andThen(state.get, previous => (isRunning(previous) || hasRefreshingFlag(previous)) ? Effect.succeed(previous) : Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous })), )), Effect.bind("progress", ({ previous }) => f(previous.progress)), Effect.let("next", ({ previous, progress }) => isRunning(previous) ? running(progress) : refreshing(previous, progress) as Final & Refreshing

), Effect.andThen(({ next }) => state.set(next)), ), } })) export namespace unsafeForkEffect { export type OutputContext = Exclude | Progress

| Progress> export interface Options { readonly initial?: Initial | Final readonly initialProgress?: P } } export const unsafeForkEffect = ( effect: Effect.Effect, options?: unsafeForkEffect.Options, NoInfer, P>, ): Effect.Effect< readonly [result: Subscribable.Subscribable, never, never>, fiber: Fiber.Fiber], never, Scope.Scope | unsafeForkEffect.OutputContext > => Effect.Do.pipe( Effect.bind("ref", () => Ref.make(options?.initial ?? initial())), Effect.bind("pubsub", () => PubSub.unbounded>()), Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State().pipe( Effect.andThen(state => state.set( (isFinal(options?.initial) && hasWillRefreshFlag(options?.initial)) ? refreshing(options.initial, options?.initialProgress) as Result : running(options?.initialProgress) ).pipe( Effect.andThen(effect), Effect.onExit(exit => Effect.andThen( state.set(fromExit(exit)), Effect.forkScoped(PubSub.shutdown(pubsub)), )), )), Effect.provide(Layer.empty.pipe( Layer.provideMerge(makeProgressLayer()), Layer.provideMerge(Layer.succeed(State(), { get: Ref.get(ref), set: v => Effect.andThen(Ref.set(ref, v), PubSub.publish(pubsub, v)) })), )), ))), Effect.map(({ ref, pubsub, fiber }) => [ Subscribable.make({ get: Ref.get(ref), changes: Stream.unwrapScoped(Effect.map( Effect.all([Ref.get(ref), Stream.fromPubSub(pubsub, { scoped: true })]), ([latest, stream]) => Stream.concat(Stream.make(latest), stream), )), }), fiber, ]), ) as Effect.Effect< readonly [result: Subscribable.Subscribable, never, never>, fiber: Fiber.Fiber], never, Scope.Scope | unsafeForkEffect.OutputContext > export namespace forkEffect { export type InputContext = R extends Progress ? [X] extends [P] ? R : never : R export type OutputContext = unsafeForkEffect.OutputContext export interface Options extends unsafeForkEffect.Options {} } export const forkEffect: { ( effect: Effect.Effect>>, options?: forkEffect.Options, NoInfer, P>, ): Effect.Effect< readonly [result: Subscribable.Subscribable, never, never>, fiber: Fiber.Fiber], never, Scope.Scope | forkEffect.OutputContext > } = unsafeForkEffect