Working query
All checks were successful
Lint / lint (push) Successful in 40s

This commit is contained in:
Julien Valverdé
2025-11-20 01:58:27 +01:00
parent 83128bb467
commit 140b2deda8
4 changed files with 112 additions and 15 deletions

View File

@@ -32,23 +32,19 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
super() super()
} }
interrupt(): Effect.Effect<void> { readonly interrupt: Effect.Effect<void, never, never> = Effect.gen(this, function*() {
return Effect.andThen(this.fiber, Option.match({ return yield* Effect.andThen(this.fiber, Option.match({
onSome: fiber => Effect.andThen( onSome: fiber => Effect.andThen(
Fiber.interrupt(fiber), Fiber.interrupt(fiber),
SubscriptionRef.set(this.fiber, Option.none()), SubscriptionRef.set(this.fiber, Option.none()),
), ),
onNone: () => Effect.void, onNone: () => Effect.void,
})) }))
} })
query(key: K): Effect.Effect< query(key: K): Effect.Effect<Result.Result<A, E, P>, never, Scope.Scope | R> {
Result.Result<A, E, P>,
never,
Result.unsafeForkEffect.OutputContext<A, E, R, P>
> {
return this.fiber.pipe( return this.fiber.pipe(
Effect.andThen(this.interrupt()), Effect.andThen(this.interrupt),
Effect.andThen(Result.unsafeForkEffect(this.f(key), { initialProgress: this.initialProgress })), Effect.andThen(Result.unsafeForkEffect(this.f(key), { initialProgress: this.initialProgress })),
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))), Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
Effect.andThen(([sub]) => Effect.all([Effect.succeed(sub), sub.get])), Effect.andThen(([sub]) => Effect.all([Effect.succeed(sub), sub.get])),
@@ -77,7 +73,7 @@ export const make = Effect.fnUntraced(function* <K extends readonly any[], A, E
): Effect.fn.Return<Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>> { ): Effect.fn.Return<Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>> {
return new QueryImpl( return new QueryImpl(
options.key, options.key,
options.f, options.f as any,
options.initialProgress as P, options.initialProgress as P,
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()), yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
@@ -87,14 +83,18 @@ export const make = Effect.fnUntraced(function* <K extends readonly any[], A, E
export const service = <K extends readonly any[], A, E = never, R = never, P = never>( export const service = <K extends readonly any[], A, E = never, R = never, P = never>(
options: make.Options<K, A, E, R, P> options: make.Options<K, A, E, R, P>
): Effect.Effect<Query<K, A, E, R, P>, never, Scope.Scope> => Effect.tap( ): Effect.Effect<
Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
never,
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
> => Effect.tap(
make(options), make(options),
query => Effect.forkScoped(run(query)), query => Effect.forkScoped(run(query)),
) )
export const run = <K extends readonly any[], A, E, R, P>( export const run = <K extends readonly any[], A, E, R, P>(
self: Query<K, A, E, R, P> self: Query<K, A, E, R, P>
) => Stream.runForEach(self.key, key => Effect.andThen( ): Effect.Effect<void, never, Scope.Scope | R> => Stream.runForEach(self.key, key => Effect.andThen(
(self as QueryImpl<K, A, E, R, P>).interrupt(), (self as QueryImpl<K, A, E, R, P>).interrupt,
Effect.forkScoped((self as QueryImpl<K, A, E, R, P>).query(key)), Effect.forkScoped((self as QueryImpl<K, A, E, R, P>).query(key)),
)) ))

View File

@@ -10,6 +10,7 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as ResultRouteImport } from './routes/result' import { Route as ResultRouteImport } from './routes/result'
import { Route as QueryRouteImport } from './routes/query'
import { Route as FormRouteImport } from './routes/form' import { Route as FormRouteImport } from './routes/form'
import { Route as BlankRouteImport } from './routes/blank' import { Route as BlankRouteImport } from './routes/blank'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
@@ -22,6 +23,11 @@ const ResultRoute = ResultRouteImport.update({
path: '/result', path: '/result',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const QueryRoute = QueryRouteImport.update({
id: '/query',
path: '/query',
getParentRoute: () => rootRouteImport,
} as any)
const FormRoute = FormRouteImport.update({ const FormRoute = FormRouteImport.update({
id: '/form', id: '/form',
path: '/form', path: '/form',
@@ -57,6 +63,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/form': typeof FormRoute '/form': typeof FormRoute
'/query': typeof QueryRoute
'/result': typeof ResultRoute '/result': typeof ResultRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute '/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute '/dev/context': typeof DevContextRoute
@@ -66,6 +73,7 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/form': typeof FormRoute '/form': typeof FormRoute
'/query': typeof QueryRoute
'/result': typeof ResultRoute '/result': typeof ResultRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute '/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute '/dev/context': typeof DevContextRoute
@@ -76,6 +84,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/form': typeof FormRoute '/form': typeof FormRoute
'/query': typeof QueryRoute
'/result': typeof ResultRoute '/result': typeof ResultRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute '/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute '/dev/context': typeof DevContextRoute
@@ -87,6 +96,7 @@ export interface FileRouteTypes {
| '/' | '/'
| '/blank' | '/blank'
| '/form' | '/form'
| '/query'
| '/result' | '/result'
| '/dev/async-rendering' | '/dev/async-rendering'
| '/dev/context' | '/dev/context'
@@ -96,6 +106,7 @@ export interface FileRouteTypes {
| '/' | '/'
| '/blank' | '/blank'
| '/form' | '/form'
| '/query'
| '/result' | '/result'
| '/dev/async-rendering' | '/dev/async-rendering'
| '/dev/context' | '/dev/context'
@@ -105,6 +116,7 @@ export interface FileRouteTypes {
| '/' | '/'
| '/blank' | '/blank'
| '/form' | '/form'
| '/query'
| '/result' | '/result'
| '/dev/async-rendering' | '/dev/async-rendering'
| '/dev/context' | '/dev/context'
@@ -115,6 +127,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
BlankRoute: typeof BlankRoute BlankRoute: typeof BlankRoute
FormRoute: typeof FormRoute FormRoute: typeof FormRoute
QueryRoute: typeof QueryRoute
ResultRoute: typeof ResultRoute ResultRoute: typeof ResultRoute
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
DevContextRoute: typeof DevContextRoute DevContextRoute: typeof DevContextRoute
@@ -130,6 +143,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ResultRouteImport preLoaderRoute: typeof ResultRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/query': {
id: '/query'
path: '/query'
fullPath: '/query'
preLoaderRoute: typeof QueryRouteImport
parentRoute: typeof rootRouteImport
}
'/form': { '/form': {
id: '/form' id: '/form'
path: '/form' path: '/form'
@@ -179,6 +199,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
BlankRoute: BlankRoute, BlankRoute: BlankRoute,
FormRoute: FormRoute, FormRoute: FormRoute,
QueryRoute: QueryRoute,
ResultRoute: ResultRoute, ResultRoute: ResultRoute,
DevAsyncRenderingRoute: DevAsyncRenderingRoute, DevAsyncRenderingRoute: DevAsyncRenderingRoute,
DevContextRoute: DevContextRoute, DevContextRoute: DevContextRoute,

View File

@@ -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<HttpClientError.HttpClientError>().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 (
<Container>
<Slider
value={[id]}
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
/>
{Match.value(result).pipe(
Match.tag("Running", () => <Text>Loading...</Text>),
Match.tag("Success", result => <>
<Heading>{result.value.title}</Heading>
<Text>{result.value.body}</Text>
</>),
Match.tag("Failure", result =>
<Text>An error has occured: {result.cause.toString()}</Text>
),
Match.orElse(() => <></>),
)}
</Container>
)
})
export const Route = createFileRoute("/query")({
component: Component.withRuntime(ResultView, runtime.context)
})

View File

@@ -14,12 +14,12 @@ const Post = Schema.Struct({
}) })
const ResultView = Component.makeUntraced("Result")(function*() { 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(client => client.get("https://jsonplaceholder.typicode.com/posts/1")),
Effect.andThen(response => response.json), Effect.andThen(response => response.json),
Effect.andThen(Schema.decodeUnknown(Post)), Effect.andThen(Schema.decodeUnknown(Post)),
Effect.tap(Effect.sleep("250 millis")), Effect.tap(Effect.sleep("250 millis")),
Result.forkEffectSubscriptionRef, Result.forkEffect,
)) ))
const [result] = yield* Subscribable.useSubscribables([resultSubscribable]) const [result] = yield* Subscribable.useSubscribables([resultSubscribable])