import { BrowserStream } from "@effect/platform-browser" import * as AsyncData from "@typed/async-data" import { Effect, Fiber, identity, Option, Ref, Stream, SubscriptionRef } from "effect" export interface QueryRunner { readonly queryRef: SubscriptionRef.SubscriptionRef> readonly stateRef: SubscriptionRef.SubscriptionRef> readonly fiberRef: SubscriptionRef.SubscriptionRef>> readonly forkInterrupt: Effect.Effect> readonly forkFetch: Effect.Effect> readonly forkRefresh: Effect.Effect> readonly refreshOnWindowFocus: Effect.Effect } export interface MakeProps { readonly query: Effect.Effect } export const make = ( props: MakeProps ): Effect.Effect, never, R> => Effect.gen(function*() { const context = yield* Effect.context() const queryRef = yield* SubscriptionRef.make(props.query) const stateRef = yield* SubscriptionRef.make(AsyncData.noData()) const fiberRef = yield* SubscriptionRef.make(Option.none>()) const interrupt = fiberRef.pipe( Effect.flatMap(Option.match({ onSome: fiber => Ref.set(fiberRef, Option.none()).pipe( Effect.andThen(Fiber.interrupt(fiber)) ), onNone: () => Effect.void, })) ) const forkInterrupt = fiberRef.pipe( Effect.flatMap(Option.match({ onSome: fiber => Ref.set(fiberRef, Option.none()).pipe( Effect.andThen(Fiber.interrupt(fiber).pipe( Effect.asVoid, Effect.forkDaemon, )) ), onNone: () => Effect.forkDaemon(Effect.void), })) ) const forkFetch = interrupt.pipe( Effect.andThen( Ref.set(stateRef, AsyncData.loading()).pipe( Effect.andThen(queryRef), Effect.flatMap(identity), Effect.matchCauseEffect({ onSuccess: v => Ref.set(stateRef, AsyncData.success(v)), onFailure: c => Ref.set(stateRef, AsyncData.failure(c)), }), Effect.provide(context), Effect.fork, ) ), Effect.flatMap(fiber => Ref.set(fiberRef, Option.some(fiber)).pipe( Effect.andThen(Fiber.join(fiber)), Effect.andThen(Ref.set(fiberRef, Option.none())), ) ), Effect.forkDaemon, ) const forkRefresh = interrupt.pipe( Effect.andThen( Ref.update(stateRef, previous => { if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous)) return AsyncData.refreshing(previous) if (AsyncData.isRefreshing(previous)) return AsyncData.refreshing(previous.previous) return AsyncData.loading() }).pipe( Effect.andThen(queryRef), Effect.flatMap(identity), Effect.matchCauseEffect({ onSuccess: v => Ref.set(stateRef, AsyncData.success(v)), onFailure: c => Ref.set(stateRef, AsyncData.failure(c)), }), Effect.provide(context), Effect.fork, ) ), Effect.flatMap(fiber => Ref.set(fiberRef, Option.some(fiber)).pipe( Effect.andThen(Fiber.join(fiber)), Effect.andThen(Ref.set(fiberRef, Option.none())), ) ), Effect.forkDaemon, ) const refreshOnWindowFocus = Stream.runForEach( BrowserStream.fromEventListenerWindow("focus"), () => forkRefresh, ) return { queryRef, stateRef, fiberRef, forkInterrupt, forkFetch, forkRefresh, refreshOnWindowFocus, } })