Compare commits
27 Commits
master
...
875a65c226
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
875a65c226 | ||
|
|
6bef41b2ee | ||
|
|
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:*",
|
"effect-fc": "workspace:*",
|
||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.510.0",
|
||||||
"mobx": "^6.13.7",
|
"mobx": "^6.13.7",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/language-service": "^0.23.4",
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.26.0",
|
||||||
"@tanstack/react-router": "^1.120.3",
|
"@tanstack/react-router": "^1.120.3",
|
||||||
"@tanstack/react-router-devtools": "^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-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-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=="],
|
"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 {
|
export interface ScopeOptions {
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||||
}
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -58,7 +14,7 @@ export const useMemo: {
|
|||||||
factory: () => Effect.Effect<A, E, R>,
|
factory: () => Effect.Effect<A, E, R>,
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
): Effect.Effect<A, E, R>
|
): Effect.Effect<A, E, R>
|
||||||
} = Effect.fnUntraced(function* <A, E, R>(
|
} = Effect.fn("useMemo")(function* <A, E, R>(
|
||||||
factory: () => Effect.Effect<A, E, R>,
|
factory: () => Effect.Effect<A, E, R>,
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
) {
|
) {
|
||||||
@@ -68,7 +24,7 @@ export const useMemo: {
|
|||||||
|
|
||||||
export const useOnce: {
|
export const useOnce: {
|
||||||
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
<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>
|
factory: () => Effect.Effect<A, E, R>
|
||||||
) {
|
) {
|
||||||
return yield* useMemo(factory, [])
|
return yield* useMemo(factory, [])
|
||||||
@@ -78,7 +34,7 @@ export const useMemoLayer: {
|
|||||||
<ROut, E, RIn>(
|
<ROut, E, RIn>(
|
||||||
layer: Layer.Layer<ROut, E, RIn>
|
layer: Layer.Layer<ROut, E, RIn>
|
||||||
): Effect.Effect<Context.Context<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>
|
layer: Layer.Layer<ROut, E, RIn>
|
||||||
) {
|
) {
|
||||||
return yield* useMemo(() => Effect.provide(Effect.context<ROut>(), layer), [layer])
|
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>,
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
): Effect.Effect<(...args: Args) => A, never, R>
|
): 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>,
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
) {
|
) {
|
||||||
@@ -103,7 +59,7 @@ export const useCallbackPromise: {
|
|||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
): 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>,
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
) {
|
) {
|
||||||
@@ -118,7 +74,7 @@ export const useEffect: {
|
|||||||
deps?: React.DependencyList,
|
deps?: React.DependencyList,
|
||||||
options?: ScopeOptions,
|
options?: ScopeOptions,
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
): 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>,
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
deps?: React.DependencyList,
|
deps?: React.DependencyList,
|
||||||
options?: ScopeOptions,
|
options?: ScopeOptions,
|
||||||
@@ -151,7 +107,7 @@ export const useLayoutEffect: {
|
|||||||
deps?: React.DependencyList,
|
deps?: React.DependencyList,
|
||||||
options?: ScopeOptions,
|
options?: ScopeOptions,
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
): 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>,
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
deps?: React.DependencyList,
|
deps?: React.DependencyList,
|
||||||
options?: ScopeOptions,
|
options?: ScopeOptions,
|
||||||
@@ -184,7 +140,7 @@ export const useFork: {
|
|||||||
deps?: React.DependencyList,
|
deps?: React.DependencyList,
|
||||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
): 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>,
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
deps?: React.DependencyList,
|
deps?: React.DependencyList,
|
||||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||||
@@ -214,7 +170,7 @@ export const useFork: {
|
|||||||
|
|
||||||
export const useRefFromReactiveValue: {
|
export const useRefFromReactiveValue: {
|
||||||
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
||||||
} = Effect.fnUntraced(function*(value) {
|
} = Effect.fn("useRefFromReactiveValue")(function*(value) {
|
||||||
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
||||||
yield* useEffect(() => Ref.set(ref, value), [value])
|
yield* useEffect(() => Ref.set(ref, value), [value])
|
||||||
return ref
|
return ref
|
||||||
@@ -224,7 +180,7 @@ export const useSubscribeRefs: {
|
|||||||
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||||
...refs: Refs
|
...refs: Refs
|
||||||
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
|
): 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
|
...refs: Refs
|
||||||
) {
|
) {
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
||||||
@@ -246,7 +202,7 @@ export const useRefState: {
|
|||||||
<A>(
|
<A>(
|
||||||
ref: SubscriptionRef.SubscriptionRef<A>
|
ref: SubscriptionRef.SubscriptionRef<A>
|
||||||
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<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))
|
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(
|
yield* useFork(() => Stream.runForEach(
|
||||||
@@ -268,7 +224,7 @@ export const useStreamFromReactiveValues: {
|
|||||||
<const A extends React.DependencyList>(
|
<const A extends React.DependencyList>(
|
||||||
values: A
|
values: A
|
||||||
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
): 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(
|
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
|
||||||
Effect.bind("latest", () => Ref.make(values)),
|
Effect.bind("latest", () => Ref.make(values)),
|
||||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
||||||
@@ -297,7 +253,7 @@ export const useSubscribeStream: {
|
|||||||
stream: Stream.Stream<A, E, R>,
|
stream: Stream.Stream<A, E, R>,
|
||||||
initialValue: A,
|
initialValue: A,
|
||||||
): Effect.Effect<Option.Some<A>, never, R>
|
): 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>,
|
stream: Stream.Stream<A, E, R>,
|
||||||
initialValue?: A,
|
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 runtime: ReactManagedRuntime<R, ER>
|
||||||
readonly children?: React.ReactNode
|
readonly children?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AsyncProvider<R, ER>(
|
export function Provider<R, ER>(
|
||||||
{ runtime, children, ...suspenseProps }: AsyncProviderProps<R, ER>
|
{ runtime, children, ...suspenseProps }: ProviderProps<R, ER>
|
||||||
): React.ReactNode {
|
): React.ReactNode {
|
||||||
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
||||||
|
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
React.Suspense,
|
React.Suspense,
|
||||||
suspenseProps,
|
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 runtime: ReactManagedRuntime<R, ER>
|
||||||
readonly promise: Promise<Runtime.Runtime<R>>
|
readonly promise: Promise<Runtime.Runtime<R>>
|
||||||
readonly children?: React.ReactNode
|
readonly children?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
function AsyncProviderInner<R, ER>(
|
function ProviderInner<R, ER>(
|
||||||
{ runtime, promise, children }: AsyncProviderInnerProps<R, ER>
|
{ runtime, promise, children }: ProviderInnerProps<R, ER>
|
||||||
): React.ReactNode {
|
): React.ReactNode {
|
||||||
const value = React.use(promise)
|
const value = React.use(promise)
|
||||||
return React.createElement(runtime.context, { value }, children)
|
return React.createElement(runtime.context, { value }, children)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * as ReactComponent from "./ReactComponent.js"
|
export * as Component from "./Component.js"
|
||||||
export * as ReactHook from "./ReactHook.js"
|
export * as Hook from "./Hook.js"
|
||||||
export * as ReactManagedRuntime from "./ReactManagedRuntime.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"
|
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,
|
readonly setter: (parentValue: B, value: A) => B,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
this.get = Effect.map(this.parent, this.getter)
|
||||||
}
|
}
|
||||||
|
|
||||||
commit() {
|
commit() {
|
||||||
@@ -86,9 +86,11 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
|||||||
|
|
||||||
export const makeFromGetSet = <A, B>(
|
export const makeFromGetSet = <A, B>(
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
getter: (parentValue: B) => A,
|
options: {
|
||||||
setter: (parentValue: B, value: A) => B,
|
readonly get: (parentValue: B) => A
|
||||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
|
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>>(
|
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
||||||
parent: SubscriptionRef.SubscriptionRef<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 => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
||||||
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
(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"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/language-service": "^0.23.4",
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.26.0",
|
||||||
"@tanstack/react-router": "^1.120.3",
|
"@tanstack/react-router": "^1.120.3",
|
||||||
"@tanstack/react-router-devtools": "^1.120.3",
|
"@tanstack/react-router-devtools": "^1.120.3",
|
||||||
@@ -36,9 +37,10 @@
|
|||||||
"@typed/id": "^0.17.2",
|
"@typed/id": "^0.17.2",
|
||||||
"@typed/lazy-ref": "^0.3.3",
|
"@typed/lazy-ref": "^0.3.3",
|
||||||
"effect": "^3.15.1",
|
"effect": "^3.15.1",
|
||||||
|
"effect-fc": "workspace:*",
|
||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.510.0",
|
||||||
"mobx": "^6.13.7",
|
"mobx": "^6.13.7",
|
||||||
"effect-fc": "workspace:*"
|
"react-icons": "^5.5.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"effect": "^3.15.1",
|
"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 { createRouter, RouterProvider } from "@tanstack/react-router"
|
||||||
import { Layer } from "effect"
|
import { ReactManagedRuntime } from "effect-fc"
|
||||||
import { StrictMode } from "react"
|
import { StrictMode } from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import { ReffuseRuntime } from "reffuse"
|
|
||||||
import { RootContext } from "./reffuse"
|
|
||||||
import { routeTree } from "./routeTree.gen"
|
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 })
|
const router = createRouter({ routeTree })
|
||||||
|
|
||||||
declare module "@tanstack/react-router" {
|
declare module "@tanstack/react-router" {
|
||||||
@@ -27,13 +14,10 @@ declare module "@tanstack/react-router" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ReffuseRuntime.Provider>
|
<ReactManagedRuntime.Provider runtime={runtime}>
|
||||||
<RootContext.Provider layer={layer}>
|
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</RootContext.Provider>
|
</ReactManagedRuntime.Provider>
|
||||||
</ReffuseRuntime.Provider>
|
|
||||||
</StrictMode>
|
</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.
|
// 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.
|
// 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'
|
const BlankRoute = BlankRouteImport.update({
|
||||||
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({
|
|
||||||
id: '/blank',
|
id: '/blank',
|
||||||
path: '/blank',
|
path: '/blank',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const IndexRoute = IndexRouteImport.update({
|
||||||
const IndexRoute = IndexImport.update({
|
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} 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 {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/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 {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/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 {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRoute
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/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 {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths: '/' | '/blank'
|
||||||
| '/'
|
|
||||||
| '/blank'
|
|
||||||
| '/count'
|
|
||||||
| '/effect-component-tests'
|
|
||||||
| '/lazyref'
|
|
||||||
| '/promise'
|
|
||||||
| '/tests'
|
|
||||||
| '/time'
|
|
||||||
| '/todos'
|
|
||||||
| '/query/service'
|
|
||||||
| '/query/usemutation'
|
|
||||||
| '/query/usequery'
|
|
||||||
| '/streams/pull'
|
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to: '/' | '/blank'
|
||||||
| '/'
|
id: '__root__' | '/' | '/blank'
|
||||||
| '/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'
|
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
BlankRoute: typeof BlankRoute
|
BlankRoute: typeof BlankRoute
|
||||||
CountRoute: typeof CountRoute
|
}
|
||||||
EffectComponentTestsRoute: typeof EffectComponentTestsRoute
|
|
||||||
LazyrefRoute: typeof LazyrefRoute
|
declare module '@tanstack/react-router' {
|
||||||
PromiseRoute: typeof PromiseRoute
|
interface FileRoutesByPath {
|
||||||
TestsRoute: typeof TestsRoute
|
'/blank': {
|
||||||
TimeRoute: typeof TimeRoute
|
id: '/blank'
|
||||||
TodosRoute: typeof TodosRoute
|
path: '/blank'
|
||||||
QueryServiceRoute: typeof QueryServiceRoute
|
fullPath: '/blank'
|
||||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
preLoaderRoute: typeof BlankRouteImport
|
||||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
parentRoute: typeof rootRouteImport
|
||||||
StreamsPullRoute: typeof StreamsPullRoute
|
}
|
||||||
|
'/': {
|
||||||
|
id: '/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BlankRoute: BlankRoute,
|
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 = rootRouteImport
|
||||||
export const routeTree = rootRoute
|
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
._addFileTypes<FileRouteTypes>()
|
._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 { Container, Flex, Theme } from "@radix-ui/themes"
|
||||||
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
||||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
|
||||||
@@ -14,21 +13,15 @@ export const Route = createRootRoute({
|
|||||||
function Root() {
|
function Root() {
|
||||||
return (
|
return (
|
||||||
<Theme>
|
<Theme>
|
||||||
<Container>
|
<Container mb="4">
|
||||||
<Flex direction="row" justify="center" align="center" gap="2">
|
<Flex direction="row" justify="center" align="center" gap="2">
|
||||||
<Link to="/">Index</Link>
|
<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>
|
<Link to="/blank">Blank</Link>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|
||||||
<VQueryErrorHandler />
|
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
</Theme>
|
</Theme>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
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() {
|
function RouteComponent() {
|
||||||
|
|||||||
@@ -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 { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Component, Hook } from "effect-fc"
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/')({
|
const TodosStateLive = TodosState.Default("todos")
|
||||||
component: RouteComponent
|
|
||||||
|
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"
|
|
||||||
122
packages/example/src/todo/Todo.tsx
Normal file
122
packages/example/src/todo/Todo.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
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
|
||||||
|
onClick={() => Runtime.runSync(runtime)(
|
||||||
|
Ref.updateSome(state.ref, todos => Option.gen(function*() {
|
||||||
|
if (props.index <= 0)
|
||||||
|
return yield* Option.none()
|
||||||
|
|
||||||
|
const toSwapIndex = props.index - 1
|
||||||
|
const toSwap = yield* Chunk.get(todos, toSwapIndex)
|
||||||
|
return todos.pipe(
|
||||||
|
Chunk.replace(props.index, toSwap),
|
||||||
|
Chunk.replace(toSwapIndex, todo),
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FaArrowUp />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
onClick={() => Runtime.runSync(runtime)(
|
||||||
|
Ref.updateSome(state.ref, todos => Option.gen(function*() {
|
||||||
|
if (props.index >= Chunk.size(todos))
|
||||||
|
return yield* Option.none()
|
||||||
|
|
||||||
|
const toSwapIndex = props.index + 1
|
||||||
|
const toSwap = yield* Chunk.get(todos, toSwapIndex)
|
||||||
|
return todos.pipe(
|
||||||
|
Chunk.replace(props.index, toSwap),
|
||||||
|
Chunk.replace(toSwapIndex, todo),
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<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": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "@effect/language-service"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user