/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */ import { Context, type Duration, Effect, Exit, Function, identity, Layer, Pipeable, Predicate, Scope, Tracer, } from "effect" import * as React from "react" export const ComponentTypeId: unique symbol = Symbol.for("@effect-fc/Component/Component") export type ComponentTypeId = typeof ComponentTypeId export interface Component

extends ComponentPrototype, ComponentOptions { new(_: never): Record readonly [ComponentTypeId]: ComponentTypeId readonly "~Props": P readonly "~Success": A readonly "~Error": E readonly "~Context": R readonly "~Function": F readonly body: (props: P) => Effect.Effect } export declare namespace Component { export type Default

= Component> export type Any = Component export type Signature = (props: any) => React.ReactNode export type DefaultSignature

= (props: P) => A export type Props = T["~Props"] export type Success = T["~Success"] export type Error = T["~Error"] export type Context = T["~Context"] export type Function = T["~Function"] export type AsComponent = Component, Success, Error, Context, Function> } export interface ComponentOptions { readonly displayName?: string readonly nonReactiveTags: readonly Context.Key[] readonly finalizerExecutionStrategy: "sequential" | "parallel" readonly finalizerExecutionDebounce: Duration.Input } export const defaultOptions: ComponentOptions = { nonReactiveTags: [Tracer.ParentSpan], finalizerExecutionStrategy: "sequential", finalizerExecutionDebounce: "100 millis", } export interface ComponentPrototype extends Pipeable.Pipeable { readonly [ComponentTypeId]: ComponentTypeId readonly use: Effect.Effect> } type ComponentImpl = Component const makeFunctionComponent = ( self: ComponentImpl, contextRef: React.RefObject>, ): Component.Signature => { if ("asFunctionComponent" in self && typeof self.asFunctionComponent === "function") { return self.asFunctionComponent(contextRef) } const FunctionComponent = (props: {}) => Effect.runSyncWith(contextRef.current)( Effect.flatMap( useScope([], self), scope => Effect.provideService(self.body(props), Scope.Scope, scope), ), ) FunctionComponent.displayName = self.displayName ?? "Anonymous" return "transformFunctionComponent" in self && typeof self.transformFunctionComponent === "function" ? self.transformFunctionComponent(FunctionComponent) : FunctionComponent } const use = Effect.fnUntraced(function* (self: ComponentImpl) { const context = yield* Effect.context() const cached = componentCache.get(self) if (cached !== undefined) { cached.contextRef.current = context return cached.component } const contextRef = { current: context } const component = makeFunctionComponent(self, contextRef) componentCache.set(self, { contextRef, component }) return component }) const componentCache = new WeakMap } readonly component: Component.Signature }>() export const ComponentPrototype = Object.freeze({ [ComponentTypeId]: ComponentTypeId, ...Pipeable.Prototype, get use() { return use(this as ComponentImpl) }, }) as unknown as ComponentPrototype export const isComponent = (u: unknown): u is Component.Any => Predicate.hasProperty(u, ComponentTypeId) type GeneratorBody

= ( props: P, ) => Effect.fn.Return type EffectBody

= ( props: P, ) => Effect.Effect export interface Make {

( body: GeneratorBody | EffectBody, ...pipeables: readonly Function[] ): Component.Default (name: string, options?: Tracer.SpanOptionsNoTrace):

( body: GeneratorBody | EffectBody, ...pipeables: readonly Function[] ) => Component.Default } const component = ( body: Function, displayName: string | undefined, traced: boolean, pipeables: readonly Function[], ): Component.Any => Object.setPrototypeOf( Object.assign(function() {}, defaultOptions, { body: traced && displayName ? Effect.fn(displayName)(body as never, ...pipeables as []) : Effect.fnUntraced(body as never, ...pipeables as []), displayName, }), ComponentPrototype, ) export const make: Make = ((nameOrBody: string | Function, ...args: readonly unknown[]) => { if (typeof nameOrBody === "string") { return (body: Function, ...pipeables: readonly Function[]) => component(body, nameOrBody, true, pipeables) } return component(nameOrBody, undefined, true, args as readonly Function[]) }) as Make export const makeUntraced: Make = ((nameOrBody: string | Function, ...args: readonly unknown[]) => { if (typeof nameOrBody === "string") { return (body: Function, ...pipeables: readonly Function[]) => component(body, nameOrBody, false, pipeables) } return component(nameOrBody, undefined, false, args as readonly Function[]) }) as Make export declare namespace withSignature { export type Result = ( & Omit> & Component, Component.Success, Component.Error, Component.Context, F> ) } export const withSignature: { (): (self: T) => withSignature.Result (self: T): withSignature.Result } = (self?: Component.Any): any => self === undefined ? identity : self 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, F>) => F

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

( self: Component, context: React.Context>, ) => function WithRuntime(props: P) { return React.createElement( Effect.runSyncWith(React.useContext(context))(self.use) as React.FC

, props, ) }) export declare namespace useScope { export interface Options { readonly finalizerExecutionStrategy?: "sequential" | "parallel" readonly finalizerExecutionDebounce?: Duration.Input } } export const useScope = Effect.fnUntraced(function* ( deps: React.DependencyList, options?: useScope.Options, ): Effect.fn.Return { const context = yield* Effect.context() const contextRef = React.useRef(context) contextRef.current = context const scope = React.useMemo( () => Scope.makeUnsafe(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy), // biome-ignore lint/correctness/useExhaustiveDependencies: caller controls scope lifetime deps, ) React.useEffect(() => { const pending = scopeCleanupTimers.get(scope) if (pending !== undefined) clearTimeout(pending) return () => { const timer = setTimeout(() => { Effect.runSyncWith(contextRef.current)(Scope.close(scope, Exit.succeed(undefined))) scopeCleanupTimers.delete(scope) }, durationMillis(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce)) scopeCleanupTimers.set(scope, timer) } }, [scope, options?.finalizerExecutionDebounce]) return scope }) const scopeCleanupTimers = new WeakMap>() const durationMillis = (input: Duration.Input): number => { if (typeof input === "number") return input return Number(Effect.runSync(Effect.map(Effect.succeed(input), value => { const match = typeof value === "string" ? /([\d.]+)\s*(millis|seconds?)/.exec(value) : undefined if (!match) return 0 return Number(match[1]) * (match[2].startsWith("second") ? 1_000 : 1) }))) } export const useOnMount = Effect.fnUntraced(function* ( f: () => Effect.Effect, ): Effect.fn.Return { const context = yield* Effect.context() const id = React.useId() let cached = mountCache.get(id) if (cached === undefined) { cached = { effect: Effect.runSyncWith(context)(Effect.cached(f())), } mountCache.set(id, cached) } React.useEffect(() => { if (cached?.cleanup !== undefined) clearTimeout(cached.cleanup) const entry = cached return () => { entry.cleanup = setTimeout(() => mountCache.delete(id), 0) } }, [id, cached]) return yield* cached.effect as Effect.Effect }) const mountCache = new Map cleanup?: ReturnType }>() export declare namespace useOnChange { export interface Options extends useScope.Options {} } export const useOnChange = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps: React.DependencyList, options?: useOnChange.Options, ): Effect.fn.Return> { const context = yield* Effect.context>() const scope = yield* useScope(deps, options) const cached = // biome-ignore lint/correctness/useExhaustiveDependencies: scope tracks the caller-provided dependency list React.useMemo( () => Effect.runSyncWith(context)(Effect.cached(Effect.provideService(f(), Scope.Scope, scope))), [scope], ) return yield* cached }) export declare namespace useReactEffect { export interface Options { readonly finalizerExecutionMode?: "sync" | "fork" readonly finalizerExecutionStrategy?: "sequential" | "parallel" } } const runReactEffect = ( context: Context.Context>, f: () => Effect.Effect, options?: useReactEffect.Options, ): (() => void) => { const scope = Scope.makeUnsafe(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy) Effect.runSyncWith(context)(Effect.exit(Effect.provideService(f(), Scope.Scope, scope))) return () => { const close = Scope.close(scope, Exit.succeed(undefined)) if ((options?.finalizerExecutionMode ?? "fork") === "sync") Effect.runSyncWith(context)(close) else Effect.runForkWith(context)(close) } } export const useReactEffect = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps?: React.DependencyList, options?: useReactEffect.Options, ): Effect.fn.Return> { const context = yield* Effect.context>() // biome-ignore lint/correctness/useExhaustiveDependencies: caller supplies React dependencies React.useEffect(() => runReactEffect(context, f, options), deps) }) export declare namespace useReactLayoutEffect { export interface Options extends useReactEffect.Options {} } export const useReactLayoutEffect = Effect.fnUntraced(function* ( f: () => Effect.Effect, deps?: React.DependencyList, options?: useReactLayoutEffect.Options, ): Effect.fn.Return> { const context = yield* Effect.context>() // biome-ignore lint/correctness/useExhaustiveDependencies: caller supplies React dependencies React.useLayoutEffect(() => runReactEffect(context, f, options), deps) }) export const useRunSync = (): Effect.Effect< (effect: Effect.Effect) => A, never, R > => Effect.map(Effect.context(), Effect.runSyncWith) export const useRunPromise = (): Effect.Effect< (effect: Effect.Effect) => Promise, never, R > => Effect.map(Effect.context(), Effect.runPromiseWith) export const useCallbackSync = Effect.fnUntraced(function* ( f: (...args: Args) => Effect.Effect, deps: React.DependencyList, ): Effect.fn.Return<(...args: Args) => A, never, R> { const context = yield* Effect.context() const contextRef = React.useRef(context) contextRef.current = context // biome-ignore lint/correctness/useExhaustiveDependencies: caller supplies React dependencies return React.useCallback((...args: Args) => Effect.runSyncWith(contextRef.current)(f(...args)), deps) }) export const useCallbackPromise = Effect.fnUntraced(function* ( f: (...args: Args) => Effect.Effect, deps: React.DependencyList, ): Effect.fn.Return<(...args: Args) => Promise, never, R> { const context = yield* Effect.context() const contextRef = React.useRef(context) contextRef.current = context // biome-ignore lint/correctness/useExhaustiveDependencies: caller supplies React dependencies return React.useCallback((...args: Args) => Effect.runPromiseWith(contextRef.current)(f(...args)), deps) }) export declare namespace useContext { export interface Options extends useOnChange.Options {} } export const useContextFromLayer = ( layer: Layer.Layer, options?: useContext.Options, ): Effect.Effect, E, RIn | Scope.Scope> => useOnChange( () => Effect.flatMap( Effect.context(), context => Layer.build(Layer.provide(layer, Layer.succeedContext(context))), ), [layer], options, )