From 3af7c3bf7ad475de5800fc63cd8a22fbf0851bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 4 Mar 2025 22:44:40 +0100 Subject: [PATCH] Query service work --- packages/example/src/query/reffuse.ts | 10 ++++ .../example/src/query/services/Uuid4Query.ts | 20 ++++++++ packages/example/src/query/services/index.ts | 1 + .../src/query/views/Uuid4QueryService.tsx | 32 ++++++++++++ packages/example/src/routeTree.gen.ts | 49 ++++++++++++++++--- packages/example/src/routes/query/service.tsx | 26 ++++++++++ packages/extension-query/src/QueryService.ts | 26 ++++++++++ packages/extension-query/src/index.ts | 1 + packages/reffuse/src/ReffuseHelpers.ts | 6 ++- 9 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 packages/example/src/query/reffuse.ts create mode 100644 packages/example/src/query/services/Uuid4Query.ts create mode 100644 packages/example/src/query/services/index.ts create mode 100644 packages/example/src/query/views/Uuid4QueryService.tsx create mode 100644 packages/example/src/routes/query/service.tsx create mode 100644 packages/extension-query/src/QueryService.ts diff --git a/packages/example/src/query/reffuse.ts b/packages/example/src/query/reffuse.ts new file mode 100644 index 0000000..ced8e18 --- /dev/null +++ b/packages/example/src/query/reffuse.ts @@ -0,0 +1,10 @@ +import { GlobalReffuse } from "@/reffuse" +import { Reffuse, ReffuseContext } from "reffuse" +import { Uuid4Query } from "./services" + + +export const QueryContext = ReffuseContext.make() + +export const R = new class QueryReffuse extends GlobalReffuse.pipe( + Reffuse.withContexts(QueryContext) +) {} diff --git a/packages/example/src/query/services/Uuid4Query.ts b/packages/example/src/query/services/Uuid4Query.ts new file mode 100644 index 0000000..ab11b0e --- /dev/null +++ b/packages/example/src/query/services/Uuid4Query.ts @@ -0,0 +1,20 @@ +import { HttpClient, HttpClientError } from "@effect/platform" +import { QueryService } from "@reffuse/extension-query" +import { Console, Effect, ParseResult, Schema } from "effect" + + +export const Result = Schema.Tuple(Schema.String) + +export class Uuid4Query extends QueryService.Tag("Uuid4Query")() {} + +export const Uuid4QueryLive = QueryService.layer(Uuid4Query, Console.log("Querying...").pipe( + Effect.andThen(Effect.sleep("500 millis")), + Effect.andThen(HttpClient.get("https://www.uuidtools.com/api/generate/v4")), + HttpClient.withTracerPropagation(false), + Effect.flatMap(res => res.json), + Effect.flatMap(Schema.decodeUnknown(Result)), + Effect.scoped, +)) diff --git a/packages/example/src/query/services/index.ts b/packages/example/src/query/services/index.ts new file mode 100644 index 0000000..4f67d41 --- /dev/null +++ b/packages/example/src/query/services/index.ts @@ -0,0 +1 @@ +export * as Uuid4Query from "./Uuid4Query" diff --git a/packages/example/src/query/views/Uuid4QueryService.tsx b/packages/example/src/query/views/Uuid4QueryService.tsx new file mode 100644 index 0000000..ff6463b --- /dev/null +++ b/packages/example/src/query/views/Uuid4QueryService.tsx @@ -0,0 +1,32 @@ +import { Button, Container, Flex, Text } from "@radix-ui/themes" +import * as AsyncData from "@typed/async-data" +import { R } from "../reffuse" +import { Uuid4Query } from "../services" + + +export function Uuid4QueryService() { + const runSync = R.useRunSync() + + const { state, refresh } = R.useMemo(() => Uuid4Query.Uuid4Query, []) + const [queryState] = R.useRefState(state) + + + return ( + + + + {AsyncData.match(queryState, { + NoData: () => "No data yet", + Loading: () => "Loading...", + Success: (value, { isRefreshing, isOptimistic }) => + `Value: ${value} ${isRefreshing ? "(refreshing)" : ""} ${isOptimistic ? "(optimistic)" : ""}`, + Failure: (cause, { isRefreshing }) => + `Error: ${cause} ${isRefreshing ? "(refreshing)" : ""}`, + })} + + + + + + ) +} diff --git a/packages/example/src/routeTree.gen.ts b/packages/example/src/routeTree.gen.ts index 20f3fba..3e3a17c 100644 --- a/packages/example/src/routeTree.gen.ts +++ b/packages/example/src/routeTree.gen.ts @@ -19,6 +19,7 @@ import { Route as LazyrefImport } from './routes/lazyref' import { Route as CountImport } from './routes/count' import { Route as BlankImport } from './routes/blank' import { Route as IndexImport } from './routes/index' +import { Route as QueryServiceImport } from './routes/query/service' // Create/Update Routes @@ -70,6 +71,12 @@ const IndexRoute = IndexImport.update({ getParentRoute: () => rootRoute, } as any) +const QueryServiceRoute = QueryServiceImport.update({ + id: '/service', + path: '/service', + getParentRoute: () => QueryRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -130,20 +137,38 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof TimeImport parentRoute: typeof rootRoute } + '/query/service': { + id: '/query/service' + path: '/service' + fullPath: '/query/service' + preLoaderRoute: typeof QueryServiceImport + parentRoute: typeof QueryImport + } } } // Create and export the route tree +interface QueryRouteChildren { + QueryServiceRoute: typeof QueryServiceRoute +} + +const QueryRouteChildren: QueryRouteChildren = { + QueryServiceRoute: QueryServiceRoute, +} + +const QueryRouteWithChildren = QueryRoute._addFileChildren(QueryRouteChildren) + export interface FileRoutesByFullPath { '/': typeof IndexRoute '/blank': typeof BlankRoute '/count': typeof CountRoute '/lazyref': typeof LazyrefRoute '/promise': typeof PromiseRoute - '/query': typeof QueryRoute + '/query': typeof QueryRouteWithChildren '/tests': typeof TestsRoute '/time': typeof TimeRoute + '/query/service': typeof QueryServiceRoute } export interface FileRoutesByTo { @@ -152,9 +177,10 @@ export interface FileRoutesByTo { '/count': typeof CountRoute '/lazyref': typeof LazyrefRoute '/promise': typeof PromiseRoute - '/query': typeof QueryRoute + '/query': typeof QueryRouteWithChildren '/tests': typeof TestsRoute '/time': typeof TimeRoute + '/query/service': typeof QueryServiceRoute } export interface FileRoutesById { @@ -164,9 +190,10 @@ export interface FileRoutesById { '/count': typeof CountRoute '/lazyref': typeof LazyrefRoute '/promise': typeof PromiseRoute - '/query': typeof QueryRoute + '/query': typeof QueryRouteWithChildren '/tests': typeof TestsRoute '/time': typeof TimeRoute + '/query/service': typeof QueryServiceRoute } export interface FileRouteTypes { @@ -180,6 +207,7 @@ export interface FileRouteTypes { | '/query' | '/tests' | '/time' + | '/query/service' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -190,6 +218,7 @@ export interface FileRouteTypes { | '/query' | '/tests' | '/time' + | '/query/service' id: | '__root__' | '/' @@ -200,6 +229,7 @@ export interface FileRouteTypes { | '/query' | '/tests' | '/time' + | '/query/service' fileRoutesById: FileRoutesById } @@ -209,7 +239,7 @@ export interface RootRouteChildren { CountRoute: typeof CountRoute LazyrefRoute: typeof LazyrefRoute PromiseRoute: typeof PromiseRoute - QueryRoute: typeof QueryRoute + QueryRoute: typeof QueryRouteWithChildren TestsRoute: typeof TestsRoute TimeRoute: typeof TimeRoute } @@ -220,7 +250,7 @@ const rootRouteChildren: RootRouteChildren = { CountRoute: CountRoute, LazyrefRoute: LazyrefRoute, PromiseRoute: PromiseRoute, - QueryRoute: QueryRoute, + QueryRoute: QueryRouteWithChildren, TestsRoute: TestsRoute, TimeRoute: TimeRoute, } @@ -261,13 +291,20 @@ export const routeTree = rootRoute "filePath": "promise.tsx" }, "/query": { - "filePath": "query.tsx" + "filePath": "query.tsx", + "children": [ + "/query/service" + ] }, "/tests": { "filePath": "tests.tsx" }, "/time": { "filePath": "time.tsx" + }, + "/query/service": { + "filePath": "query/service.tsx", + "parent": "/query" } } } diff --git a/packages/example/src/routes/query/service.tsx b/packages/example/src/routes/query/service.tsx new file mode 100644 index 0000000..ad1e3de --- /dev/null +++ b/packages/example/src/routes/query/service.tsx @@ -0,0 +1,26 @@ +import { QueryContext } from "@/query/reffuse" +import { Uuid4Query } from "@/query/services" +import { R } from "@/reffuse" +import { createFileRoute } from "@tanstack/react-router" +import { Effect, Layer } from "effect" +import { useMemo } from "react" + + +export const Route = createFileRoute("/query/service")({ + component: RouteComponent +}) + +function RouteComponent() { + const context = R.useContext() + + const layer = useMemo(() => Layer.empty.pipe( + Layer.provideMerge(Uuid4Query.Uuid4QueryLive), + Layer.provide(context) + ), []) + + return ( + + + + ) +} diff --git a/packages/extension-query/src/QueryService.ts b/packages/extension-query/src/QueryService.ts new file mode 100644 index 0000000..bde1206 --- /dev/null +++ b/packages/extension-query/src/QueryService.ts @@ -0,0 +1,26 @@ +import * as AsyncData from "@typed/async-data" +import { Context, Effect, Fiber, Layer, SubscriptionRef } from "effect" +import * as QueryRunner from "./QueryRunner.js" + + +export interface QueryService { + readonly state: SubscriptionRef.SubscriptionRef> + readonly refresh: Effect.Effect> +} + + +export const Tag = (id: Id) => < + Self, A, E = never, +>() => Effect.Tag(id)>() + +export const layer = ( + tag: Context.TagClass>, + query: Effect.Effect, +): Layer.Layer => Layer.effect(tag, Effect.gen(function*() { + const runner = yield* QueryRunner.make(query) + + return { + state: runner.stateRef, + refresh: runner.forkRefresh, + } +})) diff --git a/packages/extension-query/src/index.ts b/packages/extension-query/src/index.ts index 0052524..790fe0c 100644 --- a/packages/extension-query/src/index.ts +++ b/packages/extension-query/src/index.ts @@ -1,2 +1,3 @@ export * from "./QueryExtension.js" export * as QueryRunner from "./QueryRunner.js" +export * as QueryService from "./QueryService.js" diff --git a/packages/reffuse/src/ReffuseHelpers.ts b/packages/reffuse/src/ReffuseHelpers.ts index 77b3b6c..bdb8858 100644 --- a/packages/reffuse/src/ReffuseHelpers.ts +++ b/packages/reffuse/src/ReffuseHelpers.ts @@ -1,4 +1,4 @@ -import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, Pipeable, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect" +import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, Layer, Pipeable, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect" import * as React from "react" import * as ReffuseContext from "./ReffuseContext.js" import * as ReffuseRuntime from "./ReffuseRuntime.js" @@ -23,6 +23,10 @@ export abstract class ReffuseHelpers { return ReffuseContext.useMergeAll(...this.constructor.contexts) } + useLayer(this: ReffuseHelpers): Layer.Layer { + return ReffuseContext.useMergeAllLayers(...this.constructor.contexts) + } + useRunSync(this: ReffuseHelpers): (effect: Effect.Effect) => A { const runtime = ReffuseRuntime.useRuntime()