Compare commits

...

4 Commits

Author SHA1 Message Date
ca448c6bae Update dependency @effect/language-service to ^0.52.0
All checks were successful
Lint / lint (push) Successful in 43s
Test build / test-build (pull_request) Successful in 17s
2025-10-31 12:01:27 +00:00
Julien Valverdé
f537490f40 Add forkEffect
All checks were successful
Lint / lint (push) Successful in 13s
2025-10-31 00:52:59 +01:00
Julien Valverdé
2348ea9bc1 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-10-30 20:56:31 +01:00
Julien Valverdé
0619af6524 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-10-30 14:36:47 +01:00
6 changed files with 116 additions and 10 deletions

View File

@@ -5,7 +5,7 @@
"name": "@effect-fc/monorepo",
"devDependencies": {
"@biomejs/biome": "^2.2.5",
"@effect/language-service": "^0.49.0",
"@effect/language-service": "^0.52.0",
"@types/bun": "^1.2.23",
"npm-check-updates": "^19.0.0",
"npm-sort": "^0.0.4",
@@ -130,7 +130,7 @@
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
"@effect/language-service": ["@effect/language-service@0.49.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-m4HGX4XO+ZHN0LZPH+rCQw8iutiFpuPKRuoZCuiyisLoXDpiKHQsIIEUrccDFo4i17nNbrgFdUyqxBJr/eSdnw=="],
"@effect/language-service": ["@effect/language-service@0.52.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-1azwmMvg5UhJ2HgJ7iNhdbrbCvXgPNvszjOKBZmxEWLUSlvzki/e0JX33nz6pW15GTO2ZkuCf2ExwnsFX9atnQ=="],
"@effect/platform": ["@effect/platform@0.92.1", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.18.1" } }, "sha512-XXWCBVwyhaKZISN7aM1fv/3fWDGyxr84ObywnUrL8aHvJLoIeskWFAP/fqw3c5MFCrJ3ZV97RWLbv6JiBQugdg=="],

View File

@@ -16,7 +16,7 @@
},
"devDependencies": {
"@biomejs/biome": "^2.2.5",
"@effect/language-service": "^0.49.0",
"@effect/language-service": "^0.52.0",
"@types/bun": "^1.2.23",
"npm-check-updates": "^19.0.0",
"npm-sort": "^0.0.4",

View File

@@ -1,6 +1,6 @@
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Stream, Tracer, type Types, type Utils } from "effect"
import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Stream, Tracer, type Utils } from "effect"
import * as React from "react"
import { Memoized } from "./index.js"
import * as Result from "./Result.js"
@@ -387,9 +387,9 @@ export const withOptions: {
export const withRuntime: {
<P extends {}, A extends React.ReactNode, E, R>(
context: React.Context<Runtime.Runtime<R>>,
): (self: Component<P, A, E, Types.NoInfer<R>>) => (props: P) => A
): (self: Component<P, A, E, Scope.Scope | NoInfer<R>>) => (props: P) => A
<P extends {}, A extends React.ReactNode, E, R>(
self: Component<P, A, E, Types.NoInfer<R>>,
self: Component<P, A, E, Scope.Scope | NoInfer<R>>,
context: React.Context<Runtime.Runtime<R>>,
): (props: P) => A
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R>(
@@ -403,7 +403,7 @@ export const withRuntime: {
})
export class ScopeMap extends Effect.Service<ScopeMap>()("effect-fc/Component/ScopeMap", {
export class ScopeMap extends Effect.Service<ScopeMap>()("@effect-fc/Component/ScopeMap", {
effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<object, ScopeMap.Entry>()))
}) {}

View File

@@ -1,4 +1,4 @@
import { Cause, Context, Data, Effect, Equal, Exit, Hash, Layer, Match, Option, Pipeable, Predicate, pipe, Queue, Ref, type Scope } from "effect"
import { Cause, Context, Data, Effect, Equal, Exit, Hash, Layer, Match, Option, Pipeable, Predicate, pipe, Queue, Ref, Scope } from "effect"
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
@@ -177,6 +177,46 @@ export const makeProgressLayer = <A, E, P = never>(
}))
export namespace forkEffect {
export type InputContext<R, P> = R extends Progress<infer X> ? [X] extends [P] ? R : never : R
export type OutputContext<R> = Scope.Scope | Exclude<R, Progress<any> | Progress<never>>
export interface Options<P> {
readonly initialProgress?: P
}
}
export const forkEffect = <A, E, R, P = never>(
effect: Effect.Effect<A, E, forkEffect.InputContext<R, NoInfer<P>>>,
options?: forkEffect.Options<P>,
): Effect.Effect<
Queue.Dequeue<Result<A, E, P>>,
never,
forkEffect.OutputContext<R>
> => Effect.Do.pipe(
Effect.bind("scope", () => Scope.Scope),
Effect.bind("queue", () => Queue.unbounded<Result<A, E, P>>()),
Effect.bind("ref", () => Ref.make<Result<A, E, P>>(initial())),
Effect.tap(({ queue, ref }) => Effect.andThen(ref, v => Queue.offer(queue, v))),
Effect.tap(({ scope, queue, ref }) => Effect.forkScoped(
Effect.addFinalizer(() => Queue.shutdown(queue)).pipe(
Effect.andThen(Effect.succeed(running(options?.initialProgress)).pipe(
Effect.tap(v => Ref.set(ref, v)),
Effect.tap(v => Queue.offer(queue, v)),
)),
Effect.andThen(Effect.provideService(effect, Scope.Scope, scope)),
Effect.exit,
Effect.andThen(exit => Effect.succeed(fromExit(exit)).pipe(
Effect.tap(v => Ref.set(ref, v)),
Effect.tap(v => Queue.offer(queue, v)),
)),
Effect.scoped,
Effect.provide(makeProgressLayer(queue, ref)),
)
)),
Effect.map(({ queue }) => queue),
) as Effect.Effect<Queue.Queue<Result<A, E, P>>, never, Scope.Scope>
export namespace forkEffectScoped {
export type InputContext<R, P> = (R extends Progress<infer X>
? [X] extends [P]
@@ -200,16 +240,17 @@ export const forkEffectScoped = <A, E, R, P = never>(
never,
forkEffectScoped.OutputContext<R>
> => Effect.Do.pipe(
Effect.bind("scope", () => Scope.Scope),
Effect.bind("queue", () => Queue.unbounded<Result<A, E, P>>()),
Effect.bind("ref", () => Ref.make<Result<A, E, P>>(initial())),
Effect.tap(({ queue, ref }) => Effect.andThen(ref, v => Queue.offer(queue, v))),
Effect.tap(({ queue, ref }) => Effect.forkScoped(
Effect.tap(({ scope, queue, ref }) => Effect.forkScoped(
Effect.addFinalizer(() => Queue.shutdown(queue)).pipe(
Effect.andThen(Effect.succeed(running(options?.initialProgress)).pipe(
Effect.tap(v => Ref.set(ref, v)),
Effect.tap(v => Queue.offer(queue, v)),
)),
Effect.andThen(effect),
Effect.andThen(Effect.provideService(effect, Scope.Scope, scope)),
Effect.exit,
Effect.andThen(exit => Effect.succeed(fromExit(exit)).pipe(
Effect.tap(v => Ref.set(ref, v)),

View File

@@ -9,6 +9,7 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as ResultRouteImport } from './routes/result'
import { Route as FormRouteImport } from './routes/form'
import { Route as BlankRouteImport } from './routes/blank'
import { Route as IndexRouteImport } from './routes/index'
@@ -16,6 +17,11 @@ import { Route as DevMemoRouteImport } from './routes/dev/memo'
import { Route as DevContextRouteImport } from './routes/dev/context'
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
const ResultRoute = ResultRouteImport.update({
id: '/result',
path: '/result',
getParentRoute: () => rootRouteImport,
} as any)
const FormRoute = FormRouteImport.update({
id: '/form',
path: '/form',
@@ -51,6 +57,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/form': typeof FormRoute
'/result': typeof ResultRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute
'/dev/memo': typeof DevMemoRoute
@@ -59,6 +66,7 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/form': typeof FormRoute
'/result': typeof ResultRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute
'/dev/memo': typeof DevMemoRoute
@@ -68,6 +76,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/form': typeof FormRoute
'/result': typeof ResultRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute
'/dev/memo': typeof DevMemoRoute
@@ -78,6 +87,7 @@ export interface FileRouteTypes {
| '/'
| '/blank'
| '/form'
| '/result'
| '/dev/async-rendering'
| '/dev/context'
| '/dev/memo'
@@ -86,6 +96,7 @@ export interface FileRouteTypes {
| '/'
| '/blank'
| '/form'
| '/result'
| '/dev/async-rendering'
| '/dev/context'
| '/dev/memo'
@@ -94,6 +105,7 @@ export interface FileRouteTypes {
| '/'
| '/blank'
| '/form'
| '/result'
| '/dev/async-rendering'
| '/dev/context'
| '/dev/memo'
@@ -103,6 +115,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
BlankRoute: typeof BlankRoute
FormRoute: typeof FormRoute
ResultRoute: typeof ResultRoute
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
DevContextRoute: typeof DevContextRoute
DevMemoRoute: typeof DevMemoRoute
@@ -110,6 +123,13 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/result': {
id: '/result'
path: '/result'
fullPath: '/result'
preLoaderRoute: typeof ResultRouteImport
parentRoute: typeof rootRouteImport
}
'/form': {
id: '/form'
path: '/form'
@@ -159,6 +179,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
BlankRoute: BlankRoute,
FormRoute: FormRoute,
ResultRoute: ResultRoute,
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
DevContextRoute: DevContextRoute,
DevMemoRoute: DevMemoRoute,

View File

@@ -0,0 +1,44 @@
import { HttpClient } from "@effect/platform"
import { Container, Heading, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Effect, Match, Schema } from "effect"
import { Component } from "effect-fc"
import { runtime } from "@/runtime"
const Post = Schema.Struct({
userId: Schema.Int,
id: Schema.Int,
title: Schema.String,
body: Schema.String,
})
const Result = Component.makeUntraced("Result")(function*() {
const result = yield* Component.useOnMountResult(() => HttpClient.HttpClient.pipe(
Effect.andThen(client => client.get("https://jsonplaceholder.typicode.com/posts/1")),
Effect.andThen(Schema.decodeUnknown(Post)),
Effect.tap(Effect.sleep("250 millis")),
))
return (
<Container>
{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>
)
}).pipe(
Component.withRuntime(runtime.context)
)
export const Route = createFileRoute("/result")({
component: Result
})