From 09ed773b96f0f35f6bc321dc09dee30625c2db40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 28 Jul 2025 01:42:09 +0200 Subject: [PATCH] Refactoring --- packages/effect-fc/src/Component.ts | 2 +- packages/effect-fc/src/Hook.ts | 302 ------------------ packages/effect-fc/src/Hook/ScopeOptions.ts | 7 + packages/effect-fc/src/Hook/index.ts | 14 + packages/effect-fc/src/Hook/internal.ts | 18 ++ .../effect-fc/src/Hook/useCallbackPromise.ts | 16 + .../effect-fc/src/Hook/useCallbackSync.ts | 16 + packages/effect-fc/src/Hook/useContext.ts | 23 ++ packages/effect-fc/src/Hook/useEffect.ts | 28 ++ packages/effect-fc/src/Hook/useFork.ts | 31 ++ .../effect-fc/src/Hook/useLayoutEffect.ts | 28 ++ packages/effect-fc/src/Hook/useMemo.ts | 16 + packages/effect-fc/src/Hook/useOnce.ts | 11 + .../src/Hook/useRefFromReactiveValue.ts | 12 + packages/effect-fc/src/Hook/useRefState.ts | 28 ++ packages/effect-fc/src/Hook/useScope.ts | 36 +++ .../src/Hook/useStreamFromReactiveValues.ts | 30 ++ .../effect-fc/src/Hook/useSubscribeRefs.ts | 27 ++ .../effect-fc/src/Hook/useSubscribeStream.ts | 31 ++ packages/effect-fc/src/index.ts | 2 +- 20 files changed, 374 insertions(+), 304 deletions(-) delete mode 100644 packages/effect-fc/src/Hook.ts create mode 100644 packages/effect-fc/src/Hook/ScopeOptions.ts create mode 100644 packages/effect-fc/src/Hook/index.ts create mode 100644 packages/effect-fc/src/Hook/internal.ts create mode 100644 packages/effect-fc/src/Hook/useCallbackPromise.ts create mode 100644 packages/effect-fc/src/Hook/useCallbackSync.ts create mode 100644 packages/effect-fc/src/Hook/useContext.ts create mode 100644 packages/effect-fc/src/Hook/useEffect.ts create mode 100644 packages/effect-fc/src/Hook/useFork.ts create mode 100644 packages/effect-fc/src/Hook/useLayoutEffect.ts create mode 100644 packages/effect-fc/src/Hook/useMemo.ts create mode 100644 packages/effect-fc/src/Hook/useOnce.ts create mode 100644 packages/effect-fc/src/Hook/useRefFromReactiveValue.ts create mode 100644 packages/effect-fc/src/Hook/useRefState.ts create mode 100644 packages/effect-fc/src/Hook/useScope.ts create mode 100644 packages/effect-fc/src/Hook/useStreamFromReactiveValues.ts create mode 100644 packages/effect-fc/src/Hook/useSubscribeRefs.ts create mode 100644 packages/effect-fc/src/Hook/useSubscribeStream.ts diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index a2f0d21..98089ee 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -1,6 +1,6 @@ import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" import * as React from "react" -import * as Hook from "./Hook.js" +import * as Hook from "./Hook/index.js" import * as Memoized from "./Memoized.js" diff --git a/packages/effect-fc/src/Hook.ts b/packages/effect-fc/src/Hook.ts deleted file mode 100644 index ee766b3..0000000 --- a/packages/effect-fc/src/Hook.ts +++ /dev/null @@ -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 -} = 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, - 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: { - ( - callback: (...args: Args) => Effect.Effect, - deps: React.DependencyList, - ): Effect.Effect<(...args: Args) => A, never, R> -} = Effect.fn("useCallbackSync")(function* ( - callback: (...args: Args) => Effect.Effect, - deps: React.DependencyList, -) { - const runtime = yield* Effect.runtime() - return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps) -}) - -export const useCallbackPromise: { - ( - callback: (...args: Args) => Effect.Effect, - deps: React.DependencyList, - ): Effect.Effect<(...args: Args) => Promise, never, R> -} = Effect.fn("useCallbackPromise")(function* ( - callback: (...args: Args) => Effect.Effect, - deps: React.DependencyList, -) { - const runtime = yield* Effect.runtime() - return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps) -}) - - -export const useMemo: { - ( - factory: () => Effect.Effect, - deps: React.DependencyList, - ): Effect.Effect -} = Effect.fn("useMemo")(function* ( - factory: () => Effect.Effect, - deps: React.DependencyList, -) { - const runtime = yield* Effect.runtime() - return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps) -}) - -export const useOnce: { - (factory: () => Effect.Effect): Effect.Effect -} = Effect.fn("useOnce")(function* ( - factory: () => Effect.Effect -) { - return yield* useMemo(factory, []) -}) - - -export const useEffect: { - ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: ScopeOptions, - ): Effect.Effect> -} = Effect.fn("useEffect")(function* ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: ScopeOptions, -) { - const runtime = yield* Effect.runtime>() - - 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: { - ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: ScopeOptions, - ): Effect.Effect> -} = Effect.fn("useLayoutEffect")(function* ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: ScopeOptions, -) { - const runtime = yield* Effect.runtime>() - - 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: { - ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: Runtime.RunForkOptions & ScopeOptions, - ): Effect.Effect> -} = Effect.fn("useFork")(function* ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: Runtime.RunForkOptions & ScopeOptions, -) { - const runtime = yield* Effect.runtime>() - - 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: { - ( - layer: Layer.Layer, - options?: ScopeOptions, - ): Effect.Effect, E, Exclude> -} = Effect.fn("useContext")(function* ( - layer: Layer.Layer, - options?: ScopeOptions, -) { - const scope = yield* useScope([layer], options) - - return yield* useMemo(() => Effect.provideService( - Effect.provide(Effect.context(), layer), - Scope.Scope, - scope, - ), [scope]) -}) - - -export const useRefFromReactiveValue: { - (value: A): Effect.Effect> -} = Effect.fn("useRefFromReactiveValue")(function*(value) { - const ref = yield* useOnce(() => SubscriptionRef.make(value)) - yield* useEffect(() => Ref.set(ref, value), [value]) - return ref -}) - -export const useSubscribeRefs: { - []>( - ...refs: Refs - ): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success }> -} = Effect.fn("useSubscribeRefs")(function* []>( - ...refs: Refs -) { - const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => - Effect.all(refs as readonly SubscriptionRef.SubscriptionRef[]) - )) - - 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: { - ( - ref: SubscriptionRef.SubscriptionRef - ): Effect.Effect>]> -} = Effect.fn("useRefState")(function* (ref: SubscriptionRef.SubscriptionRef) { - 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) => - Ref.update(ref, prevState => - SetStateAction.value(setStateAction, prevState) - ), - [ref]) - - return [reactStateValue, setValue] -}) - - -export const useStreamFromReactiveValues: { - ( - values: A - ): Effect.Effect, never, Scope.Scope> -} = Effect.fn("useStreamFromReactiveValues")(function* (values: A) { - const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe( - Effect.bind("latest", () => Ref.make(values)), - Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded(), 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: { - ( - stream: Stream.Stream - ): Effect.Effect, never, R> - , E, R>( - stream: Stream.Stream, - initialValue: A, - ): Effect.Effect, never, R> -} = Effect.fn("useSubscribeStream")(function* , E, R>( - stream: Stream.Stream, - 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 -}) diff --git a/packages/effect-fc/src/Hook/ScopeOptions.ts b/packages/effect-fc/src/Hook/ScopeOptions.ts new file mode 100644 index 0000000..c2654e8 --- /dev/null +++ b/packages/effect-fc/src/Hook/ScopeOptions.ts @@ -0,0 +1,7 @@ +import type { ExecutionStrategy } from "effect" + + +export interface ScopeOptions { + readonly finalizerExecutionMode?: "sync" | "fork" + readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy +} diff --git a/packages/effect-fc/src/Hook/index.ts b/packages/effect-fc/src/Hook/index.ts new file mode 100644 index 0000000..5d0e295 --- /dev/null +++ b/packages/effect-fc/src/Hook/index.ts @@ -0,0 +1,14 @@ +export * from "./ScopeOptions.js" +export * from "./useCallbackPromise.js" +export * from "./useCallbackSync.js" +export * from "./useContext.js" +export * from "./useEffect.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" diff --git a/packages/effect-fc/src/Hook/internal.ts b/packages/effect-fc/src/Hook/internal.ts new file mode 100644 index 0000000..4c9a35e --- /dev/null +++ b/packages/effect-fc/src/Hook/internal.ts @@ -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, + 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 + } +} diff --git a/packages/effect-fc/src/Hook/useCallbackPromise.ts b/packages/effect-fc/src/Hook/useCallbackPromise.ts new file mode 100644 index 0000000..5e093ef --- /dev/null +++ b/packages/effect-fc/src/Hook/useCallbackPromise.ts @@ -0,0 +1,16 @@ +import { Effect, Runtime } from "effect" +import * as React from "react" + + +export const useCallbackPromise: { + ( + callback: (...args: Args) => Effect.Effect, + deps: React.DependencyList, + ): Effect.Effect<(...args: Args) => Promise, never, R> +} = Effect.fn("useCallbackPromise")(function* ( + callback: (...args: Args) => Effect.Effect, + deps: React.DependencyList, +) { + const runtime = yield* Effect.runtime() + return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps) +}) diff --git a/packages/effect-fc/src/Hook/useCallbackSync.ts b/packages/effect-fc/src/Hook/useCallbackSync.ts new file mode 100644 index 0000000..07ffd84 --- /dev/null +++ b/packages/effect-fc/src/Hook/useCallbackSync.ts @@ -0,0 +1,16 @@ +import { Effect, Runtime } from "effect" +import * as React from "react" + + +export const useCallbackSync: { + ( + callback: (...args: Args) => Effect.Effect, + deps: React.DependencyList, + ): Effect.Effect<(...args: Args) => A, never, R> +} = Effect.fn("useCallbackSync")(function* ( + callback: (...args: Args) => Effect.Effect, + deps: React.DependencyList, +) { + const runtime = yield* Effect.runtime() + return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps) +}) diff --git a/packages/effect-fc/src/Hook/useContext.ts b/packages/effect-fc/src/Hook/useContext.ts new file mode 100644 index 0000000..fbb9d85 --- /dev/null +++ b/packages/effect-fc/src/Hook/useContext.ts @@ -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: { + ( + layer: Layer.Layer, + options?: ScopeOptions, + ): Effect.Effect, E, Exclude> +} = Effect.fn("useContext")(function* ( + layer: Layer.Layer, + options?: ScopeOptions, +) { + const scope = yield* useScope([layer], options) + + return yield* useMemo(() => Effect.provideService( + Effect.provide(Effect.context(), layer), + Scope.Scope, + scope, + ), [scope]) +}) diff --git a/packages/effect-fc/src/Hook/useEffect.ts b/packages/effect-fc/src/Hook/useEffect.ts new file mode 100644 index 0000000..798d39f --- /dev/null +++ b/packages/effect-fc/src/Hook/useEffect.ts @@ -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: { + ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: ScopeOptions, + ): Effect.Effect> +} = Effect.fn("useEffect")(function* ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: ScopeOptions, +) { + const runtime = yield* Effect.runtime>() + + 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) +}) diff --git a/packages/effect-fc/src/Hook/useFork.ts b/packages/effect-fc/src/Hook/useFork.ts new file mode 100644 index 0000000..ec17db6 --- /dev/null +++ b/packages/effect-fc/src/Hook/useFork.ts @@ -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: { + ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: Runtime.RunForkOptions & ScopeOptions, + ): Effect.Effect> +} = Effect.fn("useFork")(function* ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: Runtime.RunForkOptions & ScopeOptions, +) { + const runtime = yield* Effect.runtime>() + + 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) +}) diff --git a/packages/effect-fc/src/Hook/useLayoutEffect.ts b/packages/effect-fc/src/Hook/useLayoutEffect.ts new file mode 100644 index 0000000..988fd14 --- /dev/null +++ b/packages/effect-fc/src/Hook/useLayoutEffect.ts @@ -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: { + ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: ScopeOptions, + ): Effect.Effect> +} = Effect.fn("useLayoutEffect")(function* ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: ScopeOptions, +) { + const runtime = yield* Effect.runtime>() + + 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) +}) diff --git a/packages/effect-fc/src/Hook/useMemo.ts b/packages/effect-fc/src/Hook/useMemo.ts new file mode 100644 index 0000000..53b1aa8 --- /dev/null +++ b/packages/effect-fc/src/Hook/useMemo.ts @@ -0,0 +1,16 @@ +import { Effect, Runtime } from "effect" +import * as React from "react" + + +export const useMemo: { + ( + factory: () => Effect.Effect, + deps: React.DependencyList, + ): Effect.Effect +} = Effect.fn("useMemo")(function* ( + factory: () => Effect.Effect, + deps: React.DependencyList, +) { + const runtime = yield* Effect.runtime() + return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps) +}) diff --git a/packages/effect-fc/src/Hook/useOnce.ts b/packages/effect-fc/src/Hook/useOnce.ts new file mode 100644 index 0000000..3598d56 --- /dev/null +++ b/packages/effect-fc/src/Hook/useOnce.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import { useMemo } from "./useMemo.js" + + +export const useOnce: { + (factory: () => Effect.Effect): Effect.Effect +} = Effect.fn("useOnce")(function* ( + factory: () => Effect.Effect +) { + return yield* useMemo(factory, []) +}) diff --git a/packages/effect-fc/src/Hook/useRefFromReactiveValue.ts b/packages/effect-fc/src/Hook/useRefFromReactiveValue.ts new file mode 100644 index 0000000..9ff1e6f --- /dev/null +++ b/packages/effect-fc/src/Hook/useRefFromReactiveValue.ts @@ -0,0 +1,12 @@ +import { Effect, Ref, SubscriptionRef } from "effect" +import { useEffect } from "./useEffect.js" +import { useOnce } from "./useOnce.js" + + +export const useRefFromReactiveValue: { + (value: A): Effect.Effect> +} = Effect.fn("useRefFromReactiveValue")(function*(value) { + const ref = yield* useOnce(() => SubscriptionRef.make(value)) + yield* useEffect(() => Ref.set(ref, value), [value]) + return ref +}) diff --git a/packages/effect-fc/src/Hook/useRefState.ts b/packages/effect-fc/src/Hook/useRefState.ts new file mode 100644 index 0000000..d1fa07f --- /dev/null +++ b/packages/effect-fc/src/Hook/useRefState.ts @@ -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: { + ( + ref: SubscriptionRef.SubscriptionRef + ): Effect.Effect>]> +} = Effect.fn("useRefState")(function* (ref: SubscriptionRef.SubscriptionRef) { + 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) => + Ref.update(ref, prevState => + SetStateAction.value(setStateAction, prevState) + ), + [ref]) + + return [reactStateValue, setValue] +}) diff --git a/packages/effect-fc/src/Hook/useScope.ts b/packages/effect-fc/src/Hook/useScope.ts new file mode 100644 index 0000000..aae4a49 --- /dev/null +++ b/packages/effect-fc/src/Hook/useScope.ts @@ -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 +} = 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 +}) diff --git a/packages/effect-fc/src/Hook/useStreamFromReactiveValues.ts b/packages/effect-fc/src/Hook/useStreamFromReactiveValues.ts new file mode 100644 index 0000000..58e3840 --- /dev/null +++ b/packages/effect-fc/src/Hook/useStreamFromReactiveValues.ts @@ -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: { + ( + values: A + ): Effect.Effect, never, Scope.Scope> +} = Effect.fn("useStreamFromReactiveValues")(function* (values: A) { + const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe( + Effect.bind("latest", () => Ref.make(values)), + Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded(), 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 +}) diff --git a/packages/effect-fc/src/Hook/useSubscribeRefs.ts b/packages/effect-fc/src/Hook/useSubscribeRefs.ts new file mode 100644 index 0000000..4f18976 --- /dev/null +++ b/packages/effect-fc/src/Hook/useSubscribeRefs.ts @@ -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: { + []>( + ...refs: Refs + ): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success }> +} = Effect.fn("useSubscribeRefs")(function* []>( + ...refs: Refs +) { + const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => + Effect.all(refs as readonly SubscriptionRef.SubscriptionRef[]) + )) + + 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 +}) diff --git a/packages/effect-fc/src/Hook/useSubscribeStream.ts b/packages/effect-fc/src/Hook/useSubscribeStream.ts new file mode 100644 index 0000000..d157111 --- /dev/null +++ b/packages/effect-fc/src/Hook/useSubscribeStream.ts @@ -0,0 +1,31 @@ +import { Effect, Equivalence, Option, Stream } from "effect" +import * as React from "react" +import { useFork } from "./useFork.js" + + +export const useSubscribeStream: { + ( + stream: Stream.Stream + ): Effect.Effect, never, R> + , E, R>( + stream: Stream.Stream, + initialValue: A, + ): Effect.Effect, never, R> +} = Effect.fn("useSubscribeStream")(function* , E, R>( + stream: Stream.Stream, + 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 +}) diff --git a/packages/effect-fc/src/index.ts b/packages/effect-fc/src/index.ts index fd1ade9..6d643c6 100644 --- a/packages/effect-fc/src/index.ts +++ b/packages/effect-fc/src/index.ts @@ -1,5 +1,5 @@ export * as Component from "./Component.js" -export * as Hook from "./Hook.js" +export * as Hook from "./Hook/index.js" export * as Memoized from "./Memoized.js" export * as ReactManagedRuntime from "./ReactManagedRuntime.js" export * as Suspense from "./Suspense.js"