Compare commits

..

8 Commits

Author SHA1 Message Date
129eee7ac5 Update dependency @effect/language-service to ^0.54.0
Some checks failed
Lint / lint (push) Failing after 11s
Test build / test-build (pull_request) Failing after 13s
2025-11-03 12:01:27 +00:00
Julien Valverdé
87e7b74ed6 Fix
Some checks failed
Lint / lint (push) Failing after 38s
2025-11-03 01:21:58 +01:00
Julien Valverdé
4b82b8e627 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-11-03 00:00:16 +01:00
Julien Valverdé
4f69f667b0 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-11-02 21:00:12 +01:00
Julien Valverdé
0b8418e114 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-11-02 13:30:02 +01:00
Julien Valverdé
65447a6fec Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-11-02 13:17:06 +01:00
Julien Valverdé
15a9ef3f79 Fix useSubscribables
All checks were successful
Lint / lint (push) Successful in 40s
2025-11-02 12:39:54 +01:00
Julien Valverdé
7132f7a463 Subscribable work
Some checks failed
Lint / lint (push) Failing after 12s
2025-11-02 00:58:05 +01:00
11 changed files with 73 additions and 31 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.54.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.54.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-s9z8sbmZH1dIQzL+9imlBTt4XEbo7o+D8lAAxyp6kcjnvwYUYoebdT3KhBYpvqzIBGgiJlCMcXNzhXUTrz25VA=="],
"@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.54.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

@@ -490,7 +490,7 @@ export const useOnMount: {
}) })
export namespace useOnChange { export namespace useOnChange {
export type Options = useScope.Options export interface Options extends useScope.Options {}
} }
export const useOnChange: { export const useOnChange: {
@@ -578,6 +578,22 @@ export const useReactLayoutEffect: {
React.useLayoutEffect(() => runReactEffect(runtime, f, options), deps) React.useLayoutEffect(() => runReactEffect(runtime, f, options), deps)
}) })
export const useRunSync: {
<R = never>(): Effect.Effect<<A, E = never>(effect: Effect.Effect<A, E, R>) => A, never, Scope.Scope | R>
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
f: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
// biome-ignore lint/style/noNonNullAssertion: context initialization
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
runtimeRef.current = yield* Effect.runtime<R>()
Runtime.runSync()
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(f(...args)), deps)
})
export const useCallbackSync: { export const useCallbackSync: {
<Args extends unknown[], A, E, R>( <Args extends unknown[], A, E, R>(
f: (...args: Args) => Effect.Effect<A, E, R>, f: (...args: Args) => Effect.Effect<A, E, R>,
@@ -613,13 +629,13 @@ export const useCallbackPromise: {
}) })
export namespace useContext { export namespace useContext {
export type Options = useOnChange.Options export interface Options extends useOnChange.Options {}
} }
export const useContext = <ROut, E, RIn>( export const useContext = <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>, layer: Layer.Layer<ROut, E, RIn>,
options?: useContext.Options, options?: useContext.Options,
): Effect.Effect<Context.Context<ROut>, E, RIn> => useOnChange(() => Effect.context<RIn>().pipe( ): Effect.Effect<Context.Context<ROut>, E, Scope.Scope | RIn> => useOnChange(() => Effect.context<RIn>().pipe(
Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))), Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))),
Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)), Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
Effect.andThen(runtime => runtime.runtimeEffect), Effect.andThen(runtime => runtime.runtimeEffect),

View File

@@ -162,6 +162,19 @@ export const submit = <A, I, R, SA, SE, SR>(
Result.initial() as Result.Result<SA, SE>, Result.initial() as Result.Result<SA, SE>,
(_, result) => Effect.as(Ref.set(self.submitResultRef, result), result), (_, result) => Effect.as(Ref.set(self.submitResultRef, result), result),
), ),
Effect.tap(result => Result.isFailure(result)
? Option.match(
Chunk.findFirst(
Cause.failures(result.cause as Cause.Cause<ParseResult.ParseError>),
e => e._tag === "ParseError",
),
{
onSome: e => Ref.set(self.errorRef, Option.some(e)),
onNone: () => Effect.void,
},
)
: Effect.void
),
), ),
self.canSubmitSubscribable.get, self.canSubmitSubscribable.get,

View File

@@ -239,7 +239,10 @@ export const forkEffectDequeue = <A, E, R, P = never>(
Effect.tap(([ref, queue]) => Effect.forkScoped(State<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(state => state.set(running(options?.initialProgress)).pipe(
Effect.andThen(effect), Effect.andThen(effect),
Effect.onExit(exit => Effect.andThen(state.set(fromExit(exit)), Queue.shutdown(queue))), Effect.onExit(exit => Effect.andThen(
state.set(fromExit(exit)),
Effect.forkScoped(Queue.shutdown(queue)),
)),
)), )),
Effect.provide(Layer.empty.pipe( Effect.provide(Layer.empty.pipe(
Layer.provideMerge(makeProgressLayer<A, E, P>()), Layer.provideMerge(makeProgressLayer<A, E, P>()),

View File

@@ -1,4 +1,4 @@
import { Effect, Equivalence, pipe, type Scope, Stream, Subscribable } from "effect" import { Effect, Equivalence, type Scope, Stream, Subscribable } from "effect"
import * as React from "react" import * as React from "react"
import * as Component from "./Component.js" import * as Component from "./Component.js"
@@ -16,30 +16,40 @@ export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<
changes: Stream.zipLatestAll(...elements.map(v => v.changes)), changes: Stream.zipLatestAll(...elements.map(v => v.changes)),
}) as any }) as any
export namespace useSubscribables {
export type Success<T extends readonly Subscribable.Subscribable<any, any, any>[]> = [T[number]] extends [never]
? never
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never }
export interface Options<A> {
readonly equivalence?: Equivalence.Equivalence<A>
}
}
export const useSubscribables: { export const useSubscribables: {
<const T extends readonly Subscribable.Subscribable<any, any, any>[]>( <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
...elements: T elements: T,
options?: useSubscribables.Options<useSubscribables.Success<NoInfer<T>>>,
): Effect.Effect< ): Effect.Effect<
[T[number]] extends [never] useSubscribables.Success<T>,
? never
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never },
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never, [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never,
([T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never) | Scope.Scope Scope.Scope | ([T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never)
> >
} = Effect.fnUntraced(function* <const T extends readonly Subscribable.Subscribable<any, any, any>[]>( } = Effect.fnUntraced(function* <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
...elements: T elements: T,
options?: useSubscribables.Options<useSubscribables.Success<NoInfer<T>>>,
) { ) {
const [reactStateValue, setReactStateValue] = React.useState( const [reactStateValue, setReactStateValue] = React.useState(
yield* Component.useOnMount(() => Effect.all(elements.map(v => v.get))) yield* Component.useOnMount(() => Effect.all(elements.map(v => v.get)))
) )
yield* Component.useReactEffect(() => Effect.forkScoped(pipe( yield* Component.useReactEffect(() => Stream.zipLatestAll(...elements.map(ref => ref.changes)).pipe(
elements.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())), Stream.changesWith((options?.equivalence as Equivalence.Equivalence<any[]> | undefined) ?? Equivalence.array(Equivalence.strict())),
streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v => Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v)) Effect.sync(() => setReactStateValue(v))
), ),
)), elements) Effect.forkScoped,
), elements)
return reactStateValue as any return reactStateValue as any
}) })

View File

@@ -28,11 +28,11 @@ export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInp
// biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported // biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported
: { optional: false, ...yield* Form.useInput(props.field, props) } : { optional: false, ...yield* Form.useInput(props.field, props) }
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables( const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
props.field.issuesSubscribable, props.field.issuesSubscribable,
props.field.isValidatingSubscribable, props.field.isValidatingSubscribable,
props.field.isSubmittingSubscribable, props.field.isSubmittingSubscribable,
) ])
return ( return (
<Flex direction="column" gap="1"> <Flex direction="column" gap="1">

View File

@@ -39,10 +39,10 @@ class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
), ),
initialEncodedValue: { email: "", password: "", birth: Option.none() }, initialEncodedValue: { email: "", password: "", birth: Option.none() },
onSubmit: v => Effect.sleep("500 millis").pipe( onSubmit: Effect.fnUntraced(function*(v) {
Effect.andThen(Console.log(v)), yield* Effect.sleep("500 millis")
Effect.andThen(Effect.sync(() => alert("Done!"))), return v
), }),
debounce: "500 millis", debounce: "500 millis",
}) })
}) {} }) {}
@@ -50,10 +50,10 @@ class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() { class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() {
const form = yield* RegisterForm const form = yield* RegisterForm
const submit = yield* Form.useSubmit(form) const submit = yield* Form.useSubmit(form)
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables( const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
form.canSubmitSubscribable, form.canSubmitSubscribable,
form.submitResultRef, form.submitResultRef,
) ])
const TextFieldFormInputFC = yield* TextFieldFormInput const TextFieldFormInputFC = yield* TextFieldFormInput
@@ -93,7 +93,7 @@ class RegisterFormView extends Component.makeUntraced("RegisterFormView")(functi
Match.tag("Initial", () => <></>), Match.tag("Initial", () => <></>),
Match.tag("Running", () => <Text>Submitting...</Text>), Match.tag("Running", () => <Text>Submitting...</Text>),
Match.tag("Success", () => <Text>Submitted successfully!</Text>), Match.tag("Success", () => <Text>Submitted successfully!</Text>),
Match.tag("Failure", v => <Text>Error: {v.cause.toString()}</Text>), Match.tag("Failure", e => <Text>Error: {e.cause.toString()}</Text>),
Match.exhaustive, Match.exhaustive,
)} )}
</Container> </Container>

View File

@@ -21,7 +21,7 @@ const ResultView = Component.makeUntraced("Result")(function*() {
Effect.tap(Effect.sleep("250 millis")), Effect.tap(Effect.sleep("250 millis")),
Result.forkEffect, Result.forkEffect,
)) ))
const [result] = yield* Subscribable.useSubscribables(resultSubscribable) const [result] = yield* Subscribable.useSubscribables([resultSubscribable])
return ( return (
<Container> <Container>

View File

@@ -78,11 +78,11 @@ export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoPr
] as const ] as const
}), [props._tag, props._tag === "edit" ? props.id : undefined]) }), [props._tag, props._tag === "edit" ? props.id : undefined])
const [index, size, canSubmit] = yield* Subscribable.useSubscribables( const [index, size, canSubmit] = yield* Subscribable.useSubscribables([
indexRef, indexRef,
state.sizeSubscribable, state.sizeSubscribable,
form.canSubmitSubscribable, form.canSubmitSubscribable,
) ])
const submit = yield* Form.useSubmit(form) const submit = yield* Form.useSubmit(form)
const TextFieldFormInputFC = yield* TextFieldFormInput const TextFieldFormInputFC = yield* TextFieldFormInput

View File

@@ -7,7 +7,7 @@ import { TodosState } from "./TodosState.service"
export class Todos extends Component.makeUntraced("Todos")(function*() { export class Todos extends Component.makeUntraced("Todos")(function*() {
const state = yield* TodosState const state = yield* TodosState
const [todos] = yield* Subscribable.useSubscribables(state.ref) const [todos] = yield* Subscribable.useSubscribables([state.ref])
yield* Component.useOnMount(() => Effect.andThen( yield* Component.useOnMount(() => Effect.andThen(
Console.log("Todos mounted"), Console.log("Todos mounted"),