diff --git a/packages/example/src/QueryErrorHandler.tsx b/packages/example/src/QueryErrorHandler.tsx deleted file mode 100644 index 5a072ee..0000000 --- a/packages/example/src/QueryErrorHandler.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes" -import { QueryErrorHandler } from "@reffuse/extension-query" -import { Cause, Console, Context, Effect, Either, flow, Match, Option, Stream } from "effect" -import { useState } from "react" -import { AppQueryErrorHandler } from "./query" -import { R } from "./reffuse" - - -export function VQueryErrorHandler() { - const [failure, setFailure] = useState(Option.none> - >>()) - - R.useFork(() => AppQueryErrorHandler.pipe(Effect.flatMap(handler => - Stream.runForEach(handler.errors, v => Console.error(v).pipe( - Effect.andThen(Effect.sync(() => { setFailure(Option.some(v)) })) - )) - )), []) - - return Option.match(failure, { - onSome: v => ( - - - Error - - {Either.match(Cause.failureOrCause(v), { - onLeft: flow( - Match.value, - Match.tag("RequestError", () => HTTP request error), - Match.tag("ResponseError", () => HTTP response error), - Match.exhaustive, - ), - - onRight: flow( - Cause.dieOption, - Option.match({ - onSome: () => Unrecoverable defect, - onNone: () => Unknown error, - }), - ), - })} - - - - - - - - - - ), - - onNone: () => <>, - }) -} diff --git a/packages/example/src/VQueryErrorHandler.tsx b/packages/example/src/VQueryErrorHandler.tsx new file mode 100644 index 0000000..0b9fa57 --- /dev/null +++ b/packages/example/src/VQueryErrorHandler.tsx @@ -0,0 +1,57 @@ +import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes" +import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect" +import { useState } from "react" +import { AppQueryErrorHandler } from "./query" +import { R } from "./reffuse" + + +export function VQueryErrorHandler() { + const [open, setOpen] = useState(false) + + const error = R.useSubscribeStream( + R.useMemo(() => AppQueryErrorHandler.pipe( + Effect.map(handler => handler.errors.pipe( + Stream.changes, + Stream.tap(Console.error), + Stream.tap(() => Effect.sync(() => setOpen(true))), + )) + ), []) + ) + + if (Option.isNone(error)) + return <> + + return ( + + + Error + + {Either.match(Cause.failureOrCause(error.value), { + onLeft: flow( + Match.value, + Match.tag("RequestError", () => HTTP request error), + Match.tag("ResponseError", () => HTTP response error), + Match.exhaustive, + ), + + onRight: flow( + Cause.dieOption, + Option.match({ + onSome: () => Unrecoverable defect, + onNone: () => Unknown error, + }), + ), + })} + + + + + + + + + + ) +} diff --git a/packages/example/src/routes/__root.tsx b/packages/example/src/routes/__root.tsx index d5180aa..12abc9b 100644 --- a/packages/example/src/routes/__root.tsx +++ b/packages/example/src/routes/__root.tsx @@ -1,4 +1,4 @@ -import { VQueryErrorHandler } from "@/QueryErrorHandler" +import { VQueryErrorHandler } from "@/VQueryErrorHandler" import { Container, Flex, Theme } from "@radix-ui/themes" import { createRootRoute, Link, Outlet } from "@tanstack/react-router" import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" diff --git a/packages/extension-lazyref/package.json b/packages/extension-lazyref/package.json index 5991388..f126cdd 100644 --- a/packages/extension-lazyref/package.json +++ b/packages/extension-lazyref/package.json @@ -1,6 +1,6 @@ { "name": "@reffuse/extension-lazyref", - "version": "0.1.2", + "version": "0.1.3", "type": "module", "files": [ "./README.md", @@ -37,6 +37,6 @@ "@types/react": "^19.0.0", "effect": "^3.13.0", "react": "^19.0.0", - "reffuse": "^0.1.6" + "reffuse": "^0.1.7" } } diff --git a/packages/extension-lazyref/src/index.ts b/packages/extension-lazyref/src/index.ts index 42b76ec..7a51a86 100644 --- a/packages/extension-lazyref/src/index.ts +++ b/packages/extension-lazyref/src/index.ts @@ -1,16 +1,43 @@ import * as LazyRef from "@typed/lazy-ref" -import { Effect, Stream } from "effect" +import { Effect, pipe, Stream } from "effect" import * as React from "react" import { ReffuseExtension, type ReffuseNamespace, SetStateAction } from "reffuse" export const LazyRefExtension = ReffuseExtension.make(() => ({ + useSubscribeLazyRefs< + const Refs extends readonly LazyRef.LazyRef[], + R, + >( + this: ReffuseNamespace.ReffuseNamespace, + ...refs: Refs + ): [...{ [K in keyof Refs]: Effect.Effect.Success }] { + const [reactStateValue, setReactStateValue] = React.useState(this.useMemo( + () => Effect.all(refs as readonly LazyRef.LazyRef[]), + [], + { doNotReExecuteOnRuntimeOrContextChange: true }, + ) as [...{ [K in keyof Refs]: Effect.Effect.Success }]) + + this.useFork(() => pipe( + refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)), + streams => Stream.zipLatestAll(...streams), + Stream.runForEach(v => + Effect.sync(() => setReactStateValue(v as [...{ [K in keyof Refs]: Effect.Effect.Success }])) + ), + ), refs) + + return reactStateValue + }, + useLazyRefState( this: ReffuseNamespace.ReffuseNamespace, ref: LazyRef.LazyRef, ): [A, React.Dispatch>] { - const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true }) - const [reactStateValue, setReactStateValue] = React.useState(initialState) + const [reactStateValue, setReactStateValue] = React.useState(this.useMemo( + () => ref, + [], + { doNotReExecuteOnRuntimeOrContextChange: true }, + )) this.useFork(() => Stream.runForEach( Stream.changesWith(ref.changes, (x, y) => x === y), diff --git a/packages/reffuse/package.json b/packages/reffuse/package.json index cd66e15..5e189c2 100644 --- a/packages/reffuse/package.json +++ b/packages/reffuse/package.json @@ -1,6 +1,6 @@ { "name": "reffuse", - "version": "0.1.6", + "version": "0.1.7", "type": "module", "files": [ "./README.md", diff --git a/packages/reffuse/src/ReffuseNamespace.ts b/packages/reffuse/src/ReffuseNamespace.ts index 7bf1549..a6d56e3 100644 --- a/packages/reffuse/src/ReffuseNamespace.ts +++ b/packages/reffuse/src/ReffuseNamespace.ts @@ -1,4 +1,4 @@ -import { Array, type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, pipe, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect" +import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Option, pipe, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect" import * as React from "react" import * as ReffuseContext from "./ReffuseContext.js" import * as ReffuseRuntime from "./ReffuseRuntime.js" @@ -21,6 +21,7 @@ export abstract class ReffuseNamespace { constructor() { this.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any this.RefState = this.RefState.bind(this as any) as any + this.SubscribeStream = this.SubscribeStream.bind(this as any) as any } @@ -396,8 +397,8 @@ export abstract class ReffuseNamespace { { doNotReExecuteOnRuntimeOrContextChange: true }, ) as [...{ [K in keyof Refs]: Effect.Effect.Success }]) - this.useFork(() => pipe(refs as readonly SubscriptionRef.SubscriptionRef[], - Array.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)), + this.useFork(() => pipe( + refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)), streams => Stream.zipLatestAll(...streams), Stream.runForEach(v => Effect.sync(() => setReactStateValue(v as [...{ [K in keyof Refs]: Effect.Effect.Success }])) @@ -418,8 +419,11 @@ export abstract class ReffuseNamespace { this: ReffuseNamespace, ref: SubscriptionRef.SubscriptionRef, ): [A, React.Dispatch>] { - const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true }) - const [reactStateValue, setReactStateValue] = React.useState(initialState) + const [reactStateValue, setReactStateValue] = React.useState(this.useMemo( + () => ref, + [], + { doNotReExecuteOnRuntimeOrContextChange: true }, + )) this.useFork(() => Stream.runForEach( Stream.changesWith(ref.changes, (x, y) => x === y), @@ -448,6 +452,21 @@ export abstract class ReffuseNamespace { return stream } + useSubscribeStream( + this: ReffuseNamespace, + stream: Stream.Stream, + initialValue?: InitialA, + ): InitialA extends A ? Option.Some : Option.Option { + const [reactStateValue, setReactStateValue] = React.useState>(Option.fromNullable(initialValue)) + + this.useFork(() => Stream.runForEach( + Stream.changesWith(stream, (x, y) => x === y), + v => Effect.sync(() => setReactStateValue(Option.some(v))), + ), [stream]) + + return reactStateValue as InitialA extends A ? Option.Some : Option.Option + } + SubscribeRefs< const Refs extends readonly SubscriptionRef.SubscriptionRef[], @@ -471,6 +490,17 @@ export abstract class ReffuseNamespace { ): React.ReactNode { return props.children(this.useRefState(props.ref)) } + + SubscribeStream( + this: ReffuseNamespace, + props: { + readonly stream: Stream.Stream + readonly initialValue?: InitialA + readonly children: (latestValue: InitialA extends A ? Option.Some : Option.Option) => React.ReactNode + }, + ): React.ReactNode { + return props.children(this.useSubscribeStream(props.stream, props.initialValue)) + } }