@@ -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
Reference in New Issue
Block a user