Compare commits
22 Commits
e9e17ac211
...
224ccd8e32
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
224ccd8e32 | ||
|
|
4cf70ada0b | ||
|
|
f9bd5d4d6b | ||
|
|
1ec1db0658 | ||
|
|
2d94e84941 | ||
|
|
aab83907ba | ||
|
|
8c0d6b4c8a | ||
|
|
d82d1d1c29 | ||
|
|
0f09573948 | ||
|
|
2b6b36713e | ||
|
|
5d0aecc9d5 | ||
|
|
f21d8b2d8a | ||
|
|
f85173fa68 | ||
|
|
65a124de1f | ||
|
|
16893761c6 | ||
|
|
3fdc2e31eb | ||
|
|
8636a28f2f | ||
|
|
d56578da8f | ||
|
|
299109d421 | ||
|
|
4995b2949f | ||
|
|
6e6e675709 | ||
|
|
b04860aa25 |
@@ -1,6 +1,6 @@
|
|||||||
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
||||||
import { ErrorHandler } from "@reffuse/extension-query"
|
import { ErrorHandler } from "@reffuse/extension-query"
|
||||||
import { Cause, Chunk, Context, Effect, Match, Option, Stream } from "effect"
|
import { Cause, Console, Context, Effect, Either, flow, Match, Option, Stream } from "effect"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { AppQueryErrorHandler } from "./query"
|
import { AppQueryErrorHandler } from "./query"
|
||||||
import { R } from "./reffuse"
|
import { R } from "./reffuse"
|
||||||
@@ -12,8 +12,8 @@ export function VQueryErrorHandler() {
|
|||||||
>>())
|
>>())
|
||||||
|
|
||||||
R.useFork(() => AppQueryErrorHandler.pipe(Effect.flatMap(handler =>
|
R.useFork(() => AppQueryErrorHandler.pipe(Effect.flatMap(handler =>
|
||||||
Stream.runForEach(handler.errors, v => Effect.sync(() =>
|
Stream.runForEach(handler.errors, v => Console.error(v).pipe(
|
||||||
setFailure(Option.some(v))
|
Effect.andThen(Effect.sync(() => { setFailure(Option.some(v)) }))
|
||||||
))
|
))
|
||||||
)), [])
|
)), [])
|
||||||
|
|
||||||
@@ -23,15 +23,22 @@ export function VQueryErrorHandler() {
|
|||||||
<AlertDialog.Content maxWidth="450px">
|
<AlertDialog.Content maxWidth="450px">
|
||||||
<AlertDialog.Title>Error</AlertDialog.Title>
|
<AlertDialog.Title>Error</AlertDialog.Title>
|
||||||
<AlertDialog.Description size="2">
|
<AlertDialog.Description size="2">
|
||||||
{Cause.failures(v).pipe(
|
{Either.match(Cause.failureOrCause(v), {
|
||||||
Chunk.head,
|
onLeft: flow(
|
||||||
Option.getOrThrow,
|
|
||||||
|
|
||||||
Match.value,
|
Match.value,
|
||||||
Match.tag("RequestError", () => <Text>HTTP request error</Text>),
|
Match.tag("RequestError", () => <Text>HTTP request error</Text>),
|
||||||
Match.tag("ResponseError", () => <Text>HTTP response error</Text>),
|
Match.tag("ResponseError", () => <Text>HTTP response error</Text>),
|
||||||
Match.exhaustive,
|
Match.exhaustive,
|
||||||
)}
|
),
|
||||||
|
|
||||||
|
onRight: flow(
|
||||||
|
Cause.dieOption,
|
||||||
|
Option.match({
|
||||||
|
onSome: () => <Text>Unrecoverable defect</Text>,
|
||||||
|
onNone: () => <Text>Unknown error</Text>,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})}
|
||||||
</AlertDialog.Description>
|
</AlertDialog.Description>
|
||||||
|
|
||||||
<Flex gap="3" mt="4" justify="end">
|
<Flex gap="3" mt="4" justify="end">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Route as CountImport } from './routes/count'
|
|||||||
import { Route as BlankImport } from './routes/blank'
|
import { Route as BlankImport } from './routes/blank'
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
import { Route as QueryUsequeryImport } from './routes/query/usequery'
|
import { Route as QueryUsequeryImport } from './routes/query/usequery'
|
||||||
|
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
|
||||||
import { Route as QueryServiceImport } from './routes/query/service'
|
import { Route as QueryServiceImport } from './routes/query/service'
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
@@ -71,6 +72,12 @@ const QueryUsequeryRoute = QueryUsequeryImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const QueryUsemutationRoute = QueryUsemutationImport.update({
|
||||||
|
id: '/query/usemutation',
|
||||||
|
path: '/query/usemutation',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const QueryServiceRoute = QueryServiceImport.update({
|
const QueryServiceRoute = QueryServiceImport.update({
|
||||||
id: '/query/service',
|
id: '/query/service',
|
||||||
path: '/query/service',
|
path: '/query/service',
|
||||||
@@ -137,6 +144,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof QueryServiceImport
|
preLoaderRoute: typeof QueryServiceImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/query/usemutation': {
|
||||||
|
id: '/query/usemutation'
|
||||||
|
path: '/query/usemutation'
|
||||||
|
fullPath: '/query/usemutation'
|
||||||
|
preLoaderRoute: typeof QueryUsemutationImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/query/usequery': {
|
'/query/usequery': {
|
||||||
id: '/query/usequery'
|
id: '/query/usequery'
|
||||||
path: '/query/usequery'
|
path: '/query/usequery'
|
||||||
@@ -158,6 +172,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
'/time': typeof TimeRoute
|
'/time': typeof TimeRoute
|
||||||
'/query/service': typeof QueryServiceRoute
|
'/query/service': typeof QueryServiceRoute
|
||||||
|
'/query/usemutation': typeof QueryUsemutationRoute
|
||||||
'/query/usequery': typeof QueryUsequeryRoute
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +185,7 @@ export interface FileRoutesByTo {
|
|||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
'/time': typeof TimeRoute
|
'/time': typeof TimeRoute
|
||||||
'/query/service': typeof QueryServiceRoute
|
'/query/service': typeof QueryServiceRoute
|
||||||
|
'/query/usemutation': typeof QueryUsemutationRoute
|
||||||
'/query/usequery': typeof QueryUsequeryRoute
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +199,7 @@ export interface FileRoutesById {
|
|||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
'/time': typeof TimeRoute
|
'/time': typeof TimeRoute
|
||||||
'/query/service': typeof QueryServiceRoute
|
'/query/service': typeof QueryServiceRoute
|
||||||
|
'/query/usemutation': typeof QueryUsemutationRoute
|
||||||
'/query/usequery': typeof QueryUsequeryRoute
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +214,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tests'
|
| '/tests'
|
||||||
| '/time'
|
| '/time'
|
||||||
| '/query/service'
|
| '/query/service'
|
||||||
|
| '/query/usemutation'
|
||||||
| '/query/usequery'
|
| '/query/usequery'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
@@ -208,6 +226,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tests'
|
| '/tests'
|
||||||
| '/time'
|
| '/time'
|
||||||
| '/query/service'
|
| '/query/service'
|
||||||
|
| '/query/usemutation'
|
||||||
| '/query/usequery'
|
| '/query/usequery'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
@@ -219,6 +238,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tests'
|
| '/tests'
|
||||||
| '/time'
|
| '/time'
|
||||||
| '/query/service'
|
| '/query/service'
|
||||||
|
| '/query/usemutation'
|
||||||
| '/query/usequery'
|
| '/query/usequery'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
@@ -232,6 +252,7 @@ export interface RootRouteChildren {
|
|||||||
TestsRoute: typeof TestsRoute
|
TestsRoute: typeof TestsRoute
|
||||||
TimeRoute: typeof TimeRoute
|
TimeRoute: typeof TimeRoute
|
||||||
QueryServiceRoute: typeof QueryServiceRoute
|
QueryServiceRoute: typeof QueryServiceRoute
|
||||||
|
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
||||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +265,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
TestsRoute: TestsRoute,
|
TestsRoute: TestsRoute,
|
||||||
TimeRoute: TimeRoute,
|
TimeRoute: TimeRoute,
|
||||||
QueryServiceRoute: QueryServiceRoute,
|
QueryServiceRoute: QueryServiceRoute,
|
||||||
|
QueryUsemutationRoute: QueryUsemutationRoute,
|
||||||
QueryUsequeryRoute: QueryUsequeryRoute,
|
QueryUsequeryRoute: QueryUsequeryRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +287,7 @@ export const routeTree = rootRoute
|
|||||||
"/tests",
|
"/tests",
|
||||||
"/time",
|
"/time",
|
||||||
"/query/service",
|
"/query/service",
|
||||||
|
"/query/usemutation",
|
||||||
"/query/usequery"
|
"/query/usequery"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -292,6 +315,9 @@ export const routeTree = rootRoute
|
|||||||
"/query/service": {
|
"/query/service": {
|
||||||
"filePath": "query/service.tsx"
|
"filePath": "query/service.tsx"
|
||||||
},
|
},
|
||||||
|
"/query/usemutation": {
|
||||||
|
"filePath": "query/usemutation.tsx"
|
||||||
|
},
|
||||||
"/query/usequery": {
|
"/query/usequery": {
|
||||||
"filePath": "query/usequery.tsx"
|
"filePath": "query/usequery.tsx"
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,9 +12,10 @@ export const LazyRefExtension = ReffuseExtension.make(() => ({
|
|||||||
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
|
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
||||||
|
|
||||||
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() =>
|
this.useFork(() => Stream.runForEach(
|
||||||
setReactStateValue(v)
|
Stream.changes(ref.changes),
|
||||||
)), [ref])
|
v => Effect.sync(() => setReactStateValue(v)),
|
||||||
|
), [ref])
|
||||||
|
|
||||||
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||||
LazyRef.update(ref, prevState =>
|
LazyRef.update(ref, prevState =>
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
// import { BrowserStream } from "@effect/platform-browser"
|
|
||||||
// import * as AsyncData from "@typed/async-data"
|
|
||||||
// import { type Cause, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
// export interface MutationRunner<K extends readonly unknown[], A, E, R> {
|
|
||||||
// readonly mutation: (...args: K) => Effect.Effect<A, E, R>
|
|
||||||
|
|
||||||
// readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
|
||||||
|
|
||||||
// readonly forkMutate: Effect.Effect<Fiber.RuntimeFiber<void>>
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// export interface MakeProps<K extends readonly unknown[], A, E, R> {
|
|
||||||
// readonly mutation: (...args: K) => Effect.Effect<A, E, R>
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export const make = <K extends readonly unknown[], A, E, R>(
|
|
||||||
// { key, query }: MakeProps<K, A, E, R>
|
|
||||||
// ): Effect.Effect<MutationRunner<K, A, E, R>, never, R> => Effect.gen(function*() {
|
|
||||||
// const context = yield* Effect.context<R>()
|
|
||||||
|
|
||||||
// const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, E>())
|
|
||||||
|
|
||||||
// const interrupt = fiberRef.pipe(
|
|
||||||
// Effect.flatMap(Option.match({
|
|
||||||
// onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
|
||||||
// Effect.andThen(Fiber.interrupt(fiber))
|
|
||||||
// ),
|
|
||||||
// onNone: () => Effect.void,
|
|
||||||
// }))
|
|
||||||
// )
|
|
||||||
|
|
||||||
// const forkInterrupt = fiberRef.pipe(
|
|
||||||
// Effect.flatMap(Option.match({
|
|
||||||
// onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
|
||||||
// Effect.andThen(Fiber.interrupt(fiber).pipe(
|
|
||||||
// Effect.asVoid,
|
|
||||||
// Effect.forkDaemon,
|
|
||||||
// ))
|
|
||||||
// ),
|
|
||||||
// onNone: () => Effect.forkDaemon(Effect.void),
|
|
||||||
// }))
|
|
||||||
// )
|
|
||||||
|
|
||||||
// const forkFetch = interrupt.pipe(
|
|
||||||
// Effect.andThen(
|
|
||||||
// Ref.set(stateRef, AsyncData.loading()).pipe(
|
|
||||||
// Effect.andThen(latestKeyRef),
|
|
||||||
// Effect.flatMap(identity),
|
|
||||||
// Effect.flatMap(key => query(key).pipe(
|
|
||||||
// Effect.matchCauseEffect({
|
|
||||||
// onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
|
|
||||||
// onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
|
|
||||||
// })
|
|
||||||
// )),
|
|
||||||
|
|
||||||
// Effect.provide(context),
|
|
||||||
// Effect.fork,
|
|
||||||
// )
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// Effect.flatMap(fiber =>
|
|
||||||
// Ref.set(fiberRef, Option.some(fiber)).pipe(
|
|
||||||
// Effect.andThen(Fiber.join(fiber)),
|
|
||||||
// Effect.andThen(Ref.set(fiberRef, Option.none())),
|
|
||||||
// )
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// Effect.forkDaemon,
|
|
||||||
// )
|
|
||||||
|
|
||||||
// const forkRefresh = interrupt.pipe(
|
|
||||||
// Effect.andThen(
|
|
||||||
// Ref.update(stateRef, previous => {
|
|
||||||
// if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
|
||||||
// return AsyncData.refreshing(previous)
|
|
||||||
// if (AsyncData.isRefreshing(previous))
|
|
||||||
// return AsyncData.refreshing(previous.previous)
|
|
||||||
// return AsyncData.loading()
|
|
||||||
// }).pipe(
|
|
||||||
// Effect.andThen(latestKeyRef),
|
|
||||||
// Effect.flatMap(identity),
|
|
||||||
// Effect.flatMap(key => query(key).pipe(
|
|
||||||
// Effect.matchCauseEffect({
|
|
||||||
// onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
|
|
||||||
// onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
|
|
||||||
// })
|
|
||||||
// )),
|
|
||||||
|
|
||||||
// Effect.provide(context),
|
|
||||||
// Effect.fork,
|
|
||||||
// )
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// Effect.flatMap(fiber =>
|
|
||||||
// Ref.set(fiberRef, Option.some(fiber)).pipe(
|
|
||||||
// Effect.andThen(Fiber.join(fiber)),
|
|
||||||
// Effect.andThen(Ref.set(fiberRef, Option.none())),
|
|
||||||
// )
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// Effect.forkDaemon,
|
|
||||||
// )
|
|
||||||
|
|
||||||
// const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
|
|
||||||
// Effect.andThen(Stream.runForEach(key, latestKey =>
|
|
||||||
// Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
|
|
||||||
// Effect.andThen(forkFetch)
|
|
||||||
// )
|
|
||||||
// ))
|
|
||||||
// )
|
|
||||||
|
|
||||||
// const refreshOnWindowFocus = Stream.runForEach(
|
|
||||||
// BrowserStream.fromEventListenerWindow("focus"),
|
|
||||||
// () => forkRefresh,
|
|
||||||
// )
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// query,
|
|
||||||
|
|
||||||
// latestKeyRef,
|
|
||||||
// stateRef,
|
|
||||||
// fiberRef,
|
|
||||||
|
|
||||||
// forkInterrupt,
|
|
||||||
// forkFetch,
|
|
||||||
// forkRefresh,
|
|
||||||
|
|
||||||
// fetchOnKeyChange,
|
|
||||||
// refreshOnWindowFocus,
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
16
packages/extension-query/src/MutationService.ts
Normal file
16
packages/extension-query/src/MutationService.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type * as AsyncData from "@typed/async-data"
|
||||||
|
import { Effect, type Fiber, type Stream, type SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export interface MutationService<K extends readonly unknown[], A, E> {
|
||||||
|
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
||||||
|
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tag = <const Id extends string>(id: Id) => <
|
||||||
|
Self, K extends readonly unknown[], A, E = never,
|
||||||
|
>() => Effect.Tag(id)<Self, MutationService<K, A, E>>()
|
||||||
@@ -2,14 +2,16 @@ import type * as AsyncData from "@typed/async-data"
|
|||||||
import { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
|
import { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { ReffuseExtension, type ReffuseHelpers } from "reffuse"
|
import { ReffuseExtension, type ReffuseHelpers } from "reffuse"
|
||||||
|
import type * as MutationService from "./MutationService.js"
|
||||||
import * as QueryClient from "./QueryClient.js"
|
import * as QueryClient from "./QueryClient.js"
|
||||||
import * as QueryRunner from "./QueryRunner.js"
|
import type * as QueryProgress from "./QueryProgress.js"
|
||||||
import type * as QueryService from "./QueryService.js"
|
import type * as QueryService from "./QueryService.js"
|
||||||
|
import { MutationRunner, QueryRunner } from "./internal/index.js"
|
||||||
|
|
||||||
|
|
||||||
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
||||||
readonly key: Stream.Stream<K>
|
readonly key: Stream.Stream<K>
|
||||||
readonly query: (key: K) => Effect.Effect<A, E, R>
|
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
readonly refreshOnWindowFocus?: boolean
|
readonly refreshOnWindowFocus?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +26,24 @@ export interface UseQueryResult<K extends readonly unknown[], A, E> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface UseMutationProps<K extends readonly unknown[], A, E, R> {
|
||||||
|
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseMutationResult<K extends readonly unknown[], A, E> {
|
||||||
|
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
||||||
|
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
|
||||||
|
readonly layer: <Self, Id extends string>(
|
||||||
|
tag: Context.TagClass<Self, Id, MutationService.MutationService<K, A, E>>
|
||||||
|
) => Layer.Layer<Self>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const QueryExtension = ReffuseExtension.make(() => ({
|
export const QueryExtension = ReffuseExtension.make(() => ({
|
||||||
useQuery<
|
useQuery<
|
||||||
EH,
|
EH,
|
||||||
@@ -61,5 +81,35 @@ export const QueryExtension = ReffuseExtension.make(() => ({
|
|||||||
refresh: runner.forkRefresh,
|
refresh: runner.forkRefresh,
|
||||||
}),
|
}),
|
||||||
}), [runner])
|
}), [runner])
|
||||||
}
|
},
|
||||||
|
|
||||||
|
useMutation<
|
||||||
|
EH,
|
||||||
|
QK extends readonly unknown[],
|
||||||
|
QA,
|
||||||
|
QE,
|
||||||
|
HandledE,
|
||||||
|
QR extends R,
|
||||||
|
R,
|
||||||
|
>(
|
||||||
|
this: ReffuseHelpers.ReffuseHelpers<R | QueryClient.TagClassShape<EH, HandledE> | EH>,
|
||||||
|
props: UseMutationProps<QK, QA, QE, QR>,
|
||||||
|
): UseMutationResult<QK, QA, Exclude<QE, HandledE>> {
|
||||||
|
const runner = this.useMemo(() => MutationRunner.make({
|
||||||
|
QueryClient: QueryClient.makeGenericTagClass<EH, HandledE>(),
|
||||||
|
mutation: props.mutation,
|
||||||
|
}), [])
|
||||||
|
|
||||||
|
return React.useMemo(() => ({
|
||||||
|
state: runner.stateRef,
|
||||||
|
mutate: runner.mutate,
|
||||||
|
forkMutate: runner.forkMutate,
|
||||||
|
|
||||||
|
layer: tag => Layer.succeed(tag, {
|
||||||
|
state: runner.stateRef,
|
||||||
|
mutate: runner.mutate,
|
||||||
|
forkMutate: runner.forkMutate,
|
||||||
|
}),
|
||||||
|
}), [runner])
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
37
packages/extension-query/src/QueryProgress.ts
Normal file
37
packages/extension-query/src/QueryProgress.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as AsyncData from "@typed/async-data"
|
||||||
|
import { Effect, flow, Layer, Match, Option } from "effect"
|
||||||
|
import { QueryState } from "./internal/index.js"
|
||||||
|
|
||||||
|
|
||||||
|
export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryProgress")<QueryProgress, {
|
||||||
|
readonly get: Effect.Effect<Option.Option<AsyncData.Progress>>
|
||||||
|
|
||||||
|
readonly update: (
|
||||||
|
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
|
||||||
|
) => Effect.Effect<void>
|
||||||
|
}>() {
|
||||||
|
static readonly Live: Layer.Layer<
|
||||||
|
QueryProgress,
|
||||||
|
never,
|
||||||
|
QueryState.QueryState<any, any>
|
||||||
|
> = Layer.effect(this, Effect.gen(function*() {
|
||||||
|
const state = yield* QueryState.makeTag()
|
||||||
|
|
||||||
|
const get = state.get.pipe(
|
||||||
|
Effect.map(flow(Match.value,
|
||||||
|
Match.tag("Loading", v => v.progress),
|
||||||
|
Match.tag("Refreshing", v => v.progress),
|
||||||
|
Match.orElse(() => Option.none()),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
const update = (f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress) => get.pipe(
|
||||||
|
Effect.map(f),
|
||||||
|
Effect.flatMap(progress => state.update(previous =>
|
||||||
|
AsyncData.updateProgress(previous, progress)
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return { get, update }
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -11,22 +11,3 @@ export interface QueryService<K extends readonly unknown[], A, E> {
|
|||||||
export const Tag = <const Id extends string>(id: Id) => <
|
export const Tag = <const Id extends string>(id: Id) => <
|
||||||
Self, K extends readonly unknown[], A, E = never,
|
Self, K extends readonly unknown[], A, E = never,
|
||||||
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>()
|
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>()
|
||||||
|
|
||||||
|
|
||||||
// export interface LayerProps<A, E, R> {
|
|
||||||
// readonly query: Effect.Effect<A, E, R>
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export const layer = <Self, Id extends string, A, E, R>(
|
|
||||||
// tag: Context.TagClass<Self, Id, QueryService<A, E>>,
|
|
||||||
// props: LayerProps<A, E, R>,
|
|
||||||
// ): Layer.Layer<Self, never, R> => Layer.effect(tag, Effect.gen(function*() {
|
|
||||||
// const runner = yield* QueryRunner.make({
|
|
||||||
// query: props.query
|
|
||||||
// })
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// state: runner.stateRef,
|
|
||||||
// refresh: runner.forkRefresh,
|
|
||||||
// }
|
|
||||||
// }))
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * as ErrorHandler from "./ErrorHandler.js"
|
export * as ErrorHandler from "./ErrorHandler.js"
|
||||||
|
export * as MutationService from "./MutationService.js"
|
||||||
export * as QueryClient from "./QueryClient.js"
|
export * as QueryClient from "./QueryClient.js"
|
||||||
export * from "./QueryExtension.js"
|
export * from "./QueryExtension.js"
|
||||||
export * as QueryRunner from "./QueryRunner.js"
|
export * as QueryProgress from "./QueryProgress.js"
|
||||||
export * as QueryService from "./QueryService.js"
|
export * as QueryService from "./QueryService.js"
|
||||||
|
|||||||
97
packages/extension-query/src/internal/MutationRunner.ts
Normal file
97
packages/extension-query/src/internal/MutationRunner.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import * as AsyncData from "@typed/async-data"
|
||||||
|
import { type Context, Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
|
||||||
|
import type * as QueryClient from "../QueryClient.js"
|
||||||
|
import * as QueryProgress from "../QueryProgress.js"
|
||||||
|
import * as QueryState from "./QueryState.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface MutationRunner<K extends readonly unknown[], A, E, R> {
|
||||||
|
readonly context: Context.Context<R>
|
||||||
|
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
|
||||||
|
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
||||||
|
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface MakeProps<EH, K extends readonly unknown[], A, E, HandledE, R> {
|
||||||
|
readonly QueryClient: QueryClient.GenericTagClass<EH, HandledE>
|
||||||
|
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <EH, K extends readonly unknown[], A, E, HandledE, R>(
|
||||||
|
{
|
||||||
|
QueryClient,
|
||||||
|
mutation,
|
||||||
|
}: MakeProps<EH, K, A, E, HandledE, R>
|
||||||
|
): Effect.Effect<
|
||||||
|
MutationRunner<K, A, Exclude<E, HandledE>, R>,
|
||||||
|
never,
|
||||||
|
R | QueryClient.TagClassShape<EH, HandledE> | EH
|
||||||
|
> => Effect.gen(function*() {
|
||||||
|
const context = yield* Effect.context<R | QueryClient.TagClassShape<EH, HandledE> | EH>()
|
||||||
|
const globalStateRef = yield* SubscriptionRef.make(AsyncData.noData<A, Exclude<E, HandledE>>())
|
||||||
|
|
||||||
|
const queryStateTag = QueryState.makeTag<A, Exclude<E, HandledE>>()
|
||||||
|
|
||||||
|
const run = (key: K) => Effect.all([
|
||||||
|
queryStateTag,
|
||||||
|
QueryClient.pipe(Effect.flatMap(client => client.ErrorHandler)),
|
||||||
|
]).pipe(
|
||||||
|
Effect.flatMap(([state, errorHandler]) => state.set(AsyncData.loading()).pipe(
|
||||||
|
Effect.andThen(mutation(key)),
|
||||||
|
errorHandler.handle,
|
||||||
|
Effect.matchCauseEffect({
|
||||||
|
onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe(
|
||||||
|
Effect.tap(state.set)
|
||||||
|
),
|
||||||
|
onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe(
|
||||||
|
Effect.tap(state.set)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(context),
|
||||||
|
Effect.provide(QueryProgress.QueryProgress.Live),
|
||||||
|
)
|
||||||
|
|
||||||
|
const mutate = (...key: K) => run(key).pipe(
|
||||||
|
Effect.provide(QueryState.layer(
|
||||||
|
queryStateTag,
|
||||||
|
globalStateRef,
|
||||||
|
value => Ref.set(globalStateRef, value),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
const forkMutate = (...key: K) => Effect.gen(function*() {
|
||||||
|
const stateRef = yield* Ref.make(AsyncData.noData<A, Exclude<E, HandledE>>())
|
||||||
|
const stateQueue = yield* Queue.unbounded<AsyncData.AsyncData<A, Exclude<E, HandledE>>>()
|
||||||
|
|
||||||
|
const fiber = yield* Effect.forkDaemon(run(key).pipe(
|
||||||
|
Effect.tap(() => Queue.shutdown(stateQueue)),
|
||||||
|
|
||||||
|
Effect.provide(QueryState.layer(
|
||||||
|
queryStateTag,
|
||||||
|
stateRef,
|
||||||
|
value => Queue.offer(stateQueue, value).pipe(
|
||||||
|
Effect.andThen(Ref.set(stateRef, value)),
|
||||||
|
Effect.andThen(Ref.set(globalStateRef, value)),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
|
||||||
|
return [fiber, Stream.fromQueue(stateQueue)] as const
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
stateRef: globalStateRef,
|
||||||
|
|
||||||
|
mutate,
|
||||||
|
forkMutate,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { BrowserStream } from "@effect/platform-browser"
|
import { BrowserStream } from "@effect/platform-browser"
|
||||||
import * as AsyncData from "@typed/async-data"
|
import * as AsyncData from "@typed/async-data"
|
||||||
import { type Cause, type Context, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
import { type Cause, type Context, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
||||||
import type * as QueryClient from "./QueryClient.js"
|
import type * as QueryClient from "../QueryClient.js"
|
||||||
|
import * as QueryProgress from "../QueryProgress.js"
|
||||||
|
import * as QueryState from "./QueryState.js"
|
||||||
|
|
||||||
|
|
||||||
export interface QueryRunner<K extends readonly unknown[], A, E, R> {
|
export interface QueryRunner<K extends readonly unknown[], A, E, R> {
|
||||||
@@ -23,7 +25,7 @@ export interface QueryRunner<K extends readonly unknown[], A, E, R> {
|
|||||||
export interface MakeProps<EH, K extends readonly unknown[], A, E, HandledE, R> {
|
export interface MakeProps<EH, K extends readonly unknown[], A, E, HandledE, R> {
|
||||||
readonly QueryClient: QueryClient.GenericTagClass<EH, HandledE>
|
readonly QueryClient: QueryClient.GenericTagClass<EH, HandledE>
|
||||||
readonly key: Stream.Stream<K>
|
readonly key: Stream.Stream<K>
|
||||||
readonly query: (key: K) => Effect.Effect<A, E, R>
|
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const make = <EH, K extends readonly unknown[], A, E, HandledE, R>(
|
export const make = <EH, K extends readonly unknown[], A, E, HandledE, R>(
|
||||||
@@ -43,6 +45,9 @@ export const make = <EH, K extends readonly unknown[], A, E, HandledE, R>(
|
|||||||
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, Exclude<E, HandledE>>())
|
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, Exclude<E, HandledE>>())
|
||||||
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>())
|
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>())
|
||||||
|
|
||||||
|
const queryStateTag = QueryState.makeTag<A, Exclude<E, HandledE>>()
|
||||||
|
const queryStateLayer = QueryState.layer(queryStateTag, stateRef, value => Ref.set(stateRef, value))
|
||||||
|
|
||||||
const interrupt = fiberRef.pipe(
|
const interrupt = fiberRef.pipe(
|
||||||
Effect.flatMap(Option.match({
|
Effect.flatMap(Option.match({
|
||||||
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
@@ -64,27 +69,32 @@ export const make = <EH, K extends readonly unknown[], A, E, HandledE, R>(
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
const run = QueryClient.pipe(
|
const run = Effect.all([
|
||||||
Effect.flatMap(client => client.ErrorHandler),
|
queryStateTag,
|
||||||
Effect.flatMap(errorHandler => latestKeyRef.pipe(
|
QueryClient.pipe(Effect.flatMap(client => client.ErrorHandler)),
|
||||||
|
]).pipe(
|
||||||
|
Effect.flatMap(([state, errorHandler]) => latestKeyRef.pipe(
|
||||||
Effect.flatMap(identity),
|
Effect.flatMap(identity),
|
||||||
Effect.flatMap(key => query(key).pipe(
|
Effect.flatMap(key => query(key).pipe(
|
||||||
errorHandler.handle,
|
errorHandler.handle,
|
||||||
Effect.matchCauseEffect({
|
Effect.matchCauseEffect({
|
||||||
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
|
onSuccess: v => state.set(AsyncData.success(v)),
|
||||||
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
|
onFailure: c => state.set(AsyncData.failure(c)),
|
||||||
}),
|
}),
|
||||||
)),
|
)),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
Effect.provide(context),
|
Effect.provide(context),
|
||||||
|
Effect.provide(QueryProgress.QueryProgress.Live),
|
||||||
)
|
)
|
||||||
|
|
||||||
const forkFetch = interrupt.pipe(
|
const forkFetch = queryStateTag.pipe(
|
||||||
Effect.andThen(Ref.set(stateRef, AsyncData.loading()).pipe(
|
Effect.flatMap(state => interrupt.pipe(
|
||||||
|
Effect.andThen(state.set(AsyncData.loading()).pipe(
|
||||||
Effect.andThen(run),
|
Effect.andThen(run),
|
||||||
Effect.fork,
|
Effect.fork,
|
||||||
)),
|
)),
|
||||||
|
)),
|
||||||
|
|
||||||
Effect.flatMap(fiber =>
|
Effect.flatMap(fiber =>
|
||||||
Ref.set(fiberRef, Option.some(fiber)).pipe(
|
Ref.set(fiberRef, Option.some(fiber)).pipe(
|
||||||
@@ -94,10 +104,12 @@ export const make = <EH, K extends readonly unknown[], A, E, HandledE, R>(
|
|||||||
),
|
),
|
||||||
|
|
||||||
Effect.forkDaemon,
|
Effect.forkDaemon,
|
||||||
|
Effect.provide(queryStateLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
const forkRefresh = interrupt.pipe(
|
const forkRefresh = queryStateTag.pipe(
|
||||||
Effect.andThen(Ref.update(stateRef, previous => {
|
Effect.flatMap(state => interrupt.pipe(
|
||||||
|
Effect.andThen(state.update(previous => {
|
||||||
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
||||||
return AsyncData.refreshing(previous)
|
return AsyncData.refreshing(previous)
|
||||||
if (AsyncData.isRefreshing(previous))
|
if (AsyncData.isRefreshing(previous))
|
||||||
@@ -106,6 +118,7 @@ export const make = <EH, K extends readonly unknown[], A, E, HandledE, R>(
|
|||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.andThen(run),
|
Effect.andThen(run),
|
||||||
Effect.fork,
|
Effect.fork,
|
||||||
|
))
|
||||||
)),
|
)),
|
||||||
|
|
||||||
Effect.flatMap(fiber =>
|
Effect.flatMap(fiber =>
|
||||||
@@ -116,10 +129,11 @@ export const make = <EH, K extends readonly unknown[], A, E, HandledE, R>(
|
|||||||
),
|
),
|
||||||
|
|
||||||
Effect.forkDaemon,
|
Effect.forkDaemon,
|
||||||
|
Effect.provide(queryStateLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
|
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
|
||||||
Effect.andThen(Stream.runForEach(key, latestKey =>
|
Effect.andThen(Stream.runForEach(Stream.changes(key), latestKey =>
|
||||||
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
|
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
|
||||||
Effect.andThen(forkFetch)
|
Effect.andThen(forkFetch)
|
||||||
)
|
)
|
||||||
24
packages/extension-query/src/internal/QueryState.ts
Normal file
24
packages/extension-query/src/internal/QueryState.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type * as AsyncData from "@typed/async-data"
|
||||||
|
import { Context, Effect, Layer } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export interface QueryState<A, E> {
|
||||||
|
readonly get: Effect.Effect<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>
|
||||||
|
readonly update: (f: (previous: AsyncData.AsyncData<A, E>) => AsyncData.AsyncData<A, E>) => Effect.Effect<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeTag = <A, E>(): Context.Tag<QueryState<A, E>, QueryState<A, E>> => Context.GenericTag("@reffuse/query-extension/QueryState")
|
||||||
|
|
||||||
|
export const layer = <A, E>(
|
||||||
|
tag: Context.Tag<QueryState<A, E>, QueryState<A, E>>,
|
||||||
|
get: Effect.Effect<AsyncData.AsyncData<A, E>>,
|
||||||
|
set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>,
|
||||||
|
): Layer.Layer<QueryState<A, E>> => Layer.succeed(tag, {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
update: f => get.pipe(
|
||||||
|
Effect.map(f),
|
||||||
|
Effect.flatMap(set),
|
||||||
|
),
|
||||||
|
})
|
||||||
3
packages/extension-query/src/internal/index.ts
Normal file
3
packages/extension-query/src/internal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * as MutationRunner from "./MutationRunner.js"
|
||||||
|
export * as QueryRunner from "./QueryRunner.js"
|
||||||
|
export * as QueryState from "./QueryState.js"
|
||||||
@@ -395,9 +395,10 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
|
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
||||||
|
|
||||||
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() =>
|
this.useFork(() => Stream.runForEach(
|
||||||
setReactStateValue(v)
|
Stream.changes(ref.changes),
|
||||||
)), [ref])
|
v => Effect.sync(() => setReactStateValue(v)),
|
||||||
|
), [ref])
|
||||||
|
|
||||||
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||||
Ref.update(ref, prevState =>
|
Ref.update(ref, prevState =>
|
||||||
|
|||||||
Reference in New Issue
Block a user