Compare commits
16 Commits
875a65c226
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e8adf8506 | |||
| 89f966d93e | |||
| a432993ac3 | |||
| 6bf4e33c29 | |||
|
|
7bba444776 | ||
| 03aa7c467c | |||
| dbc75564bf | |||
| 2cc0cf6ab3 | |||
| e3ddb5b39e | |||
| 1020e4a1de | |||
|
|
59298e7074 | ||
| 9a3c91b50b | |||
|
|
831a808568 | ||
|
|
16fa750b30 | ||
|
|
440eb38280 | ||
|
|
3cb0964a48 |
@@ -7,10 +7,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v2
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
- name: Build
|
- name: Lint TypeScript
|
||||||
run: bun run build
|
run: bun lint:tsc
|
||||||
|
- name: Lint Biome
|
||||||
|
run: bun lint:biome
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v2
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
- name: Lint TypeScript
|
||||||
|
run: bun lint:tsc
|
||||||
|
- name: Lint Biome
|
||||||
|
run: bun lint:biome
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build
|
run: bun run build
|
||||||
- name: Publish effect-fc
|
- name: Publish effect-fc
|
||||||
uses: JS-DevTools/npm-publish@v3
|
uses: JS-DevTools/npm-publish@v4
|
||||||
with:
|
with:
|
||||||
package: packages/effect-fc
|
package: packages/effect-fc
|
||||||
access: public
|
access: public
|
||||||
|
|||||||
@@ -8,16 +8,20 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v2
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "24"
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
- name: Lint TypeScript
|
||||||
|
run: bun lint:tsc
|
||||||
|
- name: Lint Biome
|
||||||
|
run: bun lint:biome
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build
|
run: bun run build
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: bun run pack
|
run: bun pack
|
||||||
|
|||||||
1
.npmrc
1
.npmrc
@@ -1 +0,0 @@
|
|||||||
@thilawyn:registry=https://git.valverde.cloud/api/packages/thilawyn/npm/
|
|
||||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.biome": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
README.md
10
README.md
@@ -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)
|
- [An example project](packages/example)
|
||||||
- [`@reffuse/extension-query`, TanStack Query style hooks for Reffuse](packages/extension-query)
|
|
||||||
- [An example project](packges/example)
|
|
||||||
|
|||||||
40
biome.json
Normal file
40
biome.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": false,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": false
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"style": {
|
||||||
|
"useShorthandFunctionType": "off"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noExplicitAny": "off",
|
||||||
|
"noShadowRestrictedNames": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "double"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assist": {
|
||||||
|
"enabled": true,
|
||||||
|
"actions": {
|
||||||
|
"source": {
|
||||||
|
"organizeImports": "on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[install.scopes]
|
|
||||||
"@thilawyn" = "https://git.valverde.cloud/api/packages/thilawyn/npm/"
|
|
||||||
23
package.json
23
package.json
@@ -1,23 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "@effect-fc/monorepo",
|
"name": "@effect-fc/monorepo",
|
||||||
"packageManager": "bun@1.2.13",
|
"packageManager": "bun@1.3.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./packages/*"
|
"./packages/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo build --filter=!@effect-fc/example",
|
"build": "turbo build",
|
||||||
"lint:tsc": "turbo lint:tsc",
|
"lint:tsc": "turbo lint:tsc",
|
||||||
"pack": "turbo pack --filter=!@effect-fc/example",
|
"lint:biome": "turbo lint:biome",
|
||||||
"publish": "turbo publish --filter=!@effect-fc/example",
|
"pack": "turbo pack",
|
||||||
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
"clean:cache": "turbo clean:cache",
|
||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "turbo clean:dist",
|
||||||
"clean:node": "rm -rf node_modules"
|
"clean:modules": "turbo clean:modules && rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"npm-check-updates": "^18.0.1",
|
"@biomejs/biome": "^2.3.8",
|
||||||
|
"@effect/language-service": "^0.65.0",
|
||||||
|
"@types/bun": "^1.3.3",
|
||||||
|
"npm-check-updates": "^19.1.2",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
"turbo": "^2.5.3",
|
"turbo": "^2.6.1",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,64 @@
|
|||||||
# 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
|
||||||
|
- React Refresh doesn't work for Effect FC's yet. Page reload is required to view changes. Regular React components are unaffected.
|
||||||
|
|
||||||
|
## What writing components looks like
|
||||||
|
```typescript
|
||||||
|
import { Component } from "effect-fc"
|
||||||
|
import { useOnce, useSubscribables } from "effect-fc/Hooks"
|
||||||
|
import { Todo } from "./Todo"
|
||||||
|
import { TodosState } from "./TodosState.service"
|
||||||
|
|
||||||
|
|
||||||
|
export class Todos extends Component.makeUntraced("Todos")(function*() {
|
||||||
|
const state = yield* TodosState
|
||||||
|
const [todos] = yield* useSubscribables(state.ref)
|
||||||
|
|
||||||
|
yield* useOnce(() => Effect.andThen(
|
||||||
|
Console.log("Todos mounted"),
|
||||||
|
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||||
|
))
|
||||||
|
|
||||||
|
const TodoFC = yield* Todo
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Heading align="center">Todos</Heading>
|
||||||
|
|
||||||
|
<Flex direction="column" align="stretch" gap="2" mt="2">
|
||||||
|
<TodoFC _tag="new" />
|
||||||
|
|
||||||
|
{Chunk.map(todos, todo =>
|
||||||
|
<TodoFC key={todo.id} _tag="edit" id={todo.id} />
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
const TodosStateLive = TodosState.Default("todos")
|
||||||
|
|
||||||
|
const Index = Component.makeUntraced("Index")(function*() {
|
||||||
|
const context = yield* useContext(TodosStateLive, { finalizerExecutionMode: "fork" })
|
||||||
|
const TodosFC = yield* Effect.provide(Todos, context)
|
||||||
|
|
||||||
|
return <TodosFC />
|
||||||
|
}).pipe(
|
||||||
|
Component.withRuntime(runtime.context)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/")({
|
||||||
|
component: Index
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|||||||
8
packages/effect-fc/biome.json
Normal file
8
packages/effect-fc/biome.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
||||||
|
"root": false,
|
||||||
|
"extends": "//",
|
||||||
|
"files": {
|
||||||
|
"includes": ["./src/**"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "effect-fc",
|
"name": "effect-fc",
|
||||||
"version": "0.1.0",
|
"description": "Write React function components with Effect",
|
||||||
|
"version": "0.2.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -16,29 +17,32 @@
|
|||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"./types": {
|
"./*": [
|
||||||
"types": "./dist/types/index.d.ts",
|
{
|
||||||
"default": "./dist/types/index.js"
|
"types": "./dist/*/index.d.ts",
|
||||||
|
"default": "./dist/*/index.js"
|
||||||
},
|
},
|
||||||
"./*": {
|
{
|
||||||
"types": "./dist/*.d.ts",
|
"types": "./dist/*.d.ts",
|
||||||
"default": "./dist/*.js"
|
"default": "./dist/*.js"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint:tsc": "tsc --noEmit",
|
"lint:tsc": "tsc --noEmit",
|
||||||
|
"lint:biome": "biome lint",
|
||||||
"pack": "npm pack",
|
"pack": "npm pack",
|
||||||
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
"clean:cache": "rm -rf .turbo tsconfig.tsbuildinfo",
|
||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "rm -rf dist",
|
||||||
"clean:node": "rm -rf node_modules"
|
"clean:modules": "rm -rf node_modules"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "^19.0.0",
|
|
||||||
"effect": "^3.15.0",
|
|
||||||
"react": "^19.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@effect/language-service": "^0.23.3"
|
"@effect/platform-browser": "^0.74.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^19.2.0",
|
||||||
|
"effect": "^3.19.0",
|
||||||
|
"react": "^19.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
packages/effect-fc/src/Async.ts
Normal file
85
packages/effect-fc/src/Async.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
|
import { Effect, Function, Predicate, Runtime, Scope } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as Component from "./Component.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const TypeId: unique symbol = Symbol.for("@effect-fc/Async/Async")
|
||||||
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
|
export interface Async extends Async.Options {
|
||||||
|
readonly [TypeId]: TypeId
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Async {
|
||||||
|
export interface Options {
|
||||||
|
readonly defaultFallback?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = Omit<React.SuspenseProps, "children">
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const AsyncProto = Object.freeze({
|
||||||
|
[TypeId]: TypeId,
|
||||||
|
|
||||||
|
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
||||||
|
this: Component.Component<P, A, E, R> & Async,
|
||||||
|
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||||
|
) {
|
||||||
|
const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
||||||
|
|
||||||
|
return ({ fallback, name, ...props }: Async.Props) => {
|
||||||
|
const promise = Runtime.runPromise(runtimeRef.current)(
|
||||||
|
Effect.andThen(
|
||||||
|
Component.useScope([], this),
|
||||||
|
scope => Effect.provideService(this.body(props as P), Scope.Scope, scope),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
React.Suspense,
|
||||||
|
{ fallback: fallback ?? this.defaultFallback, name },
|
||||||
|
React.createElement(SuspenseInner, { promise }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as const)
|
||||||
|
|
||||||
|
|
||||||
|
export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, TypeId)
|
||||||
|
|
||||||
|
export const async = <T extends Component.Component<any, any, any, any>>(
|
||||||
|
self: T
|
||||||
|
): (
|
||||||
|
& Omit<T, keyof Component.Component.AsComponent<T>>
|
||||||
|
& Component.Component<
|
||||||
|
Component.Component.Props<T> & Async.Props,
|
||||||
|
Component.Component.Success<T>,
|
||||||
|
Component.Component.Error<T>,
|
||||||
|
Component.Component.Context<T>
|
||||||
|
>
|
||||||
|
& Async
|
||||||
|
) => Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, self),
|
||||||
|
Object.freeze(Object.setPrototypeOf(
|
||||||
|
Object.assign({}, AsyncProto),
|
||||||
|
Object.getPrototypeOf(self),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const withOptions: {
|
||||||
|
<T extends Component.Component<any, any, any, any> & Async>(
|
||||||
|
options: Partial<Async.Options>
|
||||||
|
): (self: T) => T
|
||||||
|
<T extends Component.Component<any, any, any, any> & Async>(
|
||||||
|
self: T,
|
||||||
|
options: Partial<Async.Options>,
|
||||||
|
): T
|
||||||
|
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Async>(
|
||||||
|
self: T,
|
||||||
|
options: Partial<Async.Options>,
|
||||||
|
): T => Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, self, options),
|
||||||
|
Object.getPrototypeOf(self),
|
||||||
|
))
|
||||||
605
packages/effect-fc/src/Component.ts
Normal file
605
packages/effect-fc/src/Component.ts
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
|
||||||
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
|
import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Tracer, type Utils } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { Memoized } from "./index.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
||||||
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
|
export interface Component<P extends {}, A extends React.ReactNode, E, R>
|
||||||
|
extends
|
||||||
|
Effect.Effect<(props: P) => A, never, Exclude<R, Scope.Scope>>,
|
||||||
|
Component.Options
|
||||||
|
{
|
||||||
|
new(_: never): Record<string, never>
|
||||||
|
readonly [TypeId]: TypeId
|
||||||
|
readonly "~Props": P
|
||||||
|
readonly "~Success": A
|
||||||
|
readonly "~Error": E
|
||||||
|
readonly "~Context": R
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
readonly body: (props: P) => Effect.Effect<A, E, R>
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
makeFunctionComponent(
|
||||||
|
runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>
|
||||||
|
): (props: P) => A
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare namespace Component {
|
||||||
|
export type Props<T extends Component<any, any, any, any>> = [T] extends [Component<infer P, infer _A, infer _E, infer _R>] ? P : never
|
||||||
|
export type Success<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer A, infer _E, infer _R>] ? A : never
|
||||||
|
export type Error<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer _A, infer E, infer _R>] ? E : never
|
||||||
|
export type Context<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer _A, infer _E, infer R>] ? R : never
|
||||||
|
|
||||||
|
export type AsComponent<T extends Component<any, any, any, any>> = Component<Props<T>, Success<T>, Error<T>, Context<T>>
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
readonly displayName?: string
|
||||||
|
readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy
|
||||||
|
readonly finalizerExecutionDebounce: Duration.DurationInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ComponentProto = Object.freeze({
|
||||||
|
...Effectable.CommitPrototype,
|
||||||
|
[TypeId]: TypeId,
|
||||||
|
|
||||||
|
commit: Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode, E, R>(
|
||||||
|
this: Component<P, A, E, R>
|
||||||
|
) {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: React ref initialization
|
||||||
|
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
|
||||||
|
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
return yield* React.useState(() => Runtime.runSync(runtimeRef.current)(Effect.cachedFunction(
|
||||||
|
(_services: readonly any[]) => Effect.sync(() => {
|
||||||
|
const f: React.FC<P> = this.makeFunctionComponent(runtimeRef)
|
||||||
|
f.displayName = this.displayName ?? "Anonymous"
|
||||||
|
return Memoized.isMemoized(this)
|
||||||
|
? React.memo(f, this.propsAreEqual)
|
||||||
|
: f
|
||||||
|
}),
|
||||||
|
Equivalence.array(Equivalence.strict()),
|
||||||
|
)))[0](Array.from(
|
||||||
|
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
|
||||||
|
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
||||||
|
this: Component<P, A, E, R>,
|
||||||
|
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||||
|
) {
|
||||||
|
return (props: P) => Runtime.runSync(runtimeRef.current)(
|
||||||
|
Effect.andThen(
|
||||||
|
useScope([], this),
|
||||||
|
scope => Effect.provideService(this.body(props), Scope.Scope, scope),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
} as const)
|
||||||
|
|
||||||
|
const defaultOptions: Component.Options = {
|
||||||
|
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
||||||
|
finalizerExecutionDebounce: "100 millis",
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||||
|
|
||||||
|
|
||||||
|
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||||
|
|
||||||
|
export declare namespace make {
|
||||||
|
export type Gen = {
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A extends React.ReactNode, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>
|
||||||
|
): Component<
|
||||||
|
P, A,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
|
>
|
||||||
|
<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<P, Effect.Effect.Success<Effect.Effect.AsEffect<B>>, Effect.Effect.Error<B>, Effect.Effect.Context<B>>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: 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<P, Effect.Effect.Success<Effect.Effect.AsEffect<C>>, Effect.Effect.Error<C>, Effect.Effect.Context<C>>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: 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<P, Effect.Effect.Success<Effect.Effect.AsEffect<D>>, Effect.Effect.Error<D>, Effect.Effect.Context<D>>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: 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<P, Effect.Effect.Success<Effect.Effect.AsEffect<E>>, Effect.Effect.Error<E>, Effect.Effect.Context<E>>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: 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<P, Effect.Effect.Success<Effect.Effect.AsEffect<F>>, Effect.Effect.Error<F>, Effect.Effect.Context<F>>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: 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<P, Effect.Effect.Success<Effect.Effect.AsEffect<G>>, Effect.Effect.Error<G>, Effect.Effect.Context<G>>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: 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<P, Effect.Effect.Success<Effect.Effect.AsEffect<H>>, Effect.Effect.Error<H>, Effect.Effect.Context<H>>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: 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<P, Effect.Effect.Success<Effect.Effect.AsEffect<I>>, Effect.Effect.Error<I>, Effect.Effect.Context<I>>
|
||||||
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I, J extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
|
a: (
|
||||||
|
_: 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<P, Effect.Effect.Success<Effect.Effect.AsEffect<J>>, Effect.Effect.Error<J>, Effect.Effect.Context<J>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NonGen = {
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
|
body: (props: P) => Eff
|
||||||
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
|
d: (_: D, props: NoInfer<P>) => Eff,
|
||||||
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
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<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
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<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
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<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
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<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, I, P extends {} = {}>(
|
||||||
|
body: (props: P) => A,
|
||||||
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
|
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<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make: (
|
||||||
|
& make.Gen
|
||||||
|
& make.NonGen
|
||||||
|
& ((
|
||||||
|
spanName: string,
|
||||||
|
spanOptions?: Tracer.SpanOptions,
|
||||||
|
) => make.Gen & make.NonGen)
|
||||||
|
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => {
|
||||||
|
if (typeof spanNameOrBody !== "string") {
|
||||||
|
return Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, defaultOptions, {
|
||||||
|
body: Effect.fn(spanNameOrBody as any, ...pipeables),
|
||||||
|
}),
|
||||||
|
ComponentProto,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const spanOptions = pipeables[0]
|
||||||
|
return (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, defaultOptions, {
|
||||||
|
body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []),
|
||||||
|
displayName: spanNameOrBody,
|
||||||
|
}),
|
||||||
|
ComponentProto,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeUntraced: (
|
||||||
|
& make.Gen
|
||||||
|
& make.NonGen
|
||||||
|
& ((name: string) => make.Gen & make.NonGen)
|
||||||
|
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => (
|
||||||
|
typeof spanNameOrBody !== "string"
|
||||||
|
? Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, defaultOptions, {
|
||||||
|
body: Effect.fnUntraced(spanNameOrBody as any, ...pipeables as []),
|
||||||
|
}),
|
||||||
|
ComponentProto,
|
||||||
|
)
|
||||||
|
: (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, defaultOptions, {
|
||||||
|
body: Effect.fnUntraced(body, ...pipeables as []),
|
||||||
|
displayName: spanNameOrBody,
|
||||||
|
}),
|
||||||
|
ComponentProto,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const withOptions: {
|
||||||
|
<T extends Component<any, any, any, any>>(
|
||||||
|
options: Partial<Component.Options>
|
||||||
|
): (self: T) => T
|
||||||
|
<T extends Component<any, any, any, any>>(
|
||||||
|
self: T,
|
||||||
|
options: Partial<Component.Options>,
|
||||||
|
): T
|
||||||
|
} = Function.dual(2, <T extends Component<any, any, any, any>>(
|
||||||
|
self: T,
|
||||||
|
options: Partial<Component.Options>,
|
||||||
|
): T => Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, self, options),
|
||||||
|
Object.getPrototypeOf(self),
|
||||||
|
))
|
||||||
|
|
||||||
|
export const withRuntime: {
|
||||||
|
<P extends {}, A extends React.ReactNode, E, R>(
|
||||||
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
|
): (self: Component<P, A, E, Scope.Scope | NoInfer<R>>) => (props: P) => A
|
||||||
|
<P extends {}, A extends React.ReactNode, E, R>(
|
||||||
|
self: Component<P, A, E, Scope.Scope | NoInfer<R>>,
|
||||||
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
|
): (props: P) => A
|
||||||
|
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R>(
|
||||||
|
self: Component<P, A, E, R>,
|
||||||
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
|
) => function WithRuntime(props: P) {
|
||||||
|
return React.createElement(
|
||||||
|
Runtime.runSync(React.useContext(context))(self),
|
||||||
|
props,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export class ScopeMap extends Effect.Service<ScopeMap>()("@effect-fc/Component/ScopeMap", {
|
||||||
|
effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<object, ScopeMap.Entry>()))
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export declare namespace ScopeMap {
|
||||||
|
export interface Entry {
|
||||||
|
readonly scope: Scope.CloseableScope
|
||||||
|
readonly closeFiber: Option.Option<Fiber.RuntimeFiber<void>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export declare namespace useScope {
|
||||||
|
export interface Options {
|
||||||
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
|
readonly finalizerExecutionDebounce?: Duration.DurationInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useScope = Effect.fnUntraced(function*(
|
||||||
|
deps: React.DependencyList,
|
||||||
|
options?: useScope.Options,
|
||||||
|
): Effect.fn.Return<Scope.Scope> {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
||||||
|
const runtimeRef = React.useRef<Runtime.Runtime<never>>(null!)
|
||||||
|
runtimeRef.current = yield* Effect.runtime()
|
||||||
|
|
||||||
|
const scopeMap = yield* ScopeMap as unknown as Effect.Effect<ScopeMap>
|
||||||
|
|
||||||
|
const [key, scope] = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
|
||||||
|
Effect.all([Effect.succeed({}), scopeMap.ref]),
|
||||||
|
([key, map]) => Effect.andThen(
|
||||||
|
Option.match(HashMap.get(map, key), {
|
||||||
|
onSome: entry => Effect.succeed(entry.scope),
|
||||||
|
onNone: () => Effect.tap(
|
||||||
|
Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy),
|
||||||
|
scope => Ref.update(scopeMap.ref, HashMap.set(key, {
|
||||||
|
scope,
|
||||||
|
closeFiber: Option.none(),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
scope => [key, scope] as const,
|
||||||
|
),
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
||||||
|
)), deps)
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "key"
|
||||||
|
React.useEffect(() => Runtime.runSync(runtimeRef.current)(scopeMap.ref.pipe(
|
||||||
|
Effect.andThen(HashMap.get(key)),
|
||||||
|
Effect.tap(entry => Option.match(entry.closeFiber, {
|
||||||
|
onSome: fiber => Effect.andThen(
|
||||||
|
Ref.update(scopeMap.ref, HashMap.set(key, { ...entry, closeFiber: Option.none() })),
|
||||||
|
Fiber.interruptFork(fiber),
|
||||||
|
),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
})),
|
||||||
|
Effect.map(({ scope }) =>
|
||||||
|
() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
|
||||||
|
Effect.forkDaemon(Effect.sleep(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce).pipe(
|
||||||
|
Effect.andThen(Scope.close(scope, Exit.void)),
|
||||||
|
Effect.andThen(Ref.update(scopeMap.ref, HashMap.remove(key))),
|
||||||
|
)),
|
||||||
|
fiber => Ref.update(scopeMap.ref, HashMap.set(key, {
|
||||||
|
scope,
|
||||||
|
closeFiber: Option.some(fiber),
|
||||||
|
})),
|
||||||
|
))
|
||||||
|
),
|
||||||
|
)), [key])
|
||||||
|
|
||||||
|
return scope
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useOnMount = Effect.fnUntraced(function* <A, E, R>(
|
||||||
|
f: () => Effect.Effect<A, E, R>
|
||||||
|
): Effect.fn.Return<A, E, R> {
|
||||||
|
const runtime = yield* Effect.runtime<R>()
|
||||||
|
return yield* React.useState(() => Runtime.runSync(runtime)(Effect.cached(f())))[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
export declare namespace useOnChange {
|
||||||
|
export interface Options extends useScope.Options {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOnChange = Effect.fnUntraced(function* <A, E, R>(
|
||||||
|
f: () => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
options?: useOnChange.Options,
|
||||||
|
): Effect.fn.Return<A, E, Exclude<R, Scope.Scope>> {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
const scope = yield* useScope(deps, options)
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "scope"
|
||||||
|
return yield* React.useMemo(() => Runtime.runSync(runtime)(
|
||||||
|
Effect.cached(Effect.provideService(f(), Scope.Scope, scope))
|
||||||
|
), [scope])
|
||||||
|
})
|
||||||
|
|
||||||
|
export declare namespace useReactEffect {
|
||||||
|
export interface Options {
|
||||||
|
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||||
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useReactEffect = Effect.fnUntraced(function* <E, R>(
|
||||||
|
f: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: useReactEffect.Options,
|
||||||
|
): Effect.fn.Return<void, never, Exclude<R, Scope.Scope>> {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
||||||
|
React.useEffect(() => runReactEffect(runtime, f, options), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
const runReactEffect = <E, R>(
|
||||||
|
runtime: Runtime.Runtime<Exclude<R, Scope.Scope>>,
|
||||||
|
f: () => Effect.Effect<void, E, R>,
|
||||||
|
options?: useReactEffect.Options,
|
||||||
|
) => Effect.Do.pipe(
|
||||||
|
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)),
|
||||||
|
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(f(), Scope.Scope, scope))),
|
||||||
|
Effect.map(({ scope }) =>
|
||||||
|
() => {
|
||||||
|
switch (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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
)
|
||||||
|
|
||||||
|
export declare namespace useReactLayoutEffect {
|
||||||
|
export interface Options extends useReactEffect.Options {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useReactLayoutEffect = Effect.fnUntraced(function* <E, R>(
|
||||||
|
f: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: useReactLayoutEffect.Options,
|
||||||
|
): Effect.fn.Return<void, never, Exclude<R, Scope.Scope>> {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
||||||
|
React.useLayoutEffect(() => runReactEffect(runtime, f, options), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useRunSync = <R = never>(): Effect.Effect<
|
||||||
|
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => A,
|
||||||
|
never,
|
||||||
|
Scope.Scope | R
|
||||||
|
> => Effect.andThen(Effect.runtime(), Runtime.runSync)
|
||||||
|
|
||||||
|
export const useRunPromise = <R = never>(): Effect.Effect<
|
||||||
|
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => Promise<A>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | R
|
||||||
|
> => Effect.andThen(Effect.runtime(), context => Runtime.runPromise(context))
|
||||||
|
|
||||||
|
export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||||
|
f: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.fn.Return<(...args: Args) => A, never, R> {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
||||||
|
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
|
||||||
|
runtimeRef.current = yield* Effect.runtime<R>()
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
||||||
|
return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(f(...args)), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useCallbackPromise = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||||
|
f: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.fn.Return<(...args: Args) => Promise<A>, never, R> {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
||||||
|
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
|
||||||
|
runtimeRef.current = yield* Effect.runtime<R>()
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
||||||
|
return React.useCallback((...args: Args) => Runtime.runPromise(runtimeRef.current)(f(...args)), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export declare namespace useContext {
|
||||||
|
export interface Options extends useOnChange.Options {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useContext = <ROut, E, RIn>(
|
||||||
|
layer: Layer.Layer<ROut, E, RIn>,
|
||||||
|
options?: useContext.Options,
|
||||||
|
): Effect.Effect<Context.Context<ROut>, E, Scope.Scope | RIn> => useOnChange(() => Effect.context<RIn>().pipe(
|
||||||
|
Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))),
|
||||||
|
Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
|
||||||
|
Effect.andThen(runtime => runtime.runtimeEffect),
|
||||||
|
Effect.andThen(runtime => runtime.context),
|
||||||
|
), [layer], options)
|
||||||
62
packages/effect-fc/src/ErrorObserver.ts
Normal file
62
packages/effect-fc/src/ErrorObserver.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { type Cause, Context, Effect, Exit, Layer, Option, Pipeable, Predicate, PubSub, type Queue, type Scope, Supervisor } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export const TypeId: unique symbol = Symbol.for("@effect-fc/ErrorObserver/ErrorObserver")
|
||||||
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
|
export interface ErrorObserver<in out E = never> extends Pipeable.Pipeable {
|
||||||
|
readonly [TypeId]: TypeId
|
||||||
|
handle<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||||
|
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorObserver = <E = never>(): Context.Tag<ErrorObserver, ErrorObserver<E>> => Context.GenericTag("@effect-fc/ErrorObserver/ErrorObserver")
|
||||||
|
|
||||||
|
class ErrorObserverImpl<in out E = never>
|
||||||
|
extends Pipeable.Class() implements ErrorObserver<E> {
|
||||||
|
readonly [TypeId]: TypeId = TypeId
|
||||||
|
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly pubsub: PubSub.PubSub<Cause.Cause<E>>
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.subscribe = pubsub.subscribe
|
||||||
|
}
|
||||||
|
|
||||||
|
handle<A, EffE, R>(effect: Effect.Effect<A, EffE, R>): Effect.Effect<A, EffE, R> {
|
||||||
|
return Effect.tapErrorCause(effect, cause => PubSub.publish(this.pubsub, cause as Cause.Cause<E>))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor<void> {
|
||||||
|
readonly value = Effect.void
|
||||||
|
constructor(readonly pubsub: PubSub.PubSub<Cause.Cause<never>>) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnd<A, E>(_value: Exit.Exit<A, E>): void {
|
||||||
|
if (Exit.isFailure(_value)) {
|
||||||
|
Effect.runSync(PubSub.publish(this.pubsub, _value.cause as Cause.Cause<never>))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const isErrorObserver = (u: unknown): u is ErrorObserver<unknown> => Predicate.hasProperty(u, TypeId)
|
||||||
|
|
||||||
|
export const layer: Layer.Layer<ErrorObserver> = Layer.unwrapEffect(Effect.map(
|
||||||
|
PubSub.unbounded<Cause.Cause<never>>(),
|
||||||
|
pubsub => Layer.merge(
|
||||||
|
Supervisor.addSupervisor(new ErrorObserverSupervisorImpl(pubsub)),
|
||||||
|
Layer.succeed(ErrorObserver(), new ErrorObserverImpl(pubsub)),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
export const handle = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => Effect.andThen(
|
||||||
|
Effect.serviceOption(ErrorObserver()),
|
||||||
|
Option.match({
|
||||||
|
onSome: observer => observer.handle(effect),
|
||||||
|
onNone: () => effect,
|
||||||
|
}),
|
||||||
|
)
|
||||||
396
packages/effect-fc/src/Form.ts
Normal file
396
packages/effect-fc/src/Form.ts
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, flow, Hash, HashMap, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect"
|
||||||
|
import type * as React from "react"
|
||||||
|
import * as Component from "./Component.js"
|
||||||
|
import * as Mutation from "./Mutation.js"
|
||||||
|
import * as PropertyPath from "./PropertyPath.js"
|
||||||
|
import * as Result from "./Result.js"
|
||||||
|
import * as Subscribable from "./Subscribable.js"
|
||||||
|
import * as SubscriptionRef from "./SubscriptionRef.js"
|
||||||
|
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
|
||||||
|
export type FormTypeId = typeof FormTypeId
|
||||||
|
|
||||||
|
export interface Form<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||||
|
extends Pipeable.Pipeable {
|
||||||
|
readonly [FormTypeId]: FormTypeId
|
||||||
|
|
||||||
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
|
readonly context: Context.Context<Scope.Scope | R>
|
||||||
|
readonly mutation: Mutation.Mutation<
|
||||||
|
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
|
||||||
|
MA, ME, MR, MP
|
||||||
|
>
|
||||||
|
readonly autosubmit: boolean
|
||||||
|
readonly debounce: Option.Option<Duration.DurationInput>
|
||||||
|
|
||||||
|
readonly value: Subscribable.Subscribable<Option.Option<A>>
|
||||||
|
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
||||||
|
readonly error: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
|
||||||
|
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>
|
||||||
|
|
||||||
|
readonly canSubmit: Subscribable.Subscribable<boolean>
|
||||||
|
|
||||||
|
field<const P extends PropertyPath.Paths<I>>(
|
||||||
|
path: P
|
||||||
|
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>>
|
||||||
|
|
||||||
|
readonly run: Effect.Effect<void>
|
||||||
|
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormImpl<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||||
|
extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
||||||
|
readonly [FormTypeId]: FormTypeId = FormTypeId
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly schema: Schema.Schema<A, I, R>,
|
||||||
|
readonly context: Context.Context<Scope.Scope | R>,
|
||||||
|
readonly mutation: Mutation.Mutation<
|
||||||
|
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
|
||||||
|
MA, ME, MR, MP
|
||||||
|
>,
|
||||||
|
readonly autosubmit: boolean,
|
||||||
|
readonly debounce: Option.Option<Duration.DurationInput>,
|
||||||
|
|
||||||
|
readonly value: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
||||||
|
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
||||||
|
readonly error: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
||||||
|
readonly validationFiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>,
|
||||||
|
|
||||||
|
readonly runSemaphore: Effect.Semaphore,
|
||||||
|
readonly fieldCache: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.canSubmit = Subscribable.map(
|
||||||
|
Subscribable.zipLatestAll(this.value, this.error, this.validationFiber, this.mutation.result),
|
||||||
|
([value, error, validationFiber, result]) => (
|
||||||
|
Option.isSome(value) &&
|
||||||
|
Option.isNone(error) &&
|
||||||
|
Option.isNone(validationFiber) &&
|
||||||
|
!(Result.isRunning(result) || Result.hasRefreshingFlag(result))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
field<const P extends PropertyPath.Paths<I>>(
|
||||||
|
path: P
|
||||||
|
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>> {
|
||||||
|
const key = new FormFieldKey(path)
|
||||||
|
return this.fieldCache.pipe(
|
||||||
|
Effect.map(HashMap.get(key)),
|
||||||
|
Effect.flatMap(Option.match({
|
||||||
|
onSome: v => Effect.succeed(v as FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>),
|
||||||
|
onNone: () => Effect.tap(
|
||||||
|
Effect.succeed(makeFormField(this as Form<A, I, R, MA, ME, MR, MP>, path)),
|
||||||
|
v => Ref.update(this.fieldCache, HashMap.set(key, v as FormField<unknown, unknown>)),
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly canSubmit: Subscribable.Subscribable<boolean>
|
||||||
|
|
||||||
|
get run(): Effect.Effect<void> {
|
||||||
|
return this.runSemaphore.withPermits(1)(Stream.runForEach(
|
||||||
|
this.encodedValue.changes.pipe(
|
||||||
|
Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity
|
||||||
|
),
|
||||||
|
|
||||||
|
encodedValue => this.validationFiber.pipe(
|
||||||
|
Effect.andThen(Option.match({
|
||||||
|
onSome: Fiber.interrupt,
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
})),
|
||||||
|
Effect.andThen(
|
||||||
|
Effect.forkScoped(Effect.onExit(
|
||||||
|
Schema.decode(this.schema, { errors: "all" })(encodedValue),
|
||||||
|
exit => Effect.andThen(
|
||||||
|
Exit.matchEffect(exit, {
|
||||||
|
onSuccess: v => Effect.andThen(
|
||||||
|
Ref.set(this.value, Option.some(v)),
|
||||||
|
Ref.set(this.error, Option.none()),
|
||||||
|
),
|
||||||
|
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
||||||
|
onSome: e => Ref.set(this.error, Option.some(e)),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Ref.set(this.validationFiber, Option.none()),
|
||||||
|
),
|
||||||
|
)).pipe(
|
||||||
|
Effect.tap(fiber => Ref.set(this.validationFiber, Option.some(fiber))),
|
||||||
|
Effect.andThen(Fiber.join),
|
||||||
|
Effect.andThen(value => this.autosubmit
|
||||||
|
? Effect.asVoid(Effect.forkScoped(this.submitValue(value)))
|
||||||
|
: Effect.void
|
||||||
|
),
|
||||||
|
Effect.forkScoped,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
|
||||||
|
return this.value.pipe(
|
||||||
|
Effect.andThen(identity),
|
||||||
|
Effect.andThen(value => this.submitValue(value)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
submitValue(value: A): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>> {
|
||||||
|
return Effect.whenEffect(
|
||||||
|
Effect.tap(
|
||||||
|
this.mutation.mutate([value, this as any]),
|
||||||
|
result => Result.isFailure(result)
|
||||||
|
? Option.match(
|
||||||
|
Chunk.findFirst(
|
||||||
|
Cause.failures(result.cause as Cause.Cause<ParseResult.ParseError>),
|
||||||
|
e => e._tag === "ParseError",
|
||||||
|
),
|
||||||
|
{
|
||||||
|
onSome: e => Ref.set(this.error, Option.some(e)),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Effect.void
|
||||||
|
),
|
||||||
|
this.canSubmit.get,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isForm = (u: unknown): u is Form<unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
|
||||||
|
|
||||||
|
export declare namespace make {
|
||||||
|
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||||
|
extends Mutation.make.Options<
|
||||||
|
readonly [value: NoInfer<A>, form: Form<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>],
|
||||||
|
MA, ME, MR, MP
|
||||||
|
> {
|
||||||
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
|
readonly initialEncodedValue: NoInfer<I>
|
||||||
|
readonly autosubmit?: boolean
|
||||||
|
readonly debounce?: Duration.DurationInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
|
||||||
|
options: make.Options<A, I, R, MA, ME, MR, MP>
|
||||||
|
): Effect.fn.Return<
|
||||||
|
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
||||||
|
> {
|
||||||
|
return new FormImpl(
|
||||||
|
options.schema,
|
||||||
|
yield* Effect.context<Scope.Scope | R>(),
|
||||||
|
yield* Mutation.make(options),
|
||||||
|
options.autosubmit ?? false,
|
||||||
|
Option.fromNullable(options.debounce),
|
||||||
|
|
||||||
|
yield* SubscriptionRef.make(Option.none<A>()),
|
||||||
|
yield* SubscriptionRef.make(options.initialEncodedValue),
|
||||||
|
yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()),
|
||||||
|
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()),
|
||||||
|
|
||||||
|
yield* Effect.makeSemaphore(1),
|
||||||
|
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export declare namespace service {
|
||||||
|
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||||
|
extends make.Options<A, I, R, MA, ME, MR, MP> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const service = <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
|
||||||
|
options: service.Options<A, I, R, MA, ME, MR, MP>
|
||||||
|
): Effect.Effect<
|
||||||
|
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
||||||
|
> => Effect.tap(
|
||||||
|
make(options),
|
||||||
|
form => Effect.forkScoped(form.run),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const FormFieldTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormField")
|
||||||
|
export type FormFieldTypeId = typeof FormFieldTypeId
|
||||||
|
|
||||||
|
export interface FormField<in out A, in out I = A>
|
||||||
|
extends Pipeable.Pipeable {
|
||||||
|
readonly [FormFieldTypeId]: FormFieldTypeId
|
||||||
|
|
||||||
|
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>
|
||||||
|
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
||||||
|
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
||||||
|
readonly isValidating: Subscribable.Subscribable<boolean>
|
||||||
|
readonly isSubmitting: Subscribable.Subscribable<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
class FormFieldImpl<in out A, in out I = A>
|
||||||
|
extends Pipeable.Class() implements FormField<A, I> {
|
||||||
|
readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>,
|
||||||
|
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
||||||
|
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
|
||||||
|
readonly isValidating: Subscribable.Subscribable<boolean>,
|
||||||
|
readonly isSubmitting: Subscribable.Subscribable<boolean>,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFieldKeyTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormFieldKey")
|
||||||
|
type FormFieldKeyTypeId = typeof FormFieldKeyTypeId
|
||||||
|
|
||||||
|
class FormFieldKey implements Equal.Equal {
|
||||||
|
readonly [FormFieldKeyTypeId]: FormFieldKeyTypeId = FormFieldKeyTypeId
|
||||||
|
constructor(readonly path: PropertyPath.PropertyPath) {}
|
||||||
|
|
||||||
|
[Equal.symbol](that: Equal.Equal) {
|
||||||
|
return isFormFieldKey(that) && PropertyPath.equivalence(this.path, that.path)
|
||||||
|
}
|
||||||
|
[Hash.symbol]() {
|
||||||
|
return Hash.array(this.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isFormField = (u: unknown): u is FormField<unknown, unknown> => Predicate.hasProperty(u, FormFieldTypeId)
|
||||||
|
const isFormFieldKey = (u: unknown): u is FormFieldKey => Predicate.hasProperty(u, FormFieldKeyTypeId)
|
||||||
|
|
||||||
|
export const makeFormField = <A, I, R, MA, ME, MR, MP, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
||||||
|
self: Form<A, I, R, MA, ME, MR, MP>,
|
||||||
|
path: P,
|
||||||
|
): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => {
|
||||||
|
return new FormFieldImpl(
|
||||||
|
Subscribable.mapEffect(self.value, Option.match({
|
||||||
|
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
|
||||||
|
onNone: () => Option.some(Option.none()),
|
||||||
|
})),
|
||||||
|
SubscriptionSubRef.makeFromPath(self.encodedValue, path),
|
||||||
|
Subscribable.mapEffect(self.error, Option.match({
|
||||||
|
onSome: flow(
|
||||||
|
ParseResult.ArrayFormatter.formatError,
|
||||||
|
Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))),
|
||||||
|
),
|
||||||
|
onNone: () => Effect.succeed([]),
|
||||||
|
})),
|
||||||
|
Subscribable.map(self.validationFiber, Option.isSome),
|
||||||
|
Subscribable.map(self.mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export namespace useInput {
|
||||||
|
export interface Options {
|
||||||
|
readonly debounce?: Duration.DurationInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Success<T> {
|
||||||
|
readonly value: T
|
||||||
|
readonly setValue: React.Dispatch<React.SetStateAction<T>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInput = Effect.fnUntraced(function* <A, I>(
|
||||||
|
field: FormField<A, I>,
|
||||||
|
options?: useInput.Options,
|
||||||
|
): Effect.fn.Return<useInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
|
||||||
|
const internalValueRef = yield* Component.useOnChange(() => Effect.tap(
|
||||||
|
Effect.andThen(field.encodedValue, SubscriptionRef.make),
|
||||||
|
internalValueRef => Effect.forkScoped(Effect.all([
|
||||||
|
Stream.runForEach(
|
||||||
|
Stream.drop(field.encodedValue, 1),
|
||||||
|
upstreamEncodedValue => Effect.whenEffect(
|
||||||
|
Ref.set(internalValueRef, upstreamEncodedValue),
|
||||||
|
Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Stream.runForEach(
|
||||||
|
internalValueRef.changes.pipe(
|
||||||
|
Stream.drop(1),
|
||||||
|
Stream.changesWith(Equal.equivalence()),
|
||||||
|
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
||||||
|
),
|
||||||
|
internalValue => Ref.set(field.encodedValue, internalValue),
|
||||||
|
),
|
||||||
|
], { concurrency: "unbounded" })),
|
||||||
|
), [field, options?.debounce])
|
||||||
|
|
||||||
|
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef)
|
||||||
|
return { value, setValue }
|
||||||
|
})
|
||||||
|
|
||||||
|
export namespace useOptionalInput {
|
||||||
|
export interface Options<T> extends useInput.Options {
|
||||||
|
readonly defaultValue: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Success<T> extends useInput.Success<T> {
|
||||||
|
readonly enabled: boolean
|
||||||
|
readonly setEnabled: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
|
||||||
|
field: FormField<A, Option.Option<I>>,
|
||||||
|
options: useOptionalInput.Options<I>,
|
||||||
|
): Effect.fn.Return<useOptionalInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
|
||||||
|
const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap(
|
||||||
|
Effect.andThen(
|
||||||
|
field.encodedValue,
|
||||||
|
Option.match({
|
||||||
|
onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]),
|
||||||
|
onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([
|
||||||
|
Stream.runForEach(
|
||||||
|
Stream.drop(field.encodedValue, 1),
|
||||||
|
|
||||||
|
upstreamEncodedValue => Effect.whenEffect(
|
||||||
|
Option.match(upstreamEncodedValue, {
|
||||||
|
onSome: v => Effect.andThen(
|
||||||
|
Ref.set(enabledRef, true),
|
||||||
|
Ref.set(internalValueRef, v),
|
||||||
|
),
|
||||||
|
onNone: () => Effect.andThen(
|
||||||
|
Ref.set(enabledRef, false),
|
||||||
|
Ref.set(internalValueRef, options.defaultValue),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Effect.andThen(
|
||||||
|
Effect.all([enabledRef, internalValueRef]),
|
||||||
|
([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Stream.runForEach(
|
||||||
|
enabledRef.changes.pipe(
|
||||||
|
Stream.zipLatest(internalValueRef.changes),
|
||||||
|
Stream.drop(1),
|
||||||
|
Stream.changesWith(Equal.equivalence()),
|
||||||
|
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
||||||
|
),
|
||||||
|
([enabled, internalValue]) => Ref.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
||||||
|
),
|
||||||
|
], { concurrency: "unbounded" })),
|
||||||
|
), [field, options.debounce])
|
||||||
|
|
||||||
|
const [enabled, setEnabled] = yield* SubscriptionRef.useSubscriptionRefState(enabledRef)
|
||||||
|
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef)
|
||||||
|
return { enabled, setEnabled, value, setValue }
|
||||||
|
})
|
||||||
51
packages/effect-fc/src/Memoized.ts
Normal file
51
packages/effect-fc/src/Memoized.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
|
import { type Equivalence, Function, Predicate } from "effect"
|
||||||
|
import type * as Component from "./Component.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const TypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized")
|
||||||
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
|
export interface Memoized<P> extends Memoized.Options<P> {
|
||||||
|
readonly [TypeId]: TypeId
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Memoized {
|
||||||
|
export interface Options<P> {
|
||||||
|
readonly propsAreEqual?: Equivalence.Equivalence<P>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const MemoizedProto = Object.freeze({
|
||||||
|
[TypeId]: TypeId
|
||||||
|
} as const)
|
||||||
|
|
||||||
|
|
||||||
|
export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasProperty(u, TypeId)
|
||||||
|
|
||||||
|
export const memoized = <T extends Component.Component<any, any, any, any>>(
|
||||||
|
self: T
|
||||||
|
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, self),
|
||||||
|
Object.freeze(Object.setPrototypeOf(
|
||||||
|
Object.assign({}, MemoizedProto),
|
||||||
|
Object.getPrototypeOf(self),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const withOptions: {
|
||||||
|
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
||||||
|
options: Partial<Memoized.Options<Component.Component.Props<T>>>
|
||||||
|
): (self: T) => T
|
||||||
|
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
||||||
|
self: T,
|
||||||
|
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
||||||
|
): T
|
||||||
|
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
||||||
|
self: T,
|
||||||
|
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
||||||
|
): T => Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, self, options),
|
||||||
|
Object.getPrototypeOf(self),
|
||||||
|
))
|
||||||
128
packages/effect-fc/src/Mutation.ts
Normal file
128
packages/effect-fc/src/Mutation.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { type Context, Effect, Equal, type Fiber, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect"
|
||||||
|
import * as Result from "./Result.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const MutationTypeId: unique symbol = Symbol.for("@effect-fc/Mutation/Mutation")
|
||||||
|
export type MutationTypeId = typeof MutationTypeId
|
||||||
|
|
||||||
|
export interface Mutation<in out K extends Mutation.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
|
||||||
|
extends Pipeable.Pipeable {
|
||||||
|
readonly [MutationTypeId]: MutationTypeId
|
||||||
|
|
||||||
|
readonly context: Context.Context<Scope.Scope | R>
|
||||||
|
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||||
|
readonly initialProgress: P
|
||||||
|
|
||||||
|
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
||||||
|
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
||||||
|
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
|
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
||||||
|
|
||||||
|
mutate(key: K): Effect.Effect<Result.Final<A, E, P>>
|
||||||
|
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare namespace Mutation {
|
||||||
|
export type AnyKey = readonly any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MutationImpl<in out K extends Mutation.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
|
||||||
|
extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
|
||||||
|
readonly [MutationTypeId]: MutationTypeId = MutationTypeId
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly context: Context.Context<Scope.Scope | R>,
|
||||||
|
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
||||||
|
readonly initialProgress: P,
|
||||||
|
|
||||||
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
||||||
|
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
||||||
|
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
||||||
|
readonly latestFinalResult: SubscriptionRef.SubscriptionRef<Option.Option<Result.Final<A, E, P>>>,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
mutate(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
|
return SubscriptionRef.set(this.latestKey, Option.some(key)).pipe(
|
||||||
|
Effect.andThen(this.start(key)),
|
||||||
|
Effect.andThen(sub => this.watch(sub)),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
||||||
|
return SubscriptionRef.set(this.latestKey, Option.some(key)).pipe(
|
||||||
|
Effect.andThen(this.start(key)),
|
||||||
|
Effect.tap(sub => Effect.forkScoped(this.watch(sub))),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
start(key: K): Effect.Effect<
|
||||||
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | R
|
||||||
|
> {
|
||||||
|
return this.latestFinalResult.pipe(
|
||||||
|
Effect.andThen(initial => Result.unsafeForkEffect(
|
||||||
|
Effect.onExit(this.f(key), () => Effect.andThen(
|
||||||
|
Effect.all([Effect.fiberId, this.fiber]),
|
||||||
|
([currentFiberId, fiber]) => Option.match(fiber, {
|
||||||
|
onSome: v => Equal.equals(currentFiberId, v.id())
|
||||||
|
? SubscriptionRef.set(this.fiber, Option.none())
|
||||||
|
: Effect.void,
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
|
||||||
|
{
|
||||||
|
initial: Option.isSome(initial) ? Result.willFetch(initial.value) : Result.initial(),
|
||||||
|
initialProgress: this.initialProgress,
|
||||||
|
} as Result.unsafeForkEffect.Options<A, E, P>,
|
||||||
|
)),
|
||||||
|
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
||||||
|
Effect.map(([sub]) => sub),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
|
): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
|
return sub.get.pipe(
|
||||||
|
Effect.andThen(initial => Stream.runFoldEffect(
|
||||||
|
sub.changes,
|
||||||
|
initial,
|
||||||
|
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
||||||
|
) as Effect.Effect<Result.Final<A, E, P>>),
|
||||||
|
Effect.tap(result => SubscriptionRef.set(this.latestFinalResult, Option.some(result))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMutation = (u: unknown): u is Mutation<readonly unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, MutationTypeId)
|
||||||
|
|
||||||
|
export declare namespace make {
|
||||||
|
export interface Options<K extends Mutation.AnyKey = never, A = void, E = never, R = never, P = never> {
|
||||||
|
readonly f: (key: K) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
||||||
|
readonly initialProgress?: P
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = Effect.fnUntraced(function* <const K extends Mutation.AnyKey = never, A = void, E = never, R = never, P = never>(
|
||||||
|
options: make.Options<K, A, E, R, P>
|
||||||
|
): Effect.fn.Return<
|
||||||
|
Mutation<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
|
||||||
|
> {
|
||||||
|
return new MutationImpl(
|
||||||
|
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
||||||
|
options.f as any,
|
||||||
|
options.initialProgress as P,
|
||||||
|
|
||||||
|
yield* SubscriptionRef.make(Option.none<K>()),
|
||||||
|
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
||||||
|
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
||||||
|
yield* SubscriptionRef.make(Option.none<Result.Final<A, E, P>>()),
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -1,34 +1,34 @@
|
|||||||
import { Array, Function, Option, Predicate } from "effect"
|
import { Array, Equivalence, Function, Option, Predicate } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export type PropertyPath = readonly PropertyKey[]
|
||||||
|
|
||||||
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
|
|
||||||
export type Paths<T, D extends number = 5, Seen = never> = [] | (
|
export type Paths<T, D extends number = 5, Seen = never> = readonly [] | (
|
||||||
D extends never ? [] :
|
D extends never ? readonly [] :
|
||||||
T extends Seen ? [] :
|
T extends Seen ? readonly [] :
|
||||||
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
|
T extends readonly any[] ? {
|
||||||
T extends object ? ObjectPaths<T, D, Seen | T> :
|
[K in keyof T as K extends number ? K : never]:
|
||||||
|
| readonly [K]
|
||||||
|
| readonly [K, ...Paths<T[K], Prev[D], Seen | T>]
|
||||||
|
} extends infer O
|
||||||
|
? O[keyof O]
|
||||||
|
: never
|
||||||
|
:
|
||||||
|
T extends object ? {
|
||||||
|
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
||||||
|
NonNullable<T[K]> extends infer V
|
||||||
|
? readonly [K] | readonly [K, ...Paths<V, Prev[D], Seen>]
|
||||||
|
: never
|
||||||
|
} extends infer O
|
||||||
|
? O[keyof O]
|
||||||
|
: never
|
||||||
|
:
|
||||||
never
|
never
|
||||||
)
|
)
|
||||||
|
|
||||||
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
|
export type ValueFromPath<T, P extends readonly any[]> = P extends readonly [infer Head, ...infer Tail]
|
||||||
[K in keyof T as K extends number ? K : never]:
|
|
||||||
| [K]
|
|
||||||
| [K, ...Paths<T[K], Prev[D], Seen>]
|
|
||||||
} extends infer O
|
|
||||||
? O[keyof O]
|
|
||||||
: never
|
|
||||||
|
|
||||||
export type ObjectPaths<T extends object, D extends number, Seen> = {
|
|
||||||
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
|
||||||
NonNullable<T[K]> extends infer V
|
|
||||||
? [K] | [K, ...Paths<V, Prev[D], Seen>]
|
|
||||||
: never
|
|
||||||
} extends infer O
|
|
||||||
? O[keyof O]
|
|
||||||
: never
|
|
||||||
|
|
||||||
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
|
|
||||||
? Head extends keyof T
|
? Head extends keyof T
|
||||||
? ValueFromPath<T[Head], Tail>
|
? ValueFromPath<T[Head], Tail>
|
||||||
: T extends readonly any[]
|
: T extends readonly any[]
|
||||||
@@ -38,9 +38,8 @@ export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer
|
|||||||
: never
|
: never
|
||||||
: T
|
: T
|
||||||
|
|
||||||
export type AnyKey = string | number | symbol
|
|
||||||
export type AnyPath = readonly AnyKey[]
|
|
||||||
|
|
||||||
|
export const equivalence: Equivalence.Equivalence<PropertyPath> = Equivalence.array(Equivalence.strict())
|
||||||
|
|
||||||
export const unsafeGet: {
|
export const unsafeGet: {
|
||||||
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
||||||
@@ -65,16 +64,16 @@ export const get: {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const immutableSet: {
|
export const immutableSet: {
|
||||||
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
|
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => Option.Option<T>
|
||||||
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
||||||
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
||||||
const key = Array.head(path as AnyPath)
|
const key = Array.head(path as PropertyPath)
|
||||||
if (Option.isNone(key))
|
if (Option.isNone(key))
|
||||||
return Option.some(value as T)
|
return Option.some(value as T)
|
||||||
if (!Predicate.hasProperty(self, key.value))
|
if (!Predicate.hasProperty(self, key.value))
|
||||||
return Option.none()
|
return Option.none()
|
||||||
|
|
||||||
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
|
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as PropertyPath)), value)
|
||||||
if (Option.isNone(child))
|
if (Option.isNone(child))
|
||||||
return child
|
return child
|
||||||
|
|
||||||
14
packages/effect-fc/src/PubSub.ts
Normal file
14
packages/effect-fc/src/PubSub.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Effect, PubSub, type Scope } from "effect"
|
||||||
|
import type * as React from "react"
|
||||||
|
import * as Component from "./Component.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const usePubSubFromReactiveValues = Effect.fnUntraced(function* <const A extends React.DependencyList>(
|
||||||
|
values: A
|
||||||
|
): Effect.fn.Return<PubSub.PubSub<A>, never, Scope.Scope> {
|
||||||
|
const pubsub = yield* Component.useOnMount(() => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown))
|
||||||
|
yield* Component.useReactEffect(() => Effect.unlessEffect(PubSub.publish(pubsub, values), PubSub.isShutdown(pubsub)), values)
|
||||||
|
return pubsub
|
||||||
|
})
|
||||||
|
|
||||||
|
export * from "effect/PubSub"
|
||||||
319
packages/effect-fc/src/Query.ts
Normal file
319
packages/effect-fc/src/Query.ts
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
import { type Cause, type Context, DateTime, type Duration, Effect, Equal, Equivalence, Fiber, HashMap, identity, Option, Pipeable, Predicate, type Scope, Stream, Subscribable, SubscriptionRef } from "effect"
|
||||||
|
import * as QueryClient from "./QueryClient.js"
|
||||||
|
import * as Result from "./Result.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
|
||||||
|
export type QueryTypeId = typeof QueryTypeId
|
||||||
|
|
||||||
|
export interface Query<in out K extends Query.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
|
||||||
|
extends Pipeable.Pipeable {
|
||||||
|
readonly [QueryTypeId]: QueryTypeId
|
||||||
|
|
||||||
|
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>
|
||||||
|
readonly key: Stream.Stream<K>
|
||||||
|
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||||
|
readonly initialProgress: P
|
||||||
|
|
||||||
|
readonly staleTime: Duration.DurationInput
|
||||||
|
readonly refreshOnWindowFocus: boolean
|
||||||
|
|
||||||
|
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
||||||
|
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
||||||
|
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
|
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
||||||
|
|
||||||
|
readonly run: Effect.Effect<void>
|
||||||
|
fetch(key: K): Effect.Effect<Result.Final<A, E, P>>
|
||||||
|
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
||||||
|
readonly refresh: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
|
||||||
|
readonly refreshSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
|
||||||
|
|
||||||
|
readonly invalidateCache: Effect.Effect<void>
|
||||||
|
invalidateCacheEntry(key: K): Effect.Effect<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare namespace Query {
|
||||||
|
export type AnyKey = readonly any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryImpl<in out K extends Query.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
|
||||||
|
extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
||||||
|
readonly [QueryTypeId]: QueryTypeId = QueryTypeId
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>,
|
||||||
|
readonly key: Stream.Stream<K>,
|
||||||
|
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
||||||
|
readonly initialProgress: P,
|
||||||
|
|
||||||
|
readonly staleTime: Duration.DurationInput,
|
||||||
|
readonly refreshOnWindowFocus: boolean,
|
||||||
|
|
||||||
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
||||||
|
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
||||||
|
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
||||||
|
readonly latestFinalResult: SubscriptionRef.SubscriptionRef<Option.Option<Result.Final<A, E, P>>>,
|
||||||
|
|
||||||
|
readonly runSemaphore: Effect.Semaphore,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
get run(): Effect.Effect<void> {
|
||||||
|
return Effect.all([
|
||||||
|
Stream.runForEach(this.key, key => this.fetchSubscribable(key)),
|
||||||
|
|
||||||
|
Effect.promise(() => import("@effect/platform-browser")).pipe(
|
||||||
|
Effect.andThen(({ BrowserStream }) => this.refreshOnWindowFocus
|
||||||
|
? Stream.runForEach(
|
||||||
|
BrowserStream.fromEventListenerWindow("focus"),
|
||||||
|
() => this.refreshSubscribable,
|
||||||
|
)
|
||||||
|
: Effect.void
|
||||||
|
),
|
||||||
|
Effect.catchAllDefect(() => Effect.void),
|
||||||
|
),
|
||||||
|
], { concurrency: "unbounded" }).pipe(
|
||||||
|
Effect.ignore,
|
||||||
|
this.runSemaphore.withPermits(1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get interrupt(): Effect.Effect<void, never, never> {
|
||||||
|
return Effect.andThen(this.fiber, Option.match({
|
||||||
|
onSome: Fiber.interrupt,
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
|
return this.interrupt.pipe(
|
||||||
|
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
||||||
|
Effect.andThen(this.latestFinalResult),
|
||||||
|
Effect.andThen(previous => this.startCached(key, Option.isSome(previous)
|
||||||
|
? Result.willFetch(previous.value) as Result.Final<A, E, P>
|
||||||
|
: Result.initial()
|
||||||
|
)),
|
||||||
|
Effect.andThen(sub => this.watch(key, sub)),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
||||||
|
return this.interrupt.pipe(
|
||||||
|
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
||||||
|
Effect.andThen(this.latestFinalResult),
|
||||||
|
Effect.andThen(previous => this.startCached(key, Option.isSome(previous)
|
||||||
|
? Result.willFetch(previous.value) as Result.Final<A, E, P>
|
||||||
|
: Result.initial()
|
||||||
|
)),
|
||||||
|
Effect.tap(sub => Effect.forkScoped(this.watch(key, sub))),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get refresh(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> {
|
||||||
|
return this.interrupt.pipe(
|
||||||
|
Effect.andThen(Effect.Do),
|
||||||
|
Effect.bind("latestKey", () => Effect.andThen(this.latestKey, identity)),
|
||||||
|
Effect.bind("latestFinalResult", () => this.latestFinalResult),
|
||||||
|
Effect.bind("subscribable", ({ latestKey, latestFinalResult }) =>
|
||||||
|
this.startCached(latestKey, Option.isSome(latestFinalResult)
|
||||||
|
? Result.willRefresh(latestFinalResult.value) as Result.Final<A, E, P>
|
||||||
|
: Result.initial()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Effect.andThen(({ latestKey, subscribable }) => this.watch(latestKey, subscribable)),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get refreshSubscribable(): Effect.Effect<
|
||||||
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
|
Cause.NoSuchElementException
|
||||||
|
> {
|
||||||
|
return this.interrupt.pipe(
|
||||||
|
Effect.andThen(Effect.Do),
|
||||||
|
Effect.bind("latestKey", () => Effect.andThen(this.latestKey, identity)),
|
||||||
|
Effect.bind("latestFinalResult", () => this.latestFinalResult),
|
||||||
|
Effect.bind("subscribable", ({ latestKey, latestFinalResult }) =>
|
||||||
|
this.startCached(latestKey, Option.isSome(latestFinalResult)
|
||||||
|
? Result.willRefresh(latestFinalResult.value) as Result.Final<A, E, P>
|
||||||
|
: Result.initial()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Effect.tap(({ latestKey, subscribable }) => Effect.forkScoped(this.watch(latestKey, subscribable))),
|
||||||
|
Effect.map(({ subscribable }) => subscribable),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
startCached(
|
||||||
|
key: K,
|
||||||
|
initial: Result.Initial | Result.Final<A, E, P>,
|
||||||
|
): Effect.Effect<
|
||||||
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | QueryClient.QueryClient | R
|
||||||
|
> {
|
||||||
|
return Effect.andThen(this.getCacheEntry(key), Option.match({
|
||||||
|
onSome: entry => Effect.andThen(
|
||||||
|
QueryClient.isQueryClientCacheEntryStale(entry, this.staleTime),
|
||||||
|
isStale => isStale
|
||||||
|
? this.start(key, Result.willRefresh(entry.result) as Result.Final<A, E, P>)
|
||||||
|
: Effect.succeed(Subscribable.make({
|
||||||
|
get: Effect.succeed(entry.result as Result.Result<A, E, P>),
|
||||||
|
get changes() { return Stream.make(entry.result as Result.Result<A, E, P>) },
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
onNone: () => this.start(key, initial),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
start(
|
||||||
|
key: K,
|
||||||
|
initial: Result.Initial | Result.Final<A, E, P>,
|
||||||
|
): Effect.Effect<
|
||||||
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | R
|
||||||
|
> {
|
||||||
|
return Result.unsafeForkEffect(
|
||||||
|
Effect.onExit(this.f(key), () => Effect.andThen(
|
||||||
|
Effect.all([Effect.fiberId, this.fiber]),
|
||||||
|
([currentFiberId, fiber]) => Option.match(fiber, {
|
||||||
|
onSome: v => Equal.equals(currentFiberId, v.id())
|
||||||
|
? SubscriptionRef.set(this.fiber, Option.none())
|
||||||
|
: Effect.void,
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
|
||||||
|
{
|
||||||
|
initial,
|
||||||
|
initialProgress: this.initialProgress,
|
||||||
|
} as Result.unsafeForkEffect.Options<A, E, P>,
|
||||||
|
).pipe(
|
||||||
|
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
||||||
|
Effect.map(([sub]) => sub),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
key: K,
|
||||||
|
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
|
): Effect.Effect<Result.Final<A, E, P>, never, QueryClient.QueryClient> {
|
||||||
|
return sub.get.pipe(
|
||||||
|
Effect.andThen(initial => Stream.runFoldEffect(
|
||||||
|
sub.changes,
|
||||||
|
initial,
|
||||||
|
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
||||||
|
) as Effect.Effect<Result.Final<A, E, P>>),
|
||||||
|
Effect.tap(result => SubscriptionRef.set(this.latestFinalResult, Option.some(result))),
|
||||||
|
Effect.tap(result => Result.isSuccess(result)
|
||||||
|
? this.updateCacheEntry(key, result)
|
||||||
|
: Effect.void
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCacheKey(key: K): QueryClient.QueryClientCacheKey {
|
||||||
|
return new QueryClient.QueryClientCacheKey(key, this.f as (key: Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCacheEntry(
|
||||||
|
key: K
|
||||||
|
): Effect.Effect<Option.Option<QueryClient.QueryClientCacheEntry>, never, QueryClient.QueryClient> {
|
||||||
|
return QueryClient.QueryClient.pipe(
|
||||||
|
Effect.andThen(client => client.cache),
|
||||||
|
Effect.map(HashMap.get(this.makeCacheKey(key))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCacheEntry(
|
||||||
|
key: K,
|
||||||
|
result: Result.Success<A>,
|
||||||
|
): Effect.Effect<QueryClient.QueryClientCacheEntry, never, QueryClient.QueryClient> {
|
||||||
|
return Effect.Do.pipe(
|
||||||
|
Effect.bind("client", () => QueryClient.QueryClient),
|
||||||
|
Effect.bind("now", () => DateTime.now),
|
||||||
|
Effect.let("entry", ({ now }) => new QueryClient.QueryClientCacheEntry(result, now)),
|
||||||
|
Effect.tap(({ client, entry }) => SubscriptionRef.update(
|
||||||
|
client.cache,
|
||||||
|
HashMap.set(this.makeCacheKey(key), entry),
|
||||||
|
)),
|
||||||
|
Effect.map(({ entry }) => entry),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get invalidateCache(): Effect.Effect<void> {
|
||||||
|
return QueryClient.QueryClient.pipe(
|
||||||
|
Effect.andThen(client => SubscriptionRef.update(
|
||||||
|
client.cache,
|
||||||
|
HashMap.filter((_, key) => !Equivalence.strict()(key.f, this.f)),
|
||||||
|
)),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateCacheEntry(key: K): Effect.Effect<void> {
|
||||||
|
return QueryClient.QueryClient.pipe(
|
||||||
|
Effect.andThen(client => SubscriptionRef.update(
|
||||||
|
client.cache,
|
||||||
|
HashMap.remove(this.makeCacheKey(key)),
|
||||||
|
)),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, QueryTypeId)
|
||||||
|
|
||||||
|
export declare namespace make {
|
||||||
|
export interface Options<K extends Query.AnyKey, A, E = never, R = never, P = never> {
|
||||||
|
readonly key: Stream.Stream<K>
|
||||||
|
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
||||||
|
readonly initialProgress?: P
|
||||||
|
readonly staleTime?: Duration.DurationInput
|
||||||
|
readonly refreshOnWindowFocus?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, E = never, R = never, P = never>(
|
||||||
|
options: make.Options<K, A, E, R, P>
|
||||||
|
): Effect.fn.Return<
|
||||||
|
Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
|
||||||
|
> {
|
||||||
|
const client = yield* QueryClient.QueryClient
|
||||||
|
|
||||||
|
return new QueryImpl(
|
||||||
|
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
||||||
|
options.key,
|
||||||
|
options.f as any,
|
||||||
|
options.initialProgress as P,
|
||||||
|
|
||||||
|
options.staleTime ?? client.defaultStaleTime,
|
||||||
|
options.refreshOnWindowFocus ?? client.defaultRefreshOnWindowFocus,
|
||||||
|
|
||||||
|
yield* SubscriptionRef.make(Option.none<K>()),
|
||||||
|
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
||||||
|
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
||||||
|
yield* SubscriptionRef.make(Option.none<Result.Final<A, E, P>>()),
|
||||||
|
|
||||||
|
yield* Effect.makeSemaphore(1),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const service = <K extends Query.AnyKey, A, E = never, R = never, P = never>(
|
||||||
|
options: make.Options<K, A, E, R, P>
|
||||||
|
): Effect.Effect<
|
||||||
|
Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
|
||||||
|
> => Effect.tap(
|
||||||
|
make(options),
|
||||||
|
query => Effect.forkScoped(query.run),
|
||||||
|
)
|
||||||
119
packages/effect-fc/src/QueryClient.ts
Normal file
119
packages/effect-fc/src/QueryClient.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { DateTime, Duration, Effect, Equal, Equivalence, Hash, HashMap, Pipeable, Predicate, type Scope, SubscriptionRef } from "effect"
|
||||||
|
import type * as Query from "./Query.js"
|
||||||
|
import type * as Result from "./Result.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const QueryClientServiceTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientServiceTypeId")
|
||||||
|
export type QueryClientServiceTypeId = typeof QueryClientServiceTypeId
|
||||||
|
|
||||||
|
export interface QueryClientService extends Pipeable.Pipeable {
|
||||||
|
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId
|
||||||
|
readonly cache: SubscriptionRef.SubscriptionRef<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>
|
||||||
|
readonly gcTime: Duration.DurationInput
|
||||||
|
readonly defaultStaleTime: Duration.DurationInput
|
||||||
|
readonly defaultRefreshOnWindowFocus: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryClient extends Effect.Service<QueryClient>()("@effect-fc/QueryClient/QueryClient", {
|
||||||
|
scoped: Effect.suspend(() => service())
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class QueryClientServiceImpl
|
||||||
|
extends Pipeable.Class()
|
||||||
|
implements QueryClientService {
|
||||||
|
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId = QueryClientServiceTypeId
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly cache: SubscriptionRef.SubscriptionRef<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>,
|
||||||
|
readonly gcTime: Duration.DurationInput,
|
||||||
|
readonly defaultStaleTime: Duration.DurationInput,
|
||||||
|
readonly defaultRefreshOnWindowFocus: boolean,
|
||||||
|
readonly runSemaphore: Effect.Semaphore,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isQueryClientService = (u: unknown): u is QueryClientService => Predicate.hasProperty(u, QueryClientServiceTypeId)
|
||||||
|
|
||||||
|
export declare namespace make {
|
||||||
|
export interface Options {
|
||||||
|
readonly gcTime?: Duration.DurationInput
|
||||||
|
readonly defaultStaleTime?: Duration.DurationInput
|
||||||
|
readonly defaultRefreshOnWindowFocus?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = Effect.fnUntraced(function* (options: make.Options = {}): Effect.fn.Return<QueryClientService> {
|
||||||
|
return new QueryClientServiceImpl(
|
||||||
|
yield* SubscriptionRef.make(HashMap.empty<QueryClientCacheKey, QueryClientCacheEntry>()),
|
||||||
|
options.gcTime ?? "5 minutes",
|
||||||
|
options.defaultStaleTime ?? "0 minutes",
|
||||||
|
options.defaultRefreshOnWindowFocus ?? true,
|
||||||
|
yield* Effect.makeSemaphore(1),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const run = (_self: QueryClientService): Effect.Effect<void> => Effect.void
|
||||||
|
|
||||||
|
export declare namespace service {
|
||||||
|
export interface Options extends make.Options {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const service = (options?: service.Options): Effect.Effect<QueryClientService, never, Scope.Scope> => Effect.tap(
|
||||||
|
make(options),
|
||||||
|
client => Effect.forkScoped(run(client)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const QueryClientCacheKeyTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientCacheKey")
|
||||||
|
export type QueryClientCacheKeyTypeId = typeof QueryClientCacheKeyTypeId
|
||||||
|
|
||||||
|
export class QueryClientCacheKey
|
||||||
|
extends Pipeable.Class()
|
||||||
|
implements Pipeable.Pipeable, Equal.Equal {
|
||||||
|
readonly [QueryClientCacheKeyTypeId]: QueryClientCacheKeyTypeId = QueryClientCacheKeyTypeId
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly key: Query.Query.AnyKey,
|
||||||
|
readonly f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
[Equal.symbol](that: Equal.Equal) {
|
||||||
|
return isQueryClientCacheKey(that) && Equivalence.array(Equal.equivalence())(this.key, that.key) && Equivalence.strict()(this.f, that.f)
|
||||||
|
}
|
||||||
|
[Hash.symbol]() {
|
||||||
|
return Hash.combine(Hash.hash(this.f))(Hash.array(this.key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isQueryClientCacheKey = (u: unknown): u is QueryClientCacheKey => Predicate.hasProperty(u, QueryClientCacheKeyTypeId)
|
||||||
|
|
||||||
|
|
||||||
|
export const QueryClientCacheEntryTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientCacheEntry")
|
||||||
|
export type QueryClientCacheEntryTypeId = typeof QueryClientCacheEntryTypeId
|
||||||
|
|
||||||
|
export class QueryClientCacheEntry
|
||||||
|
extends Pipeable.Class()
|
||||||
|
implements Pipeable.Pipeable {
|
||||||
|
readonly [QueryClientCacheEntryTypeId]: QueryClientCacheEntryTypeId = QueryClientCacheEntryTypeId
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly result: Result.Success<unknown>,
|
||||||
|
readonly createdAt: DateTime.DateTime,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isQueryClientCacheEntry = (u: unknown): u is QueryClientCacheEntry => Predicate.hasProperty(u, QueryClientCacheEntryTypeId)
|
||||||
|
|
||||||
|
export const isQueryClientCacheEntryStale = (
|
||||||
|
self: QueryClientCacheEntry,
|
||||||
|
staleTime: Duration.DurationInput,
|
||||||
|
): Effect.Effect<boolean> => Effect.andThen(
|
||||||
|
DateTime.now,
|
||||||
|
now => Duration.greaterThanOrEqualTo(DateTime.distanceDuration(self.createdAt, now), staleTime),
|
||||||
|
)
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
import { type Context, Effect, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import { SetStateAction } from "./types/index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface ScopeOptions {
|
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const useScope: {
|
|
||||||
(options?: ScopeOptions): Effect.Effect<Scope.Scope>
|
|
||||||
} = Effect.fnUntraced(function* (options?: ScopeOptions) {
|
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
|
|
||||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
|
|
||||||
Effect.all([Ref.make(true), makeScope(options)])
|
|
||||||
), [])
|
|
||||||
const [scope, setScope] = React.useState(initialScope)
|
|
||||||
|
|
||||||
React.useEffect(() => Runtime.runSync(runtime)(
|
|
||||||
Effect.if(isInitialRun, {
|
|
||||||
onTrue: () => Effect.as(
|
|
||||||
Ref.set(isInitialRun, false),
|
|
||||||
() => closeScope(scope, runtime, options),
|
|
||||||
),
|
|
||||||
|
|
||||||
onFalse: () => makeScope(options).pipe(
|
|
||||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
|
||||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
), [])
|
|
||||||
|
|
||||||
return scope
|
|
||||||
})
|
|
||||||
|
|
||||||
const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
const closeScope = (
|
|
||||||
scope: Scope.CloseableScope,
|
|
||||||
runtime: Runtime.Runtime<never>,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) => {
|
|
||||||
switch (options?.finalizerExecutionMode ?? "sync") {
|
|
||||||
case "sync":
|
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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: {
|
|
||||||
<Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<(...args: Args) => A, never, R>
|
|
||||||
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<R>()
|
|
||||||
return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useCallbackPromise: {
|
|
||||||
<Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
|
||||||
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<R>()
|
|
||||||
return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useEffect: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fnUntraced(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const { scope, exit } = Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
)
|
|
||||||
|
|
||||||
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: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fnUntraced(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
|
||||||
const { scope, exit } = Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
)
|
|
||||||
|
|
||||||
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: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fnUntraced(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const scope = Runtime.runSync(runtime)(options?.scope
|
|
||||||
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
: Scope.make(options?.finalizerExecutionStrategy)
|
|
||||||
)
|
|
||||||
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
switch (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)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useRefFromReactiveValue: {
|
|
||||||
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
|
||||||
} = Effect.fnUntraced(function*(value) {
|
|
||||||
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
|
||||||
yield* useEffect(() => Ref.set(ref, value), [value])
|
|
||||||
return ref
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useSubscribeRefs: {
|
|
||||||
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
|
||||||
...refs: Refs
|
|
||||||
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
|
|
||||||
} = Effect.fnUntraced(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
|
||||||
...refs: Refs
|
|
||||||
) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
|
||||||
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
|
|
||||||
))
|
|
||||||
|
|
||||||
yield* useFork(() => pipe(
|
|
||||||
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
|
||||||
streams => Stream.zipLatestAll(...streams),
|
|
||||||
Stream.runForEach(v =>
|
|
||||||
Effect.sync(() => setReactStateValue(v))
|
|
||||||
),
|
|
||||||
), refs)
|
|
||||||
|
|
||||||
return reactStateValue as any
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useRefState: {
|
|
||||||
<A>(
|
|
||||||
ref: SubscriptionRef.SubscriptionRef<A>
|
|
||||||
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
|
|
||||||
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(
|
|
||||||
Stream.changesWith(ref.changes, (x, y) => x === y),
|
|
||||||
v => Effect.sync(() => setReactStateValue(v)),
|
|
||||||
), [ref])
|
|
||||||
|
|
||||||
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
|
||||||
Ref.update(ref, prevState =>
|
|
||||||
SetStateAction.value(setStateAction, prevState)
|
|
||||||
),
|
|
||||||
[ref])
|
|
||||||
|
|
||||||
return [reactStateValue, setValue]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useStreamFromReactiveValues: {
|
|
||||||
<const A extends React.DependencyList>(
|
|
||||||
values: A
|
|
||||||
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
|
||||||
} = Effect.fnUntraced(function* <const A extends React.DependencyList>(values: A) {
|
|
||||||
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
|
|
||||||
Effect.bind("latest", () => Ref.make(values)),
|
|
||||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
|
||||||
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
|
|
||||||
Effect.flatMap(a => Effect.map(
|
|
||||||
Stream.fromPubSub(pubsub, { scoped: true }),
|
|
||||||
s => Stream.concat(Stream.make(a), s),
|
|
||||||
)),
|
|
||||||
Stream.unwrapScoped,
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
|
|
||||||
yield* useEffect(() => Ref.set(latest, values).pipe(
|
|
||||||
Effect.andThen(PubSub.publish(pubsub, values)),
|
|
||||||
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
|
||||||
), values)
|
|
||||||
|
|
||||||
return stream
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useSubscribeStream: {
|
|
||||||
<A, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>
|
|
||||||
): Effect.Effect<Option.Option<A>, never, R>
|
|
||||||
<A extends NonNullable<unknown>, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue: A,
|
|
||||||
): Effect.Effect<Option.Some<A>, never, R>
|
|
||||||
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue?: A,
|
|
||||||
) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(
|
|
||||||
React.useMemo(() => initialValue
|
|
||||||
? Option.some(initialValue)
|
|
||||||
: Option.none(),
|
|
||||||
[])
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(
|
|
||||||
Stream.changesWith(stream, (x, y) => x === y),
|
|
||||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
|
||||||
), [stream])
|
|
||||||
|
|
||||||
return reactStateValue as Option.Some<A>
|
|
||||||
})
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { Effect, type Layer, ManagedRuntime, type Runtime } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export interface ReactManagedRuntime<R, ER> {
|
|
||||||
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
|
||||||
readonly context: React.Context<Runtime.Runtime<R>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = <R, ER>(
|
|
||||||
layer: Layer.Layer<R, ER>,
|
|
||||||
memoMap?: Layer.MemoMap,
|
|
||||||
): ReactManagedRuntime<R, ER> => ({
|
|
||||||
runtime: ManagedRuntime.make(layer, memoMap),
|
|
||||||
context: React.createContext<Runtime.Runtime<R>>(null!),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export interface AsyncProviderProps<R, ER> extends React.SuspenseProps {
|
|
||||||
readonly runtime: ReactManagedRuntime<R, ER>
|
|
||||||
readonly children?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AsyncProvider<R, ER>(
|
|
||||||
{ runtime, children, ...suspenseProps }: AsyncProviderProps<R, ER>
|
|
||||||
): React.ReactNode {
|
|
||||||
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
React.Suspense,
|
|
||||||
suspenseProps,
|
|
||||||
React.createElement(AsyncProviderInner<R, ER>, { runtime, promise, children }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AsyncProviderInnerProps<R, ER> {
|
|
||||||
readonly runtime: ReactManagedRuntime<R, ER>
|
|
||||||
readonly promise: Promise<Runtime.Runtime<R>>
|
|
||||||
readonly children?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
function AsyncProviderInner<R, ER>(
|
|
||||||
{ runtime, promise, children }: AsyncProviderInnerProps<R, ER>
|
|
||||||
): React.ReactNode {
|
|
||||||
const value = React.use(promise)
|
|
||||||
return React.createElement(runtime.context, { value }, children)
|
|
||||||
}
|
|
||||||
85
packages/effect-fc/src/ReactRuntime.ts
Normal file
85
packages/effect-fc/src/ReactRuntime.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
|
import { Effect, Layer, ManagedRuntime, Predicate, Runtime, Scope } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as Component from "./Component.js"
|
||||||
|
import * as ErrorObserver from "./ErrorObserver.js"
|
||||||
|
import * as QueryClient from "./QueryClient.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const TypeId: unique symbol = Symbol.for("@effect-fc/ReactRuntime/ReactRuntime")
|
||||||
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
|
export interface ReactRuntime<R, ER> {
|
||||||
|
new(_: never): Record<string, never>
|
||||||
|
readonly [TypeId]: TypeId
|
||||||
|
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
||||||
|
readonly context: React.Context<Runtime.Runtime<R>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReactRuntimeProto = Object.freeze({ [TypeId]: TypeId } as const)
|
||||||
|
|
||||||
|
export const Prelude: Layer.Layer<
|
||||||
|
| Component.ScopeMap
|
||||||
|
| ErrorObserver.ErrorObserver
|
||||||
|
| QueryClient.QueryClient
|
||||||
|
> = Layer.mergeAll(
|
||||||
|
Component.ScopeMap.Default,
|
||||||
|
ErrorObserver.layer,
|
||||||
|
QueryClient.QueryClient.Default,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const isReactRuntime = (u: unknown): u is ReactRuntime<unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||||
|
|
||||||
|
export const make = <R, ER>(
|
||||||
|
layer: Layer.Layer<R, ER>,
|
||||||
|
memoMap?: Layer.MemoMap,
|
||||||
|
): ReactRuntime<Layer.Layer.Success<typeof Prelude> | R, ER> => Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, {
|
||||||
|
runtime: ManagedRuntime.make(
|
||||||
|
Layer.merge(layer, Prelude),
|
||||||
|
memoMap,
|
||||||
|
),
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
||||||
|
context: React.createContext<Runtime.Runtime<R>>(null!),
|
||||||
|
}),
|
||||||
|
ReactRuntimeProto,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export namespace Provider {
|
||||||
|
export interface Props<R, ER> extends React.SuspenseProps {
|
||||||
|
readonly runtime: ReactRuntime<R, ER>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Provider = <R, ER>(
|
||||||
|
{ runtime, children, ...suspenseProps }: Provider.Props<R, ER>
|
||||||
|
): React.ReactNode => {
|
||||||
|
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
React.Suspense,
|
||||||
|
suspenseProps,
|
||||||
|
React.createElement(ProviderInner<R, ER>, { runtime, promise, children }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProviderInner = <R, ER>(
|
||||||
|
{ runtime, promise, children }: {
|
||||||
|
readonly runtime: ReactRuntime<R, ER>
|
||||||
|
readonly promise: Promise<Runtime.Runtime<R>>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}
|
||||||
|
): React.ReactNode => {
|
||||||
|
const effectRuntime = React.use(promise)
|
||||||
|
const scope = Runtime.runSync(effectRuntime)(Component.useScope([effectRuntime]))
|
||||||
|
Runtime.runSync(effectRuntime)(Effect.provideService(
|
||||||
|
Component.useOnChange(() => Effect.addFinalizer(() => runtime.runtime.disposeEffect), [scope]),
|
||||||
|
Scope.Scope,
|
||||||
|
scope,
|
||||||
|
))
|
||||||
|
|
||||||
|
return React.createElement(runtime.context, { value: effectRuntime }, children)
|
||||||
|
}
|
||||||
279
packages/effect-fc/src/Result.ts
Normal file
279
packages/effect-fc/src/Result.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, Pipeable, Predicate, PubSub, pipe, Ref, type Scope, Stream, Subscribable } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
|
||||||
|
export type ResultTypeId = typeof ResultTypeId
|
||||||
|
|
||||||
|
export type Result<A, E = never, P = never> = (
|
||||||
|
| Initial
|
||||||
|
| Running<P>
|
||||||
|
| Final<A, E, P>
|
||||||
|
)
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/noBannedTypes: "{}" is relevant here
|
||||||
|
export type Final<A, E = never, P = never> = (Success<A> | Failure<E>) & ({} | Flags<P>)
|
||||||
|
export type Flags<P = never> = WillFetch | WillRefresh | Refreshing<P>
|
||||||
|
|
||||||
|
export declare namespace Result {
|
||||||
|
export interface Prototype extends Pipeable.Pipeable, Equal.Equal {
|
||||||
|
readonly [ResultTypeId]: ResultTypeId
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Success<R extends Result<any, any, any>> = [R] extends [Result<infer A, infer _E, infer _P>] ? A : never
|
||||||
|
export type Failure<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer E, infer _P>] ? E : never
|
||||||
|
export type Progress<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer _E, infer P>] ? P : never
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare namespace Flags {
|
||||||
|
export type Keys = keyof WillFetch & WillRefresh & Refreshing<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Initial extends Result.Prototype {
|
||||||
|
readonly _tag: "Initial"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Running<P = never> extends Result.Prototype {
|
||||||
|
readonly _tag: "Running"
|
||||||
|
readonly progress: P
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Success<A> extends Result.Prototype {
|
||||||
|
readonly _tag: "Success"
|
||||||
|
readonly value: A
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Failure<E = never> extends Result.Prototype {
|
||||||
|
readonly _tag: "Failure"
|
||||||
|
readonly cause: Cause.Cause<E>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WillFetch {
|
||||||
|
readonly _flag: "WillFetch"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WillRefresh {
|
||||||
|
readonly _flag: "WillRefresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Refreshing<P = never> {
|
||||||
|
readonly _flag: "Refreshing"
|
||||||
|
readonly progress: P
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ResultPrototype = Object.freeze({
|
||||||
|
...Pipeable.Prototype,
|
||||||
|
[ResultTypeId]: ResultTypeId,
|
||||||
|
|
||||||
|
[Equal.symbol](this: Result<any, any, any>, that: Result<any, any, any>): boolean {
|
||||||
|
if (this._tag !== that._tag || (this as Flags)._flag !== (that as Flags)._flag)
|
||||||
|
return false
|
||||||
|
if (hasRefreshingFlag(this) && !Equal.equals(this.progress, (that as Refreshing<any>).progress))
|
||||||
|
return false
|
||||||
|
return Match.value(this).pipe(
|
||||||
|
Match.tag("Initial", () => true),
|
||||||
|
Match.tag("Running", self => Equal.equals(self.progress, (that as Running<any>).progress)),
|
||||||
|
Match.tag("Success", self => Equal.equals(self.value, (that as Success<any>).value)),
|
||||||
|
Match.tag("Failure", self => Equal.equals(self.cause, (that as Failure<any>).cause)),
|
||||||
|
Match.exhaustive,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
[Hash.symbol](this: Result<any, any, any>): number {
|
||||||
|
return pipe(Hash.string(this._tag),
|
||||||
|
tagHash => Match.value(this).pipe(
|
||||||
|
Match.tag("Initial", () => tagHash),
|
||||||
|
Match.tag("Running", self => Hash.combine(Hash.hash(self.progress))(tagHash)),
|
||||||
|
Match.tag("Success", self => Hash.combine(Hash.hash(self.value))(tagHash)),
|
||||||
|
Match.tag("Failure", self => Hash.combine(Hash.hash(self.cause))(tagHash)),
|
||||||
|
Match.exhaustive,
|
||||||
|
),
|
||||||
|
Hash.combine(Hash.hash((this as Flags)._flag)),
|
||||||
|
hash => hasRefreshingFlag(this)
|
||||||
|
? Hash.combine(Hash.hash(this.progress))(hash)
|
||||||
|
: hash,
|
||||||
|
Hash.cached(this),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
} as const satisfies Result.Prototype)
|
||||||
|
|
||||||
|
|
||||||
|
export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId)
|
||||||
|
export const isFinal = (u: unknown): u is Final<unknown, unknown, unknown> => isResult(u) && (isSuccess(u) || isFailure(u))
|
||||||
|
export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial"
|
||||||
|
export const isRunning = (u: unknown): u is Running<unknown> => isResult(u) && u._tag === "Running"
|
||||||
|
export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success"
|
||||||
|
export const isFailure = (u: unknown): u is Failure<unknown> => isResult(u) && u._tag === "Failure"
|
||||||
|
export const hasFlag = (u: unknown): u is Flags => isResult(u) && Predicate.hasProperty(u, "_flag")
|
||||||
|
export const hasWillFetchFlag = (u: unknown): u is WillFetch => isResult(u) && Predicate.hasProperty(u, "_flag") && u._flag === "WillFetch"
|
||||||
|
export const hasWillRefreshFlag = (u: unknown): u is WillRefresh => isResult(u) && Predicate.hasProperty(u, "_flag") && u._flag === "WillRefresh"
|
||||||
|
export const hasRefreshingFlag = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "_flag") && u._flag === "Refreshing"
|
||||||
|
|
||||||
|
export const initial: {
|
||||||
|
(): Initial
|
||||||
|
<A, E = never, P = never>(): Result<A, E, P>
|
||||||
|
} = (): Initial => Object.setPrototypeOf({ _tag: "Initial" }, ResultPrototype)
|
||||||
|
export const running = <P = never>(progress?: P): Running<P> => Object.setPrototypeOf({ _tag: "Running", progress }, ResultPrototype)
|
||||||
|
export const succeed = <A>(value: A): Success<A> => Object.setPrototypeOf({ _tag: "Success", value }, ResultPrototype)
|
||||||
|
export const fail = <E>(cause: Cause.Cause<E> ): Failure<E> => Object.setPrototypeOf({ _tag: "Failure", cause }, ResultPrototype)
|
||||||
|
|
||||||
|
export const willFetch = <R extends Final<any, any, any>>(
|
||||||
|
result: R
|
||||||
|
): Omit<R, keyof Flags.Keys> & WillFetch => Object.setPrototypeOf(
|
||||||
|
Object.assign({}, result, { _flag: "WillFetch" }),
|
||||||
|
Object.getPrototypeOf(result),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const willRefresh = <R extends Final<any, any, any>>(
|
||||||
|
result: R
|
||||||
|
): Omit<R, keyof Flags.Keys> & WillRefresh => Object.setPrototypeOf(
|
||||||
|
Object.assign({}, result, { _flag: "WillRefresh" }),
|
||||||
|
Object.getPrototypeOf(result),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const refreshing = <R extends Final<any, any, any>, P = never>(
|
||||||
|
result: R,
|
||||||
|
progress?: P,
|
||||||
|
): Omit<R, keyof Flags.Keys> & Refreshing<P> => Object.setPrototypeOf(
|
||||||
|
Object.assign({}, result, { _flag: "Refreshing", progress }),
|
||||||
|
Object.getPrototypeOf(result),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const fromExit: {
|
||||||
|
<A, E>(exit: Exit.Success<A, E>): Success<A>
|
||||||
|
<A, E>(exit: Exit.Failure<A, E>): Failure<E>
|
||||||
|
<A, E>(exit: Exit.Exit<A, E>): Success<A> | Failure<E>
|
||||||
|
} = exit => (exit._tag === "Success" ? succeed(exit.value) : fail(exit.cause)) as any
|
||||||
|
|
||||||
|
export const toExit: {
|
||||||
|
<A>(self: Success<A>): Exit.Success<A, never>
|
||||||
|
<E>(self: Failure<E>): Exit.Failure<never, E>
|
||||||
|
<A, E, P>(self: Final<A, E, P>): Exit.Exit<A, E>
|
||||||
|
<A, E, P>(self: Result<A, E, P>): Exit.Exit<A, E | Cause.NoSuchElementException>
|
||||||
|
} = <A, E, P>(self: Result<A, E, P>): any => {
|
||||||
|
switch (self._tag) {
|
||||||
|
case "Success":
|
||||||
|
return Exit.succeed(self.value)
|
||||||
|
case "Failure":
|
||||||
|
return Exit.failCause(self.cause)
|
||||||
|
default:
|
||||||
|
return Exit.fail(new Cause.NoSuchElementException())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface State<A, E = never, P = never> {
|
||||||
|
readonly get: Effect.Effect<Result<A, E, P>>
|
||||||
|
readonly set: (v: Result<A, E, P>) => Effect.Effect<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const State = <A, E = never, P = never>(): Context.Tag<State<A, E, P>, State<A, E, P>> => Context.GenericTag("@effect-fc/Result/State")
|
||||||
|
|
||||||
|
export interface Progress<P = never> {
|
||||||
|
readonly update: <E, R>(
|
||||||
|
f: (previous: P) => Effect.Effect<P, E, R>
|
||||||
|
) => Effect.Effect<void, PreviousResultNotRunningNorRefreshing | E, R>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{
|
||||||
|
readonly previous: Result<unknown, unknown, unknown>
|
||||||
|
}> {}
|
||||||
|
|
||||||
|
export const Progress = <P = never>(): Context.Tag<Progress<P>, Progress<P>> => Context.GenericTag("@effect-fc/Result/Progress")
|
||||||
|
|
||||||
|
export const makeProgressLayer = <A, E, P = never>(): Layer.Layer<
|
||||||
|
Progress<P>,
|
||||||
|
never,
|
||||||
|
State<A, E, P>
|
||||||
|
> => Layer.effect(Progress<P>(), Effect.gen(function*() {
|
||||||
|
const state = yield* State<A, E, P>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
update: <FE, FR>(f: (previous: P) => Effect.Effect<P, FE, FR>) => Effect.Do.pipe(
|
||||||
|
Effect.bind("previous", () => Effect.andThen(state.get, previous =>
|
||||||
|
(isRunning(previous) || hasRefreshingFlag(previous))
|
||||||
|
? Effect.succeed(previous)
|
||||||
|
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous })),
|
||||||
|
)),
|
||||||
|
Effect.bind("progress", ({ previous }) => f(previous.progress)),
|
||||||
|
Effect.let("next", ({ previous, progress }) => isRunning(previous)
|
||||||
|
? running(progress)
|
||||||
|
: refreshing(previous, progress) as Final<A, E, P> & Refreshing<P>
|
||||||
|
),
|
||||||
|
Effect.andThen(({ next }) => state.set(next)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
export namespace unsafeForkEffect {
|
||||||
|
export type OutputContext<A, E, R, P> = Exclude<R, State<A, E, P> | Progress<P> | Progress<never>>
|
||||||
|
|
||||||
|
export interface Options<A, E, P> {
|
||||||
|
readonly initial?: Initial | Final<A, E, P>
|
||||||
|
readonly initialProgress?: P
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unsafeForkEffect = <A, E, R, P = never>(
|
||||||
|
effect: Effect.Effect<A, E, R>,
|
||||||
|
options?: unsafeForkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
||||||
|
): Effect.Effect<
|
||||||
|
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
||||||
|
never,
|
||||||
|
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P>
|
||||||
|
> => Effect.Do.pipe(
|
||||||
|
Effect.bind("ref", () => Ref.make(options?.initial ?? initial<A, E, P>())),
|
||||||
|
Effect.bind("pubsub", () => PubSub.unbounded<Result<A, E, P>>()),
|
||||||
|
Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State<A, E, P>().pipe(
|
||||||
|
Effect.andThen(state => state.set(
|
||||||
|
(isFinal(options?.initial) && hasWillRefreshFlag(options?.initial))
|
||||||
|
? refreshing(options.initial, options?.initialProgress) as Result<A, E, P>
|
||||||
|
: running(options?.initialProgress)
|
||||||
|
).pipe(
|
||||||
|
Effect.andThen(effect),
|
||||||
|
Effect.onExit(exit => Effect.andThen(
|
||||||
|
state.set(fromExit(exit)),
|
||||||
|
Effect.forkScoped(PubSub.shutdown(pubsub)),
|
||||||
|
)),
|
||||||
|
)),
|
||||||
|
Effect.provide(Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(makeProgressLayer<A, E, P>()),
|
||||||
|
Layer.provideMerge(Layer.succeed(State<A, E, P>(), {
|
||||||
|
get: ref,
|
||||||
|
set: v => Effect.andThen(Ref.set(ref, v), PubSub.publish(pubsub, v))
|
||||||
|
})),
|
||||||
|
)),
|
||||||
|
))),
|
||||||
|
Effect.map(({ ref, pubsub, fiber }) => [
|
||||||
|
Subscribable.make({
|
||||||
|
get: ref,
|
||||||
|
changes: Stream.unwrapScoped(Effect.map(
|
||||||
|
Effect.all([ref, Stream.fromPubSub(pubsub, { scoped: true })]),
|
||||||
|
([latest, stream]) => Stream.concat(Stream.make(latest), stream),
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
fiber,
|
||||||
|
]),
|
||||||
|
) as Effect.Effect<
|
||||||
|
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
||||||
|
never,
|
||||||
|
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P>
|
||||||
|
>
|
||||||
|
|
||||||
|
export namespace forkEffect {
|
||||||
|
export type InputContext<R, P> = R extends Progress<infer X> ? [X] extends [P] ? R : never : R
|
||||||
|
export type OutputContext<A, E, R, P> = unsafeForkEffect.OutputContext<A, E, R, P>
|
||||||
|
export interface Options<A, E, P> extends unsafeForkEffect.Options<A, E, P> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const forkEffect: {
|
||||||
|
<A, E, R, P = never>(
|
||||||
|
effect: Effect.Effect<A, E, forkEffect.InputContext<R, NoInfer<P>>>,
|
||||||
|
options?: forkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
||||||
|
): Effect.Effect<
|
||||||
|
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
||||||
|
never,
|
||||||
|
Scope.Scope | forkEffect.OutputContext<A, E, R, P>
|
||||||
|
>
|
||||||
|
} = unsafeForkEffect
|
||||||
33
packages/effect-fc/src/Stream.ts
Normal file
33
packages/effect-fc/src/Stream.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Effect, Equivalence, Option, Stream } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as Component from "./Component.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const useStream: {
|
||||||
|
<A, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>
|
||||||
|
): Effect.Effect<Option.Option<A>, never, R>
|
||||||
|
<A extends NonNullable<unknown>, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
initialValue: A,
|
||||||
|
): Effect.Effect<Option.Some<A>, never, R>
|
||||||
|
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
initialValue?: A,
|
||||||
|
) {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(() => initialValue
|
||||||
|
? Option.some(initialValue)
|
||||||
|
: Option.none()
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Component.useReactEffect(() => Effect.forkScoped(
|
||||||
|
Stream.runForEach(
|
||||||
|
Stream.changesWith(stream, Equivalence.strict()),
|
||||||
|
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||||
|
)
|
||||||
|
), [stream])
|
||||||
|
|
||||||
|
return reactStateValue as Option.Some<A>
|
||||||
|
})
|
||||||
|
|
||||||
|
export * from "effect/Stream"
|
||||||
52
packages/effect-fc/src/Subscribable.ts
Normal file
52
packages/effect-fc/src/Subscribable.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Effect, Equivalence, Stream, Subscribable } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as Component from "./Component.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
||||||
|
...elements: T
|
||||||
|
): Subscribable.Subscribable<
|
||||||
|
[T[number]] extends [never]
|
||||||
|
? never
|
||||||
|
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never },
|
||||||
|
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never,
|
||||||
|
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never
|
||||||
|
> => Subscribable.make({
|
||||||
|
get: Effect.all(elements.map(v => v.get)),
|
||||||
|
changes: Stream.zipLatestAll(...elements.map(v => v.changes)),
|
||||||
|
}) as any
|
||||||
|
|
||||||
|
export declare namespace useSubscribables {
|
||||||
|
export type Success<T extends readonly Subscribable.Subscribable<any, any, any>[]> = [T[number]] extends [never]
|
||||||
|
? never
|
||||||
|
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never }
|
||||||
|
|
||||||
|
export interface Options<A> {
|
||||||
|
readonly equivalence?: Equivalence.Equivalence<A>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSubscribables = Effect.fnUntraced(function* <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
||||||
|
elements: T,
|
||||||
|
options?: useSubscribables.Options<useSubscribables.Success<NoInfer<T>>>,
|
||||||
|
): Effect.fn.Return<
|
||||||
|
useSubscribables.Success<T>,
|
||||||
|
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never,
|
||||||
|
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never
|
||||||
|
> {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(
|
||||||
|
yield* Component.useOnMount(() => Effect.all(elements.map(v => v.get)))
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Component.useReactEffect(() => Stream.zipLatestAll(...elements.map(ref => ref.changes)).pipe(
|
||||||
|
Stream.changesWith((options?.equivalence as Equivalence.Equivalence<any[]> | undefined) ?? Equivalence.array(Equivalence.strict())),
|
||||||
|
Stream.runForEach(v =>
|
||||||
|
Effect.sync(() => setReactStateValue(v))
|
||||||
|
),
|
||||||
|
Effect.forkScoped,
|
||||||
|
), elements)
|
||||||
|
|
||||||
|
return reactStateValue as any
|
||||||
|
})
|
||||||
|
|
||||||
|
export * from "effect/Subscribable"
|
||||||
61
packages/effect-fc/src/SubscriptionRef.ts
Normal file
61
packages/effect-fc/src/SubscriptionRef.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as Component from "./Component.js"
|
||||||
|
import * as SetStateAction from "./SetStateAction.js"
|
||||||
|
|
||||||
|
|
||||||
|
export declare namespace useSubscriptionRefState {
|
||||||
|
export interface Options<A> {
|
||||||
|
readonly equivalence?: Equivalence.Equivalence<A>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSubscriptionRefState = Effect.fnUntraced(function* <A>(
|
||||||
|
ref: SubscriptionRef.SubscriptionRef<A>,
|
||||||
|
options?: useSubscriptionRefState.Options<NoInfer<A>>,
|
||||||
|
): Effect.fn.Return<readonly [A, React.Dispatch<React.SetStateAction<A>>]> {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(yield* Component.useOnMount(() => ref))
|
||||||
|
|
||||||
|
yield* Component.useReactEffect(() => Effect.forkScoped(
|
||||||
|
Stream.runForEach(
|
||||||
|
Stream.changesWith(ref.changes, options?.equivalence ?? Equivalence.strict()),
|
||||||
|
v => Effect.sync(() => setReactStateValue(v)),
|
||||||
|
)
|
||||||
|
), [ref])
|
||||||
|
|
||||||
|
const setValue = yield* Component.useCallbackSync(
|
||||||
|
(setStateAction: React.SetStateAction<A>) => Effect.andThen(
|
||||||
|
Ref.updateAndGet(ref, prevState => SetStateAction.value(setStateAction, prevState)),
|
||||||
|
v => setReactStateValue(v),
|
||||||
|
),
|
||||||
|
[ref],
|
||||||
|
)
|
||||||
|
|
||||||
|
return [reactStateValue, setValue]
|
||||||
|
})
|
||||||
|
|
||||||
|
export declare namespace useSubscriptionRefFromState {
|
||||||
|
export interface Options<A> {
|
||||||
|
readonly equivalence?: Equivalence.Equivalence<A>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSubscriptionRefFromState = Effect.fnUntraced(function* <A>(
|
||||||
|
[value, setValue]: readonly [A, React.Dispatch<React.SetStateAction<A>>],
|
||||||
|
options?: useSubscriptionRefFromState.Options<NoInfer<A>>,
|
||||||
|
): Effect.fn.Return<SubscriptionRef.SubscriptionRef<A>> {
|
||||||
|
const ref = yield* Component.useOnChange(() => Effect.tap(
|
||||||
|
SubscriptionRef.make(value),
|
||||||
|
ref => Effect.forkScoped(
|
||||||
|
Stream.runForEach(
|
||||||
|
Stream.changesWith(ref.changes, options?.equivalence ?? Equivalence.strict()),
|
||||||
|
v => Effect.sync(() => setValue(v)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
), [setValue])
|
||||||
|
|
||||||
|
yield* Component.useReactEffect(() => Ref.set(ref, value), [value])
|
||||||
|
return ref
|
||||||
|
})
|
||||||
|
|
||||||
|
export * from "effect/SubscriptionRef"
|
||||||
186
packages/effect-fc/src/SubscriptionSubRef.ts
Normal file
186
packages/effect-fc/src/SubscriptionSubRef.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import { Chunk, Effect, Effectable, Option, Predicate, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
||||||
|
import * as PropertyPath from "./PropertyPath.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("@effect-fc/SubscriptionSubRef/SubscriptionSubRef")
|
||||||
|
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
||||||
|
|
||||||
|
export interface SubscriptionSubRef<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
|
||||||
|
extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
readonly parent: B
|
||||||
|
|
||||||
|
readonly [Unify.typeSymbol]?: unknown
|
||||||
|
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
||||||
|
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare namespace SubscriptionSubRef {
|
||||||
|
export interface Variance<in out A, in out B> {
|
||||||
|
readonly [SubscriptionSubRefTypeId]: {
|
||||||
|
readonly _A: Types.Invariant<A>
|
||||||
|
readonly _B: Types.Invariant<B>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
|
||||||
|
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
|
||||||
|
SubscriptionRef?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const refVariance = { _A: (_: any) => _ }
|
||||||
|
const synchronizedRefVariance = { _A: (_: any) => _ }
|
||||||
|
const subscriptionRefVariance = { _A: (_: any) => _ }
|
||||||
|
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
||||||
|
|
||||||
|
class SubscriptionSubRefImpl<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
|
||||||
|
extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
||||||
|
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||||
|
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||||
|
readonly [Ref.RefTypeId] = refVariance
|
||||||
|
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
|
||||||
|
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
|
||||||
|
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
|
||||||
|
|
||||||
|
readonly get: Effect.Effect<A>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly parent: B,
|
||||||
|
readonly getter: (parentValue: Effect.Effect.Success<B>) => A,
|
||||||
|
readonly setter: (parentValue: Effect.Effect.Success<B>, value: A) => Effect.Effect.Success<B>,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.get = Effect.map(this.parent, this.getter)
|
||||||
|
}
|
||||||
|
|
||||||
|
commit() {
|
||||||
|
return this.get
|
||||||
|
}
|
||||||
|
|
||||||
|
get changes(): Stream.Stream<A> {
|
||||||
|
return Stream.unwrap(
|
||||||
|
Effect.map(this.get, a => Stream.concat(
|
||||||
|
Stream.make(a),
|
||||||
|
Stream.map(this.parent.changes, this.getter),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
|
||||||
|
return this.modifyEffect(a => Effect.succeed(f(a)))
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
||||||
|
return Effect.Do.pipe(
|
||||||
|
Effect.bind("b", (): Effect.Effect<Effect.Effect.Success<B>> => this.parent),
|
||||||
|
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
||||||
|
Effect.tap(({ b, ca: [, a] }) => SubscriptionRef.set(this.parent, this.setter(b, a))),
|
||||||
|
Effect.map(({ ca: [c] }) => c),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const isSubscriptionSubRef = (u: unknown): u is SubscriptionSubRef<unknown, SubscriptionRef.SubscriptionRef<unknown>> => Predicate.hasProperty(u, SubscriptionSubRefTypeId)
|
||||||
|
|
||||||
|
export const makeFromGetSet = <A, B extends SubscriptionRef.SubscriptionRef<any>>(
|
||||||
|
parent: B,
|
||||||
|
options: {
|
||||||
|
readonly get: (parentValue: Effect.Effect.Success<B>) => A
|
||||||
|
readonly set: (parentValue: Effect.Effect.Success<B>, value: A) => Effect.Effect.Success<B>
|
||||||
|
},
|
||||||
|
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, options.get, options.set)
|
||||||
|
|
||||||
|
export const makeFromPath = <
|
||||||
|
B extends SubscriptionRef.SubscriptionRef<any>,
|
||||||
|
const P extends PropertyPath.Paths<Effect.Effect.Success<B>>,
|
||||||
|
>(
|
||||||
|
parent: B,
|
||||||
|
path: P,
|
||||||
|
): SubscriptionSubRef<PropertyPath.ValueFromPath<Effect.Effect.Success<B>, P>, B> => new SubscriptionSubRefImpl(
|
||||||
|
parent,
|
||||||
|
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
||||||
|
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const makeFromChunkIndex: {
|
||||||
|
<B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
|
||||||
|
parent: B,
|
||||||
|
index: number,
|
||||||
|
): SubscriptionSubRef<
|
||||||
|
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
|
||||||
|
B
|
||||||
|
>
|
||||||
|
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
|
||||||
|
parent: B,
|
||||||
|
index: number,
|
||||||
|
): SubscriptionSubRef<
|
||||||
|
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
|
||||||
|
B
|
||||||
|
>
|
||||||
|
} = (
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>,
|
||||||
|
index: number,
|
||||||
|
) => new SubscriptionSubRefImpl(
|
||||||
|
parent,
|
||||||
|
parentValue => Chunk.unsafeGet(parentValue, index),
|
||||||
|
(parentValue, value) => Chunk.replace(parentValue, index, value),
|
||||||
|
) as any
|
||||||
|
|
||||||
|
export const makeFromChunkFindFirst: {
|
||||||
|
<B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
|
||||||
|
parent: B,
|
||||||
|
findFirstPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never>,
|
||||||
|
): SubscriptionSubRef<
|
||||||
|
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
|
||||||
|
B
|
||||||
|
>
|
||||||
|
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
|
||||||
|
parent: B,
|
||||||
|
findFirstPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never>,
|
||||||
|
): SubscriptionSubRef<
|
||||||
|
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
|
||||||
|
B
|
||||||
|
>
|
||||||
|
} = (
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<never>>,
|
||||||
|
findFirstPredicate: Predicate.Predicate.Any,
|
||||||
|
) => new SubscriptionSubRefImpl(
|
||||||
|
parent,
|
||||||
|
parentValue => Option.getOrThrow(Chunk.findFirst(parentValue, findFirstPredicate)),
|
||||||
|
(parentValue, value) => Option.getOrThrow(Option.andThen(
|
||||||
|
Chunk.findFirstIndex(parentValue, findFirstPredicate),
|
||||||
|
index => Chunk.replace(parentValue, index, value),
|
||||||
|
)),
|
||||||
|
) as any
|
||||||
|
|
||||||
|
export const makeFromChunkFindLast: {
|
||||||
|
<B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
|
||||||
|
parent: B,
|
||||||
|
findLastPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never>,
|
||||||
|
): SubscriptionSubRef<
|
||||||
|
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
|
||||||
|
B
|
||||||
|
>
|
||||||
|
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
|
||||||
|
parent: B,
|
||||||
|
findLastPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never>,
|
||||||
|
): SubscriptionSubRef<
|
||||||
|
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
|
||||||
|
B
|
||||||
|
>
|
||||||
|
} = (
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<never>>,
|
||||||
|
findLastPredicate: Predicate.Predicate.Any,
|
||||||
|
) => new SubscriptionSubRefImpl(
|
||||||
|
parent,
|
||||||
|
parentValue => Option.getOrThrow(Chunk.findLast(parentValue, findLastPredicate)),
|
||||||
|
(parentValue, value) => Option.getOrThrow(Option.andThen(
|
||||||
|
Chunk.findLastIndex(parentValue, findLastPredicate),
|
||||||
|
index => Chunk.replace(parentValue, index, value),
|
||||||
|
)),
|
||||||
|
) as any
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
export * as ReactComponent from "./ReactComponent.js"
|
export * as Async from "./Async.js"
|
||||||
export * as ReactHook from "./ReactHook.js"
|
export * as Component from "./Component.js"
|
||||||
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
|
export * as ErrorObserver from "./ErrorObserver.js"
|
||||||
|
export * as Form from "./Form.js"
|
||||||
|
export * as Memoized from "./Memoized.js"
|
||||||
|
export * as Mutation from "./Mutation.js"
|
||||||
|
export * as PropertyPath from "./PropertyPath.js"
|
||||||
|
export * as PubSub from "./PubSub.js"
|
||||||
|
export * as Query from "./Query.js"
|
||||||
|
export * as QueryClient from "./QueryClient.js"
|
||||||
|
export * as ReactRuntime from "./ReactRuntime.js"
|
||||||
|
export * as Result from "./Result.js"
|
||||||
|
export * as SetStateAction from "./SetStateAction.js"
|
||||||
|
export * as Stream from "./Stream.js"
|
||||||
|
export * as Subscribable from "./Subscribable.js"
|
||||||
|
export * as SubscriptionRef from "./SubscriptionRef.js"
|
||||||
|
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
|
||||||
import * as PropertyPath from "./PropertyPath.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
|
|
||||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
|
||||||
|
|
||||||
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
|
||||||
|
|
||||||
readonly [Unify.typeSymbol]?: unknown
|
|
||||||
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
|
||||||
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace SubscriptionSubRef {
|
|
||||||
export interface Variance<in out A, in out B> {
|
|
||||||
readonly [SubscriptionSubRefTypeId]: {
|
|
||||||
readonly _A: Types.Invariant<A>
|
|
||||||
readonly _B: Types.Invariant<B>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
|
|
||||||
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
|
|
||||||
SubscriptionRef?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const refVariance = { _A: (_: any) => _ }
|
|
||||||
const synchronizedRefVariance = { _A: (_: any) => _ }
|
|
||||||
const subscriptionRefVariance = { _A: (_: any) => _ }
|
|
||||||
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
|
||||||
|
|
||||||
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
|
||||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
|
||||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
|
||||||
readonly [Ref.RefTypeId] = refVariance
|
|
||||||
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
|
|
||||||
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
|
|
||||||
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
|
|
||||||
|
|
||||||
readonly get: Effect.Effect<A>
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
readonly getter: (parentValue: B) => A,
|
|
||||||
readonly setter: (parentValue: B, value: A) => B,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
|
||||||
}
|
|
||||||
|
|
||||||
commit() {
|
|
||||||
return this.get
|
|
||||||
}
|
|
||||||
|
|
||||||
get changes(): Stream.Stream<A> {
|
|
||||||
return this.get.pipe(
|
|
||||||
Effect.map(a => this.parent.changes.pipe(
|
|
||||||
Stream.map(this.getter),
|
|
||||||
s => Stream.concat(Stream.make(a), s),
|
|
||||||
)),
|
|
||||||
Stream.unwrap,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
|
|
||||||
return this.modifyEffect(a => Effect.succeed(f(a)))
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
|
||||||
return Effect.Do.pipe(
|
|
||||||
Effect.bind("b", () => Ref.get(this.parent)),
|
|
||||||
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
|
||||||
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
|
||||||
Effect.map(({ ca: [c] }) => c),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const makeFromGetSet = <A, B>(
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
getter: (parentValue: B) => A,
|
|
||||||
setter: (parentValue: B, value: A) => B,
|
|
||||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
|
|
||||||
|
|
||||||
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
path: P,
|
|
||||||
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
|
|
||||||
parent,
|
|
||||||
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
|
||||||
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
|
||||||
)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * as PropertyPath from "./PropertyPath.js"
|
|
||||||
export * as SetStateAction from "./SetStateAction.js"
|
|
||||||
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
|
||||||
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
|
||||||
@@ -26,7 +26,12 @@
|
|||||||
|
|
||||||
// Build
|
// Build
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"declaration": true
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
|
||||||
|
"plugins": [
|
||||||
|
{ "name": "@effect/language-service" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"include": ["./src"]
|
"include": ["./src"]
|
||||||
|
|||||||
2
packages/example/.gitignore
vendored
2
packages/example/.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
.tanstack
|
||||||
|
|||||||
8
packages/example/biome.json
Normal file
8
packages/example/biome.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
||||||
|
"root": false,
|
||||||
|
"extends": "//",
|
||||||
|
"files": {
|
||||||
|
"includes": ["./src/**", "!src/routeTree.gen.ts"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
|
|
||||||
export default tseslint.config(
|
|
||||||
{ ignores: ['dist'] },
|
|
||||||
{
|
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
'react-hooks': reactHooks,
|
|
||||||
'react-refresh': reactRefresh,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -5,46 +5,38 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
|
||||||
"lint:tsc": "tsc --noEmit",
|
"lint:tsc": "tsc --noEmit",
|
||||||
"lint:eslint": "eslint .",
|
"lint:biome": "biome lint",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"clean:cache": "rm -rf .turbo node_modules/.tmp node_modules/.vite* .tanstack",
|
||||||
|
"clean:dist": "rm -rf dist",
|
||||||
|
"clean:modules": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.26.0",
|
"@tanstack/react-router": "^1.139.12",
|
||||||
"@tanstack/react-router": "^1.120.3",
|
"@tanstack/react-router-devtools": "^1.139.12",
|
||||||
"@tanstack/react-router-devtools": "^1.120.3",
|
"@tanstack/router-plugin": "^1.139.12",
|
||||||
"@tanstack/router-plugin": "^1.120.3",
|
"@types/react": "^19.2.7",
|
||||||
"@thilawyn/thilaschema": "^0.1.4",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react": "^19.1.4",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@types/react-dom": "^19.1.5",
|
"globals": "^17.0.0",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"react": "^19.2.0",
|
||||||
"eslint": "^9.26.0",
|
"react-dom": "^19.2.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"type-fest": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"vite": "^7.2.6"
|
||||||
"globals": "^16.1.0",
|
|
||||||
"react": "^19.1.0",
|
|
||||||
"react-dom": "^19.1.0",
|
|
||||||
"typescript-eslint": "^8.32.1",
|
|
||||||
"vite": "^6.3.5"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform": "^0.82.1",
|
"@effect/platform": "^0.94.0",
|
||||||
"@effect/platform-browser": "^0.62.1",
|
"@effect/platform-browser": "^0.74.0",
|
||||||
"@radix-ui/themes": "^3.2.1",
|
"@radix-ui/themes": "^3.2.1",
|
||||||
"@typed/async-data": "^0.13.1",
|
|
||||||
"@typed/id": "^0.17.2",
|
"@typed/id": "^0.17.2",
|
||||||
"@typed/lazy-ref": "^0.3.3",
|
"effect": "^3.19.8",
|
||||||
"effect": "^3.15.1",
|
"effect-fc": "workspace:*",
|
||||||
"lucide-react": "^0.510.0",
|
"react-icons": "^5.5.0"
|
||||||
"mobx": "^6.13.7",
|
|
||||||
"effect-fc": "workspace:*"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"effect": "^3.15.1",
|
"@types/react": "^19.2.7",
|
||||||
"@effect/platform": "^0.82.1",
|
"effect": "^3.19.8",
|
||||||
"@effect/platform-browser": "^0.62.1",
|
"react": "^19.2.0"
|
||||||
"@typed/lazy-ref": "^0.3.3",
|
|
||||||
"@typed/async-data": "^0.13.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,5 +1,5 @@
|
|||||||
import { ThSchema } from "@thilawyn/thilaschema"
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
|
import { assertEncodedJsonifiable } from "@/lib/schema"
|
||||||
|
|
||||||
|
|
||||||
export class Todo extends Schema.Class<Todo>("Todo")({
|
export class Todo extends Schema.Class<Todo>("Todo")({
|
||||||
@@ -14,7 +14,7 @@ export const TodoFromJsonStruct = Schema.Struct({
|
|||||||
...Todo.fields,
|
...Todo.fields,
|
||||||
completedAt: Schema.Option(Schema.DateTimeUtc),
|
completedAt: Schema.Option(Schema.DateTimeUtc),
|
||||||
}).pipe(
|
}).pipe(
|
||||||
ThSchema.assertEncodedJsonifiable
|
assertEncodedJsonifiable
|
||||||
)
|
)
|
||||||
|
|
||||||
export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)
|
export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)
|
||||||
|
|||||||
75
packages/example/src/lib/form/TextFieldFormInput.tsx
Normal file
75
packages/example/src/lib/form/TextFieldFormInput.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Callout, Flex, Spinner, Switch, TextField } from "@radix-ui/themes"
|
||||||
|
import { Array, Option, Struct } from "effect"
|
||||||
|
import { Component, Form, Subscribable } from "effect-fc"
|
||||||
|
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
extends TextField.RootProps, Form.useInput.Options {
|
||||||
|
readonly optional?: false
|
||||||
|
readonly field: Form.FormField<any, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionalProps
|
||||||
|
extends Omit<TextField.RootProps, "optional" | "defaultValue">, Form.useOptionalInput.Options<string> {
|
||||||
|
readonly optional: true
|
||||||
|
readonly field: Form.FormField<any, Option.Option<string>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TextFieldFormInputProps = Props | OptionalProps
|
||||||
|
|
||||||
|
|
||||||
|
export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInput")(function*(props: TextFieldFormInputProps) {
|
||||||
|
const input: (
|
||||||
|
| { readonly optional: true } & Form.useOptionalInput.Success<string>
|
||||||
|
| { readonly optional: false } & Form.useInput.Success<string>
|
||||||
|
) = props.optional
|
||||||
|
// biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported
|
||||||
|
? { optional: true, ...yield* Form.useOptionalInput(props.field, props) }
|
||||||
|
// biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported
|
||||||
|
: { optional: false, ...yield* Form.useInput(props.field, props) }
|
||||||
|
|
||||||
|
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
|
||||||
|
props.field.issues,
|
||||||
|
props.field.isValidating,
|
||||||
|
props.field.isSubmitting,
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" gap="1">
|
||||||
|
<TextField.Root
|
||||||
|
value={input.value}
|
||||||
|
onChange={e => input.setValue(e.target.value)}
|
||||||
|
disabled={(input.optional && !input.enabled) || isSubmitting}
|
||||||
|
{...Struct.omit(props, "optional", "defaultValue")}
|
||||||
|
>
|
||||||
|
{input.optional &&
|
||||||
|
<TextField.Slot side="left">
|
||||||
|
<Switch
|
||||||
|
size="1"
|
||||||
|
checked={input.enabled}
|
||||||
|
onCheckedChange={input.setEnabled}
|
||||||
|
/>
|
||||||
|
</TextField.Slot>
|
||||||
|
}
|
||||||
|
|
||||||
|
{isValidating &&
|
||||||
|
<TextField.Slot side="right">
|
||||||
|
<Spinner />
|
||||||
|
</TextField.Slot>
|
||||||
|
}
|
||||||
|
|
||||||
|
{props.children}
|
||||||
|
</TextField.Root>
|
||||||
|
|
||||||
|
{Option.match(Array.head(issues), {
|
||||||
|
onSome: issue => (
|
||||||
|
<Callout.Root>
|
||||||
|
<Callout.Text>{issue.message}</Callout.Text>
|
||||||
|
</Callout.Root>
|
||||||
|
),
|
||||||
|
|
||||||
|
onNone: () => <></>,
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}) {}
|
||||||
38
packages/example/src/lib/schema/datetime.ts
Normal file
38
packages/example/src/lib/schema/datetime.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { DateTime, Effect, Option, ParseResult, Schema } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class DateTimeUtcFromZoned extends Schema.transformOrFail(
|
||||||
|
Schema.DateTimeZonedFromSelf,
|
||||||
|
Schema.DateTimeUtcFromSelf,
|
||||||
|
{
|
||||||
|
strict: true,
|
||||||
|
encode: DateTime.setZoneCurrent,
|
||||||
|
decode: i => ParseResult.succeed(DateTime.toUtc(i)),
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class DateTimeZonedFromUtc extends Schema.transformOrFail(
|
||||||
|
Schema.DateTimeUtcFromSelf,
|
||||||
|
Schema.DateTimeZonedFromSelf,
|
||||||
|
{
|
||||||
|
strict: true,
|
||||||
|
encode: a => ParseResult.succeed(DateTime.toUtc(a)),
|
||||||
|
decode: DateTime.setZoneCurrent,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class DateTimeUtcFromZonedInput extends Schema.transformOrFail(
|
||||||
|
Schema.String,
|
||||||
|
DateTimeUtcFromZoned,
|
||||||
|
{
|
||||||
|
strict: true,
|
||||||
|
encode: a => ParseResult.succeed(DateTime.formatIsoZoned(a).slice(0, 16)),
|
||||||
|
decode: (i, _, ast) => Effect.flatMap(
|
||||||
|
DateTime.CurrentTimeZone,
|
||||||
|
timeZone => Option.match(DateTime.makeZoned(i, { timeZone, adjustForTimeZone: true }), {
|
||||||
|
onSome: ParseResult.succeed,
|
||||||
|
onNone: () => ParseResult.fail(new ParseResult.Type(ast, i, `Unable to decode ${JSON.stringify(i)} into a DateTime.Zoned`)),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
) {}
|
||||||
2
packages/example/src/lib/schema/index.ts
Normal file
2
packages/example/src/lib/schema/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./datetime"
|
||||||
|
export * from "./json"
|
||||||
6
packages/example/src/lib/schema/json.ts
Normal file
6
packages/example/src/lib/schema/json.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Schema } from "effect"
|
||||||
|
import type { JsonValue } from "type-fest"
|
||||||
|
|
||||||
|
|
||||||
|
export const assertEncodedJsonifiable = <S extends Schema.Schema<A, I, R>, A, I extends JsonValue, R>(schema: S & Schema.Schema<A, I, R>): S => schema
|
||||||
|
export const assertTypeJsonifiable = <S extends Schema.Schema<A, I, R>, A extends JsonValue, I, R>(schema: S & Schema.Schema<A, I, R>): S => schema
|
||||||
@@ -1,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 { ReactRuntime } 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,11 @@ declare module "@tanstack/react-router" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: React entrypoint
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ReffuseRuntime.Provider>
|
<ReactRuntime.Provider runtime={runtime}>
|
||||||
<RootContext.Provider layer={layer}>
|
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</RootContext.Provider>
|
</ReactRuntime.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,203 @@
|
|||||||
// 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 ResultRouteImport } from './routes/result'
|
||||||
|
import { Route as QueryRouteImport } from './routes/query'
|
||||||
|
import { Route as FormRouteImport } from './routes/form'
|
||||||
|
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 DevContextRouteImport } from './routes/dev/context'
|
||||||
|
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
|
||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
const ResultRoute = ResultRouteImport.update({
|
||||||
import { Route as TodosImport } from './routes/todos'
|
id: '/result',
|
||||||
import { Route as TimeImport } from './routes/time'
|
path: '/result',
|
||||||
import { Route as TestsImport } from './routes/tests'
|
getParentRoute: () => rootRouteImport,
|
||||||
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)
|
} as any)
|
||||||
|
const QueryRoute = QueryRouteImport.update({
|
||||||
const TimeRoute = TimeImport.update({
|
id: '/query',
|
||||||
id: '/time',
|
path: '/query',
|
||||||
path: '/time',
|
getParentRoute: () => rootRouteImport,
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
} as any)
|
||||||
|
const FormRoute = FormRouteImport.update({
|
||||||
const TestsRoute = TestsImport.update({
|
id: '/form',
|
||||||
id: '/tests',
|
path: '/form',
|
||||||
path: '/tests',
|
getParentRoute: () => rootRouteImport,
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
} as any)
|
||||||
|
const BlankRoute = BlankRouteImport.update({
|
||||||
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 DevContextRoute = DevContextRouteImport.update({
|
||||||
const QueryUsequeryRoute = QueryUsequeryImport.update({
|
id: '/dev/context',
|
||||||
id: '/query/usequery',
|
path: '/dev/context',
|
||||||
path: '/query/usequery',
|
getParentRoute: () => rootRouteImport,
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
} as any)
|
||||||
|
const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
|
||||||
const QueryUsemutationRoute = QueryUsemutationImport.update({
|
id: '/dev/async-rendering',
|
||||||
id: '/query/usemutation',
|
path: '/dev/async-rendering',
|
||||||
path: '/query/usemutation',
|
getParentRoute: () => rootRouteImport,
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
} 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
|
'/form': typeof FormRoute
|
||||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
'/query': typeof QueryRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/result': typeof ResultRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||||
'/tests': typeof TestsRoute
|
'/dev/context': typeof DevContextRoute
|
||||||
'/time': typeof TimeRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
'/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
|
'/form': typeof FormRoute
|
||||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
'/query': typeof QueryRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/result': typeof ResultRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||||
'/tests': typeof TestsRoute
|
'/dev/context': typeof DevContextRoute
|
||||||
'/time': typeof TimeRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
'/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
|
'/form': typeof FormRoute
|
||||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
'/query': typeof QueryRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/result': typeof ResultRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||||
'/tests': typeof TestsRoute
|
'/dev/context': typeof DevContextRoute
|
||||||
'/time': typeof TimeRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
'/todos': typeof TodosRoute
|
|
||||||
'/query/service': typeof QueryServiceRoute
|
|
||||||
'/query/usemutation': typeof QueryUsemutationRoute
|
|
||||||
'/query/usequery': typeof QueryUsequeryRoute
|
|
||||||
'/streams/pull': typeof StreamsPullRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/form'
|
||||||
| '/effect-component-tests'
|
| '/query'
|
||||||
| '/lazyref'
|
| '/result'
|
||||||
| '/promise'
|
| '/dev/async-rendering'
|
||||||
| '/tests'
|
| '/dev/context'
|
||||||
| '/time'
|
| '/dev/memo'
|
||||||
| '/todos'
|
|
||||||
| '/query/service'
|
|
||||||
| '/query/usemutation'
|
|
||||||
| '/query/usequery'
|
|
||||||
| '/streams/pull'
|
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/form'
|
||||||
| '/effect-component-tests'
|
| '/query'
|
||||||
| '/lazyref'
|
| '/result'
|
||||||
| '/promise'
|
| '/dev/async-rendering'
|
||||||
| '/tests'
|
| '/dev/context'
|
||||||
| '/time'
|
| '/dev/memo'
|
||||||
| '/todos'
|
|
||||||
| '/query/service'
|
|
||||||
| '/query/usemutation'
|
|
||||||
| '/query/usequery'
|
|
||||||
| '/streams/pull'
|
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/form'
|
||||||
| '/effect-component-tests'
|
| '/query'
|
||||||
| '/lazyref'
|
| '/result'
|
||||||
| '/promise'
|
| '/dev/async-rendering'
|
||||||
| '/tests'
|
| '/dev/context'
|
||||||
| '/time'
|
| '/dev/memo'
|
||||||
| '/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
|
FormRoute: typeof FormRoute
|
||||||
EffectComponentTestsRoute: typeof EffectComponentTestsRoute
|
QueryRoute: typeof QueryRoute
|
||||||
LazyrefRoute: typeof LazyrefRoute
|
ResultRoute: typeof ResultRoute
|
||||||
PromiseRoute: typeof PromiseRoute
|
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
|
||||||
TestsRoute: typeof TestsRoute
|
DevContextRoute: typeof DevContextRoute
|
||||||
TimeRoute: typeof TimeRoute
|
DevMemoRoute: typeof DevMemoRoute
|
||||||
TodosRoute: typeof TodosRoute
|
}
|
||||||
QueryServiceRoute: typeof QueryServiceRoute
|
|
||||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
declare module '@tanstack/react-router' {
|
||||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
interface FileRoutesByPath {
|
||||||
StreamsPullRoute: typeof StreamsPullRoute
|
'/result': {
|
||||||
|
id: '/result'
|
||||||
|
path: '/result'
|
||||||
|
fullPath: '/result'
|
||||||
|
preLoaderRoute: typeof ResultRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/query': {
|
||||||
|
id: '/query'
|
||||||
|
path: '/query'
|
||||||
|
fullPath: '/query'
|
||||||
|
preLoaderRoute: typeof QueryRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/form': {
|
||||||
|
id: '/form'
|
||||||
|
path: '/form'
|
||||||
|
fullPath: '/form'
|
||||||
|
preLoaderRoute: typeof FormRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/blank': {
|
||||||
|
id: '/blank'
|
||||||
|
path: '/blank'
|
||||||
|
fullPath: '/blank'
|
||||||
|
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/context': {
|
||||||
|
id: '/dev/context'
|
||||||
|
path: '/dev/context'
|
||||||
|
fullPath: '/dev/context'
|
||||||
|
preLoaderRoute: typeof DevContextRouteImport
|
||||||
|
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,
|
FormRoute: FormRoute,
|
||||||
EffectComponentTestsRoute: EffectComponentTestsRoute,
|
QueryRoute: QueryRoute,
|
||||||
LazyrefRoute: LazyrefRoute,
|
ResultRoute: ResultRoute,
|
||||||
PromiseRoute: PromiseRoute,
|
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
|
||||||
TestsRoute: TestsRoute,
|
DevContextRoute: DevContextRoute,
|
||||||
TimeRoute: TimeRoute,
|
DevMemoRoute: DevMemoRoute,
|
||||||
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>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
78
packages/example/src/routes/dev/async-rendering.tsx
Normal file
78
packages/example/src/routes/dev/async-rendering.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { Flex, Text, TextField } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Async, Component, Memoized } from "effect-fc"
|
||||||
|
import * as React from "react"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
|
// Generator version
|
||||||
|
const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
||||||
|
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent
|
||||||
|
const AsyncComponentFC = yield* AsyncComponent
|
||||||
|
const [input, setInput] = React.useState("")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
|
<TextField.Root
|
||||||
|
value={input}
|
||||||
|
onChange={e => setInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MemoizedAsyncComponentFC fallback={React.useMemo(() => <p>Loading memoized...</p>, [])} />
|
||||||
|
<AsyncComponentFC />
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Component.withRuntime(runtime.context)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pipeline version
|
||||||
|
// const RouteComponent = Component.make("RouteComponent")(() => Effect.Do,
|
||||||
|
// Effect.bind("VMemoizedAsyncComponent", () => Component.useFC(MemoizedAsyncComponent)),
|
||||||
|
// Effect.bind("VAsyncComponent", () => Component.useFC(AsyncComponent)),
|
||||||
|
// Effect.let("input", () => React.useState("")),
|
||||||
|
|
||||||
|
// Effect.map(({ input: [input, setInput], VAsyncComponent, VMemoizedAsyncComponent }) =>
|
||||||
|
// <Flex direction="column" align="stretch" gap="2">
|
||||||
|
// <TextField.Root
|
||||||
|
// value={input}
|
||||||
|
// onChange={e => setInput(e.target.value)}
|
||||||
|
// />
|
||||||
|
|
||||||
|
// <VMemoizedAsyncComponent />
|
||||||
|
// <VAsyncComponent />
|
||||||
|
// </Flex>
|
||||||
|
// ),
|
||||||
|
// ).pipe(
|
||||||
|
// Component.withRuntime(runtime.context)
|
||||||
|
// )
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
|
||||||
|
const SubComponentFC = yield* SubComponent
|
||||||
|
|
||||||
|
yield* Effect.sleep("500 millis") // Async operation
|
||||||
|
// Cannot use React hooks after the async operation
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" align="stretch">
|
||||||
|
<Text>Rendered!</Text>
|
||||||
|
<SubComponentFC />
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Async.async,
|
||||||
|
Async.withOptions({ defaultFallback: <p>Loading...</p> }),
|
||||||
|
) {}
|
||||||
|
class MemoizedAsyncComponent extends Memoized.memoized(AsyncComponent) {}
|
||||||
|
|
||||||
|
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
||||||
|
const [state] = React.useState(yield* Component.useOnMount(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
||||||
|
return <Text>{state}</Text>
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dev/async-rendering")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
42
packages/example/src/routes/dev/context.tsx
Normal file
42
packages/example/src/routes/dev/context.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Container, Flex, Text, TextField } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Console, Effect } from "effect"
|
||||||
|
import { Component } from "effect-fc"
|
||||||
|
import * as React from "react"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
|
class SubService extends Effect.Service<SubService>()("SubService", {
|
||||||
|
effect: (value: string) => Effect.succeed({ value })
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
const SubComponent = Component.makeUntraced("SubComponent")(function*() {
|
||||||
|
const service = yield* SubService
|
||||||
|
yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
|
yield* Effect.addFinalizer(() => Console.log("SubComponent unmounted"))
|
||||||
|
yield* Console.log("SubComponent mounted")
|
||||||
|
}))
|
||||||
|
|
||||||
|
return <Text>{service.value}</Text>
|
||||||
|
})
|
||||||
|
|
||||||
|
const ContextView = Component.makeUntraced("ContextView")(function*() {
|
||||||
|
const [serviceValue, setServiceValue] = React.useState("test")
|
||||||
|
const SubServiceLayer = React.useMemo(() => SubService.Default(serviceValue), [serviceValue])
|
||||||
|
const SubComponentFC = yield* Effect.provide(SubComponent, yield* Component.useContext(SubServiceLayer))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Flex direction="column" align="center">
|
||||||
|
<TextField.Root value={serviceValue} onChange={e => setServiceValue(e.target.value)} />
|
||||||
|
<SubComponentFC />
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Component.withRuntime(runtime.context)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dev/context")({
|
||||||
|
component: ContextView
|
||||||
|
})
|
||||||
37
packages/example/src/routes/dev/memo.tsx
Normal file
37
packages/example/src/routes/dev/memo.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Flex, Text, TextField } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Component, Memoized } from "effect-fc"
|
||||||
|
import * as React from "react"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
|
const RouteComponent = Component.makeUntraced("RouteComponent")(function*() {
|
||||||
|
const [value, setValue] = React.useState("")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<TextField.Root
|
||||||
|
value={value}
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{yield* Effect.map(SubComponent, FC => <FC />)}
|
||||||
|
{yield* Effect.map(MemoizedSubComponent, FC => <FC />)}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Component.withRuntime(runtime.context)
|
||||||
|
)
|
||||||
|
|
||||||
|
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
||||||
|
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
|
||||||
|
return <Text>{id}</Text>
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
class MemoizedSubComponent extends Memoized.memoized(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"),
|
|
||||||
)
|
|
||||||
132
packages/example/src/routes/form.tsx
Normal file
132
packages/example/src/routes/form.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
||||||
|
import { Component, Form, Subscribable } from "effect-fc"
|
||||||
|
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||||
|
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
|
const email = Schema.pattern<typeof Schema.String>(
|
||||||
|
/^(?!\.)(?!.*\.\.)([A-Z0-9_+-.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i,
|
||||||
|
|
||||||
|
{
|
||||||
|
identifier: "email",
|
||||||
|
title: "email",
|
||||||
|
message: () => "Not an email address",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const RegisterFormSchema = Schema.Struct({
|
||||||
|
email: Schema.String.pipe(email),
|
||||||
|
password: Schema.String.pipe(Schema.minLength(3)),
|
||||||
|
birth: Schema.OptionFromSelf(DateTimeUtcFromZonedInput),
|
||||||
|
})
|
||||||
|
|
||||||
|
const RegisterFormSubmitSchema = Schema.Struct({
|
||||||
|
email: Schema.transformOrFail(
|
||||||
|
Schema.String,
|
||||||
|
Schema.String,
|
||||||
|
{
|
||||||
|
decode: (input, _options, ast) => input !== "admin@admin.com"
|
||||||
|
? ParseResult.succeed(input)
|
||||||
|
: ParseResult.fail(new ParseResult.Type(ast, input, "This email is already in use.")),
|
||||||
|
encode: ParseResult.succeed,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
password: Schema.String,
|
||||||
|
birth: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
|
||||||
|
})
|
||||||
|
|
||||||
|
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||||
|
scoped: Form.service({
|
||||||
|
schema: RegisterFormSchema.pipe(
|
||||||
|
Schema.compose(
|
||||||
|
Schema.transformOrFail(
|
||||||
|
Schema.typeSchema(RegisterFormSchema),
|
||||||
|
Schema.typeSchema(RegisterFormSchema),
|
||||||
|
{
|
||||||
|
decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
|
||||||
|
encode: ParseResult.succeed,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
initialEncodedValue: { email: "", password: "", birth: Option.none() },
|
||||||
|
f: Effect.fnUntraced(function*([value]) {
|
||||||
|
yield* Effect.sleep("500 millis")
|
||||||
|
return yield* Schema.decode(RegisterFormSubmitSchema)(value)
|
||||||
|
}),
|
||||||
|
debounce: "500 millis",
|
||||||
|
})
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() {
|
||||||
|
const form = yield* RegisterForm
|
||||||
|
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
|
||||||
|
form.canSubmit,
|
||||||
|
form.mutation.result,
|
||||||
|
])
|
||||||
|
|
||||||
|
const runPromise = yield* Component.useRunPromise()
|
||||||
|
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||||
|
|
||||||
|
yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
|
yield* Effect.addFinalizer(() => Console.log("RegisterFormView unmounted"))
|
||||||
|
yield* Console.log("RegisterFormView mounted")
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container width="300">
|
||||||
|
<form onSubmit={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
void runPromise(form.submit)
|
||||||
|
}}>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<TextFieldFormInputFC
|
||||||
|
field={yield* form.field(["email"])}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextFieldFormInputFC
|
||||||
|
field={yield* form.field(["password"])}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextFieldFormInputFC
|
||||||
|
optional
|
||||||
|
type="datetime-local"
|
||||||
|
field={yield* form.field(["birth"])}
|
||||||
|
defaultValue=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button disabled={!canSubmit}>Submit</Button>
|
||||||
|
</Flex>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{Match.value(submitResult).pipe(
|
||||||
|
Match.tag("Initial", () => <></>),
|
||||||
|
Match.tag("Running", () => <Text>Submitting...</Text>),
|
||||||
|
Match.tag("Success", () => <Text>Submitted successfully!</Text>),
|
||||||
|
Match.tag("Failure", e => <Text>Error: {e.cause.toString()}</Text>),
|
||||||
|
Match.exhaustive,
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
const RegisterPage = Component.makeUntraced("RegisterPage")(function*() {
|
||||||
|
const RegisterFormViewFC = yield* Effect.provide(
|
||||||
|
RegisterFormView,
|
||||||
|
yield* Component.useContext(RegisterForm.Default),
|
||||||
|
)
|
||||||
|
|
||||||
|
return <RegisterFormViewFC />
|
||||||
|
}).pipe(
|
||||||
|
Component.withRuntime(runtime.context)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/form")({
|
||||||
|
component: RegisterPage
|
||||||
|
})
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Component } from "effect-fc"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
import { Todos } from "@/todo/Todos"
|
||||||
|
import { TodosState } from "@/todo/TodosState.service"
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/')({
|
const TodosStateLive = TodosState.Default("todos")
|
||||||
component: RouteComponent
|
|
||||||
|
const Index = Component.makeUntraced("Index")(function*() {
|
||||||
|
const TodosFC = yield* Effect.provide(
|
||||||
|
Todos,
|
||||||
|
yield* Component.useContext(TodosStateLive),
|
||||||
|
)
|
||||||
|
|
||||||
|
return <TodosFC />
|
||||||
|
}).pipe(
|
||||||
|
Component.withRuntime(runtime.context)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/")({
|
||||||
|
component: Index
|
||||||
})
|
})
|
||||||
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
116
packages/example/src/routes/query.tsx
Normal file
116
packages/example/src/routes/query.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { HttpClient, type HttpClientError } from "@effect/platform"
|
||||||
|
import { Button, Container, Flex, Heading, Slider, Text } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Array, Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect"
|
||||||
|
import { Component, ErrorObserver, Mutation, Query, Result, Subscribable, SubscriptionRef } from "effect-fc"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
|
const Post = Schema.Struct({
|
||||||
|
userId: Schema.Int,
|
||||||
|
id: Schema.Int,
|
||||||
|
title: Schema.String,
|
||||||
|
body: Schema.String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ResultView = Component.makeUntraced("Result")(function*() {
|
||||||
|
const runPromise = yield* Component.useRunPromise()
|
||||||
|
|
||||||
|
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
|
const idRef = yield* SubscriptionRef.make(1)
|
||||||
|
|
||||||
|
const query = yield* Query.service({
|
||||||
|
key: Stream.zipLatest(Stream.make("posts" as const), idRef.changes),
|
||||||
|
f: ([, id]) => HttpClient.HttpClient.pipe(
|
||||||
|
Effect.tap(Effect.sleep("500 millis")),
|
||||||
|
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||||
|
Effect.andThen(response => response.json),
|
||||||
|
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||||
|
),
|
||||||
|
staleTime: "10 seconds",
|
||||||
|
})
|
||||||
|
|
||||||
|
const mutation = yield* Mutation.make({
|
||||||
|
f: ([id]: readonly [id: number]) => HttpClient.HttpClient.pipe(
|
||||||
|
Effect.tap(Effect.sleep("500 millis")),
|
||||||
|
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||||
|
Effect.andThen(response => response.json),
|
||||||
|
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
return [idRef, query, mutation] as const
|
||||||
|
}))
|
||||||
|
|
||||||
|
const [id, setId] = yield* SubscriptionRef.useSubscriptionRefState(idRef)
|
||||||
|
const [queryResult, mutationResult] = yield* Subscribable.useSubscribables([query.result, mutation.result])
|
||||||
|
|
||||||
|
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||||
|
Effect.andThen(observer => observer.subscribe),
|
||||||
|
Effect.andThen(Stream.fromQueue),
|
||||||
|
Stream.unwrapScoped,
|
||||||
|
Stream.runForEach(flow(
|
||||||
|
Cause.failures,
|
||||||
|
Chunk.findFirst(e => e._tag === "RequestError" || e._tag === "ResponseError"),
|
||||||
|
Option.match({
|
||||||
|
onSome: e => Console.log("ResultView HttpClient error", e),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
Effect.forkScoped,
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Flex direction="column" align="center" gap="2">
|
||||||
|
<Slider
|
||||||
|
value={[id]}
|
||||||
|
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{Match.value(queryResult).pipe(
|
||||||
|
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||||
|
Match.tag("Success", result => <>
|
||||||
|
<Heading>{result.value.title}</Heading>
|
||||||
|
<Text>{result.value.body}</Text>
|
||||||
|
{Result.hasRefreshingFlag(result) && <Text>Refreshing...</Text>}
|
||||||
|
</>),
|
||||||
|
Match.tag("Failure", result =>
|
||||||
|
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||||
|
),
|
||||||
|
Match.orElse(() => <></>),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Flex direction="row" justify="center" align="center" gap="1">
|
||||||
|
<Button onClick={() => runPromise(query.refresh)}>Refresh</Button>
|
||||||
|
<Button onClick={() => runPromise(query.invalidateCache)}>Invalidate cache</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{Match.value(mutationResult).pipe(
|
||||||
|
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||||
|
Match.tag("Success", result => <>
|
||||||
|
<Heading>{result.value.title}</Heading>
|
||||||
|
<Text>{result.value.body}</Text>
|
||||||
|
{Result.hasRefreshingFlag(result) && <Text>Refreshing...</Text>}
|
||||||
|
</>),
|
||||||
|
Match.tag("Failure", result =>
|
||||||
|
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||||
|
),
|
||||||
|
Match.orElse(() => <></>),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Flex direction="row" justify="center" align="center" gap="1">
|
||||||
|
<Button onClick={() => runPromise(Effect.andThen(idRef, id => mutation.mutate([id])))}>Mutate</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/query")({
|
||||||
|
component: Component.withRuntime(ResultView, runtime.context)
|
||||||
|
})
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
60
packages/example/src/routes/result.tsx
Normal file
60
packages/example/src/routes/result.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { HttpClient, type HttpClientError } from "@effect/platform"
|
||||||
|
import { Container, Heading, Text } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect"
|
||||||
|
import { Component, ErrorObserver, Result, Subscribable } from "effect-fc"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
|
const Post = Schema.Struct({
|
||||||
|
userId: Schema.Int,
|
||||||
|
id: Schema.Int,
|
||||||
|
title: Schema.String,
|
||||||
|
body: Schema.String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ResultView = Component.makeUntraced("Result")(function*() {
|
||||||
|
const [resultSubscribable] = yield* Component.useOnMount(() => HttpClient.HttpClient.pipe(
|
||||||
|
Effect.andThen(client => client.get("https://jsonplaceholder.typicode.com/posts/1")),
|
||||||
|
Effect.andThen(response => response.json),
|
||||||
|
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||||
|
Effect.tap(Effect.sleep("250 millis")),
|
||||||
|
Result.forkEffect,
|
||||||
|
))
|
||||||
|
const [result] = yield* Subscribable.useSubscribables([resultSubscribable])
|
||||||
|
|
||||||
|
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||||
|
Effect.andThen(observer => observer.subscribe),
|
||||||
|
Effect.andThen(Stream.fromQueue),
|
||||||
|
Stream.unwrapScoped,
|
||||||
|
Stream.runForEach(flow(
|
||||||
|
Cause.failures,
|
||||||
|
Chunk.findFirst(e => e._tag === "RequestError" || e._tag === "ResponseError"),
|
||||||
|
Option.match({
|
||||||
|
onSome: e => Console.log("ResultView HttpClient error", e),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
Effect.forkScoped,
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{Match.value(result).pipe(
|
||||||
|
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||||
|
Match.tag("Success", result => <>
|
||||||
|
<Heading>{result.value.title}</Heading>
|
||||||
|
<Text>{result.value.body}</Text>
|
||||||
|
</>),
|
||||||
|
Match.tag("Failure", result =>
|
||||||
|
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||||
|
),
|
||||||
|
Match.orElse(() => <></>),
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/result")({
|
||||||
|
component: Component.withRuntime(ResultView, runtime.context)
|
||||||
|
})
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
15
packages/example/src/runtime.ts
Normal file
15
packages/example/src/runtime.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { FetchHttpClient } from "@effect/platform"
|
||||||
|
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||||
|
import { DateTime, Layer } from "effect"
|
||||||
|
import { ReactRuntime } from "effect-fc"
|
||||||
|
|
||||||
|
|
||||||
|
export const AppLive = Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(DateTime.layerCurrentZoneLocal),
|
||||||
|
Layer.provideMerge(Clipboard.layer),
|
||||||
|
Layer.provideMerge(Geolocation.layer),
|
||||||
|
Layer.provideMerge(Permissions.layer),
|
||||||
|
Layer.provideMerge(FetchHttpClient.layer),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const runtime = ReactRuntime.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"
|
|
||||||
135
packages/example/src/todo/Todo.tsx
Normal file
135
packages/example/src/todo/Todo.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
|
||||||
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
|
||||||
|
import { Component, Form, Subscribable } from "effect-fc"
|
||||||
|
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||||
|
import { FaDeleteLeft } from "react-icons/fa6"
|
||||||
|
import * as Domain from "@/domain"
|
||||||
|
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||||
|
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||||
|
import { TodosState } from "./TodosState.service"
|
||||||
|
|
||||||
|
|
||||||
|
const TodoFormSchema = Schema.compose(Schema.Struct({
|
||||||
|
...Domain.Todo.Todo.fields,
|
||||||
|
completedAt: Schema.OptionFromSelf(DateTimeUtcFromZonedInput),
|
||||||
|
}), Domain.Todo.Todo)
|
||||||
|
|
||||||
|
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 _tag: "edit", readonly id: string }
|
||||||
|
)
|
||||||
|
|
||||||
|
export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoProps) {
|
||||||
|
const state = yield* TodosState
|
||||||
|
|
||||||
|
const [
|
||||||
|
indexRef,
|
||||||
|
form,
|
||||||
|
contentField,
|
||||||
|
completedAtField,
|
||||||
|
] = yield* Component.useOnChange(() => Effect.gen(function*() {
|
||||||
|
const indexRef = Match.value(props).pipe(
|
||||||
|
Match.tag("new", () => Subscribable.make({ get: Effect.succeed(-1), changes: Stream.make(-1) })),
|
||||||
|
Match.tag("edit", ({ id }) => state.getIndexSubscribable(id)),
|
||||||
|
Match.exhaustive,
|
||||||
|
)
|
||||||
|
|
||||||
|
const form = yield* Form.service({
|
||||||
|
schema: TodoFormSchema,
|
||||||
|
initialEncodedValue: yield* Schema.encode(TodoFormSchema)(
|
||||||
|
yield* Match.value(props).pipe(
|
||||||
|
Match.tag("new", () => makeTodo),
|
||||||
|
Match.tag("edit", ({ id }) => state.getElementRef(id)),
|
||||||
|
Match.exhaustive,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
f: ([todo, form]) => Match.value(props).pipe(
|
||||||
|
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
|
||||||
|
Effect.andThen(makeTodo),
|
||||||
|
Effect.andThen(Schema.encode(TodoFormSchema)),
|
||||||
|
Effect.andThen(v => Ref.set(form.encodedValue, v)),
|
||||||
|
)),
|
||||||
|
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
|
||||||
|
Match.exhaustive,
|
||||||
|
),
|
||||||
|
autosubmit: props._tag === "edit",
|
||||||
|
debounce: "250 millis",
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
indexRef,
|
||||||
|
form,
|
||||||
|
yield* form.field(["content"]),
|
||||||
|
yield* form.field(["completedAt"]),
|
||||||
|
] as const
|
||||||
|
}), [props._tag, props._tag === "edit" ? props.id : undefined])
|
||||||
|
|
||||||
|
const [index, size, canSubmit] = yield* Subscribable.useSubscribables([
|
||||||
|
indexRef,
|
||||||
|
state.sizeSubscribable,
|
||||||
|
form.canSubmit,
|
||||||
|
])
|
||||||
|
|
||||||
|
const runSync = yield* Component.useRunSync()
|
||||||
|
const runPromise = yield* Component.useRunPromise<DateTime.CurrentTimeZone>()
|
||||||
|
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="row" align="center" gap="2">
|
||||||
|
<Box flexGrow="1">
|
||||||
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
|
<TextFieldFormInputFC field={contentField} />
|
||||||
|
|
||||||
|
<Flex direction="row" justify="center" align="center" gap="2">
|
||||||
|
<TextFieldFormInputFC
|
||||||
|
optional
|
||||||
|
field={completedAtField}
|
||||||
|
type="datetime-local"
|
||||||
|
defaultValue=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
{props._tag === "new" &&
|
||||||
|
<Button disabled={!canSubmit} onClick={() => void runPromise(form.submit)}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{props._tag === "edit" &&
|
||||||
|
<Flex direction="column" justify="center" align="center" gap="1">
|
||||||
|
<IconButton
|
||||||
|
disabled={index <= 0}
|
||||||
|
onClick={() => runSync(state.moveLeft(props.id))}
|
||||||
|
>
|
||||||
|
<FaArrowUp />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
disabled={index >= size - 1}
|
||||||
|
onClick={() => runSync(state.moveRight(props.id))}
|
||||||
|
>
|
||||||
|
<FaArrowDown />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton onClick={() => runSync(state.remove(props.id))}>
|
||||||
|
<FaDeleteLeft />
|
||||||
|
</IconButton>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}) {}
|
||||||
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, Subscribable } from "effect-fc"
|
||||||
|
import { Todo } from "./Todo"
|
||||||
|
import { TodosState } from "./TodosState.service"
|
||||||
|
|
||||||
|
|
||||||
|
export class Todos extends Component.makeUntraced("Todos")(function*() {
|
||||||
|
const state = yield* TodosState
|
||||||
|
const [todos] = yield* Subscribable.useSubscribables([state.ref])
|
||||||
|
|
||||||
|
yield* Component.useOnMount(() => Effect.andThen(
|
||||||
|
Console.log("Todos mounted"),
|
||||||
|
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||||
|
))
|
||||||
|
|
||||||
|
const TodoFC = yield* Todo
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Heading align="center">Todos</Heading>
|
||||||
|
|
||||||
|
<Flex direction="column" align="stretch" gap="2" mt="2">
|
||||||
|
<TodoFC _tag="new" />
|
||||||
|
|
||||||
|
{Chunk.map(todos, todo =>
|
||||||
|
<TodoFC key={todo.id} _tag="edit" id={todo.id} />
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}) {}
|
||||||
94
packages/example/src/todo/TodosState.service.ts
Normal file
94
packages/example/src/todo/TodosState.service.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { KeyValueStore } from "@effect/platform"
|
||||||
|
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||||
|
import { Chunk, Console, Effect, Option, Schema, Stream, SubscriptionRef } from "effect"
|
||||||
|
import { Subscribable, SubscriptionSubRef } from "effect-fc"
|
||||||
|
import { Todo } from "@/domain"
|
||||||
|
|
||||||
|
|
||||||
|
export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||||
|
scoped: Effect.fnUntraced(function*(key: string) {
|
||||||
|
const kv = yield* KeyValueStore.KeyValueStore
|
||||||
|
|
||||||
|
const readFromLocalStorage = Console.log("Reading todos from local storage...").pipe(
|
||||||
|
Effect.andThen(kv.get(key)),
|
||||||
|
Effect.andThen(Option.match({
|
||||||
|
onSome: Schema.decode(
|
||||||
|
Schema.parseJson(Schema.Chunk(Todo.TodoFromJson))
|
||||||
|
),
|
||||||
|
onNone: () => Effect.succeed(Chunk.empty()),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const saveToLocalStorage = (todos: Chunk.Chunk<Todo.Todo>) => Effect.andThen(
|
||||||
|
Console.log("Saving todos to local storage..."),
|
||||||
|
Chunk.isNonEmpty(todos)
|
||||||
|
? Effect.andThen(
|
||||||
|
Schema.encode(
|
||||||
|
Schema.parseJson(Schema.Chunk(Todo.TodoFromJson))
|
||||||
|
)(todos),
|
||||||
|
v => kv.set(key, v),
|
||||||
|
)
|
||||||
|
: kv.remove(key)
|
||||||
|
)
|
||||||
|
|
||||||
|
const ref = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
||||||
|
yield* Effect.forkScoped(ref.changes.pipe(
|
||||||
|
Stream.debounce("500 millis"),
|
||||||
|
Stream.runForEach(saveToLocalStorage),
|
||||||
|
))
|
||||||
|
yield* Effect.addFinalizer(() => ref.pipe(
|
||||||
|
Effect.andThen(saveToLocalStorage),
|
||||||
|
Effect.ignore,
|
||||||
|
))
|
||||||
|
|
||||||
|
const sizeSubscribable = Subscribable.make({
|
||||||
|
get: Effect.andThen(ref, Chunk.size),
|
||||||
|
get changes() { return Stream.map(ref.changes, Chunk.size) },
|
||||||
|
})
|
||||||
|
const getElementRef = (id: string) => SubscriptionSubRef.makeFromChunkFindFirst(ref, v => v.id === id)
|
||||||
|
const getIndexSubscribable = (id: string) => Subscribable.make({
|
||||||
|
get: Effect.flatMap(ref, Chunk.findFirstIndex(v => v.id === id)),
|
||||||
|
get changes() { return Stream.flatMap(ref.changes, Chunk.findFirstIndex(v => v.id === id)) },
|
||||||
|
})
|
||||||
|
|
||||||
|
const moveLeft = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.Do.pipe(
|
||||||
|
Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === id)),
|
||||||
|
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
||||||
|
Effect.bind("previous", ({ index }) => Chunk.get(todos, index - 1)),
|
||||||
|
Effect.andThen(({ todo, index, previous }) => index > 0
|
||||||
|
? todos.pipe(
|
||||||
|
Chunk.replace(index, previous),
|
||||||
|
Chunk.replace(index - 1, todo),
|
||||||
|
)
|
||||||
|
: todos
|
||||||
|
),
|
||||||
|
))
|
||||||
|
const moveRight = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.Do.pipe(
|
||||||
|
Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === id)),
|
||||||
|
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
||||||
|
Effect.bind("next", ({ index }) => Chunk.get(todos, index + 1)),
|
||||||
|
Effect.andThen(({ todo, index, next }) => index < Chunk.size(todos) - 1
|
||||||
|
? todos.pipe(
|
||||||
|
Chunk.replace(index, next),
|
||||||
|
Chunk.replace(index + 1, todo),
|
||||||
|
)
|
||||||
|
: todos
|
||||||
|
),
|
||||||
|
))
|
||||||
|
const remove = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.andThen(
|
||||||
|
Chunk.findFirstIndex(todos, v => v.id === id),
|
||||||
|
index => Chunk.remove(todos, index),
|
||||||
|
))
|
||||||
|
|
||||||
|
return {
|
||||||
|
ref,
|
||||||
|
sizeSubscribable,
|
||||||
|
getElementRef,
|
||||||
|
getIndexSubscribable,
|
||||||
|
moveLeft,
|
||||||
|
moveRight,
|
||||||
|
remove,
|
||||||
|
} as const
|
||||||
|
}),
|
||||||
|
|
||||||
|
dependencies: [BrowserKeyValueStore.layerLocalStorage],
|
||||||
|
}) {}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -22,9 +22,15 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"plugins": [
|
||||||
|
{ "name": "@effect/language-service" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,5 +20,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
|
import { tanstackRouter } from "@tanstack/router-plugin/vite"
|
||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
@@ -7,7 +7,10 @@ import { defineConfig } from "vite"
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
TanStackRouterVite(),
|
tanstackRouter({
|
||||||
|
target: "react",
|
||||||
|
autoCodeSplitting: true,
|
||||||
|
}),
|
||||||
react(),
|
react(),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
19
renovate.json
Normal file
19
renovate.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["config:recommended"],
|
||||||
|
"baseBranchPatterns": ["next"],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchManagers": ["bun", "npm"],
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"groupName": "bun minor+patch updates",
|
||||||
|
"groupSlug": "bun-minor-patch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": ["dockerfile", "docker-compose"],
|
||||||
|
"matchUpdateTypes": ["minor", "patch", "digest"],
|
||||||
|
"groupName": "docker minor+patch+digest updates",
|
||||||
|
"groupSlug": "docker-minor-patch-digest"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
21
turbo.json
21
turbo.json
@@ -1,11 +1,30 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://turbo.build/schema.json",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
"lint:tsc": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"lint:biome": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"inputs": ["./src/**"],
|
"inputs": ["./src/**"],
|
||||||
"outputs": ["./dist/**"]
|
"outputs": ["./dist/**"]
|
||||||
},
|
},
|
||||||
"pack": {}
|
"pack": {
|
||||||
|
"dependsOn": ["^pack"],
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"clean:cache": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"clean:dist": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"clean:modules": {
|
||||||
|
"cache": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user