0.2.0 #18

Merged
Thilawyn merged 44 commits from next into master 2025-10-24 01:36:27 +02:00
3 changed files with 74 additions and 27 deletions
Showing only changes of commit 72495bb9b5 - Show all commits

View File

@@ -1,6 +1,6 @@
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
import { Context, Effect, Effectable, ExecutionStrategy, Exit, Function, Layer, ManagedRuntime, Predicate, Ref, Runtime, Scope, Tracer, type Types, type Utils } from "effect"
import { Context, Effect, Effectable, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Tracer, type Types, type Utils } from "effect"
import * as React from "react"
import { Memoized } from "./index.js"
@@ -60,8 +60,8 @@ const ComponentProto = Object.freeze({
) {
const self = this
// biome-ignore lint/style/noNonNullAssertion: React ref initialization
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
const runtimeRef = React.useRef<Runtime.Runtime<ComponentScopeMap | Exclude<R, Scope.Scope>>>(null!)
runtimeRef.current = yield* Effect.runtime<ComponentScopeMap | Exclude<R, Scope.Scope>>()
return React.useRef(function ScopeProvider(props: P) {
const scope = Runtime.runSync(runtimeRef.current)(useScope(
@@ -413,35 +413,73 @@ export const withRuntime: {
})
export class ComponentScopeMap extends Effect.Service<ComponentScopeMap>()("effect-fc/Component/ComponentScopeMap", {
effect: Effect.bind(
Effect.Do,
"ref",
() => Ref.make(HashMap.empty<string, ComponentScopeMap.Entry>()),
),
}) {}
export namespace ComponentScopeMap {
export interface Entry {
readonly scope: Scope.CloseableScope
readonly closeFiber: Option.Option<Fiber.RuntimeFiber<void>>
}
}
export const useScope: {
(
deps: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<Scope.Scope>
): Effect.Effect<Scope.Scope, never, ComponentScopeMap>
} = Effect.fnUntraced(function*(deps, options) {
const runtime = yield* Effect.runtime()
// biome-ignore lint/style/noNonNullAssertion: context initialization
const runtimeRef = React.useRef<Runtime.Runtime<never>>(null!)
runtimeRef.current = yield* Effect.runtime()
// biome-ignore lint/correctness/useExhaustiveDependencies: no reactivity needed
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([
Ref.make(true),
const key = React.useId()
const scopeMap = yield* ComponentScopeMap
const scope = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
scopeMap.ref,
map => Option.match(HashMap.get(map, key), {
onSome: entry => Effect.succeed(entry.scope),
onNone: () => Effect.tap(
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),
scope => Ref.update(scopeMap.ref, HashMap.set(key, {
scope,
closeFiber: Option.none(),
}))
),
onFalse: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe(
Effect.tap(scope => Effect.sync(() => setScope(scope))),
Effect.map(scope => () => closeScope(scope, runtime, options)),
),
})
}),
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
), deps)
)), deps)
// biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "scope"
React.useEffect(() => Runtime.runSync(runtimeRef.current)(scopeMap.ref.pipe(
Effect.andThen(HashMap.get(key)),
Effect.tap(entry => Option.match(entry.closeFiber, {
onSome: fiber => Effect.andThen(
Ref.update(scopeMap.ref, HashMap.set(key, { ...entry, closeFiber: Option.none() })),
Fiber.interruptFork(fiber),
),
onNone: () => Effect.void,
})),
Effect.map(({ scope }) =>
() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
Effect.forkDaemon(Effect.andThen(
Effect.sleep("100 millis"),
Scope.close(scope, Exit.void),
)),
fiber => Ref.update(scopeMap.ref, HashMap.set(key, {
scope,
closeFiber: Option.some(fiber),
})),
))
),
)), [scope])
return scope
})
@@ -572,7 +610,7 @@ export const useContext: {
<ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>,
options?: ScopeOptions,
): Effect.Effect<Context.Context<ROut>, E, RIn>
): Effect.Effect<Context.Context<ROut>, E, RIn | ComponentScopeMap>
} = Effect.fnUntraced(function* <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>,
options?: ScopeOptions,

View File

@@ -1,6 +1,7 @@
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
import { Effect, type Layer, ManagedRuntime, Predicate, type Runtime } from "effect"
import { Effect, Layer, ManagedRuntime, Predicate, type Runtime } from "effect"
import * as React from "react"
import * as Component from "./Component.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/ReactRuntime")
@@ -21,9 +22,12 @@ export const isReactRuntime = (u: unknown): u is ReactRuntime<unknown, unknown>
export const make = <R, ER>(
layer: Layer.Layer<R, ER>,
memoMap?: Layer.MemoMap,
): ReactRuntime<R, ER> => Object.setPrototypeOf(
): ReactRuntime<R | Component.ComponentScopeMap, ER> => Object.setPrototypeOf(
Object.assign(function() {}, {
runtime: ManagedRuntime.make(layer, memoMap),
runtime: ManagedRuntime.make(
Layer.merge(layer, Component.ComponentScopeMap.Default),
memoMap,
),
// biome-ignore lint/style/noNonNullAssertion: context initialization
context: React.createContext<Runtime.Runtime<R>>(null!),
}),

View File

@@ -54,6 +54,11 @@ class RegisterFormView extends Component.makeUntraced("RegisterFormView")(functi
const TextFieldFormInputFC = yield* TextFieldFormInput
yield* Component.useOnMount(() => Effect.gen(function*() {
yield* Effect.addFinalizer(() => Console.log("RegisterFormView unmounted"))
yield* Console.log("RegisterFormView mounted")
}))
return (
<Container width="300">