Compare commits

..

1 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
6 changed files with 159 additions and 89 deletions

View File

@@ -5,7 +5,7 @@
"name": "@effect-fc/monorepo", "name": "@effect-fc/monorepo",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.2.5", "@biomejs/biome": "^2.2.5",
"@effect/language-service": "^0.53.0", "@effect/language-service": "^0.52.0",
"@types/bun": "^1.2.23", "@types/bun": "^1.2.23",
"npm-check-updates": "^19.0.0", "npm-check-updates": "^19.0.0",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
@@ -130,7 +130,7 @@
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"], "@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
"@effect/language-service": ["@effect/language-service@0.53.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-EZQMDtDqwOrgB2z7ncOLgOJYK4E0nv6XYeEOqEH7s8W/bxT/SPaaAd+thTcMvGyiz6MqRMaUMQMUyLa4H5Jetg=="], "@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=="], "@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": { "devDependencies": {
"@biomejs/biome": "^2.2.5", "@biomejs/biome": "^2.2.5",
"@effect/language-service": "^0.53.0", "@effect/language-service": "^0.52.0",
"@types/bun": "^1.2.23", "@types/bun": "^1.2.23",
"npm-check-updates": "^19.0.0", "npm-check-updates": "^19.0.0",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",

View File

@@ -1,8 +1,9 @@
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */ /** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ /** 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, Tracer, 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 * as React from "react"
import { Memoized } from "./index.js" import { Memoized } from "./index.js"
import * as Result from "./Result.js"
export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component") export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
@@ -513,6 +514,65 @@ export const useOnChange: {
), [scope]) ), [scope])
}) })
export namespace useOnMountResult {
export interface Options<A, E, P> extends Result.forkEffectScoped.Options<P> {
readonly equivalence?: Equivalence.Equivalence<Result.Result<A, E, P>>
}
}
export const useOnMountResult: {
<A, E, R, P = never>(
f: () => Effect.Effect<A, E, Result.forkEffectScoped.InputContext<R, NoInfer<P>>>,
options?: useOnChangeResult.Options<A, E, P>,
): Effect.Effect<Result.Result<A, E, P>, never, Result.forkEffectScoped.OutputContext<R>>
} = Effect.fnUntraced(function* <A, E, R, P = never>(
f: () => Effect.Effect<A, E, Result.forkEffectScoped.InputContext<R, NoInfer<P>>>,
options?: useOnChangeResult.Options<A, E, P>,
) {
const [result, setResult] = React.useState(() => Result.initial() as Result.Result<A, E, P>)
yield* useOnMount(() => Result.forkEffectScoped(f(), options).pipe(
Effect.andThen(Stream.fromQueue),
Stream.unwrap,
Stream.changesWith(options?.equivalence ?? Equivalence.strict()),
Stream.runForEach(result => Effect.sync(() => setResult(result))),
))
return result
})
export namespace useOnChangeResult {
export interface Options<A, E, P>
extends useOnMountResult.Options<A, E, P>, useReactEffect.Options {}
}
export const useOnChangeResult: {
<A, E, R, P = never>(
f: () => Effect.Effect<A, E, Result.forkEffectScoped.InputContext<R, NoInfer<P>>>,
deps?: React.DependencyList,
options?: useOnChangeResult.Options<A, E, P>,
): Effect.Effect<
Result.Result<A, E, P>,
never,
Exclude<Result.forkEffectScoped.OutputContext<R>, Scope.Scope>
>
} = Effect.fnUntraced(function* <A, E, R, P = never>(
f: () => Effect.Effect<A, E, Result.forkEffectScoped.InputContext<R, NoInfer<P>>>,
deps?: React.DependencyList,
options?: useOnChangeResult.Options<A, E, P>,
) {
const [result, setResult] = React.useState(() => Result.initial() as Result.Result<A, E, P>)
yield* useReactEffect(() => Result.forkEffectScoped(f(), options).pipe(
Effect.andThen(Stream.fromQueue),
Stream.unwrap,
Stream.changesWith(options?.equivalence ?? Equivalence.strict()),
Stream.runForEach(result => Effect.sync(() => setResult(result))),
), deps, options)
return result
})
export namespace useReactEffect { export namespace useReactEffect {
export interface Options { export interface Options {
readonly finalizerExecutionMode?: "sync" | "fork" readonly finalizerExecutionMode?: "sync" | "fork"

View File

@@ -153,8 +153,8 @@ export const submit = <A, I, R, SA, SE, SR>(
): Effect.Effect<Option.Option<Result.Result<SA, SE>>, NoSuchElementException, Scope.Scope | SR> => Effect.whenEffect( ): Effect.Effect<Option.Option<Result.Result<SA, SE>>, NoSuchElementException, Scope.Scope | SR> => Effect.whenEffect(
self.valueRef.pipe( self.valueRef.pipe(
Effect.andThen(identity), Effect.andThen(identity),
Effect.andThen(value => Result.forkEffectDequeue( Effect.andThen(value => Result.forkEffectScoped(
self.onSubmit(value) as Effect.Effect<SA, SE, Result.forkEffectDequeue.InputContext<SR, never>>) self.onSubmit(value) as Effect.Effect<SA, SE, Result.forkEffectScoped.InputContext<SR, never>>)
), ),
Effect.andThen(Stream.fromQueue), Effect.andThen(Stream.fromQueue),
Stream.unwrap, Stream.unwrap,

View File

@@ -1,4 +1,4 @@
import { Cause, Context, Data, Effect, Equal, Exit, Hash, Layer, Match, Option, Pipeable, Predicate, pipe, Queue, Ref, type Scope, type Subscribable, SubscriptionRef } 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") export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
@@ -142,47 +142,38 @@ export const toExit = <A, E, P>(
} }
export interface State<A, E = never, P = never> {
readonly get: Effect.Effect<Result<A, E, P>>
readonly set: (v: Result<A, E, P>) => Effect.Effect<void>
}
export const State = <A, E = never, P = never>(): Context.Tag<State<A, E, P>, State<A, E, P>> => Context.GenericTag("@effect-fc/Result/State")
export interface Progress<P = never> { export interface Progress<P = never> {
readonly update: <E, R>( readonly update: <E, R>(
f: (previous: P) => Effect.Effect<P, E, R> f: (previous: P) => Effect.Effect<P, E, R>
) => Effect.Effect<void, PreviousResultNotRunningNorRefreshing | E, R> ) => Effect.Effect<void, PreviousResultNotRunningOrRefreshing | E, R>
} }
export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{ export class PreviousResultNotRunningOrRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningOrRefreshing")<{
readonly previous: Result<unknown, unknown, unknown> readonly previous: Result<unknown, unknown, unknown>
}> {} }> {}
export const Progress = <P = never>(): Context.Tag<Progress<P>, Progress<P>> => Context.GenericTag("@effect-fc/Result/Progress") export const Progress = <P = never>(): Context.Tag<Progress<P>, Progress<P>> => Context.GenericTag("@effect-fc/Result/Progress")
export const makeProgressLayer = <A, E, P = never>(): Layer.Layer< export const makeProgressLayer = <A, E, P = never>(
Progress<P>, queue: Queue.Enqueue<Result<A, E, P>>,
never, ref: Ref.Ref<Result<A, E, P>>,
State<A, E, P> ): Layer.Layer<Progress<P>> => Layer.sync(Progress<P>(), () => ({
> => Layer.effect(Progress<P>(), Effect.gen(function*() {
const state = yield* State<A, E, P>()
return {
update: <E, R>(f: (previous: P) => Effect.Effect<P, E, R>) => Effect.Do.pipe( update: <E, R>(f: (previous: P) => Effect.Effect<P, E, R>) => Effect.Do.pipe(
Effect.bind("previous", () => Effect.andThen(state.get, previous => Effect.bind("previous", () => Effect.andThen(
isRunning(previous) || isRefreshing(previous) ref,
previous => isRunning(previous) || isRefreshing(previous)
? Effect.succeed(previous) ? Effect.succeed(previous)
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous })), : Effect.fail(new PreviousResultNotRunningOrRefreshing({ previous })),
)), )),
Effect.bind("progress", ({ previous }) => f(previous.progress)), Effect.bind("progress", ({ previous }) => f(previous.progress)),
Effect.let("next", ({ previous, progress }) => Object.setPrototypeOf( Effect.let("next", ({ previous, progress }) => Object.setPrototypeOf(
Object.assign({}, previous, { progress }), Object.assign({}, previous, { progress }),
Object.getPrototypeOf(previous), Object.getPrototypeOf(previous),
)), )),
Effect.andThen(({ next }) => state.set(next)), Effect.tap(({ next }) => Ref.set(ref, next)),
Effect.tap(({ next }) => Queue.offer(queue, next)),
Effect.asVoid,
), ),
}
})) }))
@@ -199,55 +190,75 @@ export const forkEffect = <A, E, R, P = never>(
effect: Effect.Effect<A, E, forkEffect.InputContext<R, NoInfer<P>>>, effect: Effect.Effect<A, E, forkEffect.InputContext<R, NoInfer<P>>>,
options?: forkEffect.Options<P>, options?: forkEffect.Options<P>,
): Effect.Effect< ): Effect.Effect<
Subscribable.Subscribable<Result<A, E, P>>, Queue.Dequeue<Result<A, E, P>>,
never, never,
forkEffect.OutputContext<R> forkEffect.OutputContext<R>
> => Effect.tap( > => Effect.Do.pipe(
SubscriptionRef.make<Result<A, E, P>>(initial()), Effect.bind("scope", () => Scope.Scope),
ref => Effect.forkScoped(State<A, E, P>().pipe( Effect.bind("queue", () => Queue.unbounded<Result<A, E, P>>()),
Effect.andThen(state => state.set(running(options?.initialProgress)).pipe( Effect.bind("ref", () => Ref.make<Result<A, E, P>>(initial())),
Effect.andThen(effect), Effect.tap(({ queue, ref }) => Effect.andThen(ref, v => Queue.offer(queue, v))),
Effect.onExit(exit => state.set(fromExit(exit))), 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.provide(Layer.empty.pipe( Effect.andThen(Effect.provideService(effect, Scope.Scope, scope)),
Layer.provideMerge(makeProgressLayer<A, E, P>()), Effect.exit,
Layer.provideMerge(Layer.succeed(State<A, E, P>(), { Effect.andThen(exit => Effect.succeed(fromExit(exit)).pipe(
get: ref, Effect.tap(v => Ref.set(ref, v)),
set: v => Ref.set(ref, v), Effect.tap(v => Queue.offer(queue, v)),
})),
)), )),
Effect.scoped,
Effect.provide(makeProgressLayer(queue, ref)),
)
)), )),
) as Effect.Effect<Subscribable.Subscribable<Result<A, E, P>>, never, Scope.Scope> Effect.map(({ queue }) => queue),
) as Effect.Effect<Queue.Queue<Result<A, E, P>>, never, Scope.Scope>
export namespace forkEffectDequeue { export namespace forkEffectScoped {
export type InputContext<R, P> = forkEffect.InputContext<R, P> export type InputContext<R, P> = (R extends Progress<infer X>
export type OutputContext<R> = forkEffect.OutputContext<R> ? [X] extends [P]
export interface Options<P> extends forkEffect.Options<P> {} ? R
: never
: R
)
export interface Options<P> {
readonly initialProgress?: P
} }
export const forkEffectDequeue = <A, E, R, P = never>( export type OutputContext<R> = Scope.Scope | Exclude<R, Progress<any> | Progress<never>>
effect: Effect.Effect<A, E, forkEffectDequeue.InputContext<R, NoInfer<P>>>, }
options?: forkEffectDequeue.Options<P>,
export const forkEffectScoped = <A, E, R, P = never>(
effect: Effect.Effect<A, E, forkEffectScoped.InputContext<R, NoInfer<P>>>,
options?: forkEffectScoped.Options<P>,
): Effect.Effect< ): Effect.Effect<
Queue.Dequeue<Result<A, E, P>>, Queue.Dequeue<Result<A, E, P>>,
never, never,
forkEffectDequeue.OutputContext<R> forkEffectScoped.OutputContext<R>
> => Effect.all([ > => Effect.Do.pipe(
Ref.make<Result<A, E, P>>(initial()), Effect.bind("scope", () => Scope.Scope),
Queue.unbounded<Result<A, E, P>>(), Effect.bind("queue", () => Queue.unbounded<Result<A, E, P>>()),
]).pipe( Effect.bind("ref", () => Ref.make<Result<A, E, P>>(initial())),
Effect.tap(([ref, queue]) => Effect.forkScoped(State<A, E, P>().pipe( Effect.tap(({ queue, ref }) => Effect.andThen(ref, v => Queue.offer(queue, v))),
Effect.andThen(state => state.set(running(options?.initialProgress)).pipe( Effect.tap(({ scope, queue, ref }) => Effect.forkScoped(
Effect.andThen(effect), Effect.addFinalizer(() => Queue.shutdown(queue)).pipe(
Effect.onExit(exit => Effect.andThen(state.set(fromExit(exit)), Queue.shutdown(queue))), Effect.andThen(Effect.succeed(running(options?.initialProgress)).pipe(
Effect.tap(v => Ref.set(ref, v)),
Effect.tap(v => Queue.offer(queue, v)),
)), )),
Effect.provide(Layer.empty.pipe( Effect.andThen(Effect.provideService(effect, Scope.Scope, scope)),
Layer.provideMerge(makeProgressLayer<A, E, P>()), Effect.exit,
Layer.provideMerge(Layer.succeed(State<A, E, P>(), { Effect.andThen(exit => Effect.succeed(fromExit(exit)).pipe(
get: ref, Effect.tap(v => Ref.set(ref, v)),
set: v => Effect.andThen(Ref.set(ref, v), Queue.offer(queue, v)) Effect.tap(v => Queue.offer(queue, v)),
})),
)), )),
))), Effect.scoped,
Effect.map(([, queue]) => queue), Effect.provide(makeProgressLayer(queue, ref)),
) as Effect.Effect<Queue.Dequeue<Result<A, E, P>>, never, Scope.Scope> )
)),
Effect.map(({ queue }) => queue),
) as Effect.Effect<Queue.Queue<Result<A, E, P>>, never, Scope.Scope>

View File

@@ -2,7 +2,7 @@ import { HttpClient } from "@effect/platform"
import { Container, Heading, Text } from "@radix-ui/themes" import { Container, Heading, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Effect, Match, Schema } from "effect" import { Effect, Match, Schema } from "effect"
import { Component, Result, Subscribable } from "effect-fc" import { Component } from "effect-fc"
import { runtime } from "@/runtime" import { runtime } from "@/runtime"
@@ -13,15 +13,12 @@ const Post = Schema.Struct({
body: Schema.String, body: Schema.String,
}) })
const ResultView = Component.makeUntraced("Result")(function*() { const Result = Component.makeUntraced("Result")(function*() {
const resultSubscribable = yield* Component.useOnMount(() => HttpClient.HttpClient.pipe( const result = yield* Component.useOnMountResult(() => 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(Schema.decodeUnknown(Post)), Effect.andThen(Schema.decodeUnknown(Post)),
Effect.tap(Effect.sleep("250 millis")), Effect.tap(Effect.sleep("250 millis")),
Result.forkEffect,
)) ))
const [result] = yield* Subscribable.useSubscribables(resultSubscribable)
return ( return (
<Container> <Container>
@@ -38,8 +35,10 @@ const ResultView = Component.makeUntraced("Result")(function*() {
)} )}
</Container> </Container>
) )
}) }).pipe(
Component.withRuntime(runtime.context)
)
export const Route = createFileRoute("/result")({ export const Route = createFileRoute("/result")({
component: Component.withRuntime(ResultView, runtime.context) component: Result
}) })