0.2.1 #26
@@ -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)),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
76
packages/example/src/routes/query.tsx
Normal file
76
packages/example/src/routes/query.tsx
Normal 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)
|
||||||
|
})
|
||||||
@@ -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])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user