0.1.4 (#6)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/6
This commit was merged in pull request #6.
This commit is contained in:
57
packages/example/src/QueryErrorHandler.tsx
Normal file
57
packages/example/src/QueryErrorHandler.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { ErrorHandler } 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<Cause.Cause<
|
||||
ErrorHandler.Error<Context.Tag.Service<AppQueryErrorHandler>>
|
||||
>>())
|
||||
|
||||
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 => (
|
||||
<AlertDialog.Root open>
|
||||
<AlertDialog.Content maxWidth="450px">
|
||||
<AlertDialog.Title>Error</AlertDialog.Title>
|
||||
<AlertDialog.Description size="2">
|
||||
{Either.match(Cause.failureOrCause(v), {
|
||||
onLeft: flow(
|
||||
Match.value,
|
||||
Match.tag("RequestError", () => <Text>HTTP request error</Text>),
|
||||
Match.tag("ResponseError", () => <Text>HTTP response error</Text>),
|
||||
Match.exhaustive,
|
||||
),
|
||||
|
||||
onRight: flow(
|
||||
Cause.dieOption,
|
||||
Option.match({
|
||||
onSome: () => <Text>Unrecoverable defect</Text>,
|
||||
onNone: () => <Text>Unknown error</Text>,
|
||||
}),
|
||||
),
|
||||
})}
|
||||
</AlertDialog.Description>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<AlertDialog.Action>
|
||||
<Button variant="solid" color="red" onClick={() => setFailure(Option.none())}>
|
||||
Ok
|
||||
</Button>
|
||||
</AlertDialog.Action>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
),
|
||||
|
||||
onNone: () => <></>,
|
||||
})
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Layer } from "effect"
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { ReffuseRuntime } from "reffuse"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./query"
|
||||
import { GlobalContext } from "./reffuse"
|
||||
import { routeTree } from "./routeTree.gen"
|
||||
|
||||
@@ -14,6 +15,8 @@ const layer = Layer.empty.pipe(
|
||||
Layer.provideMerge(Geolocation.layer),
|
||||
Layer.provideMerge(Permissions.layer),
|
||||
Layer.provideMerge(FetchHttpClient.layer),
|
||||
Layer.provideMerge(AppQueryClient.Live),
|
||||
Layer.provideMerge(AppQueryErrorHandler.Live),
|
||||
)
|
||||
|
||||
const router = createRouter({ routeTree })
|
||||
|
||||
21
packages/example/src/query.ts
Normal file
21
packages/example/src/query.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { HttpClientError } from "@effect/platform"
|
||||
import { ErrorHandler, QueryClient } from "@reffuse/extension-query"
|
||||
import { Effect } from "effect"
|
||||
|
||||
|
||||
export class AppQueryErrorHandler extends ErrorHandler.Service<AppQueryErrorHandler,
|
||||
HttpClientError.HttpClientError
|
||||
>()(
|
||||
"AppQueryErrorHandler",
|
||||
|
||||
(self, failure, defect) => self.pipe(
|
||||
Effect.catchTags({
|
||||
RequestError: failure,
|
||||
ResponseError: failure,
|
||||
}),
|
||||
|
||||
Effect.catchAllDefect(defect),
|
||||
),
|
||||
) {}
|
||||
|
||||
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({ ErrorHandler: AppQueryErrorHandler }) {}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { HttpClientError } from "@effect/platform"
|
||||
import { QueryService } from "@reffuse/extension-query"
|
||||
import { ParseResult, Schema } from "effect"
|
||||
|
||||
@@ -8,5 +7,5 @@ export const Result = Schema.Array(Schema.String)
|
||||
export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query,
|
||||
readonly ["uuid4", number],
|
||||
typeof Result.Type,
|
||||
HttpClientError.HttpClientError | ParseResult.ParseError
|
||||
ParseResult.ParseError
|
||||
>() {}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Uuid4Query } from "../services"
|
||||
|
||||
|
||||
export function Uuid4QueryService() {
|
||||
const runSync = R.useRunSync()
|
||||
const runFork = R.useRunFork()
|
||||
|
||||
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
||||
const [state] = R.useRefState(query.state)
|
||||
@@ -25,7 +25,7 @@ export function Uuid4QueryService() {
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button onClick={() => runSync(query.refresh)}>Refresh</Button>
|
||||
<Button onClick={() => runFork(query.forkRefresh)}>Refresh</Button>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
||||
import { QueryExtension } from "@reffuse/extension-query"
|
||||
import { Reffuse, ReffuseContext } from "reffuse"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./query"
|
||||
|
||||
|
||||
export const GlobalContext = ReffuseContext.make<
|
||||
@@ -10,6 +11,8 @@ export const GlobalContext = ReffuseContext.make<
|
||||
| Geolocation.Geolocation
|
||||
| Permissions.Permissions
|
||||
| HttpClient.HttpClient
|
||||
| AppQueryClient
|
||||
| AppQueryErrorHandler
|
||||
>()
|
||||
|
||||
export class GlobalReffuse extends Reffuse.Reffuse.pipe(
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Route as CountImport } from './routes/count'
|
||||
import { Route as BlankImport } from './routes/blank'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as QueryUsequeryImport } from './routes/query/usequery'
|
||||
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
|
||||
import { Route as QueryServiceImport } from './routes/query/service'
|
||||
|
||||
// Create/Update Routes
|
||||
@@ -71,6 +72,12 @@ const QueryUsequeryRoute = QueryUsequeryImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const QueryUsemutationRoute = QueryUsemutationImport.update({
|
||||
id: '/query/usemutation',
|
||||
path: '/query/usemutation',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const QueryServiceRoute = QueryServiceImport.update({
|
||||
id: '/query/service',
|
||||
path: '/query/service',
|
||||
@@ -137,6 +144,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof QueryServiceImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/query/usemutation': {
|
||||
id: '/query/usemutation'
|
||||
path: '/query/usemutation'
|
||||
fullPath: '/query/usemutation'
|
||||
preLoaderRoute: typeof QueryUsemutationImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/query/usequery': {
|
||||
id: '/query/usequery'
|
||||
path: '/query/usequery'
|
||||
@@ -158,6 +172,7 @@ export interface FileRoutesByFullPath {
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
}
|
||||
|
||||
@@ -170,6 +185,7 @@ export interface FileRoutesByTo {
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
}
|
||||
|
||||
@@ -183,6 +199,7 @@ export interface FileRoutesById {
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
}
|
||||
|
||||
@@ -197,6 +214,7 @@ export interface FileRouteTypes {
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
@@ -208,6 +226,7 @@ export interface FileRouteTypes {
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
id:
|
||||
| '__root__'
|
||||
@@ -219,6 +238,7 @@ export interface FileRouteTypes {
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
@@ -232,6 +252,7 @@ export interface RootRouteChildren {
|
||||
TestsRoute: typeof TestsRoute
|
||||
TimeRoute: typeof TimeRoute
|
||||
QueryServiceRoute: typeof QueryServiceRoute
|
||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
||||
}
|
||||
|
||||
@@ -244,6 +265,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
TestsRoute: TestsRoute,
|
||||
TimeRoute: TimeRoute,
|
||||
QueryServiceRoute: QueryServiceRoute,
|
||||
QueryUsemutationRoute: QueryUsemutationRoute,
|
||||
QueryUsequeryRoute: QueryUsequeryRoute,
|
||||
}
|
||||
|
||||
@@ -265,6 +287,7 @@ export const routeTree = rootRoute
|
||||
"/tests",
|
||||
"/time",
|
||||
"/query/service",
|
||||
"/query/usemutation",
|
||||
"/query/usequery"
|
||||
]
|
||||
},
|
||||
@@ -292,6 +315,9 @@ export const routeTree = rootRoute
|
||||
"/query/service": {
|
||||
"filePath": "query/service.tsx"
|
||||
},
|
||||
"/query/usemutation": {
|
||||
"filePath": "query/usemutation.tsx"
|
||||
},
|
||||
"/query/usequery": {
|
||||
"filePath": "query/usequery.tsx"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { VQueryErrorHandler } from "@/QueryErrorHandler"
|
||||
import { Container, Flex, Theme } from "@radix-ui/themes"
|
||||
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
||||
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
|
||||
|
||||
import "@radix-ui/themes/styles.css"
|
||||
import "../index.css"
|
||||
@@ -26,6 +27,8 @@ function Root() {
|
||||
</Container>
|
||||
|
||||
<Outlet />
|
||||
|
||||
<VQueryErrorHandler />
|
||||
<TanStackRouterDevtools />
|
||||
</Theme>
|
||||
)
|
||||
|
||||
81
packages/example/src/routes/query/usemutation.tsx
Normal file
81
packages/example/src/routes/query/usemutation.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { HttpClient } from "@effect/platform"
|
||||
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
|
||||
import { QueryProgress } from "@reffuse/extension-query"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import * as AsyncData from "@typed/async-data"
|
||||
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
|
||||
import { useState } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/query/usemutation")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
|
||||
const Result = Schema.Array(Schema.String)
|
||||
|
||||
function RouteComponent() {
|
||||
const runFork = R.useRunFork()
|
||||
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
const mutation = R.useMutation({
|
||||
mutation: ([count]: readonly [count: number]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||
Effect.andThen(QueryProgress.QueryProgress.update(() =>
|
||||
AsyncData.Progress.make({ loaded: 0, total: Option.some(100) })
|
||||
)),
|
||||
Effect.andThen(Effect.sleep("500 millis")),
|
||||
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
|
||||
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
|
||||
)),
|
||||
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
HttpClient.withTracerPropagation(false),
|
||||
Effect.flatMap(res => res.json),
|
||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||
Effect.scoped,
|
||||
)
|
||||
})
|
||||
|
||||
const [state] = R.useRefState(mutation.state)
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Flex direction="column" align="center" gap="2">
|
||||
<Slider
|
||||
min={1}
|
||||
max={100}
|
||||
value={[count]}
|
||||
onValueChange={flow(
|
||||
Array.head,
|
||||
Option.getOrThrow,
|
||||
setCount,
|
||||
)}
|
||||
/>
|
||||
|
||||
<Text>
|
||||
{AsyncData.match(state, {
|
||||
NoData: () => "No data yet",
|
||||
Loading: progress =>
|
||||
`Loading...
|
||||
${ Option.match(progress, {
|
||||
onSome: ({ loaded, total }) => ` (${ loaded }/${ Option.getOrElse(total, () => "unknown") })`,
|
||||
onNone: () => "",
|
||||
}) }`,
|
||||
Success: value => `Value: ${ value }`,
|
||||
Failure: cause => `Error: ${ cause }`,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button onClick={() => mutation.forkMutate(count).pipe(
|
||||
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
|
||||
Effect.andThen(Console.log("Mutation done.")),
|
||||
runFork,
|
||||
)}>
|
||||
Get
|
||||
</Button>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { HttpClient } from "@effect/platform"
|
||||
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import * as AsyncData from "@typed/async-data"
|
||||
import { Array, Console, Effect, flow, Option, Schema } from "effect"
|
||||
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
|
||||
import { useState } from "react"
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute("/query/usequery")({
|
||||
const Result = Schema.Array(Schema.String)
|
||||
|
||||
function RouteComponent() {
|
||||
const runSync = R.useRunSync()
|
||||
const runFork = R.useRunFork()
|
||||
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
@@ -59,7 +59,15 @@ function RouteComponent() {
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button onClick={() => runSync(query.refresh)}>Refresh</Button>
|
||||
<Button
|
||||
onClick={() => query.forkRefresh.pipe(
|
||||
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
|
||||
Effect.andThen(Console.log("Refresh finished or stopped")),
|
||||
runFork,
|
||||
)}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user