Component v4 migration work
Lint / lint (push) Failing after 16s

This commit is contained in:
Julien Valverdé
2026-06-22 06:36:32 +02:00
parent 091e102b23
commit a38be0b172
2 changed files with 1354 additions and 224 deletions
@@ -0,0 +1,385 @@
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
import {
type 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<P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>
extends ComponentPrototype<R, F>, ComponentOptions {
new(_: never): Record<string, never>
readonly [ComponentTypeId]: ComponentTypeId
readonly "~Props": P
readonly "~Success": A
readonly "~Error": E
readonly "~Context": R
readonly "~Function": F
readonly body: (props: P) => Effect.Effect<A, E, R>
}
export declare namespace Component {
export type Default<P extends {}, A extends React.ReactNode, E, R> = Component<P, A, E, R, DefaultSignature<P, A>>
export type Any = Component<any, any, any, any, any>
export type Signature = (props: any) => React.ReactNode
export type DefaultSignature<P extends {}, A extends React.ReactNode> = (props: P) => A
export type Props<T extends Any> = T["~Props"]
export type Success<T extends Any> = T["~Success"]
export type Error<T extends Any> = T["~Error"]
export type Context<T extends Any> = T["~Context"]
export type Function<T extends Any> = T["~Function"]
export type AsComponent<T extends Any> = Component<Props<T>, Success<T>, Error<T>, Context<T>, Function<T>>
}
export interface ComponentOptions {
readonly displayName?: string
readonly nonReactiveTags: readonly Context.Key<any, any>[]
readonly finalizerExecutionStrategy: "sequential" | "parallel"
readonly finalizerExecutionDebounce: Duration.Input
}
export const defaultOptions: ComponentOptions = {
nonReactiveTags: [Tracer.ParentSpan],
finalizerExecutionStrategy: "sequential",
finalizerExecutionDebounce: "100 millis",
}
export interface ComponentPrototype<R, F extends Component.Signature> extends Pipeable.Pipeable {
readonly [ComponentTypeId]: ComponentTypeId
readonly use: Effect.Effect<F, never, Exclude<R, Scope.Scope>>
}
type ComponentImpl = Component<any, React.ReactNode, any, any, Component.Signature>
const makeFunctionComponent = (
self: ComponentImpl,
contextRef: React.RefObject<Context.Context<any>>,
): 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<any>()
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<ComponentImpl, {
readonly contextRef: { current: Context.Context<any> }
readonly component: Component.Signature
}>()
export const ComponentPrototype = Object.freeze({
[ComponentTypeId]: ComponentTypeId,
...Pipeable.Prototype,
get use() {
return use(this as ComponentImpl)
},
}) as unknown as ComponentPrototype<any, any>
export const isComponent = (u: unknown): u is Component.Any => Predicate.hasProperty(u, ComponentTypeId)
type GeneratorBody<P extends {}, A extends React.ReactNode, E, R> = (
props: P,
) => Effect.fn.Return<A, E, R>
type EffectBody<P extends {}, A extends React.ReactNode, E, R> = (
props: P,
) => Effect.Effect<A, E, R>
export interface Make {
<P extends {}, A extends React.ReactNode, E = never, R = never>(
body: GeneratorBody<P, A, E, R> | EffectBody<P, A, E, R>,
...pipeables: readonly Function[]
): Component.Default<P, A, E, R>
(name: string, options?: Tracer.SpanOptionsNoTrace): <P extends {}, A extends React.ReactNode, E = never, R = never>(
body: GeneratorBody<P, A, E, R> | EffectBody<P, A, E, R>,
...pipeables: readonly Function[]
) => Component.Default<P, A, E, R>
}
const component = (
body: Function,
displayName: string | undefined,
traced: boolean,
pipeables: readonly Function[],
): Component.Any => Object.setPrototypeOf(
Object.assign(() => {}, 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<T extends Component.Any, F extends Component.Signature> = (
& Omit<T, keyof Component.AsComponent<T>>
& Component<Component.Props<T>, Component.Success<T>, Component.Error<T>, Component.Context<T>, F>
)
}
export const withSignature: {
<F extends Component.Signature>(): <T extends Component.Any>(self: T) => withSignature.Result<T, F>
<F extends Component.Signature, T extends Component.Any>(self: T): withSignature.Result<T, F>
} = (self?: Component.Any): any => self === undefined ? identity : self
export const withOptions: {
<T extends Component.Any>(options: Partial<ComponentOptions>): (self: T) => T
<T extends Component.Any>(self: T, options: Partial<ComponentOptions>): T
} = Function.dual(2, <T extends Component.Any>(self: T, options: Partial<ComponentOptions>): T => Object.setPrototypeOf(
Object.assign(() => {}, self, options),
Object.getPrototypeOf(self),
))
export const withRuntime: {
<P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
context: React.Context<Context.Context<R>>,
): (self: Component<P, A, E, Scope.Scope | NoInfer<R>, F>) => F
<P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
self: Component<P, A, E, Scope.Scope | NoInfer<R>, F>,
context: React.Context<Context.Context<R>>,
): F
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
self: Component<P, A, E, Scope.Scope | R, F>,
context: React.Context<Context.Context<R>>,
) => function WithRuntime(props: P) {
return React.createElement(
Effect.runSyncWith(React.useContext(context))(self.use) as React.FC<P>,
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<Scope.Scope> {
const context = yield* Effect.context<never>()
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<object, ReturnType<typeof setTimeout>>()
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* <A, E, R>(
f: () => Effect.Effect<A, E, R>,
): Effect.fn.Return<A, E, R> {
const context = yield* Effect.context<R>()
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<A, E, R>
})
const mountCache = new Map<string, {
readonly effect: Effect.Effect<unknown, unknown, unknown>
cleanup?: ReturnType<typeof setTimeout>
}>()
export declare namespace useOnChange {
export interface Options extends useScope.Options {}
}
export const useOnChange = Effect.fnUntraced(function* <A, E, R>(
f: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
options?: useOnChange.Options,
): Effect.fn.Return<A, E, Exclude<R, Scope.Scope>> {
const context = yield* Effect.context<Exclude<R, Scope.Scope>>()
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 = <E, R>(
context: Context.Context<Exclude<R, Scope.Scope>>,
f: () => Effect.Effect<void, E, R>,
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* <E, R>(
f: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: useReactEffect.Options,
): Effect.fn.Return<void, never, Exclude<R, Scope.Scope>> {
const context = yield* Effect.context<Exclude<R, Scope.Scope>>()
// 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* <E, R>(
f: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: useReactLayoutEffect.Options,
): Effect.fn.Return<void, never, Exclude<R, Scope.Scope>> {
const context = yield* Effect.context<Exclude<R, Scope.Scope>>()
// biome-ignore lint/correctness/useExhaustiveDependencies: caller supplies React dependencies
React.useLayoutEffect(() => runReactEffect(context, f, options), deps)
})
export const useRunSync = <R = never>(): Effect.Effect<
<A, E = never>(effect: Effect.Effect<A, E, R>) => A,
never,
R
> => Effect.map(Effect.context<R>(), Effect.runSyncWith)
export const useRunPromise = <R = never>(): Effect.Effect<
<A, E = never>(effect: Effect.Effect<A, E, R>) => Promise<A>,
never,
R
> => Effect.map(Effect.context<R>(), Effect.runPromiseWith)
export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
f: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.fn.Return<(...args: Args) => A, never, R> {
const context = yield* Effect.context<R>()
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* <Args extends unknown[], A, E, R>(
f: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.fn.Return<(...args: Args) => Promise<A>, never, R> {
const context = yield* Effect.context<R>()
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 = <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>,
options?: useContext.Options,
): Effect.Effect<Context.Context<ROut>, E, RIn | Scope.Scope> => useOnChange(
() => Effect.flatMap(
Effect.context<RIn>(),
context => Layer.build(Layer.provide(layer, Layer.succeedContext(context))),
),
[layer],
options,
)
File diff suppressed because it is too large Load Diff