Compare commits
6 Commits
ca448c6bae
...
f23e54b96c
| Author | SHA1 | Date | |
|---|---|---|---|
| f23e54b96c | |||
|
|
56f05e93e7 | ||
|
|
9beddc0877 | ||
|
|
8a354b5519 | ||
|
|
5de4773974 | ||
|
|
1090a685d2 |
4
bun.lock
4
bun.lock
@@ -5,7 +5,7 @@
|
||||
"name": "@effect-fc/monorepo",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.2.5",
|
||||
"@effect/language-service": "^0.49.0",
|
||||
"@effect/language-service": "^0.53.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.53.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-EZQMDtDqwOrgB2z7ncOLgOJYK4E0nv6XYeEOqEH7s8W/bxT/SPaaAd+thTcMvGyiz6MqRMaUMQMUyLa4H5Jetg=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.2.5",
|
||||
"@effect/language-service": "^0.49.0",
|
||||
"@effect/language-service": "^0.53.0",
|
||||
"@types/bun": "^1.2.23",
|
||||
"npm-check-updates": "^19.0.0",
|
||||
"npm-sort": "^0.0.4",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/** 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 Utils } from "effect"
|
||||
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 * as React from "react"
|
||||
import { Memoized } from "./index.js"
|
||||
import * as Result from "./Result.js"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
||||
@@ -514,65 +513,6 @@ export const useOnChange: {
|
||||
), [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 interface Options {
|
||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||
|
||||
@@ -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(
|
||||
self.valueRef.pipe(
|
||||
Effect.andThen(identity),
|
||||
Effect.andThen(value => Result.forkEffectScoped(
|
||||
self.onSubmit(value) as Effect.Effect<SA, SE, Result.forkEffectScoped.InputContext<SR, never>>)
|
||||
Effect.andThen(value => Result.forkEffectDequeue(
|
||||
self.onSubmit(value) as Effect.Effect<SA, SE, Result.forkEffectDequeue.InputContext<SR, never>>)
|
||||
),
|
||||
Effect.andThen(Stream.fromQueue),
|
||||
Stream.unwrap,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cause, Context, Data, Effect, Equal, Exit, Hash, Layer, Match, Option, Pipeable, Predicate, pipe, Queue, Ref, Scope } from "effect"
|
||||
import { Cause, Context, Data, Effect, Equal, Exit, Hash, Layer, Match, Option, Pipeable, Predicate, pipe, Queue, Ref, type Scope, type Subscribable, SubscriptionRef } from "effect"
|
||||
|
||||
|
||||
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
|
||||
@@ -142,38 +142,47 @@ 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> {
|
||||
readonly update: <E, R>(
|
||||
f: (previous: P) => Effect.Effect<P, E, R>
|
||||
) => Effect.Effect<void, PreviousResultNotRunningOrRefreshing | E, R>
|
||||
) => Effect.Effect<void, PreviousResultNotRunningNorRefreshing | E, R>
|
||||
}
|
||||
|
||||
export class PreviousResultNotRunningOrRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningOrRefreshing")<{
|
||||
export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{
|
||||
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 makeProgressLayer = <A, E, P = never>(
|
||||
queue: Queue.Enqueue<Result<A, E, P>>,
|
||||
ref: Ref.Ref<Result<A, E, P>>,
|
||||
): Layer.Layer<Progress<P>> => Layer.sync(Progress<P>(), () => ({
|
||||
update: <E, R>(f: (previous: P) => Effect.Effect<P, E, R>) => Effect.Do.pipe(
|
||||
Effect.bind("previous", () => Effect.andThen(
|
||||
ref,
|
||||
previous => isRunning(previous) || isRefreshing(previous)
|
||||
? Effect.succeed(previous)
|
||||
: Effect.fail(new PreviousResultNotRunningOrRefreshing({ previous })),
|
||||
)),
|
||||
Effect.bind("progress", ({ previous }) => f(previous.progress)),
|
||||
Effect.let("next", ({ previous, progress }) => Object.setPrototypeOf(
|
||||
Object.assign({}, previous, { progress }),
|
||||
Object.getPrototypeOf(previous),
|
||||
)),
|
||||
Effect.tap(({ next }) => Ref.set(ref, next)),
|
||||
Effect.tap(({ next }) => Queue.offer(queue, next)),
|
||||
Effect.asVoid,
|
||||
),
|
||||
export const makeProgressLayer = <A, E, P = never>(): Layer.Layer<
|
||||
Progress<P>,
|
||||
never,
|
||||
State<A, E, 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(
|
||||
Effect.bind("previous", () => Effect.andThen(state.get, previous =>
|
||||
isRunning(previous) || isRefreshing(previous)
|
||||
? Effect.succeed(previous)
|
||||
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous })),
|
||||
)),
|
||||
Effect.bind("progress", ({ previous }) => f(previous.progress)),
|
||||
Effect.let("next", ({ previous, progress }) => Object.setPrototypeOf(
|
||||
Object.assign({}, previous, { progress }),
|
||||
Object.getPrototypeOf(previous),
|
||||
)),
|
||||
Effect.andThen(({ next }) => state.set(next)),
|
||||
),
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
@@ -190,75 +199,55 @@ 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>>,
|
||||
Subscribable.Subscribable<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.tap(
|
||||
SubscriptionRef.make<Result<A, E, P>>(initial()),
|
||||
ref => Effect.forkScoped(State<A, E, P>().pipe(
|
||||
Effect.andThen(state => state.set(running(options?.initialProgress)).pipe(
|
||||
Effect.andThen(effect),
|
||||
Effect.onExit(exit => state.set(fromExit(exit))),
|
||||
)),
|
||||
Effect.provide(Layer.empty.pipe(
|
||||
Layer.provideMerge(makeProgressLayer<A, E, P>()),
|
||||
Layer.provideMerge(Layer.succeed(State<A, E, P>(), {
|
||||
get: ref,
|
||||
set: v => Ref.set(ref, v),
|
||||
})),
|
||||
)),
|
||||
)),
|
||||
Effect.map(({ queue }) => queue),
|
||||
) as Effect.Effect<Queue.Queue<Result<A, E, P>>, never, Scope.Scope>
|
||||
) as Effect.Effect<Subscribable.Subscribable<Result<A, E, P>>, never, Scope.Scope>
|
||||
|
||||
export namespace forkEffectScoped {
|
||||
export type InputContext<R, P> = (R extends Progress<infer X>
|
||||
? [X] extends [P]
|
||||
? R
|
||||
: never
|
||||
: R
|
||||
)
|
||||
|
||||
export interface Options<P> {
|
||||
readonly initialProgress?: P
|
||||
}
|
||||
|
||||
export type OutputContext<R> = Scope.Scope | Exclude<R, Progress<any> | Progress<never>>
|
||||
export namespace forkEffectDequeue {
|
||||
export type InputContext<R, P> = forkEffect.InputContext<R, P>
|
||||
export type OutputContext<R> = forkEffect.OutputContext<R>
|
||||
export interface Options<P> extends forkEffect.Options<P> {}
|
||||
}
|
||||
|
||||
export const forkEffectScoped = <A, E, R, P = never>(
|
||||
effect: Effect.Effect<A, E, forkEffectScoped.InputContext<R, NoInfer<P>>>,
|
||||
options?: forkEffectScoped.Options<P>,
|
||||
export const forkEffectDequeue = <A, E, R, P = never>(
|
||||
effect: Effect.Effect<A, E, forkEffectDequeue.InputContext<R, NoInfer<P>>>,
|
||||
options?: forkEffectDequeue.Options<P>,
|
||||
): Effect.Effect<
|
||||
Queue.Dequeue<Result<A, E, P>>,
|
||||
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(({ 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>
|
||||
forkEffectDequeue.OutputContext<R>
|
||||
> => Effect.all([
|
||||
Ref.make<Result<A, E, P>>(initial()),
|
||||
Queue.unbounded<Result<A, E, P>>(),
|
||||
]).pipe(
|
||||
Effect.tap(([ref, queue]) => Effect.forkScoped(State<A, E, P>().pipe(
|
||||
Effect.andThen(state => state.set(running(options?.initialProgress)).pipe(
|
||||
Effect.andThen(effect),
|
||||
Effect.onExit(exit => Effect.andThen(state.set(fromExit(exit)), Queue.shutdown(queue))),
|
||||
)),
|
||||
Effect.provide(Layer.empty.pipe(
|
||||
Layer.provideMerge(makeProgressLayer<A, E, P>()),
|
||||
Layer.provideMerge(Layer.succeed(State<A, E, P>(), {
|
||||
get: ref,
|
||||
set: v => Effect.andThen(Ref.set(ref, v), Queue.offer(queue, v))
|
||||
})),
|
||||
)),
|
||||
))),
|
||||
Effect.map(([, queue]) => queue),
|
||||
) as Effect.Effect<Queue.Dequeue<Result<A, E, P>>, never, Scope.Scope>
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 { Component, Result, Subscribable } from "effect-fc"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
@@ -13,12 +13,15 @@ const Post = Schema.Struct({
|
||||
body: Schema.String,
|
||||
})
|
||||
|
||||
const Result = Component.makeUntraced("Result")(function*() {
|
||||
const result = yield* Component.useOnMountResult(() => HttpClient.HttpClient.pipe(
|
||||
const ResultView = Component.makeUntraced("Result")(function*() {
|
||||
const resultSubscribable = yield* Component.useOnMount(() => HttpClient.HttpClient.pipe(
|
||||
Effect.andThen(client => client.get("https://jsonplaceholder.typicode.com/posts/1")),
|
||||
Effect.andThen(response => response.json),
|
||||
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||
Effect.tap(Effect.sleep("250 millis")),
|
||||
Result.forkEffect,
|
||||
))
|
||||
const [result] = yield* Subscribable.useSubscribables(resultSubscribable)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -35,10 +38,8 @@ const Result = Component.makeUntraced("Result")(function*() {
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
})
|
||||
|
||||
export const Route = createFileRoute("/result")({
|
||||
component: Result
|
||||
component: Component.withRuntime(ResultView, runtime.context)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user