diff --git a/packages/effect-fc/src/Query.ts b/packages/effect-fc/src/Query.ts index 091093d..e2df2aa 100644 --- a/packages/effect-fc/src/Query.ts +++ b/packages/effect-fc/src/Query.ts @@ -32,23 +32,19 @@ extends Pipeable.Class() implements Query { super() } - interrupt(): Effect.Effect { - return Effect.andThen(this.fiber, Option.match({ + readonly interrupt: Effect.Effect = Effect.gen(this, function*() { + return yield* Effect.andThen(this.fiber, Option.match({ onSome: fiber => Effect.andThen( Fiber.interrupt(fiber), SubscriptionRef.set(this.fiber, Option.none()), ), onNone: () => Effect.void, })) - } + }) - query(key: K): Effect.Effect< - Result.Result, - never, - Result.unsafeForkEffect.OutputContext - > { + query(key: K): Effect.Effect, never, Scope.Scope | R> { return this.fiber.pipe( - Effect.andThen(this.interrupt()), + Effect.andThen(this.interrupt), Effect.andThen(Result.unsafeForkEffect(this.f(key), { initialProgress: this.initialProgress })), Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))), Effect.andThen(([sub]) => Effect.all([Effect.succeed(sub), sub.get])), @@ -77,7 +73,7 @@ export const make = Effect.fnUntraced(function* , P>> { return new QueryImpl( options.key, - options.f, + options.f as any, options.initialProgress as P, yield* SubscriptionRef.make(Option.none>()), @@ -87,14 +83,18 @@ export const make = Effect.fnUntraced(function* ( options: make.Options -): Effect.Effect, never, Scope.Scope> => Effect.tap( +): Effect.Effect< + Query, P>, + never, + Scope.Scope | Result.forkEffect.OutputContext +> => Effect.tap( make(options), query => Effect.forkScoped(run(query)), ) export const run = ( self: Query -) => Stream.runForEach(self.key, key => Effect.andThen( - (self as QueryImpl).interrupt(), +): Effect.Effect => Stream.runForEach(self.key, key => Effect.andThen( + (self as QueryImpl).interrupt, Effect.forkScoped((self as QueryImpl).query(key)), )) diff --git a/packages/example/src/routeTree.gen.ts b/packages/example/src/routeTree.gen.ts index 13eb94e..779c018 100644 --- a/packages/example/src/routeTree.gen.ts +++ b/packages/example/src/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as ResultRouteImport } from './routes/result' +import { Route as QueryRouteImport } from './routes/query' import { Route as FormRouteImport } from './routes/form' import { Route as BlankRouteImport } from './routes/blank' import { Route as IndexRouteImport } from './routes/index' @@ -22,6 +23,11 @@ const ResultRoute = ResultRouteImport.update({ path: '/result', getParentRoute: () => rootRouteImport, } as any) +const QueryRoute = QueryRouteImport.update({ + id: '/query', + path: '/query', + getParentRoute: () => rootRouteImport, +} as any) const FormRoute = FormRouteImport.update({ id: '/form', path: '/form', @@ -57,6 +63,7 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/blank': typeof BlankRoute '/form': typeof FormRoute + '/query': typeof QueryRoute '/result': typeof ResultRoute '/dev/async-rendering': typeof DevAsyncRenderingRoute '/dev/context': typeof DevContextRoute @@ -66,6 +73,7 @@ export interface FileRoutesByTo { '/': typeof IndexRoute '/blank': typeof BlankRoute '/form': typeof FormRoute + '/query': typeof QueryRoute '/result': typeof ResultRoute '/dev/async-rendering': typeof DevAsyncRenderingRoute '/dev/context': typeof DevContextRoute @@ -76,6 +84,7 @@ export interface FileRoutesById { '/': typeof IndexRoute '/blank': typeof BlankRoute '/form': typeof FormRoute + '/query': typeof QueryRoute '/result': typeof ResultRoute '/dev/async-rendering': typeof DevAsyncRenderingRoute '/dev/context': typeof DevContextRoute @@ -87,6 +96,7 @@ export interface FileRouteTypes { | '/' | '/blank' | '/form' + | '/query' | '/result' | '/dev/async-rendering' | '/dev/context' @@ -96,6 +106,7 @@ export interface FileRouteTypes { | '/' | '/blank' | '/form' + | '/query' | '/result' | '/dev/async-rendering' | '/dev/context' @@ -105,6 +116,7 @@ export interface FileRouteTypes { | '/' | '/blank' | '/form' + | '/query' | '/result' | '/dev/async-rendering' | '/dev/context' @@ -115,6 +127,7 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute BlankRoute: typeof BlankRoute FormRoute: typeof FormRoute + QueryRoute: typeof QueryRoute ResultRoute: typeof ResultRoute DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute DevContextRoute: typeof DevContextRoute @@ -130,6 +143,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResultRouteImport parentRoute: typeof rootRouteImport } + '/query': { + id: '/query' + path: '/query' + fullPath: '/query' + preLoaderRoute: typeof QueryRouteImport + parentRoute: typeof rootRouteImport + } '/form': { id: '/form' path: '/form' @@ -179,6 +199,7 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, BlankRoute: BlankRoute, FormRoute: FormRoute, + QueryRoute: QueryRoute, ResultRoute: ResultRoute, DevAsyncRenderingRoute: DevAsyncRenderingRoute, DevContextRoute: DevContextRoute, diff --git a/packages/example/src/routes/query.tsx b/packages/example/src/routes/query.tsx new file mode 100644 index 0000000..2253fc8 --- /dev/null +++ b/packages/example/src/routes/query.tsx @@ -0,0 +1,76 @@ +import { HttpClient, type HttpClientError } from "@effect/platform" +import { Container, Heading, Slider, Text } from "@radix-ui/themes" +import { createFileRoute } from "@tanstack/react-router" +import { Array, Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect" +import { Component, ErrorObserver, Query, Subscribable, SubscriptionRef } from "effect-fc" +import { runtime } from "@/runtime" + + +const Post = Schema.Struct({ + userId: Schema.Int, + id: Schema.Int, + title: Schema.String, + body: Schema.String, +}) + +const ResultView = Component.makeUntraced("Result")(function*() { + const [idRef, query] = yield* Component.useOnMount(() => Effect.gen(function*() { + const idRef = yield* SubscriptionRef.make(1) + const key = Stream.zipLatest(Stream.make("posts" as const), idRef.changes) + + const query = yield* Query.service({ + key, + f: ([, id]) => HttpClient.HttpClient.pipe( + Effect.tap(Effect.sleep("250 millis")), + Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)), + Effect.andThen(response => response.json), + Effect.andThen(Schema.decodeUnknown(Post)), + ), + }) + + return [idRef, query] as const + })) + + const [id, setId] = yield* SubscriptionRef.useSubscriptionRefState(idRef) + const [result] = yield* Subscribable.useSubscribables([query.result]) + + yield* Component.useOnMount(() => ErrorObserver.ErrorObserver().pipe( + Effect.andThen(observer => observer.subscribe), + Effect.andThen(Stream.fromQueue), + Stream.unwrapScoped, + Stream.runForEach(flow( + Cause.failures, + Chunk.findFirst(e => e._tag === "RequestError" || e._tag === "ResponseError"), + Option.match({ + onSome: e => Console.log("ResultView HttpClient error", e), + onNone: () => Effect.void, + }), + )), + Effect.forkScoped, + )) + + return ( + + + + {Match.value(result).pipe( + Match.tag("Running", () => Loading...), + Match.tag("Success", result => <> + {result.value.title} + {result.value.body} + ), + Match.tag("Failure", result => + An error has occured: {result.cause.toString()} + ), + Match.orElse(() => <>), + )} + + ) +}) + +export const Route = createFileRoute("/query")({ + component: Component.withRuntime(ResultView, runtime.context) +}) diff --git a/packages/example/src/routes/result.tsx b/packages/example/src/routes/result.tsx index 8dc31b3..5e3c545 100644 --- a/packages/example/src/routes/result.tsx +++ b/packages/example/src/routes/result.tsx @@ -14,12 +14,12 @@ const Post = Schema.Struct({ }) const ResultView = Component.makeUntraced("Result")(function*() { - const resultSubscribable = yield* Component.useOnMount(() => HttpClient.HttpClient.pipe( + const [resultSubscribable] = yield* Component.useOnMount(() => HttpClient.HttpClient.pipe( Effect.andThen(client => client.get("https://jsonplaceholder.typicode.com/posts/1")), Effect.andThen(response => response.json), Effect.andThen(Schema.decodeUnknown(Post)), Effect.tap(Effect.sleep("250 millis")), - Result.forkEffectSubscriptionRef, + Result.forkEffect, )) const [result] = yield* Subscribable.useSubscribables([resultSubscribable])