/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ import { Effect, type Equivalence, Function, Predicate, Runtime, Scope } from "effect" import * as React from "react" import * as Component from "./Component.js" /** * A unique symbol representing the Async component type. * Used as a type brand to identify Async components. * * @experimental */ export const AsyncTypeId: unique symbol = Symbol.for("@effect-fc/Async/Async") /** * The type of the Async type ID symbol. */ export type AsyncTypeId = typeof AsyncTypeId /** * An Async component that supports suspense and promise-based async operations. * Combines Component behavior with Async-specific options. * * @example * ```ts * const MyAsyncComponent = async(component({ ... })) * ``` */ export interface Async extends AsyncPrototype, AsyncOptions {} /** * The prototype object for Async components containing their methods and behaviors. */ export interface AsyncPrototype { /** * The Async type ID brand. */ readonly [AsyncTypeId]: AsyncTypeId } /** * Configuration options for Async components. */ export interface AsyncOptions { /** * The default fallback React node to display while the async operation is pending. * Used if no fallback is provided to the component when rendering. */ readonly defaultFallback?: React.ReactNode } /** * Props for Async components, extending React.SuspenseProps without the children prop. * The children are managed internally by the Async component. */ export type AsyncProps = Omit /** * The prototype object for Async components. * Provides the `asFunctionComponent` method for converting async components to React function components. * * @internal Use the `async` function to create Async components instead of accessing this directly. */ export const AsyncPrototype: AsyncPrototype = Object.freeze({ [AsyncTypeId]: AsyncTypeId, /** * Converts an Async component to a React function component. * * @param runtimeRef - A reference to the Effect runtime for executing effects * @returns A React function component that suspends while the async operation is executing * * @example * ```ts * const MyComponent = component({ ... }) * const AsyncMyComponent = async(MyComponent) * const FunctionComponent = AsyncMyComponent.asFunctionComponent(runtimeRef) * ``` */ asFunctionComponent

( this: Component.Component & Async, runtimeRef: React.RefObject>>, ) { const Inner = (props: { readonly promise: Promise }) => React.use(props.promise) return ({ fallback, name, ...props }: AsyncProps) => { const promise = Runtime.runPromise(runtimeRef.current)( Effect.andThen( Component.useScope([], this), scope => Effect.provideService(this.body(props as P), Scope.Scope, scope), ) ) return React.createElement( React.Suspense, { fallback: fallback ?? this.defaultFallback, name }, React.createElement(Inner, { promise }), ) } }, } as const) /** * An equivalence function for comparing AsyncProps that ignores the `fallback` property. * Useful for memoization and re-render optimization. * * @param self - The first props object to compare * @param that - The second props object to compare * @returns `true` if the props are equivalent (excluding fallback), `false` otherwise * * @internal */ export const defaultPropsEquivalence: Equivalence.Equivalence = ( self: Record, that: Record, ) => { if (self === that) return true for (const key in self) { if (key === "fallback") continue if (!(key in that) || !Object.is(self[key], that[key])) return false } for (const key in that) { if (key === "fallback") continue if (!(key in self)) return false } return true } /** * A type guard to check if a value is an Async component. * * @param u - The value to check * @returns `true` if the value is an Async component, `false` otherwise * * @example * ```ts * if (isAsync(component)) { * // component is an Async component * } * ``` */ export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, AsyncTypeId) /** * Converts a Component into an Async component that supports suspense and promise-based async operations. * * The resulting component will wrap the original component with React.Suspense, * allowing async Effect computations to suspend and resolve properly. * * Note: The component cannot have a prop named "promise" as it's reserved for internal use. * * @param self - The component to convert to an Async component * @returns An Async component with the same body, error, and context types as the input * * @example * ```ts * const MyComponent = component({ * body: (props) => // ... * }) * * const AsyncMyComponent = async(MyComponent) * ``` * * @throws Will produce a type error if the component has a "promise" prop */ export const async = >( self: T & ( "promise" extends keyof Component.Component.Props ? "The 'promise' prop name is restricted for Async components. Please rename the 'promise' prop to something else." : T ) ): ( & Omit> & Component.Component< Component.Component.Props & AsyncProps, Component.Component.Success, Component.Component.Error, Component.Component.Context > & Async ) => Object.setPrototypeOf( Object.assign(function() {}, self, { propsEquivalence: defaultPropsEquivalence }), Object.freeze(Object.setPrototypeOf( Object.assign({}, AsyncPrototype), Object.getPrototypeOf(self), )), ) /** * Applies options to an Async component, returning a new Async component with the updated configuration. * * Supports both curried and uncurried application styles. * * @param self - The Async component to apply options to (in uncurried form) * @param options - The options to apply to the component * @returns An Async component with the applied options * * @example * ```ts * const AsyncComponent = async(myComponent) * * // Uncurried * const configured = withOptions(AsyncComponent, { defaultFallback: }) * * // Curried * const configurer = withOptions({ defaultFallback: }) * const configured = configurer(AsyncComponent) * ``` */ export const withOptions: { & Async>( options: Partial ): (self: T) => T & Async>( self: T, options: Partial, ): T } = Function.dual(2, & Async>( self: T, options: Partial, ): T => Object.setPrototypeOf( Object.assign(function() {}, self, options), Object.getPrototypeOf(self), ))