Compare commits
23 Commits
782629d5b3
...
reffuse-ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a78232ec7 | ||
|
|
19194d6677 | ||
|
|
40871b793d | ||
|
|
f079b90f28 | ||
|
|
28b6e9276e | ||
|
|
8025ec4a22 | ||
|
|
02ee2c10cc | ||
|
|
bb1a71f63b | ||
|
|
a9448f55cf | ||
|
|
c0f3073d20 | ||
|
|
8cfe186574 | ||
|
|
625cecda27 | ||
|
|
7cc0a68170 | ||
|
|
8be1295e2f | ||
|
|
a781be8f24 | ||
|
|
4913f5cc35 | ||
|
|
2a37f843ca | ||
|
|
78a3735038 | ||
|
|
37d9400ada | ||
|
|
2ef47bed70 | ||
|
|
2b78d4dc49 | ||
|
|
6fa73ee33f | ||
|
|
3ea4c81872 |
@@ -16,6 +16,10 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/types/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./dist/*.d.ts",
|
||||
"default": "./dist/*.js"
|
||||
|
||||
@@ -1,89 +1,72 @@
|
||||
import { Context, Effect, ExecutionStrategy, Exit, Ref, Runtime, Scope, Tracer } from "effect"
|
||||
import { Context, Effect, Function, Runtime, Scope, Tracer } from "effect"
|
||||
import type { Mutable } from "effect/Types"
|
||||
import * as React from "react"
|
||||
import * as ReactHook from "./ReactHook.js"
|
||||
|
||||
|
||||
export interface ReactComponent<P, E, R> {
|
||||
export interface ReactComponent<E, R, P> {
|
||||
(props: P): Effect.Effect<React.ReactNode, E, R>
|
||||
readonly displayName?: string
|
||||
}
|
||||
|
||||
export const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||
|
||||
export const withDisplayName: {
|
||||
<C extends ReactComponent<any, any, any>>(displayName: string): (self: C) => C
|
||||
<C extends ReactComponent<any, any, any>>(self: C, displayName: string): C
|
||||
} = Function.dual(2, <C extends ReactComponent<any, any, any>>(
|
||||
self: C,
|
||||
displayName: string,
|
||||
): C => {
|
||||
(self as Mutable<C>).displayName = displayName
|
||||
return self
|
||||
})
|
||||
|
||||
export const useFC: {
|
||||
<P, E, R>(
|
||||
self: ReactComponent<P, E, R>,
|
||||
<E, R, P extends {} = {}>(
|
||||
self: ReactComponent<E, R, P>,
|
||||
options?: ReactHook.ScopeOptions,
|
||||
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fn(function* useFC<P, E, R>(
|
||||
self: ReactComponent<P, E, R>,
|
||||
} = Effect.fnUntraced(function* <E, R, P extends {}>(
|
||||
self: ReactComponent<E, R, P>,
|
||||
options?: ReactHook.ScopeOptions,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
return React.useCallback((props: P) => Runtime.runSync(runtime)(
|
||||
self(props) as Effect.Effect<React.ReactNode, E, Exclude<R, Scope.Scope>>
|
||||
), [])
|
||||
return React.useMemo(() => function ScopeProvider(props: P) {
|
||||
const scope = Runtime.runSync(runtime)(ReactHook.useScope(options))
|
||||
|
||||
const FC = React.useMemo(() => {
|
||||
const f = (props: P) => Runtime.runSync(runtime)(
|
||||
Effect.provideService(self(props), Scope.Scope, scope)
|
||||
)
|
||||
if (self.displayName) f.displayName = self.displayName
|
||||
return f
|
||||
}, [scope])
|
||||
|
||||
return React.createElement(FC, props)
|
||||
}, Array.from(
|
||||
Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values()
|
||||
))
|
||||
})
|
||||
|
||||
export const use: {
|
||||
<P, E, R>(
|
||||
self: ReactComponent<P, E, R>,
|
||||
<E, R, P extends {} = {}>(
|
||||
self: ReactComponent<E, R, P>,
|
||||
fn: (Component: React.FC<P>) => React.ReactNode,
|
||||
options?: ReactHook.ScopeOptions,
|
||||
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fn(function* use<P, E, R>(
|
||||
self: ReactComponent<P, E, R>,
|
||||
fn: (Component: React.FC<P>) => React.ReactNode,
|
||||
options?: ReactHook.ScopeOptions,
|
||||
) {
|
||||
} = Effect.fnUntraced(function*(self, fn, options) {
|
||||
return fn(yield* useFC(self, options))
|
||||
})
|
||||
|
||||
|
||||
const FC = <P, E, R>(
|
||||
self: ReactComponent<P, E, R>,
|
||||
runtime: Runtime.Runtime<R>,
|
||||
props: P,
|
||||
options?: ReactHook.ScopeOptions,
|
||||
): React.ReactNode => {
|
||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
|
||||
Effect.all([Ref.make(true), makeScope(options)])
|
||||
), [])
|
||||
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: () => makeScope(options).pipe(
|
||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
||||
),
|
||||
})
|
||||
), [])
|
||||
|
||||
return React.useMemo(() => Runtime.runSync(runtime)(
|
||||
Effect.provideService(self(props), Scope.Scope, scope)
|
||||
), [
|
||||
props,
|
||||
...Array.from(Context.omit(Tracer.ParentSpan)(runtime.context).unsafeMap.values()),
|
||||
])
|
||||
}
|
||||
|
||||
const makeScope = (options?: ReactHook.ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
const closeScope = (
|
||||
scope: Scope.CloseableScope,
|
||||
runtime: Runtime.Runtime<never>,
|
||||
options?: ReactHook.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 withRuntime: {
|
||||
<E, R, P extends {} = {}>(context: React.Context<Runtime.Runtime<R>>): (self: ReactComponent<E, R, P>) => React.FC<P>
|
||||
<E, R, P extends {} = {}>(self: ReactComponent<E, R, P>, context: React.Context<Runtime.Runtime<R>>): React.FC<P>
|
||||
} = Function.dual(2, <E, R, P extends {}>(
|
||||
self: ReactComponent<E, R, P>,
|
||||
context: React.Context<Runtime.Runtime<R>>,
|
||||
): React.FC<P> => function WithRuntime(props) {
|
||||
const runtime = React.useContext(context)
|
||||
return React.createElement(Runtime.runSync(runtime)(useFC(self)), props)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
|
||||
import { type Context, Effect, 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 {
|
||||
@@ -8,39 +9,121 @@ export interface ScopeOptions {
|
||||
}
|
||||
|
||||
|
||||
export const useScope: {
|
||||
(options?: ScopeOptions): Effect.Effect<Scope.Scope>
|
||||
} = Effect.fnUntraced(function* (options?: ScopeOptions) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
|
||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
|
||||
Effect.all([Ref.make(true), makeScope(options)])
|
||||
), [])
|
||||
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: () => makeScope(options).pipe(
|
||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
||||
),
|
||||
})
|
||||
), [])
|
||||
|
||||
return scope
|
||||
})
|
||||
|
||||
const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
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 useMemo: {
|
||||
<A, E, R>(
|
||||
factory: () => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<A, never, R>
|
||||
} = Effect.fnUntraced(function* useMemo<A, E, R>(
|
||||
): 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<R>()
|
||||
return React.useMemo(() => Runtime.runSync(runtime)(factory()), deps)
|
||||
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, never, R>
|
||||
} = Effect.fnUntraced(function* useOnce<A, E, R>(
|
||||
<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, [])
|
||||
})
|
||||
|
||||
export const useMemoLayer: {
|
||||
<ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>
|
||||
): Effect.Effect<Context.Context<ROut>, E, RIn>
|
||||
} = Effect.fnUntraced(function* <ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>
|
||||
) {
|
||||
return yield* useMemo(() => Effect.provide(Effect.context<ROut>(), layer), [layer])
|
||||
})
|
||||
|
||||
|
||||
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 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.fnUntraced(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 useEffect: {
|
||||
<E, R>(
|
||||
effect: () => Effect.Effect<void, E, R | Scope.Scope>,
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<void, never, R>
|
||||
} = Effect.fnUntraced(function* useEffect<E, R>(
|
||||
effect: () => Effect.Effect<void, E, R | Scope.Scope>,
|
||||
): 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<R>()
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
React.useEffect(() => {
|
||||
const { scope, exit } = Effect.Do.pipe(
|
||||
@@ -64,16 +147,16 @@ export const useEffect: {
|
||||
|
||||
export const useLayoutEffect: {
|
||||
<E, R>(
|
||||
effect: () => Effect.Effect<void, E, R | Scope.Scope>,
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<void, never, R>
|
||||
} = Effect.fnUntraced(function* useLayoutEffect<E, R>(
|
||||
effect: () => Effect.Effect<void, E, R | Scope.Scope>,
|
||||
): 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<R>()
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const { scope, exit } = Effect.Do.pipe(
|
||||
@@ -94,3 +177,141 @@ export const useLayoutEffect: {
|
||||
}
|
||||
}, 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.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)
|
||||
)
|
||||
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
||||
|
||||
return () => {
|
||||
switch (options?.finalizerExecutionMode ?? "fork") {
|
||||
case "sync":
|
||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
||||
break
|
||||
case "fork":
|
||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
||||
break
|
||||
}
|
||||
}
|
||||
}, deps)
|
||||
})
|
||||
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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, (x, y) => x === y)),
|
||||
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.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
||||
|
||||
yield* useFork(() => Stream.runForEach(
|
||||
Stream.changesWith(ref.changes, (x, y) => x === y),
|
||||
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.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
|
||||
})
|
||||
|
||||
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, (x, y) => x === y),
|
||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||
), [stream])
|
||||
|
||||
return reactStateValue as Option.Some<A>
|
||||
})
|
||||
|
||||
47
packages/effect-components/src/ReactManagedRuntime.ts
Normal file
47
packages/effect-components/src/ReactManagedRuntime.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Effect, type Layer, ManagedRuntime, type Runtime } from "effect"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
export interface ReactManagedRuntime<R, ER> {
|
||||
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
||||
readonly context: React.Context<Runtime.Runtime<R>>
|
||||
}
|
||||
|
||||
export const make = <R, ER>(
|
||||
layer: Layer.Layer<R, ER>,
|
||||
memoMap?: Layer.MemoMap,
|
||||
): ReactManagedRuntime<R, ER> => ({
|
||||
runtime: ManagedRuntime.make(layer, memoMap),
|
||||
context: React.createContext<Runtime.Runtime<R>>(null!),
|
||||
})
|
||||
|
||||
|
||||
export interface AsyncProviderProps<R, ER> extends React.SuspenseProps {
|
||||
readonly runtime: ReactManagedRuntime<R, ER>
|
||||
readonly children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function AsyncProvider<R, ER>(
|
||||
{ runtime, children, ...suspenseProps }: AsyncProviderProps<R, ER>
|
||||
): React.ReactNode {
|
||||
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
||||
|
||||
return React.createElement(
|
||||
React.Suspense,
|
||||
suspenseProps,
|
||||
React.createElement(AsyncProviderInner<R, ER>, { runtime, promise, children }),
|
||||
)
|
||||
}
|
||||
|
||||
interface AsyncProviderInnerProps<R, ER> {
|
||||
readonly runtime: ReactManagedRuntime<R, ER>
|
||||
readonly promise: Promise<Runtime.Runtime<R>>
|
||||
readonly children?: React.ReactNode
|
||||
}
|
||||
|
||||
function AsyncProviderInner<R, ER>(
|
||||
{ runtime, promise, children }: AsyncProviderInnerProps<R, ER>
|
||||
): React.ReactNode {
|
||||
const value = React.use(promise)
|
||||
return React.createElement(runtime.context, { value }, children)
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * as ReactComponent from "./ReactComponent.js"
|
||||
export * as ReactHook from "./ReactHook.js"
|
||||
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
|
||||
|
||||
99
packages/effect-components/src/types/PropertyPath.ts
Normal file
99
packages/effect-components/src/types/PropertyPath.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Array, Function, Option, Predicate } from "effect"
|
||||
|
||||
|
||||
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
|
||||
export type Paths<T, D extends number = 5, Seen = never> = [] | (
|
||||
D extends never ? [] :
|
||||
T extends Seen ? [] :
|
||||
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
|
||||
T extends object ? ObjectPaths<T, D, Seen | T> :
|
||||
never
|
||||
)
|
||||
|
||||
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
|
||||
[K in keyof T as K extends number ? K : never]:
|
||||
| [K]
|
||||
| [K, ...Paths<T[K], Prev[D], Seen>]
|
||||
} extends infer O
|
||||
? O[keyof O]
|
||||
: never
|
||||
|
||||
export type ObjectPaths<T extends object, D extends number, Seen> = {
|
||||
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
||||
NonNullable<T[K]> extends infer V
|
||||
? [K] | [K, ...Paths<V, Prev[D], Seen>]
|
||||
: never
|
||||
} extends infer O
|
||||
? O[keyof O]
|
||||
: never
|
||||
|
||||
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
|
||||
? Head extends keyof T
|
||||
? ValueFromPath<T[Head], Tail>
|
||||
: T extends readonly any[]
|
||||
? Head extends number
|
||||
? ValueFromPath<T[number], Tail>
|
||||
: never
|
||||
: never
|
||||
: T
|
||||
|
||||
export type AnyKey = string | number | symbol
|
||||
export type AnyPath = readonly AnyKey[]
|
||||
|
||||
|
||||
export const unsafeGet: {
|
||||
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
||||
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
|
||||
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
|
||||
path.reduce((acc: any, key: any) => acc?.[key], self)
|
||||
)
|
||||
|
||||
export const get: {
|
||||
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
|
||||
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
|
||||
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
|
||||
path.reduce(
|
||||
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
|
||||
? Predicate.hasProperty(acc.value, key)
|
||||
? Option.some(acc.value[key])
|
||||
: Option.none()
|
||||
: acc,
|
||||
|
||||
Option.some(self),
|
||||
)
|
||||
)
|
||||
|
||||
export const immutableSet: {
|
||||
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
|
||||
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
||||
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
||||
const key = Array.head(path as AnyPath)
|
||||
if (Option.isNone(key))
|
||||
return Option.some(value as T)
|
||||
if (!Predicate.hasProperty(self, key.value))
|
||||
return Option.none()
|
||||
|
||||
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
|
||||
if (Option.isNone(child))
|
||||
return child
|
||||
|
||||
if (Array.isArray(self))
|
||||
return typeof key.value === "number"
|
||||
? Option.some([
|
||||
...self.slice(0, key.value),
|
||||
child.value,
|
||||
...self.slice(key.value + 1),
|
||||
] as T)
|
||||
: Option.none()
|
||||
|
||||
if (typeof self === "object")
|
||||
return Option.some(
|
||||
Object.assign(
|
||||
Object.create(Object.getPrototypeOf(self)),
|
||||
{ ...self, [key.value]: child.value },
|
||||
)
|
||||
)
|
||||
|
||||
return Option.none()
|
||||
})
|
||||
12
packages/effect-components/src/types/SetStateAction.ts
Normal file
12
packages/effect-components/src/types/SetStateAction.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Function } from "effect"
|
||||
import type * as React from "react"
|
||||
|
||||
|
||||
export const value: {
|
||||
<S>(prevState: S): (self: React.SetStateAction<S>) => S
|
||||
<S>(self: React.SetStateAction<S>, prevState: S): S
|
||||
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
|
||||
typeof self === "function"
|
||||
? (self as (prevState: S) => S)(prevState)
|
||||
: self
|
||||
)
|
||||
100
packages/effect-components/src/types/SubscriptionSubRef.ts
Normal file
100
packages/effect-components/src/types/SubscriptionSubRef.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
||||
import * as PropertyPath from "./PropertyPath.js"
|
||||
|
||||
|
||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
|
||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
||||
|
||||
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
||||
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
||||
|
||||
readonly [Unify.typeSymbol]?: unknown
|
||||
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
||||
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
|
||||
}
|
||||
|
||||
export declare namespace SubscriptionSubRef {
|
||||
export interface Variance<in out A, in out B> {
|
||||
readonly [SubscriptionSubRefTypeId]: {
|
||||
readonly _A: Types.Invariant<A>
|
||||
readonly _B: Types.Invariant<B>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
|
||||
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
|
||||
}
|
||||
|
||||
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
|
||||
SubscriptionRef?: true
|
||||
}
|
||||
|
||||
|
||||
const refVariance = { _A: (_: any) => _ }
|
||||
const synchronizedRefVariance = { _A: (_: any) => _ }
|
||||
const subscriptionRefVariance = { _A: (_: any) => _ }
|
||||
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
||||
|
||||
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||
readonly [Ref.RefTypeId] = refVariance
|
||||
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
|
||||
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
|
||||
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
|
||||
|
||||
readonly get: Effect.Effect<A>
|
||||
|
||||
constructor(
|
||||
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
||||
readonly getter: (parentValue: B) => A,
|
||||
readonly setter: (parentValue: B, value: A) => B,
|
||||
) {
|
||||
super()
|
||||
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
||||
}
|
||||
|
||||
commit() {
|
||||
return this.get
|
||||
}
|
||||
|
||||
get changes(): Stream.Stream<A> {
|
||||
return this.get.pipe(
|
||||
Effect.map(a => this.parent.changes.pipe(
|
||||
Stream.map(this.getter),
|
||||
s => Stream.concat(Stream.make(a), s),
|
||||
)),
|
||||
Stream.unwrap,
|
||||
)
|
||||
}
|
||||
|
||||
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
|
||||
return this.modifyEffect(a => Effect.succeed(f(a)))
|
||||
}
|
||||
|
||||
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
||||
return Effect.Do.pipe(
|
||||
Effect.bind("b", () => Ref.get(this.parent)),
|
||||
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
||||
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
||||
Effect.map(({ ca: [c] }) => c),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const makeFromGetSet = <A, B>(
|
||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||
getter: (parentValue: B) => A,
|
||||
setter: (parentValue: B, value: A) => B,
|
||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
|
||||
|
||||
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||
path: P,
|
||||
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
|
||||
parent,
|
||||
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
||||
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
||||
)
|
||||
3
packages/effect-components/src/types/index.ts
Normal file
3
packages/effect-components/src/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * as PropertyPath from "./PropertyPath.js"
|
||||
export * as SetStateAction from "./SetStateAction.js"
|
||||
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||
@@ -1,8 +1,26 @@
|
||||
import { Box, Text, TextField } from "@radix-ui/themes"
|
||||
import { Box, TextField } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Layer, ManagedRuntime, SubscriptionRef } from "effect"
|
||||
import { ReactComponent, ReactHook } from "effect-components"
|
||||
import * as React from "react"
|
||||
import { Array, Console, Effect, Layer, pipe, Ref, Runtime, SubscriptionRef } from "effect"
|
||||
import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components"
|
||||
|
||||
|
||||
const LogLive = Layer.scopedDiscard(Effect.acquireRelease(
|
||||
Console.log("Runtime built."),
|
||||
() => Console.log("Runtime destroyed."),
|
||||
))
|
||||
|
||||
class TestService extends Effect.Service<TestService>()("TestService", {
|
||||
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
|
||||
}) {}
|
||||
|
||||
class SubService extends Effect.Service<SubService>()("SubService", {
|
||||
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("subvalue")),
|
||||
}) {}
|
||||
|
||||
const runtime = ReactManagedRuntime.make(Layer.empty.pipe(
|
||||
Layer.provideMerge(LogLive),
|
||||
Layer.provideMerge(TestService.Default),
|
||||
))
|
||||
|
||||
|
||||
export const Route = createFileRoute("/effect-component-tests")({
|
||||
@@ -10,39 +28,78 @@ export const Route = createFileRoute("/effect-component-tests")({
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const runtime = React.useMemo(() => ManagedRuntime.make(Layer.empty), [])
|
||||
|
||||
return <>
|
||||
{runtime.runSync(ReactComponent.use(MyTestComponent, Component => (
|
||||
<Component />
|
||||
)).pipe(
|
||||
Effect.scoped
|
||||
))}
|
||||
</>
|
||||
return (
|
||||
<ReactManagedRuntime.AsyncProvider runtime={runtime}>
|
||||
<MyRoute />
|
||||
</ReactManagedRuntime.AsyncProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const MyRoute = pipe(
|
||||
Effect.fn(function*() {
|
||||
const runtime = yield* Effect.runtime()
|
||||
|
||||
class TestService extends Effect.Service<TestService>()("TestService", {
|
||||
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
|
||||
}) {}
|
||||
const service = yield* TestService
|
||||
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
|
||||
|
||||
const MyTestComponent = Effect.fn(function* MyTestComponent(props?: { readonly value?: string }) {
|
||||
const [state, setState] = React.useState("value")
|
||||
const effectValue = yield* Effect.succeed(`state: ${ state }`)
|
||||
|
||||
yield* ReactHook.useOnce(() => Effect.andThen(
|
||||
Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
|
||||
Console.log("MyTestComponent mounted"),
|
||||
))
|
||||
// const MyTestComponentFC = yield* Effect.provide(
|
||||
// ReactComponent.useFC(MyTestComponent),
|
||||
// yield* ReactHook.useMemoLayer(SubService.Default),
|
||||
// )
|
||||
|
||||
return <>
|
||||
<Text>{effectValue}</Text>
|
||||
|
||||
<Box>
|
||||
<TextField.Root
|
||||
value={state}
|
||||
onChange={e => setState(e.target.value)}
|
||||
value={value}
|
||||
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* {yield* ReactComponent.use(MyTestComponent, C => <C />).pipe(
|
||||
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default))
|
||||
)} */}
|
||||
|
||||
{/* {Array.range(0, 3).map(k =>
|
||||
<MyTestComponentFC key={k} />
|
||||
)} */}
|
||||
|
||||
{yield* pipe(
|
||||
Array.range(0, 3),
|
||||
Array.map(k => ReactComponent.use(MyTestComponent, FC =>
|
||||
<FC key={k} />
|
||||
)),
|
||||
Effect.all,
|
||||
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default)),
|
||||
)}
|
||||
</>
|
||||
}),
|
||||
|
||||
ReactComponent.withDisplayName("MyRoute"),
|
||||
ReactComponent.withRuntime(runtime.context),
|
||||
)
|
||||
|
||||
|
||||
const MyTestComponent = pipe(
|
||||
Effect.fn(function*() {
|
||||
const runtime = yield* Effect.runtime()
|
||||
|
||||
const service = yield* SubService
|
||||
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
|
||||
|
||||
// yield* ReactHook.useMemo(() => Effect.andThen(
|
||||
// Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
|
||||
// Console.log("MyTestComponent mounted"),
|
||||
// ), [])
|
||||
|
||||
return <>
|
||||
<Box>
|
||||
<TextField.Root
|
||||
value={value}
|
||||
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
})
|
||||
}),
|
||||
|
||||
ReactComponent.withDisplayName("MyTestComponent"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user