0.1.3 (#4)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: https://gitea:3000/Thilawyn/effect-fc/pulls/4
This commit was merged in pull request #4.
This commit is contained in:
@@ -11,25 +11,26 @@ Documentation is currently being written. In the meantime, you can take a look a
|
||||
- `react` & `@types/react` 19+
|
||||
|
||||
## Known issues
|
||||
- React Refresh replacement doesn't work for Effect FC's yet. Page reload is required to view changes. Regular React components are unaffected.
|
||||
- React Refresh doesn't work for Effect FC's yet. Page reload is required to view changes. Regular React components are unaffected.
|
||||
|
||||
## What writing components looks like
|
||||
```typescript
|
||||
import { Component, Hook, Memoized } from "effect-fc"
|
||||
import { Component } from "effect-fc"
|
||||
import { useOnce, useSubscribe } from "effect-fc/hooks"
|
||||
import { Todo } from "./Todo"
|
||||
import { TodosState } from "./TodosState.service"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
class Todos extends Component.make(function* Todos() {
|
||||
|
||||
export class Todos extends Component.makeUntraced(function* Todos() {
|
||||
const state = yield* TodosState
|
||||
const [todos] = yield* Hook.useSubscribeRefs(state.ref)
|
||||
const [todos] = yield* useSubscribe(state.ref)
|
||||
|
||||
yield* Hook.useOnce(() => Effect.andThen(
|
||||
yield* useOnce(() => Effect.andThen(
|
||||
Console.log("Todos mounted"),
|
||||
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||
))
|
||||
|
||||
const TodoFC = yield* Component.useFC(Todo)
|
||||
const TodoFC = yield* Todo
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -38,22 +39,26 @@ class Todos extends Component.make(function* Todos() {
|
||||
<Flex direction="column" align="stretch" gap="2" mt="2">
|
||||
<TodoFC _tag="new" />
|
||||
|
||||
{Chunk.map(todos, (v, k) =>
|
||||
<TodoFC key={v.id} _tag="edit" index={k} />
|
||||
{Chunk.map(todos, todo =>
|
||||
<TodoFC key={todo.id} _tag="edit" id={todo.id} />
|
||||
)}
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
}).pipe(
|
||||
Memoized.memo
|
||||
) {}
|
||||
}) {}
|
||||
|
||||
const TodosEntrypoint = Component.make(function* TodosEntrypoint() {
|
||||
const context = yield* Hook.useContext(TodosState.Default, { finalizerExecutionMode: "fork" })
|
||||
const TodosFC = yield* Effect.provide(Component.useFC(Todos), context)
|
||||
const TodosStateLive = TodosState.Default("todos")
|
||||
|
||||
const Index = Component.makeUntraced(function* Index() {
|
||||
const context = yield* useContext(TodosStateLive, { finalizerExecutionMode: "fork" })
|
||||
const TodosFC = yield* Effect.provide(Todos, context)
|
||||
|
||||
return <TodosFC />
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: Index
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "effect-fc",
|
||||
"description": "Write React function components with Effect",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
@@ -17,6 +17,10 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./hooks": {
|
||||
"types": "./dist/hooks/index.d.ts",
|
||||
"default": "./dist/hooks/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/types/index.js"
|
||||
@@ -40,6 +44,6 @@
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/language-service": "^0.23.3"
|
||||
"@effect/language-service": "^0.35.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect"
|
||||
import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, String, Tracer, type Types, type Utils } from "effect"
|
||||
import * as React from "react"
|
||||
import * as Hook from "./Hook.js"
|
||||
import * as Memoized from "./Memoized.js"
|
||||
import { Hooks } from "./hooks/index.js"
|
||||
import * as Memo from "./Memo.js"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("effect-fc/Component")
|
||||
export type TypeId = typeof TypeId
|
||||
|
||||
export interface Component<P extends {} = {}, E = never, R = never>
|
||||
extends Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>, Component.Options {
|
||||
export interface Component<P extends {}, A extends React.ReactNode, E, R>
|
||||
extends
|
||||
Effect.Effect<(props: P) => A, never, Exclude<R, Scope.Scope>>,
|
||||
Component.Options
|
||||
{
|
||||
new(_: never): {}
|
||||
readonly [TypeId]: TypeId
|
||||
readonly ["~Props"]: P
|
||||
readonly ["~Success"]: A
|
||||
readonly ["~Error"]: E
|
||||
readonly ["~Context"]: R
|
||||
|
||||
/** @internal */
|
||||
makeFunctionComponent(runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>, scope: Scope.Scope): React.FC<P>
|
||||
readonly body: (props: P) => Effect.Effect<A, E, R>
|
||||
|
||||
/** @internal */
|
||||
readonly body: (props: P) => Effect.Effect<React.ReactNode, E, R>
|
||||
makeFunctionComponent(
|
||||
runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||
scope: Scope.Scope,
|
||||
): (props: P) => A
|
||||
}
|
||||
|
||||
export namespace Component {
|
||||
export type Props<T> = T extends Component<infer P, infer _E, infer _R> ? P : never
|
||||
export type Error<T> = T extends Component<infer _P, infer E, infer _R> ? E : never
|
||||
export type Context<T> = T extends Component<infer _P, infer _E, infer R> ? R : never
|
||||
export type Props<T extends Component<any, any, any, any>> = [T] extends [Component<infer P, infer _A, infer _E, infer _R>] ? P : never
|
||||
export type Success<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer A, infer _E, infer _R>] ? A : never
|
||||
export type Error<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer _A, infer E, infer _R>] ? E : never
|
||||
export type Context<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer _A, infer _E, infer R>] ? R : never
|
||||
|
||||
export type AsComponent<T extends Component<any, any, any, any>> = Component<Props<T>, Success<T>, Error<T>, Context<T>>
|
||||
|
||||
export interface Options {
|
||||
readonly displayName?: string
|
||||
@@ -34,13 +49,15 @@ const ComponentProto = Object.freeze({
|
||||
...Effectable.CommitPrototype,
|
||||
[TypeId]: TypeId,
|
||||
|
||||
commit: Effect.fn("Component")(function* <P extends {}, E, R>(this: Component<P, E, R>) {
|
||||
commit: Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode, E, R>(
|
||||
this: Component<P, A, E, R>
|
||||
) {
|
||||
const self = this
|
||||
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
|
||||
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
return React.useCallback(function ScopeProvider(props: P) {
|
||||
const scope = Runtime.runSync(runtimeRef.current)(Hook.useScope(
|
||||
return React.useRef(function ScopeProvider(props: P) {
|
||||
const scope = Runtime.runSync(runtimeRef.current)(Hooks.useScope(
|
||||
Array.from(
|
||||
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
||||
),
|
||||
@@ -48,22 +65,22 @@ const ComponentProto = Object.freeze({
|
||||
))
|
||||
|
||||
const FC = React.useMemo(() => {
|
||||
const f = self.makeFunctionComponent(runtimeRef, scope)
|
||||
const f: React.FC<P> = self.makeFunctionComponent(runtimeRef, scope)
|
||||
f.displayName = self.displayName ?? "Anonymous"
|
||||
return Memoized.isMemoized(self)
|
||||
return Memo.isMemo(self)
|
||||
? React.memo(f, self.propsAreEqual)
|
||||
: f
|
||||
}, [scope])
|
||||
|
||||
return React.createElement(FC, props)
|
||||
}, [])
|
||||
}).current
|
||||
}),
|
||||
|
||||
makeFunctionComponent <P extends {}, E, R>(
|
||||
this: Component<P, E, R>,
|
||||
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
||||
this: Component<P, A, E, R>,
|
||||
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||
scope: Scope.Scope,
|
||||
): React.FC<P> {
|
||||
) {
|
||||
return (props: P) => Runtime.runSync(runtimeRef.current)(
|
||||
Effect.provideService(this.body(props), Scope.Scope, scope)
|
||||
)
|
||||
@@ -78,14 +95,14 @@ const defaultOptions = {
|
||||
const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||
|
||||
|
||||
export const isComponent = (u: unknown): u is Component<{}, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||
|
||||
export namespace make {
|
||||
export type Gen = {
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, React.ReactNode, never>,
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A extends React.ReactNode, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, A, never>
|
||||
): Component<
|
||||
P,
|
||||
P, A,
|
||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||
>
|
||||
@@ -98,8 +115,8 @@ export namespace make {
|
||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||
>,
|
||||
props: NoInfer<P>,
|
||||
) => B
|
||||
): Component<P, Effect.Effect.Error<B>, Effect.Effect.Context<B>>
|
||||
) => B,
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<B>>, Effect.Effect.Error<B>, Effect.Effect.Context<B>>
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, A, never>,
|
||||
a: (
|
||||
@@ -111,7 +128,7 @@ export namespace make {
|
||||
props: NoInfer<P>,
|
||||
) => B,
|
||||
b: (_: B, props: NoInfer<P>) => C,
|
||||
): Component<P, Effect.Effect.Error<C>, Effect.Effect.Context<C>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<C>>, Effect.Effect.Error<C>, Effect.Effect.Context<C>>
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, A, never>,
|
||||
a: (
|
||||
@@ -124,7 +141,7 @@ export namespace make {
|
||||
) => B,
|
||||
b: (_: B, props: NoInfer<P>) => C,
|
||||
c: (_: C, props: NoInfer<P>) => D,
|
||||
): Component<P, Effect.Effect.Error<D>, Effect.Effect.Context<D>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<D>>, Effect.Effect.Error<D>, Effect.Effect.Context<D>>
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, A, never>,
|
||||
a: (
|
||||
@@ -138,7 +155,7 @@ export namespace make {
|
||||
b: (_: B, props: NoInfer<P>) => C,
|
||||
c: (_: C, props: NoInfer<P>) => D,
|
||||
d: (_: D, props: NoInfer<P>) => E,
|
||||
): Component<P, Effect.Effect.Error<E>, Effect.Effect.Context<E>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<E>>, Effect.Effect.Error<E>, Effect.Effect.Context<E>>
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, A, never>,
|
||||
a: (
|
||||
@@ -153,7 +170,7 @@ export namespace make {
|
||||
c: (_: C, props: NoInfer<P>) => D,
|
||||
d: (_: D, props: NoInfer<P>) => E,
|
||||
e: (_: E, props: NoInfer<P>) => F,
|
||||
): Component<P, Effect.Effect.Error<F>, Effect.Effect.Context<F>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<F>>, Effect.Effect.Error<F>, Effect.Effect.Context<F>>
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, A, never>,
|
||||
a: (
|
||||
@@ -169,7 +186,7 @@ export namespace make {
|
||||
d: (_: D, props: NoInfer<P>) => E,
|
||||
e: (_: E, props: NoInfer<P>) => F,
|
||||
f: (_: F, props: NoInfer<P>) => G,
|
||||
): Component<P, Effect.Effect.Error<G>, Effect.Effect.Context<G>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<G>>, Effect.Effect.Error<G>, Effect.Effect.Context<G>>
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, A, never>,
|
||||
a: (
|
||||
@@ -186,7 +203,7 @@ export namespace make {
|
||||
e: (_: E, props: NoInfer<P>) => F,
|
||||
f: (_: F, props: NoInfer<P>) => G,
|
||||
g: (_: G, props: NoInfer<P>) => H,
|
||||
): Component<P, Effect.Effect.Error<H>, Effect.Effect.Context<H>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<H>>, Effect.Effect.Error<H>, Effect.Effect.Context<H>>
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, A, never>,
|
||||
a: (
|
||||
@@ -204,7 +221,7 @@ export namespace make {
|
||||
f: (_: F, props: NoInfer<P>) => G,
|
||||
g: (_: G, props: NoInfer<P>) => H,
|
||||
h: (_: H, props: NoInfer<P>) => I,
|
||||
): Component<P, Effect.Effect.Error<I>, Effect.Effect.Context<I>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<I>>, Effect.Effect.Error<I>, Effect.Effect.Context<I>>
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I, J extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, A, never>,
|
||||
a: (
|
||||
@@ -223,35 +240,35 @@ export namespace make {
|
||||
g: (_: G, props: NoInfer<P>) => H,
|
||||
h: (_: H, props: NoInfer<P>) => I,
|
||||
i: (_: I, props: NoInfer<P>) => J,
|
||||
): Component<P, Effect.Effect.Error<J>, Effect.Effect.Context<J>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<J>>, Effect.Effect.Error<J>, Effect.Effect.Context<J>>
|
||||
}
|
||||
|
||||
export type NonGen = {
|
||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||
body: (props: P) => Eff
|
||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, P extends {} = {}>(
|
||||
body: (props: P) => A,
|
||||
a: (_: A, props: NoInfer<P>) => Eff,
|
||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, P extends {} = {}>(
|
||||
body: (props: P) => A,
|
||||
a: (_: A, props: NoInfer<P>) => B,
|
||||
b: (_: B, props: NoInfer<P>) => Eff,
|
||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, P extends {} = {}>(
|
||||
body: (props: P) => A,
|
||||
a: (_: A, props: NoInfer<P>) => B,
|
||||
b: (_: B, props: NoInfer<P>) => C,
|
||||
c: (_: C, props: NoInfer<P>) => Eff,
|
||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, P extends {} = {}>(
|
||||
body: (props: P) => A,
|
||||
a: (_: A, props: NoInfer<P>) => B,
|
||||
b: (_: B, props: NoInfer<P>) => C,
|
||||
c: (_: C, props: NoInfer<P>) => D,
|
||||
d: (_: D, props: NoInfer<P>) => Eff,
|
||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, P extends {} = {}>(
|
||||
body: (props: P) => A,
|
||||
a: (_: A, props: NoInfer<P>) => B,
|
||||
@@ -259,7 +276,7 @@ export namespace make {
|
||||
c: (_: C, props: NoInfer<P>) => D,
|
||||
d: (_: D, props: NoInfer<P>) => E,
|
||||
e: (_: E, props: NoInfer<P>) => Eff,
|
||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, P extends {} = {}>(
|
||||
body: (props: P) => A,
|
||||
a: (_: A, props: NoInfer<P>) => B,
|
||||
@@ -268,7 +285,7 @@ export namespace make {
|
||||
d: (_: D, props: NoInfer<P>) => E,
|
||||
e: (_: E, props: NoInfer<P>) => F,
|
||||
f: (_: F, props: NoInfer<P>) => Eff,
|
||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, P extends {} = {}>(
|
||||
body: (props: P) => A,
|
||||
a: (_: A, props: NoInfer<P>) => B,
|
||||
@@ -278,7 +295,7 @@ export namespace make {
|
||||
e: (_: E, props: NoInfer<P>) => F,
|
||||
f: (_: F, props: NoInfer<P>) => G,
|
||||
g: (_: G, props: NoInfer<P>) => Eff,
|
||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, P extends {} = {}>(
|
||||
body: (props: P) => A,
|
||||
a: (_: A, props: NoInfer<P>) => B,
|
||||
@@ -289,7 +306,7 @@ export namespace make {
|
||||
f: (_: F, props: NoInfer<P>) => G,
|
||||
g: (_: G, props: NoInfer<P>) => H,
|
||||
h: (_: H, props: NoInfer<P>) => Eff,
|
||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, I, P extends {} = {}>(
|
||||
body: (props: P) => A,
|
||||
a: (_: A, props: NoInfer<P>) => B,
|
||||
@@ -301,7 +318,7 @@ export namespace make {
|
||||
g: (_: G, props: NoInfer<P>) => H,
|
||||
h: (_: H, props: NoInfer<P>) => I,
|
||||
i: (_: I, props: NoInfer<P>) => Eff,
|
||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,14 +368,14 @@ export const makeUntraced: make.Gen & make.NonGen = (
|
||||
const displayNameFromBody = (body: Function) => !String.isEmpty(body.name) ? body.name : undefined
|
||||
|
||||
export const withOptions: {
|
||||
<T extends Component<any, any, any>>(
|
||||
<T extends Component<any, any, any, any>>(
|
||||
options: Partial<Component.Options>
|
||||
): (self: T) => T
|
||||
<T extends Component<any, any, any>>(
|
||||
<T extends Component<any, any, any, any>>(
|
||||
self: T,
|
||||
options: Partial<Component.Options>,
|
||||
): T
|
||||
} = Function.dual(2, <T extends Component<any, any, any>>(
|
||||
} = Function.dual(2, <T extends Component<any, any, any, any>>(
|
||||
self: T,
|
||||
options: Partial<Component.Options>,
|
||||
): T => Object.setPrototypeOf(
|
||||
@@ -367,17 +384,17 @@ export const withOptions: {
|
||||
))
|
||||
|
||||
export const withRuntime: {
|
||||
<P extends {}, E, R>(
|
||||
<P extends {}, A extends React.ReactNode, E, R>(
|
||||
context: React.Context<Runtime.Runtime<R>>,
|
||||
): (self: Component<P, E, R>) => React.FC<P>
|
||||
<P extends {}, E, R>(
|
||||
self: Component<P, E, R>,
|
||||
): (self: Component<P, A, E, Types.NoInfer<R>>) => (props: P) => A
|
||||
<P extends {}, A extends React.ReactNode, E, R>(
|
||||
self: Component<P, A, E, Types.NoInfer<R>>,
|
||||
context: React.Context<Runtime.Runtime<R>>,
|
||||
): React.FC<P>
|
||||
} = Function.dual(2, <P extends {}, E, R>(
|
||||
self: Component<P, E, R>,
|
||||
): (props: P) => A
|
||||
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R>(
|
||||
self: Component<P, A, E, R>,
|
||||
context: React.Context<Runtime.Runtime<R>>,
|
||||
): React.FC<P> => function WithRuntime(props) {
|
||||
) => function WithRuntime(props: P) {
|
||||
return React.createElement(
|
||||
Runtime.runSync(React.useContext(context))(self),
|
||||
props,
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
import { type Context, Effect, Equivalence, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||
import * as React from "react"
|
||||
import { SetStateAction } from "./types/index.js"
|
||||
|
||||
|
||||
export interface ScopeOptions {
|
||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
}
|
||||
|
||||
|
||||
export const useScope: {
|
||||
(
|
||||
deps: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<Scope.Scope>
|
||||
} = Effect.fn("useScope")(function*(deps, options) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
|
||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([
|
||||
Ref.make(true),
|
||||
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
||||
])), [])
|
||||
const [scope, setScope] = React.useState(initialScope)
|
||||
|
||||
React.useEffect(() => Runtime.runSync(runtime)(
|
||||
Effect.if(isInitialRun, {
|
||||
onTrue: () => Effect.as(
|
||||
Ref.set(isInitialRun, false),
|
||||
() => closeScope(scope, runtime, options),
|
||||
),
|
||||
|
||||
onFalse: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe(
|
||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
||||
),
|
||||
})
|
||||
), deps)
|
||||
|
||||
return scope
|
||||
})
|
||||
|
||||
const closeScope = (
|
||||
scope: Scope.CloseableScope,
|
||||
runtime: Runtime.Runtime<never>,
|
||||
options?: ScopeOptions,
|
||||
) => {
|
||||
switch (options?.finalizerExecutionMode ?? "sync") {
|
||||
case "sync":
|
||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
||||
break
|
||||
case "fork":
|
||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const useCallbackSync: {
|
||||
<Args extends unknown[], A, E, R>(
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<(...args: Args) => A, never, R>
|
||||
} = Effect.fn("useCallbackSync")(function* <Args extends unknown[], A, E, R>(
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime<R>()
|
||||
return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps)
|
||||
})
|
||||
|
||||
export const useCallbackPromise: {
|
||||
<Args extends unknown[], A, E, R>(
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
||||
} = Effect.fn("useCallbackPromise")(function* <Args extends unknown[], A, E, R>(
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime<R>()
|
||||
return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps)
|
||||
})
|
||||
|
||||
|
||||
export const useMemo: {
|
||||
<A, E, R>(
|
||||
factory: () => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<A, E, R>
|
||||
} = Effect.fn("useMemo")(function* <A, E, R>(
|
||||
factory: () => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
|
||||
})
|
||||
|
||||
export const useOnce: {
|
||||
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||
} = Effect.fn("useOnce")(function* <A, E, R>(
|
||||
factory: () => Effect.Effect<A, E, R>
|
||||
) {
|
||||
return yield* useMemo(factory, [])
|
||||
})
|
||||
|
||||
|
||||
export const useEffect: {
|
||||
<E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fn("useEffect")(function* <E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
React.useEffect(() => Effect.Do.pipe(
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||
Effect.map(({ scope }) =>
|
||||
() => closeScope(scope, runtime, options)
|
||||
),
|
||||
Runtime.runSync(runtime),
|
||||
), deps)
|
||||
})
|
||||
|
||||
export const useLayoutEffect: {
|
||||
<E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fn("useLayoutEffect")(function* <E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
React.useLayoutEffect(() => Effect.Do.pipe(
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||
Effect.map(({ scope }) =>
|
||||
() => closeScope(scope, runtime, options)
|
||||
),
|
||||
Runtime.runSync(runtime),
|
||||
), deps)
|
||||
})
|
||||
|
||||
export const useFork: {
|
||||
<E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fn("useFork")(function* <E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
React.useEffect(() => {
|
||||
const scope = Runtime.runSync(runtime)(options?.scope
|
||||
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
)
|
||||
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
||||
return () => closeScope(scope, runtime, {
|
||||
...options,
|
||||
finalizerExecutionMode: options?.finalizerExecutionMode ?? "fork",
|
||||
})
|
||||
}, deps)
|
||||
})
|
||||
|
||||
|
||||
export const useContext: {
|
||||
<ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>>
|
||||
} = Effect.fn("useContext")(function* <ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>,
|
||||
options?: ScopeOptions,
|
||||
) {
|
||||
const scope = yield* useScope([layer], options)
|
||||
|
||||
return yield* useMemo(() => Effect.provideService(
|
||||
Effect.provide(Effect.context<ROut>(), layer),
|
||||
Scope.Scope,
|
||||
scope,
|
||||
), [scope])
|
||||
})
|
||||
|
||||
|
||||
export const useRefFromReactiveValue: {
|
||||
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
||||
} = Effect.fn("useRefFromReactiveValue")(function*(value) {
|
||||
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
||||
yield* useEffect(() => Ref.set(ref, value), [value])
|
||||
return ref
|
||||
})
|
||||
|
||||
export const useSubscribeRefs: {
|
||||
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||
...refs: Refs
|
||||
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
|
||||
} = Effect.fn("useSubscribeRefs")(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||
...refs: Refs
|
||||
) {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
||||
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
|
||||
))
|
||||
|
||||
yield* useFork(() => pipe(
|
||||
refs.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())),
|
||||
streams => Stream.zipLatestAll(...streams),
|
||||
Stream.runForEach(v =>
|
||||
Effect.sync(() => setReactStateValue(v))
|
||||
),
|
||||
), refs)
|
||||
|
||||
return reactStateValue as any
|
||||
})
|
||||
|
||||
export const useRefState: {
|
||||
<A>(
|
||||
ref: SubscriptionRef.SubscriptionRef<A>
|
||||
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
|
||||
} = Effect.fn("useRefState")(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
||||
|
||||
yield* useFork(() => Stream.runForEach(
|
||||
Stream.changesWith(ref.changes, Equivalence.strict()),
|
||||
v => Effect.sync(() => setReactStateValue(v)),
|
||||
), [ref])
|
||||
|
||||
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||
Ref.update(ref, prevState =>
|
||||
SetStateAction.value(setStateAction, prevState)
|
||||
),
|
||||
[ref])
|
||||
|
||||
return [reactStateValue, setValue]
|
||||
})
|
||||
|
||||
|
||||
export const useStreamFromReactiveValues: {
|
||||
<const A extends React.DependencyList>(
|
||||
values: A
|
||||
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
||||
} = Effect.fn("useStreamFromReactiveValues")(function* <const A extends React.DependencyList>(values: A) {
|
||||
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
|
||||
Effect.bind("latest", () => Ref.make(values)),
|
||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
||||
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
|
||||
Effect.flatMap(a => Effect.map(
|
||||
Stream.fromPubSub(pubsub, { scoped: true }),
|
||||
s => Stream.concat(Stream.make(a), s),
|
||||
)),
|
||||
Stream.unwrapScoped,
|
||||
)),
|
||||
))
|
||||
|
||||
yield* useEffect(() => Ref.set(latest, values).pipe(
|
||||
Effect.andThen(PubSub.publish(pubsub, values)),
|
||||
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
||||
), values)
|
||||
|
||||
return stream
|
||||
})
|
||||
|
||||
export const useSubscribeStream: {
|
||||
<A, E, R>(
|
||||
stream: Stream.Stream<A, E, R>
|
||||
): Effect.Effect<Option.Option<A>, never, R>
|
||||
<A extends NonNullable<unknown>, E, R>(
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue: A,
|
||||
): Effect.Effect<Option.Some<A>, never, R>
|
||||
} = Effect.fn("useSubscribeStream")(function* <A extends NonNullable<unknown>, E, R>(
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue?: A,
|
||||
) {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(
|
||||
React.useMemo(() => initialValue
|
||||
? Option.some(initialValue)
|
||||
: Option.none(),
|
||||
[])
|
||||
)
|
||||
|
||||
yield* useFork(() => Stream.runForEach(
|
||||
Stream.changesWith(stream, Equivalence.strict()),
|
||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||
), [stream])
|
||||
|
||||
return reactStateValue as Option.Some<A>
|
||||
})
|
||||
50
packages/effect-fc/src/Memo.ts
Normal file
50
packages/effect-fc/src/Memo.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { type Equivalence, Function, Predicate } from "effect"
|
||||
import type * as Component from "./Component.js"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("effect-fc/Memo")
|
||||
export type TypeId = typeof TypeId
|
||||
|
||||
export interface Memo<P> extends Memo.Options<P> {
|
||||
readonly [TypeId]: TypeId
|
||||
}
|
||||
|
||||
export namespace Memo {
|
||||
export interface Options<P> {
|
||||
readonly propsAreEqual?: Equivalence.Equivalence<P>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const MemoProto = Object.freeze({
|
||||
[TypeId]: TypeId
|
||||
} as const)
|
||||
|
||||
|
||||
export const isMemo = (u: unknown): u is Memo<unknown> => Predicate.hasProperty(u, TypeId)
|
||||
|
||||
export const memo = <T extends Component.Component<any, any, any, any>>(
|
||||
self: T
|
||||
): T & Memo<Component.Component.Props<T>> => Object.setPrototypeOf(
|
||||
Object.assign(function() {}, self),
|
||||
Object.freeze(Object.setPrototypeOf(
|
||||
Object.assign({}, MemoProto),
|
||||
Object.getPrototypeOf(self),
|
||||
)),
|
||||
)
|
||||
|
||||
export const withOptions: {
|
||||
<T extends Component.Component<any, any, any, any> & Memo<any>>(
|
||||
options: Partial<Memo.Options<Component.Component.Props<T>>>
|
||||
): (self: T) => T
|
||||
<T extends Component.Component<any, any, any, any> & Memo<any>>(
|
||||
self: T,
|
||||
options: Partial<Memo.Options<Component.Component.Props<T>>>,
|
||||
): T
|
||||
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Memo<any>>(
|
||||
self: T,
|
||||
options: Partial<Memo.Options<Component.Component.Props<T>>>,
|
||||
): T => Object.setPrototypeOf(
|
||||
Object.assign(function() {}, self, options),
|
||||
Object.getPrototypeOf(self),
|
||||
))
|
||||
@@ -1,47 +0,0 @@
|
||||
import { type Equivalence, Function, Predicate } from "effect"
|
||||
import type * as Component from "./Component.js"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("effect-fc/Memoized")
|
||||
export type TypeId = typeof TypeId
|
||||
|
||||
export interface Memoized<P> extends Memoized.Options<P> {
|
||||
readonly [TypeId]: TypeId
|
||||
}
|
||||
|
||||
export namespace Memoized {
|
||||
export interface Options<P> {
|
||||
readonly propsAreEqual?: Equivalence.Equivalence<P>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const MemoizedProto = Object.freeze({
|
||||
[TypeId]: TypeId
|
||||
} as const)
|
||||
|
||||
|
||||
export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasProperty(u, TypeId)
|
||||
|
||||
export const memo = <T extends Component.Component<any, any, any>>(
|
||||
self: T
|
||||
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
||||
Object.assign(function() {}, self, MemoizedProto),
|
||||
Object.getPrototypeOf(self),
|
||||
)
|
||||
|
||||
export const withOptions: {
|
||||
<T extends Component.Component<any, any, any> & Memoized<any>>(
|
||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>
|
||||
): (self: T) => T
|
||||
<T extends Component.Component<any, any, any> & Memoized<any>>(
|
||||
self: T,
|
||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
||||
): T
|
||||
} = Function.dual(2, <T extends Component.Component<any, any, any> & Memoized<any>>(
|
||||
self: T,
|
||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
||||
): T => Object.setPrototypeOf(
|
||||
Object.assign(function() {}, self, options),
|
||||
Object.getPrototypeOf(self),
|
||||
))
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Effect, type Layer, ManagedRuntime, type Runtime } from "effect"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
export interface ReactManagedRuntime<R, ER> {
|
||||
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
||||
readonly context: React.Context<Runtime.Runtime<R>>
|
||||
}
|
||||
|
||||
export const make = <R, ER>(
|
||||
layer: Layer.Layer<R, ER>,
|
||||
memoMap?: Layer.MemoMap,
|
||||
): ReactManagedRuntime<R, ER> => ({
|
||||
runtime: ManagedRuntime.make(layer, memoMap),
|
||||
context: React.createContext<Runtime.Runtime<R>>(null!),
|
||||
})
|
||||
|
||||
|
||||
export interface ProviderProps<R, ER> extends React.SuspenseProps {
|
||||
readonly runtime: ReactManagedRuntime<R, ER>
|
||||
readonly children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function Provider<R, ER>(
|
||||
{ runtime, children, ...suspenseProps }: ProviderProps<R, ER>
|
||||
): React.ReactNode {
|
||||
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
||||
|
||||
return React.createElement(
|
||||
React.Suspense,
|
||||
suspenseProps,
|
||||
React.createElement(ProviderInner<R, ER>, { runtime, promise, children }),
|
||||
)
|
||||
}
|
||||
|
||||
interface ProviderInnerProps<R, ER> {
|
||||
readonly runtime: ReactManagedRuntime<R, ER>
|
||||
readonly promise: Promise<Runtime.Runtime<R>>
|
||||
readonly children?: React.ReactNode
|
||||
}
|
||||
|
||||
function ProviderInner<R, ER>(
|
||||
{ runtime, promise, children }: ProviderInnerProps<R, ER>
|
||||
): React.ReactNode {
|
||||
const value = React.use(promise)
|
||||
return React.createElement(runtime.context, { value }, children)
|
||||
}
|
||||
65
packages/effect-fc/src/ReactRuntime.ts
Normal file
65
packages/effect-fc/src/ReactRuntime.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Effect, type Layer, ManagedRuntime, Predicate, type Runtime } from "effect"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("effect-fc/ReactRuntime")
|
||||
export type TypeId = typeof TypeId
|
||||
|
||||
export interface ReactRuntime<R, ER> {
|
||||
new(_: never): {}
|
||||
readonly [TypeId]: TypeId
|
||||
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
||||
readonly context: React.Context<Runtime.Runtime<R>>
|
||||
}
|
||||
|
||||
const ReactRuntimeProto = Object.freeze({ [TypeId]: TypeId } as const)
|
||||
|
||||
|
||||
export const isReactRuntime = (u: unknown): u is ReactRuntime<unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||
|
||||
export const make = <R, ER>(
|
||||
layer: Layer.Layer<R, ER>,
|
||||
memoMap?: Layer.MemoMap,
|
||||
): ReactRuntime<R, ER> => Object.setPrototypeOf(
|
||||
Object.assign(function() {}, {
|
||||
runtime: ManagedRuntime.make(layer, memoMap),
|
||||
context: React.createContext<Runtime.Runtime<R>>(null!),
|
||||
}),
|
||||
ReactRuntimeProto,
|
||||
)
|
||||
|
||||
|
||||
export namespace Provider {
|
||||
export interface Props<R, ER> extends React.SuspenseProps {
|
||||
readonly runtime: ReactRuntime<R, ER>
|
||||
readonly children?: React.ReactNode
|
||||
}
|
||||
}
|
||||
|
||||
export const Provider = <R, ER>(
|
||||
{ runtime, children, ...suspenseProps }: Provider.Props<R, ER>
|
||||
): React.ReactNode => {
|
||||
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
||||
|
||||
return React.createElement(
|
||||
React.Suspense,
|
||||
suspenseProps,
|
||||
React.createElement(ProviderInner<R, ER>, { runtime, promise, children }),
|
||||
)
|
||||
}
|
||||
|
||||
namespace ProviderInner {
|
||||
export interface Props<R, ER> {
|
||||
readonly runtime: ReactRuntime<R, ER>
|
||||
readonly promise: Promise<Runtime.Runtime<R>>
|
||||
readonly children?: React.ReactNode
|
||||
}
|
||||
}
|
||||
|
||||
const ProviderInner = <R, ER>(
|
||||
{ runtime, promise, children }: ProviderInner.Props<R, ER>
|
||||
): React.ReactNode => React.createElement(
|
||||
runtime.context,
|
||||
{ value: React.use(promise) },
|
||||
children,
|
||||
)
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Effect, Function, Predicate, Runtime, Scope } from "effect"
|
||||
import * as React from "react"
|
||||
import type * as Component from "./Component.js"
|
||||
import type { ExcludeKeys } from "./utils.js"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("effect-fc/Suspense")
|
||||
@@ -22,16 +21,17 @@ export namespace Suspense {
|
||||
|
||||
const SuspenseProto = Object.freeze({
|
||||
[TypeId]: TypeId,
|
||||
makeFunctionComponent(
|
||||
this: Component.Component<any, any, any> & Suspense,
|
||||
runtimeRef: React.RefObject<Runtime.Runtime<any>>,
|
||||
|
||||
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
||||
this: Component.Component<P, A, E, R> & Suspense,
|
||||
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||
scope: Scope.Scope,
|
||||
): React.FC<any> {
|
||||
) {
|
||||
const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
||||
|
||||
return ({ fallback, name, ...props }: Suspense.Props) => {
|
||||
const promise = Runtime.runPromise(runtimeRef.current)(
|
||||
Effect.provideService(this.body(props), Scope.Scope, scope)
|
||||
Effect.provideService(this.body(props as P), Scope.Scope, scope)
|
||||
)
|
||||
|
||||
return React.createElement(
|
||||
@@ -46,26 +46,34 @@ const SuspenseProto = Object.freeze({
|
||||
|
||||
export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u, TypeId)
|
||||
|
||||
export const suspense = <T extends Component.Component<P, any, any>, P extends {}>(
|
||||
self: T & Component.Component<ExcludeKeys<P, keyof Suspense.Props>, any, any>
|
||||
export const suspense = <T extends Component.Component<any, any, any, any>>(
|
||||
self: T
|
||||
): (
|
||||
& Omit<T, keyof Component.Component<P, Component.Component.Error<T>, Component.Component.Context<T>>>
|
||||
& Component.Component<P & Suspense.Props, Component.Component.Error<T>, Component.Component.Context<T>>
|
||||
& Omit<T, keyof Component.Component.AsComponent<T>>
|
||||
& Component.Component<
|
||||
Component.Component.Props<T> & Suspense.Props,
|
||||
Component.Component.Success<T>,
|
||||
Component.Component.Error<T>,
|
||||
Component.Component.Context<T>
|
||||
>
|
||||
& Suspense
|
||||
) => Object.setPrototypeOf(
|
||||
Object.assign(function() {}, self, SuspenseProto),
|
||||
Object.getPrototypeOf(self),
|
||||
Object.assign(function() {}, self),
|
||||
Object.freeze(Object.setPrototypeOf(
|
||||
Object.assign({}, SuspenseProto),
|
||||
Object.getPrototypeOf(self),
|
||||
)),
|
||||
)
|
||||
|
||||
export const withOptions: {
|
||||
<T extends Component.Component<any, any, any> & Suspense>(
|
||||
<T extends Component.Component<any, any, any, any> & Suspense>(
|
||||
options: Partial<Suspense.Options>
|
||||
): (self: T) => T
|
||||
<T extends Component.Component<any, any, any> & Suspense>(
|
||||
<T extends Component.Component<any, any, any, any> & Suspense>(
|
||||
self: T,
|
||||
options: Partial<Suspense.Options>,
|
||||
): T
|
||||
} = Function.dual(2, <T extends Component.Component<any, any, any> & Suspense>(
|
||||
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Suspense>(
|
||||
self: T,
|
||||
options: Partial<Suspense.Options>,
|
||||
): T => Object.setPrototypeOf(
|
||||
|
||||
7
packages/effect-fc/src/hooks/Hooks/ScopeOptions.ts
Normal file
7
packages/effect-fc/src/hooks/Hooks/ScopeOptions.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ExecutionStrategy } from "effect"
|
||||
|
||||
|
||||
export interface ScopeOptions {
|
||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
}
|
||||
16
packages/effect-fc/src/hooks/Hooks/index.ts
Normal file
16
packages/effect-fc/src/hooks/Hooks/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export * from "./input/index.js"
|
||||
export * from "./ScopeOptions.js"
|
||||
export * from "./useCallbackPromise.js"
|
||||
export * from "./useCallbackSync.js"
|
||||
export * from "./useContext.js"
|
||||
export * from "./useEffect.js"
|
||||
export * from "./useFork.js"
|
||||
export * from "./useLayoutEffect.js"
|
||||
export * from "./useMemo.js"
|
||||
export * from "./useOnce.js"
|
||||
export * from "./useRefFromState.js"
|
||||
export * from "./useRefState.js"
|
||||
export * from "./useScope.js"
|
||||
export * from "./useStreamFromReactiveValues.js"
|
||||
export * from "./useSubscribe.js"
|
||||
export * from "./useSubscribeStream.js"
|
||||
2
packages/effect-fc/src/hooks/Hooks/input/index.ts
Normal file
2
packages/effect-fc/src/hooks/Hooks/input/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./useInput.js"
|
||||
export * from "./useOptionalInput.js"
|
||||
67
packages/effect-fc/src/hooks/Hooks/input/useInput.ts
Normal file
67
packages/effect-fc/src/hooks/Hooks/input/useInput.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { type Duration, Effect, Equal, Equivalence, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
|
||||
import * as React from "react"
|
||||
import { useFork } from "../useFork.js"
|
||||
import { useOnce } from "../useOnce.js"
|
||||
import { useRefState } from "../useRefState.js"
|
||||
|
||||
|
||||
export namespace useInput {
|
||||
export interface Options<A, R> {
|
||||
readonly schema: Schema.Schema<A, string, R>
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
readonly ref: SubscriptionRef.SubscriptionRef<A>
|
||||
readonly debounce?: Duration.DurationInput
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
readonly value: string
|
||||
readonly setValue: React.Dispatch<React.SetStateAction<string>>
|
||||
readonly error: Option.Option<ParseResult.ParseError>
|
||||
}
|
||||
}
|
||||
|
||||
export const useInput: {
|
||||
<A, R>(options: useInput.Options<A, R>): Effect.Effect<useInput.Result, ParseResult.ParseError, R>
|
||||
} = Effect.fnUntraced(function* <A, R>(options: useInput.Options<A, R>) {
|
||||
const internalRef = yield* useOnce(() => options.ref.pipe(
|
||||
Effect.andThen(Schema.encode(options.schema)),
|
||||
Effect.andThen(SubscriptionRef.make),
|
||||
))
|
||||
const [error, setError] = React.useState(Option.none<ParseResult.ParseError>())
|
||||
|
||||
yield* useFork(() => Effect.all([
|
||||
// Sync the upstream state with the internal state
|
||||
// Only mutate the internal state if the upstream value is actually different. This avoids infinite re-render loops.
|
||||
Stream.runForEach(Stream.changesWith(options.ref.changes, Equivalence.strict()), upstreamValue =>
|
||||
Effect.whenEffect(
|
||||
Effect.andThen(
|
||||
Schema.encode(options.schema)(upstreamValue),
|
||||
encodedUpstreamValue => Ref.set(internalRef, encodedUpstreamValue),
|
||||
),
|
||||
internalRef.pipe(
|
||||
Effect.andThen(Schema.decode(options.schema)),
|
||||
Effect.andThen(decodedInternalValue => !(options.equivalence ?? Equal.equals)(upstreamValue, decodedInternalValue)),
|
||||
Effect.catchTag("ParseError", () => Effect.succeed(false)),
|
||||
),
|
||||
)
|
||||
),
|
||||
|
||||
// Sync all changes to the internal state with upstream
|
||||
Stream.runForEach(
|
||||
internalRef.changes.pipe(
|
||||
Stream.changesWith(Equivalence.strict()),
|
||||
options.debounce ? Stream.debounce(options.debounce) : identity,
|
||||
Stream.drop(1),
|
||||
),
|
||||
flow(
|
||||
Schema.decode(options.schema),
|
||||
Effect.andThen(v => Ref.set(options.ref, v)),
|
||||
Effect.andThen(() => setError(Option.none())),
|
||||
Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))),
|
||||
),
|
||||
),
|
||||
], { concurrency: "unbounded" }), [options.schema, options.equivalence, options.ref, options.debounce, internalRef])
|
||||
|
||||
const [value, setValue] = yield* useRefState(internalRef)
|
||||
return { value, setValue, error }
|
||||
})
|
||||
107
packages/effect-fc/src/hooks/Hooks/input/useOptionalInput.ts
Normal file
107
packages/effect-fc/src/hooks/Hooks/input/useOptionalInput.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { type Duration, Effect, Equal, Equivalence, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
|
||||
import * as React from "react"
|
||||
import { SetStateAction } from "../../../types/index.js"
|
||||
import { useCallbackSync } from "../useCallbackSync.js"
|
||||
import { useFork } from "../useFork.js"
|
||||
import { useOnce } from "../useOnce.js"
|
||||
import { useRefState } from "../useRefState.js"
|
||||
import { useSubscribe } from "../useSubscribe.js"
|
||||
|
||||
|
||||
export namespace useOptionalInput {
|
||||
export interface Options<A, R> {
|
||||
readonly schema: Schema.Schema<A, string, R>
|
||||
readonly defaultValue?: A
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
readonly ref: SubscriptionRef.SubscriptionRef<Option.Option<A>>
|
||||
readonly debounce?: Duration.DurationInput
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
readonly value: string
|
||||
readonly setValue: React.Dispatch<React.SetStateAction<string>>
|
||||
readonly enabled: boolean
|
||||
readonly setEnabled: React.Dispatch<React.SetStateAction<boolean>>
|
||||
readonly error: Option.Option<ParseResult.ParseError>
|
||||
}
|
||||
}
|
||||
|
||||
export const useOptionalInput: {
|
||||
<A, R>(options: useOptionalInput.Options<A, R>): Effect.Effect<useOptionalInput.Result, ParseResult.ParseError, R>
|
||||
} = Effect.fnUntraced(function* <A, R>(options: useOptionalInput.Options<A, R>) {
|
||||
const [internalRef, enabledRef] = yield* useOnce(() => Effect.andThen(options.ref, upstreamValue =>
|
||||
Effect.all([
|
||||
Effect.andThen(
|
||||
Option.match(upstreamValue, {
|
||||
onSome: Schema.encode(options.schema),
|
||||
onNone: () => options.defaultValue
|
||||
? Schema.encode(options.schema)(options.defaultValue)
|
||||
: Effect.succeed(""),
|
||||
}),
|
||||
SubscriptionRef.make,
|
||||
),
|
||||
|
||||
SubscriptionRef.make(Option.isSome(upstreamValue)),
|
||||
])
|
||||
))
|
||||
|
||||
const [error, setError] = React.useState(Option.none<ParseResult.ParseError>())
|
||||
|
||||
yield* useFork(() => Effect.all([
|
||||
// Sync the upstream state with the internal state
|
||||
// Only mutate the internal state if the upstream value is actually different. This avoids infinite re-render loops.
|
||||
Stream.runForEach(Stream.changesWith(options.ref.changes, Equivalence.strict()), Option.match({
|
||||
onSome: upstreamValue => Effect.andThen(
|
||||
Ref.set(enabledRef, true),
|
||||
|
||||
Effect.whenEffect(
|
||||
Effect.andThen(
|
||||
Schema.encode(options.schema)(upstreamValue),
|
||||
encodedUpstreamValue => Ref.set(internalRef, encodedUpstreamValue),
|
||||
),
|
||||
internalRef.pipe(
|
||||
Effect.andThen(Schema.decode(options.schema)),
|
||||
Effect.andThen(decodedInternalValue => !(options.equivalence ?? Equal.equals)(upstreamValue, decodedInternalValue)),
|
||||
Effect.catchTag("ParseError", () => Effect.succeed(false)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
onNone: () => Ref.set(enabledRef, false),
|
||||
})),
|
||||
|
||||
// Sync all changes to the internal state with upstream
|
||||
Stream.runForEach(
|
||||
internalRef.changes.pipe(
|
||||
Stream.changesWith(Equivalence.strict()),
|
||||
options.debounce ? Stream.debounce(options.debounce) : identity,
|
||||
Stream.drop(1),
|
||||
),
|
||||
flow(
|
||||
Schema.decode(options.schema),
|
||||
Effect.andThen(v => Ref.set(options.ref, Option.some(v))),
|
||||
Effect.andThen(() => setError(Option.none())),
|
||||
Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))),
|
||||
),
|
||||
),
|
||||
], { concurrency: "unbounded" }), [options.schema, options.equivalence, options.ref, options.debounce, internalRef])
|
||||
|
||||
const setEnabled = yield* useCallbackSync(
|
||||
(setStateAction: React.SetStateAction<boolean>) => Effect.andThen(
|
||||
Ref.updateAndGet(enabledRef, prevState => SetStateAction.value(setStateAction, prevState)),
|
||||
enabled => enabled
|
||||
? internalRef.pipe(
|
||||
Effect.andThen(Schema.decode(options.schema)),
|
||||
Effect.andThen(v => Ref.set(options.ref, Option.some(v))),
|
||||
Effect.andThen(() => setError(Option.none())),
|
||||
Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))),
|
||||
)
|
||||
: Ref.set(options.ref, Option.none()),
|
||||
),
|
||||
[options.schema, options.ref, internalRef, enabledRef],
|
||||
)
|
||||
|
||||
const [enabled] = yield* useSubscribe(enabledRef)
|
||||
const [value, setValue] = yield* useRefState(internalRef)
|
||||
return { value, setValue, enabled, setEnabled, error }
|
||||
})
|
||||
18
packages/effect-fc/src/hooks/Hooks/internal.ts
Normal file
18
packages/effect-fc/src/hooks/Hooks/internal.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Exit, Runtime, Scope } from "effect"
|
||||
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||
|
||||
|
||||
export const closeScope = (
|
||||
scope: Scope.CloseableScope,
|
||||
runtime: Runtime.Runtime<never>,
|
||||
options?: ScopeOptions,
|
||||
) => {
|
||||
switch (options?.finalizerExecutionMode ?? "sync") {
|
||||
case "sync":
|
||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
||||
break
|
||||
case "fork":
|
||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
||||
break
|
||||
}
|
||||
}
|
||||
18
packages/effect-fc/src/hooks/Hooks/useCallbackPromise.ts
Normal file
18
packages/effect-fc/src/hooks/Hooks/useCallbackPromise.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Effect, Runtime } from "effect"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
export const useCallbackPromise: {
|
||||
<Args extends unknown[], A, E, R>(
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
||||
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
|
||||
runtimeRef.current = yield* Effect.runtime<R>()
|
||||
|
||||
return React.useCallback((...args: Args) => Runtime.runPromise(runtimeRef.current)(callback(...args)), deps)
|
||||
})
|
||||
18
packages/effect-fc/src/hooks/Hooks/useCallbackSync.ts
Normal file
18
packages/effect-fc/src/hooks/Hooks/useCallbackSync.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Effect, Runtime } from "effect"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
export const useCallbackSync: {
|
||||
<Args extends unknown[], A, E, R>(
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<(...args: Args) => A, never, R>
|
||||
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
|
||||
runtimeRef.current = yield* Effect.runtime<R>()
|
||||
|
||||
return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(callback(...args)), deps)
|
||||
})
|
||||
25
packages/effect-fc/src/hooks/Hooks/useContext.ts
Normal file
25
packages/effect-fc/src/hooks/Hooks/useContext.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type Context, Effect, Layer, ManagedRuntime, Scope } from "effect"
|
||||
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||
import { useMemo } from "./useMemo.js"
|
||||
import { useScope } from "./useScope.js"
|
||||
|
||||
|
||||
export const useContext: {
|
||||
<ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<Context.Context<ROut>, E, RIn>
|
||||
} = Effect.fnUntraced(function* <ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>,
|
||||
options?: ScopeOptions,
|
||||
) {
|
||||
const scope = yield* useScope([layer], options)
|
||||
|
||||
return yield* useMemo(() => 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])
|
||||
})
|
||||
28
packages/effect-fc/src/hooks/Hooks/useEffect.ts
Normal file
28
packages/effect-fc/src/hooks/Hooks/useEffect.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
|
||||
import * as React from "react"
|
||||
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||
import { closeScope } from "./internal.js"
|
||||
|
||||
|
||||
export const useEffect: {
|
||||
<E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function* <E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
React.useEffect(() => Effect.Do.pipe(
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||
Effect.map(({ scope }) =>
|
||||
() => closeScope(scope, runtime, options)
|
||||
),
|
||||
Runtime.runSync(runtime),
|
||||
), deps)
|
||||
})
|
||||
31
packages/effect-fc/src/hooks/Hooks/useFork.ts
Normal file
31
packages/effect-fc/src/hooks/Hooks/useFork.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
|
||||
import * as React from "react"
|
||||
import { closeScope } from "./internal.js"
|
||||
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||
|
||||
|
||||
export const useFork: {
|
||||
<E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function* <E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
React.useEffect(() => {
|
||||
const scope = Runtime.runSync(runtime)(options?.scope
|
||||
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
)
|
||||
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
||||
return () => closeScope(scope, runtime, {
|
||||
...options,
|
||||
finalizerExecutionMode: options?.finalizerExecutionMode ?? "fork",
|
||||
})
|
||||
}, deps)
|
||||
})
|
||||
28
packages/effect-fc/src/hooks/Hooks/useLayoutEffect.ts
Normal file
28
packages/effect-fc/src/hooks/Hooks/useLayoutEffect.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
|
||||
import * as React from "react"
|
||||
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||
import { closeScope } from "./internal.js"
|
||||
|
||||
|
||||
export const useLayoutEffect: {
|
||||
<E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function* <E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
React.useLayoutEffect(() => Effect.Do.pipe(
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||
Effect.map(({ scope }) =>
|
||||
() => closeScope(scope, runtime, options)
|
||||
),
|
||||
Runtime.runSync(runtime),
|
||||
), deps)
|
||||
})
|
||||
16
packages/effect-fc/src/hooks/Hooks/useMemo.ts
Normal file
16
packages/effect-fc/src/hooks/Hooks/useMemo.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Effect, Runtime } from "effect"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
export const useMemo: {
|
||||
<A, E, R>(
|
||||
factory: () => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<A, E, R>
|
||||
} = Effect.fnUntraced(function* <A, E, R>(
|
||||
factory: () => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
|
||||
})
|
||||
11
packages/effect-fc/src/hooks/Hooks/useOnce.ts
Normal file
11
packages/effect-fc/src/hooks/Hooks/useOnce.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import { useMemo } from "./useMemo.js"
|
||||
|
||||
|
||||
export const useOnce: {
|
||||
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||
} = Effect.fnUntraced(function* <A, E, R>(
|
||||
factory: () => Effect.Effect<A, E, R>
|
||||
) {
|
||||
return yield* useMemo(factory, [])
|
||||
})
|
||||
20
packages/effect-fc/src/hooks/Hooks/useRefFromState.ts
Normal file
20
packages/effect-fc/src/hooks/Hooks/useRefFromState.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect"
|
||||
import type * as React from "react"
|
||||
import { useEffect } from "./useEffect.js"
|
||||
import { useFork } from "./useFork.js"
|
||||
import { useOnce } from "./useOnce.js"
|
||||
|
||||
|
||||
export const useRefFromState: {
|
||||
<A>(state: readonly [A, React.Dispatch<React.SetStateAction<A>>]): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
||||
} = Effect.fnUntraced(function*([value, setValue]) {
|
||||
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
||||
|
||||
yield* useEffect(() => Ref.set(ref, value), [value])
|
||||
yield* useFork(() => Stream.runForEach(
|
||||
Stream.changesWith(ref.changes, Equivalence.strict()),
|
||||
v => Effect.sync(() => setValue(v)),
|
||||
), [setValue])
|
||||
|
||||
return ref
|
||||
})
|
||||
29
packages/effect-fc/src/hooks/Hooks/useRefState.ts
Normal file
29
packages/effect-fc/src/hooks/Hooks/useRefState.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect"
|
||||
import * as React from "react"
|
||||
import { SetStateAction } from "../../types/index.js"
|
||||
import { useCallbackSync } from "./useCallbackSync.js"
|
||||
import { useFork } from "./useFork.js"
|
||||
import { useOnce } from "./useOnce.js"
|
||||
|
||||
|
||||
export const useRefState: {
|
||||
<A>(
|
||||
ref: SubscriptionRef.SubscriptionRef<A>
|
||||
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
|
||||
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
||||
|
||||
yield* useFork(() => Stream.runForEach(
|
||||
Stream.changesWith(ref.changes, Equivalence.strict()),
|
||||
v => Effect.sync(() => setReactStateValue(v)),
|
||||
), [ref])
|
||||
|
||||
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||
Effect.andThen(
|
||||
Ref.updateAndGet(ref, prevState => SetStateAction.value(setStateAction, prevState)),
|
||||
v => setReactStateValue(v),
|
||||
),
|
||||
[ref])
|
||||
|
||||
return [reactStateValue, setValue]
|
||||
})
|
||||
36
packages/effect-fc/src/hooks/Hooks/useScope.ts
Normal file
36
packages/effect-fc/src/hooks/Hooks/useScope.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Effect, ExecutionStrategy, Ref, Runtime, Scope } from "effect"
|
||||
import * as React from "react"
|
||||
import type { ScopeOptions } from "./ScopeOptions.js"
|
||||
import { closeScope } from "./internal.js"
|
||||
|
||||
|
||||
export const useScope: {
|
||||
(
|
||||
deps: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<Scope.Scope>
|
||||
} = Effect.fnUntraced(function*(deps, options) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
|
||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([
|
||||
Ref.make(true),
|
||||
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
||||
])), [])
|
||||
const [scope, setScope] = React.useState(initialScope)
|
||||
|
||||
React.useEffect(() => Runtime.runSync(runtime)(
|
||||
Effect.if(isInitialRun, {
|
||||
onTrue: () => Effect.as(
|
||||
Ref.set(isInitialRun, false),
|
||||
() => closeScope(scope, runtime, options),
|
||||
),
|
||||
|
||||
onFalse: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe(
|
||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
||||
),
|
||||
})
|
||||
), deps)
|
||||
|
||||
return scope
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Effect, PubSub, Ref, type Scope, Stream } from "effect"
|
||||
import type * as React from "react"
|
||||
import { useEffect } from "./useEffect.js"
|
||||
import { useOnce } from "./useOnce.js"
|
||||
|
||||
|
||||
export const useStreamFromReactiveValues: {
|
||||
<const A extends React.DependencyList>(
|
||||
values: A
|
||||
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
||||
} = Effect.fnUntraced(function* <const A extends React.DependencyList>(values: A) {
|
||||
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
|
||||
Effect.bind("latest", () => Ref.make(values)),
|
||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
||||
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
|
||||
Effect.flatMap(a => Effect.map(
|
||||
Stream.fromPubSub(pubsub, { scoped: true }),
|
||||
s => Stream.concat(Stream.make(a), s),
|
||||
)),
|
||||
Stream.unwrapScoped,
|
||||
)),
|
||||
))
|
||||
|
||||
yield* useEffect(() => Ref.set(latest, values).pipe(
|
||||
Effect.andThen(PubSub.publish(pubsub, values)),
|
||||
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
||||
), values)
|
||||
|
||||
return stream
|
||||
})
|
||||
31
packages/effect-fc/src/hooks/Hooks/useSubscribe.ts
Normal file
31
packages/effect-fc/src/hooks/Hooks/useSubscribe.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Effect, Equivalence, pipe, Stream, type Subscribable } from "effect"
|
||||
import * as React from "react"
|
||||
import { useFork } from "./useFork.js"
|
||||
import { useOnce } from "./useOnce.js"
|
||||
|
||||
|
||||
export const useSubscribe: {
|
||||
<const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
||||
...elements: T
|
||||
): Effect.Effect<
|
||||
{ [K in keyof T]: Effect.Effect.Success<T[K]["get"]> | Stream.Stream.Success<T[K]["changes"]> },
|
||||
Effect.Effect.Error<T[number]["get"]> | Stream.Stream.Error<T[number]["changes"]>,
|
||||
Effect.Effect.Context<T[number]["get"]> | Stream.Stream.Context<T[number]["changes"]>
|
||||
>
|
||||
} = Effect.fnUntraced(function* <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
||||
...elements: T
|
||||
) {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
||||
Effect.all(elements.map(v => v.get))
|
||||
))
|
||||
|
||||
yield* useFork(() => pipe(
|
||||
elements.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())),
|
||||
streams => Stream.zipLatestAll(...streams),
|
||||
Stream.runForEach(v =>
|
||||
Effect.sync(() => setReactStateValue(v))
|
||||
),
|
||||
), elements)
|
||||
|
||||
return reactStateValue as any
|
||||
})
|
||||
31
packages/effect-fc/src/hooks/Hooks/useSubscribeStream.ts
Normal file
31
packages/effect-fc/src/hooks/Hooks/useSubscribeStream.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Effect, Equivalence, Option, Stream } from "effect"
|
||||
import * as React from "react"
|
||||
import { useFork } from "./useFork.js"
|
||||
|
||||
|
||||
export const useSubscribeStream: {
|
||||
<A, E, R>(
|
||||
stream: Stream.Stream<A, E, R>
|
||||
): Effect.Effect<Option.Option<A>, never, R>
|
||||
<A extends NonNullable<unknown>, E, R>(
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue: A,
|
||||
): Effect.Effect<Option.Some<A>, never, R>
|
||||
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue?: A,
|
||||
) {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(
|
||||
React.useMemo(() => initialValue
|
||||
? Option.some(initialValue)
|
||||
: Option.none(),
|
||||
[])
|
||||
)
|
||||
|
||||
yield* useFork(() => Stream.runForEach(
|
||||
Stream.changesWith(stream, Equivalence.strict()),
|
||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||
), [stream])
|
||||
|
||||
return reactStateValue as Option.Some<A>
|
||||
})
|
||||
2
packages/effect-fc/src/hooks/index.ts
Normal file
2
packages/effect-fc/src/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./Hooks/index.js"
|
||||
export * as Hooks from "./Hooks/index.js"
|
||||
@@ -1,5 +1,4 @@
|
||||
export * as Component from "./Component.js"
|
||||
export * as Hook from "./Hook.js"
|
||||
export * as Memoized from "./Memoized.js"
|
||||
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
|
||||
export * as Memo from "./Memo.js"
|
||||
export * as ReactRuntime from "./ReactRuntime.js"
|
||||
export * as Suspense from "./Suspense.js"
|
||||
|
||||
@@ -38,8 +38,7 @@ export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer
|
||||
: never
|
||||
: T
|
||||
|
||||
export type AnyKey = string | number | symbol
|
||||
export type AnyPath = readonly AnyKey[]
|
||||
export type AnyPath = readonly PropertyKey[]
|
||||
|
||||
|
||||
export const unsafeGet: {
|
||||
|
||||
24
packages/effect-fc/src/types/Subscribable.ts
Normal file
24
packages/effect-fc/src/types/Subscribable.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { type Effect, Effectable, Readable, type Stream, Subscribable } from "effect"
|
||||
|
||||
|
||||
class SubscribableImpl<A, E, R>
|
||||
extends Effectable.Class<A, E, R> implements Subscribable.Subscribable<A, E, R> {
|
||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||
|
||||
constructor(
|
||||
readonly get: Effect.Effect<A, E, R>,
|
||||
readonly changes: Stream.Stream<A, E, R>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
commit() {
|
||||
return this.get
|
||||
}
|
||||
}
|
||||
|
||||
export const make = <A, E, R>(values: {
|
||||
readonly get: Effect.Effect<A, E, R>
|
||||
readonly changes: Stream.Stream<A, E, R>
|
||||
}): Subscribable.Subscribable<A, E, R> => new SubscribableImpl(values.get, values.changes)
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Chunk, Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
||||
import { Chunk, Effect, Effectable, Option, Predicate, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
||||
import * as PropertyPath from "./PropertyPath.js"
|
||||
|
||||
|
||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/types/SubscriptionSubRef")
|
||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
||||
|
||||
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
||||
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
||||
export interface SubscriptionSubRef<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
|
||||
extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
||||
readonly parent: B
|
||||
|
||||
readonly [Unify.typeSymbol]?: unknown
|
||||
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
||||
@@ -36,7 +37,8 @@ const synchronizedRefVariance = { _A: (_: any) => _ }
|
||||
const subscriptionRefVariance = { _A: (_: any) => _ }
|
||||
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
||||
|
||||
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
||||
class SubscriptionSubRefImpl<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
|
||||
extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||
readonly [Ref.RefTypeId] = refVariance
|
||||
@@ -47,9 +49,9 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
||||
readonly get: Effect.Effect<A>
|
||||
|
||||
constructor(
|
||||
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
||||
readonly getter: (parentValue: B) => A,
|
||||
readonly setter: (parentValue: B, value: A) => B,
|
||||
readonly parent: B,
|
||||
readonly getter: (parentValue: Effect.Effect.Success<B>) => A,
|
||||
readonly setter: (parentValue: Effect.Effect.Success<B>, value: A) => Effect.Effect.Success<B>,
|
||||
) {
|
||||
super()
|
||||
this.get = Effect.map(this.parent, this.getter)
|
||||
@@ -60,12 +62,11 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
||||
}
|
||||
|
||||
get changes(): Stream.Stream<A> {
|
||||
return this.get.pipe(
|
||||
Effect.map(a => this.parent.changes.pipe(
|
||||
Stream.map(this.getter),
|
||||
s => Stream.concat(Stream.make(a), s),
|
||||
)),
|
||||
Stream.unwrap,
|
||||
return Stream.unwrap(
|
||||
Effect.map(this.get, a => Stream.concat(
|
||||
Stream.make(a),
|
||||
Stream.map(this.parent.changes, this.getter),
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
||||
|
||||
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
||||
return Effect.Do.pipe(
|
||||
Effect.bind("b", () => Ref.get(this.parent)),
|
||||
Effect.bind("b", (): Effect.Effect<Effect.Effect.Success<B>> => this.parent),
|
||||
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
||||
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
||||
Effect.map(({ ca: [c] }) => c),
|
||||
@@ -84,28 +85,102 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
||||
}
|
||||
|
||||
|
||||
export const makeFromGetSet = <A, B>(
|
||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||
export const isSubscriptionSubRef = (u: unknown): u is SubscriptionSubRef<unknown, SubscriptionRef.SubscriptionRef<unknown>> => Predicate.hasProperty(u, SubscriptionSubRefTypeId)
|
||||
|
||||
export const makeFromGetSet = <A, B extends SubscriptionRef.SubscriptionRef<any>>(
|
||||
parent: B,
|
||||
options: {
|
||||
readonly get: (parentValue: B) => A
|
||||
readonly set: (parentValue: B, value: A) => B
|
||||
readonly get: (parentValue: Effect.Effect.Success<B>) => A
|
||||
readonly set: (parentValue: Effect.Effect.Success<B>, value: A) => Effect.Effect.Success<B>
|
||||
},
|
||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, options.get, options.set)
|
||||
|
||||
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||
export const makeFromPath = <
|
||||
B extends SubscriptionRef.SubscriptionRef<any>,
|
||||
const P extends PropertyPath.Paths<Effect.Effect.Success<B>>,
|
||||
>(
|
||||
parent: B,
|
||||
path: P,
|
||||
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
|
||||
): SubscriptionSubRef<PropertyPath.ValueFromPath<Effect.Effect.Success<B>, P>, B> => new SubscriptionSubRefImpl(
|
||||
parent,
|
||||
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
||||
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
||||
)
|
||||
|
||||
export const makeFromChunkRef = <A>(
|
||||
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<A>>,
|
||||
export const makeFromChunkIndex: {
|
||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
|
||||
parent: B,
|
||||
index: number,
|
||||
): SubscriptionSubRef<
|
||||
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
|
||||
B
|
||||
>
|
||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
|
||||
parent: B,
|
||||
index: number,
|
||||
): SubscriptionSubRef<
|
||||
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
|
||||
B
|
||||
>
|
||||
} = (
|
||||
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>,
|
||||
index: number,
|
||||
): SubscriptionSubRef<A, Chunk.Chunk<A>> => new SubscriptionSubRefImpl(
|
||||
) => new SubscriptionSubRefImpl(
|
||||
parent,
|
||||
parentValue => Chunk.unsafeGet(parentValue, index),
|
||||
(parentValue, value) => Chunk.replace(parentValue, index, value),
|
||||
)
|
||||
) as any
|
||||
|
||||
export const makeFromChunkFindFirst: {
|
||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
|
||||
parent: B,
|
||||
findFirstPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never>,
|
||||
): SubscriptionSubRef<
|
||||
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
|
||||
B
|
||||
>
|
||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
|
||||
parent: B,
|
||||
findFirstPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never>,
|
||||
): SubscriptionSubRef<
|
||||
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
|
||||
B
|
||||
>
|
||||
} = (
|
||||
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<never>>,
|
||||
findFirstPredicate: Predicate.Predicate.Any,
|
||||
) => new SubscriptionSubRefImpl(
|
||||
parent,
|
||||
parentValue => Option.getOrThrow(Chunk.findFirst(parentValue, findFirstPredicate)),
|
||||
(parentValue, value) => Option.getOrThrow(Option.andThen(
|
||||
Chunk.findFirstIndex(parentValue, findFirstPredicate),
|
||||
index => Chunk.replace(parentValue, index, value),
|
||||
)),
|
||||
) as any
|
||||
|
||||
export const makeFromChunkFindLast: {
|
||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
|
||||
parent: B,
|
||||
findLastPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never>,
|
||||
): SubscriptionSubRef<
|
||||
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
|
||||
B
|
||||
>
|
||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
|
||||
parent: B,
|
||||
findLastPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never>,
|
||||
): SubscriptionSubRef<
|
||||
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
|
||||
B
|
||||
>
|
||||
} = (
|
||||
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<never>>,
|
||||
findLastPredicate: Predicate.Predicate.Any,
|
||||
) => new SubscriptionSubRefImpl(
|
||||
parent,
|
||||
parentValue => Option.getOrThrow(Chunk.findLast(parentValue, findLastPredicate)),
|
||||
(parentValue, value) => Option.getOrThrow(Option.andThen(
|
||||
Chunk.findLastIndex(parentValue, findLastPredicate),
|
||||
index => Chunk.replace(parentValue, index, value),
|
||||
)),
|
||||
) as any
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * as PropertyPath from "./PropertyPath.js"
|
||||
export * as SetStateAction from "./SetStateAction.js"
|
||||
export * as Subscribable from "./Subscribable.js"
|
||||
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||
|
||||
@@ -11,42 +11,37 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/language-service": "^0.23.4",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@tanstack/react-router": "^1.120.3",
|
||||
"@tanstack/react-router-devtools": "^1.120.3",
|
||||
"@tanstack/router-plugin": "^1.120.3",
|
||||
"@thilawyn/thilaschema": "^0.1.4",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.26.0",
|
||||
"@effect/language-service": "^0.35.2",
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@tanstack/react-router": "^1.131.27",
|
||||
"@tanstack/react-router-devtools": "^1.131.27",
|
||||
"@tanstack/router-plugin": "^1.131.27",
|
||||
"@types/react": "^19.1.11",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react": "^5.0.1",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.1.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"vite": "^6.3.5"
|
||||
"globals": "^16.3.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"vite": "^7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform": "^0.82.1",
|
||||
"@effect/platform-browser": "^0.62.1",
|
||||
"@effect/platform": "^0.90.6",
|
||||
"@effect/platform-browser": "^0.70.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.15.1",
|
||||
"effect": "^3.17.9",
|
||||
"effect-fc": "workspace:*",
|
||||
"lucide-react": "^0.510.0",
|
||||
"mobx": "^6.13.7",
|
||||
"react-icons": "^5.5.0"
|
||||
},
|
||||
"overrides": {
|
||||
"effect": "^3.15.1",
|
||||
"@effect/platform": "^0.82.1",
|
||||
"@effect/platform-browser": "^0.62.1",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"@typed/async-data": "^0.13.1"
|
||||
"@types/react": "^19.1.11",
|
||||
"effect": "^3.17.9",
|
||||
"react": "^19.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ThSchema } from "@thilawyn/thilaschema"
|
||||
import { assertEncodedJsonifiable } from "@/lib/schema"
|
||||
import { Schema } from "effect"
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export const TodoFromJsonStruct = Schema.Struct({
|
||||
...Todo.fields,
|
||||
completedAt: Schema.Option(Schema.DateTimeUtc),
|
||||
}).pipe(
|
||||
ThSchema.assertEncodedJsonifiable
|
||||
assertEncodedJsonifiable
|
||||
)
|
||||
|
||||
export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)
|
||||
|
||||
40
packages/example/src/lib/input/TextAreaInput.tsx
Normal file
40
packages/example/src/lib/input/TextAreaInput.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Callout, Flex, TextArea, TextAreaProps } from "@radix-ui/themes"
|
||||
import { Array, Equivalence, Option, ParseResult, Schema, Struct } from "effect"
|
||||
import { Component } from "effect-fc"
|
||||
import { useInput } from "effect-fc/hooks"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
export type TextAreaInputProps<A, R> = Omit<useInput.Options<A, R>, "schema" | "equivalence"> & Omit<TextAreaProps, "ref">
|
||||
|
||||
export const TextAreaInput = <A, R>(options: {
|
||||
readonly schema: Schema.Schema<A, string, R>
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
}): Component.Component<
|
||||
TextAreaInputProps<A, R>,
|
||||
React.JSX.Element,
|
||||
ParseResult.ParseError,
|
||||
R
|
||||
> => Component.makeUntraced(function* TextFieldInput(props) {
|
||||
const input = yield* useInput({ ...options, ...props })
|
||||
const issue = React.useMemo(() => input.error.pipe(
|
||||
Option.map(ParseResult.ArrayFormatter.formatErrorSync),
|
||||
Option.flatMap(Array.head),
|
||||
), [input.error])
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="1">
|
||||
<TextArea
|
||||
value={input.value}
|
||||
onChange={e => input.setValue(e.target.value)}
|
||||
{...Struct.omit(props, "ref")}
|
||||
/>
|
||||
|
||||
{Option.isSome(issue) &&
|
||||
<Callout.Root color="red" role="alert">
|
||||
<Callout.Text>{issue.value.message}</Callout.Text>
|
||||
</Callout.Root>
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
})
|
||||
70
packages/example/src/lib/input/TextFieldInput.tsx
Normal file
70
packages/example/src/lib/input/TextFieldInput.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Callout, Checkbox, Flex, TextField } from "@radix-ui/themes"
|
||||
import { Array, Equivalence, Option, ParseResult, Schema, Struct } from "effect"
|
||||
import { Component, Memo } from "effect-fc"
|
||||
import { useInput, useOptionalInput } from "effect-fc/hooks"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
export type TextFieldInputProps<A, R> = (
|
||||
& Omit<useInput.Options<A, R>, "schema" | "equivalence">
|
||||
& Omit<TextField.RootProps, "ref">
|
||||
)
|
||||
export type TextFieldOptionalInputProps<A, R> = (
|
||||
& Omit<useOptionalInput.Options<A, R>, "schema" | "equivalence">
|
||||
& Omit<TextField.RootProps, "ref" | "defaultValue">
|
||||
)
|
||||
|
||||
export const TextFieldInput = <A, R, O extends boolean = false>(options: {
|
||||
readonly optional?: O
|
||||
readonly schema: Schema.Schema<A, string, R>
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
}) => Component.makeUntraced(function* TextFieldInput(props: O extends true
|
||||
? TextFieldOptionalInputProps<A, R>
|
||||
: TextFieldInputProps<A, R>
|
||||
) {
|
||||
const input: (
|
||||
| { readonly optional: true } & useOptionalInput.Result
|
||||
| { readonly optional: false } & useInput.Result
|
||||
) = options.optional
|
||||
? {
|
||||
optional: true,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
...yield* useOptionalInput({ ...options, ...props as TextFieldOptionalInputProps<A, R> }),
|
||||
}
|
||||
: {
|
||||
optional: false,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
...yield* useInput({ ...options, ...props as TextFieldInputProps<A, R> }),
|
||||
}
|
||||
|
||||
const issue = React.useMemo(() => input.error.pipe(
|
||||
Option.map(ParseResult.ArrayFormatter.formatErrorSync),
|
||||
Option.flatMap(Array.head),
|
||||
), [input.error])
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="1">
|
||||
<Flex direction="row" align="center" gap="1">
|
||||
{input.optional &&
|
||||
<Checkbox
|
||||
checked={input.enabled}
|
||||
onCheckedChange={checked => input.setEnabled(checked !== "indeterminate" && checked)}
|
||||
/>
|
||||
}
|
||||
|
||||
<TextField.Root
|
||||
value={input.value}
|
||||
onChange={e => input.setValue(e.target.value)}
|
||||
disabled={input.optional ? !input.enabled : undefined}
|
||||
{...Struct.omit(props as TextFieldOptionalInputProps<A, R> | TextFieldInputProps<A, R>, "ref", "defaultValue")}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{(!(input.optional && !input.enabled) && Option.isSome(issue)) &&
|
||||
<Callout.Root color="red" role="alert">
|
||||
<Callout.Text>{issue.value.message}</Callout.Text>
|
||||
</Callout.Root>
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
}).pipe(Memo.memo)
|
||||
38
packages/example/src/lib/schema/datetime.ts
Normal file
38
packages/example/src/lib/schema/datetime.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { DateTime, Effect, Option, ParseResult, Schema } from "effect"
|
||||
|
||||
|
||||
export class DateTimeUtcFromZoned extends Schema.transformOrFail(
|
||||
Schema.DateTimeZonedFromSelf,
|
||||
Schema.DateTimeUtcFromSelf,
|
||||
{
|
||||
strict: true,
|
||||
encode: DateTime.setZoneCurrent,
|
||||
decode: i => ParseResult.succeed(DateTime.toUtc(i)),
|
||||
},
|
||||
) {}
|
||||
|
||||
export class DateTimeZonedFromUtc extends Schema.transformOrFail(
|
||||
Schema.DateTimeUtcFromSelf,
|
||||
Schema.DateTimeZonedFromSelf,
|
||||
{
|
||||
strict: true,
|
||||
encode: a => ParseResult.succeed(DateTime.toUtc(a)),
|
||||
decode: DateTime.setZoneCurrent,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class DateTimeUtcFromZonedInput extends Schema.transformOrFail(
|
||||
Schema.String,
|
||||
DateTimeUtcFromZoned,
|
||||
{
|
||||
strict: true,
|
||||
encode: a => ParseResult.succeed(DateTime.formatIsoZoned(a).slice(0, 16)),
|
||||
decode: (i, _, ast) => Effect.flatMap(
|
||||
DateTime.CurrentTimeZone,
|
||||
timeZone => Option.match(DateTime.makeZoned(i, { timeZone, adjustForTimeZone: true }), {
|
||||
onSome: ParseResult.succeed,
|
||||
onNone: () => ParseResult.fail(new ParseResult.Type(ast, i, `Unable to decode ${JSON.stringify(i)} into a DateTime.Zoned`)),
|
||||
}),
|
||||
),
|
||||
},
|
||||
) {}
|
||||
2
packages/example/src/lib/schema/index.ts
Normal file
2
packages/example/src/lib/schema/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./datetime"
|
||||
export * from "./json"
|
||||
6
packages/example/src/lib/schema/json.ts
Normal file
6
packages/example/src/lib/schema/json.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Schema } from "effect"
|
||||
import type { JsonValue } from "type-fest"
|
||||
|
||||
|
||||
export const assertEncodedJsonifiable = <S extends Schema.Schema<A, I, R>, A, I extends JsonValue, R>(schema: S & Schema.Schema<A, I, R>): S => schema
|
||||
export const assertTypeJsonifiable = <S extends Schema.Schema<A, I, R>, A extends JsonValue, I, R>(schema: S & Schema.Schema<A, I, R>): S => schema
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
||||
import { ReactManagedRuntime } from "effect-fc"
|
||||
import { ReactRuntime } from "effect-fc"
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { routeTree } from "./routeTree.gen"
|
||||
@@ -16,8 +16,8 @@ declare module "@tanstack/react-router" {
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ReactManagedRuntime.Provider runtime={runtime}>
|
||||
<ReactRuntime.Provider runtime={runtime}>
|
||||
<RouterProvider router={router} />
|
||||
</ReactManagedRuntime.Provider>
|
||||
</ReactRuntime.Provider>
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as BlankRouteImport } from './routes/blank'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DevMemoRouteImport } from './routes/dev/memo'
|
||||
import { Route as DevInputRouteImport } from './routes/dev/input'
|
||||
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
|
||||
|
||||
const BlankRoute = BlankRouteImport.update({
|
||||
@@ -29,6 +30,11 @@ const DevMemoRoute = DevMemoRouteImport.update({
|
||||
path: '/dev/memo',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DevInputRoute = DevInputRouteImport.update({
|
||||
id: '/dev/input',
|
||||
path: '/dev/input',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
|
||||
id: '/dev/async-rendering',
|
||||
path: '/dev/async-rendering',
|
||||
@@ -39,12 +45,14 @@ export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/input': typeof DevInputRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/input': typeof DevInputRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
@@ -52,20 +60,33 @@ export interface FileRoutesById {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/input': typeof DevInputRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/blank' | '/dev/async-rendering' | '/dev/memo'
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/input'
|
||||
| '/dev/memo'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/blank' | '/dev/async-rendering' | '/dev/memo'
|
||||
id: '__root__' | '/' | '/blank' | '/dev/async-rendering' | '/dev/memo'
|
||||
to: '/' | '/blank' | '/dev/async-rendering' | '/dev/input' | '/dev/memo'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/input'
|
||||
| '/dev/memo'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
BlankRoute: typeof BlankRoute
|
||||
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
|
||||
DevInputRoute: typeof DevInputRoute
|
||||
DevMemoRoute: typeof DevMemoRoute
|
||||
}
|
||||
|
||||
@@ -92,6 +113,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof DevMemoRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/dev/input': {
|
||||
id: '/dev/input'
|
||||
path: '/dev/input'
|
||||
fullPath: '/dev/input'
|
||||
preLoaderRoute: typeof DevInputRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/dev/async-rendering': {
|
||||
id: '/dev/async-rendering'
|
||||
path: '/dev/async-rendering'
|
||||
@@ -106,6 +134,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
BlankRoute: BlankRoute,
|
||||
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
|
||||
DevInputRoute: DevInputRoute,
|
||||
DevMemoRoute: DevMemoRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
|
||||
@@ -3,12 +3,13 @@ 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 { Component, Hook, Memoized, Suspense } from "effect-fc"
|
||||
import { Component, Memo, Suspense } from "effect-fc"
|
||||
import { Hooks } from "effect-fc/hooks"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
// Generator version
|
||||
const RouteComponent = Component.make(function* AsyncRendering() {
|
||||
const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
||||
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent
|
||||
const AsyncComponentFC = yield* AsyncComponent
|
||||
const [input, setInput] = React.useState("")
|
||||
@@ -50,7 +51,7 @@ const RouteComponent = Component.make(function* AsyncRendering() {
|
||||
// )
|
||||
|
||||
|
||||
class AsyncComponent extends Component.make(function* AsyncComponent() {
|
||||
class AsyncComponent extends Component.makeUntraced(function* AsyncComponent() {
|
||||
const SubComponentFC = yield* SubComponent
|
||||
|
||||
yield* Effect.sleep("500 millis") // Async operation
|
||||
@@ -66,10 +67,10 @@ class AsyncComponent extends Component.make(function* AsyncComponent() {
|
||||
Suspense.suspense,
|
||||
Suspense.withOptions({ defaultFallback: <p>Loading...</p> }),
|
||||
) {}
|
||||
class MemoizedAsyncComponent extends Memoized.memo(AsyncComponent) {}
|
||||
class MemoizedAsyncComponent extends Memo.memo(AsyncComponent) {}
|
||||
|
||||
class SubComponent extends Component.make(function* SubComponent() {
|
||||
const [state] = React.useState(yield* Hook.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
||||
class SubComponent extends Component.makeUntraced(function* SubComponent() {
|
||||
const [state] = React.useState(yield* Hooks.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
||||
return <Text>{state}</Text>
|
||||
}) {}
|
||||
|
||||
|
||||
42
packages/example/src/routes/dev/input.tsx
Normal file
42
packages/example/src/routes/dev/input.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { TextFieldInput } from "@/lib/input/TextFieldInput"
|
||||
import { runtime } from "@/runtime"
|
||||
import { Container } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Schema, SubscriptionRef } from "effect"
|
||||
import { Component, Memo } from "effect-fc"
|
||||
import { useInput, useOnce, useRefState } from "effect-fc/hooks"
|
||||
|
||||
|
||||
const IntFromString = Schema.NumberFromString.pipe(Schema.int())
|
||||
|
||||
const IntTextFieldInput = TextFieldInput({ schema: IntFromString })
|
||||
const StringTextFieldInput = TextFieldInput({ schema: Schema.String })
|
||||
|
||||
const Input = Component.makeUntraced(function* Input() {
|
||||
const IntTextFieldInputFC = yield* IntTextFieldInput
|
||||
const StringTextFieldInputFC = yield* StringTextFieldInput
|
||||
|
||||
const intRef1 = yield* useOnce(() => SubscriptionRef.make(0))
|
||||
const intRef2 = yield* useOnce(() => SubscriptionRef.make(0))
|
||||
const stringRef = yield* useOnce(() => SubscriptionRef.make(""))
|
||||
// yield* useFork(() => Stream.runForEach(intRef1.changes, Console.log), [intRef1])
|
||||
|
||||
const input2 = yield* useInput({ schema: IntFromString, ref: intRef2 })
|
||||
|
||||
const [str, setStr] = yield* useRefState(stringRef)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<IntTextFieldInputFC ref={intRef1} />
|
||||
<StringTextFieldInputFC ref={stringRef} />
|
||||
<StringTextFieldInputFC ref={stringRef} />
|
||||
</Container>
|
||||
)
|
||||
}).pipe(
|
||||
Memo.memo,
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
export const Route = createFileRoute("/dev/input")({
|
||||
component: Input,
|
||||
})
|
||||
@@ -3,11 +3,11 @@ 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 { Component, Memoized } from "effect-fc"
|
||||
import { Component, Memo } from "effect-fc"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
const RouteComponent = Component.make(function* RouteComponent() {
|
||||
const RouteComponent = Component.makeUntraced(function* RouteComponent() {
|
||||
const [value, setValue] = React.useState("")
|
||||
|
||||
return (
|
||||
@@ -25,12 +25,12 @@ const RouteComponent = Component.make(function* RouteComponent() {
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
class SubComponent extends Component.make(function* SubComponent() {
|
||||
class SubComponent extends Component.makeUntraced(function* SubComponent() {
|
||||
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
|
||||
return <Text>{id}</Text>
|
||||
}) {}
|
||||
|
||||
class MemoizedSubComponent extends Memoized.memo(SubComponent) {}
|
||||
class MemoizedSubComponent extends Memo.memo(SubComponent) {}
|
||||
|
||||
export const Route = createFileRoute("/dev/memo")({
|
||||
component: RouteComponent,
|
||||
|
||||
@@ -3,18 +3,21 @@ import { Todos } from "@/todo/Todos"
|
||||
import { TodosState } from "@/todo/TodosState.service"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Effect } from "effect"
|
||||
import { Component, Hook } from "effect-fc"
|
||||
import { Component } from "effect-fc"
|
||||
import { useContext } from "effect-fc/hooks"
|
||||
|
||||
|
||||
const TodosStateLive = TodosState.Default("todos")
|
||||
|
||||
const Index = Component.makeUntraced(function* Index() {
|
||||
const context = yield* useContext(TodosStateLive, { finalizerExecutionMode: "fork" })
|
||||
const TodosFC = yield* Effect.provide(Todos, context)
|
||||
|
||||
return <TodosFC />
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: Component.make(function* Index() {
|
||||
return yield* Todos.pipe(
|
||||
Effect.map(FC => <FC />),
|
||||
Effect.provide(yield* Hook.useContext(TodosStateLive, { finalizerExecutionMode: "fork" })),
|
||||
)
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
component: Index
|
||||
})
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { FetchHttpClient } from "@effect/platform"
|
||||
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||
import { Layer } from "effect"
|
||||
import { ReactManagedRuntime } from "effect-fc"
|
||||
import { DateTime, Layer } from "effect"
|
||||
import { ReactRuntime } from "effect-fc"
|
||||
|
||||
|
||||
export const AppLive = Layer.empty.pipe(
|
||||
Layer.provideMerge(DateTime.layerCurrentZoneLocal),
|
||||
Layer.provideMerge(Clipboard.layer),
|
||||
Layer.provideMerge(Geolocation.layer),
|
||||
Layer.provideMerge(Permissions.layer),
|
||||
Layer.provideMerge(FetchHttpClient.layer),
|
||||
)
|
||||
|
||||
export const runtime = ReactManagedRuntime.make(AppLive)
|
||||
export const runtime = ReactRuntime.make(AppLive)
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import * as Domain from "@/domain"
|
||||
import { Box, Button, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
||||
import { TextAreaInput } from "@/lib/input/TextAreaInput"
|
||||
import { TextFieldInput } from "@/lib/input/TextFieldInput"
|
||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, Effect, Match, Option, Ref, Runtime, SubscriptionRef } from "effect"
|
||||
import { Component, Hook, Memoized } from "effect-fc"
|
||||
import { SubscriptionSubRef } from "effect-fc/types"
|
||||
import { Chunk, DateTime, Effect, Match, Option, Ref, Runtime, Schema, Stream, SubscriptionRef } from "effect"
|
||||
import { Component, Memo } from "effect-fc"
|
||||
import { useMemo, useOnce, useSubscribe } from "effect-fc/hooks"
|
||||
import { Subscribable, SubscriptionSubRef } from "effect-fc/types"
|
||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||
import { FaDeleteLeft } from "react-icons/fa6"
|
||||
import { TodosState } from "./TodosState.service"
|
||||
|
||||
|
||||
const StringTextAreaInput = TextAreaInput({ schema: Schema.String })
|
||||
const OptionalDateTimeInput = TextFieldInput({ optional: true, schema: DateTimeUtcFromZonedInput })
|
||||
|
||||
const makeTodo = makeUuid4.pipe(
|
||||
Effect.map(id => Domain.Todo.Todo.make({
|
||||
id,
|
||||
@@ -20,96 +27,86 @@ const makeTodo = makeUuid4.pipe(
|
||||
|
||||
|
||||
export type TodoProps = (
|
||||
| { readonly _tag: "new", readonly index?: never }
|
||||
| { readonly _tag: "edit", readonly index: number }
|
||||
| { readonly _tag: "new" }
|
||||
| { readonly _tag: "edit", readonly id: string }
|
||||
)
|
||||
|
||||
export class Todo extends Component.make(function* Todo(props: TodoProps) {
|
||||
export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
const state = yield* TodosState
|
||||
|
||||
const [ref, contentRef] = yield* Hook.useMemo(() => Match.value(props).pipe(
|
||||
Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)),
|
||||
Match.tag("edit", ({ index }) => Effect.succeed(SubscriptionSubRef.makeFromChunkRef(state.ref, index))),
|
||||
const { ref, indexRef, contentRef, completedAtRef } = yield* useMemo(() => Match.value(props).pipe(
|
||||
Match.tag("new", () => Effect.Do.pipe(
|
||||
Effect.bind("ref", () => Effect.andThen(makeTodo, SubscriptionRef.make)),
|
||||
Effect.let("indexRef", () => Subscribable.make({ get: Effect.succeed(-1), changes: Stream.empty })),
|
||||
)),
|
||||
Match.tag("edit", ({ id }) => Effect.Do.pipe(
|
||||
Effect.let("ref", () => state.getElementRef(id)),
|
||||
Effect.let("indexRef", () => state.getIndexSubscribable(id)),
|
||||
)),
|
||||
Match.exhaustive,
|
||||
|
||||
Effect.map(ref => [
|
||||
ref,
|
||||
SubscriptionSubRef.makeFromPath(ref, ["content"]),
|
||||
] as const),
|
||||
), [props._tag, props.index])
|
||||
Effect.let("contentRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["content"])),
|
||||
Effect.let("completedAtRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["completedAt"])),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
), [props._tag, props._tag === "edit" ? props.id : undefined])
|
||||
|
||||
const [index, size] = yield* useSubscribe(indexRef, state.sizeSubscribable)
|
||||
|
||||
const StringTextAreaInputFC = yield* StringTextAreaInput
|
||||
const OptionalDateTimeInputFC = yield* OptionalDateTimeInput
|
||||
|
||||
const [content, size] = yield* Hook.useSubscribeRefs(contentRef, state.sizeRef)
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<Flex direction="row" align="center" gap="2">
|
||||
<Box flexGrow="1">
|
||||
<TextArea
|
||||
value={content}
|
||||
onChange={e => Runtime.runSync(runtime)(Ref.set(contentRef, e.target.value))}
|
||||
/>
|
||||
</Box>
|
||||
<Flex direction="row" align="center" gap="2">
|
||||
<Box flexGrow="1">
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<StringTextAreaInputFC ref={contentRef} />
|
||||
|
||||
{props._tag === "edit" &&
|
||||
<Flex direction="column" justify="center" align="center" gap="1">
|
||||
<IconButton
|
||||
disabled={props.index <= 0}
|
||||
onClick={() => Runtime.runSync(runtime)(
|
||||
SubscriptionRef.updateEffect(state.ref, todos => Effect.gen(function*() {
|
||||
if (props.index <= 0) return yield* Option.none()
|
||||
return todos.pipe(
|
||||
Chunk.replace(props.index, yield* Chunk.get(todos, props.index - 1)),
|
||||
Chunk.replace(props.index - 1, yield* ref),
|
||||
)
|
||||
}))
|
||||
)}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</IconButton>
|
||||
<Flex direction="row" justify="center" align="center" gap="2">
|
||||
<OptionalDateTimeInputFC
|
||||
type="datetime-local"
|
||||
ref={completedAtRef}
|
||||
defaultValue={yield* useOnce(() => DateTime.now)}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
disabled={props.index >= size - 1}
|
||||
onClick={() => Runtime.runSync(runtime)(
|
||||
SubscriptionRef.updateEffect(state.ref, todos => Effect.gen(function*() {
|
||||
if (props.index >= size - 1) return yield* Option.none()
|
||||
return todos.pipe(
|
||||
Chunk.replace(props.index, yield* Chunk.get(todos, props.index + 1)),
|
||||
Chunk.replace(props.index + 1, yield* ref),
|
||||
)
|
||||
}))
|
||||
)}
|
||||
>
|
||||
<FaArrowDown />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
onClick={() => Runtime.runSync(runtime)(
|
||||
Ref.update(state.ref, Chunk.remove(props.index))
|
||||
)}
|
||||
>
|
||||
<FaDeleteLeft />
|
||||
</IconButton>
|
||||
{props._tag === "new" &&
|
||||
<Button
|
||||
onClick={() => ref.pipe(
|
||||
Effect.andThen(todo => Ref.update(state.ref, Chunk.prepend(todo))),
|
||||
Effect.andThen(makeTodo),
|
||||
Effect.andThen(todo => Ref.set(ref, todo)),
|
||||
Runtime.runSync(runtime),
|
||||
)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{props._tag === "new" &&
|
||||
<Flex direction="row" justify="center">
|
||||
<Button
|
||||
onClick={() => ref.pipe(
|
||||
Effect.andThen(todo => Ref.update(state.ref, Chunk.prepend(todo))),
|
||||
Effect.andThen(makeTodo),
|
||||
Effect.andThen(todo => Ref.set(ref, todo)),
|
||||
Runtime.runSync(runtime),
|
||||
)}
|
||||
{props._tag === "edit" &&
|
||||
<Flex direction="column" justify="center" align="center" gap="1">
|
||||
<IconButton
|
||||
disabled={index <= 0}
|
||||
onClick={() => Runtime.runSync(runtime)(state.moveLeft(props.id))}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<FaArrowUp />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={index >= size - 1}
|
||||
onClick={() => Runtime.runSync(runtime)(state.moveRight(props.id))}
|
||||
>
|
||||
<FaArrowDown />
|
||||
</IconButton>
|
||||
|
||||
<IconButton onClick={() => Runtime.runSync(runtime)(state.remove(props.id))}>
|
||||
<FaDeleteLeft />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
}).pipe(
|
||||
Memoized.memo
|
||||
) {}
|
||||
}).pipe(Memo.memo) {}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||
import { Chunk, Console, Effect } from "effect"
|
||||
import { Component, Hook } from "effect-fc"
|
||||
import { Component } from "effect-fc"
|
||||
import { useOnce, useSubscribe } from "effect-fc/hooks"
|
||||
import { Todo } from "./Todo"
|
||||
import { TodosState } from "./TodosState.service"
|
||||
|
||||
|
||||
export class Todos extends Component.make(function* Todos() {
|
||||
export class Todos extends Component.makeUntraced(function* Todos() {
|
||||
const state = yield* TodosState
|
||||
const [todos] = yield* Hook.useSubscribeRefs(state.ref)
|
||||
const [todos] = yield* useSubscribe(state.ref)
|
||||
|
||||
yield* Hook.useOnce(() => Effect.andThen(
|
||||
yield* useOnce(() => Effect.andThen(
|
||||
Console.log("Todos mounted"),
|
||||
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||
))
|
||||
@@ -23,8 +24,8 @@ export class Todos extends Component.make(function* Todos() {
|
||||
<Flex direction="column" align="stretch" gap="2" mt="2">
|
||||
<TodoFC _tag="new" />
|
||||
|
||||
{Chunk.map(todos, (v, k) =>
|
||||
<TodoFC key={v.id} _tag="edit" index={k} />
|
||||
{Chunk.map(todos, todo =>
|
||||
<TodoFC key={todo.id} _tag="edit" id={todo.id} />
|
||||
)}
|
||||
</Flex>
|
||||
</Container>
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Todo } from "@/domain"
|
||||
import { KeyValueStore } from "@effect/platform"
|
||||
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||
import { Chunk, Console, Effect, Option, Schema, Stream, SubscriptionRef } from "effect"
|
||||
import { SubscriptionSubRef } from "effect-fc/types"
|
||||
import { Subscribable, SubscriptionSubRef } from "effect-fc/types"
|
||||
|
||||
|
||||
export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||
effect: Effect.fn("TodosState")(function*(key: string) {
|
||||
scoped: Effect.fnUntraced(function*(key: string) {
|
||||
const kv = yield* KeyValueStore.KeyValueStore
|
||||
|
||||
const readFromLocalStorage = Console.log("Reading todos from local storage...").pipe(
|
||||
@@ -18,7 +18,6 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||
onNone: () => Effect.succeed(Chunk.empty()),
|
||||
}))
|
||||
)
|
||||
|
||||
const saveToLocalStorage = (todos: Chunk.Chunk<Todo.Todo>) => Effect.andThen(
|
||||
Console.log("Saving todos to local storage..."),
|
||||
Chunk.isNonEmpty(todos)
|
||||
@@ -32,8 +31,6 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||
)
|
||||
|
||||
const ref = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
||||
const sizeRef = SubscriptionSubRef.makeFromPath(ref, ["length"])
|
||||
|
||||
yield* Effect.forkScoped(ref.changes.pipe(
|
||||
Stream.debounce("500 millis"),
|
||||
Stream.runForEach(saveToLocalStorage),
|
||||
@@ -43,7 +40,54 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||
Effect.ignore,
|
||||
))
|
||||
|
||||
return { ref, sizeRef } as const
|
||||
const sizeSubscribable = Subscribable.make({
|
||||
get: Effect.andThen(ref, Chunk.size),
|
||||
get changes() { return Stream.map(ref.changes, Chunk.size) },
|
||||
})
|
||||
const getElementRef = (id: string) => SubscriptionSubRef.makeFromChunkFindFirst(ref, v => v.id === id)
|
||||
const getIndexSubscribable = (id: string) => Subscribable.make({
|
||||
get: Effect.flatMap(ref, Chunk.findFirstIndex(v => v.id === id)),
|
||||
get changes() { return Stream.flatMap(ref.changes, Chunk.findFirstIndex(v => v.id === id)) },
|
||||
})
|
||||
|
||||
const moveLeft = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.Do.pipe(
|
||||
Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === id)),
|
||||
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
||||
Effect.bind("previous", ({ index }) => Chunk.get(todos, index - 1)),
|
||||
Effect.andThen(({ todo, index, previous }) => index > 0
|
||||
? todos.pipe(
|
||||
Chunk.replace(index, previous),
|
||||
Chunk.replace(index - 1, todo),
|
||||
)
|
||||
: todos
|
||||
),
|
||||
))
|
||||
const moveRight = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.Do.pipe(
|
||||
Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === id)),
|
||||
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
||||
Effect.bind("next", ({ index }) => Chunk.get(todos, index + 1)),
|
||||
Effect.andThen(({ todo, index, next }) => index < Chunk.size(todos) - 1
|
||||
? todos.pipe(
|
||||
Chunk.replace(index, next),
|
||||
Chunk.replace(index + 1, todo),
|
||||
)
|
||||
: todos
|
||||
),
|
||||
))
|
||||
const remove = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.andThen(
|
||||
Chunk.findFirstIndex(todos, v => v.id === id),
|
||||
index => Chunk.remove(todos, index),
|
||||
))
|
||||
|
||||
return {
|
||||
ref,
|
||||
sizeSubscribable,
|
||||
getElementRef,
|
||||
getIndexSubscribable,
|
||||
moveLeft,
|
||||
moveRight,
|
||||
remove,
|
||||
} as const
|
||||
}),
|
||||
|
||||
dependencies: [BrowserKeyValueStore.layerLocalStorage],
|
||||
|
||||
Reference in New Issue
Block a user