diff --git a/packages/effect-fc-next/src/Query.ts b/packages/effect-fc-next/src/Query.ts index ff84519..4d7c3e7 100644 --- a/packages/effect-fc-next/src/Query.ts +++ b/packages/effect-fc-next/src/Query.ts @@ -254,43 +254,44 @@ extends Pipeable.Class implements Query { }), })), - Effect.onExit(this.f(previous.key), exit => Lens.update( - state, - previous => Exit.match(exit, { - onSuccess: v => ({ - key: previous.key, - result: AsyncResult.success(v), - }), - onFailure: c => AsyncResult.match(previous.result, { - onInitial: () => ({ - key: previous.key, - result: AsyncResult.failure(c), - }), - onSuccess: v => ({ - key: previous.key, - result: AsyncResult.failure(c, { - previousSuccess: Option.some(v), - }), - }), - onFailure: v => ({ - key: previous.key, - result: AsyncResult.failure(c, { - previousSuccess: v.previousSuccess, - }), - }), - }), - }), - ).pipe( - Effect.andThen(Effect.all([ - Effect.fiberId, - Lens.get(this.fiber), - ])), + Effect.onExit(this.f(previous.key), exit => Effect.all([ + Effect.fiberId, + Lens.get(this.fiber), + ]).pipe( Effect.flatMap(([fiberId, fiber]) => Option.match(fiber, { onSome: v => fiberId === v.id ? Lens.set(this.fiber, Option.none()) : Effect.void, onNone: () => Effect.void, })), + Effect.andThen(Lens.updateAndGet( + state, + previous => Exit.match(exit, { + onSuccess: v => ({ + key: previous.key, + result: AsyncResult.success(v), + }), + onFailure: c => AsyncResult.match(previous.result, { + onInitial: () => ({ + key: previous.key, + result: AsyncResult.failure(c), + }), + onSuccess: v => ({ + key: previous.key, + result: AsyncResult.failure(c, { + previousSuccess: Option.some(v), + }), + }), + onFailure: v => ({ + key: previous.key, + result: AsyncResult.failure(c, { + previousSuccess: v.previousSuccess, + }), + }), + }), + }), + )), + Effect.flatMap(finalState => Lens.set(this.latestFinalState, Option.some(finalState as FinalQueryState))), )), )) diff --git a/packages/example-next/src/post.ts b/packages/example-next/src/post.ts deleted file mode 100644 index 2bbe6e6..0000000 --- a/packages/example-next/src/post.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Effect } from "effect" - - -export interface Post { - readonly title: string - readonly body: string -} - -export const fetchPost = (id: number) => Effect.tryPromise({ - try: () => - fetch(`https://jsonplaceholder.typicode.com/posts/${id}`) - .then(response => response.json() as Promise), - - catch: () => new Error("Unable to fetch post"), -}) diff --git a/packages/example-next/src/routes/query.tsx b/packages/example-next/src/routes/query.tsx index 313bbff..fd41ad8 100644 --- a/packages/example-next/src/routes/query.tsx +++ b/packages/example-next/src/routes/query.tsx @@ -1,49 +1,68 @@ import { Button, Container, Flex, Heading, Slider, Text } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" -import { Effect, SubscriptionRef } from "effect" +import { Effect, Schema, SubscriptionRef } from "effect" +import { HttpClient } from "effect/unstable/http" import { AsyncResult } from "effect/unstable/reactivity" import { Component, Lens, Mutation, Query, View } from "effect-fc-next" -import { fetchPost, type Post } from "@/post" import { runtime } from "@/runtime" +const Post = Schema.Struct({ + userId: Schema.Int, + id: Schema.Int, + title: Schema.String, + body: Schema.String, +}) + + interface PostResultViewProps { - readonly result: AsyncResult.AsyncResult + readonly result: AsyncResult.AsyncResult } -const PostResultView = (props: PostResultViewProps) => - AsyncResult.match(props.result, { - onInitial: () => Loading..., - onFailure: () => Request failed., - onSuccess: result => ( - <> - {result.value.title} - {result.value.body} - - ), - }) +const PostResultView = (props: PostResultViewProps) => AsyncResult.match(props.result, { + onInitial: result => result.waiting + ? Loading... + : No data., + onFailure: result => Request failed: { result.cause.toString() }, + onSuccess: result => <> + {result.waiting && Refreshing...} + {result.value.title} + {result.value.body} + , +}) const QueryRouteComponent = Component.make("QueryRouteView")(function*() { const [idLens, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() { - const idLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(1)) + const keyLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(["post", 1 as number] as const)) + const idLens = Lens.focusTupleAt(keyLens, 1) + const query = yield* Query.service({ - key: idLens, - f: fetchPost, + key: keyLens, + f: ([, id]) => HttpClient.HttpClient.pipe( + Effect.tap(Effect.sleep("1 second")), + Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)), + Effect.andThen(response => response.json), + Effect.andThen(Schema.decodeUnknownEffect(Post)), + ), staleTime: "10 seconds", }) + const mutation = yield* Mutation.make({ - f: fetchPost, + f: ([id]: [id: number]) => HttpClient.HttpClient.pipe( + Effect.tap(Effect.sleep("1 second")), + Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)), + Effect.andThen(response => response.json), + Effect.andThen(Schema.decodeUnknownEffect(Post)), + ), }) return [idLens, query, mutation] as const })) - const [id] = yield* View.useAll([idLens]) - const [queryState, mutationResult] = yield* View.useAll([ - query.state, - mutation.state, - ]) - const runPromise = yield* Component.useRunPromise() + const [id, setId] = yield* Lens.useState(idLens) + const [queryState, mutationState] = yield* View.useAll([query.state, mutation.state]) + + const runSync = yield* Component.useRunSync() return ( @@ -52,25 +71,23 @@ const QueryRouteComponent = Component.make("QueryRouteView")(function*() { value={[id]} min={1} max={10} - onValueChange={([value]) => - void runPromise(Lens.set(idLens, value ?? 1)) - } + onValueChange={([value]) => setId(value ?? 1)} /> - - - + - diff --git a/packages/example-next/src/runtime.ts b/packages/example-next/src/runtime.ts index e09447f..c50daee 100644 --- a/packages/example-next/src/runtime.ts +++ b/packages/example-next/src/runtime.ts @@ -1,4 +1,16 @@ +import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser" +import { DateTime, Layer } from "effect" +import { FetchHttpClient } from "effect/unstable/http" import { QueryClient, ReactRuntime } from "effect-fc-next" -export const runtime = ReactRuntime.make(QueryClient.layer()) +export const layer = Layer.empty.pipe( + Layer.provideMerge(QueryClient.layer()), + Layer.provideMerge(DateTime.layerCurrentZoneLocal), + Layer.provideMerge(Clipboard.layer), + Layer.provideMerge(Geolocation.layer), + Layer.provideMerge(Permissions.layer), + Layer.provideMerge(FetchHttpClient.layer), +) + +export const runtime = ReactRuntime.make(layer)