import { type Context, Effect, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect" import * as React from "react" import { SetStateAction } from "./types/index.js" export interface ScopeOptions { readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionMode?: "sync" | "fork" } export const useMemo: { ( factory: () => Effect.Effect, deps: React.DependencyList, ): Effect.Effect } = Effect.fnUntraced(function* ( factory: () => Effect.Effect, deps: React.DependencyList, ) { const runtime = yield* Effect.runtime() return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps) }) export const useOnce: { (factory: () => Effect.Effect): Effect.Effect } = Effect.fnUntraced(function* ( factory: () => Effect.Effect ) { return yield* useMemo(factory, []) }) export const useMemoLayer: { ( layer: Layer.Layer ): Effect.Effect, E, RIn> } = Effect.fnUntraced(function* ( layer: Layer.Layer ) { return yield* useMemo(() => Effect.provide(Effect.context(), layer), [layer]) }) export const useCallbackSync: { ( callback: (...args: Args) => Effect.Effect, deps: React.DependencyList, ): Effect.Effect<(...args: Args) => A, never, R> } = Effect.fnUntraced(function* ( callback: (...args: Args) => Effect.Effect, deps: React.DependencyList, ) { const runtime = yield* Effect.runtime() return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps) }) export const useCallbackPromise: { ( callback: (...args: Args) => Effect.Effect, deps: React.DependencyList, ): Effect.Effect<(...args: Args) => Promise, never, R> } = Effect.fnUntraced(function* ( callback: (...args: Args) => Effect.Effect, deps: React.DependencyList, ) { const runtime = yield* Effect.runtime() return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps) }) export const useEffect: { ( effect: () => Effect.Effect, deps?: React.DependencyList, options?: ScopeOptions, ): Effect.Effect> } = Effect.fnUntraced(function* ( effect: () => Effect.Effect, deps?: React.DependencyList, options?: ScopeOptions, ) { const runtime = yield* Effect.runtime>() React.useEffect(() => { const { scope, exit } = Effect.Do.pipe( Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)), Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))), Runtime.runSync(runtime), ) return () => { switch (options?.finalizerExecutionMode ?? "sync") { case "sync": Runtime.runSync(runtime)(Scope.close(scope, exit)) break case "fork": Runtime.runFork(runtime)(Scope.close(scope, exit)) break } } }, deps) }) export const useLayoutEffect: { ( effect: () => Effect.Effect, deps?: React.DependencyList, options?: ScopeOptions, ): Effect.Effect> } = Effect.fnUntraced(function* ( effect: () => Effect.Effect, deps?: React.DependencyList, options?: ScopeOptions, ) { const runtime = yield* Effect.runtime>() React.useLayoutEffect(() => { const { scope, exit } = Effect.Do.pipe( Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)), Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))), Runtime.runSync(runtime), ) return () => { switch (options?.finalizerExecutionMode ?? "sync") { case "sync": Runtime.runSync(runtime)(Scope.close(scope, exit)) break case "fork": Runtime.runFork(runtime)(Scope.close(scope, exit)) break } } }, deps) }) export const useFork: { ( effect: () => Effect.Effect, deps?: React.DependencyList, options?: Runtime.RunForkOptions & ScopeOptions, ): Effect.Effect> } = Effect.fnUntraced(function* ( effect: () => Effect.Effect, deps?: React.DependencyList, options?: Runtime.RunForkOptions & ScopeOptions, ) { const runtime = yield* Effect.runtime>() React.useEffect(() => { const scope = Runtime.runSync(runtime)(options?.scope ? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) : Scope.make(options?.finalizerExecutionStrategy) ) Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope }) return () => { switch (options?.finalizerExecutionMode ?? "fork") { case "sync": Runtime.runSync(runtime)(Scope.close(scope, Exit.void)) break case "fork": Runtime.runFork(runtime)(Scope.close(scope, Exit.void)) break } } }, deps) }) export const useRefFromReactiveValue: { (value: A): Effect.Effect> } = Effect.fnUntraced(function*(value) { const ref = yield* useOnce(() => SubscriptionRef.make(value)) yield* useEffect(() => Ref.set(ref, value), [value]) return ref }) export const useSubscribeRefs: { []>( ...refs: Refs ): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success }> } = Effect.fnUntraced(function* []>( ...refs: Refs ) { const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef[]) )) yield* useFork(() => pipe( refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)), streams => Stream.zipLatestAll(...streams), Stream.runForEach(v => Effect.sync(() => setReactStateValue(v)) ), ), refs) return reactStateValue as any }) export const useRefState: { ( ref: SubscriptionRef.SubscriptionRef ): Effect.Effect>]> } = Effect.fnUntraced(function* (ref: SubscriptionRef.SubscriptionRef) { const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref)) yield* useFork(() => Stream.runForEach( Stream.changesWith(ref.changes, (x, y) => x === y), v => Effect.sync(() => setReactStateValue(v)), ), [ref]) const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction) => Ref.update(ref, prevState => SetStateAction.value(setStateAction, prevState) ), [ref]) return [reactStateValue, setValue] }) export const useStreamFromReactiveValues: { ( values: A ): Effect.Effect, never, Scope.Scope> } = Effect.fnUntraced(function* (values: A) { const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe( Effect.bind("latest", () => Ref.make(values)), Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown)), Effect.let("stream", ({ latest, pubsub }) => latest.pipe( Effect.flatMap(a => Effect.map( Stream.fromPubSub(pubsub, { scoped: true }), s => Stream.concat(Stream.make(a), s), )), Stream.unwrapScoped, )), )) yield* useEffect(() => Ref.set(latest, values).pipe( Effect.andThen(PubSub.publish(pubsub, values)), Effect.unlessEffect(PubSub.isShutdown(pubsub)), ), values) return stream }) export const useSubscribeStream: { ( stream: Stream.Stream ): Effect.Effect, never, R> , E, R>( stream: Stream.Stream, initialValue: A, ): Effect.Effect, never, R> } = Effect.fnUntraced(function* , E, R>( stream: Stream.Stream, initialValue?: A, ) { const [reactStateValue, setReactStateValue] = React.useState( React.useMemo(() => initialValue ? Option.some(initialValue) : Option.none(), []) ) yield* useFork(() => Stream.runForEach( Stream.changesWith(stream, (x, y) => x === y), v => Effect.sync(() => setReactStateValue(Option.some(v))), ), [stream]) return reactStateValue as Option.Some })