/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ import { Context, Effect, type Equivalence, Function, Predicate, Scope } from "effect" import * as React from "react" import * as Component from "./Component.js" export const AsyncTypeId: unique symbol = Symbol.for("@effect-fc/Async/Async") export type AsyncTypeId = typeof AsyncTypeId /** * A trait for `Component`'s that allows them running asynchronous effects. */ export interface Async extends AsyncPrototype, AsyncOptions {} export interface AsyncPrototype { 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. */ export type AsyncProps = Omit export const AsyncPrototype: AsyncPrototype = Object.freeze({ [AsyncTypeId]: AsyncTypeId, asFunctionComponent

( this: Component.Component & Async, contextRef: React.RefObject>>, ) { const Inner = (props: { readonly promise: Promise }) => React.use(props.promise) return ({ fallback, name, ...props }: AsyncProps) => { const promise = Effect.runPromiseWith(contextRef.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. * Used by default by async components with `Memoized.memoized` applied. */ 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 } export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, AsyncTypeId) /** * Converts a Component into an `Async` component that supports running asynchronous effects. * * 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 A new `Async` component with the same body, error, and context types as the input * * @example * ```ts * const MyAsyncComponent = MyComponent.pipe( * Async.async, * ) * ``` */ 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, Component.Component.DefaultSignature & AsyncProps, Component.Component.Success> > & 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 * // Curried * const MyAsyncComponent = MyComponent.pipe( * Async.async, * Async.withOptions({ defaultFallback:

Loading...

}), * ) * * // Uncurried * const MyAsyncComponent = Async.withOptions( * Async.async(MyComponent), * { defaultFallback:

Loading...

}, * ) * ``` */ 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), ))