Compare commits
73 Commits
next
...
f23e54b96c
| Author | SHA1 | Date | |
|---|---|---|---|
| f23e54b96c | |||
|
|
56f05e93e7 | ||
|
|
9beddc0877 | ||
|
|
8a354b5519 | ||
|
|
5de4773974 | ||
|
|
1090a685d2 | ||
|
|
f537490f40 | ||
|
|
2348ea9bc1 | ||
|
|
0619af6524 | ||
|
|
993e97676f | ||
|
|
95f53b8a00 | ||
|
|
8b948b2251 | ||
|
|
626a9292d5 | ||
| cb40ecff06 | |||
| b9b9f37859 | |||
|
|
363c7d24f4 | ||
|
|
d57654d872 | ||
|
|
0b7d9383ec | ||
|
|
c380fe9d08 | ||
|
|
92722444cf | ||
|
|
882054b53d | ||
|
|
1c0519cfaf | ||
|
|
f69125012e | ||
|
|
8c8560b63c | ||
|
|
86e8a7bd92 | ||
|
|
12878cd76b | ||
|
|
308025d662 | ||
|
|
2094f254b3 | ||
|
|
8ce4a959a6 | ||
| 3708059da4 | |||
|
|
cd8b5e6364 | ||
|
|
a48b623822 | ||
|
|
499e1e174b | ||
|
|
6b9c177ae7 | ||
|
|
b73b053cc8 | ||
|
|
bbad86bf97 | ||
|
|
6ae311cdfd | ||
|
|
03eca8a1af | ||
|
|
c03d697361 | ||
|
|
3847686d54 | ||
|
|
9801444c0a | ||
|
|
68d8c9fa84 | ||
|
|
cba42bfa52 | ||
|
|
874da0b963 | ||
|
|
bb0579408d | ||
|
|
b39c5946f9 | ||
|
|
aaf494e27a | ||
|
|
dbc5694b6d | ||
|
|
86582de0c5 | ||
|
|
72495bb9b5 | ||
|
|
312c103e71 | ||
|
|
a252cfec27 | ||
|
|
4a5f4c329d | ||
|
|
6f96608f64 | ||
| 0bc29b2cb9 | |||
|
|
8642619a6a | ||
|
|
e8b8df9449 | ||
|
|
3695128923 | ||
|
|
1f14e8be6b | ||
|
|
adc8835304 | ||
|
|
8b06c56ec0 | ||
|
|
003d2f19a2 | ||
|
|
15f6d695f8 | ||
|
|
64583601dc | ||
|
|
cf4ba5805f | ||
|
|
90db94e905 | ||
|
|
336ea67ea2 | ||
|
|
1af839f036 | ||
|
|
6bdf2a4d87 | ||
| 8d55a67e75 | |||
|
|
a1ec5c4781 | ||
| 756b652861 | |||
| 59f9358b9a |
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24"
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v5
|
||||
- name: Install dependencies
|
||||
|
||||
15
bun.lock
15
bun.lock
@@ -5,7 +5,7 @@
|
||||
"name": "@effect-fc/monorepo",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.2.5",
|
||||
"@effect/language-service": "^0.48.0",
|
||||
"@effect/language-service": "^0.53.0",
|
||||
"@types/bun": "^1.2.23",
|
||||
"npm-check-updates": "^19.0.0",
|
||||
"npm-sort": "^0.0.4",
|
||||
@@ -15,10 +15,7 @@
|
||||
},
|
||||
"packages/effect-fc": {
|
||||
"name": "effect-fc",
|
||||
"version": "0.1.5",
|
||||
"dependencies": {
|
||||
"@typed/async-data": "^0.13.1",
|
||||
},
|
||||
"version": "0.2.0",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"effect": "^3.15.0",
|
||||
@@ -32,9 +29,7 @@
|
||||
"@effect/platform": "^0.92.1",
|
||||
"@effect/platform-browser": "^0.72.0",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@typed/async-data": "^0.13.1",
|
||||
"@typed/id": "^0.17.2",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"effect": "^3.18.1",
|
||||
"effect-fc": "workspace:*",
|
||||
"react-icons": "^5.5.0",
|
||||
@@ -135,7 +130,7 @@
|
||||
|
||||
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
|
||||
|
||||
"@effect/language-service": ["@effect/language-service@0.48.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-u7DTPoGFFeDGSdomjY5C2nCGNWSisxpYSqHp3dlSG8kCZh5cay+166bveHRYvuJSJS5yomdkPTJwjwrqMmT7Og=="],
|
||||
"@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=="],
|
||||
|
||||
@@ -423,12 +418,8 @@
|
||||
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.132.31", "", {}, "sha512-rxS8Cm2nIXroLqkm9pE/8X2lFNuvcTIIiFi5VH4PwzvKscAuaW3YRMN1WmaGDI2mVEn+GLaoY6Kc3jOczL5i4w=="],
|
||||
|
||||
"@typed/async-data": ["@typed/async-data@0.13.1", "", { "dependencies": { "@typed/lazy-ref": "^0.3.2", "effect": "^3.11.9" } }, "sha512-rKv3HQtoHeGJwZpEaTL0FAEKfqHcMr/x3GtgkE01p2tJiKjq1eVaPZYpweZEEF/zUutox7DQ14oH85x+ZpPA/Q=="],
|
||||
|
||||
"@typed/id": ["@typed/id@0.17.2", "", { "peerDependencies": { "effect": "^3.14.7" } }, "sha512-z/Z14/moeu9x45IpkGaRwuvb+CQ3s3UCc/agcpZibTz1yPb3RgSDXx4rOHIuyb6hG6oNzqe9yY4GbbMq3Hb5Ug=="],
|
||||
|
||||
"@typed/lazy-ref": ["@typed/lazy-ref@0.3.3", "", { "dependencies": { "effect": "^3.11.9" } }, "sha512-qJoy01/RFYwWBaWhQBzL3Ow20Q+CPybJ/KJnGNKzyDpRUFcEvd3YSQMqZjRdBZmG2wnEpjedAnlCx9ApvKJIlA=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.2.5",
|
||||
"@effect/language-service": "^0.48.0",
|
||||
"@effect/language-service": "^0.53.0",
|
||||
"@types/bun": "^1.2.23",
|
||||
"npm-check-updates": "^19.0.0",
|
||||
"npm-sort": "^0.0.4",
|
||||
|
||||
@@ -41,8 +41,5 @@
|
||||
"@types/react": "^19.0.0",
|
||||
"effect": "^3.15.0",
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typed/async-data": "^0.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, 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, Tracer, type Utils } from "effect"
|
||||
import * as React from "react"
|
||||
import { Memoized } from "./index.js"
|
||||
|
||||
@@ -386,9 +386,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>(
|
||||
@@ -402,7 +402,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>()))
|
||||
}) {}
|
||||
|
||||
@@ -613,25 +613,15 @@ export const useCallbackPromise: {
|
||||
})
|
||||
|
||||
export namespace useContext {
|
||||
export type Options = useScope.Options
|
||||
export type Options = useOnChange.Options
|
||||
}
|
||||
|
||||
export const useContext: {
|
||||
<ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>,
|
||||
options?: useContext.Options,
|
||||
): Effect.Effect<Context.Context<ROut>, E, RIn>
|
||||
} = Effect.fnUntraced(function* <ROut, E, RIn>(
|
||||
export const useContext = <ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>,
|
||||
options?: useContext.Options,
|
||||
) {
|
||||
const scope = yield* useScope([layer], options)
|
||||
|
||||
return yield* useOnChange(() => Effect.context<RIn>().pipe(
|
||||
Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))),
|
||||
Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
|
||||
Effect.andThen(runtime => runtime.runtimeEffect),
|
||||
Effect.andThen(runtime => runtime.context),
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
), [scope])
|
||||
})
|
||||
): Effect.Effect<Context.Context<ROut>, E, RIn> => useOnChange(() => Effect.context<RIn>().pipe(
|
||||
Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))),
|
||||
Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
|
||||
Effect.andThen(runtime => runtime.runtimeEffect),
|
||||
Effect.andThen(runtime => runtime.context),
|
||||
), [layer], options)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as AsyncData from "@typed/async-data"
|
||||
import { Array, Cause, Chunk, type Duration, Effect, Equal, Exit, Fiber, flow, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect"
|
||||
import type { NoSuchElementException } from "effect/Cause"
|
||||
import * as React from "react"
|
||||
import * as Component from "./Component.js"
|
||||
import * as PropertyPath from "./PropertyPath.js"
|
||||
import * as Result from "./Result.js"
|
||||
import * as Subscribable from "./Subscribable.js"
|
||||
import * as SubscriptionRef from "./SubscriptionRef.js"
|
||||
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||
@@ -25,7 +25,7 @@ extends Pipeable.Pipeable {
|
||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>
|
||||
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>
|
||||
readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<void, never>>>
|
||||
readonly submitStateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<SA, SE>>
|
||||
readonly submitResultRef: SubscriptionRef.SubscriptionRef<Result.Result<SA, SE>>
|
||||
|
||||
readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>
|
||||
}
|
||||
@@ -44,7 +44,7 @@ extends Pipeable.Class() implements Form<A, I, R, SA, SE, SR> {
|
||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
|
||||
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
||||
readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<void, never>>>,
|
||||
readonly submitStateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<SA, SE>>,
|
||||
readonly submitResultRef: SubscriptionRef.SubscriptionRef<Result.Result<SA, SE>>,
|
||||
|
||||
readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>,
|
||||
) {
|
||||
@@ -77,7 +77,7 @@ export const make: {
|
||||
const valueRef = yield* SubscriptionRef.make(Option.none<A>())
|
||||
const errorRef = yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>())
|
||||
const validationFiberRef = yield* SubscriptionRef.make(Option.none<Fiber.Fiber<void, never>>())
|
||||
const submitStateRef = yield* SubscriptionRef.make(AsyncData.noData<SA, SE>())
|
||||
const submitResultRef = yield* SubscriptionRef.make<Result.Result<SA, SE>>(Result.initial())
|
||||
|
||||
return new FormImpl(
|
||||
options.schema,
|
||||
@@ -89,15 +89,15 @@ export const make: {
|
||||
yield* SubscriptionRef.make(options.initialEncodedValue),
|
||||
errorRef,
|
||||
validationFiberRef,
|
||||
submitStateRef,
|
||||
submitResultRef,
|
||||
|
||||
Subscribable.map(
|
||||
Subscribable.zipLatestAll(valueRef, errorRef, validationFiberRef, submitStateRef),
|
||||
([value, error, validationFiber, submitState]) => (
|
||||
Subscribable.zipLatestAll(valueRef, errorRef, validationFiberRef, submitResultRef),
|
||||
([value, error, validationFiber, submitResult]) => (
|
||||
Option.isSome(value) &&
|
||||
Option.isNone(error) &&
|
||||
Option.isNone(validationFiber) &&
|
||||
!AsyncData.isLoading(submitState)
|
||||
!(Result.isRunning(submitResult) || Result.isRefreshing(submitResult))
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -150,20 +150,18 @@ export const run = <A, I, R, SA, SE, SR>(
|
||||
|
||||
export const submit = <A, I, R, SA, SE, SR>(
|
||||
self: Form<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<Option.Option<AsyncData.AsyncData<SA, SE>>, NoSuchElementException, SR> => Effect.whenEffect(
|
||||
): Effect.Effect<Option.Option<Result.Result<SA, SE>>, NoSuchElementException, Scope.Scope | SR> => Effect.whenEffect(
|
||||
self.valueRef.pipe(
|
||||
Effect.andThen(identity),
|
||||
Effect.tap(Ref.set(self.submitStateRef, AsyncData.loading())),
|
||||
Effect.andThen(flow(
|
||||
self.onSubmit as (value: NoInfer<A>) => Effect.Effect<SA, SE | ParseResult.ParseError, SR>,
|
||||
Effect.tapErrorTag("ParseError", e => Ref.set(self.errorRef, Option.some(e as ParseResult.ParseError))),
|
||||
Effect.exit,
|
||||
Effect.map(Exit.match({
|
||||
onSuccess: a => AsyncData.success(a),
|
||||
onFailure: e => AsyncData.failure(e as Cause.Cause<SE>),
|
||||
})),
|
||||
Effect.tap(v => Ref.set(self.submitStateRef, v)),
|
||||
)),
|
||||
Effect.andThen(value => Result.forkEffectDequeue(
|
||||
self.onSubmit(value) as Effect.Effect<SA, SE, Result.forkEffectDequeue.InputContext<SR, never>>)
|
||||
),
|
||||
Effect.andThen(Stream.fromQueue),
|
||||
Stream.unwrap,
|
||||
Stream.runFoldEffect(
|
||||
Result.initial() as Result.Result<SA, SE>,
|
||||
(_, result) => Effect.as(Ref.set(self.submitResultRef, result), result),
|
||||
),
|
||||
),
|
||||
|
||||
self.canSubmitSubscribable.get,
|
||||
@@ -198,11 +196,11 @@ export const field = <A, I, R, SA, SE, SR, const P extends PropertyPath.Paths<No
|
||||
onNone: () => Effect.succeed([]),
|
||||
})),
|
||||
Subscribable.map(self.validationFiberRef, Option.isSome),
|
||||
Subscribable.map(self.submitStateRef, AsyncData.isLoading)
|
||||
Subscribable.map(self.submitResultRef, result => Result.isRunning(result) || Result.isRefreshing(result)),
|
||||
)
|
||||
|
||||
|
||||
export const FormFieldTypeId: unique symbol = Symbol.for("effect-fc/FormField")
|
||||
export const FormFieldTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormField")
|
||||
export type FormFieldTypeId = typeof FormFieldTypeId
|
||||
|
||||
export interface FormField<in out A, in out I = A>
|
||||
@@ -237,9 +235,9 @@ export const isFormField = (u: unknown): u is FormField<unknown, unknown> => Pre
|
||||
export const useSubmit = <A, I, R, SA, SE, SR>(
|
||||
self: Form<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<
|
||||
() => Promise<Option.Option<AsyncData.AsyncData<SA, SE>>>,
|
||||
() => Promise<Option.Option<Result.Result<SA, SE>>>,
|
||||
never,
|
||||
SR
|
||||
Scope.Scope | SR
|
||||
> => Component.useCallbackPromise(() => submit(self), [self])
|
||||
|
||||
export const useField = <A, I, R, SA, SE, SR, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
||||
|
||||
253
packages/effect-fc/src/Result.ts
Normal file
253
packages/effect-fc/src/Result.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
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")
|
||||
export type ResultTypeId = typeof ResultTypeId
|
||||
|
||||
export type Result<A, E = never, P = never> = (
|
||||
| Initial
|
||||
| Running<P>
|
||||
| Success<A>
|
||||
| (Success<A> & Refreshing<P>)
|
||||
| Failure<A, E>
|
||||
| (Failure<A, E> & Refreshing<P>)
|
||||
)
|
||||
|
||||
export namespace Result {
|
||||
export interface Prototype extends Pipeable.Pipeable, Equal.Equal {
|
||||
readonly [ResultTypeId]: ResultTypeId
|
||||
}
|
||||
|
||||
export type Success<R extends Result<any, any, any>> = [R] extends [Result<infer A, infer _E, infer _P>] ? A : never
|
||||
export type Failure<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer E, infer _P>] ? E : never
|
||||
export type Progress<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer _E, infer P>] ? P : never
|
||||
}
|
||||
|
||||
export interface Initial extends Result.Prototype {
|
||||
readonly _tag: "Initial"
|
||||
}
|
||||
|
||||
export interface Running<P = never> extends Result.Prototype {
|
||||
readonly _tag: "Running"
|
||||
readonly progress: P
|
||||
}
|
||||
|
||||
export interface Success<A> extends Result.Prototype {
|
||||
readonly _tag: "Success"
|
||||
readonly value: A
|
||||
}
|
||||
|
||||
export interface Failure<A, E = never> extends Result.Prototype {
|
||||
readonly _tag: "Failure"
|
||||
readonly cause: Cause.Cause<E>
|
||||
readonly previousSuccess: Option.Option<Success<A>>
|
||||
}
|
||||
|
||||
export interface Refreshing<P = never> {
|
||||
readonly refreshing: true
|
||||
readonly progress: P
|
||||
}
|
||||
|
||||
|
||||
const ResultPrototype = Object.freeze({
|
||||
...Pipeable.Prototype,
|
||||
[ResultTypeId]: ResultTypeId,
|
||||
|
||||
[Equal.symbol](this: Result<any, any, any>, that: Result<any, any, any>): boolean {
|
||||
if (this._tag !== that._tag)
|
||||
return false
|
||||
|
||||
return Match.value(this).pipe(
|
||||
Match.tag("Initial", () => true),
|
||||
Match.tag("Running", self => Equal.equals(self.progress, (that as Running<any>).progress)),
|
||||
Match.tag("Success", self =>
|
||||
Equal.equals(self.value, (that as Success<any>).value) &&
|
||||
(isRefreshing(self) ? self.refreshing : false) === (isRefreshing(that) ? that.refreshing : false) &&
|
||||
Equal.equals(isRefreshing(self) ? self.progress : undefined, isRefreshing(that) ? that.progress : undefined)
|
||||
),
|
||||
Match.tag("Failure", self =>
|
||||
Equal.equals(self.cause, (that as Failure<any, any>).cause) &&
|
||||
(isRefreshing(self) ? self.refreshing : false) === (isRefreshing(that) ? that.refreshing : false) &&
|
||||
Equal.equals(isRefreshing(self) ? self.progress : undefined, isRefreshing(that) ? that.progress : undefined)
|
||||
),
|
||||
Match.exhaustive,
|
||||
)
|
||||
},
|
||||
|
||||
[Hash.symbol](this: Result<any, any, any>): number {
|
||||
const tagHash = Hash.string(this._tag)
|
||||
|
||||
return Match.value(this).pipe(
|
||||
Match.tag("Initial", () => tagHash),
|
||||
Match.tag("Running", self => Hash.combine(Hash.hash(self.progress))(tagHash)),
|
||||
Match.tag("Success", self => pipe(tagHash,
|
||||
Hash.combine(Hash.hash(self.value)),
|
||||
Hash.combine(Hash.hash(isRefreshing(self) ? self.progress : undefined)),
|
||||
)),
|
||||
Match.tag("Failure", self => pipe(tagHash,
|
||||
Hash.combine(Hash.hash(self.cause)),
|
||||
Hash.combine(Hash.hash(isRefreshing(self) ? self.progress : undefined)),
|
||||
)),
|
||||
Match.exhaustive,
|
||||
Hash.cached(this),
|
||||
)
|
||||
},
|
||||
} as const satisfies Result.Prototype)
|
||||
|
||||
|
||||
export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId)
|
||||
export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial"
|
||||
export const isRunning = (u: unknown): u is Running<unknown> => isResult(u) && u._tag === "Running"
|
||||
export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success"
|
||||
export const isFailure = (u: unknown): u is Failure<unknown, unknown> => isResult(u) && u._tag === "Failure"
|
||||
export const isRefreshing = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "refreshing") && u.refreshing
|
||||
|
||||
export const initial = (): Initial => Object.setPrototypeOf({ _tag: "Initial" }, ResultPrototype)
|
||||
export const running = <P = never>(progress?: P): Running<P> => Object.setPrototypeOf({ _tag: "Running", progress }, ResultPrototype)
|
||||
export const succeed = <A>(value: A): Success<A> => Object.setPrototypeOf({ _tag: "Success", value }, ResultPrototype)
|
||||
|
||||
export const fail = <E, A = never>(
|
||||
cause: Cause.Cause<E>,
|
||||
previousSuccess?: Success<A>,
|
||||
): Failure<A, E> => Object.setPrototypeOf({
|
||||
_tag: "Failure",
|
||||
cause,
|
||||
previousSuccess: Option.fromNullable(previousSuccess),
|
||||
}, ResultPrototype)
|
||||
export const refreshing = <R extends Success<any> | Failure<any, any>, P = never>(
|
||||
result: R,
|
||||
progress?: P,
|
||||
): Omit<R, keyof Refreshing<Result.Progress<R>>> & Refreshing<P> => Object.setPrototypeOf(
|
||||
Object.assign({}, result, { progress }),
|
||||
Object.getPrototypeOf(result),
|
||||
)
|
||||
|
||||
export const fromExit = <A, E>(
|
||||
exit: Exit.Exit<A, E>
|
||||
): Success<A> | Failure<A, E> => exit._tag === "Success"
|
||||
? succeed(exit.value)
|
||||
: fail(exit.cause)
|
||||
|
||||
export const toExit = <A, E, P>(
|
||||
self: Result<A, E, P>
|
||||
): Exit.Exit<A, E | Cause.NoSuchElementException> => {
|
||||
switch (self._tag) {
|
||||
case "Success":
|
||||
return Exit.succeed(self.value)
|
||||
case "Failure":
|
||||
return Exit.failCause(self.cause)
|
||||
default:
|
||||
return Exit.fail(new Cause.NoSuchElementException())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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, PreviousResultNotRunningNorRefreshing | E, R>
|
||||
}
|
||||
|
||||
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>(): 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)),
|
||||
),
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
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<
|
||||
Subscribable.Subscribable<Result<A, E, P>>,
|
||||
never,
|
||||
forkEffect.OutputContext<R>
|
||||
> => 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),
|
||||
})),
|
||||
)),
|
||||
)),
|
||||
) as Effect.Effect<Subscribable.Subscribable<Result<A, E, P>>, never, Scope.Scope>
|
||||
|
||||
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 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,
|
||||
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>
|
||||
@@ -4,16 +4,26 @@ import * as Component from "./Component.js"
|
||||
import * as SetStateAction from "./SetStateAction.js"
|
||||
|
||||
|
||||
export namespace useSubscriptionRefState {
|
||||
export interface Options<A> {
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
}
|
||||
}
|
||||
|
||||
export const useSubscriptionRefState: {
|
||||
<A>(
|
||||
ref: SubscriptionRef.SubscriptionRef<A>
|
||||
ref: SubscriptionRef.SubscriptionRef<A>,
|
||||
options?: useSubscriptionRefState.Options<NoInfer<A>>,
|
||||
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>], never, Scope.Scope>
|
||||
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
||||
} = Effect.fnUntraced(function* <A>(
|
||||
ref: SubscriptionRef.SubscriptionRef<A>,
|
||||
options?: useSubscriptionRefState.Options<NoInfer<A>>,
|
||||
) {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(yield* Component.useOnMount(() => ref))
|
||||
|
||||
yield* Component.useReactEffect(() => Effect.forkScoped(
|
||||
Stream.runForEach(
|
||||
Stream.changesWith(ref.changes, Equivalence.strict()),
|
||||
Stream.changesWith(ref.changes, options?.equivalence ?? Equivalence.strict()),
|
||||
v => Effect.sync(() => setReactStateValue(v)),
|
||||
)
|
||||
), [ref])
|
||||
@@ -28,14 +38,23 @@ export const useSubscriptionRefState: {
|
||||
return [reactStateValue, setValue]
|
||||
})
|
||||
|
||||
export namespace useSubscriptionRefFromState {
|
||||
export interface Options<A> {
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
}
|
||||
}
|
||||
|
||||
export const useSubscriptionRefFromState: {
|
||||
<A>(state: readonly [A, React.Dispatch<React.SetStateAction<A>>]): Effect.Effect<SubscriptionRef.SubscriptionRef<A>, never, Scope.Scope>
|
||||
} = Effect.fnUntraced(function*([value, setValue]) {
|
||||
<A>(
|
||||
state: readonly [A, React.Dispatch<React.SetStateAction<A>>],
|
||||
options?: useSubscriptionRefFromState.Options<NoInfer<A>>,
|
||||
): Effect.Effect<SubscriptionRef.SubscriptionRef<A>, never, Scope.Scope>
|
||||
} = Effect.fnUntraced(function*([value, setValue], options) {
|
||||
const ref = yield* Component.useOnChange(() => Effect.tap(
|
||||
SubscriptionRef.make(value),
|
||||
ref => Effect.forkScoped(
|
||||
Stream.runForEach(
|
||||
Stream.changesWith(ref.changes, Equivalence.strict()),
|
||||
Stream.changesWith(ref.changes, options?.equivalence ?? Equivalence.strict()),
|
||||
v => Effect.sync(() => setValue(v)),
|
||||
)
|
||||
),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Chunk, Effect, Effectable, Option, Predicate, Readable, Ref, Stream, Su
|
||||
import * as PropertyPath from "./PropertyPath.js"
|
||||
|
||||
|
||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/SubscriptionSubRef/SubscriptionSubRef")
|
||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("@effect-fc/SubscriptionSubRef/SubscriptionSubRef")
|
||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
||||
|
||||
export interface SubscriptionSubRef<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
|
||||
|
||||
@@ -4,6 +4,7 @@ export * as Form from "./Form.js"
|
||||
export * as Memoized from "./Memoized.js"
|
||||
export * as PropertyPath from "./PropertyPath.js"
|
||||
export * as ReactRuntime from "./ReactRuntime.js"
|
||||
export * as Result from "./Result.js"
|
||||
export * as SetStateAction from "./SetStateAction.js"
|
||||
export * as Stream from "./Stream.js"
|
||||
export * as Subscribable from "./Subscribable.js"
|
||||
|
||||
@@ -29,9 +29,7 @@
|
||||
"@effect/platform": "^0.92.1",
|
||||
"@effect/platform-browser": "^0.72.0",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@typed/async-data": "^0.13.1",
|
||||
"@typed/id": "^0.17.2",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"effect": "^3.18.1",
|
||||
"effect-fc": "workspace:*",
|
||||
"react-icons": "^5.5.0"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button, Container, Flex } from "@radix-ui/themes"
|
||||
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Option, ParseResult, Schema } from "effect"
|
||||
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
||||
import { Component, Form, Subscribable } from "effect-fc"
|
||||
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||
@@ -50,7 +50,10 @@ class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||
class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() {
|
||||
const form = yield* RegisterForm
|
||||
const submit = yield* Form.useSubmit(form)
|
||||
const [canSubmit] = yield* Subscribable.useSubscribables(form.canSubmitSubscribable)
|
||||
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables(
|
||||
form.canSubmitSubscribable,
|
||||
form.submitResultRef,
|
||||
)
|
||||
|
||||
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||
|
||||
@@ -85,6 +88,14 @@ class RegisterFormView extends Component.makeUntraced("RegisterFormView")(functi
|
||||
<Button disabled={!canSubmit}>Submit</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
|
||||
{Match.value(submitResult).pipe(
|
||||
Match.tag("Initial", () => <></>),
|
||||
Match.tag("Running", () => <Text>Submitting...</Text>),
|
||||
Match.tag("Success", () => <Text>Submitted successfully!</Text>),
|
||||
Match.tag("Failure", v => <Text>Error: {v.cause.toString()}</Text>),
|
||||
Match.exhaustive,
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}) {}
|
||||
|
||||
45
packages/example/src/routes/result.tsx
Normal file
45
packages/example/src/routes/result.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
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, Result, Subscribable } 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 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>
|
||||
{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("/result")({
|
||||
component: Component.withRuntime(ResultView, runtime.context)
|
||||
})
|
||||
Reference in New Issue
Block a user