Compare commits
25 Commits
master
...
8bee721ca4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bee721ca4 | ||
|
|
6b13932280 | ||
|
|
7ff331b062 | ||
|
|
0060bb792a | ||
|
|
bbfbcda12e | ||
|
|
cbb7530f19 | ||
|
|
5b6bb5863b | ||
|
|
a296d4874c | ||
|
|
2e914fb15b | ||
|
|
c53139d242 | ||
|
|
10f8a98b3f | ||
|
|
c74f3025d2 | ||
|
|
8e19774272 | ||
|
|
198df1f2f2 | ||
|
|
a4dca4281a | ||
|
|
507ee472ad | ||
|
|
a5eb50eec9 | ||
|
|
db822fe85d | ||
|
|
c98e5423cd | ||
|
|
51a8db3137 | ||
|
|
2ff38c435f | ||
|
|
a0a78187df | ||
|
|
71f8f4553d | ||
|
|
13c8b341c1 | ||
|
|
6c8f6622dd |
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
4
bun.lock
4
bun.lock
@@ -36,8 +36,10 @@
|
||||
"effect-fc": "workspace:*",
|
||||
"lucide-react": "^0.510.0",
|
||||
"mobx": "^6.13.7",
|
||||
"react-icons": "^5.5.0",
|
||||
},
|
||||
"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",
|
||||
@@ -724,6 +726,8 @@
|
||||
|
||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||
|
||||
"react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
|
||||
|
||||
152
packages/effect-fc/src/Component.ts
Normal file
152
packages/effect-fc/src/Component.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Context, Effect, ExecutionStrategy, Exit, Function, Pipeable, Ref, Runtime, Scope, String, Tracer, type Types, type Utils } from "effect"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
export interface Component<E, R, P extends {}> extends Pipeable.Pipeable {
|
||||
(props: P): Effect.Effect<React.ReactNode, E, R>
|
||||
readonly displayName?: string
|
||||
readonly options: Options
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
readonly finalizerExecutionMode: "sync" | "fork"
|
||||
readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy
|
||||
}
|
||||
|
||||
export type Error<T> = T extends Component<infer E, infer _R, infer _P> ? E : never
|
||||
export type Context<T> = T extends Component<infer _E, infer R, infer _P> ? R : never
|
||||
export type Props<T> = T extends Component<infer _E, infer _R, infer P> ? P : never
|
||||
|
||||
export const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||
|
||||
|
||||
export interface MakeOptions {
|
||||
readonly traced?: boolean
|
||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
}
|
||||
|
||||
export const make = <
|
||||
Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>,
|
||||
P extends {} = {},
|
||||
>(
|
||||
body: (props: P) => Generator<Eff, React.ReactNode, never>,
|
||||
options?: MakeOptions,
|
||||
): Component<
|
||||
[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,
|
||||
P
|
||||
> => {
|
||||
const displayName = !String.isEmpty(body.name) ? body.name : undefined
|
||||
|
||||
const f = ((options?.traced ?? true)
|
||||
? displayName
|
||||
? Effect.fn(displayName)(body)
|
||||
: Effect.fn(body)
|
||||
: Effect.fnUntraced(body)
|
||||
) as Component<any, any, any>
|
||||
|
||||
f.pipe = function pipe() { return Pipeable.pipeArguments(this, arguments) };
|
||||
(f as Types.Mutable<typeof f>).displayName = displayName;
|
||||
(f as Types.Mutable<typeof f>).options = {
|
||||
finalizerExecutionMode: options?.finalizerExecutionMode ?? "sync",
|
||||
finalizerExecutionStrategy: options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential,
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
|
||||
export const useFC: {
|
||||
<E, R, P extends {}>(
|
||||
self: Component<E, R, P>
|
||||
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fn("useFC")(function* <E, R, P extends {}>(
|
||||
self: Component<E, R, P>
|
||||
) {
|
||||
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
|
||||
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
return React.useMemo(() => function ScopeProvider(props: P) {
|
||||
const scope = useScope(runtimeRef.current, self.options)
|
||||
|
||||
const FC = React.useMemo(() => {
|
||||
const f = (props: P) => Runtime.runSync(runtimeRef.current)(
|
||||
Effect.provideService(self(props), Scope.Scope, scope)
|
||||
)
|
||||
f.displayName = self.displayName ?? "Anonymous"
|
||||
return f
|
||||
}, [scope])
|
||||
|
||||
return React.createElement(FC, props)
|
||||
}, Array.from(
|
||||
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
||||
))
|
||||
})
|
||||
|
||||
const useScope = (
|
||||
runtime: Runtime.Runtime<never>,
|
||||
options: Options,
|
||||
) => {
|
||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
|
||||
Effect.all([Ref.make(true), Scope.make(options.finalizerExecutionStrategy)])
|
||||
), [])
|
||||
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).pipe(
|
||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
||||
),
|
||||
})
|
||||
), [])
|
||||
|
||||
return scope
|
||||
}
|
||||
|
||||
const closeScope = (
|
||||
scope: Scope.CloseableScope,
|
||||
runtime: Runtime.Runtime<never>,
|
||||
options: Options,
|
||||
) => {
|
||||
switch (options.finalizerExecutionMode) {
|
||||
case "sync":
|
||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
||||
break
|
||||
case "fork":
|
||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const use: {
|
||||
<E, R, P extends {}>(
|
||||
self: Component<E, R, P>,
|
||||
fn: (Component: React.FC<P>) => React.ReactNode,
|
||||
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fn("use")(function*(self, fn) {
|
||||
return fn(yield* useFC(self))
|
||||
})
|
||||
|
||||
export const withRuntime: {
|
||||
<E, R, P extends {}>(
|
||||
context: React.Context<Runtime.Runtime<R>>,
|
||||
): (self: Component<E, R | Scope.Scope, P>) => React.FC<P>
|
||||
<E, R, P extends {}>(
|
||||
self: Component<E, R | Scope.Scope, P>,
|
||||
context: React.Context<Runtime.Runtime<R>>,
|
||||
): React.FC<P>
|
||||
} = Function.dual(2, <E, R, P extends {}>(
|
||||
self: Component<E, R | Scope.Scope, P>,
|
||||
context: React.Context<Runtime.Runtime<R>>,
|
||||
): React.FC<P> => function WithRuntime(props) {
|
||||
const runtime = React.useContext(context)
|
||||
return React.createElement(Runtime.runSync(runtime)(useFC(self)), props)
|
||||
})
|
||||
@@ -4,52 +4,8 @@ import { SetStateAction } from "./types/index.js"
|
||||
|
||||
|
||||
export interface ScopeOptions {
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||
}
|
||||
|
||||
|
||||
export const useScope: {
|
||||
(options?: ScopeOptions): Effect.Effect<Scope.Scope>
|
||||
} = Effect.fnUntraced(function* (options?: ScopeOptions) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
|
||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
|
||||
Effect.all([Ref.make(true), makeScope(options)])
|
||||
), [])
|
||||
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: () => makeScope(options).pipe(
|
||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
||||
),
|
||||
})
|
||||
), [])
|
||||
|
||||
return scope
|
||||
})
|
||||
|
||||
const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
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
|
||||
}
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +14,7 @@ export const useMemo: {
|
||||
factory: () => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<A, E, R>
|
||||
} = Effect.fnUntraced(function* <A, E, R>(
|
||||
} = Effect.fn("useMemo")(function* <A, E, R>(
|
||||
factory: () => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
@@ -68,7 +24,7 @@ export const useMemo: {
|
||||
|
||||
export const useOnce: {
|
||||
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||
} = Effect.fnUntraced(function* <A, E, R>(
|
||||
} = Effect.fn("useOnce")(function* <A, E, R>(
|
||||
factory: () => Effect.Effect<A, E, R>
|
||||
) {
|
||||
return yield* useMemo(factory, [])
|
||||
@@ -78,7 +34,7 @@ export const useMemoLayer: {
|
||||
<ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>
|
||||
): Effect.Effect<Context.Context<ROut>, E, RIn>
|
||||
} = Effect.fnUntraced(function* <ROut, E, RIn>(
|
||||
} = Effect.fn("useMemoLayer")(function* <ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>
|
||||
) {
|
||||
return yield* useMemo(() => Effect.provide(Effect.context<ROut>(), layer), [layer])
|
||||
@@ -90,7 +46,7 @@ export const useCallbackSync: {
|
||||
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>(
|
||||
} = Effect.fn("useCallbackSync")(function* <Args extends unknown[], A, E, R>(
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
@@ -103,7 +59,7 @@ export const useCallbackPromise: {
|
||||
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>(
|
||||
} = Effect.fn("useCallbackPromise")(function* <Args extends unknown[], A, E, R>(
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
@@ -118,7 +74,7 @@ export const useEffect: {
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function* <E, R>(
|
||||
} = Effect.fn("useEffect")(function* <E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
@@ -151,7 +107,7 @@ export const useLayoutEffect: {
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function* <E, R>(
|
||||
} = Effect.fn("useLayoutEffect")(function* <E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: ScopeOptions,
|
||||
@@ -184,7 +140,7 @@ export const useFork: {
|
||||
deps?: React.DependencyList,
|
||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function* <E, R>(
|
||||
} = Effect.fn("useFork")(function* <E, R>(
|
||||
effect: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||
@@ -214,7 +170,7 @@ export const useFork: {
|
||||
|
||||
export const useRefFromReactiveValue: {
|
||||
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
||||
} = Effect.fnUntraced(function*(value) {
|
||||
} = Effect.fn("useRefFromReactiveValue")(function*(value) {
|
||||
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
||||
yield* useEffect(() => Ref.set(ref, value), [value])
|
||||
return ref
|
||||
@@ -224,7 +180,7 @@ export const useSubscribeRefs: {
|
||||
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||
...refs: Refs
|
||||
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
|
||||
} = Effect.fnUntraced(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||
} = Effect.fn("useSubscribeRefs")(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||
...refs: Refs
|
||||
) {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
||||
@@ -246,7 +202,7 @@ 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>) {
|
||||
} = Effect.fn("useRefState")(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
||||
|
||||
yield* useFork(() => Stream.runForEach(
|
||||
@@ -268,7 +224,7 @@ 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) {
|
||||
} = 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)),
|
||||
@@ -297,7 +253,7 @@ export const useSubscribeStream: {
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue: A,
|
||||
): Effect.Effect<Option.Some<A>, never, R>
|
||||
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
|
||||
} = Effect.fn("useSubscribeStream")(function* <A extends NonNullable<unknown>, E, R>(
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue?: A,
|
||||
) {
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Context, Effect, Function, Runtime, Scope, Tracer } from "effect"
|
||||
import type { Mutable } from "effect/Types"
|
||||
import * as React from "react"
|
||||
import * as ReactHook from "./ReactHook.js"
|
||||
|
||||
|
||||
export interface ReactComponent<E, R, P> {
|
||||
(props: P): Effect.Effect<React.ReactNode, E, R>
|
||||
readonly displayName?: string
|
||||
}
|
||||
|
||||
export const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||
|
||||
export const withDisplayName: {
|
||||
<C extends ReactComponent<any, any, any>>(displayName: string): (self: C) => C
|
||||
<C extends ReactComponent<any, any, any>>(self: C, displayName: string): C
|
||||
} = Function.dual(2, <C extends ReactComponent<any, any, any>>(
|
||||
self: C,
|
||||
displayName: string,
|
||||
): C => {
|
||||
(self as Mutable<C>).displayName = displayName
|
||||
return self
|
||||
})
|
||||
|
||||
export const useFC: {
|
||||
<E, R, P extends {} = {}>(
|
||||
self: ReactComponent<E, R, P>,
|
||||
options?: ReactHook.ScopeOptions,
|
||||
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function* <E, R, P extends {}>(
|
||||
self: ReactComponent<E, R, P>,
|
||||
options?: ReactHook.ScopeOptions,
|
||||
) {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
|
||||
return React.useMemo(() => function ScopeProvider(props: P) {
|
||||
const scope = Runtime.runSync(runtime)(ReactHook.useScope(options))
|
||||
|
||||
const FC = React.useMemo(() => {
|
||||
const f = (props: P) => Runtime.runSync(runtime)(
|
||||
Effect.provideService(self(props), Scope.Scope, scope)
|
||||
)
|
||||
if (self.displayName) f.displayName = self.displayName
|
||||
return f
|
||||
}, [scope])
|
||||
|
||||
return React.createElement(FC, props)
|
||||
}, Array.from(
|
||||
Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values()
|
||||
))
|
||||
})
|
||||
|
||||
export const use: {
|
||||
<E, R, P extends {} = {}>(
|
||||
self: ReactComponent<E, R, P>,
|
||||
fn: (Component: React.FC<P>) => React.ReactNode,
|
||||
options?: ReactHook.ScopeOptions,
|
||||
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function*(self, fn, options) {
|
||||
return fn(yield* useFC(self, options))
|
||||
})
|
||||
|
||||
export const withRuntime: {
|
||||
<E, R, P extends {} = {}>(context: React.Context<Runtime.Runtime<R>>): (self: ReactComponent<E, R, P>) => React.FC<P>
|
||||
<E, R, P extends {} = {}>(self: ReactComponent<E, R, P>, context: React.Context<Runtime.Runtime<R>>): React.FC<P>
|
||||
} = Function.dual(2, <E, R, P extends {}>(
|
||||
self: ReactComponent<E, R, P>,
|
||||
context: React.Context<Runtime.Runtime<R>>,
|
||||
): React.FC<P> => function WithRuntime(props) {
|
||||
const runtime = React.useContext(context)
|
||||
return React.createElement(Runtime.runSync(runtime)(useFC(self)), props)
|
||||
})
|
||||
@@ -16,31 +16,31 @@ export const make = <R, ER>(
|
||||
})
|
||||
|
||||
|
||||
export interface AsyncProviderProps<R, ER> extends React.SuspenseProps {
|
||||
export interface ProviderProps<R, ER> extends React.SuspenseProps {
|
||||
readonly runtime: ReactManagedRuntime<R, ER>
|
||||
readonly children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function AsyncProvider<R, ER>(
|
||||
{ runtime, children, ...suspenseProps }: AsyncProviderProps<R, ER>
|
||||
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(AsyncProviderInner<R, ER>, { runtime, promise, children }),
|
||||
React.createElement(ProviderInner<R, ER>, { runtime, promise, children }),
|
||||
)
|
||||
}
|
||||
|
||||
interface AsyncProviderInnerProps<R, ER> {
|
||||
interface ProviderInnerProps<R, ER> {
|
||||
readonly runtime: ReactManagedRuntime<R, ER>
|
||||
readonly promise: Promise<Runtime.Runtime<R>>
|
||||
readonly children?: React.ReactNode
|
||||
}
|
||||
|
||||
function AsyncProviderInner<R, ER>(
|
||||
{ runtime, promise, children }: AsyncProviderInnerProps<R, ER>
|
||||
function ProviderInner<R, ER>(
|
||||
{ runtime, promise, children }: ProviderInnerProps<R, ER>
|
||||
): React.ReactNode {
|
||||
const value = React.use(promise)
|
||||
return React.createElement(runtime.context, { value }, children)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * as ReactComponent from "./ReactComponent.js"
|
||||
export * as ReactHook from "./ReactHook.js"
|
||||
export * as Component from "./Component.js"
|
||||
export * as Hook from "./Hook.js"
|
||||
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
||||
import { Chunk, Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
||||
import * as PropertyPath from "./PropertyPath.js"
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
||||
readonly setter: (parentValue: B, value: A) => B,
|
||||
) {
|
||||
super()
|
||||
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
||||
this.get = Effect.map(this.parent, this.getter)
|
||||
}
|
||||
|
||||
commit() {
|
||||
@@ -86,9 +86,11 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
||||
|
||||
export const makeFromGetSet = <A, B>(
|
||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||
getter: (parentValue: B) => A,
|
||||
setter: (parentValue: B, value: A) => B,
|
||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
|
||||
options: {
|
||||
readonly get: (parentValue: B) => A
|
||||
readonly set: (parentValue: B, value: A) => 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>,
|
||||
@@ -98,3 +100,12 @@ export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
||||
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>>,
|
||||
index: number,
|
||||
): SubscriptionSubRef<A, Chunk.Chunk<A>> => new SubscriptionSubRefImpl(
|
||||
parent,
|
||||
parentValue => Chunk.unsafeGet(parentValue, index),
|
||||
(parentValue, value) => Chunk.replace(parentValue, index, value),
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"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",
|
||||
@@ -36,9 +37,10 @@
|
||||
"@typed/id": "^0.17.2",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"effect": "^3.15.1",
|
||||
"effect-fc": "workspace:*",
|
||||
"lucide-react": "^0.510.0",
|
||||
"mobx": "^6.13.7",
|
||||
"effect-fc": "workspace:*"
|
||||
"react-icons": "^5.5.0"
|
||||
},
|
||||
"overrides": {
|
||||
"effect": "^3.15.1",
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
|
||||
import { useState } from "react"
|
||||
import { R } from "./reffuse"
|
||||
import { AppQueryErrorHandler } from "./services"
|
||||
|
||||
|
||||
export function VQueryErrorHandler() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const error = R.useSubscribeStream(
|
||||
R.useMemo(() => AppQueryErrorHandler.AppQueryErrorHandler.pipe(
|
||||
Effect.map(handler => handler.errors.pipe(
|
||||
Stream.changes,
|
||||
Stream.tap(Console.error),
|
||||
Stream.tap(() => Effect.sync(() => setOpen(true))),
|
||||
))
|
||||
), [])
|
||||
)
|
||||
|
||||
if (Option.isNone(error))
|
||||
return <></>
|
||||
|
||||
return (
|
||||
<AlertDialog.Root open={open}>
|
||||
<AlertDialog.Content maxWidth="450px">
|
||||
<AlertDialog.Title>Error</AlertDialog.Title>
|
||||
<AlertDialog.Description size="2">
|
||||
{Either.match(Cause.failureOrCause(error.value), {
|
||||
onLeft: flow(
|
||||
Match.value,
|
||||
Match.tag("RequestError", () => <Text>HTTP request error</Text>),
|
||||
Match.tag("ResponseError", () => <Text>HTTP response error</Text>),
|
||||
Match.exhaustive,
|
||||
),
|
||||
|
||||
onRight: flow(
|
||||
Cause.dieOption,
|
||||
Option.match({
|
||||
onSome: () => <Text>Unrecoverable defect</Text>,
|
||||
onNone: () => <Text>Unknown error</Text>,
|
||||
}),
|
||||
),
|
||||
})}
|
||||
</AlertDialog.Description>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<AlertDialog.Action>
|
||||
<Button variant="solid" color="red" onClick={() => setOpen(false)}>
|
||||
Ok
|
||||
</Button>
|
||||
</AlertDialog.Action>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,11 @@
|
||||
import { FetchHttpClient } from "@effect/platform"
|
||||
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
||||
import { Layer } from "effect"
|
||||
import { ReactManagedRuntime } from "effect-fc"
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { ReffuseRuntime } from "reffuse"
|
||||
import { RootContext } from "./reffuse"
|
||||
import { routeTree } from "./routeTree.gen"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
||||
import { runtime } from "./runtime"
|
||||
|
||||
|
||||
const layer = Layer.empty.pipe(
|
||||
Layer.provideMerge(AppQueryClient.AppQueryClient.Default),
|
||||
Layer.provideMerge(AppQueryErrorHandler.AppQueryErrorHandler.Default),
|
||||
Layer.provideMerge(Clipboard.layer),
|
||||
Layer.provideMerge(Geolocation.layer),
|
||||
Layer.provideMerge(Permissions.layer),
|
||||
Layer.provideMerge(FetchHttpClient.layer),
|
||||
)
|
||||
|
||||
const router = createRouter({ routeTree })
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
@@ -27,13 +14,10 @@ declare module "@tanstack/react-router" {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ReffuseRuntime.Provider>
|
||||
<RootContext.Provider layer={layer}>
|
||||
<RouterProvider router={router} />
|
||||
</RootContext.Provider>
|
||||
</ReffuseRuntime.Provider>
|
||||
<ReactManagedRuntime.Provider runtime={runtime}>
|
||||
<RouterProvider router={router} />
|
||||
</ReactManagedRuntime.Provider>
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { RootReffuse } from "@/reffuse"
|
||||
import { Reffuse, ReffuseContext } from "reffuse"
|
||||
import { Uuid4Query } from "./services"
|
||||
|
||||
|
||||
export const QueryContext = ReffuseContext.make<Uuid4Query.Uuid4Query>()
|
||||
|
||||
export const R = new class QueryReffuse extends RootReffuse.pipe(
|
||||
Reffuse.withContexts(QueryContext)
|
||||
) {}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { QueryRunner } from "@reffuse/extension-query"
|
||||
import { ParseResult, Schema } from "effect"
|
||||
|
||||
|
||||
export const Result = Schema.Array(Schema.String)
|
||||
|
||||
export class Uuid4Query extends QueryRunner.Tag("Uuid4Query")<Uuid4Query,
|
||||
readonly ["uuid4", number],
|
||||
typeof Result.Type,
|
||||
ParseResult.ParseError
|
||||
>() {}
|
||||
@@ -1 +0,0 @@
|
||||
export * as Uuid4Query from "./Uuid4Query"
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
||||
import * as AsyncData from "@typed/async-data"
|
||||
import { R } from "../reffuse"
|
||||
import { Uuid4Query } from "../services"
|
||||
|
||||
|
||||
export function Uuid4QueryService() {
|
||||
const runFork = R.useRunFork()
|
||||
|
||||
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
||||
const [state] = R.useSubscribeRefs(query.stateRef)
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Flex direction="column" align="center" gap="2">
|
||||
<Text>
|
||||
{AsyncData.match(state, {
|
||||
NoData: () => "No data yet",
|
||||
Loading: () => "Loading...",
|
||||
Success: (value, { isRefreshing, isOptimistic }) =>
|
||||
`Value: ${value} ${isRefreshing ? "(refreshing)" : ""} ${isOptimistic ? "(optimistic)" : ""}`,
|
||||
Failure: (cause, { isRefreshing }) =>
|
||||
`Error: ${cause} ${isRefreshing ? "(refreshing)" : ""}`,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button onClick={() => runFork(query.forkRefresh)}>Refresh</Button>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { HttpClient } from "@effect/platform"
|
||||
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
||||
import { QueryExtension } from "@reffuse/extension-query"
|
||||
import { Reffuse, ReffuseContext } from "reffuse"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
||||
|
||||
|
||||
export const RootContext = ReffuseContext.make<
|
||||
| AppQueryClient.AppQueryClient
|
||||
| AppQueryErrorHandler.AppQueryErrorHandler
|
||||
| Clipboard.Clipboard
|
||||
| Geolocation.Geolocation
|
||||
| Permissions.Permissions
|
||||
| HttpClient.HttpClient
|
||||
>()
|
||||
|
||||
export class RootReffuse extends Reffuse.Reffuse.pipe(
|
||||
Reffuse.withExtension(LazyRefExtension),
|
||||
Reffuse.withExtension(QueryExtension),
|
||||
Reffuse.withContexts(RootContext),
|
||||
) {}
|
||||
|
||||
export const R = new RootReffuse()
|
||||
@@ -8,397 +8,70 @@
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
// Import Routes
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as BlankRouteImport } from './routes/blank'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as TodosImport } from './routes/todos'
|
||||
import { Route as TimeImport } from './routes/time'
|
||||
import { Route as TestsImport } from './routes/tests'
|
||||
import { Route as PromiseImport } from './routes/promise'
|
||||
import { Route as LazyrefImport } from './routes/lazyref'
|
||||
import { Route as EffectComponentTestsImport } from './routes/effect-component-tests'
|
||||
import { Route as CountImport } from './routes/count'
|
||||
import { Route as BlankImport } from './routes/blank'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as StreamsPullImport } from './routes/streams/pull'
|
||||
import { Route as QueryUsequeryImport } from './routes/query/usequery'
|
||||
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
|
||||
import { Route as QueryServiceImport } from './routes/query/service'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const TodosRoute = TodosImport.update({
|
||||
id: '/todos',
|
||||
path: '/todos',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const TimeRoute = TimeImport.update({
|
||||
id: '/time',
|
||||
path: '/time',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const TestsRoute = TestsImport.update({
|
||||
id: '/tests',
|
||||
path: '/tests',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const PromiseRoute = PromiseImport.update({
|
||||
id: '/promise',
|
||||
path: '/promise',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const LazyrefRoute = LazyrefImport.update({
|
||||
id: '/lazyref',
|
||||
path: '/lazyref',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const EffectComponentTestsRoute = EffectComponentTestsImport.update({
|
||||
id: '/effect-component-tests',
|
||||
path: '/effect-component-tests',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const CountRoute = CountImport.update({
|
||||
id: '/count',
|
||||
path: '/count',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const BlankRoute = BlankImport.update({
|
||||
const BlankRoute = BlankRouteImport.update({
|
||||
id: '/blank',
|
||||
path: '/blank',
|
||||
getParentRoute: () => rootRoute,
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
const StreamsPullRoute = StreamsPullImport.update({
|
||||
id: '/streams/pull',
|
||||
path: '/streams/pull',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const QueryUsequeryRoute = QueryUsequeryImport.update({
|
||||
id: '/query/usequery',
|
||||
path: '/query/usequery',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const QueryUsemutationRoute = QueryUsemutationImport.update({
|
||||
id: '/query/usemutation',
|
||||
path: '/query/usemutation',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const QueryServiceRoute = QueryServiceImport.update({
|
||||
id: '/query/service',
|
||||
path: '/query/service',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/blank': {
|
||||
id: '/blank'
|
||||
path: '/blank'
|
||||
fullPath: '/blank'
|
||||
preLoaderRoute: typeof BlankImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/count': {
|
||||
id: '/count'
|
||||
path: '/count'
|
||||
fullPath: '/count'
|
||||
preLoaderRoute: typeof CountImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/effect-component-tests': {
|
||||
id: '/effect-component-tests'
|
||||
path: '/effect-component-tests'
|
||||
fullPath: '/effect-component-tests'
|
||||
preLoaderRoute: typeof EffectComponentTestsImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/lazyref': {
|
||||
id: '/lazyref'
|
||||
path: '/lazyref'
|
||||
fullPath: '/lazyref'
|
||||
preLoaderRoute: typeof LazyrefImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/promise': {
|
||||
id: '/promise'
|
||||
path: '/promise'
|
||||
fullPath: '/promise'
|
||||
preLoaderRoute: typeof PromiseImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/tests': {
|
||||
id: '/tests'
|
||||
path: '/tests'
|
||||
fullPath: '/tests'
|
||||
preLoaderRoute: typeof TestsImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/time': {
|
||||
id: '/time'
|
||||
path: '/time'
|
||||
fullPath: '/time'
|
||||
preLoaderRoute: typeof TimeImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/todos': {
|
||||
id: '/todos'
|
||||
path: '/todos'
|
||||
fullPath: '/todos'
|
||||
preLoaderRoute: typeof TodosImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/query/service': {
|
||||
id: '/query/service'
|
||||
path: '/query/service'
|
||||
fullPath: '/query/service'
|
||||
preLoaderRoute: typeof QueryServiceImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/query/usemutation': {
|
||||
id: '/query/usemutation'
|
||||
path: '/query/usemutation'
|
||||
fullPath: '/query/usemutation'
|
||||
preLoaderRoute: typeof QueryUsemutationImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/query/usequery': {
|
||||
id: '/query/usequery'
|
||||
path: '/query/usequery'
|
||||
fullPath: '/query/usequery'
|
||||
preLoaderRoute: typeof QueryUsequeryImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/streams/pull': {
|
||||
id: '/streams/pull'
|
||||
path: '/streams/pull'
|
||||
fullPath: '/streams/pull'
|
||||
preLoaderRoute: typeof StreamsPullImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/count': typeof CountRoute
|
||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
||||
'/lazyref': typeof LazyrefRoute
|
||||
'/promise': typeof PromiseRoute
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/todos': typeof TodosRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
'/streams/pull': typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/count': typeof CountRoute
|
||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
||||
'/lazyref': typeof LazyrefRoute
|
||||
'/promise': typeof PromiseRoute
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/todos': typeof TodosRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
'/streams/pull': typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/count': typeof CountRoute
|
||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
||||
'/lazyref': typeof LazyrefRoute
|
||||
'/promise': typeof PromiseRoute
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/todos': typeof TodosRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
'/streams/pull': typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/count'
|
||||
| '/effect-component-tests'
|
||||
| '/lazyref'
|
||||
| '/promise'
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/todos'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
| '/streams/pull'
|
||||
fullPaths: '/' | '/blank'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/count'
|
||||
| '/effect-component-tests'
|
||||
| '/lazyref'
|
||||
| '/promise'
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/todos'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
| '/streams/pull'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/count'
|
||||
| '/effect-component-tests'
|
||||
| '/lazyref'
|
||||
| '/promise'
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/todos'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
| '/streams/pull'
|
||||
to: '/' | '/blank'
|
||||
id: '__root__' | '/' | '/blank'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
BlankRoute: typeof BlankRoute
|
||||
CountRoute: typeof CountRoute
|
||||
EffectComponentTestsRoute: typeof EffectComponentTestsRoute
|
||||
LazyrefRoute: typeof LazyrefRoute
|
||||
PromiseRoute: typeof PromiseRoute
|
||||
TestsRoute: typeof TestsRoute
|
||||
TimeRoute: typeof TimeRoute
|
||||
TodosRoute: typeof TodosRoute
|
||||
QueryServiceRoute: typeof QueryServiceRoute
|
||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
||||
StreamsPullRoute: typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/blank': {
|
||||
id: '/blank'
|
||||
path: '/blank'
|
||||
fullPath: '/blank'
|
||||
preLoaderRoute: typeof BlankRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
BlankRoute: BlankRoute,
|
||||
CountRoute: CountRoute,
|
||||
EffectComponentTestsRoute: EffectComponentTestsRoute,
|
||||
LazyrefRoute: LazyrefRoute,
|
||||
PromiseRoute: PromiseRoute,
|
||||
TestsRoute: TestsRoute,
|
||||
TimeRoute: TimeRoute,
|
||||
TodosRoute: TodosRoute,
|
||||
QueryServiceRoute: QueryServiceRoute,
|
||||
QueryUsemutationRoute: QueryUsemutationRoute,
|
||||
QueryUsequeryRoute: QueryUsequeryRoute,
|
||||
StreamsPullRoute: StreamsPullRoute,
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
/* ROUTE_MANIFEST_START
|
||||
{
|
||||
"routes": {
|
||||
"__root__": {
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/blank",
|
||||
"/count",
|
||||
"/effect-component-tests",
|
||||
"/lazyref",
|
||||
"/promise",
|
||||
"/tests",
|
||||
"/time",
|
||||
"/todos",
|
||||
"/query/service",
|
||||
"/query/usemutation",
|
||||
"/query/usequery",
|
||||
"/streams/pull"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
"filePath": "index.tsx"
|
||||
},
|
||||
"/blank": {
|
||||
"filePath": "blank.tsx"
|
||||
},
|
||||
"/count": {
|
||||
"filePath": "count.tsx"
|
||||
},
|
||||
"/effect-component-tests": {
|
||||
"filePath": "effect-component-tests.tsx"
|
||||
},
|
||||
"/lazyref": {
|
||||
"filePath": "lazyref.tsx"
|
||||
},
|
||||
"/promise": {
|
||||
"filePath": "promise.tsx"
|
||||
},
|
||||
"/tests": {
|
||||
"filePath": "tests.tsx"
|
||||
},
|
||||
"/time": {
|
||||
"filePath": "time.tsx"
|
||||
},
|
||||
"/todos": {
|
||||
"filePath": "todos.tsx"
|
||||
},
|
||||
"/query/service": {
|
||||
"filePath": "query/service.tsx"
|
||||
},
|
||||
"/query/usemutation": {
|
||||
"filePath": "query/usemutation.tsx"
|
||||
},
|
||||
"/query/usequery": {
|
||||
"filePath": "query/usequery.tsx"
|
||||
},
|
||||
"/streams/pull": {
|
||||
"filePath": "streams/pull.tsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
ROUTE_MANIFEST_END */
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VQueryErrorHandler } from "@/VQueryErrorHandler"
|
||||
import { Container, Flex, Theme } from "@radix-ui/themes"
|
||||
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
|
||||
@@ -14,21 +13,15 @@ export const Route = createRootRoute({
|
||||
function Root() {
|
||||
return (
|
||||
<Theme>
|
||||
<Container>
|
||||
<Container mb="4">
|
||||
<Flex direction="row" justify="center" align="center" gap="2">
|
||||
<Link to="/">Index</Link>
|
||||
<Link to="/time">Time</Link>
|
||||
<Link to="/count">Count</Link>
|
||||
<Link to="/tests">Tests</Link>
|
||||
<Link to="/promise">Promise</Link>
|
||||
<Link to="/query/usequery">Query</Link>
|
||||
<Link to="/blank">Blank</Link>
|
||||
</Flex>
|
||||
</Container>
|
||||
|
||||
<Outlet />
|
||||
|
||||
<VQueryErrorHandler />
|
||||
<TanStackRouterDevtools />
|
||||
</Theme>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
export const Route = createFileRoute('/blank')({
|
||||
component: RouteComponent,
|
||||
|
||||
export const Route = createFileRoute("/blank")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/blank"!</div>
|
||||
return <div>Hello "/blank"!</div>
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Effect, Ref } from "effect"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/count")({
|
||||
component: Count
|
||||
})
|
||||
|
||||
function Count() {
|
||||
|
||||
const runSync = R.useRunSync()
|
||||
|
||||
const countRef = R.useRef(() => Effect.succeed(0))
|
||||
const [count] = R.useSubscribeRefs(countRef)
|
||||
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<button onClick={() => runSync(Ref.update(countRef, count => count + 1))}>
|
||||
count is {count}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Box, TextField } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Array, Console, Effect, Layer, pipe, Ref, Runtime, SubscriptionRef } from "effect"
|
||||
import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components"
|
||||
|
||||
|
||||
const LogLive = Layer.scopedDiscard(Effect.acquireRelease(
|
||||
Console.log("Runtime built."),
|
||||
() => Console.log("Runtime destroyed."),
|
||||
))
|
||||
|
||||
class TestService extends Effect.Service<TestService>()("TestService", {
|
||||
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
|
||||
}) {}
|
||||
|
||||
class SubService extends Effect.Service<SubService>()("SubService", {
|
||||
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("subvalue")),
|
||||
}) {}
|
||||
|
||||
const runtime = ReactManagedRuntime.make(Layer.empty.pipe(
|
||||
Layer.provideMerge(LogLive),
|
||||
Layer.provideMerge(TestService.Default),
|
||||
))
|
||||
|
||||
|
||||
export const Route = createFileRoute("/effect-component-tests")({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<ReactManagedRuntime.AsyncProvider runtime={runtime}>
|
||||
<MyRoute />
|
||||
</ReactManagedRuntime.AsyncProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const MyRoute = pipe(
|
||||
Effect.fn(function*() {
|
||||
const runtime = yield* Effect.runtime()
|
||||
|
||||
const service = yield* TestService
|
||||
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
|
||||
|
||||
// const MyTestComponentFC = yield* Effect.provide(
|
||||
// ReactComponent.useFC(MyTestComponent),
|
||||
// yield* ReactHook.useMemoLayer(SubService.Default),
|
||||
// )
|
||||
|
||||
return <>
|
||||
<Box>
|
||||
<TextField.Root
|
||||
value={value}
|
||||
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* {yield* ReactComponent.use(MyTestComponent, C => <C />).pipe(
|
||||
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default))
|
||||
)} */}
|
||||
|
||||
{/* {Array.range(0, 3).map(k =>
|
||||
<MyTestComponentFC key={k} />
|
||||
)} */}
|
||||
|
||||
{yield* pipe(
|
||||
Array.range(0, 3),
|
||||
Array.map(k => ReactComponent.use(MyTestComponent, FC =>
|
||||
<FC key={k} />
|
||||
)),
|
||||
Effect.all,
|
||||
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default)),
|
||||
)}
|
||||
</>
|
||||
}),
|
||||
|
||||
ReactComponent.withDisplayName("MyRoute"),
|
||||
ReactComponent.withRuntime(runtime.context),
|
||||
)
|
||||
|
||||
|
||||
const MyTestComponent = pipe(
|
||||
Effect.fn(function*() {
|
||||
const runtime = yield* Effect.runtime()
|
||||
|
||||
const service = yield* SubService
|
||||
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
|
||||
|
||||
// yield* ReactHook.useMemo(() => Effect.andThen(
|
||||
// Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
|
||||
// Console.log("MyTestComponent mounted"),
|
||||
// ), [])
|
||||
|
||||
return <>
|
||||
<Box>
|
||||
<TextField.Root
|
||||
value={value}
|
||||
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
}),
|
||||
|
||||
ReactComponent.withDisplayName("MyTestComponent"),
|
||||
)
|
||||
@@ -1,10 +1,22 @@
|
||||
import { runtime } from "@/runtime"
|
||||
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"
|
||||
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: RouteComponent
|
||||
const TodosStateLive = TodosState.Default("todos")
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: Component.make(function* Index() {
|
||||
return yield* Effect.provide(
|
||||
Component.use(Todos, Todos => <Todos />),
|
||||
yield* Hook.useMemoLayer(TodosStateLive),
|
||||
)
|
||||
}, {
|
||||
finalizerExecutionMode: "fork"
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context),
|
||||
)
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/"!</div>
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { Button, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import * as LazyRef from "@typed/lazy-ref"
|
||||
import { Suspense, use } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/lazyref")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const promise = R.usePromise(() => LazyRef.of(0), [])
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Text>Loading...</Text>}>
|
||||
<LazyRefComponent promise={promise} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
function LazyRefComponent({ promise }: { readonly promise: Promise<LazyRef.LazyRef<number>> }) {
|
||||
const ref = use(promise)
|
||||
const [value, setValue] = R.useLazyRefState(ref)
|
||||
|
||||
return (
|
||||
<Button onClick={() => setValue(prev => prev + 1)}>
|
||||
{value}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { HttpClient } from "@effect/platform"
|
||||
import { Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Schema } from "effect"
|
||||
import { Suspense, use } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/promise")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
|
||||
const Result = Schema.Tuple(Schema.String)
|
||||
type Result = typeof Result.Type
|
||||
|
||||
function RouteComponent() {
|
||||
const promise = R.usePromise(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
||||
Effect.andThen(HttpClient.get("https://www.uuidtools.com/api/generate/v4")),
|
||||
HttpClient.withTracerPropagation(false),
|
||||
Effect.flatMap(res => res.json),
|
||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||
), [])
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Text>Loading...</Text>}>
|
||||
<AsyncComponent promise={promise} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
function AsyncComponent({ promise }: { readonly promise: Promise<Result> }) {
|
||||
const [uuid] = use(promise)
|
||||
return <Text>{uuid}</Text>
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { QueryContext } from "@/query/reffuse"
|
||||
import { Uuid4Query } from "@/query/services"
|
||||
import { Uuid4QueryService } from "@/query/views/Uuid4QueryService"
|
||||
import { R } from "@/reffuse"
|
||||
import { HttpClient } from "@effect/platform"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Layer, Schema } from "effect"
|
||||
import { useMemo } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/query/service")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const query = R.useQuery({
|
||||
key: R.useStreamFromReactiveValues(["uuid4", 10 as number]),
|
||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||
Effect.andThen(Effect.sleep("500 millis")),
|
||||
Effect.andThen(Effect.map(
|
||||
HttpClient.HttpClient,
|
||||
HttpClient.withTracerPropagation(false),
|
||||
)),
|
||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
Effect.flatMap(res => res.json),
|
||||
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
|
||||
Effect.scoped,
|
||||
),
|
||||
})
|
||||
|
||||
const layer = useMemo(() => Layer.succeed(Uuid4Query.Uuid4Query, query), [query])
|
||||
|
||||
return (
|
||||
<QueryContext.Provider layer={layer}>
|
||||
<Uuid4QueryService />
|
||||
</QueryContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { HttpClient } from "@effect/platform"
|
||||
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
|
||||
import { QueryProgress } from "@reffuse/extension-query"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import * as AsyncData from "@typed/async-data"
|
||||
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
|
||||
import { useState } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/query/usemutation")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
|
||||
const Result = Schema.Array(Schema.String)
|
||||
|
||||
function RouteComponent() {
|
||||
const runFork = R.useRunFork()
|
||||
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
const mutation = R.useMutation({
|
||||
mutation: ([count]: readonly [count: number]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||
Effect.andThen(QueryProgress.QueryProgress.update(() =>
|
||||
AsyncData.Progress.make({ loaded: 0, total: Option.some(100) })
|
||||
)),
|
||||
Effect.andThen(Effect.sleep("500 millis")),
|
||||
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
|
||||
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
|
||||
)),
|
||||
Effect.andThen(Effect.map(
|
||||
HttpClient.HttpClient,
|
||||
HttpClient.withTracerPropagation(false),
|
||||
)),
|
||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
Effect.flatMap(res => res.json),
|
||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||
Effect.scoped,
|
||||
)
|
||||
})
|
||||
|
||||
const [state] = R.useSubscribeRefs(mutation.stateRef)
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Flex direction="column" align="center" gap="2">
|
||||
<Slider
|
||||
min={1}
|
||||
max={100}
|
||||
value={[count]}
|
||||
onValueChange={flow(
|
||||
Array.head,
|
||||
Option.getOrThrow,
|
||||
setCount,
|
||||
)}
|
||||
/>
|
||||
|
||||
<Text>
|
||||
{AsyncData.match(state, {
|
||||
NoData: () => "No data yet",
|
||||
Loading: progress =>
|
||||
`Loading...
|
||||
${ Option.match(progress, {
|
||||
onSome: ({ loaded, total }) => ` (${ loaded }/${ Option.getOrElse(total, () => "unknown") })`,
|
||||
onNone: () => "",
|
||||
}) }`,
|
||||
Success: value => `Value: ${ value }`,
|
||||
Failure: cause => `Error: ${ cause }`,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button onClick={() => mutation.forkMutate(count).pipe(
|
||||
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
|
||||
Effect.andThen(Console.log("Mutation done.")),
|
||||
runFork,
|
||||
)}>
|
||||
Get
|
||||
</Button>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { HttpClient } from "@effect/platform"
|
||||
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import * as AsyncData from "@typed/async-data"
|
||||
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
|
||||
import { useState } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/query/usequery")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
|
||||
const Result = Schema.Array(Schema.String)
|
||||
|
||||
function RouteComponent() {
|
||||
const runFork = R.useRunFork()
|
||||
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
const query = R.useQuery({
|
||||
key: R.useStreamFromReactiveValues(["uuid4", count]),
|
||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||
Effect.andThen(Effect.sleep("500 millis")),
|
||||
Effect.andThen(Effect.map(
|
||||
HttpClient.HttpClient,
|
||||
HttpClient.withTracerPropagation(false),
|
||||
)),
|
||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
Effect.flatMap(res => res.json),
|
||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||
Effect.scoped,
|
||||
),
|
||||
})
|
||||
|
||||
const [state] = R.useSubscribeRefs(query.stateRef)
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Flex direction="column" align="center" gap="2">
|
||||
<Slider
|
||||
min={1}
|
||||
max={100}
|
||||
value={[count]}
|
||||
onValueChange={flow(
|
||||
Array.head,
|
||||
Option.getOrThrow,
|
||||
setCount,
|
||||
)}
|
||||
/>
|
||||
|
||||
<Text>
|
||||
{AsyncData.match(state, {
|
||||
NoData: () => "No data yet",
|
||||
Loading: () => "Loading...",
|
||||
Success: (value, { isRefreshing, isOptimistic }) =>
|
||||
`Value: ${value} ${isRefreshing ? "(refreshing)" : ""} ${isOptimistic ? "(optimistic)" : ""}`,
|
||||
Failure: (cause, { isRefreshing }) =>
|
||||
`Error: ${cause} ${isRefreshing ? "(refreshing)" : ""}`,
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
onClick={() => query.forkRefresh.pipe(
|
||||
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
|
||||
Effect.andThen(Console.log("Refresh finished or stopped")),
|
||||
runFork,
|
||||
)}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Chunk, Effect, Exit, Option, Queue, Random, Scope, Stream } from "effect"
|
||||
import { useMemo, useState } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/streams/pull")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const stream = useMemo(() => Stream.repeatEffect(Random.nextInt), [])
|
||||
const streamScope = R.useScope([stream], { finalizerExecutionMode: "fork" })
|
||||
|
||||
const queue = R.useMemo(() => Effect.provideService(Stream.toQueueOfElements(stream), Scope.Scope, streamScope), [streamScope])
|
||||
|
||||
const [value, setValue] = useState(Option.none<number>())
|
||||
const pullLatest = R.useCallbackSync(() => Queue.takeAll(queue).pipe(
|
||||
Effect.flatMap(Chunk.last),
|
||||
Effect.flatMap(Exit.matchEffect({
|
||||
onSuccess: Effect.succeed,
|
||||
onFailure: Effect.fail,
|
||||
})),
|
||||
Effect.tap(v => Effect.sync(() => setValue(Option.some(v)))),
|
||||
), [queue])
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center" gap="2">
|
||||
{Option.isSome(value) && <Text>{value.value}</Text>}
|
||||
<Button onClick={pullLatest}>Pull latest</Button>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Console, Effect, Option } from "effect"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
|
||||
interface Node {
|
||||
value: string
|
||||
left?: Leaf
|
||||
right?: Leaf
|
||||
}
|
||||
interface Leaf {
|
||||
node: Node
|
||||
}
|
||||
|
||||
|
||||
const makeUuid = Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)
|
||||
|
||||
|
||||
export const Route = createFileRoute("/tests")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const runSync = R.useRunSync()
|
||||
|
||||
const [uuid, setUuid] = useState(R.useMemo(() => makeUuid, []))
|
||||
const generateUuid = R.useCallbackSync(() => makeUuid.pipe(
|
||||
Effect.tap(v => Effect.sync(() => setUuid(v)))
|
||||
), [])
|
||||
|
||||
const uuidStream = R.useStreamFromReactiveValues([uuid])
|
||||
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
|
||||
|
||||
const [, scopeLayer] = R.useScope([uuid])
|
||||
|
||||
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
|
||||
Effect.andThen(Console.log("Scope changed")),
|
||||
Effect.provide(scopeLayer),
|
||||
runSync,
|
||||
), [scopeLayer, runSync])
|
||||
|
||||
|
||||
const nodeRef = R.useRef(() => Effect.succeed<Node>({ value: "prout" }))
|
||||
const nodeValueRef = R.useSubRefFromPath(nodeRef, ["value"])
|
||||
|
||||
|
||||
return (
|
||||
<Flex direction="column" justify="center" align="center" gap="2">
|
||||
<Text>{uuid}</Text>
|
||||
<Button onClick={generateUuid}>Generate UUID</Button>
|
||||
<Text>
|
||||
{Option.match(uuidStreamLatestValue, {
|
||||
onSome: ([v]) => v,
|
||||
onNone: () => <></>,
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, DateTime, Effect, Ref, Schedule, Stream, SubscriptionRef } from "effect"
|
||||
|
||||
|
||||
const timeEverySecond = Stream.repeatEffectWithSchedule(
|
||||
DateTime.now,
|
||||
Schedule.intersect(Schedule.forever, Schedule.spaced("1 second")),
|
||||
)
|
||||
|
||||
|
||||
export const Route = createFileRoute("/time")({
|
||||
component: Time
|
||||
})
|
||||
|
||||
function Time() {
|
||||
|
||||
const timeRef = R.useMemo(() => DateTime.now.pipe(Effect.flatMap(SubscriptionRef.make)), [])
|
||||
|
||||
R.useFork(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
||||
Effect.andThen(Stream.runForEach(timeEverySecond, v => Ref.set(timeRef, v)))
|
||||
), [timeRef])
|
||||
|
||||
const [time] = R.useRefState(timeRef)
|
||||
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<p className="text-center">
|
||||
{DateTime.format(time, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { TodosContext } from "@/todos/reffuse"
|
||||
import { TodosState } from "@/todos/services"
|
||||
import { VTodos } from "@/todos/views/VTodos"
|
||||
import { Container } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Layer } from "effect"
|
||||
import { useMemo } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/todos")({
|
||||
component: Todos
|
||||
})
|
||||
|
||||
function Todos() {
|
||||
|
||||
const todosLayer = useMemo(() => Layer.empty.pipe(
|
||||
Layer.provideMerge(TodosState.make("todos")),
|
||||
|
||||
Layer.merge(Layer.effectDiscard(
|
||||
Effect.addFinalizer(() => Console.log("TodosContext cleaned up")).pipe(
|
||||
Effect.andThen(Console.log("TodosContext constructed"))
|
||||
)
|
||||
)),
|
||||
), [])
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TodosContext.Provider layer={todosLayer} finalizerExecutionMode="fork">
|
||||
<VTodos />
|
||||
</TodosContext.Provider>
|
||||
</Container>
|
||||
)
|
||||
|
||||
}
|
||||
14
packages/example/src/runtime.ts
Normal file
14
packages/example/src/runtime.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { FetchHttpClient } from "@effect/platform"
|
||||
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||
import { Layer } from "effect"
|
||||
import { ReactManagedRuntime } from "effect-fc"
|
||||
|
||||
|
||||
export const AppLive = Layer.empty.pipe(
|
||||
Layer.provideMerge(Clipboard.layer),
|
||||
Layer.provideMerge(Geolocation.layer),
|
||||
Layer.provideMerge(Permissions.layer),
|
||||
Layer.provideMerge(FetchHttpClient.layer),
|
||||
)
|
||||
|
||||
export const runtime = ReactManagedRuntime.make(AppLive)
|
||||
@@ -1,7 +0,0 @@
|
||||
import { QueryClient } from "@reffuse/extension-query"
|
||||
import * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
||||
|
||||
|
||||
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({
|
||||
errorHandler: AppQueryErrorHandler.AppQueryErrorHandler
|
||||
}) {}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { HttpClientError } from "@effect/platform"
|
||||
import { QueryErrorHandler } from "@reffuse/extension-query"
|
||||
import { Effect } from "effect"
|
||||
|
||||
|
||||
export class AppQueryErrorHandler extends Effect.Service<AppQueryErrorHandler>()("AppQueryErrorHandler", {
|
||||
effect: QueryErrorHandler.make<HttpClientError.HttpClientError>()(
|
||||
(self, failure, defect) => self.pipe(
|
||||
Effect.catchTag("RequestError", "ResponseError", failure),
|
||||
Effect.catchAllDefect(defect),
|
||||
)
|
||||
)
|
||||
}) {}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * as AppQueryClient from "./AppQueryClient"
|
||||
export * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
||||
96
packages/example/src/todo/Todo.tsx
Normal file
96
packages/example/src/todo/Todo.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as Domain from "@/domain"
|
||||
import { Box, Button, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, Effect, Match, Option, Ref, Runtime, SubscriptionRef } from "effect"
|
||||
import { Component, Hook } from "effect-fc"
|
||||
import { SubscriptionSubRef } from "effect-fc/types"
|
||||
import * as React from "react"
|
||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||
import { FaDeleteLeft } from "react-icons/fa6"
|
||||
import { TodosState } from "./TodosState.service"
|
||||
|
||||
|
||||
const makeTodo = makeUuid4.pipe(
|
||||
Effect.map(id => Domain.Todo.Todo.make({
|
||||
id,
|
||||
content: "",
|
||||
completedAt: Option.none(),
|
||||
})),
|
||||
Effect.provide(GetRandomValues.CryptoRandom),
|
||||
)
|
||||
|
||||
|
||||
export type TodoProps = (
|
||||
| {
|
||||
readonly _tag: "new"
|
||||
readonly index?: never
|
||||
}
|
||||
| {
|
||||
readonly _tag: "edit"
|
||||
readonly index: number
|
||||
}
|
||||
)
|
||||
|
||||
export const Todo = Component.make(function* Todo(props: TodoProps) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
const state = yield* TodosState
|
||||
|
||||
const ref = 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))),
|
||||
Match.exhaustive,
|
||||
), [props._tag, props.index])
|
||||
|
||||
const contentRef = React.useMemo(() => SubscriptionSubRef.makeFromPath(ref, ["content"]), [ref])
|
||||
const [todo] = yield* Hook.useSubscribeRefs(ref)
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<Flex direction="row" align="center" gap="2">
|
||||
<Box flexGrow="1">
|
||||
<TextArea
|
||||
value={todo.content}
|
||||
onChange={e => Runtime.runSync(runtime)(Ref.set(contentRef, e.target.value))}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{props._tag === "edit" &&
|
||||
<Flex direction="column" justify="center" align="center" gap="1">
|
||||
<IconButton
|
||||
>
|
||||
<FaArrowUp />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
>
|
||||
<FaArrowDown />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
onClick={() => Runtime.runSync(runtime)(
|
||||
Ref.update(state.ref, Chunk.remove(props.index))
|
||||
)}
|
||||
>
|
||||
<FaDeleteLeft />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
|
||||
{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),
|
||||
)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
})
|
||||
27
packages/example/src/todo/Todos.tsx
Normal file
27
packages/example/src/todo/Todos.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||
import { Chunk } from "effect"
|
||||
import { Component, Hook } from "effect-fc"
|
||||
import { Todo } from "./Todo"
|
||||
import { TodosState } from "./TodosState.service"
|
||||
|
||||
|
||||
export const Todos = Component.make(function* Todos() {
|
||||
const state = yield* TodosState
|
||||
const [todos] = yield* Hook.useSubscribeRefs(state.ref)
|
||||
|
||||
const VTodo = yield* Component.useFC(Todo)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Heading align="center">Todos</Heading>
|
||||
|
||||
<Flex direction="column" align="stretch" gap="2" mt="2">
|
||||
<VTodo _tag="new" />
|
||||
|
||||
{Chunk.map(todos, (v, k) =>
|
||||
<VTodo key={v.id} _tag="edit" index={k} />
|
||||
)}
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
48
packages/example/src/todo/TodosState.service.ts
Normal file
48
packages/example/src/todo/TodosState.service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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"
|
||||
|
||||
|
||||
export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||
effect: Effect.fn("TodosState")(function*(key: string) {
|
||||
const kv = yield* KeyValueStore.KeyValueStore
|
||||
|
||||
const readFromLocalStorage = Console.log("Reading todos from local storage...").pipe(
|
||||
Effect.andThen(kv.get(key)),
|
||||
Effect.andThen(Option.match({
|
||||
onSome: Schema.decode(
|
||||
Schema.parseJson(Schema.Chunk(Todo.TodoFromJson))
|
||||
),
|
||||
onNone: () => Effect.succeed(Chunk.empty()),
|
||||
}))
|
||||
)
|
||||
|
||||
const saveToLocalStorage = (todos: Chunk.Chunk<Todo.Todo>) => Effect.andThen(
|
||||
Console.log("Saving todos to local storage..."),
|
||||
Chunk.isNonEmpty(todos)
|
||||
? Effect.andThen(
|
||||
Schema.encode(
|
||||
Schema.parseJson(Schema.Chunk(Todo.TodoFromJson))
|
||||
)(todos),
|
||||
v => kv.set(key, v),
|
||||
)
|
||||
: kv.remove(key)
|
||||
)
|
||||
|
||||
const ref = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
||||
|
||||
yield* Effect.forkScoped(ref.changes.pipe(
|
||||
Stream.debounce("500 millis"),
|
||||
Stream.runForEach(saveToLocalStorage),
|
||||
))
|
||||
yield* Effect.addFinalizer(() => ref.pipe(
|
||||
Effect.andThen(saveToLocalStorage),
|
||||
Effect.ignore,
|
||||
))
|
||||
|
||||
return { ref } as const
|
||||
}),
|
||||
|
||||
dependencies: [BrowserKeyValueStore.layerLocalStorage],
|
||||
}) {}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { RootReffuse } from "@/reffuse"
|
||||
import { Reffuse, ReffuseContext } from "reffuse"
|
||||
import { TodosState } from "./services"
|
||||
|
||||
|
||||
export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
|
||||
|
||||
export const R = new class TodosReffuse extends RootReffuse.pipe(
|
||||
Reffuse.withContexts(TodosContext)
|
||||
) {}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Todo } from "@/domain"
|
||||
import { KeyValueStore } from "@effect/platform"
|
||||
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||
import { PlatformError } from "@effect/platform/Error"
|
||||
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
|
||||
|
||||
|
||||
export class TodosState extends Context.Tag("TodosState")<TodosState, {
|
||||
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
|
||||
readonly load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||
readonly save: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||
}>() {}
|
||||
|
||||
|
||||
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
|
||||
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
|
||||
Effect.flatMap(kv => kv.get(key)),
|
||||
Effect.flatMap(identity),
|
||||
Effect.flatMap(Schema.decode(
|
||||
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
||||
)),
|
||||
Effect.catchTag("NoSuchElementException", () => Effect.succeed(Chunk.empty<Todo.Todo>())),
|
||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||
)
|
||||
|
||||
const writeToLocalStorage = (values: Chunk.Chunk<Todo.Todo>) => KeyValueStore.KeyValueStore.pipe(
|
||||
Effect.flatMap(kv => values.pipe(
|
||||
Schema.encode(
|
||||
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
||||
),
|
||||
Effect.flatMap(v => kv.set(key, v)),
|
||||
)),
|
||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||
)
|
||||
|
||||
const todos = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
||||
const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v))
|
||||
const save = Effect.flatMap(todos, writeToLocalStorage)
|
||||
|
||||
// Sync changes with local storage
|
||||
yield* Effect.forkScoped(Stream.runForEach(todos.changes, writeToLocalStorage))
|
||||
|
||||
return { todos, load, save }
|
||||
}))
|
||||
@@ -1 +0,0 @@
|
||||
export * as TodosState from "./TodosState"
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Todo } from "@/domain"
|
||||
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, Effect, Option, Ref } from "effect"
|
||||
import { R } from "../reffuse"
|
||||
import { TodosState } from "../services"
|
||||
|
||||
|
||||
const createEmptyTodo = makeUuid4.pipe(
|
||||
Effect.map(id => Todo.Todo.make({ id, content: "", completedAt: Option.none()}, true)),
|
||||
Effect.provide(GetRandomValues.CryptoRandom),
|
||||
)
|
||||
|
||||
|
||||
export function VNewTodo() {
|
||||
|
||||
const todoRef = R.useRef(() => createEmptyTodo)
|
||||
const [content, setContent] = R.useRefState(R.useSubRefFromPath(todoRef, ["content"]))
|
||||
|
||||
const add = R.useCallbackSync(() => Effect.all([TodosState.TodosState, todoRef]).pipe(
|
||||
Effect.flatMap(([state, todo]) => Ref.update(state.todos, Chunk.prepend(todo))),
|
||||
Effect.andThen(createEmptyTodo),
|
||||
Effect.flatMap(v => Ref.set(todoRef, v)),
|
||||
), [todoRef])
|
||||
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Card>
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<TextArea
|
||||
value={content}
|
||||
onChange={e => setContent(e.target.value)}
|
||||
/>
|
||||
|
||||
<Flex direction="row" justify="center" align="center">
|
||||
<Button onClick={add}>Add</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Todo } from "@/domain"
|
||||
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
||||
import { Effect, Ref, Stream, SubscriptionRef } from "effect"
|
||||
import { Delete } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { R } from "../reffuse"
|
||||
|
||||
|
||||
export interface VTodoProps {
|
||||
readonly todoRef: SubscriptionRef.SubscriptionRef<Todo.Todo>
|
||||
readonly remove: Effect.Effect<void>
|
||||
}
|
||||
|
||||
export function VTodo({ todoRef, remove }: VTodoProps) {
|
||||
|
||||
const runSync = R.useRunSync()
|
||||
|
||||
const localTodoRef = R.useRef(() => todoRef)
|
||||
const [content, setContent] = R.useRefState(R.useSubRefFromPath(localTodoRef, ["content"]))
|
||||
|
||||
R.useFork(() => localTodoRef.changes.pipe(
|
||||
Stream.debounce("250 millis"),
|
||||
Stream.runForEach(v => Ref.set(todoRef, v)),
|
||||
), [localTodoRef])
|
||||
|
||||
const editorMode = useState(false)
|
||||
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Card>
|
||||
<Flex direction="column" align="stretch" gap="1">
|
||||
<TextArea
|
||||
value={content}
|
||||
onChange={e => setContent(e.target.value)}
|
||||
disabled={!editorMode}
|
||||
/>
|
||||
|
||||
<Flex direction="row" justify="between" align="center">
|
||||
<Box></Box>
|
||||
|
||||
<Flex direction="row" align="center" gap="1">
|
||||
<IconButton onClick={() => runSync(remove)}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Box, Flex } from "@radix-ui/themes"
|
||||
import { Chunk, Effect, Ref } from "effect"
|
||||
import { R } from "../reffuse"
|
||||
import { TodosState } from "../services"
|
||||
import { VNewTodo } from "./VNewTodo"
|
||||
import { VTodo } from "./VTodo"
|
||||
|
||||
|
||||
export function VTodos() {
|
||||
|
||||
const todosRef = R.useMemo(() => Effect.map(TodosState.TodosState, state => state.todos), [])
|
||||
const [todos] = R.useSubscribeRefs(todosRef)
|
||||
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center" gap="3">
|
||||
<Box width="500px">
|
||||
<VNewTodo />
|
||||
</Box>
|
||||
|
||||
{Chunk.map(todos, (todo, index) => (
|
||||
<Box key={todo.id} width="500px">
|
||||
<R.SubRefFromGetSet
|
||||
parent={todosRef}
|
||||
getter={parentValue => Chunk.unsafeGet(parentValue, index)}
|
||||
setter={(parentValue, value) => Chunk.replace(parentValue, index, value)}
|
||||
>
|
||||
{ref => <VTodo
|
||||
todoRef={ref}
|
||||
remove={Ref.update(todosRef, Chunk.remove(index))}
|
||||
/>}
|
||||
</R.SubRefFromGetSet>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -24,7 +24,13 @@
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
"plugins": [
|
||||
{
|
||||
"name": "@effect/language-service"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user