@@ -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