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
})