Compare commits
21 Commits
next
...
5fb22ad183
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fb22ad183 | ||
|
|
8e4bcfe59d | ||
|
|
c158cdef19 | ||
|
|
fa90d9438b | ||
|
|
f8b356ef39 | ||
|
|
d38a5a4afd | ||
|
|
53bceb3a8a | ||
|
|
d3afca85da | ||
|
|
9cfc7072df | ||
|
|
bb9c41deae | ||
|
|
41b1396a58 | ||
|
|
b2b002852c | ||
|
|
ec8f9f2ddb | ||
|
|
55ca8a0dd4 | ||
|
|
6b39671d60 | ||
|
|
bada57a591 | ||
|
|
09ed773b96 | ||
|
|
956a532195 | ||
|
|
1c1659e82c | ||
|
|
051226ebd4 | ||
|
|
35463d5607 |
@@ -17,6 +17,10 @@
|
|||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
},
|
},
|
||||||
|
"./hooks": {
|
||||||
|
"types": "./dist/hooks/index.d.ts",
|
||||||
|
"default": "./dist/hooks/index.js"
|
||||||
|
},
|
||||||
"./types": {
|
"./types": {
|
||||||
"types": "./dist/types/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"default": "./dist/types/index.js"
|
"default": "./dist/types/index.js"
|
||||||
|
|||||||
@@ -1,26 +1,41 @@
|
|||||||
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 * as React from "react"
|
||||||
import * as Hook from "./Hook.js"
|
import { Hooks } from "./hooks/index.js"
|
||||||
import * as Memoized from "./Memoized.js"
|
import * as Memoized from "./Memoized.js"
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("effect-fc/Component")
|
export const TypeId: unique symbol = Symbol.for("effect-fc/Component")
|
||||||
export type TypeId = typeof TypeId
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
export interface Component<P extends {} = {}, E = never, R = never>
|
export interface Component<F extends FunctionComponent, E, R>
|
||||||
extends Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>, Component.Options {
|
extends
|
||||||
|
Effect.Effect<F, never, Exclude<R, Scope.Scope>>,
|
||||||
|
Component.Options
|
||||||
|
{
|
||||||
new(_: never): {}
|
new(_: never): {}
|
||||||
readonly [TypeId]: TypeId
|
readonly [TypeId]: TypeId
|
||||||
|
readonly ["~FunctionComponent"]: F
|
||||||
|
readonly ["~Props"]: FunctionComponent.Props<F>
|
||||||
|
readonly ["~Success"]: FunctionComponent.Success<F>
|
||||||
|
readonly ["~Error"]: E
|
||||||
|
readonly ["~Context"]: R
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
makeFunctionComponent(runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>, scope: Scope.Scope): React.FC<P>
|
readonly body: (props: FunctionComponent.Props<F>) => Effect.Effect<FunctionComponent.Success<F>, E, R>
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
readonly body: (props: P) => Effect.Effect<React.ReactNode, E, R>
|
makeFunctionComponent(
|
||||||
|
runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||||
|
scope: Scope.Scope,
|
||||||
|
): F
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Component {
|
export namespace Component {
|
||||||
export type Props<T> = T extends Component<infer P, infer _E, infer _R> ? P : never
|
export type FunctionComponent<T extends Component<any, any, any>> = T extends Component<infer F, infer _E, infer _R> ? F : never
|
||||||
export type Error<T> = T extends Component<infer _P, infer E, infer _R> ? E : never
|
export type Error<T extends Component<any, any, any>> = T extends Component<infer _F, infer E, infer _R> ? E : never
|
||||||
export type Context<T> = T extends Component<infer _P, infer _E, infer R> ? R : never
|
export type Context<T extends Component<any, any, any>> = T extends Component<infer _F, infer _E, infer R> ? R : never
|
||||||
|
|
||||||
|
export type AsComponent<T extends Component<any, any, any>> = Component<FunctionComponent<T>, Error<T>, Context<T>>
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
readonly displayName?: string
|
readonly displayName?: string
|
||||||
@@ -29,18 +44,27 @@ export namespace Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FunctionComponent = (...args: readonly any[]) => React.ReactNode
|
||||||
|
|
||||||
|
export namespace FunctionComponent {
|
||||||
|
export type Props<T extends FunctionComponent> = T extends (...args: readonly [infer P, ...any[]]) => infer _A ? P : never
|
||||||
|
export type Success<T extends FunctionComponent> = T extends (...args: infer _Args) => infer A ? A : never
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const ComponentProto = Object.freeze({
|
const ComponentProto = Object.freeze({
|
||||||
...Effectable.CommitPrototype,
|
...Effectable.CommitPrototype,
|
||||||
[TypeId]: TypeId,
|
[TypeId]: TypeId,
|
||||||
|
|
||||||
commit: Effect.fn("Component")(function* <P extends {}, E, R>(this: Component<P, E, R>) {
|
commit: Effect.fnUntraced(function* <F extends FunctionComponent, E, R>(
|
||||||
|
this: Component<F, E, R>
|
||||||
|
) {
|
||||||
const self = this
|
const self = this
|
||||||
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
|
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
|
||||||
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
return React.useCallback(function ScopeProvider(props: P) {
|
return React.useCallback(function ScopeProvider(props: FunctionComponent.Props<F>) {
|
||||||
const scope = Runtime.runSync(runtimeRef.current)(Hook.useScope(
|
const scope = Runtime.runSync(runtimeRef.current)(Hooks.useScope(
|
||||||
Array.from(
|
Array.from(
|
||||||
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
||||||
),
|
),
|
||||||
@@ -48,7 +72,7 @@ const ComponentProto = Object.freeze({
|
|||||||
))
|
))
|
||||||
|
|
||||||
const FC = React.useMemo(() => {
|
const FC = React.useMemo(() => {
|
||||||
const f = self.makeFunctionComponent(runtimeRef, scope)
|
const f: React.FC<FunctionComponent.Props<F>> = self.makeFunctionComponent(runtimeRef, scope)
|
||||||
f.displayName = self.displayName ?? "Anonymous"
|
f.displayName = self.displayName ?? "Anonymous"
|
||||||
return Memoized.isMemoized(self)
|
return Memoized.isMemoized(self)
|
||||||
? React.memo(f, self.propsAreEqual)
|
? React.memo(f, self.propsAreEqual)
|
||||||
@@ -59,12 +83,12 @@ const ComponentProto = Object.freeze({
|
|||||||
}, [])
|
}, [])
|
||||||
}),
|
}),
|
||||||
|
|
||||||
makeFunctionComponent <P extends {}, E, R>(
|
makeFunctionComponent<F extends FunctionComponent, E, R>(
|
||||||
this: Component<P, E, R>,
|
this: Component<F, E, R>,
|
||||||
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||||
scope: Scope.Scope,
|
scope: Scope.Scope,
|
||||||
): React.FC<P> {
|
) {
|
||||||
return (props: P) => Runtime.runSync(runtimeRef.current)(
|
return (props: FunctionComponent.Props<F>) => Runtime.runSync(runtimeRef.current)(
|
||||||
Effect.provideService(this.body(props), Scope.Scope, scope)
|
Effect.provideService(this.body(props), Scope.Scope, scope)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -78,277 +102,23 @@ const defaultOptions = {
|
|||||||
const nonReactiveTags = [Tracer.ParentSpan] as const
|
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<FunctionComponent, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||||
|
|
||||||
export namespace make {
|
export const make = <Args extends readonly any[], A extends React.ReactNode, E, R>(
|
||||||
export type Gen = {
|
body: (...args: Args) => Effect.Effect<A, E, R>
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, P extends {} = {}>(
|
): Component<(...args: Args) => A, E, R> => Object.setPrototypeOf(
|
||||||
body: (props: P) => Generator<Eff, React.ReactNode, never>,
|
|
||||||
): Component<
|
|
||||||
P,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B
|
|
||||||
): Component<P, Effect.Effect.Error<B>, Effect.Effect.Context<B>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
): Component<P, Effect.Effect.Error<C>, Effect.Effect.Context<C>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
): Component<P, Effect.Effect.Error<D>, Effect.Effect.Context<D>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
): Component<P, Effect.Effect.Error<E>, Effect.Effect.Context<E>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
): Component<P, Effect.Effect.Error<F>, Effect.Effect.Context<F>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
): Component<P, Effect.Effect.Error<G>, Effect.Effect.Context<G>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
|
||||||
): Component<P, Effect.Effect.Error<H>, Effect.Effect.Context<H>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
|
||||||
h: (_: H, props: NoInfer<P>) => I,
|
|
||||||
): Component<P, Effect.Effect.Error<I>, Effect.Effect.Context<I>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I, J extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
|
||||||
h: (_: H, props: NoInfer<P>) => I,
|
|
||||||
i: (_: I, props: NoInfer<P>) => J,
|
|
||||||
): Component<P, Effect.Effect.Error<J>, Effect.Effect.Context<J>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NonGen = {
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Eff
|
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
|
||||||
h: (_: H, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, I, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
|
||||||
h: (_: H, props: NoInfer<P>) => I,
|
|
||||||
i: (_: I, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
Object.assign(function() {}, defaultOptions, {
|
Object.assign(function() {}, defaultOptions, {
|
||||||
body: Effect.fnUntraced(body as any, ...pipeables as []),
|
body,
|
||||||
displayName: displayNameFromBody(body),
|
displayName: !String.isEmpty(body.name) ? body.name : undefined,
|
||||||
}),
|
}),
|
||||||
ComponentProto,
|
ComponentProto,
|
||||||
)
|
)
|
||||||
|
|
||||||
const displayNameFromBody = (body: Function) => !String.isEmpty(body.name) ? body.name : undefined
|
export const withFunctionComponentSignature: {
|
||||||
|
<F extends FunctionComponent>(): <T extends Component<any, any, any>>(self: T) =>
|
||||||
|
& Omit<T, keyof Component.AsComponent<T>>
|
||||||
|
& Component<F, Component.Error<T>, Component.Context<T>>
|
||||||
|
} = () => identity
|
||||||
|
|
||||||
export const withOptions: {
|
export const withOptions: {
|
||||||
<T extends Component<any, any, any>>(
|
<T extends Component<any, any, any>>(
|
||||||
@@ -367,17 +137,17 @@ export const withOptions: {
|
|||||||
))
|
))
|
||||||
|
|
||||||
export const withRuntime: {
|
export const withRuntime: {
|
||||||
<P extends {}, E, R>(
|
<F extends FunctionComponent, E, R>(
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
): (self: Component<P, E, R>) => React.FC<P>
|
): (self: Component<F, E, Types.NoInfer<R>>) => F
|
||||||
<P extends {}, E, R>(
|
<F extends FunctionComponent, E, R>(
|
||||||
self: Component<P, E, R>,
|
self: Component<F, E, Types.NoInfer<R>>,
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
): React.FC<P>
|
): F
|
||||||
} = Function.dual(2, <P extends {}, E, R>(
|
} = Function.dual(2, <F extends FunctionComponent, E, R>(
|
||||||
self: Component<P, E, R>,
|
self: Component<F, E, R>,
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
): React.FC<P> => function WithRuntime(props) {
|
) => function WithRuntime(props: FunctionComponent.Props<F>) {
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
Runtime.runSync(React.useContext(context))(self),
|
Runtime.runSync(React.useContext(context))(self),
|
||||||
props,
|
props,
|
||||||
|
|||||||
@@ -1,302 +0,0 @@
|
|||||||
import { type Context, Effect, Equivalence, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import { SetStateAction } from "./types/index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface ScopeOptions {
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const useScope: {
|
|
||||||
(
|
|
||||||
deps: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<Scope.Scope>
|
|
||||||
} = Effect.fn("useScope")(function*(deps, options) {
|
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
|
|
||||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([
|
|
||||||
Ref.make(true),
|
|
||||||
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
|
||||||
])), [])
|
|
||||||
const [scope, setScope] = React.useState(initialScope)
|
|
||||||
|
|
||||||
React.useEffect(() => Runtime.runSync(runtime)(
|
|
||||||
Effect.if(isInitialRun, {
|
|
||||||
onTrue: () => Effect.as(
|
|
||||||
Ref.set(isInitialRun, false),
|
|
||||||
() => closeScope(scope, runtime, options),
|
|
||||||
),
|
|
||||||
|
|
||||||
onFalse: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe(
|
|
||||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
|
||||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
), deps)
|
|
||||||
|
|
||||||
return scope
|
|
||||||
})
|
|
||||||
|
|
||||||
const closeScope = (
|
|
||||||
scope: Scope.CloseableScope,
|
|
||||||
runtime: Runtime.Runtime<never>,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) => {
|
|
||||||
switch (options?.finalizerExecutionMode ?? "sync") {
|
|
||||||
case "sync":
|
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const useCallbackSync: {
|
|
||||||
<Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<(...args: Args) => A, never, R>
|
|
||||||
} = Effect.fn("useCallbackSync")(function* <Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<R>()
|
|
||||||
return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useCallbackPromise: {
|
|
||||||
<Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
|
||||||
} = Effect.fn("useCallbackPromise")(function* <Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<R>()
|
|
||||||
return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useMemo: {
|
|
||||||
<A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<A, E, R>
|
|
||||||
} = Effect.fn("useMemo")(function* <A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useOnce: {
|
|
||||||
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
|
||||||
} = Effect.fn("useOnce")(function* <A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>
|
|
||||||
) {
|
|
||||||
return yield* useMemo(factory, [])
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useEffect: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fn("useEffect")(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useEffect(() => Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
|
||||||
Effect.map(({ scope }) =>
|
|
||||||
() => closeScope(scope, runtime, options)
|
|
||||||
),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useLayoutEffect: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fn("useLayoutEffect")(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useLayoutEffect(() => Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
|
||||||
Effect.map(({ scope }) =>
|
|
||||||
() => closeScope(scope, runtime, options)
|
|
||||||
),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useFork: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fn("useFork")(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const scope = Runtime.runSync(runtime)(options?.scope
|
|
||||||
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
)
|
|
||||||
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
|
||||||
return () => closeScope(scope, runtime, {
|
|
||||||
...options,
|
|
||||||
finalizerExecutionMode: options?.finalizerExecutionMode ?? "fork",
|
|
||||||
})
|
|
||||||
}, deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useContext: {
|
|
||||||
<ROut, E, RIn>(
|
|
||||||
layer: Layer.Layer<ROut, E, RIn>,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>>
|
|
||||||
} = Effect.fn("useContext")(function* <ROut, E, RIn>(
|
|
||||||
layer: Layer.Layer<ROut, E, RIn>,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) {
|
|
||||||
const scope = yield* useScope([layer], options)
|
|
||||||
|
|
||||||
return yield* useMemo(() => Effect.provideService(
|
|
||||||
Effect.provide(Effect.context<ROut>(), layer),
|
|
||||||
Scope.Scope,
|
|
||||||
scope,
|
|
||||||
), [scope])
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useRefFromReactiveValue: {
|
|
||||||
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
|
||||||
} = Effect.fn("useRefFromReactiveValue")(function*(value) {
|
|
||||||
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
|
||||||
yield* useEffect(() => Ref.set(ref, value), [value])
|
|
||||||
return ref
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useSubscribeRefs: {
|
|
||||||
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
|
||||||
...refs: Refs
|
|
||||||
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
|
|
||||||
} = Effect.fn("useSubscribeRefs")(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
|
||||||
...refs: Refs
|
|
||||||
) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
|
||||||
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
|
|
||||||
))
|
|
||||||
|
|
||||||
yield* useFork(() => pipe(
|
|
||||||
refs.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())),
|
|
||||||
streams => Stream.zipLatestAll(...streams),
|
|
||||||
Stream.runForEach(v =>
|
|
||||||
Effect.sync(() => setReactStateValue(v))
|
|
||||||
),
|
|
||||||
), refs)
|
|
||||||
|
|
||||||
return reactStateValue as any
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useRefState: {
|
|
||||||
<A>(
|
|
||||||
ref: SubscriptionRef.SubscriptionRef<A>
|
|
||||||
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
|
|
||||||
} = Effect.fn("useRefState")(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(
|
|
||||||
Stream.changesWith(ref.changes, Equivalence.strict()),
|
|
||||||
v => Effect.sync(() => setReactStateValue(v)),
|
|
||||||
), [ref])
|
|
||||||
|
|
||||||
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
|
||||||
Ref.update(ref, prevState =>
|
|
||||||
SetStateAction.value(setStateAction, prevState)
|
|
||||||
),
|
|
||||||
[ref])
|
|
||||||
|
|
||||||
return [reactStateValue, setValue]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useStreamFromReactiveValues: {
|
|
||||||
<const A extends React.DependencyList>(
|
|
||||||
values: A
|
|
||||||
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
|
||||||
} = Effect.fn("useStreamFromReactiveValues")(function* <const A extends React.DependencyList>(values: A) {
|
|
||||||
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
|
|
||||||
Effect.bind("latest", () => Ref.make(values)),
|
|
||||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
|
||||||
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
|
|
||||||
Effect.flatMap(a => Effect.map(
|
|
||||||
Stream.fromPubSub(pubsub, { scoped: true }),
|
|
||||||
s => Stream.concat(Stream.make(a), s),
|
|
||||||
)),
|
|
||||||
Stream.unwrapScoped,
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
|
|
||||||
yield* useEffect(() => Ref.set(latest, values).pipe(
|
|
||||||
Effect.andThen(PubSub.publish(pubsub, values)),
|
|
||||||
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
|
||||||
), values)
|
|
||||||
|
|
||||||
return stream
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useSubscribeStream: {
|
|
||||||
<A, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>
|
|
||||||
): Effect.Effect<Option.Option<A>, never, R>
|
|
||||||
<A extends NonNullable<unknown>, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue: A,
|
|
||||||
): Effect.Effect<Option.Some<A>, never, R>
|
|
||||||
} = Effect.fn("useSubscribeStream")(function* <A extends NonNullable<unknown>, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue?: A,
|
|
||||||
) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(
|
|
||||||
React.useMemo(() => initialValue
|
|
||||||
? Option.some(initialValue)
|
|
||||||
: Option.none(),
|
|
||||||
[])
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(
|
|
||||||
Stream.changesWith(stream, Equivalence.strict()),
|
|
||||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
|
||||||
), [stream])
|
|
||||||
|
|
||||||
return reactStateValue as Option.Some<A>
|
|
||||||
})
|
|
||||||
@@ -25,22 +25,22 @@ export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasP
|
|||||||
|
|
||||||
export const memo = <T extends Component.Component<any, any, any>>(
|
export const memo = <T extends Component.Component<any, any, any>>(
|
||||||
self: T
|
self: T
|
||||||
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
): T & Memoized<Component.FunctionComponent.Props<Component.Component.FunctionComponent<T>>> => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, self, MemoizedProto),
|
Object.assign(function() {}, self),
|
||||||
Object.getPrototypeOf(self),
|
Object.freeze({ ...Object.getPrototypeOf(self), ...MemoizedProto }),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const withOptions: {
|
export const withOptions: {
|
||||||
<T extends Component.Component<any, any, any> & Memoized<any>>(
|
<T extends Component.Component<any, any, any> & Memoized<any>>(
|
||||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>
|
options: Partial<Memoized.Options<Component.FunctionComponent.Props<Component.Component.FunctionComponent<T>>>>
|
||||||
): (self: T) => T
|
): (self: T) => T
|
||||||
<T extends Component.Component<any, any, any> & Memoized<any>>(
|
<T extends Component.Component<any, any, any> & Memoized<any>>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
options: Partial<Memoized.Options<Component.FunctionComponent.Props<Component.Component.FunctionComponent<T>>>>,
|
||||||
): T
|
): T
|
||||||
} = Function.dual(2, <T extends Component.Component<any, any, any> & Memoized<any>>(
|
} = Function.dual(2, <T extends Component.Component<any, any, any> & Memoized<any>>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
options: Partial<Memoized.Options<Component.FunctionComponent.Props<Component.Component.FunctionComponent<T>>>>,
|
||||||
): T => Object.setPrototypeOf(
|
): T => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, self, options),
|
Object.assign(function() {}, self, options),
|
||||||
Object.getPrototypeOf(self),
|
Object.getPrototypeOf(self),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Effect, Function, Predicate, Runtime, Scope } from "effect"
|
import { Effect, Function, Predicate, Runtime, Scope } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import type * as Component from "./Component.js"
|
import type * as Component from "./Component.js"
|
||||||
import type { ExcludeKeys } from "./utils.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("effect-fc/Suspense")
|
export const TypeId: unique symbol = Symbol.for("effect-fc/Suspense")
|
||||||
@@ -22,16 +21,17 @@ export namespace Suspense {
|
|||||||
|
|
||||||
const SuspenseProto = Object.freeze({
|
const SuspenseProto = Object.freeze({
|
||||||
[TypeId]: TypeId,
|
[TypeId]: TypeId,
|
||||||
makeFunctionComponent(
|
|
||||||
this: Component.Component<any, any, any> & Suspense,
|
makeFunctionComponent<F extends Component.FunctionComponent, E, R>(
|
||||||
runtimeRef: React.RefObject<Runtime.Runtime<any>>,
|
this: Component.Component<F, E, R> & Suspense,
|
||||||
|
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||||
scope: Scope.Scope,
|
scope: Scope.Scope,
|
||||||
): React.FC<any> {
|
) {
|
||||||
const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
||||||
|
|
||||||
return ({ fallback, name, ...props }: Suspense.Props) => {
|
return ({ fallback, name, ...props }: Suspense.Props) => {
|
||||||
const promise = Runtime.runPromise(runtimeRef.current)(
|
const promise = Runtime.runPromise(runtimeRef.current)(
|
||||||
Effect.provideService(this.body(props), Scope.Scope, scope)
|
Effect.provideService(this.body(props as Component.FunctionComponent.Props<F>), Scope.Scope, scope)
|
||||||
)
|
)
|
||||||
|
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
@@ -46,15 +46,22 @@ const SuspenseProto = Object.freeze({
|
|||||||
|
|
||||||
export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u, TypeId)
|
export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u, TypeId)
|
||||||
|
|
||||||
export const suspense = <T extends Component.Component<P, any, any>, P extends {}>(
|
export const suspense = <T extends Component.Component<any, any, any>>(
|
||||||
self: T & Component.Component<ExcludeKeys<P, keyof Suspense.Props>, any, any>
|
self: T
|
||||||
): (
|
): (
|
||||||
& Omit<T, keyof Component.Component<P, Component.Component.Error<T>, Component.Component.Context<T>>>
|
& Omit<T, keyof Component.Component.AsComponent<T>>
|
||||||
& Component.Component<P & Suspense.Props, Component.Component.Error<T>, Component.Component.Context<T>>
|
& Component.Component<
|
||||||
|
Component.Component.FunctionComponent<T> extends (...args: readonly [infer P, ...infer Args]) => infer A
|
||||||
|
? A extends React.ReactNode
|
||||||
|
? (...args: readonly [props: P & Suspense.Props, ...Args]) => A
|
||||||
|
: never
|
||||||
|
: never,
|
||||||
|
Component.Component.Error<T>,
|
||||||
|
Component.Component.Context<T>>
|
||||||
& Suspense
|
& Suspense
|
||||||
) => Object.setPrototypeOf(
|
) => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, self, SuspenseProto),
|
Object.assign(function() {}, self),
|
||||||
Object.getPrototypeOf(self),
|
Object.freeze({ ...Object.getPrototypeOf(self), ...SuspenseProto }),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const withOptions: {
|
export const withOptions: {
|
||||||
|
|||||||
7
packages/effect-fc/src/hooks/Hooks/ScopeOptions.ts
Normal file
7
packages/effect-fc/src/hooks/Hooks/ScopeOptions.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ExecutionStrategy } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export interface ScopeOptions {
|
||||||
|
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||||
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
|
}
|
||||||
15
packages/effect-fc/src/hooks/Hooks/index.ts
Normal file
15
packages/effect-fc/src/hooks/Hooks/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export * from "./ScopeOptions.js"
|
||||||
|
export * from "./useCallbackPromise.js"
|
||||||
|
export * from "./useCallbackSync.js"
|
||||||
|
export * from "./useContext.js"
|
||||||
|
export * from "./useEffect.js"
|
||||||
|
export * from "./useInput.js"
|
||||||
|
export * from "./useLayoutEffect.js"
|
||||||
|
export * from "./useMemo.js"
|
||||||
|
export * from "./useOnce.js"
|
||||||
|
export * from "./useRefFromReactiveValue.js"
|
||||||
|
export * from "./useRefState.js"
|
||||||
|
export * from "./useScope.js"
|
||||||
|
export * from "./useStreamFromReactiveValues.js"
|
||||||
|
export * from "./useSubscribeRefs.js"
|
||||||
|
export * from "./useSubscribeStream.js"
|
||||||
18
packages/effect-fc/src/hooks/Hooks/internal.ts
Normal file
18
packages/effect-fc/src/hooks/Hooks/internal.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Exit, Runtime, Scope } from "effect"
|
||||||
|
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const closeScope = (
|
||||||
|
scope: Scope.CloseableScope,
|
||||||
|
runtime: Runtime.Runtime<never>,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) => {
|
||||||
|
switch (options?.finalizerExecutionMode ?? "sync") {
|
||||||
|
case "sync":
|
||||||
|
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
||||||
|
break
|
||||||
|
case "fork":
|
||||||
|
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/effect-fc/src/hooks/Hooks/useCallbackPromise.ts
Normal file
18
packages/effect-fc/src/hooks/Hooks/useCallbackPromise.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Effect, Runtime } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const useCallbackPromise: {
|
||||||
|
<Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
||||||
|
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
) {
|
||||||
|
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
|
||||||
|
runtimeRef.current = yield* Effect.runtime<R>()
|
||||||
|
|
||||||
|
return React.useCallback((...args: Args) => Runtime.runPromise(runtimeRef.current)(callback(...args)), deps)
|
||||||
|
})
|
||||||
18
packages/effect-fc/src/hooks/Hooks/useCallbackSync.ts
Normal file
18
packages/effect-fc/src/hooks/Hooks/useCallbackSync.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Effect, Runtime } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const useCallbackSync: {
|
||||||
|
<Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.Effect<(...args: Args) => A, never, R>
|
||||||
|
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
) {
|
||||||
|
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
|
||||||
|
runtimeRef.current = yield* Effect.runtime<R>()
|
||||||
|
|
||||||
|
return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(callback(...args)), deps)
|
||||||
|
})
|
||||||
23
packages/effect-fc/src/hooks/Hooks/useContext.ts
Normal file
23
packages/effect-fc/src/hooks/Hooks/useContext.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { type Context, Effect, type Layer, Scope } from "effect"
|
||||||
|
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||||
|
import { useMemo } from "./useMemo.js"
|
||||||
|
import { useScope } from "./useScope.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useContext: {
|
||||||
|
<ROut, E, RIn>(
|
||||||
|
layer: Layer.Layer<ROut, E, RIn>,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <ROut, E, RIn>(
|
||||||
|
layer: Layer.Layer<ROut, E, RIn>,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) {
|
||||||
|
const scope = yield* useScope([layer], options)
|
||||||
|
|
||||||
|
return yield* useMemo(() => Effect.provideService(
|
||||||
|
Effect.provide(Effect.context<ROut>(), layer),
|
||||||
|
Scope.Scope,
|
||||||
|
scope,
|
||||||
|
), [scope])
|
||||||
|
})
|
||||||
28
packages/effect-fc/src/hooks/Hooks/useEffect.ts
Normal file
28
packages/effect-fc/src/hooks/Hooks/useEffect.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||||
|
import { closeScope } from "./internal.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useEffect: {
|
||||||
|
<E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
React.useEffect(() => Effect.Do.pipe(
|
||||||
|
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||||
|
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||||
|
Effect.map(({ scope }) =>
|
||||||
|
() => closeScope(scope, runtime, options)
|
||||||
|
),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
), deps)
|
||||||
|
})
|
||||||
31
packages/effect-fc/src/hooks/Hooks/useFork.ts
Normal file
31
packages/effect-fc/src/hooks/Hooks/useFork.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { closeScope } from "./internal.js"
|
||||||
|
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useFork: {
|
||||||
|
<E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||||
|
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const scope = Runtime.runSync(runtime)(options?.scope
|
||||||
|
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
|
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
|
)
|
||||||
|
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
||||||
|
return () => closeScope(scope, runtime, {
|
||||||
|
...options,
|
||||||
|
finalizerExecutionMode: options?.finalizerExecutionMode ?? "fork",
|
||||||
|
})
|
||||||
|
}, deps)
|
||||||
|
})
|
||||||
59
packages/effect-fc/src/hooks/Hooks/useInput.ts
Normal file
59
packages/effect-fc/src/hooks/Hooks/useInput.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { type Duration, Effect, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef, type Types } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { useCallbackSync } from "./useCallbackSync.js"
|
||||||
|
import { useFork } from "./useFork.js"
|
||||||
|
import { useOnce } from "./useOnce.js"
|
||||||
|
import { useSubscribeRefs } from "./useSubscribeRefs.js"
|
||||||
|
|
||||||
|
|
||||||
|
export namespace useInput {
|
||||||
|
export interface Options<A, R> {
|
||||||
|
readonly ref: SubscriptionRef.SubscriptionRef<A>
|
||||||
|
readonly schema: Schema.Schema<Types.NoInfer<A>, string, R>
|
||||||
|
readonly debounce?: Duration.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
readonly value: string
|
||||||
|
readonly onChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
readonly error: Option.Option<ParseResult.ParseError>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInput: {
|
||||||
|
<A, R>(options: useInput.Options<A, R>): Effect.Effect<useInput.Result, ParseResult.ParseError, R>
|
||||||
|
} = Effect.fnUntraced(function* <A, R>(options: useInput.Options<A, R>) {
|
||||||
|
const internalRef = yield* useOnce(() => options.ref.pipe(
|
||||||
|
Effect.andThen(Schema.encode(options.schema)),
|
||||||
|
Effect.andThen(SubscriptionRef.make),
|
||||||
|
))
|
||||||
|
const [error, setError] = React.useState(Option.none<ParseResult.ParseError>())
|
||||||
|
|
||||||
|
yield* useFork(() => Effect.all([
|
||||||
|
Stream.runForEach(options.ref.changes, upstreamValue =>
|
||||||
|
Effect.andThen(internalRef, internalValue =>
|
||||||
|
upstreamValue !== internalValue
|
||||||
|
? Effect.andThen(Schema.encode(options.schema)(upstreamValue), v => Ref.set(internalRef, v))
|
||||||
|
: Effect.void
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
Stream.runForEach(
|
||||||
|
internalRef.changes.pipe(options.debounce ? Stream.debounce(options.debounce) : identity),
|
||||||
|
flow(
|
||||||
|
Schema.decode(options.schema),
|
||||||
|
Effect.andThen(v => Ref.set(options.ref, v)),
|
||||||
|
Effect.andThen(() => setError(Option.none())),
|
||||||
|
Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, internalRef])
|
||||||
|
|
||||||
|
const [value] = yield* useSubscribeRefs(internalRef)
|
||||||
|
const onChange = yield* useCallbackSync((e: React.ChangeEvent<HTMLInputElement>) => Ref.set(
|
||||||
|
internalRef,
|
||||||
|
e.target.value,
|
||||||
|
), [internalRef])
|
||||||
|
|
||||||
|
return { value, onChange, error }
|
||||||
|
})
|
||||||
28
packages/effect-fc/src/hooks/Hooks/useLayoutEffect.ts
Normal file
28
packages/effect-fc/src/hooks/Hooks/useLayoutEffect.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||||
|
import { closeScope } from "./internal.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useLayoutEffect: {
|
||||||
|
<E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => Effect.Do.pipe(
|
||||||
|
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||||
|
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||||
|
Effect.map(({ scope }) =>
|
||||||
|
() => closeScope(scope, runtime, options)
|
||||||
|
),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
), deps)
|
||||||
|
})
|
||||||
16
packages/effect-fc/src/hooks/Hooks/useMemo.ts
Normal file
16
packages/effect-fc/src/hooks/Hooks/useMemo.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Effect, Runtime } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const useMemo: {
|
||||||
|
<A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.Effect<A, E, R>
|
||||||
|
} = Effect.fnUntraced(function* <A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
|
||||||
|
})
|
||||||
11
packages/effect-fc/src/hooks/Hooks/useOnce.ts
Normal file
11
packages/effect-fc/src/hooks/Hooks/useOnce.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import { useMemo } from "./useMemo.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useOnce: {
|
||||||
|
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||||
|
} = Effect.fnUntraced(function* <A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>
|
||||||
|
) {
|
||||||
|
return yield* useMemo(factory, [])
|
||||||
|
})
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Effect, Ref, SubscriptionRef } from "effect"
|
||||||
|
import { useEffect } from "./useEffect.js"
|
||||||
|
import { useOnce } from "./useOnce.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useRefFromReactiveValue: {
|
||||||
|
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
||||||
|
} = Effect.fnUntraced(function*(value) {
|
||||||
|
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
||||||
|
yield* useEffect(() => Ref.set(ref, value), [value])
|
||||||
|
return ref
|
||||||
|
})
|
||||||
28
packages/effect-fc/src/hooks/Hooks/useRefState.ts
Normal file
28
packages/effect-fc/src/hooks/Hooks/useRefState.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { SetStateAction } from "../../types/index.js"
|
||||||
|
import { useCallbackSync } from "./useCallbackSync.js"
|
||||||
|
import { useFork } from "./useFork.js"
|
||||||
|
import { useOnce } from "./useOnce.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useRefState: {
|
||||||
|
<A>(
|
||||||
|
ref: SubscriptionRef.SubscriptionRef<A>
|
||||||
|
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
|
||||||
|
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
||||||
|
|
||||||
|
yield* useFork(() => Stream.runForEach(
|
||||||
|
Stream.changesWith(ref.changes, Equivalence.strict()),
|
||||||
|
v => Effect.sync(() => setReactStateValue(v)),
|
||||||
|
), [ref])
|
||||||
|
|
||||||
|
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||||
|
Ref.update(ref, prevState =>
|
||||||
|
SetStateAction.value(setStateAction, prevState)
|
||||||
|
),
|
||||||
|
[ref])
|
||||||
|
|
||||||
|
return [reactStateValue, setValue]
|
||||||
|
})
|
||||||
36
packages/effect-fc/src/hooks/Hooks/useScope.ts
Normal file
36
packages/effect-fc/src/hooks/Hooks/useScope.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Effect, ExecutionStrategy, Ref, Runtime, Scope } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||||
|
import { closeScope } from "./internal.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useScope: {
|
||||||
|
(
|
||||||
|
deps: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
): Effect.Effect<Scope.Scope>
|
||||||
|
} = Effect.fnUntraced(function*(deps, options) {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
|
||||||
|
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([
|
||||||
|
Ref.make(true),
|
||||||
|
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
||||||
|
])), [])
|
||||||
|
const [scope, setScope] = React.useState(initialScope)
|
||||||
|
|
||||||
|
React.useEffect(() => Runtime.runSync(runtime)(
|
||||||
|
Effect.if(isInitialRun, {
|
||||||
|
onTrue: () => Effect.as(
|
||||||
|
Ref.set(isInitialRun, false),
|
||||||
|
() => closeScope(scope, runtime, options),
|
||||||
|
),
|
||||||
|
|
||||||
|
onFalse: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe(
|
||||||
|
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
||||||
|
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
), deps)
|
||||||
|
|
||||||
|
return scope
|
||||||
|
})
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Effect, PubSub, Ref, Scope, Stream } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { useEffect } from "./useEffect.js"
|
||||||
|
import { useOnce } from "./useOnce.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useStreamFromReactiveValues: {
|
||||||
|
<const A extends React.DependencyList>(
|
||||||
|
values: A
|
||||||
|
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
||||||
|
} = Effect.fnUntraced(function* <const A extends React.DependencyList>(values: A) {
|
||||||
|
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
|
||||||
|
Effect.bind("latest", () => Ref.make(values)),
|
||||||
|
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
||||||
|
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
|
||||||
|
Effect.flatMap(a => Effect.map(
|
||||||
|
Stream.fromPubSub(pubsub, { scoped: true }),
|
||||||
|
s => Stream.concat(Stream.make(a), s),
|
||||||
|
)),
|
||||||
|
Stream.unwrapScoped,
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
|
||||||
|
yield* useEffect(() => Ref.set(latest, values).pipe(
|
||||||
|
Effect.andThen(PubSub.publish(pubsub, values)),
|
||||||
|
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
||||||
|
), values)
|
||||||
|
|
||||||
|
return stream
|
||||||
|
})
|
||||||
27
packages/effect-fc/src/hooks/Hooks/useSubscribeRefs.ts
Normal file
27
packages/effect-fc/src/hooks/Hooks/useSubscribeRefs.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Effect, Equivalence, pipe, Stream, SubscriptionRef } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { useFork } from "./useFork.js"
|
||||||
|
import { useOnce } from "./useOnce.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useSubscribeRefs: {
|
||||||
|
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||||
|
...refs: Refs
|
||||||
|
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
|
||||||
|
} = Effect.fnUntraced(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||||
|
...refs: Refs
|
||||||
|
) {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
||||||
|
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
|
||||||
|
))
|
||||||
|
|
||||||
|
yield* useFork(() => pipe(
|
||||||
|
refs.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())),
|
||||||
|
streams => Stream.zipLatestAll(...streams),
|
||||||
|
Stream.runForEach(v =>
|
||||||
|
Effect.sync(() => setReactStateValue(v))
|
||||||
|
),
|
||||||
|
), refs)
|
||||||
|
|
||||||
|
return reactStateValue as any
|
||||||
|
})
|
||||||
31
packages/effect-fc/src/hooks/Hooks/useSubscribeStream.ts
Normal file
31
packages/effect-fc/src/hooks/Hooks/useSubscribeStream.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Effect, Equivalence, Option, Stream } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { useFork } from "./useFork.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useSubscribeStream: {
|
||||||
|
<A, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>
|
||||||
|
): Effect.Effect<Option.Option<A>, never, R>
|
||||||
|
<A extends NonNullable<unknown>, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
initialValue: A,
|
||||||
|
): Effect.Effect<Option.Some<A>, never, R>
|
||||||
|
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
initialValue?: A,
|
||||||
|
) {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(
|
||||||
|
React.useMemo(() => initialValue
|
||||||
|
? Option.some(initialValue)
|
||||||
|
: Option.none(),
|
||||||
|
[])
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* useFork(() => Stream.runForEach(
|
||||||
|
Stream.changesWith(stream, Equivalence.strict()),
|
||||||
|
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||||
|
), [stream])
|
||||||
|
|
||||||
|
return reactStateValue as Option.Some<A>
|
||||||
|
})
|
||||||
2
packages/effect-fc/src/hooks/index.ts
Normal file
2
packages/effect-fc/src/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./Hooks/index.js"
|
||||||
|
export * as Hooks from "./Hooks/index.js"
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
export * as Component from "./Component.js"
|
export * as Component from "./Component.js"
|
||||||
export * as Hook from "./Hook.js"
|
|
||||||
export * as Memoized from "./Memoized.js"
|
export * as Memoized from "./Memoized.js"
|
||||||
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
|
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
|
||||||
export * as Suspense from "./Suspense.js"
|
export * as Suspense from "./Suspense.js"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import * as PropertyPath from "./PropertyPath.js"
|
|||||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/types/SubscriptionSubRef")
|
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/types/SubscriptionSubRef")
|
||||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
||||||
|
|
||||||
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
export interface SubscriptionSubRef<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
|
||||||
|
extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
||||||
|
|
||||||
readonly [Unify.typeSymbol]?: unknown
|
readonly [Unify.typeSymbol]?: unknown
|
||||||
@@ -36,7 +37,8 @@ const synchronizedRefVariance = { _A: (_: any) => _ }
|
|||||||
const subscriptionRefVariance = { _A: (_: any) => _ }
|
const subscriptionRefVariance = { _A: (_: any) => _ }
|
||||||
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
||||||
|
|
||||||
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
class SubscriptionSubRefImpl<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
|
||||||
|
extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
||||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||||
readonly [Ref.RefTypeId] = refVariance
|
readonly [Ref.RefTypeId] = refVariance
|
||||||
@@ -47,9 +49,9 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
|||||||
readonly get: Effect.Effect<A>
|
readonly get: Effect.Effect<A>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
readonly parent: B,
|
||||||
readonly getter: (parentValue: B) => A,
|
readonly getter: (parentValue: Effect.Effect.Success<B>) => A,
|
||||||
readonly setter: (parentValue: B, value: A) => B,
|
readonly setter: (parentValue: Effect.Effect.Success<B>, value: A) => Effect.Effect.Success<B>,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.get = Effect.map(this.parent, this.getter)
|
this.get = Effect.map(this.parent, this.getter)
|
||||||
@@ -60,12 +62,11 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
get changes(): Stream.Stream<A> {
|
get changes(): Stream.Stream<A> {
|
||||||
return this.get.pipe(
|
return Stream.unwrap(
|
||||||
Effect.map(a => this.parent.changes.pipe(
|
Effect.map(this.get, a => Stream.concat(
|
||||||
Stream.map(this.getter),
|
Stream.make(a),
|
||||||
s => Stream.concat(Stream.make(a), s),
|
Stream.map(this.parent.changes, this.getter),
|
||||||
)),
|
))
|
||||||
Stream.unwrap,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
|||||||
|
|
||||||
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
||||||
return Effect.Do.pipe(
|
return Effect.Do.pipe(
|
||||||
Effect.bind("b", () => Ref.get(this.parent)),
|
Effect.bind("b", (): Effect.Effect<Effect.Effect.Success<B>> => this.parent),
|
||||||
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
||||||
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
||||||
Effect.map(({ ca: [c] }) => c),
|
Effect.map(({ ca: [c] }) => c),
|
||||||
@@ -84,28 +85,46 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const makeFromGetSet = <A, B>(
|
export const makeFromGetSet = <A, B extends SubscriptionRef.SubscriptionRef<any>>(
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
parent: B,
|
||||||
options: {
|
options: {
|
||||||
readonly get: (parentValue: B) => A
|
readonly get: (parentValue: Effect.Effect.Success<B>) => A
|
||||||
readonly set: (parentValue: B, value: A) => B
|
readonly set: (parentValue: Effect.Effect.Success<B>, value: A) => Effect.Effect.Success<B>
|
||||||
},
|
},
|
||||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, options.get, options.set)
|
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, options.get, options.set)
|
||||||
|
|
||||||
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
export const makeFromPath = <
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
B extends SubscriptionRef.SubscriptionRef<any>,
|
||||||
|
const P extends PropertyPath.Paths<Effect.Effect.Success<B>>,
|
||||||
|
>(
|
||||||
|
parent: B,
|
||||||
path: P,
|
path: P,
|
||||||
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
|
): SubscriptionSubRef<PropertyPath.ValueFromPath<Effect.Effect.Success<B>, P>, B> => new SubscriptionSubRefImpl(
|
||||||
parent,
|
parent,
|
||||||
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
||||||
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const makeFromChunkRef = <A>(
|
export const makeFromChunkRef: {
|
||||||
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<A>>,
|
<B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
|
||||||
|
parent: B,
|
||||||
|
index: number,
|
||||||
|
): SubscriptionSubRef<
|
||||||
|
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
|
||||||
|
B
|
||||||
|
>
|
||||||
|
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
|
||||||
|
parent: B,
|
||||||
|
index: number,
|
||||||
|
): SubscriptionSubRef<
|
||||||
|
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
|
||||||
|
B
|
||||||
|
>
|
||||||
|
} = (
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>,
|
||||||
index: number,
|
index: number,
|
||||||
): SubscriptionSubRef<A, Chunk.Chunk<A>> => new SubscriptionSubRefImpl(
|
) => new SubscriptionSubRefImpl(
|
||||||
parent,
|
parent,
|
||||||
parentValue => Chunk.unsafeGet(parentValue, index),
|
parentValue => Chunk.unsafeGet(parentValue, index),
|
||||||
(parentValue, value) => Chunk.replace(parentValue, index, value),
|
(parentValue, value) => Chunk.replace(parentValue, index, value),
|
||||||
)
|
) as any
|
||||||
|
|||||||
22
packages/example/src/lib/TextInput.tsx
Normal file
22
packages/example/src/lib/TextInput.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { TextField } from "@radix-ui/themes"
|
||||||
|
import { Effect, Schema } from "effect"
|
||||||
|
import { Component } from "effect-fc"
|
||||||
|
import { useInput } from "effect-fc/hooks"
|
||||||
|
|
||||||
|
|
||||||
|
export namespace TextInput {
|
||||||
|
export interface Props<A, R> extends Omit<useInput.Options<A, R>, "schema">, TextField.RootProps {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextInput = <A, R>(schema: Schema.Schema<A, string, R>) => Component.make(
|
||||||
|
Effect.fnUntraced(function*(props: TextInput.Props<A, R>) {
|
||||||
|
const input = yield* useInput({ ...props, schema })
|
||||||
|
return (
|
||||||
|
<TextField.Root
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
|
||||||
|
</TextField.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -2,13 +2,14 @@ import { runtime } from "@/runtime"
|
|||||||
import { Flex, Text, TextField } from "@radix-ui/themes"
|
import { Flex, Text, TextField } from "@radix-ui/themes"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
import { Effect } from "effect"
|
import { Effect, Types } from "effect"
|
||||||
import { Component, Hook, Memoized, Suspense } from "effect-fc"
|
import { Component, Memoized, Suspense } from "effect-fc"
|
||||||
|
import { Hooks } from "effect-fc/hooks"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
// Generator version
|
// Generator version
|
||||||
const RouteComponent = Component.make(function* AsyncRendering() {
|
const RouteComponent = Component.make(Effect.fnUntraced(function* AsyncRendering() {
|
||||||
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent
|
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent
|
||||||
const AsyncComponentFC = yield* AsyncComponent
|
const AsyncComponentFC = yield* AsyncComponent
|
||||||
const [input, setInput] = React.useState("")
|
const [input, setInput] = React.useState("")
|
||||||
@@ -24,7 +25,7 @@ const RouteComponent = Component.make(function* AsyncRendering() {
|
|||||||
<AsyncComponentFC />
|
<AsyncComponentFC />
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}).pipe(
|
})).pipe(
|
||||||
Component.withRuntime(runtime.context)
|
Component.withRuntime(runtime.context)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ const RouteComponent = Component.make(function* AsyncRendering() {
|
|||||||
// )
|
// )
|
||||||
|
|
||||||
|
|
||||||
class AsyncComponent extends Component.make(function* AsyncComponent() {
|
class AsyncComponent extends Component.make(Effect.fnUntraced(function* AsyncComponent() {
|
||||||
const SubComponentFC = yield* SubComponent
|
const SubComponentFC = yield* SubComponent
|
||||||
|
|
||||||
yield* Effect.sleep("500 millis") // Async operation
|
yield* Effect.sleep("500 millis") // Async operation
|
||||||
@@ -62,16 +63,18 @@ class AsyncComponent extends Component.make(function* AsyncComponent() {
|
|||||||
<SubComponentFC />
|
<SubComponentFC />
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}).pipe(
|
})).pipe(
|
||||||
Suspense.suspense,
|
// Suspense.suspense,
|
||||||
Suspense.withOptions({ defaultFallback: <p>Loading...</p> }),
|
// Suspense.withOptions({ defaultFallback: <p>Loading...</p> }),
|
||||||
) {}
|
) {}
|
||||||
|
const AsyncComponent2 = Suspense.withOptions(Suspense.suspense(AsyncComponent), {})
|
||||||
|
type T = Types.Simplify<typeof AsyncComponent2>
|
||||||
class MemoizedAsyncComponent extends Memoized.memo(AsyncComponent) {}
|
class MemoizedAsyncComponent extends Memoized.memo(AsyncComponent) {}
|
||||||
|
|
||||||
class SubComponent extends Component.make(function* SubComponent() {
|
class SubComponent extends Component.make(Effect.fnUntraced(function* SubComponent() {
|
||||||
const [state] = React.useState(yield* Hook.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
const [state] = React.useState(yield* Hooks.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
||||||
return <Text>{state}</Text>
|
return <Text>{state}</Text>
|
||||||
}) {}
|
})) {}
|
||||||
|
|
||||||
export const Route = createFileRoute("/dev/async-rendering")({
|
export const Route = createFileRoute("/dev/async-rendering")({
|
||||||
component: RouteComponent
|
component: RouteComponent
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Component, Memoized } from "effect-fc"
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
const RouteComponent = Component.make(function* RouteComponent() {
|
const RouteComponent = Component.make(Effect.fnUntraced(function* RouteComponent() {
|
||||||
const [value, setValue] = React.useState("")
|
const [value, setValue] = React.useState("")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,14 +21,14 @@ const RouteComponent = Component.make(function* RouteComponent() {
|
|||||||
{yield* Effect.map(MemoizedSubComponent, FC => <FC />)}
|
{yield* Effect.map(MemoizedSubComponent, FC => <FC />)}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}).pipe(
|
})).pipe(
|
||||||
Component.withRuntime(runtime.context)
|
Component.withRuntime(runtime.context)
|
||||||
)
|
)
|
||||||
|
|
||||||
class SubComponent extends Component.make(function* SubComponent() {
|
class SubComponent extends Component.make(Effect.fnUntraced(function* SubComponent() {
|
||||||
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
|
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
|
||||||
return <Text>{id}</Text>
|
return <Text>{id}</Text>
|
||||||
}) {}
|
})) {}
|
||||||
|
|
||||||
class MemoizedSubComponent extends Memoized.memo(SubComponent) {}
|
class MemoizedSubComponent extends Memoized.memo(SubComponent) {}
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ import { Todos } from "@/todo/Todos"
|
|||||||
import { TodosState } from "@/todo/TodosState.service"
|
import { TodosState } from "@/todo/TodosState.service"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Component, Hook } from "effect-fc"
|
import { Component } from "effect-fc"
|
||||||
|
import { Hooks } from "effect-fc/hooks"
|
||||||
|
|
||||||
|
|
||||||
const TodosStateLive = TodosState.Default("todos")
|
const TodosStateLive = TodosState.Default("todos")
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: Component.make(function* Index() {
|
component: Component.make(Effect.fnUntraced(function* Index() {
|
||||||
return yield* Todos.pipe(
|
return yield* Todos.pipe(
|
||||||
Effect.map(FC => <FC />),
|
Effect.map(FC => <FC />),
|
||||||
Effect.provide(yield* Hook.useContext(TodosStateLive, { finalizerExecutionMode: "fork" })),
|
Effect.provide(yield* Hooks.useContext(TodosStateLive, { finalizerExecutionMode: "fork" })),
|
||||||
)
|
)
|
||||||
}).pipe(
|
})).pipe(
|
||||||
Component.withRuntime(runtime.context)
|
Component.withRuntime(runtime.context)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import * as Domain from "@/domain"
|
import * as Domain from "@/domain"
|
||||||
import { Box, Button, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
import { Box, Button, Callout, Flex, IconButton, Text, TextArea } from "@radix-ui/themes"
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
import { Chunk, Effect, Match, Option, Ref, Runtime, SubscriptionRef } from "effect"
|
import { Chunk, Effect, Match, Option, ParseResult, Ref, Runtime, Schema, SubscriptionRef } from "effect"
|
||||||
import { Component, Hook, Memoized } from "effect-fc"
|
import { Component, Memoized } from "effect-fc"
|
||||||
|
import { Hooks } from "effect-fc/hooks"
|
||||||
import { SubscriptionSubRef } from "effect-fc/types"
|
import { SubscriptionSubRef } from "effect-fc/types"
|
||||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||||
import { FaDeleteLeft } from "react-icons/fa6"
|
import { FaDeleteLeft } from "react-icons/fa6"
|
||||||
@@ -24,11 +25,11 @@ export type TodoProps = (
|
|||||||
| { readonly _tag: "edit", readonly index: number }
|
| { readonly _tag: "edit", readonly index: number }
|
||||||
)
|
)
|
||||||
|
|
||||||
export class Todo extends Component.make(function* Todo(props: TodoProps) {
|
export class Todo extends Component.make(Effect.fnUntraced(function* Todo(props: TodoProps) {
|
||||||
const runtime = yield* Effect.runtime()
|
const runtime = yield* Effect.runtime()
|
||||||
const state = yield* TodosState
|
const state = yield* TodosState
|
||||||
|
|
||||||
const [ref, contentRef] = yield* Hook.useMemo(() => Match.value(props).pipe(
|
const [ref, contentRef] = yield* Hooks.useMemo(() => Match.value(props).pipe(
|
||||||
Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)),
|
Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)),
|
||||||
Match.tag("edit", ({ index }) => Effect.succeed(SubscriptionSubRef.makeFromChunkRef(state.ref, index))),
|
Match.tag("edit", ({ index }) => Effect.succeed(SubscriptionSubRef.makeFromChunkRef(state.ref, index))),
|
||||||
Match.exhaustive,
|
Match.exhaustive,
|
||||||
@@ -39,15 +40,26 @@ export class Todo extends Component.make(function* Todo(props: TodoProps) {
|
|||||||
] as const),
|
] as const),
|
||||||
), [props._tag, props.index])
|
), [props._tag, props.index])
|
||||||
|
|
||||||
const [content, size] = yield* Hook.useSubscribeRefs(contentRef, state.sizeRef)
|
const [size] = yield* Hooks.useSubscribeRefs(state.sizeRef)
|
||||||
|
const contentInput = yield* Hooks.useInput({ ref: contentRef, schema: Schema.Any })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" align="stretch" gap="2">
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
|
{Option.isSome(contentInput.error) &&
|
||||||
|
<Callout.Root color="red">
|
||||||
|
<Callout.Text>
|
||||||
|
{ParseResult.ArrayFormatter.formatErrorSync(contentInput.error.value).map(e => <>
|
||||||
|
<Text>• {e.message}</Text><br />
|
||||||
|
</>)}
|
||||||
|
</Callout.Text>
|
||||||
|
</Callout.Root>
|
||||||
|
}
|
||||||
|
|
||||||
<Flex direction="row" align="center" gap="2">
|
<Flex direction="row" align="center" gap="2">
|
||||||
<Box flexGrow="1">
|
<Box flexGrow="1">
|
||||||
<TextArea
|
<TextArea
|
||||||
value={content}
|
value={contentInput.value}
|
||||||
onChange={e => Runtime.runSync(runtime)(Ref.set(contentRef, e.target.value))}
|
onChange={contentInput.onChange}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -110,6 +122,6 @@ export class Todo extends Component.make(function* Todo(props: TodoProps) {
|
|||||||
}
|
}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}).pipe(
|
})).pipe(
|
||||||
Memoized.memo
|
Memoized.memo
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { Container, Flex, Heading } from "@radix-ui/themes"
|
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||||
import { Chunk, Console, Effect } from "effect"
|
import { Chunk, Console, Effect } from "effect"
|
||||||
import { Component, Hook } from "effect-fc"
|
import { Component } from "effect-fc"
|
||||||
|
import { Hooks } from "effect-fc/hooks"
|
||||||
import { Todo } from "./Todo"
|
import { Todo } from "./Todo"
|
||||||
import { TodosState } from "./TodosState.service"
|
import { TodosState } from "./TodosState.service"
|
||||||
|
|
||||||
|
|
||||||
export class Todos extends Component.make(function* Todos() {
|
export class Todos extends Component.make(Effect.fnUntraced(function* Todos() {
|
||||||
const state = yield* TodosState
|
const state = yield* TodosState
|
||||||
const [todos] = yield* Hook.useSubscribeRefs(state.ref)
|
const [todos] = yield* Hooks.useSubscribeRefs(state.ref)
|
||||||
|
|
||||||
yield* Hook.useOnce(() => Effect.andThen(
|
yield* Hooks.useOnce(() => Effect.andThen(
|
||||||
Console.log("Todos mounted"),
|
Console.log("Todos mounted"),
|
||||||
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||||
))
|
))
|
||||||
@@ -29,4 +30,4 @@ export class Todos extends Component.make(function* Todos() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}) {}
|
})) {}
|
||||||
|
|||||||
Reference in New Issue
Block a user