/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */ /** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Stream, Tracer, type Utils } from "effect" import * as React from "react" import { Memoized } from "./index.js" import * as Result from "./Result.js" export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component") export type TypeId = typeof TypeId export interface Component

extends Effect.Effect<(props: P) => A, never, Exclude>, Component.Options { new(_: never): Record readonly [TypeId]: TypeId readonly "~Props": P readonly "~Success": A readonly "~Error": E readonly "~Context": R /** @internal */ readonly body: (props: P) => Effect.Effect /** @internal */ makeFunctionComponent( runtimeRef: React.Ref>> ): (props: P) => A } export namespace Component { export type Props> = [T] extends [Component] ? P : never export type Success> = [T] extends [Component] ? A : never export type Error> = [T] extends [Component] ? E : never export type Context> = [T] extends [Component] ? R : never export type AsComponent> = Component, Success, Error, Context> export interface Options { readonly displayName?: string readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionDebounce: Duration.DurationInput } } const ComponentProto = Object.freeze({ ...Effectable.CommitPrototype, [TypeId]: TypeId, commit: Effect.fnUntraced(function*

( this: Component ) { // biome-ignore lint/style/noNonNullAssertion: React ref initialization const runtimeRef = React.useRef>>(null!) runtimeRef.current = yield* Effect.runtime>() return yield* React.useState(() => Runtime.runSync(runtimeRef.current)(Effect.cachedFunction( (_services: readonly any[]) => Effect.sync(() => { const f: React.FC

= this.makeFunctionComponent(runtimeRef) f.displayName = this.displayName ?? "Anonymous" return Memoized.isMemoized(this) ? React.memo(f, this.propsAreEqual) : f }), Equivalence.array(Equivalence.strict()), )))[0](Array.from( Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values() )) }), makeFunctionComponent

( this: Component, runtimeRef: React.RefObject>>, ) { return (props: P) => Runtime.runSync(runtimeRef.current)( Effect.andThen( useScope([], this), scope => Effect.provideService(this.body(props), Scope.Scope, scope), ) ) }, } as const) const defaultOptions: Component.Options = { finalizerExecutionStrategy: ExecutionStrategy.sequential, finalizerExecutionDebounce: "100 millis", } const nonReactiveTags = [Tracer.ParentSpan] as const export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId) export namespace make { export type Gen = { >, A extends React.ReactNode, P extends {} = {}>( body: (props: P) => Generator ): Component< P, A, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never > >, A, B extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never >, props: NoInfer

, ) => B, ): Component>, Effect.Effect.Error, Effect.Effect.Context> >, A, B, C extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never >, props: NoInfer

, ) => B, b: (_: B, props: NoInfer

) => C, ): Component>, Effect.Effect.Error, Effect.Effect.Context> >, A, B, C, D extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never >, props: NoInfer

, ) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, ): Component>, Effect.Effect.Error, Effect.Effect.Context> >, A, B, C, D, E extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never >, props: NoInfer

, ) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, ): Component>, Effect.Effect.Error, Effect.Effect.Context> >, A, B, C, D, E, F extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never >, props: NoInfer

, ) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, ): Component>, Effect.Effect.Error, Effect.Effect.Context> >, A, B, C, D, E, F, G extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never >, props: NoInfer

, ) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => G, ): Component>, Effect.Effect.Error, Effect.Effect.Context> >, A, B, C, D, E, F, G, H extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never >, props: NoInfer

, ) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => G, g: (_: G, props: NoInfer

) => H, ): Component>, Effect.Effect.Error, Effect.Effect.Context> >, A, B, C, D, E, F, G, H, I extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never >, props: NoInfer

, ) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => G, g: (_: G, props: NoInfer

) => H, h: (_: H, props: NoInfer

) => I, ): Component>, Effect.Effect.Error, Effect.Effect.Context> >, A, B, C, D, E, F, G, H, I, J extends Effect.Effect, P extends {} = {}>( body: (props: P) => Generator, a: ( _: Effect.Effect< A, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never >, props: NoInfer

, ) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => G, g: (_: G, props: NoInfer

) => H, h: (_: H, props: NoInfer

) => I, i: (_: I, props: NoInfer

) => J, ): Component>, Effect.Effect.Error, Effect.Effect.Context> } export type NonGen = { , P extends {} = {}>( body: (props: P) => Eff ): Component>, Effect.Effect.Error, Effect.Effect.Context> , A, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => Eff, ): Component>, Effect.Effect.Error, Effect.Effect.Context> , A, B, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => Eff, ): Component>, Effect.Effect.Error, Effect.Effect.Context> , A, B, C, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => Eff, ): Component>, Effect.Effect.Error, Effect.Effect.Context> , A, B, C, D, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => Eff, ): Component>, Effect.Effect.Error, Effect.Effect.Context> , A, B, C, D, E, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => Eff, ): Component>, Effect.Effect.Error, Effect.Effect.Context> , A, B, C, D, E, F, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => Eff, ): Component>, Effect.Effect.Error, Effect.Effect.Context> , A, B, C, D, E, F, G, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => G, g: (_: G, props: NoInfer

) => Eff, ): Component>, Effect.Effect.Error, Effect.Effect.Context> , A, B, C, D, E, F, G, H, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => G, g: (_: G, props: NoInfer

) => H, h: (_: H, props: NoInfer

) => Eff, ): Component>, Effect.Effect.Error, Effect.Effect.Context> , A, B, C, D, E, F, G, H, I, P extends {} = {}>( body: (props: P) => A, a: (_: A, props: NoInfer

) => B, b: (_: B, props: NoInfer

) => C, c: (_: C, props: NoInfer

) => D, d: (_: D, props: NoInfer

) => E, e: (_: E, props: NoInfer

) => F, f: (_: F, props: NoInfer

) => G, g: (_: G, props: NoInfer

) => H, h: (_: H, props: NoInfer

) => I, i: (_: I, props: NoInfer

) => Eff, ): Component>, Effect.Effect.Error, Effect.Effect.Context> } } export const make: ( & make.Gen & make.NonGen & (( spanName: string, spanOptions?: Tracer.SpanOptions, ) => make.Gen & make.NonGen) ) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => { if (typeof spanNameOrBody !== "string") { return Object.setPrototypeOf( Object.assign(function() {}, defaultOptions, { body: Effect.fn(spanNameOrBody as any, ...pipeables), }), ComponentProto, ) } else { const spanOptions = pipeables[0] return (body: any, ...pipeables: any[]) => Object.setPrototypeOf( Object.assign(function() {}, defaultOptions, { body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []), displayName: spanNameOrBody, }), ComponentProto, ) } } export const makeUntraced: ( & make.Gen & make.NonGen & ((name: string) => make.Gen & make.NonGen) ) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => ( typeof spanNameOrBody !== "string" ? Object.setPrototypeOf( Object.assign(function() {}, defaultOptions, { body: Effect.fnUntraced(spanNameOrBody as any, ...pipeables as []), }), ComponentProto, ) : (body: any, ...pipeables: any[]) => Object.setPrototypeOf( Object.assign(function() {}, defaultOptions, { body: Effect.fnUntraced(body, ...pipeables as []), displayName: spanNameOrBody, }), ComponentProto, ) ) export const withOptions: { >( options: Partial ): (self: T) => T >( self: T, options: Partial, ): T } = Function.dual(2, >( self: T, options: Partial, ): T => Object.setPrototypeOf( Object.assign(function() {}, self, options), Object.getPrototypeOf(self), )) export const withRuntime: {

( context: React.Context>, ): (self: Component>) => (props: P) => A

( self: Component>, context: React.Context>, ): (props: P) => A } = Function.dual(2,

( self: Component, context: React.Context>, ) => function WithRuntime(props: P) { return React.createElement( Runtime.runSync(React.useContext(context))(self), props, ) }) export class ScopeMap extends Effect.Service()("@effect-fc/Component/ScopeMap", { effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty())) }) {} export namespace ScopeMap { export interface Entry { readonly scope: Scope.CloseableScope readonly closeFiber: Option.Option> } } export namespace useScope { export interface Options { readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionDebounce?: Duration.DurationInput } } export const useScope: { ( deps: React.DependencyList, options?: useScope.Options, ): Effect.Effect } = Effect.fnUntraced(function*(deps, options) { // biome-ignore lint/style/noNonNullAssertion: context initialization const runtimeRef = React.useRef>(null!) runtimeRef.current = yield* Effect.runtime() const scopeMap = yield* ScopeMap as unknown as Effect.Effect const [key, scope] = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.andThen( Effect.all([Effect.succeed({}), scopeMap.ref]), ([key, map]) => Effect.andThen( Option.match(HashMap.get(map, key), { onSome: entry => Effect.succeed(entry.scope), onNone: () => Effect.tap( Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy), scope => Ref.update(scopeMap.ref, HashMap.set(key, { scope, closeFiber: Option.none(), })), ), }), scope => [key, scope] as const, ), // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList )), deps) // biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "key" React.useEffect(() => Runtime.runSync(runtimeRef.current)(scopeMap.ref.pipe( Effect.andThen(HashMap.get(key)), Effect.tap(entry => Option.match(entry.closeFiber, { onSome: fiber => Effect.andThen( Ref.update(scopeMap.ref, HashMap.set(key, { ...entry, closeFiber: Option.none() })), Fiber.interruptFork(fiber), ), onNone: () => Effect.void, })), Effect.map(({ scope }) => () => Runtime.runSync(runtimeRef.current)(Effect.andThen( Effect.forkDaemon(Effect.sleep(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce).pipe( Effect.andThen(Scope.close(scope, Exit.void)), Effect.andThen(Ref.update(scopeMap.ref, HashMap.remove(key))), )), fiber => Ref.update(scopeMap.ref, HashMap.set(key, { scope, closeFiber: Option.some(fiber), })), )) ), )), [key]) return scope }) export const useOnMount: { ( f: () => Effect.Effect ): Effect.Effect } = Effect.fnUntraced(function* ( f: () => Effect.Effect ) { const runtime = yield* Effect.runtime() return yield* React.useState(() => Runtime.runSync(runtime)(Effect.cached(f())))[0] }) export namespace useOnChange { export type Options = useScope.Options } export const useOnChange: { ( f: () => Effect.Effect, deps: React.DependencyList, options?: useOnChange.Options, ): Effect.Effect> } = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps: React.DependencyList, options?: useOnChange.Options, ) { const runtime = yield* Effect.runtime>() const scope = yield* useScope(deps, options) // biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "scope" return yield* React.useMemo(() => Runtime.runSync(runtime)( Effect.cached(Effect.provideService(f(), Scope.Scope, scope)) ), [scope]) }) export namespace useOnMountResult { export interface Options extends Result.forkEffectScoped.Options

{ readonly equivalence?: Equivalence.Equivalence> } } export const useOnMountResult: { ( f: () => Effect.Effect>>, options?: useOnChangeResult.Options, ): Effect.Effect, never, Result.forkEffectScoped.OutputContext> } = Effect.fnUntraced(function* ( f: () => Effect.Effect>>, options?: useOnChangeResult.Options, ) { const [result, setResult] = React.useState(() => Result.initial() as Result.Result) yield* useOnMount(() => Result.forkEffectScoped(f(), options).pipe( Effect.andThen(Stream.fromQueue), Stream.unwrap, Stream.changesWith(options?.equivalence ?? Equivalence.strict()), Stream.runForEach(result => Effect.sync(() => setResult(result))), )) return result }) export namespace useOnChangeResult { export interface Options extends useOnMountResult.Options, useReactEffect.Options {} } export const useOnChangeResult: { ( f: () => Effect.Effect>>, deps?: React.DependencyList, options?: useOnChangeResult.Options, ): Effect.Effect< Result.Result, never, Exclude, Scope.Scope> > } = Effect.fnUntraced(function* ( f: () => Effect.Effect>>, deps?: React.DependencyList, options?: useOnChangeResult.Options, ) { const [result, setResult] = React.useState(() => Result.initial() as Result.Result) yield* useReactEffect(() => Result.forkEffectScoped(f(), options).pipe( Effect.andThen(Stream.fromQueue), Stream.unwrap, Stream.changesWith(options?.equivalence ?? Equivalence.strict()), Stream.runForEach(result => Effect.sync(() => setResult(result))), ), deps, options) return result }) export namespace useReactEffect { export interface Options { readonly finalizerExecutionMode?: "sync" | "fork" readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy } } export const useReactEffect: { ( f: () => Effect.Effect, deps?: React.DependencyList, options?: useReactEffect.Options, ): Effect.Effect> } = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps?: React.DependencyList, options?: useReactEffect.Options, ) { const runtime = yield* Effect.runtime>() // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList React.useEffect(() => runReactEffect(runtime, f, options), deps) }) const runReactEffect = ( runtime: Runtime.Runtime>, f: () => Effect.Effect, options?: useReactEffect.Options, ) => Effect.Do.pipe( Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)), Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(f(), Scope.Scope, scope))), Effect.map(({ scope }) => () => { 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 } } ), Runtime.runSync(runtime), ) export namespace useReactLayoutEffect { export type Options = useReactEffect.Options } export const useReactLayoutEffect: { ( f: () => Effect.Effect, deps?: React.DependencyList, options?: useReactLayoutEffect.Options, ): Effect.Effect> } = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps?: React.DependencyList, options?: useReactLayoutEffect.Options, ) { const runtime = yield* Effect.runtime>() // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList React.useLayoutEffect(() => runReactEffect(runtime, f, options), deps) }) export const useCallbackSync: { ( f: (...args: Args) => Effect.Effect, deps: React.DependencyList, ): Effect.Effect<(...args: Args) => A, never, R> } = Effect.fnUntraced(function* ( f: (...args: Args) => Effect.Effect, deps: React.DependencyList, ) { // biome-ignore lint/style/noNonNullAssertion: context initialization const runtimeRef = React.useRef>(null!) runtimeRef.current = yield* Effect.runtime() // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(f(...args)), deps) }) export const useCallbackPromise: { ( f: (...args: Args) => Effect.Effect, deps: React.DependencyList, ): Effect.Effect<(...args: Args) => Promise, never, R> } = Effect.fnUntraced(function* ( f: (...args: Args) => Effect.Effect, deps: React.DependencyList, ) { // biome-ignore lint/style/noNonNullAssertion: context initialization const runtimeRef = React.useRef>(null!) runtimeRef.current = yield* Effect.runtime() // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList return React.useCallback((...args: Args) => Runtime.runPromise(runtimeRef.current)(f(...args)), deps) }) export namespace useContext { export type Options = useOnChange.Options } export const useContext = ( layer: Layer.Layer, options?: useContext.Options, ): Effect.Effect, E, RIn> => useOnChange(() => Effect.context().pipe( Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))), Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)), Effect.andThen(runtime => runtime.runtimeEffect), Effect.andThen(runtime => runtime.context), ), [layer], options)