0.2.2 #31

Merged
Thilawyn merged 184 commits from next into master 2026-01-16 17:05:31 +01: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/noBannedTypes: {} is the default type for React props */
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ /** 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 * as React from "react"
import { Memoized } from "./index.js" import { Memoized } from "./index.js"
@@ -60,8 +60,8 @@ const ComponentProto = Object.freeze({
) { ) {
const self = this const self = this
// biome-ignore lint/style/noNonNullAssertion: React ref initialization // biome-ignore lint/style/noNonNullAssertion: React ref initialization
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!) const runtimeRef = React.useRef<Runtime.Runtime<ComponentScopeMap | Exclude<R, Scope.Scope>>>(null!)
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>() runtimeRef.current = yield* Effect.runtime<ComponentScopeMap | Exclude<R, Scope.Scope>>()
return React.useRef(function ScopeProvider(props: P) { return React.useRef(function ScopeProvider(props: P) {
const scope = Runtime.runSync(runtimeRef.current)(useScope( 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: { export const useScope: {
( (
deps: React.DependencyList, deps: React.DependencyList,
options?: ScopeOptions, options?: ScopeOptions,
): Effect.Effect<Scope.Scope> ): Effect.Effect<Scope.Scope, never, ComponentScopeMap>
} = Effect.fnUntraced(function*(deps, options) { } = 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 key = React.useId()
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([ const scopeMap = yield* ComponentScopeMap
Ref.make(true),
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
])), [])
const [scope, setScope] = React.useState(initialScope)
React.useEffect(() => Runtime.runSync(runtime)( const scope = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
Effect.if(isInitialRun, { scopeMap.ref,
onTrue: () => Effect.as( map => Option.match(HashMap.get(map, key), {
Ref.set(isInitialRun, false), onSome: entry => Effect.succeed(entry.scope),
() => closeScope(scope, runtime, options), onNone: () => Effect.tap(
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
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 // 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 return scope
}) })
@@ -572,7 +610,7 @@ export const useContext: {
<ROut, E, RIn>( <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>, layer: Layer.Layer<ROut, E, RIn>,
options?: ScopeOptions, options?: ScopeOptions,
): Effect.Effect<Context.Context<ROut>, E, RIn> ): Effect.Effect<Context.Context<ROut>, E, RIn | ComponentScopeMap>
} = Effect.fnUntraced(function* <ROut, E, RIn>( } = Effect.fnUntraced(function* <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>, layer: Layer.Layer<ROut, E, RIn>,
options?: ScopeOptions, options?: ScopeOptions,

View File

@@ -1,6 +1,7 @@
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ /** 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 React from "react"
import * as Component from "./Component.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/ReactRuntime") 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>( export const make = <R, ER>(
layer: Layer.Layer<R, ER>, layer: Layer.Layer<R, ER>,
memoMap?: Layer.MemoMap, memoMap?: Layer.MemoMap,
): ReactRuntime<R, ER> => Object.setPrototypeOf( ): ReactRuntime<R | Component.ComponentScopeMap, ER> => Object.setPrototypeOf(
Object.assign(function() {}, { 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 // biome-ignore lint/style/noNonNullAssertion: context initialization
context: React.createContext<Runtime.Runtime<R>>(null!), context: React.createContext<Runtime.Runtime<R>>(null!),
}), }),

View File

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