0.1.0 #1
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
# Reffuse Monorepo
|
# Effect FC Monorepo
|
||||||
|
|
||||||
Reffuse is a [Effect-TS](https://effect.website/) integration for React 19+ with the aim of integrating the Effect context system within React's component hierarchy, while avoiding touching React's internals.
|
[Effect-TS](https://effect.website/) integration for React 19+ that allows you to write function components using Effect generators.
|
||||||
|
|
||||||
This monorepo contains:
|
This monorepo contains:
|
||||||
- [The `reffuse` library](packages/reffuse)
|
- [The `effect-fc` library](packages/effect-fc)
|
||||||
- [`@reffuse/extension-lazyref`, a LazyRef integration for Reffuse](packages/extension-lazyref)
|
|
||||||
- [`@reffuse/extension-query`, TanStack Query style hooks for Reffuse](packages/extension-query)
|
|
||||||
- [An example project](packges/example)
|
- [An example project](packges/example)
|
||||||
|
|||||||
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=="],
|
||||||
|
|||||||
@@ -1,11 +1,51 @@
|
|||||||
# Reffuse
|
# Effect FC
|
||||||
|
|
||||||
[Effect-TS](https://effect.website/) integration for React 19+ with the aim of integrating the Effect context system within React's component hierarchy, while avoiding touching React's internals.
|
[Effect-TS](https://effect.website/) integration for React 19+ that allows you to write function components using Effect generators.
|
||||||
|
|
||||||
This library is in early development. While it is (almost) feature complete and mostly usable, expect bugs and quirks. Things are still being ironed out, so ideas and criticisms are more than welcome.
|
This library is in early development. While it is (almost) feature complete and mostly usable, expect bugs and quirks. Things are still being ironed out, so ideas and criticisms are more than welcome.
|
||||||
|
|
||||||
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
|
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
|
||||||
|
|
||||||
## Peer dependencies
|
## Peer dependencies
|
||||||
- `effect` 3.13+
|
- `effect` 3.15+
|
||||||
- `react` & `@types/react` 19+
|
- `react` & `@types/react` 19+
|
||||||
|
|
||||||
|
## Known issues
|
||||||
|
- Hot module replacement doesn't work for Effect FC's yet. Page reload is required to view changes. Regular React components are unaffected.
|
||||||
|
|
||||||
|
## What writing components looks like
|
||||||
|
```typescript
|
||||||
|
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||||
|
import { Chunk, Console, Effect } from "effect"
|
||||||
|
import { Component, Hook } from "effect-fc"
|
||||||
|
import { Todo } from "./Todo"
|
||||||
|
import { TodosState } from "./TodosState.service"
|
||||||
|
|
||||||
|
// Component.Component<never, TodosState | Scope, {}>
|
||||||
|
// VVV
|
||||||
|
export const Todos = Component.make(function* Todos() {
|
||||||
|
const state = yield* TodosState
|
||||||
|
const [todos] = yield* Hook.useSubscribeRefs(state.ref)
|
||||||
|
|
||||||
|
yield* Hook.useOnce(() => Effect.andThen(
|
||||||
|
Console.log("Todos mounted"),
|
||||||
|
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||||
|
))
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "effect-fc",
|
"name": "effect-fc",
|
||||||
|
"description": "Write React function components using Effect generators",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
464
packages/effect-fc/src/Component.ts
Normal file
464
packages/effect-fc/src/Component.ts
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
import { Context, Effect, type Equivalence, ExecutionStrategy, Function, pipe, Pipeable, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as Hook from "./Hook.js"
|
||||||
|
import type { ExcludeKeys } from "./utils.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface Component<E, R, P extends {}> extends Pipeable.Pipeable {
|
||||||
|
readonly body: (props: P) => Effect.Effect<React.ReactNode, E, R>
|
||||||
|
readonly displayName?: string
|
||||||
|
readonly options: Component.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Component {
|
||||||
|
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 interface Options {
|
||||||
|
readonly finalizerExecutionMode: "sync" | "fork"
|
||||||
|
readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ComponentProto = Object.seal({
|
||||||
|
pipe() { return Pipeable.pipeArguments(this, arguments) }
|
||||||
|
} as const)
|
||||||
|
|
||||||
|
const defaultOptions: Component.Options = {
|
||||||
|
finalizerExecutionMode: "sync",
|
||||||
|
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||||
|
|
||||||
|
|
||||||
|
export namespace make {
|
||||||
|
export type Gen = {
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, React.ReactNode, never>,
|
||||||
|
): 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
|
||||||
|
>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: Effect.Effect<
|
||||||
|
A,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
|
>,
|
||||||
|
props: NoInfer<P>,
|
||||||
|
) => B
|
||||||
|
): Component<Effect.Effect.Error<B>, Effect.Effect.Context<B>, P>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: Effect.Effect<
|
||||||
|
A,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
|
>,
|
||||||
|
props: NoInfer<P>,
|
||||||
|
) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
): Component<Effect.Effect.Error<C>, Effect.Effect.Context<C>, P>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: Effect.Effect<
|
||||||
|
A,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
|
>,
|
||||||
|
props: NoInfer<P>,
|
||||||
|
) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
): Component<Effect.Effect.Error<D>, Effect.Effect.Context<D>, P>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: Effect.Effect<
|
||||||
|
A,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
|
>,
|
||||||
|
props: NoInfer<P>,
|
||||||
|
) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
): Component<Effect.Effect.Error<E>, Effect.Effect.Context<E>, P>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: Effect.Effect<
|
||||||
|
A,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
|
>,
|
||||||
|
props: NoInfer<P>,
|
||||||
|
) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
|
): Component<Effect.Effect.Error<F>, Effect.Effect.Context<F>, P>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: Effect.Effect<
|
||||||
|
A,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
|
>,
|
||||||
|
props: NoInfer<P>,
|
||||||
|
) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
|
): Component<Effect.Effect.Error<G>, Effect.Effect.Context<G>, P>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: Effect.Effect<
|
||||||
|
A,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
|
>,
|
||||||
|
props: NoInfer<P>,
|
||||||
|
) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
|
g: (_: G, props: NoInfer<P>) => H,
|
||||||
|
): Component<Effect.Effect.Error<H>, Effect.Effect.Context<H>, P>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: Effect.Effect<
|
||||||
|
A,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
|
>,
|
||||||
|
props: NoInfer<P>,
|
||||||
|
) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
|
g: (_: G, props: NoInfer<P>) => H,
|
||||||
|
h: (_: H, props: NoInfer<P>) => I,
|
||||||
|
): Component<Effect.Effect.Error<I>, Effect.Effect.Context<I>, P>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I, J extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: Effect.Effect<
|
||||||
|
A,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
|
>,
|
||||||
|
props: NoInfer<P>,
|
||||||
|
) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
|
g: (_: G, props: NoInfer<P>) => H,
|
||||||
|
h: (_: H, props: NoInfer<P>) => I,
|
||||||
|
i: (_: I, props: NoInfer<P>) => J,
|
||||||
|
): Component<Effect.Effect.Error<J>, Effect.Effect.Context<J>, P>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NonGen = {
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Eff
|
||||||
|
): Component<Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>, P>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>, P>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>, P>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>, P>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>, P>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
e: (_: E, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>, P>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
|
f: (_: F, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>, P>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
|
g: (_: G, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>, P>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
|
g: (_: G, props: NoInfer<P>) => H,
|
||||||
|
h: (_: H, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>, P>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, I, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
|
g: (_: G, props: NoInfer<P>) => H,
|
||||||
|
h: (_: H, props: NoInfer<P>) => I,
|
||||||
|
i: (_: I, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>, P>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make: (
|
||||||
|
& make.Gen
|
||||||
|
& make.NonGen
|
||||||
|
& ((
|
||||||
|
spanName: string,
|
||||||
|
spanOptions?: Tracer.SpanOptions,
|
||||||
|
) => make.Gen & make.NonGen)
|
||||||
|
) = (spanNameOrBody: Function | string, ...pipeables: any[]) => {
|
||||||
|
if (typeof spanNameOrBody !== "string") {
|
||||||
|
const displayName = displayNameFromBody(spanNameOrBody)
|
||||||
|
return Object.setPrototypeOf({
|
||||||
|
body: displayName
|
||||||
|
? Effect.fn(displayName)(spanNameOrBody as any, ...pipeables as [])
|
||||||
|
: Effect.fn(spanNameOrBody as any, ...pipeables),
|
||||||
|
displayName,
|
||||||
|
options: { ...defaultOptions },
|
||||||
|
}, ComponentProto)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const spanOptions = pipeables[0]
|
||||||
|
return (body: any, ...pipeables: any[]) => Object.setPrototypeOf({
|
||||||
|
body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []),
|
||||||
|
displayName: displayNameFromBody(body) ?? spanNameOrBody,
|
||||||
|
options: { ...defaultOptions },
|
||||||
|
}, ComponentProto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeUntraced: make.Gen & make.NonGen = (body: Function, ...pipeables: any[]) => Object.setPrototypeOf({
|
||||||
|
body: Effect.fnUntraced(body as any, ...pipeables as []),
|
||||||
|
displayName: displayNameFromBody(body),
|
||||||
|
options: { ...defaultOptions },
|
||||||
|
}, ComponentProto)
|
||||||
|
|
||||||
|
const displayNameFromBody = (body: Function) => !String.isEmpty(body.name) ? body.name : undefined
|
||||||
|
|
||||||
|
|
||||||
|
export const withDisplayName: {
|
||||||
|
<T extends Component<any, any, any>>(
|
||||||
|
displayName: string
|
||||||
|
): (self: T) => T
|
||||||
|
<T extends Component<any, any, any>>(
|
||||||
|
self: T,
|
||||||
|
displayName: string,
|
||||||
|
): T
|
||||||
|
} = Function.dual(2, <T extends Component<any, any, any>>(
|
||||||
|
self: T,
|
||||||
|
displayName: string,
|
||||||
|
): T => Object.setPrototypeOf(
|
||||||
|
{ ...self, displayName },
|
||||||
|
Object.getPrototypeOf(self),
|
||||||
|
))
|
||||||
|
|
||||||
|
export const withOptions: {
|
||||||
|
<T extends Component<any, any, any>>(
|
||||||
|
options: Partial<Component.Options>
|
||||||
|
): (self: T) => T
|
||||||
|
<T extends Component<any, any, any>>(
|
||||||
|
self: T,
|
||||||
|
options: Partial<Component.Options>,
|
||||||
|
): T
|
||||||
|
} = Function.dual(2, <T extends Component<any, any, any>>(
|
||||||
|
self: T,
|
||||||
|
options: Partial<Component.Options>,
|
||||||
|
): T => Object.setPrototypeOf(
|
||||||
|
{ ...self, options: { ...self.options, ...options } },
|
||||||
|
Object.getPrototypeOf(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)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export interface Memoized<P> {
|
||||||
|
readonly memo: true
|
||||||
|
readonly memoOptions: Memoized.Options<P>
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Memoized {
|
||||||
|
export interface Options<P> {
|
||||||
|
readonly propsAreEqual?: Equivalence.Equivalence<P>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const memo = <T extends Component<any, any, any>>(
|
||||||
|
self: ExcludeKeys<T, keyof Memoized<Component.Props<T>>>
|
||||||
|
): T & Memoized<Component.Props<T>> => Object.setPrototypeOf(
|
||||||
|
{ ...self, memo: true, memoOptions: {} },
|
||||||
|
Object.getPrototypeOf(self),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const memoWithEquivalence: {
|
||||||
|
<T extends Component<any, any, any>>(
|
||||||
|
propsAreEqual: Equivalence.Equivalence<Component.Props<T>>
|
||||||
|
): (
|
||||||
|
self: ExcludeKeys<T, keyof Memoized<Component.Props<T>>>
|
||||||
|
) => T & Memoized<Component.Props<T>>
|
||||||
|
<T extends Component<any, any, any>>(
|
||||||
|
self: ExcludeKeys<T, keyof Memoized<Component.Props<T>>>,
|
||||||
|
propsAreEqual: Equivalence.Equivalence<Component.Props<T>>,
|
||||||
|
): T & Memoized<Component.Props<T>>
|
||||||
|
} = Function.dual(2, <T extends Component<any, any, any>>(
|
||||||
|
self: ExcludeKeys<T, keyof Memoized<Component.Props<T>>>,
|
||||||
|
propsAreEqual: Equivalence.Equivalence<Component.Props<T>>,
|
||||||
|
): T & Memoized<Component.Props<T>> => Object.setPrototypeOf(
|
||||||
|
{ ...self, memo: true, memoOptions: { propsAreEqual } },
|
||||||
|
Object.getPrototypeOf(self),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
export interface Suspense {
|
||||||
|
readonly suspense: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SuspenseProps = Omit<React.SuspenseProps, "children">
|
||||||
|
|
||||||
|
export const suspense = <T extends Component<any, any, P>, P extends {}>(
|
||||||
|
self: ExcludeKeys<T, keyof Suspense> & Component<any, any, ExcludeKeys<P, keyof SuspenseProps>>
|
||||||
|
): T & Suspense => Object.setPrototypeOf(
|
||||||
|
{ ...self, suspense: true },
|
||||||
|
Object.getPrototypeOf(self),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const useFC: {
|
||||||
|
<E, R, P extends {}>(
|
||||||
|
self: Component<E, R, P> & Suspense
|
||||||
|
): Effect.Effect<React.FC<P & SuspenseProps>, never, Exclude<R, Scope.Scope>>
|
||||||
|
<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> & (Memoized<P> | Suspense | {})
|
||||||
|
) {
|
||||||
|
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
|
||||||
|
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
return React.useCallback(function ScopeProvider(props: P) {
|
||||||
|
const scope = Runtime.runSync(runtimeRef.current)(Hook.useScope(
|
||||||
|
Array.from(
|
||||||
|
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
||||||
|
),
|
||||||
|
self.options,
|
||||||
|
))
|
||||||
|
|
||||||
|
const FC = React.useMemo(() => {
|
||||||
|
const f: React.FC<P> = Predicate.hasProperty(self, "suspense")
|
||||||
|
? pipe(
|
||||||
|
function SuspenseInner(props: { readonly promise: Promise<React.ReactNode> }) {
|
||||||
|
return React.use(props.promise)
|
||||||
|
},
|
||||||
|
|
||||||
|
SuspenseInner => ({ fallback, name, ...props }: P & SuspenseProps) => {
|
||||||
|
const promise = Runtime.runPromise(runtimeRef.current)(
|
||||||
|
Effect.provideService(self.body(props as P), Scope.Scope, scope)
|
||||||
|
)
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
React.Suspense,
|
||||||
|
{ fallback, name },
|
||||||
|
React.createElement(SuspenseInner, { promise }),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: (props: P) => Runtime.runSync(runtimeRef.current)(
|
||||||
|
Effect.provideService(self.body(props), Scope.Scope, scope)
|
||||||
|
)
|
||||||
|
|
||||||
|
f.displayName = self.displayName ?? "Anonymous"
|
||||||
|
return Predicate.hasProperty(self, "memo")
|
||||||
|
? React.memo(f, self.memoOptions.propsAreEqual)
|
||||||
|
: f
|
||||||
|
}, [scope])
|
||||||
|
|
||||||
|
return React.createElement(FC, props)
|
||||||
|
}, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
export const use: {
|
||||||
|
<E, R, P extends {}>(
|
||||||
|
self: Component<E, R, P> & Suspense,
|
||||||
|
fn: (Component: React.FC<P & SuspenseProps>) => React.ReactNode,
|
||||||
|
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
|
||||||
|
<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))
|
||||||
|
})
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
import { type Context, Effect, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
import { type Context, Effect, Equivalence, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { SetStateAction } from "./types/index.js"
|
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: {
|
export const useScope: {
|
||||||
(options?: ScopeOptions): Effect.Effect<Scope.Scope>
|
(
|
||||||
} = Effect.fnUntraced(function* (options?: ScopeOptions) {
|
deps: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
): Effect.Effect<Scope.Scope>
|
||||||
|
} = Effect.fn("useScope")(function*(deps, options) {
|
||||||
const runtime = yield* Effect.runtime()
|
const runtime = yield* Effect.runtime()
|
||||||
|
|
||||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
|
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([
|
||||||
Effect.all([Ref.make(true), makeScope(options)])
|
Ref.make(true),
|
||||||
), [])
|
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
||||||
|
])), [])
|
||||||
const [scope, setScope] = React.useState(initialScope)
|
const [scope, setScope] = React.useState(initialScope)
|
||||||
|
|
||||||
React.useEffect(() => Runtime.runSync(runtime)(
|
React.useEffect(() => Runtime.runSync(runtime)(
|
||||||
@@ -26,17 +30,16 @@ export const useScope: {
|
|||||||
() => closeScope(scope, runtime, options),
|
() => closeScope(scope, runtime, options),
|
||||||
),
|
),
|
||||||
|
|
||||||
onFalse: () => makeScope(options).pipe(
|
onFalse: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe(
|
||||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
||||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
), [])
|
), deps)
|
||||||
|
|
||||||
return scope
|
return scope
|
||||||
})
|
})
|
||||||
|
|
||||||
const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
const closeScope = (
|
const closeScope = (
|
||||||
scope: Scope.CloseableScope,
|
scope: Scope.CloseableScope,
|
||||||
runtime: Runtime.Runtime<never>,
|
runtime: Runtime.Runtime<never>,
|
||||||
@@ -53,44 +56,12 @@ const closeScope = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const useMemo: {
|
|
||||||
<A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<A, E, R>
|
|
||||||
} = Effect.fnUntraced(function* <A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useOnce: {
|
|
||||||
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
|
||||||
} = Effect.fnUntraced(function* <A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>
|
|
||||||
) {
|
|
||||||
return yield* useMemo(factory, [])
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useMemoLayer: {
|
|
||||||
<ROut, E, RIn>(
|
|
||||||
layer: Layer.Layer<ROut, E, RIn>
|
|
||||||
): Effect.Effect<Context.Context<ROut>, E, RIn>
|
|
||||||
} = Effect.fnUntraced(function* <ROut, E, RIn>(
|
|
||||||
layer: Layer.Layer<ROut, E, RIn>
|
|
||||||
) {
|
|
||||||
return yield* useMemo(() => Effect.provide(Effect.context<ROut>(), layer), [layer])
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useCallbackSync: {
|
export const useCallbackSync: {
|
||||||
<Args extends unknown[], A, E, R>(
|
<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,
|
||||||
): 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 +74,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,
|
||||||
) {
|
) {
|
||||||
@@ -112,37 +83,49 @@ export const useCallbackPromise: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useMemo: {
|
||||||
|
<A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.Effect<A, E, R>
|
||||||
|
} = Effect.fn("useMemo")(function* <A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useOnce: {
|
||||||
|
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||||
|
} = Effect.fn("useOnce")(function* <A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>
|
||||||
|
) {
|
||||||
|
return yield* useMemo(factory, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
export const useEffect: {
|
export const useEffect: {
|
||||||
<E, R>(
|
<E, R>(
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
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,
|
||||||
) {
|
) {
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => Effect.Do.pipe(
|
||||||
const { scope, exit } = Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||||
|
Effect.map(({ scope }) =>
|
||||||
|
() => closeScope(scope, runtime, options)
|
||||||
|
),
|
||||||
Runtime.runSync(runtime),
|
Runtime.runSync(runtime),
|
||||||
)
|
), deps)
|
||||||
|
|
||||||
return () => {
|
|
||||||
switch (options?.finalizerExecutionMode ?? "sync") {
|
|
||||||
case "sync":
|
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, exit))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, exit))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, deps)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useLayoutEffect: {
|
export const useLayoutEffect: {
|
||||||
@@ -151,31 +134,21 @@ 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,
|
||||||
) {
|
) {
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => Effect.Do.pipe(
|
||||||
const { scope, exit } = Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||||
|
Effect.map(({ scope }) =>
|
||||||
|
() => closeScope(scope, runtime, options)
|
||||||
|
),
|
||||||
Runtime.runSync(runtime),
|
Runtime.runSync(runtime),
|
||||||
)
|
), deps)
|
||||||
|
|
||||||
return () => {
|
|
||||||
switch (options?.finalizerExecutionMode ?? "sync") {
|
|
||||||
case "sync":
|
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, exit))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, exit))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, deps)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useFork: {
|
export const useFork: {
|
||||||
@@ -184,7 +157,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,
|
||||||
@@ -194,27 +167,39 @@ export const useFork: {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const scope = Runtime.runSync(runtime)(options?.scope
|
const scope = Runtime.runSync(runtime)(options?.scope
|
||||||
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
: Scope.make(options?.finalizerExecutionStrategy)
|
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
)
|
)
|
||||||
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
||||||
|
return () => closeScope(scope, runtime, {
|
||||||
return () => {
|
...options,
|
||||||
switch (options?.finalizerExecutionMode ?? "fork") {
|
finalizerExecutionMode: options?.finalizerExecutionMode ?? "fork",
|
||||||
case "sync":
|
})
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, deps)
|
}, deps)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useContext: {
|
||||||
|
<ROut, E, RIn>(
|
||||||
|
layer: Layer.Layer<ROut, E, RIn>,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>>
|
||||||
|
} = Effect.fn("useContext")(function* <ROut, E, RIn>(
|
||||||
|
layer: Layer.Layer<ROut, E, RIn>,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) {
|
||||||
|
const scope = yield* useScope([layer], options)
|
||||||
|
|
||||||
|
return yield* useMemo(() => Effect.provideService(
|
||||||
|
Effect.provide(Effect.context<ROut>(), layer),
|
||||||
|
Scope.Scope,
|
||||||
|
scope,
|
||||||
|
), [scope])
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
export const useRefFromReactiveValue: {
|
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 +209,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(() =>
|
||||||
@@ -232,7 +217,7 @@ export const useSubscribeRefs: {
|
|||||||
))
|
))
|
||||||
|
|
||||||
yield* useFork(() => pipe(
|
yield* useFork(() => pipe(
|
||||||
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
refs.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())),
|
||||||
streams => Stream.zipLatestAll(...streams),
|
streams => Stream.zipLatestAll(...streams),
|
||||||
Stream.runForEach(v =>
|
Stream.runForEach(v =>
|
||||||
Effect.sync(() => setReactStateValue(v))
|
Effect.sync(() => setReactStateValue(v))
|
||||||
@@ -246,11 +231,11 @@ 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(
|
||||||
Stream.changesWith(ref.changes, (x, y) => x === y),
|
Stream.changesWith(ref.changes, Equivalence.strict()),
|
||||||
v => Effect.sync(() => setReactStateValue(v)),
|
v => Effect.sync(() => setReactStateValue(v)),
|
||||||
), [ref])
|
), [ref])
|
||||||
|
|
||||||
@@ -268,7 +253,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 +282,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,
|
||||||
) {
|
) {
|
||||||
@@ -309,7 +294,7 @@ export const useSubscribeStream: {
|
|||||||
)
|
)
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(
|
yield* useFork(() => Stream.runForEach(
|
||||||
Stream.changesWith(stream, (x, y) => x === y),
|
Stream.changesWith(stream, Equivalence.strict()),
|
||||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||||
), [stream])
|
), [stream])
|
||||||
|
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
|
|||||||
3
packages/effect-fc/src/utils.ts
Normal file
3
packages/effect-fc/src/utils.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type ExcludeKeys<T, K extends PropertyKey> = K extends keyof T ? (
|
||||||
|
{ [P in K]?: never } & Omit<T, K>
|
||||||
|
) : T
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/dev/memo')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <div>Hello "/dev/memo"!</div>
|
||||||
|
}
|
||||||
@@ -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,106 @@
|
|||||||
// 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 DevMemoRouteImport } from './routes/dev/memo'
|
||||||
|
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
|
||||||
|
|
||||||
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 DevMemoRoute = DevMemoRouteImport.update({
|
||||||
const StreamsPullRoute = StreamsPullImport.update({
|
id: '/dev/memo',
|
||||||
id: '/streams/pull',
|
path: '/dev/memo',
|
||||||
path: '/streams/pull',
|
getParentRoute: () => rootRouteImport,
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
} as any)
|
||||||
|
const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
|
||||||
const QueryUsequeryRoute = QueryUsequeryImport.update({
|
id: '/dev/async-rendering',
|
||||||
id: '/query/usequery',
|
path: '/dev/async-rendering',
|
||||||
path: '/query/usequery',
|
getParentRoute: () => rootRouteImport,
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
} 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
|
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
'/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
|
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
'/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
|
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
'/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' | '/dev/async-rendering' | '/dev/memo'
|
||||||
| '/'
|
|
||||||
| '/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' | '/dev/async-rendering' | '/dev/memo'
|
||||||
| '/'
|
id: '__root__' | '/' | '/blank' | '/dev/async-rendering' | '/dev/memo'
|
||||||
| '/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
|
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
|
||||||
EffectComponentTestsRoute: typeof EffectComponentTestsRoute
|
DevMemoRoute: typeof DevMemoRoute
|
||||||
LazyrefRoute: typeof LazyrefRoute
|
}
|
||||||
PromiseRoute: typeof PromiseRoute
|
|
||||||
TestsRoute: typeof TestsRoute
|
declare module '@tanstack/react-router' {
|
||||||
TimeRoute: typeof TimeRoute
|
interface FileRoutesByPath {
|
||||||
TodosRoute: typeof TodosRoute
|
'/blank': {
|
||||||
QueryServiceRoute: typeof QueryServiceRoute
|
id: '/blank'
|
||||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
path: '/blank'
|
||||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
fullPath: '/blank'
|
||||||
StreamsPullRoute: typeof StreamsPullRoute
|
preLoaderRoute: typeof BlankRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/': {
|
||||||
|
id: '/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/dev/memo': {
|
||||||
|
id: '/dev/memo'
|
||||||
|
path: '/dev/memo'
|
||||||
|
fullPath: '/dev/memo'
|
||||||
|
preLoaderRoute: typeof DevMemoRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/dev/async-rendering': {
|
||||||
|
id: '/dev/async-rendering'
|
||||||
|
path: '/dev/async-rendering'
|
||||||
|
fullPath: '/dev/async-rendering'
|
||||||
|
preLoaderRoute: typeof DevAsyncRenderingRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BlankRoute: BlankRoute,
|
BlankRoute: BlankRoute,
|
||||||
CountRoute: CountRoute,
|
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
|
||||||
EffectComponentTestsRoute: EffectComponentTestsRoute,
|
DevMemoRoute: DevMemoRoute,
|
||||||
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>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
54
packages/example/src/routes/dev/async-rendering.tsx
Normal file
54
packages/example/src/routes/dev/async-rendering.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { runtime } from "@/runtime"
|
||||||
|
import { Flex, Text, TextField } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Console, Effect } from "effect"
|
||||||
|
import { Component, Hook } from "effect-fc"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
const RouteComponent = Component.make(function* AsyncRendering() {
|
||||||
|
const VMemoizedAsyncComponent = yield* Component.useFC(MemoizedAsyncComponent)
|
||||||
|
const VAsyncComponent = yield* Component.useFC(AsyncComponent)
|
||||||
|
const [input, setInput] = React.useState("")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
|
<TextField.Root
|
||||||
|
value={input}
|
||||||
|
onChange={e => setInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VMemoizedAsyncComponent />
|
||||||
|
<VAsyncComponent />
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Component.withRuntime(runtime.context)
|
||||||
|
)
|
||||||
|
|
||||||
|
const AsyncComponent = Component.make(function* AsyncComponent() {
|
||||||
|
yield* Console.log("rendering")
|
||||||
|
|
||||||
|
const VSubComponent = yield* Component.useFC(SubComponent)
|
||||||
|
yield* Effect.sleep("500 millis")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" align="stretch">
|
||||||
|
<Text>Rendered!</Text>
|
||||||
|
<VSubComponent />
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Component.suspense
|
||||||
|
)
|
||||||
|
const MemoizedAsyncComponent = Component.memo(AsyncComponent)
|
||||||
|
|
||||||
|
const SubComponent = Component.make(function* SubComponent() {
|
||||||
|
const [state] = React.useState(yield* Hook.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
||||||
|
return <Text>{state}</Text>
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dev/async-rendering")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
40
packages/example/src/routes/dev/memo.tsx
Normal file
40
packages/example/src/routes/dev/memo.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { runtime } from "@/runtime"
|
||||||
|
import { Flex, Text, TextField } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Component } from "effect-fc"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
const RouteComponent = Component.make(function* RouteComponent() {
|
||||||
|
const VSubComponent = yield* Component.useFC(SubComponent)
|
||||||
|
const VMemoizedSubComponent = yield* Component.useFC(MemoizedSubComponent)
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState("")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<TextField.Root
|
||||||
|
value={value}
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VSubComponent />
|
||||||
|
<VMemoizedSubComponent />
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Component.withRuntime(runtime.context)
|
||||||
|
)
|
||||||
|
|
||||||
|
const SubComponent = Component.make(function* SubComponent() {
|
||||||
|
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
|
||||||
|
return <Text>{id}</Text>
|
||||||
|
})
|
||||||
|
|
||||||
|
const MemoizedSubComponent = Component.memo(SubComponent)
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dev/memo")({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
@@ -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,25 @@
|
|||||||
|
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() {
|
||||||
|
const context = yield* Hook.useContext(TodosStateLive, { finalizerExecutionMode: "fork" })
|
||||||
|
return yield* Effect.provide(Component.use(Todos, Todos => <Todos />), context)
|
||||||
|
}).pipe(
|
||||||
|
Component.withRuntime(runtime.context)
|
||||||
|
)
|
||||||
|
|
||||||
|
// component: Component.make("Index")(
|
||||||
|
// () => Hook.useContext(TodosStateLive, { finalizerExecutionMode: "fork" }),
|
||||||
|
// Effect.andThen(context => Effect.provide(Component.use(Todos, Todos => <Todos />), context)),
|
||||||
|
// ).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"
|
|
||||||
115
packages/example/src/todo/Todo.tsx
Normal file
115
packages/example/src/todo/Todo.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
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 { 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, contentRef] = yield* Hook.useMemo(() => Match.value(props).pipe(
|
||||||
|
Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)),
|
||||||
|
Match.tag("edit", ({ index }) => Effect.succeed(SubscriptionSubRef.makeFromChunkRef(state.ref, index))),
|
||||||
|
Match.exhaustive,
|
||||||
|
|
||||||
|
Effect.map(ref => [
|
||||||
|
ref,
|
||||||
|
SubscriptionSubRef.makeFromPath(ref, ["content"]),
|
||||||
|
] as const),
|
||||||
|
), [props._tag, props.index])
|
||||||
|
|
||||||
|
const [content, size] = yield* Hook.useSubscribeRefs(contentRef, state.sizeRef)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
|
<Flex direction="row" align="center" gap="2">
|
||||||
|
<Box flexGrow="1">
|
||||||
|
<TextArea
|
||||||
|
value={content}
|
||||||
|
onChange={e => Runtime.runSync(runtime)(Ref.set(contentRef, e.target.value))}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{props._tag === "edit" &&
|
||||||
|
<Flex direction="column" justify="center" align="center" gap="1">
|
||||||
|
<IconButton
|
||||||
|
disabled={props.index <= 0}
|
||||||
|
onClick={() => Runtime.runSync(runtime)(
|
||||||
|
SubscriptionRef.updateEffect(state.ref, todos => Effect.gen(function*() {
|
||||||
|
if (props.index <= 0) return yield* Option.none()
|
||||||
|
return todos.pipe(
|
||||||
|
Chunk.replace(props.index, yield* Chunk.get(todos, props.index - 1)),
|
||||||
|
Chunk.replace(props.index - 1, yield* ref),
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FaArrowUp />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
disabled={props.index >= size - 1}
|
||||||
|
onClick={() => Runtime.runSync(runtime)(
|
||||||
|
SubscriptionRef.updateEffect(state.ref, todos => Effect.gen(function*() {
|
||||||
|
if (props.index >= size - 1) return yield* Option.none()
|
||||||
|
return todos.pipe(
|
||||||
|
Chunk.replace(props.index, yield* Chunk.get(todos, props.index + 1)),
|
||||||
|
Chunk.replace(props.index + 1, yield* ref),
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FaArrowDown />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
onClick={() => Runtime.runSync(runtime)(
|
||||||
|
Ref.update(state.ref, Chunk.remove(props.index))
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FaDeleteLeft />
|
||||||
|
</IconButton>
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Component.memo
|
||||||
|
)
|
||||||
32
packages/example/src/todo/Todos.tsx
Normal file
32
packages/example/src/todo/Todos.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||||
|
import { Chunk, Console, Effect } 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)
|
||||||
|
|
||||||
|
yield* Hook.useOnce(() => Effect.andThen(
|
||||||
|
Console.log("Todos mounted"),
|
||||||
|
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||||
|
))
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
})
|
||||||
50
packages/example/src/todo/TodosState.service.ts
Normal file
50
packages/example/src/todo/TodosState.service.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Todo } from "@/domain"
|
||||||
|
import { KeyValueStore } from "@effect/platform"
|
||||||
|
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||||
|
import { Chunk, Console, Effect, Option, Schema, Stream, SubscriptionRef } from "effect"
|
||||||
|
import { SubscriptionSubRef } from "effect-fc/types"
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
const sizeRef = SubscriptionSubRef.makeFromPath(ref, ["length"])
|
||||||
|
|
||||||
|
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, sizeRef } 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