0.1.4 (#6)
All checks were successful
Publish / publish (push) Successful in 30s
Lint / lint (push) Successful in 13s

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:
Julien Valverdé
2025-03-24 19:39:29 +01:00
parent a7b5a32071
commit d01152bdcf
30 changed files with 938 additions and 327 deletions

View File

@@ -11,41 +11,41 @@
"preview": "vite preview"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@tanstack/react-router": "^1.112.7",
"@tanstack/router-devtools": "^1.112.7",
"@tanstack/router-plugin": "^1.112.7",
"@eslint/js": "^9.23.0",
"@tanstack/react-router": "^1.114.27",
"@tanstack/react-router-devtools": "^1.114.27",
"@tanstack/router-plugin": "^1.114.27",
"@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.0.10",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint": "^9.23.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript-eslint": "^8.26.0",
"vite": "^6.2.0"
"typescript-eslint": "^8.28.0",
"vite": "^6.2.3"
},
"dependencies": {
"@effect/platform": "^0.77.6",
"@effect/platform-browser": "^0.56.6",
"@effect/platform": "^0.80.1",
"@effect/platform-browser": "^0.59.1",
"@radix-ui/themes": "^3.2.1",
"@reffuse/extension-lazyref": "workspace:*",
"@reffuse/extension-query": "workspace:*",
"@typed/async-data": "^0.13.1",
"@typed/id": "^0.17.1",
"@typed/lazy-ref": "^0.3.3",
"effect": "^3.13.6",
"lucide-react": "^0.477.0",
"mobx": "^6.13.6",
"effect": "^3.14.1",
"lucide-react": "^0.483.0",
"mobx": "^6.13.7",
"reffuse": "workspace:*"
},
"overrides": {
"effect": "^3.13.6",
"@effect/platform": "^0.77.6",
"@effect/platform-browser": "^0.56.6",
"effect": "^3.14.1",
"@effect/platform": "^0.80.1",
"@effect/platform-browser": "^0.59.1",
"@typed/lazy-ref": "^0.3.3",
"@typed/async-data": "^0.13.1"
}

View 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: () => <></>,
})
}

View File

@@ -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 })

View 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 }) {}

View File

@@ -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
>() {}

View File

@@ -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>
)

View File

@@ -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(

View File

@@ -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"
}

View File

@@ -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>
)

View 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>
)
}

View File

@@ -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>
)