From a89f5a7ec2fe94be8da8ce99ff1766d405b8b698 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 15 Sep 2023 13:27:39 +1200 Subject: [PATCH 1/4] add RuntimeContext module --- src/FiberStore.ts | 7 +- src/Hooks.ts | 45 ++++++ src/Result.ts | 3 + src/ResultBag.ts | 5 +- src/{ => ResultBag}/TrackedProperties.ts | 5 +- src/RuntimeContext.ts | 141 ++++++++++++++++++ src/RuntimeProvider.ts | 100 ------------- src/internal/fiberStore.ts | 11 +- src/internal/hooks.ts | 71 +++++++++ src/internal/hooks/useResult.ts | 32 ---- src/internal/hooks/useResultCallback.ts | 27 ---- src/internal/resultBag.ts | 2 +- .../{ => resultBag}/trackedProperties.ts | 2 +- test/hooks/useResult.ts | 6 +- test/hooks/useResultCallback.ts | 6 +- 15 files changed, 289 insertions(+), 174 deletions(-) create mode 100644 src/Hooks.ts rename src/{ => ResultBag}/TrackedProperties.ts (89%) create mode 100644 src/RuntimeContext.ts delete mode 100644 src/RuntimeProvider.ts create mode 100644 src/internal/hooks.ts delete mode 100644 src/internal/hooks/useResult.ts delete mode 100644 src/internal/hooks/useResultCallback.ts rename src/internal/{ => resultBag}/trackedProperties.ts (94%) diff --git a/src/FiberStore.ts b/src/FiberStore.ts index fcbfe96..4d92eb3 100644 --- a/src/FiberStore.ts +++ b/src/FiberStore.ts @@ -1,7 +1,10 @@ -import type * as Runtime from "@effect/io/Runtime" +/** + * @since 1.0.0 + */ import type * as Stream from "@effect/stream/Stream" import * as internal from "effect-react/internal/fiberStore" import type * as ResultBag from "effect-react/ResultBag" +import type * as RuntimeContext from "effect-react/RuntimeContext" /** * @since 1.0.0 @@ -18,4 +21,4 @@ export interface FiberStore { * @since 1.0.0 * @category constructors */ -export const make: (runtime: Runtime.Runtime) => FiberStore = internal.make +export const make: (runtime: RuntimeContext.RuntimeEffect) => FiberStore = internal.make diff --git a/src/Hooks.ts b/src/Hooks.ts new file mode 100644 index 0000000..575b21c --- /dev/null +++ b/src/Hooks.ts @@ -0,0 +1,45 @@ +/** + * @since 1.0.0 + */ +import type * as Stream from "@effect/stream/Stream" +import * as internal from "effect-react/internal/hooks" +import type * as ResultBag from "effect-react/ResultBag" +import type * as RuntimeContext from "effect-react/RuntimeContext" +import type { DependencyList } from "react" + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + runtimeContext: RuntimeContext.ReactContext +) => { + useResult: ( + evaluate: () => Stream.Stream, + deps: DependencyList + ) => ResultBag.ResultBag + useResultCallback: , R0 extends R, E, A>( + f: (...args: Args) => Stream.Stream + ) => readonly [ResultBag.ResultBag, (...args: Args) => void] +} = internal.make + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeUseResult: ( + runtimeContext: RuntimeContext.ReactContext +) => ( + evaluate: () => Stream.Stream, + deps: DependencyList +) => ResultBag.ResultBag = internal.makeUseResult + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeUseResultCallback: ( + runtimeContext: RuntimeContext.ReactContext +) => , R0 extends R, E, A>( + f: (...args: Args) => Stream.Stream +) => readonly [ResultBag.ResultBag, (...args: Args) => void] = internal.makeUseResultCallback diff --git a/src/Result.ts b/src/Result.ts index 256717c..3ae34a8 100644 --- a/src/Result.ts +++ b/src/Result.ts @@ -1,3 +1,6 @@ +/** + * @since 1.0.0 + */ import type * as Data from "@effect/data/Data" import type * as Option from "@effect/data/Option" import type { Pipeable } from "@effect/data/Pipeable" diff --git a/src/ResultBag.ts b/src/ResultBag.ts index 30cec18..997f060 100644 --- a/src/ResultBag.ts +++ b/src/ResultBag.ts @@ -1,8 +1,11 @@ +/** + * @since 1.0.0 + */ import type * as Option from "@effect/data/Option" import type * as Cause from "@effect/io/Cause" import * as internal from "effect-react/internal/resultBag" import type * as Result from "effect-react/Result" -import type * as TrackedProperties from "effect-react/TrackedProperties" +import type * as TrackedProperties from "effect-react/ResultBag/TrackedProperties" /** * @since 1.0.0 diff --git a/src/TrackedProperties.ts b/src/ResultBag/TrackedProperties.ts similarity index 89% rename from src/TrackedProperties.ts rename to src/ResultBag/TrackedProperties.ts index 910e4c3..522ea58 100644 --- a/src/TrackedProperties.ts +++ b/src/ResultBag/TrackedProperties.ts @@ -1,6 +1,9 @@ +/** + * @since 1.0.0 + */ import type * as Option from "@effect/data/Option" import type * as Cause from "@effect/io/Cause" -import * as internal from "effect-react/internal/trackedProperties" +import * as internal from "effect-react/internal/resultBag/trackedProperties" import type * as Result from "effect-react/Result" /** diff --git a/src/RuntimeContext.ts b/src/RuntimeContext.ts new file mode 100644 index 0000000..d705630 --- /dev/null +++ b/src/RuntimeContext.ts @@ -0,0 +1,141 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "@effect/data/Context" +import { dual, pipe } from "@effect/data/Function" +import * as Effect from "@effect/io/Effect" +import * as Exit from "@effect/io/Exit" +import * as Fiber from "@effect/io/Fiber" +import * as Layer from "@effect/io/Layer" +import * as Runtime from "@effect/io/Runtime" +import * as Scope from "@effect/io/Scope" +import * as React from "react" + +/** + * @since 1.0.0 + * @category type ids + */ +export const RuntimeContextTypeId = Symbol.for("@effect/react/RuntimeContext") + +/** + * @since 1.0.0 + * @category type ids + */ +export type RuntimeContextTypeId = typeof RuntimeContextTypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface RuntimeContext extends ReactContext { + readonly [RuntimeContextTypeId]: { + readonly scope: Scope.CloseableScope + readonly context: Effect.Effect> + } +} + +/** + * @since 1.0.0 + * @category models + */ +export type ReactContext = React.Context>> + +/** + * @since 1.0.0 + * @category models + */ +export type RuntimeEffect = Effect.Effect> + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromContext = ( + context: Context.Context +): RuntimeContext => fromContextEffect(Effect.succeed(context)) + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromContextEffect = ( + effect: Effect.Effect> +): RuntimeContext => { + const scope = Effect.runSync(Scope.make()) + const context = Scope.use(effect, scope) + const runtime = pipe( + context, + Effect.flatMap((context) => Effect.provideContext(Effect.runtime(), context)), + Effect.cached, + Effect.runSync + ) + // populate the cache + Effect.runFork(runtime) + + const RuntimeContext = React.createContext(runtime) + return { + ...RuntimeContext, + [RuntimeContextTypeId]: { + scope, + context + } + } +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const fromLayer = (layer: Layer.Layer): RuntimeContext => + fromContextEffect(Effect.runSync(Effect.cached(Layer.build(layer)))) + +/** + * @since 1.0.0 + * @category combinators + */ +export const use = dual< + ( + layer: Layer.Layer + ) => (self: RuntimeContext) => RuntimeContext, + ( + self: RuntimeContext, + layer: Layer.Layer + ) => RuntimeContext +>(2, (self, layer) => { + const context = self[RuntimeContextTypeId].context + return fromLayer(Layer.provideMerge(Layer.effectContext(context), layer)) +}) + +/** + * @since 1.0.0 + * @category combinators + */ +export const closeEffect = (self: RuntimeContext): Effect.Effect => + Scope.close(self[RuntimeContextTypeId].scope, Exit.unit) + +/** + * @since 1.0.0 + * @category combinators + */ +export const close = (self: RuntimeContext): () => void => { + const effect = closeEffect(self) + return () => { + Effect.runFork(effect) + } +} + +/** + * @since 1.0.0 + * @category combinators + */ +export const provide = ( + runtime: RuntimeEffect +) => + (effect: Effect.Effect): Effect.Effect => + Effect.flatMap( + runtime, + (runtime) => { + const fiber = Runtime.runFork(runtime)(effect) + return Fiber.join(fiber) + } + ) diff --git a/src/RuntimeProvider.ts b/src/RuntimeProvider.ts deleted file mode 100644 index f6982d9..0000000 --- a/src/RuntimeProvider.ts +++ /dev/null @@ -1,100 +0,0 @@ -"use client" -import type { LazyArg } from "@effect/data/Function" -import { pipe } from "@effect/data/Function" -import * as Effect from "@effect/io/Effect" -import * as Layer from "@effect/io/Layer" -import type * as Runtime from "@effect/io/Runtime" -import * as Scope from "@effect/io/Scope" -import type * as Stream from "@effect/stream/Stream" -import * as internalUseResult from "effect-react/internal/hooks/useResult" -import * as internalUseResultCallback from "effect-react/internal/hooks/useResultCallback" -import type * as ResultBag from "effect-react/ResultBag" -import type { DependencyList } from "react" -import { createContext } from "react" - -/** - * @since 1.0.0 - * @category models - */ -export type RuntimeContext = React.Context> - -/** - * @since 1.0.0 - * @category hooks - */ -export type UseResult = ( - evaluate: LazyArg>, - deps: DependencyList -) => ResultBag.ResultBag - -/** - * @since 1.0.0 - * @category hooks - */ -export type UseResultCallback = , R0 extends R, E, A>( - f: (...args: Args) => Stream.Stream -) => readonly [ResultBag.ResultBag, (...args: Args) => void] - -/** - * @since 1.0.0 - * @category models - */ -export interface ReactEffectBag { - readonly RuntimeContext: React.Context> - readonly useResultCallback: UseResultCallback - readonly useResult: UseResult -} - -/** - * @since 1.0.0 - * @category constructors - */ -export const makeFromLayer = ( - layer: Layer.Layer -): ReactEffectBag => { - const scope = Effect.runSync(Scope.make()) - - const runtime = pipe( - Layer.toRuntime(layer), - Effect.provideService(Scope.Scope, scope), - Effect.runSync - ) - - const RuntimeContext = createContext(runtime) - - return { - RuntimeContext, - useResultCallback: internalUseResultCallback.make(RuntimeContext), - useResult: internalUseResult.make(RuntimeContext) - } -} - -/** - * @since 1.0.0 - * @category constructors - */ -export const makeFromRuntime = ( - runtime: Runtime.Runtime -): ReactEffectBag => { - const RuntimeContext = createContext(runtime) - - return { - RuntimeContext, - useResultCallback: internalUseResultCallback.make(RuntimeContext), - useResult: internalUseResult.make(RuntimeContext) - } -} - -/** - * @since 1.0.0 - * @category constructors - */ -export const makeFromRuntimeContext = ( - RuntimeContext: React.Context> -): ReactEffectBag => { - return { - RuntimeContext, - useResultCallback: internalUseResultCallback.make(RuntimeContext), - useResult: internalUseResult.make(RuntimeContext) - } -} diff --git a/src/internal/fiberStore.ts b/src/internal/fiberStore.ts index 88ebd96..c970d1e 100644 --- a/src/internal/fiberStore.ts +++ b/src/internal/fiberStore.ts @@ -2,21 +2,21 @@ import { pipe } from "@effect/data/Function" import * as Effect from "@effect/io/Effect" import type * as Fiber from "@effect/io/Fiber" import * as Ref from "@effect/io/Ref" -import * as Runtime from "@effect/io/Runtime" import * as Stream from "@effect/stream/Stream" import type * as FiberStore from "effect-react/FiberStore" import * as Result from "effect-react/Result" import * as ResultBag from "effect-react/ResultBag" -import * as TrackedProperties from "effect-react/TrackedProperties" +import * as TrackedProperties from "effect-react/ResultBag/TrackedProperties" +import * as RuntimeContext from "effect-react/RuntimeContext" /** @internal */ export const make = ( - runtime: Runtime.Runtime + runtime: RuntimeContext.RuntimeEffect ): FiberStore.FiberStore => new FiberStoreImpl(runtime) class FiberStoreImpl implements FiberStore.FiberStore { constructor( - readonly runtime: Runtime.Runtime + readonly runtime: RuntimeContext.RuntimeEffect ) {} // listeners @@ -82,8 +82,9 @@ class FiberStoreImpl implements FiberStore.FiberStore { return stream }), Stream.runForEach((_) => maybeSetResult(Result.success(_))), + RuntimeContext.provide(this.runtime), Effect.catchAllCause((cause) => maybeSetResult(Result.failCause(cause))), - Runtime.runFork(this.runtime) + Effect.runFork ) this.fiberState = { diff --git a/src/internal/hooks.ts b/src/internal/hooks.ts new file mode 100644 index 0000000..65a948b --- /dev/null +++ b/src/internal/hooks.ts @@ -0,0 +1,71 @@ +import * as Hash from "@effect/data/Hash" +import type * as Stream from "@effect/stream/Stream" +import * as FiberStore from "effect-react/FiberStore" +import type * as ResultBag from "effect-react/ResultBag" +import type * as RuntimeContext from "effect-react/RuntimeContext" +import type { DependencyList } from "react" +import { useCallback, useContext, useRef, useSyncExternalStore } from "react" + +/** @internal */ +export const make = ( + runtimeContext: RuntimeContext.ReactContext +): { + useResult: ( + evaluate: () => Stream.Stream, + deps: DependencyList + ) => ResultBag.ResultBag + useResultCallback: , R0 extends R, E, A>( + f: (...args: Args) => Stream.Stream + ) => readonly [ResultBag.ResultBag, (...args: Args) => void] +} => ({ + useResult: makeUseResult(runtimeContext), + useResultCallback: makeUseResultCallback(runtimeContext) +}) + +/** @internal */ +export const makeUseResult = ( + runtimeContext: RuntimeContext.ReactContext +) => + ( + evaluate: () => Stream.Stream, + deps: DependencyList + ): ResultBag.ResultBag => { + const runtime = useContext(runtimeContext) + const storeRef = useRef>(undefined as any) + if (storeRef.current === undefined) { + storeRef.current = FiberStore.make(runtime) + } + const resultBag = useSyncExternalStore( + storeRef.current.subscribe, + storeRef.current.snapshot + ) + const depsHash = useRef(null as any) + const currentDepsHash = Hash.array(deps) + if (depsHash.current !== currentDepsHash) { + depsHash.current = currentDepsHash + storeRef.current.run(evaluate()) + } + return resultBag + } + +/** @internal */ +export const makeUseResultCallback = ( + runtimeContext: RuntimeContext.ReactContext +) => + , R0 extends R, E, A>( + f: (...args: Args) => Stream.Stream + ): readonly [ResultBag.ResultBag, (...args: Args) => void] => { + const runtime = useContext(runtimeContext) + const storeRef = useRef>(undefined as any) + if (storeRef.current === undefined) { + storeRef.current = FiberStore.make(runtime) + } + const resultBag = useSyncExternalStore( + storeRef.current.subscribe, + storeRef.current.snapshot + ) + const run = useCallback((...args: Args) => { + storeRef.current.run(f(...args)) + }, [f]) + return [resultBag, run] as const + } diff --git a/src/internal/hooks/useResult.ts b/src/internal/hooks/useResult.ts deleted file mode 100644 index 5985b5d..0000000 --- a/src/internal/hooks/useResult.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as Hash from "@effect/data/Hash" -import type * as Stream from "@effect/stream/Stream" -import * as FiberStore from "effect-react/FiberStore" -import type * as ResultBag from "effect-react/ResultBag" -import type * as RuntimeProvider from "effect-react/RuntimeProvider" -import type { DependencyList } from "react" -import { useContext, useRef, useSyncExternalStore } from "react" - -export const make = ( - runtimeContext: RuntimeProvider.RuntimeContext -): RuntimeProvider.UseResult => - ( - evaluate: () => Stream.Stream, - deps: DependencyList - ): ResultBag.ResultBag => { - const runtime = useContext(runtimeContext) - const storeRef = useRef>(undefined as any) - if (storeRef.current === undefined) { - storeRef.current = FiberStore.make(runtime) - } - const resultBag = useSyncExternalStore( - storeRef.current.subscribe, - storeRef.current.snapshot - ) - const depsHash = useRef(null as any) - const currentDepsHash = Hash.array(deps) - if (depsHash.current !== currentDepsHash) { - depsHash.current = currentDepsHash - storeRef.current.run(evaluate()) - } - return resultBag - } diff --git a/src/internal/hooks/useResultCallback.ts b/src/internal/hooks/useResultCallback.ts deleted file mode 100644 index 8f4568f..0000000 --- a/src/internal/hooks/useResultCallback.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type * as Stream from "@effect/stream/Stream" -import * as FiberStore from "effect-react/FiberStore" -import type * as ResultBag from "effect-react/ResultBag" -import type * as RuntimeProvider from "effect-react/RuntimeProvider" -import { useCallback, useContext, useRef, useSyncExternalStore } from "react" - -/** @internal */ -export const make = ( - runtimeContext: RuntimeProvider.RuntimeContext -): RuntimeProvider.UseResultCallback => - , R0 extends R, E, A>( - f: (...args: Args) => Stream.Stream - ): readonly [ResultBag.ResultBag, (...args: Args) => void] => { - const runtime = useContext(runtimeContext) - const storeRef = useRef>(undefined as any) - if (storeRef.current === undefined) { - storeRef.current = FiberStore.make(runtime) - } - const resultBag = useSyncExternalStore( - storeRef.current.subscribe, - storeRef.current.snapshot - ) - const run = useCallback((...args: Args) => { - storeRef.current.run(f(...args)) - }, [f]) - return [resultBag, run] as const - } diff --git a/src/internal/resultBag.ts b/src/internal/resultBag.ts index bfe16af..a02605f 100644 --- a/src/internal/resultBag.ts +++ b/src/internal/resultBag.ts @@ -5,7 +5,7 @@ import * as Order from "@effect/data/Order" import type * as Cause from "@effect/io/Cause" import * as Result from "effect-react/Result" import type * as ResultBag from "effect-react/ResultBag" -import type * as TrackedProperties from "effect-react/TrackedProperties" +import type * as TrackedProperties from "effect-react/ResultBag/TrackedProperties" const optionDateGreaterThan = pipe( N.Order, diff --git a/src/internal/trackedProperties.ts b/src/internal/resultBag/trackedProperties.ts similarity index 94% rename from src/internal/trackedProperties.ts rename to src/internal/resultBag/trackedProperties.ts index b4f5d6c..dfed889 100644 --- a/src/internal/trackedProperties.ts +++ b/src/internal/resultBag/trackedProperties.ts @@ -1,7 +1,7 @@ import * as Option from "@effect/data/Option" import * as Cause from "@effect/io/Cause" import type * as Result from "effect-react/Result" -import type * as TrackedProperties from "effect-react/TrackedProperties" +import type * as TrackedProperties from "effect-react/ResultBag/TrackedProperties" /** @internal */ export const initial = (): TrackedProperties.TrackedProperties => ({ diff --git a/test/hooks/useResult.ts b/test/hooks/useResult.ts index 04351c9..7971f5f 100644 --- a/test/hooks/useResult.ts +++ b/test/hooks/useResult.ts @@ -3,8 +3,9 @@ import * as Effect from "@effect/io/Effect" import * as Layer from "@effect/io/Layer" import * as Stream from "@effect/stream/Stream" import { renderHook, waitFor } from "@testing-library/react" +import * as Hooks from "effect-react/Hooks" import * as Result from "effect-react/Result" -import * as RuntimeProvider from "effect-react/RuntimeProvider" +import * as RuntimeContext from "effect-react/RuntimeContext" import { describe, expect, it } from "vitest" interface Foo { @@ -12,7 +13,8 @@ interface Foo { } const foo = Context.Tag() -const { useResult } = RuntimeProvider.makeFromLayer(Layer.succeed(foo, { value: 1 })) +const context = RuntimeContext.fromLayer(Layer.succeed(foo, { value: 1 })) +const useResult = Hooks.makeUseResult(context) describe("useResult", () => { it("should run effects", async () => { diff --git a/test/hooks/useResultCallback.ts b/test/hooks/useResultCallback.ts index 48c121a..18bf0c6 100644 --- a/test/hooks/useResultCallback.ts +++ b/test/hooks/useResultCallback.ts @@ -2,8 +2,9 @@ import * as Context from "@effect/data/Context" import * as Effect from "@effect/io/Effect" import * as Layer from "@effect/io/Layer" import { act, renderHook, waitFor } from "@testing-library/react" +import * as Hooks from "effect-react/Hooks" import * as Result from "effect-react/Result" -import * as RuntimeProvider from "effect-react/RuntimeProvider" +import * as RuntimeContext from "effect-react/RuntimeContext" import { describe, expect, it } from "vitest" interface Foo { @@ -11,7 +12,8 @@ interface Foo { } const foo = Context.Tag() -const { useResultCallback } = RuntimeProvider.makeFromLayer(Layer.succeed(foo, { value: 1 })) +const context = RuntimeContext.fromLayer(Layer.succeed(foo, { value: 1 })) +const useResultCallback = Hooks.makeUseResultCallback(context) describe("useResultCallback", () => { it("should do good", async () => { From 2e59d9d1fd4fcf8eb5eb6f4ebd0fb0e7a70f5525 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 15 Sep 2023 13:36:54 +1200 Subject: [PATCH 2/4] api rename --- src/RuntimeContext.ts | 4 ++-- src/internal/fiberStore.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RuntimeContext.ts b/src/RuntimeContext.ts index d705630..183c78f 100644 --- a/src/RuntimeContext.ts +++ b/src/RuntimeContext.ts @@ -93,7 +93,7 @@ export const fromLayer = (layer: Layer.Layer): RuntimeContext * @since 1.0.0 * @category combinators */ -export const use = dual< +export const provideMerge = dual< ( layer: Layer.Layer ) => (self: RuntimeContext) => RuntimeContext, @@ -128,7 +128,7 @@ export const close = (self: RuntimeContext): () => void => { * @since 1.0.0 * @category combinators */ -export const provide = ( +export const runForkJoin = ( runtime: RuntimeEffect ) => (effect: Effect.Effect): Effect.Effect => diff --git a/src/internal/fiberStore.ts b/src/internal/fiberStore.ts index c970d1e..fc5b068 100644 --- a/src/internal/fiberStore.ts +++ b/src/internal/fiberStore.ts @@ -82,7 +82,7 @@ class FiberStoreImpl implements FiberStore.FiberStore { return stream }), Stream.runForEach((_) => maybeSetResult(Result.success(_))), - RuntimeContext.provide(this.runtime), + RuntimeContext.runForkJoin(this.runtime), Effect.catchAllCause((cause) => maybeSetResult(Result.failCause(cause))), Effect.runFork ) From 6546dbe9a9f5530f7598bf8d3bad49039d5d61fe Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 15 Sep 2023 14:31:16 +1200 Subject: [PATCH 3/4] make failed runtime build a defect --- src/FiberStore.ts | 2 +- src/Hooks.ts | 24 ++++++++++------------ src/RuntimeContext.ts | 41 ++++++++++++++++++++++---------------- src/internal/fiberStore.ts | 4 ++-- src/internal/hooks.ts | 28 +++++++++++++------------- 5 files changed, 52 insertions(+), 47 deletions(-) diff --git a/src/FiberStore.ts b/src/FiberStore.ts index 4d92eb3..48ac637 100644 --- a/src/FiberStore.ts +++ b/src/FiberStore.ts @@ -21,4 +21,4 @@ export interface FiberStore { * @since 1.0.0 * @category constructors */ -export const make: (runtime: RuntimeContext.RuntimeEffect) => FiberStore = internal.make +export const make: (runtime: RuntimeContext.RuntimeEffect) => FiberStore = internal.make diff --git a/src/Hooks.ts b/src/Hooks.ts index 575b21c..7de372f 100644 --- a/src/Hooks.ts +++ b/src/Hooks.ts @@ -11,35 +11,33 @@ import type { DependencyList } from "react" * @since 1.0.0 * @category constructors */ -export const make: ( - runtimeContext: RuntimeContext.ReactContext +export const make: ( + runtimeContext: RuntimeContext.ReactContext ) => { useResult: ( evaluate: () => Stream.Stream, deps: DependencyList - ) => ResultBag.ResultBag + ) => ResultBag.ResultBag useResultCallback: , R0 extends R, E, A>( f: (...args: Args) => Stream.Stream - ) => readonly [ResultBag.ResultBag, (...args: Args) => void] + ) => readonly [ResultBag.ResultBag, (...args: Args) => void] } = internal.make /** * @since 1.0.0 * @category constructors */ -export const makeUseResult: ( - runtimeContext: RuntimeContext.ReactContext -) => ( - evaluate: () => Stream.Stream, - deps: DependencyList -) => ResultBag.ResultBag = internal.makeUseResult +export const makeUseResult: ( + runtimeContext: RuntimeContext.ReactContext +) => (evaluate: () => Stream.Stream, deps: DependencyList) => ResultBag.ResultBag = + internal.makeUseResult /** * @since 1.0.0 * @category constructors */ -export const makeUseResultCallback: ( - runtimeContext: RuntimeContext.ReactContext +export const makeUseResultCallback: ( + runtimeContext: RuntimeContext.ReactContext ) => , R0 extends R, E, A>( f: (...args: Args) => Stream.Stream -) => readonly [ResultBag.ResultBag, (...args: Args) => void] = internal.makeUseResultCallback +) => readonly [ResultBag.ResultBag, (...args: Args) => void] = internal.makeUseResultCallback diff --git a/src/RuntimeContext.ts b/src/RuntimeContext.ts index 183c78f..9b0d292 100644 --- a/src/RuntimeContext.ts +++ b/src/RuntimeContext.ts @@ -27,10 +27,10 @@ export type RuntimeContextTypeId = typeof RuntimeContextTypeId * @since 1.0.0 * @category models */ -export interface RuntimeContext extends ReactContext { +export interface RuntimeContext extends ReactContext { readonly [RuntimeContextTypeId]: { readonly scope: Scope.CloseableScope - readonly context: Effect.Effect> + readonly context: Effect.Effect> } } @@ -38,13 +38,13 @@ export interface RuntimeContext extends ReactContext { * @since 1.0.0 * @category models */ -export type ReactContext = React.Context>> +export type ReactContext = React.Context>> /** * @since 1.0.0 * @category models */ -export type RuntimeEffect = Effect.Effect> +export type RuntimeEffect = Effect.Effect> /** * @since 1.0.0 @@ -52,7 +52,7 @@ export type RuntimeEffect = Effect.Effect> */ export const fromContext = ( context: Context.Context -): RuntimeContext => fromContextEffect(Effect.succeed(context)) +): RuntimeContext => fromContextEffect(Effect.succeed(context)) /** * @since 1.0.0 @@ -60,9 +60,16 @@ export const fromContext = ( */ export const fromContextEffect = ( effect: Effect.Effect> -): RuntimeContext => { +): RuntimeContext => { const scope = Effect.runSync(Scope.make()) - const context = Scope.use(effect, scope) + const error = new Error() + const context = Scope.use( + Effect.orDieWith(effect, (e) => { + error.message = `Could not build RuntimeContext: ${e}` + return error + }), + scope + ) const runtime = pipe( context, Effect.flatMap((context) => Effect.provideContext(Effect.runtime(), context)), @@ -86,7 +93,7 @@ export const fromContextEffect = ( * @since 1.0.0 * @category constructors */ -export const fromLayer = (layer: Layer.Layer): RuntimeContext => +export const fromLayer = (layer: Layer.Layer): RuntimeContext => fromContextEffect(Effect.runSync(Effect.cached(Layer.build(layer)))) /** @@ -96,11 +103,11 @@ export const fromLayer = (layer: Layer.Layer): RuntimeContext export const provideMerge = dual< ( layer: Layer.Layer - ) => (self: RuntimeContext) => RuntimeContext, - ( - self: RuntimeContext, + ) => (self: RuntimeContext) => RuntimeContext, + ( + self: RuntimeContext, layer: Layer.Layer - ) => RuntimeContext + ) => RuntimeContext >(2, (self, layer) => { const context = self[RuntimeContextTypeId].context return fromLayer(Layer.provideMerge(Layer.effectContext(context), layer)) @@ -110,14 +117,14 @@ export const provideMerge = dual< * @since 1.0.0 * @category combinators */ -export const closeEffect = (self: RuntimeContext): Effect.Effect => +export const closeEffect = (self: RuntimeContext): Effect.Effect => Scope.close(self[RuntimeContextTypeId].scope, Exit.unit) /** * @since 1.0.0 * @category combinators */ -export const close = (self: RuntimeContext): () => void => { +export const close = (self: RuntimeContext): () => void => { const effect = closeEffect(self) return () => { Effect.runFork(effect) @@ -128,10 +135,10 @@ export const close = (self: RuntimeContext): () => void => { * @since 1.0.0 * @category combinators */ -export const runForkJoin = ( - runtime: RuntimeEffect +export const runForkJoin = ( + runtime: RuntimeEffect ) => - (effect: Effect.Effect): Effect.Effect => + (effect: Effect.Effect): Effect.Effect => Effect.flatMap( runtime, (runtime) => { diff --git a/src/internal/fiberStore.ts b/src/internal/fiberStore.ts index fc5b068..6220bef 100644 --- a/src/internal/fiberStore.ts +++ b/src/internal/fiberStore.ts @@ -11,12 +11,12 @@ import * as RuntimeContext from "effect-react/RuntimeContext" /** @internal */ export const make = ( - runtime: RuntimeContext.RuntimeEffect + runtime: RuntimeContext.RuntimeEffect ): FiberStore.FiberStore => new FiberStoreImpl(runtime) class FiberStoreImpl implements FiberStore.FiberStore { constructor( - readonly runtime: RuntimeContext.RuntimeEffect + readonly runtime: RuntimeContext.RuntimeEffect ) {} // listeners diff --git a/src/internal/hooks.ts b/src/internal/hooks.ts index 65a948b..55e1dac 100644 --- a/src/internal/hooks.ts +++ b/src/internal/hooks.ts @@ -7,33 +7,33 @@ import type { DependencyList } from "react" import { useCallback, useContext, useRef, useSyncExternalStore } from "react" /** @internal */ -export const make = ( - runtimeContext: RuntimeContext.ReactContext +export const make = ( + runtimeContext: RuntimeContext.ReactContext ): { useResult: ( evaluate: () => Stream.Stream, deps: DependencyList - ) => ResultBag.ResultBag + ) => ResultBag.ResultBag useResultCallback: , R0 extends R, E, A>( f: (...args: Args) => Stream.Stream - ) => readonly [ResultBag.ResultBag, (...args: Args) => void] + ) => readonly [ResultBag.ResultBag, (...args: Args) => void] } => ({ useResult: makeUseResult(runtimeContext), useResultCallback: makeUseResultCallback(runtimeContext) }) /** @internal */ -export const makeUseResult = ( - runtimeContext: RuntimeContext.ReactContext +export const makeUseResult = ( + runtimeContext: RuntimeContext.ReactContext ) => ( evaluate: () => Stream.Stream, deps: DependencyList - ): ResultBag.ResultBag => { + ): ResultBag.ResultBag => { const runtime = useContext(runtimeContext) - const storeRef = useRef>(undefined as any) + const storeRef = useRef>(undefined as any) if (storeRef.current === undefined) { - storeRef.current = FiberStore.make(runtime) + storeRef.current = FiberStore.make(runtime) } const resultBag = useSyncExternalStore( storeRef.current.subscribe, @@ -49,16 +49,16 @@ export const makeUseResult = ( } /** @internal */ -export const makeUseResultCallback = ( - runtimeContext: RuntimeContext.ReactContext +export const makeUseResultCallback = ( + runtimeContext: RuntimeContext.ReactContext ) => , R0 extends R, E, A>( f: (...args: Args) => Stream.Stream - ): readonly [ResultBag.ResultBag, (...args: Args) => void] => { + ): readonly [ResultBag.ResultBag, (...args: Args) => void] => { const runtime = useContext(runtimeContext) - const storeRef = useRef>(undefined as any) + const storeRef = useRef>(undefined as any) if (storeRef.current === undefined) { - storeRef.current = FiberStore.make(runtime) + storeRef.current = FiberStore.make(runtime) } const resultBag = useSyncExternalStore( storeRef.current.subscribe, From eb6da12d47781a48a823f5c1625a992f9de2a0ed Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 15 Sep 2023 16:37:27 +1200 Subject: [PATCH 4/4] add api for overriding runtime --- src/RuntimeContext.ts | 79 ++++++++++++++++++++++++++++++----------- test/hooks/useResult.ts | 13 +++++++ tsconfig.base.json | 1 + 3 files changed, 73 insertions(+), 20 deletions(-) diff --git a/src/RuntimeContext.ts b/src/RuntimeContext.ts index 9b0d292..30b1b3c 100644 --- a/src/RuntimeContext.ts +++ b/src/RuntimeContext.ts @@ -54,6 +54,17 @@ export const fromContext = ( context: Context.Context ): RuntimeContext => fromContextEffect(Effect.succeed(context)) +const makeRuntimeEffect = (context: Effect.Effect>): RuntimeEffect => { + const runtime = pipe( + context, + Effect.flatMap((context) => Effect.provideContext(Effect.runtime(), context)), + Effect.cached, + Effect.runSync + ) + Effect.runFork(runtime) // prime cache + return runtime +} + /** * @since 1.0.0 * @category constructors @@ -70,23 +81,22 @@ export const fromContextEffect = ( }), scope ) - const runtime = pipe( - context, - Effect.flatMap((context) => Effect.provideContext(Effect.runtime(), context)), - Effect.cached, - Effect.runSync - ) - // populate the cache - Effect.runFork(runtime) - + const runtime = makeRuntimeEffect(context) const RuntimeContext = React.createContext(runtime) - return { - ...RuntimeContext, - [RuntimeContextTypeId]: { - scope, - context + return new Proxy(RuntimeContext, { + has(target, p) { + return p === RuntimeContextTypeId || p in target + }, + get(target, p, _receiver) { + if (p === RuntimeContextTypeId) { + return { + scope, + context + } + } + return (target as any)[p] } - } + }) as any } /** @@ -101,18 +111,47 @@ export const fromLayer = (layer: Layer.Layer): RuntimeContext * @category combinators */ export const provideMerge = dual< - ( - layer: Layer.Layer + ( + layer: Layer.Layer ) => (self: RuntimeContext) => RuntimeContext, - ( + ( self: RuntimeContext, - layer: Layer.Layer + layer: Layer.Layer ) => RuntimeContext >(2, (self, layer) => { const context = self[RuntimeContextTypeId].context return fromLayer(Layer.provideMerge(Layer.effectContext(context), layer)) }) +/** + * @since 1.0.0 + * @category combinators + */ +export const toRuntimeEffect = ( + self: RuntimeContext +): RuntimeEffect => makeRuntimeEffect(self[RuntimeContextTypeId].context) + +/** + * @since 1.0.0 + * @category combinators + */ +export const overrideLayer = dual< + ( + layer: Layer.Layer + ) => (self: RuntimeContext) => readonly [React.ExoticComponent, Scope.CloseableScope], + ( + self: RuntimeContext, + layer: Layer.Layer + ) => readonly [React.ExoticComponent, Scope.CloseableScope] +>(2, (self, layer) => { + const context = fromLayer(layer) + const runtime = toRuntimeEffect(context) + return [ + (props: React.PropsWithChildren) => React.createElement(self.Provider, { value: runtime }, props.children), + context[RuntimeContextTypeId].scope + ] as any +}) + /** * @since 1.0.0 * @category combinators @@ -133,7 +172,7 @@ export const close = (self: RuntimeContext): () => void => { /** * @since 1.0.0 - * @category combinators + * @category execution */ export const runForkJoin = ( runtime: RuntimeEffect diff --git a/test/hooks/useResult.ts b/test/hooks/useResult.ts index 7971f5f..91f1e2f 100644 --- a/test/hooks/useResult.ts +++ b/test/hooks/useResult.ts @@ -23,6 +23,19 @@ describe("useResult", () => { expect(Result.isSuccess(result.current.result)).toBe(true) }) + it("override Provider value", async () => { + const [Override] = RuntimeContext.overrideLayer(context, Layer.succeed(foo, { value: 2 })) + const testEffect = Effect.map(foo, (_) => _.value) + const { result } = await waitFor(async () => + renderHook(() => useResult(() => testEffect, []), { + wrapper: Override + }) + ) + await waitFor(() => expect(Result.isSuccess(result.current.result)).toBe(true)) + assert(Result.isSuccess(result.current.result)) + expect(result.current.result.value).toBe(2) + }) + it("should provide context", async () => { const testEffect = Effect.map(foo, (_) => _.value) const { result } = await waitFor(async () => renderHook(() => useResult(() => testEffect, []))) diff --git a/tsconfig.base.json b/tsconfig.base.json index c4ccf8b..ebd072f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -23,6 +23,7 @@ "noUnusedLocals": true, "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, + "jsx": "react", "noEmitOnError": false, "noErrorTruncation": false, "allowJs": false,