From 1f541b423492e8ce88ff6d6555875137fbe6ce71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 7 Jul 2025 05:46:43 +0200 Subject: [PATCH] Scope refactoring --- packages/effect-fc/src/Component.ts | 47 +-------- packages/effect-fc/src/Hook.ts | 140 +++++++++++++++----------- packages/example/src/routes/index.tsx | 8 +- 3 files changed, 88 insertions(+), 107 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 2c043ba..e1c37e3 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -1,5 +1,6 @@ -import { Context, Effect, ExecutionStrategy, Exit, Function, Pipeable, Ref, Runtime, Scope, String, Tracer, type Types, type Utils } from "effect" +import { Context, Effect, ExecutionStrategy, Function, Pipeable, Runtime, Scope, String, Tracer, type Types, type Utils } from "effect" import * as React from "react" +import * as Hook from "./Hook.js" export interface Component extends Pipeable.Pipeable { @@ -68,7 +69,7 @@ export const useFC: { runtimeRef.current = yield* Effect.runtime>() return React.useMemo(() => function ScopeProvider(props: P) { - const scope = useScope(runtimeRef.current, self.options) + const scope = Runtime.runSync(runtimeRef.current)(Hook.useScope([], self.options)) const FC = React.useMemo(() => { const f = (props: P) => Runtime.runSync(runtimeRef.current)( @@ -84,48 +85,6 @@ export const useFC: { )) }) -const useScope = ( - runtime: Runtime.Runtime, - options: Options, -) => { - const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)( - Effect.all([Ref.make(true), Scope.make(options.finalizerExecutionStrategy)]) - ), []) - 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).pipe( - Effect.tap(scope => Effect.sync(() => setScope(scope))), - Effect.map(scope => () => closeScope(scope, runtime, options)), - ), - }) - ), []) - - return scope -} - -const closeScope = ( - scope: Scope.CloseableScope, - runtime: Runtime.Runtime, - options: Options, -) => { - switch (options.finalizerExecutionMode) { - case "sync": - Runtime.runSync(runtime)(Scope.close(scope, Exit.void)) - break - case "fork": - Runtime.runFork(runtime)(Scope.close(scope, Exit.void)) - break - } -} - - export const use: { ( self: Component, diff --git a/packages/effect-fc/src/Hook.ts b/packages/effect-fc/src/Hook.ts index 4f1a441..3977afe 100644 --- a/packages/effect-fc/src/Hook.ts +++ b/packages/effect-fc/src/Hook.ts @@ -9,6 +9,53 @@ export interface ScopeOptions { } +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.parallel), + ])), []) + 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.parallel).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, @@ -57,16 +104,6 @@ export const useOnce: { return yield* useMemo(factory, []) }) -export const useMemoLayer: { - ( - layer: Layer.Layer - ): Effect.Effect, E, RIn> -} = Effect.fn("useMemoLayer")(function* ( - layer: Layer.Layer -) { - return yield* useMemo(() => Effect.provide(Effect.context(), layer), [layer]) -}) - export const useEffect: { ( @@ -81,24 +118,14 @@ export const useEffect: { ) { const runtime = yield* Effect.runtime>() - React.useEffect(() => { - const { scope, exit } = Effect.Do.pipe( - Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)), - Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))), - Runtime.runSync(runtime), - ) - - return () => { - switch (options?.finalizerExecutionMode ?? "sync") { - case "sync": - Runtime.runSync(runtime)(Scope.close(scope, exit)) - break - case "fork": - Runtime.runFork(runtime)(Scope.close(scope, exit)) - break - } - } - }, deps) + 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: { @@ -114,24 +141,14 @@ export const useLayoutEffect: { ) { const runtime = yield* Effect.runtime>() - React.useLayoutEffect(() => { - const { scope, exit } = Effect.Do.pipe( - Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)), - Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))), - Runtime.runSync(runtime), - ) - - return () => { - switch (options?.finalizerExecutionMode ?? "sync") { - case "sync": - Runtime.runSync(runtime)(Scope.close(scope, exit)) - break - case "fork": - Runtime.runFork(runtime)(Scope.close(scope, exit)) - break - } - } - }, deps) + 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: { @@ -153,21 +170,30 @@ export const useFork: { : 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 - } - } + return () => closeScope(scope, runtime, options) }, deps) }) +export const useContextSync: { + ( + layer: Layer.Layer, + options?: ScopeOptions, + ): Effect.Effect, E, Exclude> +} = Effect.fn("useContextSync")(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) { diff --git a/packages/example/src/routes/index.tsx b/packages/example/src/routes/index.tsx index 71b68bf..2baf5e4 100644 --- a/packages/example/src/routes/index.tsx +++ b/packages/example/src/routes/index.tsx @@ -10,12 +10,8 @@ const TodosStateLive = TodosState.Default("todos") export const Route = createFileRoute("/")({ component: Component.make(function* Index() { - return yield* Effect.provide( - Component.use(Todos, Todos => ), - yield* Hook.useMemoLayer(TodosStateLive), - ) - }, { - finalizerExecutionMode: "fork" + const context = yield* Hook.useContextSync(TodosStateLive, { finalizerExecutionMode: "fork" }) + return yield* Effect.provide(Component.use(Todos, Todos => ), context) }).pipe( Component.withRuntime(runtime.context), )