1 Commits

Author SHA1 Message Date
11c10f7142 Update dependency @effect/language-service to ^0.77.0
All checks were successful
Lint / lint (push) Successful in 12s
Test build / test-build (pull_request) Successful in 18s
2026-02-27 12:01:47 +00:00
15 changed files with 169 additions and 163 deletions

View File

@@ -6,7 +6,7 @@
"name": "@effect-fc/monorepo", "name": "@effect-fc/monorepo",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.11", "@biomejs/biome": "^2.3.11",
"@effect/language-service": "^0.78.0", "@effect/language-service": "^0.77.0",
"@types/bun": "^1.3.6", "@types/bun": "^1.3.6",
"npm-check-updates": "^19.3.1", "npm-check-updates": "^19.3.1",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
@@ -116,7 +116,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.78.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-dEYMhvhKFhikXpXjOlcByRKOU34xFeZbZcOuVTeWTQmjmZ/ZF4K3z0Ku6vL4cWKC8RRIB5di+ix18Y4f9ey5Dg=="], "@effect/language-service": ["@effect/language-service@0.77.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-QP2bri8DdcK7Eo+SqFS2yNeD0Ch9kKHYxq2jeE9CaPpBknevCNFb3+hT6qSsPt2P6yOkhNP83KMy5Uk7DGBXlg=="],
"@effect/platform": ["@effect/platform@0.94.2", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.15" } }, "sha512-85vdwpnK4oH/rJ3EuX/Gi2Hkt+K4HvXWr9bxCuqvty9hxyEcRxkJcqTesYrcVoQB6aULb1Za2B0MKoTbvffB3Q=="], "@effect/platform": ["@effect/platform@0.94.2", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.15" } }, "sha512-85vdwpnK4oH/rJ3EuX/Gi2Hkt+K4HvXWr9bxCuqvty9hxyEcRxkJcqTesYrcVoQB6aULb1Za2B0MKoTbvffB3Q=="],

View File

@@ -16,7 +16,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.11", "@biomejs/biome": "^2.3.11",
"@effect/language-service": "^0.78.0", "@effect/language-service": "^0.77.0",
"@types/bun": "^1.3.6", "@types/bun": "^1.3.6",
"npm-check-updates": "^19.3.1", "npm-check-updates": "^19.3.1",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",

View File

@@ -4,13 +4,11 @@ import * as React from "react"
import * as Component from "./Component.js" import * as Component from "./Component.js"
export const AsyncTypeId: unique symbol = Symbol.for("@effect-fc/Async/Async") export const TypeId: unique symbol = Symbol.for("@effect-fc/Async/Async")
export type AsyncTypeId = typeof AsyncTypeId export type TypeId = typeof TypeId
export interface Async extends AsyncPrototype, AsyncOptions {} export interface Async extends AsyncOptions {
readonly [TypeId]: TypeId
export interface AsyncPrototype {
readonly [AsyncTypeId]: AsyncTypeId
} }
export interface AsyncOptions { export interface AsyncOptions {
@@ -20,14 +18,14 @@ export interface AsyncOptions {
export type AsyncProps = Omit<React.SuspenseProps, "children"> export type AsyncProps = Omit<React.SuspenseProps, "children">
export const AsyncPrototype: AsyncPrototype = Object.freeze({ export const AsyncPrototype = Object.freeze({
[AsyncTypeId]: AsyncTypeId, [TypeId]: TypeId,
asFunctionComponent<P extends {}, A extends React.ReactNode, E, R>( asFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
this: Component.Component<P, A, E, R> & Async, this: Component.Component<P, A, E, R> & Async,
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>, runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
) { ) {
const Inner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise) const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
return ({ fallback, name, ...props }: AsyncProps) => { return ({ fallback, name, ...props }: AsyncProps) => {
const promise = Runtime.runPromise(runtimeRef.current)( const promise = Runtime.runPromise(runtimeRef.current)(
@@ -40,21 +38,17 @@ export const AsyncPrototype: AsyncPrototype = Object.freeze({
return React.createElement( return React.createElement(
React.Suspense, React.Suspense,
{ fallback: fallback ?? this.defaultFallback, name }, { fallback: fallback ?? this.defaultFallback, name },
React.createElement(Inner, { promise }), React.createElement(SuspenseInner, { promise }),
) )
} }
}, },
} as const) } as const)
export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, AsyncTypeId) export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, TypeId)
export const async = <T extends Component.Component<any, any, any, any>>( export const async = <T extends Component.Component<any, any, any, any>>(
self: T & ( self: T
"promise" extends keyof Component.Component.Props<T>
? "The 'promise' prop name is restricted for Async components. Please rename the 'promise' prop to something else."
: T
)
): ( ): (
& Omit<T, keyof Component.Component.AsComponent<T>> & Omit<T, keyof Component.Component.AsComponent<T>>
& Component.Component< & Component.Component<

View File

@@ -6,12 +6,12 @@ import * as Result from "./Result.js"
export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query") export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
export type QueryTypeId = typeof QueryTypeId export type QueryTypeId = typeof QueryTypeId
export interface Query<in out K extends Query.AnyKey, in out A, in out KE = never, in out KR = never, in out E = never, in out R = never, in out P = never> export interface Query<in out K extends Query.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
extends Pipeable.Pipeable { extends Pipeable.Pipeable {
readonly [QueryTypeId]: QueryTypeId readonly [QueryTypeId]: QueryTypeId
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | KR | R> readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>
readonly key: Stream.Stream<K, KE, KR> readonly key: Stream.Stream<K>
readonly f: (key: K) => Effect.Effect<A, E, R> readonly f: (key: K) => Effect.Effect<A, E, R>
readonly initialProgress: P readonly initialProgress: P
@@ -37,13 +37,13 @@ export declare namespace Query {
export type AnyKey = readonly any[] export type AnyKey = readonly any[]
} }
export class QueryImpl<in out K extends Query.AnyKey, in out A, in out KE = never, in out KR = never, in out E = never, in out R = never, in out P = never> export class QueryImpl<in out K extends Query.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
extends Pipeable.Class() implements Query<K, A, KE, KR, E, R, P> { extends Pipeable.Class() implements Query<K, A, E, R, P> {
readonly [QueryTypeId]: QueryTypeId = QueryTypeId readonly [QueryTypeId]: QueryTypeId = QueryTypeId
constructor( constructor(
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | KR | R>, readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>,
readonly key: Stream.Stream<K, KE, KR>, readonly key: Stream.Stream<K>,
readonly f: (key: K) => Effect.Effect<A, E, R>, readonly f: (key: K) => Effect.Effect<A, E, R>,
readonly initialProgress: P, readonly initialProgress: P,
@@ -77,7 +77,6 @@ extends Pipeable.Class() implements Query<K, A, KE, KR, E, R, P> {
], { concurrency: "unbounded" }).pipe( ], { concurrency: "unbounded" }).pipe(
Effect.ignore, Effect.ignore,
this.runSemaphore.withPermits(1), this.runSemaphore.withPermits(1),
Effect.provide(this.context),
) )
} }
@@ -266,11 +265,11 @@ extends Pipeable.Class() implements Query<K, A, KE, KR, E, R, P> {
} }
} }
export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown> => Predicate.hasProperty(u, QueryTypeId) export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, QueryTypeId)
export declare namespace make { export declare namespace make {
export interface Options<K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never> { export interface Options<K extends Query.AnyKey, A, E = never, R = never, P = never> {
readonly key: Stream.Stream<K, KE, KR> readonly key: Stream.Stream<K>
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>> readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
readonly initialProgress?: P readonly initialProgress?: P
readonly staleTime?: Duration.DurationInput readonly staleTime?: Duration.DurationInput
@@ -278,17 +277,17 @@ export declare namespace make {
} }
} }
export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>( export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, E = never, R = never, P = never>(
options: make.Options<K, A, KE, KR, E, R, P> options: make.Options<K, A, E, R, P>
): Effect.fn.Return< ): Effect.fn.Return<
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>, Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
never, never,
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P> Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
> { > {
const client = yield* QueryClient.QueryClient const client = yield* QueryClient.QueryClient
return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>( return new QueryImpl(
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>>(), yield* Effect.context<Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>>(),
options.key, options.key,
options.f as any, options.f as any,
options.initialProgress as P, options.initialProgress as P,
@@ -305,12 +304,12 @@ export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE =
) )
}) })
export const service = <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>( export const service = <K extends Query.AnyKey, A, E = never, R = never, P = never>(
options: make.Options<K, A, KE, KR, E, R, P> options: make.Options<K, A, E, R, P>
): Effect.Effect< ): Effect.Effect<
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>, Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
never, never,
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P> Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
> => Effect.tap( > => Effect.tap(
make(options), make(options),
query => Effect.forkScoped(query.run), query => Effect.forkScoped(query.run),

View File

@@ -240,16 +240,16 @@ export const unsafeForkEffect = <A, E, R, P = never>(
Effect.provide(Layer.empty.pipe( Effect.provide(Layer.empty.pipe(
Layer.provideMerge(makeProgressLayer<A, E, P>()), Layer.provideMerge(makeProgressLayer<A, E, P>()),
Layer.provideMerge(Layer.succeed(State<A, E, P>(), { Layer.provideMerge(Layer.succeed(State<A, E, P>(), {
get: Ref.get(ref), get: ref,
set: v => Effect.andThen(Ref.set(ref, v), PubSub.publish(pubsub, v)) set: v => Effect.andThen(Ref.set(ref, v), PubSub.publish(pubsub, v))
})), })),
)), )),
))), ))),
Effect.map(({ ref, pubsub, fiber }) => [ Effect.map(({ ref, pubsub, fiber }) => [
Subscribable.make({ Subscribable.make({
get: Ref.get(ref), get: ref,
changes: Stream.unwrapScoped(Effect.map( changes: Stream.unwrapScoped(Effect.map(
Effect.all([Ref.get(ref), Stream.fromPubSub(pubsub, { scoped: true })]), Effect.all([ref, Stream.fromPubSub(pubsub, { scoped: true })]),
([latest, stream]) => Stream.concat(Stream.make(latest), stream), ([latest, stream]) => Stream.concat(Stream.make(latest), stream),
)), )),
}), }),

View File

@@ -18,7 +18,7 @@ extends Omit<TextField.RootProps, "optional" | "defaultValue">, Form.useOptional
export type TextFieldFormInputProps = Props | OptionalProps export type TextFieldFormInputProps = Props | OptionalProps
export class TextFieldFormInputView extends Component.make("TextFieldFormInputView")(function*(props: TextFieldFormInputProps) { export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInput")(function*(props: TextFieldFormInputProps) {
const input: ( const input: (
| { readonly optional: true } & Form.useOptionalInput.Success<string> | { readonly optional: true } & Form.useOptionalInput.Success<string>
| { readonly optional: false } & Form.useInput.Success<string> | { readonly optional: false } & Form.useInput.Success<string>

View File

@@ -13,10 +13,10 @@ import { Route as ResultRouteImport } from './routes/result'
import { Route as QueryRouteImport } from './routes/query' import { Route as QueryRouteImport } from './routes/query'
import { Route as FormRouteImport } from './routes/form' import { Route as FormRouteImport } from './routes/form'
import { Route as BlankRouteImport } from './routes/blank' import { Route as BlankRouteImport } from './routes/blank'
import { Route as AsyncRouteImport } from './routes/async'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as DevMemoRouteImport } from './routes/dev/memo' import { Route as DevMemoRouteImport } from './routes/dev/memo'
import { Route as DevContextRouteImport } from './routes/dev/context' import { Route as DevContextRouteImport } from './routes/dev/context'
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
const ResultRoute = ResultRouteImport.update({ const ResultRoute = ResultRouteImport.update({
id: '/result', id: '/result',
@@ -38,11 +38,6 @@ const BlankRoute = BlankRouteImport.update({
path: '/blank', path: '/blank',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AsyncRoute = AsyncRouteImport.update({
id: '/async',
path: '/async',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@@ -58,35 +53,40 @@ const DevContextRoute = DevContextRouteImport.update({
path: '/dev/context', path: '/dev/context',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
id: '/dev/async-rendering',
path: '/dev/async-rendering',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/async': typeof AsyncRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/form': typeof FormRoute '/form': typeof FormRoute
'/query': typeof QueryRoute '/query': typeof QueryRoute
'/result': typeof ResultRoute '/result': typeof ResultRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute '/dev/context': typeof DevContextRoute
'/dev/memo': typeof DevMemoRoute '/dev/memo': typeof DevMemoRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/async': typeof AsyncRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/form': typeof FormRoute '/form': typeof FormRoute
'/query': typeof QueryRoute '/query': typeof QueryRoute
'/result': typeof ResultRoute '/result': typeof ResultRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute '/dev/context': typeof DevContextRoute
'/dev/memo': typeof DevMemoRoute '/dev/memo': typeof DevMemoRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/async': typeof AsyncRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/form': typeof FormRoute '/form': typeof FormRoute
'/query': typeof QueryRoute '/query': typeof QueryRoute
'/result': typeof ResultRoute '/result': typeof ResultRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute '/dev/context': typeof DevContextRoute
'/dev/memo': typeof DevMemoRoute '/dev/memo': typeof DevMemoRoute
} }
@@ -94,42 +94,42 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: fullPaths:
| '/' | '/'
| '/async'
| '/blank' | '/blank'
| '/form' | '/form'
| '/query' | '/query'
| '/result' | '/result'
| '/dev/async-rendering'
| '/dev/context' | '/dev/context'
| '/dev/memo' | '/dev/memo'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
| '/async'
| '/blank' | '/blank'
| '/form' | '/form'
| '/query' | '/query'
| '/result' | '/result'
| '/dev/async-rendering'
| '/dev/context' | '/dev/context'
| '/dev/memo' | '/dev/memo'
id: id:
| '__root__' | '__root__'
| '/' | '/'
| '/async'
| '/blank' | '/blank'
| '/form' | '/form'
| '/query' | '/query'
| '/result' | '/result'
| '/dev/async-rendering'
| '/dev/context' | '/dev/context'
| '/dev/memo' | '/dev/memo'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AsyncRoute: typeof AsyncRoute
BlankRoute: typeof BlankRoute BlankRoute: typeof BlankRoute
FormRoute: typeof FormRoute FormRoute: typeof FormRoute
QueryRoute: typeof QueryRoute QueryRoute: typeof QueryRoute
ResultRoute: typeof ResultRoute ResultRoute: typeof ResultRoute
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
DevContextRoute: typeof DevContextRoute DevContextRoute: typeof DevContextRoute
DevMemoRoute: typeof DevMemoRoute DevMemoRoute: typeof DevMemoRoute
} }
@@ -164,13 +164,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof BlankRouteImport preLoaderRoute: typeof BlankRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/async': {
id: '/async'
path: '/async'
fullPath: '/async'
preLoaderRoute: typeof AsyncRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@@ -192,16 +185,23 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DevContextRouteImport preLoaderRoute: typeof DevContextRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/dev/async-rendering': {
id: '/dev/async-rendering'
path: '/dev/async-rendering'
fullPath: '/dev/async-rendering'
preLoaderRoute: typeof DevAsyncRenderingRouteImport
parentRoute: typeof rootRouteImport
}
} }
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AsyncRoute: AsyncRoute,
BlankRoute: BlankRoute, BlankRoute: BlankRoute,
FormRoute: FormRoute, FormRoute: FormRoute,
QueryRoute: QueryRoute, QueryRoute: QueryRoute,
ResultRoute: ResultRoute, ResultRoute: ResultRoute,
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
DevContextRoute: DevContextRoute, DevContextRoute: DevContextRoute,
DevMemoRoute: DevMemoRoute, DevMemoRoute: DevMemoRoute,
} }

View File

@@ -1,64 +0,0 @@
import { HttpClient } from "@effect/platform"
import { Container, Flex, Heading, Slider, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Array, Effect, flow, Option, Schema } from "effect"
import { Async, Component, Memoized } from "effect-fc"
import * as React from "react"
import { runtime } from "@/runtime"
const Post = Schema.Struct({
userId: Schema.Int,
id: Schema.Int,
title: Schema.String,
body: Schema.String,
})
interface AsyncFetchPostViewProps {
readonly id: number
}
class AsyncFetchPostView extends Component.make("AsyncFetchPostView")(function*(props: AsyncFetchPostViewProps) {
const post = yield* Component.useOnChange(() => HttpClient.HttpClient.pipe(
Effect.tap(Effect.sleep("500 millis")),
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ props.id }`)),
Effect.andThen(response => response.json),
Effect.andThen(Schema.decodeUnknown(Post)),
), [props.id])
return (
<div>
<Heading>{post.title}</Heading>
<Text>{post.body}</Text>
</div>
)
}).pipe(
Async.async,
Async.withOptions({ defaultFallback: <Text>Loading post...</Text> }),
Memoized.memoized,
) {}
const AsyncRouteComponent = Component.make("AsyncRouteView")(function*() {
const [id, setId] = React.useState(1)
const AsyncFetchPost = yield* AsyncFetchPostView.use
return (
<Container>
<Flex direction="column" align="center" gap="2">
<Slider
value={[id]}
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
/>
<AsyncFetchPost id={id} />
</Flex>
</Container>
)
}).pipe(
Component.withRuntime(runtime.context)
)
export const Route = createFileRoute("/async")({
component: AsyncRouteComponent,
})

View File

@@ -0,0 +1,78 @@
import { Flex, Text, TextField } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Effect } from "effect"
import { Async, Component, Memoized } from "effect-fc"
import * as React from "react"
import { runtime } from "@/runtime"
// Generator version
const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent.use
const AsyncComponentFC = yield* AsyncComponent.use
const [input, setInput] = React.useState("")
return (
<Flex direction="column" align="stretch" gap="2">
<TextField.Root
value={input}
onChange={e => setInput(e.target.value)}
/>
<MemoizedAsyncComponentFC fallback={React.useMemo(() => <p>Loading memoized...</p>, [])} />
<AsyncComponentFC />
</Flex>
)
}).pipe(
Component.withRuntime(runtime.context)
)
// Pipeline version
// const RouteComponent = Component.make("RouteComponent")(() => Effect.Do,
// Effect.bind("VMemoizedAsyncComponent", () => Component.useFC(MemoizedAsyncComponent)),
// Effect.bind("VAsyncComponent", () => Component.useFC(AsyncComponent)),
// Effect.let("input", () => React.useState("")),
// Effect.map(({ input: [input, setInput], VAsyncComponent, VMemoizedAsyncComponent }) =>
// <Flex direction="column" align="stretch" gap="2">
// <TextField.Root
// value={input}
// onChange={e => setInput(e.target.value)}
// />
// <VMemoizedAsyncComponent />
// <VAsyncComponent />
// </Flex>
// ),
// ).pipe(
// Component.withRuntime(runtime.context)
// )
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
const SubComponentFC = yield* SubComponent.use
yield* Effect.sleep("500 millis") // Async operation
// Cannot use React hooks after the async operation
return (
<Flex direction="column" align="stretch">
<Text>Rendered!</Text>
<SubComponentFC />
</Flex>
)
}).pipe(
Async.async,
Async.withOptions({ defaultFallback: <p>Loading...</p> }),
) {}
class MemoizedAsyncComponent extends Memoized.memoized(AsyncComponent) {}
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
const [state] = React.useState(yield* Component.useOnMount(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
return <Text>{state}</Text>
}) {}
export const Route = createFileRoute("/dev/async-rendering")({
component: RouteComponent
})

View File

@@ -2,7 +2,7 @@ import { Button, Container, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect" import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
import { Component, Form, Subscribable } from "effect-fc" import { Component, Form, Subscribable } from "effect-fc"
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView" import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
import { DateTimeUtcFromZonedInput } from "@/lib/schema" import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { runtime } from "@/runtime" import { runtime } from "@/runtime"
@@ -38,7 +38,7 @@ const RegisterFormSubmitSchema = Schema.Struct({
birth: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf), birth: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
}) })
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", { class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
scoped: Form.service({ scoped: Form.service({
schema: RegisterFormSchema.pipe( schema: RegisterFormSchema.pipe(
Schema.compose( Schema.compose(
@@ -62,15 +62,15 @@ class RegisterFormService extends Effect.Service<RegisterFormService>()("Registe
}) })
}) {} }) {}
class RegisterFormView extends Component.make("RegisterFormView")(function*() { class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() {
const form = yield* RegisterFormService const form = yield* RegisterForm
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([ const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
form.canSubmit, form.canSubmit,
form.mutation.result, form.mutation.result,
]) ])
const runPromise = yield* Component.useRunPromise() const runPromise = yield* Component.useRunPromise()
const TextFieldFormInput = yield* TextFieldFormInputView.use const TextFieldFormInputFC = yield* TextFieldFormInput.use
yield* Component.useOnMount(() => Effect.gen(function*() { yield* Component.useOnMount(() => Effect.gen(function*() {
yield* Effect.addFinalizer(() => Console.log("RegisterFormView unmounted")) yield* Effect.addFinalizer(() => Console.log("RegisterFormView unmounted"))
@@ -85,15 +85,15 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {
void runPromise(form.submit) void runPromise(form.submit)
}}> }}>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<TextFieldFormInput <TextFieldFormInputFC
field={yield* form.field(["email"])} field={yield* form.field(["email"])}
/> />
<TextFieldFormInput <TextFieldFormInputFC
field={yield* form.field(["password"])} field={yield* form.field(["password"])}
/> />
<TextFieldFormInput <TextFieldFormInputFC
optional optional
type="datetime-local" type="datetime-local"
field={yield* form.field(["birth"])} field={yield* form.field(["birth"])}
@@ -115,13 +115,13 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {
) )
}) {} }) {}
const RegisterPage = Component.make("RegisterPageView")(function*() { const RegisterPage = Component.makeUntraced("RegisterPage")(function*() {
const RegisterForm = yield* Effect.provide( const RegisterFormViewFC = yield* Effect.provide(
RegisterFormView.use, RegisterFormView.use,
yield* Component.useContextFromLayer(RegisterFormService.Default), yield* Component.useContextFromLayer(RegisterForm.Default),
) )
return <RegisterForm /> return <RegisterFormViewFC />
}).pipe( }).pipe(
Component.withRuntime(runtime.context) Component.withRuntime(runtime.context)
) )

View File

@@ -2,19 +2,19 @@ import { createFileRoute } from "@tanstack/react-router"
import { Effect } from "effect" import { Effect } from "effect"
import { Component } from "effect-fc" import { Component } from "effect-fc"
import { runtime } from "@/runtime" import { runtime } from "@/runtime"
import { TodosState } from "@/todo/TodosState" import { Todos } from "@/todo/Todos"
import { TodosView } from "@/todo/TodosView" import { TodosState } from "@/todo/TodosState.service"
const TodosStateLive = TodosState.Default("todos") const TodosStateLive = TodosState.Default("todos")
const Index = Component.make("IndexView")(function*() { const Index = Component.makeUntraced("Index")(function*() {
const Todos = yield* Effect.provide( const TodosFC = yield* Effect.provide(
TodosView.use, Todos.use,
yield* Component.useContextFromLayer(TodosStateLive), yield* Component.useContextFromLayer(TodosStateLive),
) )
return <Todos /> return <TodosFC />
}).pipe( }).pipe(
Component.withRuntime(runtime.context) Component.withRuntime(runtime.context)
) )

View File

@@ -13,16 +13,15 @@ const Post = Schema.Struct({
body: Schema.String, body: Schema.String,
}) })
const ResultView = Component.make("ResultView")(function*() { const ResultView = Component.makeUntraced("Result")(function*() {
const runPromise = yield* Component.useRunPromise() const runPromise = yield* Component.useRunPromise()
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() { const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
const idRef = yield* SubscriptionRef.make(1) const idRef = yield* SubscriptionRef.make(1)
const key = Stream.map(idRef.changes, id => [id] as const)
const query = yield* Query.service({ const query = yield* Query.service({
key, key: Stream.zipLatest(Stream.make("posts" as const), idRef.changes),
f: ([id]) => HttpClient.HttpClient.pipe( f: ([, id]) => HttpClient.HttpClient.pipe(
Effect.tap(Effect.sleep("500 millis")), Effect.tap(Effect.sleep("500 millis")),
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)), Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
Effect.andThen(response => response.json), Effect.andThen(response => response.json),

View File

@@ -5,9 +5,9 @@ import { Component, Form, Subscribable } from "effect-fc"
import { FaArrowDown, FaArrowUp } from "react-icons/fa" import { FaArrowDown, FaArrowUp } from "react-icons/fa"
import { FaDeleteLeft } from "react-icons/fa6" import { FaDeleteLeft } from "react-icons/fa6"
import * as Domain from "@/domain" import * as Domain from "@/domain"
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView" import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
import { DateTimeUtcFromZonedInput } from "@/lib/schema" import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { TodosState } from "./TodosState" import { TodosState } from "./TodosState.service"
const TodoFormSchema = Schema.compose(Schema.Struct({ const TodoFormSchema = Schema.compose(Schema.Struct({
@@ -30,7 +30,7 @@ export type TodoProps = (
| { readonly _tag: "edit", readonly id: string } | { readonly _tag: "edit", readonly id: string }
) )
export class TodoView extends Component.make("TodoView")(function*(props: TodoProps) { export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoProps) {
const state = yield* TodosState const state = yield* TodosState
const [ const [
@@ -83,17 +83,17 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
const runSync = yield* Component.useRunSync() const runSync = yield* Component.useRunSync()
const runPromise = yield* Component.useRunPromise<DateTime.CurrentTimeZone>() const runPromise = yield* Component.useRunPromise<DateTime.CurrentTimeZone>()
const TextFieldFormInput = yield* TextFieldFormInputView.use const TextFieldFormInputFC = yield* TextFieldFormInput.use
return ( return (
<Flex direction="row" align="center" gap="2"> <Flex direction="row" align="center" gap="2">
<Box flexGrow="1"> <Box flexGrow="1">
<Flex direction="column" align="stretch" gap="2"> <Flex direction="column" align="stretch" gap="2">
<TextFieldFormInput field={contentField} /> <TextFieldFormInputFC field={contentField} />
<Flex direction="row" justify="center" align="center" gap="2"> <Flex direction="row" justify="center" align="center" gap="2">
<TextFieldFormInput <TextFieldFormInputFC
optional optional
field={completedAtField} field={completedAtField}
type="datetime-local" type="datetime-local"

View File

@@ -1,11 +1,11 @@
import { Container, Flex, Heading } from "@radix-ui/themes" import { Container, Flex, Heading } from "@radix-ui/themes"
import { Chunk, Console, Effect } from "effect" import { Chunk, Console, Effect } from "effect"
import { Component, Subscribable } from "effect-fc" import { Component, Subscribable } from "effect-fc"
import { TodosState } from "./TodosState" import { Todo } from "./Todo"
import { TodoView } from "./TodoView" import { TodosState } from "./TodosState.service"
export class TodosView extends Component.make("TodosView")(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])
@@ -14,17 +14,17 @@ export class TodosView extends Component.make("TodosView")(function*() {
Effect.addFinalizer(() => Console.log("Todos unmounted")), Effect.addFinalizer(() => Console.log("Todos unmounted")),
)) ))
const Todo = yield* TodoView.use const TodoFC = yield* Todo.use
return ( return (
<Container> <Container>
<Heading align="center">Todos</Heading> <Heading align="center">Todos</Heading>
<Flex direction="column" align="stretch" gap="2" mt="2"> <Flex direction="column" align="stretch" gap="2" mt="2">
<Todo _tag="new" /> <TodoFC _tag="new" />
{Chunk.map(todos, todo => {Chunk.map(todos, todo =>
<Todo key={todo.id} _tag="edit" id={todo.id} /> <TodoFC key={todo.id} _tag="edit" id={todo.id} />
)} )}
</Flex> </Flex>
</Container> </Container>