From 9cfc7072dffafedc496f85e03a92ba6570c82e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 7 Aug 2025 03:05:18 +0200 Subject: [PATCH] Component refactoring --- packages/effect-fc/src/Component.ts | 346 +++++----------------------- 1 file changed, 58 insertions(+), 288 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 36205bf..9843bb8 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -1,4 +1,4 @@ -import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" +import { Context, Effect, Effectable, ExecutionStrategy, Function, identity, Predicate, Runtime, Scope, String, Tracer, type Types } from "effect" import * as React from "react" import { Hooks } from "./hooks/index.js" import * as Memoized from "./Memoized.js" @@ -7,20 +7,35 @@ import * as Memoized from "./Memoized.js" export const TypeId: unique symbol = Symbol.for("effect-fc/Component") export type TypeId = typeof TypeId -export interface Component

-extends Effect.Effect, never, Exclude>, Component.Options { +export interface Component +extends + Effect.Effect>, + Component.Options +{ new(_: never): {} readonly [TypeId]: TypeId + readonly ["~FunctionComponent"]: F + readonly ["~Props"]: FunctionComponent.Props + readonly ["~Success"]: FunctionComponent.Success + readonly ["~Error"]: E + readonly ["~Context"]: R + /** @internal */ - makeFunctionComponent(runtimeRef: React.Ref>>, scope: Scope.Scope): React.FC

+ readonly body: (props: FunctionComponent.Props) => Effect.Effect, E, R> + /** @internal */ - readonly body: (props: P) => Effect.Effect + makeFunctionComponent( + runtimeRef: React.Ref>>, + scope: Scope.Scope, + ): F } export namespace Component { - export type Props = T extends Component ? P : never - export type Error = T extends Component ? E : never - export type Context = T extends Component ? R : never + export type FunctionComponent> = T extends Component ? F : never + export type Error> = T extends Component ? E : never + export type Context> = T extends Component ? R : never + + export type AsComponent> = Component, Error, Context> export interface Options { readonly displayName?: string @@ -29,17 +44,26 @@ export namespace Component { } } +export type FunctionComponent = (...args: readonly any[]) => React.ReactNode + +export namespace FunctionComponent { + export type Props = T extends (...args: readonly [infer P, ...any[]]) => infer _A ? P : never + export type Success = T extends (...args: infer _Args) => infer A ? A : never +} + const ComponentProto = Object.freeze({ ...Effectable.CommitPrototype, [TypeId]: TypeId, - commit: Effect.fnUntraced(function*

(this: Component) { + commit: Effect.fnUntraced(function* ( + this: Component + ) { const self = this const runtimeRef = React.useRef>>(null!) runtimeRef.current = yield* Effect.runtime>() - return React.useCallback(function ScopeProvider(props: P) { + return React.useCallback(function ScopeProvider(props: FunctionComponent.Props) { const scope = Runtime.runSync(runtimeRef.current)(Hooks.useScope( Array.from( Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values() @@ -48,7 +72,7 @@ const ComponentProto = Object.freeze({ )) const FC = React.useMemo(() => { - const f = self.makeFunctionComponent(runtimeRef, scope) + const f: React.FC> = self.makeFunctionComponent(runtimeRef, scope) f.displayName = self.displayName ?? "Anonymous" return Memoized.isMemoized(self) ? React.memo(f, self.propsAreEqual) @@ -59,12 +83,12 @@ const ComponentProto = Object.freeze({ }, []) }), - makeFunctionComponent

( - this: Component, + makeFunctionComponent( + this: Component, runtimeRef: React.RefObject>>, scope: Scope.Scope, - ): React.FC

{ - return (props: P) => Runtime.runSync(runtimeRef.current)( + ) { + return (props: FunctionComponent.Props) => Runtime.runSync(runtimeRef.current)( Effect.provideService(this.body(props), Scope.Scope, scope) ) }, @@ -78,277 +102,23 @@ const defaultOptions = { const nonReactiveTags = [Tracer.ParentSpan] as const -export const isComponent = (u: unknown): u is Component<{}, unknown, unknown> => Predicate.hasProperty(u, TypeId) +export const isComponent = (u: unknown): u is Component => Predicate.hasProperty(u, TypeId) -export namespace make { - export type Gen = { - >, P extends {} = {}>( - body: (props: P) => Generator, - ): Component< - P, - [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.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.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.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.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.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.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.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.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.Context> - } - - export type NonGen = { - , P extends {} = {}>( - body: (props: P) => Eff - ): Component, Effect.Effect.Context> - , A, P extends {} = {}>( - body: (props: P) => A, - a: (_: A, props: NoInfer

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

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

) => Eff, - ): Component, 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.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.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.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.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.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.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.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") { - const displayName = displayNameFromBody(spanNameOrBody) - return Object.setPrototypeOf( - Object.assign(function() {}, defaultOptions, { - body: displayName - ? Effect.fn(displayName)(spanNameOrBody as any, ...pipeables as []) - : Effect.fn(spanNameOrBody as any, ...pipeables), - displayName, - }), - 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: displayNameFromBody(body) ?? spanNameOrBody, - }), - ComponentProto, - ) - } -} - -export const makeUntraced: make.Gen & make.NonGen = ( - body: Function, - ...pipeables: any[] -) => Object.setPrototypeOf( +export const make = ( + body: (...args: Args) => Effect.Effect +): Component<(...args: Args) => A, E, R> => Object.setPrototypeOf( Object.assign(function() {}, defaultOptions, { - body: Effect.fnUntraced(body as any, ...pipeables as []), - displayName: displayNameFromBody(body), + body, + displayName: !String.isEmpty(body.name) ? body.name : undefined, }), ComponentProto, ) -const displayNameFromBody = (body: Function) => !String.isEmpty(body.name) ? body.name : undefined +export const withFunctionComponentSignature: { + (): >(self: T) => + & Omit> + & Component, Component.Context> +} = () => identity export const withOptions: { >( @@ -367,17 +137,17 @@ export const withOptions: { )) export const withRuntime: { -

( + ( context: React.Context>, - ): (self: Component) => React.FC

-

( - self: Component, + ): (self: Component>) => F + ( + self: Component>, context: React.Context>, - ): React.FC

-} = Function.dual(2,

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

=> function WithRuntime(props) { +) => function WithRuntime(props: FunctionComponent.Props) { return React.createElement( Runtime.runSync(React.useContext(context))(self), props,