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",
"devDependencies": {
"@biomejs/biome": "^2.2.5",
"@effect/language-service": "^0.49.0",
"@effect/language-service": "^0.54.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.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=="],

View File

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

View File

@@ -490,7 +490,7 @@ export const useOnMount: {
})
export namespace useOnChange {
export type Options = useScope.Options
export interface Options extends useScope.Options {}
}
export const useOnChange: {
@@ -578,6 +578,22 @@ export const useReactLayoutEffect: {
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: {
<Args extends unknown[], A, E, R>(
f: (...args: Args) => Effect.Effect<A, E, R>,
@@ -613,13 +629,13 @@ export const useCallbackPromise: {
})
export namespace useContext {
export type Options = useOnChange.Options
export interface Options extends useOnChange.Options {}
}
export const useContext = <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>,
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.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
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) => 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,

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.andThen(state => state.set(running(options?.initialProgress)).pipe(
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(
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 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)),
}) 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: {
<const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
...elements: T
elements: T,
options?: useSubscribables.Options<useSubscribables.Success<NoInfer<T>>>,
): Effect.Effect<
[T[number]] extends [never]
? never
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never },
useSubscribables.Success<T>,
[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>[]>(
...elements: T
elements: T,
options?: useSubscribables.Options<useSubscribables.Success<NoInfer<T>>>,
) {
const [reactStateValue, setReactStateValue] = React.useState(
yield* Component.useOnMount(() => Effect.all(elements.map(v => v.get)))
)
yield* Component.useReactEffect(() => Effect.forkScoped(pipe(
elements.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())),
streams => Stream.zipLatestAll(...streams),
yield* Component.useReactEffect(() => Stream.zipLatestAll(...elements.map(ref => ref.changes)).pipe(
Stream.changesWith((options?.equivalence as Equivalence.Equivalence<any[]> | undefined) ?? Equivalence.array(Equivalence.strict())),
Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v))
),
)), elements)
Effect.forkScoped,
), elements)
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
: { 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.isValidatingSubscribable,
props.field.isSubmittingSubscribable,
)
])
return (
<Flex direction="column" gap="1">

View File

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

View File

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

View File

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

View File

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