diff --git a/packages/reffuse/src/ReffuseContext.ts b/packages/reffuse/src/ReffuseContext.ts index 4470cfa..becd542 100644 --- a/packages/reffuse/src/ReffuseContext.ts +++ b/packages/reffuse/src/ReffuseContext.ts @@ -1,4 +1,4 @@ -import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Ref, Runtime, Scope } from "effect" +import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Match, Ref, Runtime, Scope } from "effect" import * as React from "react" import * as ReffuseRuntime from "./ReffuseRuntime.js" @@ -25,6 +25,8 @@ export type R = T extends ReffuseContext ? R : never export type ReactProvider = React.FC<{ readonly layer: Layer.Layer readonly scope?: Scope.Scope + readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy + readonly finalizerExecutionMode?: "sync" | "fork" readonly children?: React.ReactNode }> @@ -32,16 +34,25 @@ const makeProvider = (Context: React.Context>): ReactProvi return function ReffuseContextReactProvider(props) { const runtime = ReffuseRuntime.useRuntime() const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime]) + const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime]) const makeScope = React.useMemo(() => props.scope - ? Scope.fork(props.scope, ExecutionStrategy.sequential) - : Scope.make(), + ? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) + : Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential), [props.scope]) - const makeContext = React.useCallback((scope: Scope.CloseableScope) => Effect.context().pipe( + const makeContext = (scope: Scope.CloseableScope) => Effect.context().pipe( Effect.provide(props.layer), Effect.provideService(Scope.Scope, scope), - ), [props.layer]) + ) + + const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe( + effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe( + Match.when("sync", () => { runSync(effect) }), + Match.when("fork", () => { runFork(effect) }), + Match.exhaustive, + ) + ) const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe( Effect.bind("isInitialRun", () => Ref.make(true)), @@ -57,7 +68,7 @@ const makeProvider = (Context: React.Context>): ReactProvi Effect.if({ onTrue: () => Ref.set(isInitialRun, false).pipe( Effect.map(() => - () => runSync(Scope.close(initialScope, Exit.void)) + () => closeScope(initialScope) ) ), @@ -68,13 +79,13 @@ const makeProvider = (Context: React.Context>): ReactProvi Effect.sync(() => setValue(context)) ), Effect.map(({ scope }) => - () => runSync(Scope.close(scope, Exit.void)) + () => closeScope(scope) ), ), }), runSync, - ), [makeScope, makeContext, runSync]) + ), [makeScope, runSync, runFork]) return React.createElement(Context, { ...props, value }) } @@ -84,6 +95,7 @@ export type AsyncReactProvider = React.FC<{ readonly layer: Layer.Layer readonly scope?: Scope.Scope readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy + readonly finalizerExecutionMode?: "sync" | "fork" readonly fallback?: React.ReactNode readonly children?: React.ReactNode }> @@ -112,7 +124,7 @@ const makeAsyncProvider = (Context: React.Context>): Async const scope = runSync(props.scope ? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) - : Scope.make(props.finalizerExecutionStrategy) + : Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) ) Effect.context().pipe( @@ -126,7 +138,13 @@ const makeAsyncProvider = (Context: React.Context>): Async effect => runFork(effect, { ...props, scope }), ) - return () => { runFork(Scope.close(scope, Exit.void)) } + return () => Scope.close(scope, Exit.void).pipe( + effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe( + Match.when("sync", () => { runSync(effect) }), + Match.when("fork", () => { runFork(effect) }), + Match.exhaustive, + ) + ) }, [props.layer, runSync, runFork]) return React.createElement(React.Suspense, { diff --git a/packages/reffuse/src/ReffuseNamespace.ts b/packages/reffuse/src/ReffuseNamespace.ts index 87ffb59..10290b5 100644 --- a/packages/reffuse/src/ReffuseNamespace.ts +++ b/packages/reffuse/src/ReffuseNamespace.ts @@ -15,6 +15,7 @@ export interface ScopeOptions { } export interface UseScopeOptions extends RenderOptions, ScopeOptions { + readonly scope?: Scope.Scope readonly finalizerExecutionMode?: "sync" | "fork" } @@ -101,10 +102,23 @@ export abstract class ReffuseNamespace { const runSync = this.useRunSync() const runFork = this.useRunFork() + const makeScope = React.useMemo(() => options?.scope + ? Scope.fork(options.scope, options.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) + : Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential), + [options?.scope]) + + const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe( + effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe( + Match.when("sync", () => { runSync(effect) }), + Match.when("fork", () => { runFork(effect) }), + Match.exhaustive, + ) + ) + const [isInitialRun, initialScope] = React.useMemo(() => runSync(Effect.all([ Ref.make(true), - Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential), - ])), []) + makeScope, + ])), [makeScope]) const [scope, setScope] = React.useState(initialScope) @@ -112,29 +126,18 @@ export abstract class ReffuseNamespace { Effect.if({ onTrue: () => Effect.as( Ref.set(isInitialRun, false), - () => Scope.close(initialScope, Exit.void).pipe( - effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe( - Match.when("sync", () => { runSync(effect) }), - Match.when("fork", () => { runFork(effect) }), - Match.exhaustive, - ) - ), + () => closeScope(initialScope), ), - onFalse: () => Scope.make(options?.finalizerExecutionStrategy).pipe( + onFalse: () => makeScope.pipe( Effect.tap(v => Effect.sync(() => setScope(v))), - Effect.map(v => () => Scope.close(v, Exit.void).pipe( - effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe( - Match.when("sync", () => { runSync(effect) }), - Match.when("fork", () => { runFork(effect) }), - Match.exhaustive, - ) - )), + Effect.map(v => () => closeScope(v)), ), }), runSync, ), [ + makeScope, ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork], ...deps, ])