@@ -2,6 +2,7 @@
|
||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||
import { Cause, Context, type Duration, Effect, Equal, Equivalence, Exit, Fiber, Function, HashMap, identity, Layer, Option, Pipeable, Predicate, Ref, Scheduler, Scope, Tracer } from "effect"
|
||||
import * as React from "react"
|
||||
import * as ScopeRegistry from "./ScopeRegistry.js"
|
||||
|
||||
|
||||
export const ComponentTypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
||||
@@ -636,31 +637,6 @@ export const withContext: {
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Internal Effect service that maintains a registry of scopes associated with React component instances.
|
||||
*
|
||||
* This service is used internally by the `useScope` hook to manage the lifecycle of component scopes,
|
||||
* including tracking active scopes and coordinating their cleanup when components unmount or dependencies change.
|
||||
*/
|
||||
export class ScopeMap extends Context.Service<ScopeMap, {
|
||||
readonly ref: Ref.Ref<HashMap.HashMap<object, ScopeMap.Entry>>
|
||||
}>()(
|
||||
"@effect-fc/Component/ScopeMap"
|
||||
) {
|
||||
static readonly layer = Layer.effect(ScopeMap, Effect.map(
|
||||
Ref.make(HashMap.empty<object, ScopeMap.Entry>()),
|
||||
ref => ({ ref }),
|
||||
))
|
||||
}
|
||||
|
||||
export declare namespace ScopeMap {
|
||||
export interface Entry {
|
||||
readonly scope: Scope.Closeable
|
||||
readonly closeFiber: Option.Option<Fiber.Fiber<void>>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export declare namespace useScope {
|
||||
export interface Options {
|
||||
readonly finalizerExecutionStrategy?: "sequential" | "parallel"
|
||||
@@ -694,14 +670,14 @@ export const useScope = Effect.fnUntraced(function*(
|
||||
contextRef.current = yield* Effect.context()
|
||||
|
||||
const { key, scope } = React.useMemo(() => Effect.runSyncWith(contextRef.current)(Effect.Do.pipe(
|
||||
Effect.bind("scopeMapRef", () => Effect.map(
|
||||
ScopeMap as unknown as Effect.Effect<ScopeMap["Service"]>,
|
||||
scopeMap => scopeMap.ref,
|
||||
Effect.bind("scopeRegistryRef", () => Effect.map(
|
||||
ScopeRegistry.ScopeRegistry as unknown as Effect.Effect<ScopeRegistry.ScopeRegistry["Service"]>,
|
||||
scopeRegistry => scopeRegistry.ref,
|
||||
)),
|
||||
Effect.let("key", () => Equal.byReference({})),
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)),
|
||||
Effect.tap(({ scopeMapRef, key, scope }) =>
|
||||
Ref.update(scopeMapRef, HashMap.set(key, Equal.byReference({
|
||||
Effect.tap(({ scopeRegistryRef, key, scope }) =>
|
||||
Ref.update(scopeRegistryRef, HashMap.set(key, Equal.byReference({
|
||||
scope,
|
||||
closeFiber: Option.none(),
|
||||
})))
|
||||
@@ -711,8 +687,8 @@ export const useScope = Effect.fnUntraced(function*(
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "key"
|
||||
React.useEffect(() => Effect.runSyncWith(contextRef.current)(
|
||||
(ScopeMap as unknown as Effect.Effect<ScopeMap["Service"]>).pipe(
|
||||
Effect.map(scopeMap => scopeMap.ref),
|
||||
(ScopeRegistry.ScopeRegistry as unknown as Effect.Effect<ScopeRegistry.ScopeRegistry["Service"]>).pipe(
|
||||
Effect.map(scopeRegistry => scopeRegistry.ref),
|
||||
Effect.tap(ref => Ref.get(ref).pipe(
|
||||
Effect.flatMap(map => Effect.fromOption(HashMap.get(map, key))),
|
||||
Effect.flatMap(entry => Option.match(entry.closeFiber, {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { type Context, Effect, Layer, ManagedRuntime, Predicate } from "effect"
|
||||
import * as React from "react"
|
||||
import * as Component from "./Component.js"
|
||||
import * as ScopeRegistry from "./ScopeRegistry.js"
|
||||
|
||||
|
||||
export const ReactRuntimeTypeId: unique symbol = Symbol.for("@effect-fc/ReactRuntime/ReactRuntime")
|
||||
@@ -16,7 +17,7 @@ export interface ReactRuntime<R, ER> {
|
||||
|
||||
const ReactRuntimePrototype = Object.freeze({ [ReactRuntimeTypeId]: ReactRuntimeTypeId } as const)
|
||||
|
||||
export const preludeLayer: Layer.Layer<Component.ScopeMap> = Component.ScopeMap.layer
|
||||
export const preludeLayer: Layer.Layer<ScopeRegistry.ScopeRegistry> = ScopeRegistry.ScopeRegistry.layer
|
||||
|
||||
export const isReactRuntime = (u: unknown): u is ReactRuntime<unknown, unknown> => Predicate.hasProperty(u, ReactRuntimeTypeId)
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Context, Effect, type Fiber, HashMap, Layer, type Option, Ref, type Scope } from "effect"
|
||||
|
||||
|
||||
/**
|
||||
* Internal Effect service that maintains a registry of scopes associated with React component instances.
|
||||
*
|
||||
* This service is used internally by the `Component.useScope` hook to manage the lifecycle of component scopes,
|
||||
* including tracking active scopes and coordinating their cleanup when components unmount or dependencies change.
|
||||
*/
|
||||
export class ScopeRegistry extends Context.Service<ScopeRegistry, {
|
||||
readonly ref: Ref.Ref<HashMap.HashMap<object, ScopeRegistry.Entry>>
|
||||
}>()(
|
||||
"@effect-fc/ScopeRegistry/ScopeRegistry"
|
||||
) {
|
||||
static readonly layer = Layer.effect(ScopeRegistry, Effect.map(
|
||||
Ref.make(HashMap.empty<object, ScopeRegistry.Entry>()),
|
||||
ref => ({ ref }),
|
||||
))
|
||||
}
|
||||
|
||||
export declare namespace ScopeRegistry {
|
||||
export interface Entry {
|
||||
readonly scope: Scope.Closeable
|
||||
readonly closeFiber: Option.Option<Fiber.Fiber<void>>
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export * as PubSub from "./PubSub.js"
|
||||
export * as Query from "./Query.js"
|
||||
export * as QueryClient from "./QueryClient.js"
|
||||
export * as ReactRuntime from "./ReactRuntime.js"
|
||||
export * as ScopeRegistry from "./ScopeRegistry.js"
|
||||
export * as SetStateAction from "./SetStateAction.js"
|
||||
export * as Stream from "./Stream.js"
|
||||
export * as View from "./View.js"
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as React from "react"
|
||||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import * as Component from "../src/Component.js"
|
||||
import * as ReactRuntime from "../src/ReactRuntime.js"
|
||||
import * as ScopeRegistry from "../src/ScopeRegistry.js"
|
||||
|
||||
|
||||
class ValueService extends Context.Service<ValueService, { readonly value: string }>()("ValueService") {}
|
||||
@@ -365,7 +366,7 @@ describe("Component", () => {
|
||||
const setup = vi.fn()
|
||||
const runtime = ReactRuntime.make(Layer.empty)
|
||||
const effectRuntime = await runtime.runtime.context()
|
||||
const scopeMap = Context.get(effectRuntime, Component.ScopeMap)
|
||||
const scopeRegistry = Context.get(effectRuntime, ScopeRegistry.ScopeRegistry)
|
||||
const pending = new Promise<void>(() => {})
|
||||
|
||||
const Probe = Component.makeUntraced("DiscardedSuspenseProbe")(function*() {
|
||||
@@ -390,7 +391,7 @@ describe("Component", () => {
|
||||
try {
|
||||
await screen.findByText("fallback")
|
||||
expect(setup).not.toHaveBeenCalled()
|
||||
expect(HashMap.size(Effect.runSync(Ref.get(scopeMap.ref)))).toBe(0)
|
||||
expect(HashMap.size(Effect.runSync(Ref.get(scopeRegistry.ref)))).toBe(0)
|
||||
}
|
||||
finally {
|
||||
view.unmount()
|
||||
@@ -398,11 +399,11 @@ describe("Component", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it.fails("clears ScopeMap after a suspended render retries, commits, and unmounts", async () => {
|
||||
it.fails("clears ScopeRegistry after a suspended render retries, commits, and unmounts", async () => {
|
||||
const lifecycle = vi.fn<(message: string) => void>()
|
||||
const runtime = ReactRuntime.make(Layer.empty)
|
||||
const effectRuntime = await runtime.runtime.context()
|
||||
const scopeMap = Context.get(effectRuntime, Component.ScopeMap)
|
||||
const scopeRegistry = Context.get(effectRuntime, ScopeRegistry.ScopeRegistry)
|
||||
let resolve!: () => void
|
||||
const pending = new Promise<void>(complete => {
|
||||
resolve = complete
|
||||
@@ -440,7 +441,7 @@ describe("Component", () => {
|
||||
await waitFor(() => expect(lifecycle).toHaveBeenCalledWith("mount"))
|
||||
|
||||
view.unmount()
|
||||
await waitFor(() => expect(HashMap.size(Effect.runSync(Ref.get(scopeMap.ref)))).toBe(0))
|
||||
await waitFor(() => expect(HashMap.size(Effect.runSync(Ref.get(scopeRegistry.ref)))).toBe(0))
|
||||
expect(lifecycle.mock.calls.filter(([message]) => message === "mount")).toHaveLength(2)
|
||||
expect(lifecycle.mock.calls.filter(([message]) => message === "cleanup")).toHaveLength(2)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user