Compare commits
14 Commits
220f009978
...
renovate/b
| Author | SHA1 | Date | |
|---|---|---|---|
| e25592530a | |||
| 0e8adf8506 | |||
| 89f966d93e | |||
| a432993ac3 | |||
| 6bf4e33c29 | |||
|
|
7bba444776 | ||
| 03aa7c467c | |||
| dbc75564bf | |||
| 2cc0cf6ab3 | |||
| e3ddb5b39e | |||
| 1020e4a1de | |||
|
|
59298e7074 | ||
| 9a3c91b50b | |||
|
|
831a808568 |
@@ -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/
|
|
||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.biome": "explicit"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
This monorepo contains:
|
This monorepo contains:
|
||||||
- [The `effect-fc` library](packages/effect-fc)
|
- [The `effect-fc` library](packages/effect-fc)
|
||||||
- [An example project](packges/example)
|
- [An example project](packages/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.69.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,25 +11,26 @@ Documentation is currently being written. In the meantime, you can take a look a
|
|||||||
- `react` & `@types/react` 19+
|
- `react` & `@types/react` 19+
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
- React Refresh replacement doesn't work for Effect FC's yet. Page reload is required to view changes. Regular React components are unaffected.
|
- React Refresh doesn't work for Effect FC's yet. Page reload is required to view changes. Regular React components are unaffected.
|
||||||
|
|
||||||
## What writing components looks like
|
## What writing components looks like
|
||||||
```typescript
|
```typescript
|
||||||
import { Component, Hook, Memoized } from "effect-fc"
|
import { Component } from "effect-fc"
|
||||||
|
import { useOnce, useSubscribables } from "effect-fc/Hooks"
|
||||||
import { Todo } from "./Todo"
|
import { Todo } from "./Todo"
|
||||||
import { TodosState } from "./TodosState.service"
|
import { TodosState } from "./TodosState.service"
|
||||||
import { runtime } from "@/runtime"
|
|
||||||
|
|
||||||
class Todos extends Component.make(function* Todos() {
|
|
||||||
|
export class Todos extends Component.makeUntraced("Todos")(function*() {
|
||||||
const state = yield* TodosState
|
const state = yield* TodosState
|
||||||
const [todos] = yield* Hook.useSubscribeRefs(state.ref)
|
const [todos] = yield* useSubscribables(state.ref)
|
||||||
|
|
||||||
yield* Hook.useOnce(() => Effect.andThen(
|
yield* useOnce(() => Effect.andThen(
|
||||||
Console.log("Todos mounted"),
|
Console.log("Todos mounted"),
|
||||||
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||||
))
|
))
|
||||||
|
|
||||||
const TodoFC = yield* Component.useFC(Todo)
|
const TodoFC = yield* Todo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@@ -38,22 +39,26 @@ class Todos extends Component.make(function* Todos() {
|
|||||||
<Flex direction="column" align="stretch" gap="2" mt="2">
|
<Flex direction="column" align="stretch" gap="2" mt="2">
|
||||||
<TodoFC _tag="new" />
|
<TodoFC _tag="new" />
|
||||||
|
|
||||||
{Chunk.map(todos, (v, k) =>
|
{Chunk.map(todos, todo =>
|
||||||
<TodoFC key={v.id} _tag="edit" index={k} />
|
<TodoFC key={todo.id} _tag="edit" id={todo.id} />
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}).pipe(
|
}) {}
|
||||||
Memoized.memo
|
|
||||||
) {}
|
|
||||||
|
|
||||||
const TodosEntrypoint = Component.make(function* TodosEntrypoint() {
|
const TodosStateLive = TodosState.Default("todos")
|
||||||
const context = yield* Hook.useContext(TodosState.Default, { finalizerExecutionMode: "fork" })
|
|
||||||
const TodosFC = yield* Effect.provide(Component.useFC(Todos), context)
|
const Index = Component.makeUntraced("Index")(function*() {
|
||||||
|
const context = yield* useContext(TodosStateLive, { finalizerExecutionMode: "fork" })
|
||||||
|
const TodosFC = yield* Effect.provide(Todos, context)
|
||||||
|
|
||||||
return <TodosFC />
|
return <TodosFC />
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Component.withRuntime(runtime.context)
|
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,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "effect-fc",
|
"name": "effect-fc",
|
||||||
"description": "Write React function components with Effect",
|
"description": "Write React function components with Effect",
|
||||||
"version": "0.1.2",
|
"version": "0.2.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -17,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),
|
||||||
|
))
|
||||||
@@ -1,31 +1,46 @@
|
|||||||
import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect"
|
/** 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 * as React from "react"
|
||||||
import * as Hook from "./Hook.js"
|
import { Memoized } from "./index.js"
|
||||||
import * as Memoized from "./Memoized.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("effect-fc/Component")
|
export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
||||||
export type TypeId = typeof TypeId
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
export interface Component<P extends {} = {}, E = never, R = never>
|
export interface Component<P extends {}, A extends React.ReactNode, E, R>
|
||||||
extends Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>, Component.Options {
|
extends
|
||||||
new(_: never): {}
|
Effect.Effect<(props: P) => A, never, Exclude<R, Scope.Scope>>,
|
||||||
|
Component.Options
|
||||||
|
{
|
||||||
|
new(_: never): Record<string, never>
|
||||||
readonly [TypeId]: TypeId
|
readonly [TypeId]: TypeId
|
||||||
|
readonly "~Props": P
|
||||||
|
readonly "~Success": A
|
||||||
|
readonly "~Error": E
|
||||||
|
readonly "~Context": R
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
makeFunctionComponent(runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>, scope: Scope.Scope): React.FC<P>
|
readonly body: (props: P) => Effect.Effect<A, E, R>
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
readonly body: (props: P) => Effect.Effect<React.ReactNode, E, R>
|
makeFunctionComponent(
|
||||||
|
runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>
|
||||||
|
): (props: P) => A
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Component {
|
export declare namespace Component {
|
||||||
export type Props<T> = T extends Component<infer P, infer _E, infer _R> ? P : never
|
export type Props<T extends Component<any, any, any, any>> = [T] extends [Component<infer P, infer _A, infer _E, infer _R>] ? P : never
|
||||||
export type Error<T> = T extends Component<infer _P, infer E, infer _R> ? E : 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 Context<T> = T extends Component<infer _P, infer _E, infer R> ? R : 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 {
|
export interface Options {
|
||||||
readonly displayName?: string
|
readonly displayName?: string
|
||||||
readonly finalizerExecutionMode: "sync" | "fork"
|
|
||||||
readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy
|
readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy
|
||||||
|
readonly finalizerExecutionDebounce: Duration.DurationInput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,58 +49,56 @@ const ComponentProto = Object.freeze({
|
|||||||
...Effectable.CommitPrototype,
|
...Effectable.CommitPrototype,
|
||||||
[TypeId]: TypeId,
|
[TypeId]: TypeId,
|
||||||
|
|
||||||
commit: Effect.fn("Component")(function* <P extends {}, E, R>(this: Component<P, E, R>) {
|
commit: Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode, E, R>(
|
||||||
const self = this
|
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!)
|
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
|
||||||
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
return React.useCallback(function ScopeProvider(props: P) {
|
return yield* React.useState(() => Runtime.runSync(runtimeRef.current)(Effect.cachedFunction(
|
||||||
const scope = Runtime.runSync(runtimeRef.current)(Hook.useScope(
|
(_services: readonly any[]) => Effect.sync(() => {
|
||||||
Array.from(
|
const f: React.FC<P> = this.makeFunctionComponent(runtimeRef)
|
||||||
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
f.displayName = this.displayName ?? "Anonymous"
|
||||||
),
|
return Memoized.isMemoized(this)
|
||||||
self,
|
? React.memo(f, this.propsAreEqual)
|
||||||
))
|
|
||||||
|
|
||||||
const FC = React.useMemo(() => {
|
|
||||||
const f = self.makeFunctionComponent(runtimeRef, scope)
|
|
||||||
f.displayName = self.displayName ?? "Anonymous"
|
|
||||||
return Memoized.isMemoized(self)
|
|
||||||
? React.memo(f, self.propsAreEqual)
|
|
||||||
: f
|
: f
|
||||||
}, [scope])
|
}),
|
||||||
|
Equivalence.array(Equivalence.strict()),
|
||||||
return React.createElement(FC, props)
|
)))[0](Array.from(
|
||||||
}, [])
|
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
||||||
|
))
|
||||||
}),
|
}),
|
||||||
|
|
||||||
makeFunctionComponent <P extends {}, E, R>(
|
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
||||||
this: Component<P, E, R>,
|
this: Component<P, A, E, R>,
|
||||||
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||||
scope: Scope.Scope,
|
) {
|
||||||
): React.FC<P> {
|
|
||||||
return (props: P) => Runtime.runSync(runtimeRef.current)(
|
return (props: P) => Runtime.runSync(runtimeRef.current)(
|
||||||
Effect.provideService(this.body(props), Scope.Scope, scope)
|
Effect.andThen(
|
||||||
|
useScope([], this),
|
||||||
|
scope => Effect.provideService(this.body(props), Scope.Scope, scope),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
} as const)
|
} as const)
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions: Component.Options = {
|
||||||
finalizerExecutionMode: "sync",
|
|
||||||
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
||||||
} as const
|
finalizerExecutionDebounce: "100 millis",
|
||||||
|
}
|
||||||
|
|
||||||
const nonReactiveTags = [Tracer.ParentSpan] as const
|
const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||||
|
|
||||||
|
|
||||||
export const isComponent = (u: unknown): u is Component<{}, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||||
|
|
||||||
export namespace make {
|
export declare namespace make {
|
||||||
export type Gen = {
|
export type Gen = {
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, P extends {} = {}>(
|
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A extends React.ReactNode, P extends {} = {}>(
|
||||||
body: (props: P) => Generator<Eff, React.ReactNode, never>,
|
body: (props: P) => Generator<Eff, A, never>
|
||||||
): Component<
|
): Component<
|
||||||
P,
|
P, A,
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
>
|
>
|
||||||
@@ -98,8 +111,8 @@ export namespace make {
|
|||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
||||||
>,
|
>,
|
||||||
props: NoInfer<P>,
|
props: NoInfer<P>,
|
||||||
) => B
|
) => B,
|
||||||
): Component<P, Effect.Effect.Error<B>, Effect.Effect.Context<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 {} = {}>(
|
<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>,
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
a: (
|
a: (
|
||||||
@@ -111,7 +124,7 @@ export namespace make {
|
|||||||
props: NoInfer<P>,
|
props: NoInfer<P>,
|
||||||
) => B,
|
) => B,
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
): Component<P, Effect.Effect.Error<C>, Effect.Effect.Context<C>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<C>>, Effect.Effect.Error<C>, Effect.Effect.Context<C>>
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
<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>,
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
a: (
|
a: (
|
||||||
@@ -124,7 +137,7 @@ export namespace make {
|
|||||||
) => B,
|
) => B,
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
): Component<P, Effect.Effect.Error<D>, Effect.Effect.Context<D>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<D>>, Effect.Effect.Error<D>, Effect.Effect.Context<D>>
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
<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>,
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
a: (
|
a: (
|
||||||
@@ -138,7 +151,7 @@ export namespace make {
|
|||||||
b: (_: B, props: NoInfer<P>) => C,
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
): Component<P, Effect.Effect.Error<E>, Effect.Effect.Context<E>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<E>>, Effect.Effect.Error<E>, Effect.Effect.Context<E>>
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
<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>,
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
a: (
|
a: (
|
||||||
@@ -153,7 +166,7 @@ export namespace make {
|
|||||||
c: (_: C, props: NoInfer<P>) => D,
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
): Component<P, Effect.Effect.Error<F>, Effect.Effect.Context<F>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<F>>, Effect.Effect.Error<F>, Effect.Effect.Context<F>>
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
<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>,
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
a: (
|
a: (
|
||||||
@@ -169,7 +182,7 @@ export namespace make {
|
|||||||
d: (_: D, props: NoInfer<P>) => E,
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
): Component<P, Effect.Effect.Error<G>, Effect.Effect.Context<G>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<G>>, Effect.Effect.Error<G>, Effect.Effect.Context<G>>
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
<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>,
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
a: (
|
a: (
|
||||||
@@ -186,7 +199,7 @@ export namespace make {
|
|||||||
e: (_: E, props: NoInfer<P>) => F,
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
g: (_: G, props: NoInfer<P>) => H,
|
||||||
): Component<P, Effect.Effect.Error<H>, Effect.Effect.Context<H>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<H>>, Effect.Effect.Error<H>, Effect.Effect.Context<H>>
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
<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>,
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
a: (
|
a: (
|
||||||
@@ -204,7 +217,7 @@ export namespace make {
|
|||||||
f: (_: F, props: NoInfer<P>) => G,
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
g: (_: G, props: NoInfer<P>) => H,
|
||||||
h: (_: H, props: NoInfer<P>) => I,
|
h: (_: H, props: NoInfer<P>) => I,
|
||||||
): Component<P, Effect.Effect.Error<I>, Effect.Effect.Context<I>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<I>>, Effect.Effect.Error<I>, Effect.Effect.Context<I>>
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I, J extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
<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>,
|
body: (props: P) => Generator<Eff, A, never>,
|
||||||
a: (
|
a: (
|
||||||
@@ -223,35 +236,35 @@ export namespace make {
|
|||||||
g: (_: G, props: NoInfer<P>) => H,
|
g: (_: G, props: NoInfer<P>) => H,
|
||||||
h: (_: H, props: NoInfer<P>) => I,
|
h: (_: H, props: NoInfer<P>) => I,
|
||||||
i: (_: I, props: NoInfer<P>) => J,
|
i: (_: I, props: NoInfer<P>) => J,
|
||||||
): Component<P, Effect.Effect.Error<J>, Effect.Effect.Context<J>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<J>>, Effect.Effect.Error<J>, Effect.Effect.Context<J>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NonGen = {
|
export type NonGen = {
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
||||||
body: (props: P) => Eff
|
body: (props: P) => Eff
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, P extends {} = {}>(
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, P extends {} = {}>(
|
||||||
body: (props: P) => A,
|
body: (props: P) => A,
|
||||||
a: (_: A, props: NoInfer<P>) => Eff,
|
a: (_: A, props: NoInfer<P>) => Eff,
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, P extends {} = {}>(
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, P extends {} = {}>(
|
||||||
body: (props: P) => A,
|
body: (props: P) => A,
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
b: (_: B, props: NoInfer<P>) => Eff,
|
b: (_: B, props: NoInfer<P>) => Eff,
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, P extends {} = {}>(
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, P extends {} = {}>(
|
||||||
body: (props: P) => A,
|
body: (props: P) => A,
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
c: (_: C, props: NoInfer<P>) => Eff,
|
c: (_: C, props: NoInfer<P>) => Eff,
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, P extends {} = {}>(
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, P extends {} = {}>(
|
||||||
body: (props: P) => A,
|
body: (props: P) => A,
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
b: (_: B, props: NoInfer<P>) => C,
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
d: (_: D, props: NoInfer<P>) => Eff,
|
d: (_: D, props: NoInfer<P>) => Eff,
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, P extends {} = {}>(
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, P extends {} = {}>(
|
||||||
body: (props: P) => A,
|
body: (props: P) => A,
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
@@ -259,7 +272,7 @@ export namespace make {
|
|||||||
c: (_: C, props: NoInfer<P>) => D,
|
c: (_: C, props: NoInfer<P>) => D,
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
e: (_: E, props: NoInfer<P>) => Eff,
|
e: (_: E, props: NoInfer<P>) => Eff,
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, P extends {} = {}>(
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, P extends {} = {}>(
|
||||||
body: (props: P) => A,
|
body: (props: P) => A,
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
@@ -268,7 +281,7 @@ export namespace make {
|
|||||||
d: (_: D, props: NoInfer<P>) => E,
|
d: (_: D, props: NoInfer<P>) => E,
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
f: (_: F, props: NoInfer<P>) => Eff,
|
f: (_: F, props: NoInfer<P>) => Eff,
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, P extends {} = {}>(
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, P extends {} = {}>(
|
||||||
body: (props: P) => A,
|
body: (props: P) => A,
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
@@ -278,7 +291,7 @@ export namespace make {
|
|||||||
e: (_: E, props: NoInfer<P>) => F,
|
e: (_: E, props: NoInfer<P>) => F,
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
g: (_: G, props: NoInfer<P>) => Eff,
|
g: (_: G, props: NoInfer<P>) => Eff,
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, P extends {} = {}>(
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, P extends {} = {}>(
|
||||||
body: (props: P) => A,
|
body: (props: P) => A,
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
@@ -289,7 +302,7 @@ export namespace make {
|
|||||||
f: (_: F, props: NoInfer<P>) => G,
|
f: (_: F, props: NoInfer<P>) => G,
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
g: (_: G, props: NoInfer<P>) => H,
|
||||||
h: (_: H, props: NoInfer<P>) => Eff,
|
h: (_: H, props: NoInfer<P>) => Eff,
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, I, P extends {} = {}>(
|
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, I, P extends {} = {}>(
|
||||||
body: (props: P) => A,
|
body: (props: P) => A,
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
a: (_: A, props: NoInfer<P>) => B,
|
||||||
@@ -301,7 +314,7 @@ export namespace make {
|
|||||||
g: (_: G, props: NoInfer<P>) => H,
|
g: (_: G, props: NoInfer<P>) => H,
|
||||||
h: (_: H, props: NoInfer<P>) => I,
|
h: (_: H, props: NoInfer<P>) => I,
|
||||||
i: (_: I, props: NoInfer<P>) => Eff,
|
i: (_: I, props: NoInfer<P>) => Eff,
|
||||||
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,13 +327,9 @@ export const make: (
|
|||||||
) => make.Gen & make.NonGen)
|
) => make.Gen & make.NonGen)
|
||||||
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => {
|
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => {
|
||||||
if (typeof spanNameOrBody !== "string") {
|
if (typeof spanNameOrBody !== "string") {
|
||||||
const displayName = displayNameFromBody(spanNameOrBody)
|
|
||||||
return Object.setPrototypeOf(
|
return Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, defaultOptions, {
|
Object.assign(function() {}, defaultOptions, {
|
||||||
body: displayName
|
body: Effect.fn(spanNameOrBody as any, ...pipeables),
|
||||||
? Effect.fn(displayName)(spanNameOrBody as any, ...pipeables as [])
|
|
||||||
: Effect.fn(spanNameOrBody as any, ...pipeables),
|
|
||||||
displayName,
|
|
||||||
}),
|
}),
|
||||||
ComponentProto,
|
ComponentProto,
|
||||||
)
|
)
|
||||||
@@ -330,35 +339,43 @@ export const make: (
|
|||||||
return (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
return (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, defaultOptions, {
|
Object.assign(function() {}, defaultOptions, {
|
||||||
body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []),
|
body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []),
|
||||||
displayName: displayNameFromBody(body) ?? spanNameOrBody,
|
displayName: spanNameOrBody,
|
||||||
}),
|
}),
|
||||||
ComponentProto,
|
ComponentProto,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeUntraced: make.Gen & make.NonGen = (
|
export const makeUntraced: (
|
||||||
body: Function,
|
& make.Gen
|
||||||
...pipeables: any[]
|
& make.NonGen
|
||||||
) => Object.setPrototypeOf(
|
& ((name: string) => make.Gen & make.NonGen)
|
||||||
|
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => (
|
||||||
|
typeof spanNameOrBody !== "string"
|
||||||
|
? Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, defaultOptions, {
|
Object.assign(function() {}, defaultOptions, {
|
||||||
body: Effect.fnUntraced(body as any, ...pipeables as []),
|
body: Effect.fnUntraced(spanNameOrBody as any, ...pipeables as []),
|
||||||
displayName: displayNameFromBody(body),
|
|
||||||
}),
|
}),
|
||||||
ComponentProto,
|
ComponentProto,
|
||||||
|
)
|
||||||
|
: (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
||||||
|
Object.assign(function() {}, defaultOptions, {
|
||||||
|
body: Effect.fnUntraced(body, ...pipeables as []),
|
||||||
|
displayName: spanNameOrBody,
|
||||||
|
}),
|
||||||
|
ComponentProto,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const displayNameFromBody = (body: Function) => !String.isEmpty(body.name) ? body.name : undefined
|
|
||||||
|
|
||||||
export const withOptions: {
|
export const withOptions: {
|
||||||
<T extends Component<any, any, any>>(
|
<T extends Component<any, any, any, any>>(
|
||||||
options: Partial<Component.Options>
|
options: Partial<Component.Options>
|
||||||
): (self: T) => T
|
): (self: T) => T
|
||||||
<T extends Component<any, any, any>>(
|
<T extends Component<any, any, any, any>>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<Component.Options>,
|
options: Partial<Component.Options>,
|
||||||
): T
|
): T
|
||||||
} = Function.dual(2, <T extends Component<any, any, any>>(
|
} = Function.dual(2, <T extends Component<any, any, any, any>>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<Component.Options>,
|
options: Partial<Component.Options>,
|
||||||
): T => Object.setPrototypeOf(
|
): T => Object.setPrototypeOf(
|
||||||
@@ -367,19 +384,222 @@ export const withOptions: {
|
|||||||
))
|
))
|
||||||
|
|
||||||
export const withRuntime: {
|
export const withRuntime: {
|
||||||
<P extends {}, E, R>(
|
<P extends {}, A extends React.ReactNode, E, R>(
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
): (self: Component<P, E, R>) => React.FC<P>
|
): (self: Component<P, A, E, Scope.Scope | NoInfer<R>>) => (props: P) => A
|
||||||
<P extends {}, E, R>(
|
<P extends {}, A extends React.ReactNode, E, R>(
|
||||||
self: Component<P, E, R>,
|
self: Component<P, A, E, Scope.Scope | NoInfer<R>>,
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
): React.FC<P>
|
): (props: P) => A
|
||||||
} = Function.dual(2, <P extends {}, E, R>(
|
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R>(
|
||||||
self: Component<P, E, R>,
|
self: Component<P, A, E, R>,
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
): React.FC<P> => function WithRuntime(props) {
|
) => function WithRuntime(props: P) {
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
Runtime.runSync(React.useContext(context))(self),
|
Runtime.runSync(React.useContext(context))(self),
|
||||||
props,
|
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 }
|
||||||
|
})
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
import { type Context, Effect, Equivalence, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import { SetStateAction } from "./types/index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface ScopeOptions {
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const useScope: {
|
|
||||||
(
|
|
||||||
deps: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<Scope.Scope>
|
|
||||||
} = Effect.fn("useScope")(function*(deps, options) {
|
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
|
|
||||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([
|
|
||||||
Ref.make(true),
|
|
||||||
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
|
||||||
])), [])
|
|
||||||
const [scope, setScope] = React.useState(initialScope)
|
|
||||||
|
|
||||||
React.useEffect(() => Runtime.runSync(runtime)(
|
|
||||||
Effect.if(isInitialRun, {
|
|
||||||
onTrue: () => Effect.as(
|
|
||||||
Ref.set(isInitialRun, false),
|
|
||||||
() => closeScope(scope, runtime, options),
|
|
||||||
),
|
|
||||||
|
|
||||||
onFalse: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe(
|
|
||||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
|
||||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
), deps)
|
|
||||||
|
|
||||||
return scope
|
|
||||||
})
|
|
||||||
|
|
||||||
const closeScope = (
|
|
||||||
scope: Scope.CloseableScope,
|
|
||||||
runtime: Runtime.Runtime<never>,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) => {
|
|
||||||
switch (options?.finalizerExecutionMode ?? "sync") {
|
|
||||||
case "sync":
|
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const useCallbackSync: {
|
|
||||||
<Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<(...args: Args) => A, never, R>
|
|
||||||
} = Effect.fn("useCallbackSync")(function* <Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<R>()
|
|
||||||
return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useCallbackPromise: {
|
|
||||||
<Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
|
||||||
} = Effect.fn("useCallbackPromise")(function* <Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<R>()
|
|
||||||
return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useMemo: {
|
|
||||||
<A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<A, E, R>
|
|
||||||
} = Effect.fn("useMemo")(function* <A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useOnce: {
|
|
||||||
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
|
||||||
} = Effect.fn("useOnce")(function* <A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>
|
|
||||||
) {
|
|
||||||
return yield* useMemo(factory, [])
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useEffect: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fn("useEffect")(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useEffect(() => Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
|
||||||
Effect.map(({ scope }) =>
|
|
||||||
() => closeScope(scope, runtime, options)
|
|
||||||
),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useLayoutEffect: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fn("useLayoutEffect")(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useLayoutEffect(() => Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
|
||||||
Effect.map(({ scope }) =>
|
|
||||||
() => closeScope(scope, runtime, options)
|
|
||||||
),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useFork: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fn("useFork")(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const scope = Runtime.runSync(runtime)(options?.scope
|
|
||||||
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
)
|
|
||||||
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
|
||||||
return () => closeScope(scope, runtime, {
|
|
||||||
...options,
|
|
||||||
finalizerExecutionMode: options?.finalizerExecutionMode ?? "fork",
|
|
||||||
})
|
|
||||||
}, deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useContext: {
|
|
||||||
<ROut, E, RIn>(
|
|
||||||
layer: Layer.Layer<ROut, E, RIn>,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>>
|
|
||||||
} = Effect.fn("useContext")(function* <ROut, E, RIn>(
|
|
||||||
layer: Layer.Layer<ROut, E, RIn>,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) {
|
|
||||||
const scope = yield* useScope([layer], options)
|
|
||||||
|
|
||||||
return yield* useMemo(() => Effect.provideService(
|
|
||||||
Effect.provide(Effect.context<ROut>(), layer),
|
|
||||||
Scope.Scope,
|
|
||||||
scope,
|
|
||||||
), [scope])
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useRefFromReactiveValue: {
|
|
||||||
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
|
||||||
} = Effect.fn("useRefFromReactiveValue")(function*(value) {
|
|
||||||
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
|
||||||
yield* useEffect(() => Ref.set(ref, value), [value])
|
|
||||||
return ref
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useSubscribeRefs: {
|
|
||||||
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
|
||||||
...refs: Refs
|
|
||||||
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
|
|
||||||
} = Effect.fn("useSubscribeRefs")(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
|
||||||
...refs: Refs
|
|
||||||
) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
|
||||||
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
|
|
||||||
))
|
|
||||||
|
|
||||||
yield* useFork(() => pipe(
|
|
||||||
refs.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())),
|
|
||||||
streams => Stream.zipLatestAll(...streams),
|
|
||||||
Stream.runForEach(v =>
|
|
||||||
Effect.sync(() => setReactStateValue(v))
|
|
||||||
),
|
|
||||||
), refs)
|
|
||||||
|
|
||||||
return reactStateValue as any
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useRefState: {
|
|
||||||
<A>(
|
|
||||||
ref: SubscriptionRef.SubscriptionRef<A>
|
|
||||||
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
|
|
||||||
} = Effect.fn("useRefState")(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(
|
|
||||||
Stream.changesWith(ref.changes, Equivalence.strict()),
|
|
||||||
v => Effect.sync(() => setReactStateValue(v)),
|
|
||||||
), [ref])
|
|
||||||
|
|
||||||
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
|
||||||
Ref.update(ref, prevState =>
|
|
||||||
SetStateAction.value(setStateAction, prevState)
|
|
||||||
),
|
|
||||||
[ref])
|
|
||||||
|
|
||||||
return [reactStateValue, setValue]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useStreamFromReactiveValues: {
|
|
||||||
<const A extends React.DependencyList>(
|
|
||||||
values: A
|
|
||||||
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
|
||||||
} = Effect.fn("useStreamFromReactiveValues")(function* <const A extends React.DependencyList>(values: A) {
|
|
||||||
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
|
|
||||||
Effect.bind("latest", () => Ref.make(values)),
|
|
||||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
|
||||||
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
|
|
||||||
Effect.flatMap(a => Effect.map(
|
|
||||||
Stream.fromPubSub(pubsub, { scoped: true }),
|
|
||||||
s => Stream.concat(Stream.make(a), s),
|
|
||||||
)),
|
|
||||||
Stream.unwrapScoped,
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
|
|
||||||
yield* useEffect(() => Ref.set(latest, values).pipe(
|
|
||||||
Effect.andThen(PubSub.publish(pubsub, values)),
|
|
||||||
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
|
||||||
), values)
|
|
||||||
|
|
||||||
return stream
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useSubscribeStream: {
|
|
||||||
<A, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>
|
|
||||||
): Effect.Effect<Option.Option<A>, never, R>
|
|
||||||
<A extends NonNullable<unknown>, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue: A,
|
|
||||||
): Effect.Effect<Option.Some<A>, never, R>
|
|
||||||
} = Effect.fn("useSubscribeStream")(function* <A extends NonNullable<unknown>, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue?: A,
|
|
||||||
) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(
|
|
||||||
React.useMemo(() => initialValue
|
|
||||||
? Option.some(initialValue)
|
|
||||||
: Option.none(),
|
|
||||||
[])
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(
|
|
||||||
Stream.changesWith(stream, Equivalence.strict()),
|
|
||||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
|
||||||
), [stream])
|
|
||||||
|
|
||||||
return reactStateValue as Option.Some<A>
|
|
||||||
})
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
import { type Equivalence, Function, Predicate } from "effect"
|
import { type Equivalence, Function, Predicate } from "effect"
|
||||||
import type * as Component from "./Component.js"
|
import type * as Component from "./Component.js"
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("effect-fc/Memoized")
|
export const TypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized")
|
||||||
export type TypeId = typeof TypeId
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
export interface Memoized<P> extends Memoized.Options<P> {
|
export interface Memoized<P> extends Memoized.Options<P> {
|
||||||
@@ -23,22 +24,25 @@ const MemoizedProto = Object.freeze({
|
|||||||
|
|
||||||
export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasProperty(u, TypeId)
|
export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasProperty(u, TypeId)
|
||||||
|
|
||||||
export const memo = <T extends Component.Component<any, any, any>>(
|
export const memoized = <T extends Component.Component<any, any, any, any>>(
|
||||||
self: T
|
self: T
|
||||||
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, self, MemoizedProto),
|
Object.assign(function() {}, self),
|
||||||
|
Object.freeze(Object.setPrototypeOf(
|
||||||
|
Object.assign({}, MemoizedProto),
|
||||||
Object.getPrototypeOf(self),
|
Object.getPrototypeOf(self),
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const withOptions: {
|
export const withOptions: {
|
||||||
<T extends Component.Component<any, any, any> & Memoized<any>>(
|
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
||||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>
|
options: Partial<Memoized.Options<Component.Component.Props<T>>>
|
||||||
): (self: T) => T
|
): (self: T) => T
|
||||||
<T extends Component.Component<any, any, any> & Memoized<any>>(
|
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
||||||
): T
|
): T
|
||||||
} = Function.dual(2, <T extends Component.Component<any, any, any> & Memoized<any>>(
|
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
||||||
): T => Object.setPrototypeOf(
|
): T => Object.setPrototypeOf(
|
||||||
|
|||||||
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,47 +0,0 @@
|
|||||||
import { Effect, type Layer, ManagedRuntime, type Runtime } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export interface ReactManagedRuntime<R, ER> {
|
|
||||||
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
|
||||||
readonly context: React.Context<Runtime.Runtime<R>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = <R, ER>(
|
|
||||||
layer: Layer.Layer<R, ER>,
|
|
||||||
memoMap?: Layer.MemoMap,
|
|
||||||
): ReactManagedRuntime<R, ER> => ({
|
|
||||||
runtime: ManagedRuntime.make(layer, memoMap),
|
|
||||||
context: React.createContext<Runtime.Runtime<R>>(null!),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export interface ProviderProps<R, ER> extends React.SuspenseProps {
|
|
||||||
readonly runtime: ReactManagedRuntime<R, ER>
|
|
||||||
readonly children?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Provider<R, ER>(
|
|
||||||
{ runtime, children, ...suspenseProps }: ProviderProps<R, ER>
|
|
||||||
): React.ReactNode {
|
|
||||||
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
React.Suspense,
|
|
||||||
suspenseProps,
|
|
||||||
React.createElement(ProviderInner<R, ER>, { runtime, promise, children }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProviderInnerProps<R, ER> {
|
|
||||||
readonly runtime: ReactManagedRuntime<R, ER>
|
|
||||||
readonly promise: Promise<Runtime.Runtime<R>>
|
|
||||||
readonly children?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProviderInner<R, ER>(
|
|
||||||
{ runtime, promise, children }: ProviderInnerProps<R, ER>
|
|
||||||
): React.ReactNode {
|
|
||||||
const value = React.use(promise)
|
|
||||||
return React.createElement(runtime.context, { value }, children)
|
|
||||||
}
|
|
||||||
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,74 +0,0 @@
|
|||||||
import { Effect, Function, Predicate, Runtime, Scope } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import type * as Component from "./Component.js"
|
|
||||||
import type { ExcludeKeys } from "./utils.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("effect-fc/Suspense")
|
|
||||||
export type TypeId = typeof TypeId
|
|
||||||
|
|
||||||
export interface Suspense extends Suspense.Options {
|
|
||||||
readonly [TypeId]: TypeId
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace Suspense {
|
|
||||||
export interface Options {
|
|
||||||
readonly defaultFallback?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Props = Omit<React.SuspenseProps, "children">
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const SuspenseProto = Object.freeze({
|
|
||||||
[TypeId]: TypeId,
|
|
||||||
makeFunctionComponent(
|
|
||||||
this: Component.Component<any, any, any> & Suspense,
|
|
||||||
runtimeRef: React.RefObject<Runtime.Runtime<any>>,
|
|
||||||
scope: Scope.Scope,
|
|
||||||
): React.FC<any> {
|
|
||||||
const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
|
||||||
|
|
||||||
return ({ fallback, name, ...props }: Suspense.Props) => {
|
|
||||||
const promise = Runtime.runPromise(runtimeRef.current)(
|
|
||||||
Effect.provideService(this.body(props), Scope.Scope, scope)
|
|
||||||
)
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
React.Suspense,
|
|
||||||
{ fallback: fallback ?? this.defaultFallback, name },
|
|
||||||
React.createElement(SuspenseInner, { promise }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} as const)
|
|
||||||
|
|
||||||
|
|
||||||
export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u, TypeId)
|
|
||||||
|
|
||||||
export const suspense = <T extends Component.Component<P, any, any>, P extends {}>(
|
|
||||||
self: T & Component.Component<ExcludeKeys<P, keyof Suspense.Props>, any, any>
|
|
||||||
): (
|
|
||||||
& Omit<T, keyof Component.Component<P, Component.Component.Error<T>, Component.Component.Context<T>>>
|
|
||||||
& Component.Component<P & Suspense.Props, Component.Component.Error<T>, Component.Component.Context<T>>
|
|
||||||
& Suspense
|
|
||||||
) => Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, self, SuspenseProto),
|
|
||||||
Object.getPrototypeOf(self),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const withOptions: {
|
|
||||||
<T extends Component.Component<any, any, any> & Suspense>(
|
|
||||||
options: Partial<Suspense.Options>
|
|
||||||
): (self: T) => T
|
|
||||||
<T extends Component.Component<any, any, any> & Suspense>(
|
|
||||||
self: T,
|
|
||||||
options: Partial<Suspense.Options>,
|
|
||||||
): T
|
|
||||||
} = Function.dual(2, <T extends Component.Component<any, any, any> & Suspense>(
|
|
||||||
self: T,
|
|
||||||
options: Partial<Suspense.Options>,
|
|
||||||
): T => Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, self, options),
|
|
||||||
Object.getPrototypeOf(self),
|
|
||||||
))
|
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
|
export * as Async from "./Async.js"
|
||||||
export * as Component from "./Component.js"
|
export * as Component from "./Component.js"
|
||||||
export * as Hook from "./Hook.js"
|
export * as ErrorObserver from "./ErrorObserver.js"
|
||||||
|
export * as Form from "./Form.js"
|
||||||
export * as Memoized from "./Memoized.js"
|
export * as Memoized from "./Memoized.js"
|
||||||
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
|
export * as Mutation from "./Mutation.js"
|
||||||
export * as Suspense from "./Suspense.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,111 +0,0 @@
|
|||||||
import { Chunk, 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("effect-fc/types/SubscriptionSubRef")
|
|
||||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
|
||||||
|
|
||||||
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
|
||||||
|
|
||||||
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(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>,
|
|
||||||
options: {
|
|
||||||
readonly get: (parentValue: B) => A
|
|
||||||
readonly set: (parentValue: B, value: A) => B
|
|
||||||
},
|
|
||||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, options.get, options.set)
|
|
||||||
|
|
||||||
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
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)),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const makeFromChunkRef = <A>(
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<A>>,
|
|
||||||
index: number,
|
|
||||||
): SubscriptionSubRef<A, Chunk.Chunk<A>> => new SubscriptionSubRefImpl(
|
|
||||||
parent,
|
|
||||||
parentValue => Chunk.unsafeGet(parentValue, index),
|
|
||||||
(parentValue, value) => Chunk.replace(parentValue, index, value),
|
|
||||||
)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * as PropertyPath from "./PropertyPath.js"
|
|
||||||
export * as SetStateAction from "./SetStateAction.js"
|
|
||||||
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
|
||||||
@@ -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"]
|
||||||
|
|||||||
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,48 +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": {
|
||||||
"@effect/language-service": "^0.23.4",
|
"@tanstack/react-router": "^1.139.12",
|
||||||
"@eslint/js": "^9.26.0",
|
"@tanstack/react-router-devtools": "^1.139.12",
|
||||||
"@tanstack/react-router": "^1.120.3",
|
"@tanstack/router-plugin": "^1.139.12",
|
||||||
"@tanstack/react-router-devtools": "^1.120.3",
|
"@types/react": "^19.2.7",
|
||||||
"@tanstack/router-plugin": "^1.120.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@thilawyn/thilaschema": "^0.1.4",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@types/react": "^19.1.4",
|
"globals": "^17.0.0",
|
||||||
"@types/react-dom": "^19.1.5",
|
"react": "^19.2.0",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"react-dom": "^19.2.0",
|
||||||
"eslint": "^9.26.0",
|
"type-fest": "^5.2.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"vite": "^7.2.6"
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
|
||||||
"globals": "^16.1.0",
|
|
||||||
"react": "^19.1.0",
|
|
||||||
"react-dom": "^19.1.0",
|
|
||||||
"typescript-eslint": "^8.32.1",
|
|
||||||
"vite": "^6.3.5"
|
|
||||||
},
|
},
|
||||||
"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:*",
|
"effect-fc": "workspace:*",
|
||||||
"lucide-react": "^0.510.0",
|
|
||||||
"mobx": "^6.13.7",
|
|
||||||
"react-icons": "^5.5.0"
|
"react-icons": "^5.5.0"
|
||||||
},
|
},
|
||||||
"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,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,5 +1,5 @@
|
|||||||
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
||||||
import { ReactManagedRuntime } from "effect-fc"
|
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 { routeTree } from "./routeTree.gen"
|
import { routeTree } from "./routeTree.gen"
|
||||||
@@ -14,10 +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>
|
||||||
<ReactManagedRuntime.Provider runtime={runtime}>
|
<ReactRuntime.Provider runtime={runtime}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</ReactManagedRuntime.Provider>
|
</ReactRuntime.Provider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,11 +9,30 @@
|
|||||||
// 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 { Route as rootRouteImport } from './routes/__root'
|
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 BlankRouteImport } from './routes/blank'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as DevMemoRouteImport } from './routes/dev/memo'
|
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 DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
|
||||||
|
|
||||||
|
const ResultRoute = ResultRouteImport.update({
|
||||||
|
id: '/result',
|
||||||
|
path: '/result',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const QueryRoute = QueryRouteImport.update({
|
||||||
|
id: '/query',
|
||||||
|
path: '/query',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const FormRoute = FormRouteImport.update({
|
||||||
|
id: '/form',
|
||||||
|
path: '/form',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const BlankRoute = BlankRouteImport.update({
|
const BlankRoute = BlankRouteImport.update({
|
||||||
id: '/blank',
|
id: '/blank',
|
||||||
path: '/blank',
|
path: '/blank',
|
||||||
@@ -29,6 +48,11 @@ const DevMemoRoute = DevMemoRouteImport.update({
|
|||||||
path: '/dev/memo',
|
path: '/dev/memo',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const DevContextRoute = DevContextRouteImport.update({
|
||||||
|
id: '/dev/context',
|
||||||
|
path: '/dev/context',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
|
const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
|
||||||
id: '/dev/async-rendering',
|
id: '/dev/async-rendering',
|
||||||
path: '/dev/async-rendering',
|
path: '/dev/async-rendering',
|
||||||
@@ -38,39 +62,101 @@ const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
|
|||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
|
'/form': typeof FormRoute
|
||||||
|
'/query': typeof QueryRoute
|
||||||
|
'/result': typeof ResultRoute
|
||||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||||
|
'/dev/context': typeof DevContextRoute
|
||||||
'/dev/memo': typeof DevMemoRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
|
'/form': typeof FormRoute
|
||||||
|
'/query': typeof QueryRoute
|
||||||
|
'/result': typeof ResultRoute
|
||||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||||
|
'/dev/context': typeof DevContextRoute
|
||||||
'/dev/memo': typeof DevMemoRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
|
'/form': typeof FormRoute
|
||||||
|
'/query': typeof QueryRoute
|
||||||
|
'/result': typeof ResultRoute
|
||||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||||
|
'/dev/context': typeof DevContextRoute
|
||||||
'/dev/memo': typeof DevMemoRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/blank' | '/dev/async-rendering' | '/dev/memo'
|
fullPaths:
|
||||||
|
| '/'
|
||||||
|
| '/blank'
|
||||||
|
| '/form'
|
||||||
|
| '/query'
|
||||||
|
| '/result'
|
||||||
|
| '/dev/async-rendering'
|
||||||
|
| '/dev/context'
|
||||||
|
| '/dev/memo'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/blank' | '/dev/async-rendering' | '/dev/memo'
|
to:
|
||||||
id: '__root__' | '/' | '/blank' | '/dev/async-rendering' | '/dev/memo'
|
| '/'
|
||||||
|
| '/blank'
|
||||||
|
| '/form'
|
||||||
|
| '/query'
|
||||||
|
| '/result'
|
||||||
|
| '/dev/async-rendering'
|
||||||
|
| '/dev/context'
|
||||||
|
| '/dev/memo'
|
||||||
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/blank'
|
||||||
|
| '/form'
|
||||||
|
| '/query'
|
||||||
|
| '/result'
|
||||||
|
| '/dev/async-rendering'
|
||||||
|
| '/dev/context'
|
||||||
|
| '/dev/memo'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
BlankRoute: typeof BlankRoute
|
BlankRoute: typeof BlankRoute
|
||||||
|
FormRoute: typeof FormRoute
|
||||||
|
QueryRoute: typeof QueryRoute
|
||||||
|
ResultRoute: typeof ResultRoute
|
||||||
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
|
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
|
||||||
|
DevContextRoute: typeof DevContextRoute
|
||||||
DevMemoRoute: typeof DevMemoRoute
|
DevMemoRoute: typeof DevMemoRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
|
'/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': {
|
'/blank': {
|
||||||
id: '/blank'
|
id: '/blank'
|
||||||
path: '/blank'
|
path: '/blank'
|
||||||
@@ -92,6 +178,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DevMemoRouteImport
|
preLoaderRoute: typeof DevMemoRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/dev/context': {
|
||||||
|
id: '/dev/context'
|
||||||
|
path: '/dev/context'
|
||||||
|
fullPath: '/dev/context'
|
||||||
|
preLoaderRoute: typeof DevContextRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/dev/async-rendering': {
|
'/dev/async-rendering': {
|
||||||
id: '/dev/async-rendering'
|
id: '/dev/async-rendering'
|
||||||
path: '/dev/async-rendering'
|
path: '/dev/async-rendering'
|
||||||
@@ -105,7 +198,11 @@ declare module '@tanstack/react-router' {
|
|||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BlankRoute: BlankRoute,
|
BlankRoute: BlankRoute,
|
||||||
|
FormRoute: FormRoute,
|
||||||
|
QueryRoute: QueryRoute,
|
||||||
|
ResultRoute: ResultRoute,
|
||||||
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
|
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
|
||||||
|
DevContextRoute: DevContextRoute,
|
||||||
DevMemoRoute: DevMemoRoute,
|
DevMemoRoute: DevMemoRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { runtime } from "@/runtime"
|
|
||||||
import { Flex, Text, TextField } from "@radix-ui/themes"
|
import { Flex, Text, TextField } from "@radix-ui/themes"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Component, Hook, Memoized, Suspense } from "effect-fc"
|
import { Async, Component, Memoized } from "effect-fc"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
// Generator version
|
// Generator version
|
||||||
const RouteComponent = Component.make(function* AsyncRendering() {
|
const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
||||||
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent
|
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent
|
||||||
const AsyncComponentFC = yield* AsyncComponent
|
const AsyncComponentFC = yield* AsyncComponent
|
||||||
const [input, setInput] = React.useState("")
|
const [input, setInput] = React.useState("")
|
||||||
@@ -50,7 +50,7 @@ const RouteComponent = Component.make(function* AsyncRendering() {
|
|||||||
// )
|
// )
|
||||||
|
|
||||||
|
|
||||||
class AsyncComponent extends Component.make(function* AsyncComponent() {
|
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
|
||||||
const SubComponentFC = yield* SubComponent
|
const SubComponentFC = yield* SubComponent
|
||||||
|
|
||||||
yield* Effect.sleep("500 millis") // Async operation
|
yield* Effect.sleep("500 millis") // Async operation
|
||||||
@@ -63,13 +63,13 @@ class AsyncComponent extends Component.make(function* AsyncComponent() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Suspense.suspense,
|
Async.async,
|
||||||
Suspense.withOptions({ defaultFallback: <p>Loading...</p> }),
|
Async.withOptions({ defaultFallback: <p>Loading...</p> }),
|
||||||
) {}
|
) {}
|
||||||
class MemoizedAsyncComponent extends Memoized.memo(AsyncComponent) {}
|
class MemoizedAsyncComponent extends Memoized.memoized(AsyncComponent) {}
|
||||||
|
|
||||||
class SubComponent extends Component.make(function* SubComponent() {
|
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
||||||
const [state] = React.useState(yield* Hook.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
const [state] = React.useState(yield* Component.useOnMount(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
||||||
return <Text>{state}</Text>
|
return <Text>{state}</Text>
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
|
})
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { runtime } from "@/runtime"
|
|
||||||
import { Flex, Text, TextField } from "@radix-ui/themes"
|
import { Flex, Text, TextField } from "@radix-ui/themes"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Component, Memoized } from "effect-fc"
|
import { Component, Memoized } from "effect-fc"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
const RouteComponent = Component.make(function* RouteComponent() {
|
const RouteComponent = Component.makeUntraced("RouteComponent")(function*() {
|
||||||
const [value, setValue] = React.useState("")
|
const [value, setValue] = React.useState("")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,12 +25,12 @@ const RouteComponent = Component.make(function* RouteComponent() {
|
|||||||
Component.withRuntime(runtime.context)
|
Component.withRuntime(runtime.context)
|
||||||
)
|
)
|
||||||
|
|
||||||
class SubComponent extends Component.make(function* SubComponent() {
|
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
||||||
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
|
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
|
||||||
return <Text>{id}</Text>
|
return <Text>{id}</Text>
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
class MemoizedSubComponent extends Memoized.memo(SubComponent) {}
|
class MemoizedSubComponent extends Memoized.memoized(SubComponent) {}
|
||||||
|
|
||||||
export const Route = createFileRoute("/dev/memo")({
|
export const Route = createFileRoute("/dev/memo")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|||||||
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,20 +1,24 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Component } from "effect-fc"
|
||||||
import { runtime } from "@/runtime"
|
import { runtime } from "@/runtime"
|
||||||
import { Todos } from "@/todo/Todos"
|
import { Todos } from "@/todo/Todos"
|
||||||
import { TodosState } from "@/todo/TodosState.service"
|
import { TodosState } from "@/todo/TodosState.service"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
import { Component, Hook } from "effect-fc"
|
|
||||||
|
|
||||||
|
|
||||||
const TodosStateLive = TodosState.Default("todos")
|
const TodosStateLive = TodosState.Default("todos")
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
const Index = Component.makeUntraced("Index")(function*() {
|
||||||
component: Component.make(function* Index() {
|
const TodosFC = yield* Effect.provide(
|
||||||
return yield* Todos.pipe(
|
Todos,
|
||||||
Effect.map(FC => <FC />),
|
yield* Component.useContext(TodosStateLive),
|
||||||
Effect.provide(yield* Hook.useContext(TodosStateLive, { finalizerExecutionMode: "fork" })),
|
|
||||||
)
|
)
|
||||||
}).pipe(
|
|
||||||
|
return <TodosFC />
|
||||||
|
}).pipe(
|
||||||
Component.withRuntime(runtime.context)
|
Component.withRuntime(runtime.context)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/")({
|
||||||
|
component: Index
|
||||||
})
|
})
|
||||||
|
|||||||
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)
|
||||||
|
})
|
||||||
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,14 +1,15 @@
|
|||||||
import { FetchHttpClient } from "@effect/platform"
|
import { FetchHttpClient } from "@effect/platform"
|
||||||
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||||
import { Layer } from "effect"
|
import { DateTime, Layer } from "effect"
|
||||||
import { ReactManagedRuntime } from "effect-fc"
|
import { ReactRuntime } from "effect-fc"
|
||||||
|
|
||||||
|
|
||||||
export const AppLive = Layer.empty.pipe(
|
export const AppLive = Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(DateTime.layerCurrentZoneLocal),
|
||||||
Layer.provideMerge(Clipboard.layer),
|
Layer.provideMerge(Clipboard.layer),
|
||||||
Layer.provideMerge(Geolocation.layer),
|
Layer.provideMerge(Geolocation.layer),
|
||||||
Layer.provideMerge(Permissions.layer),
|
Layer.provideMerge(Permissions.layer),
|
||||||
Layer.provideMerge(FetchHttpClient.layer),
|
Layer.provideMerge(FetchHttpClient.layer),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const runtime = ReactManagedRuntime.make(AppLive)
|
export const runtime = ReactRuntime.make(AppLive)
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import * as Domain from "@/domain"
|
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
|
||||||
import { Box, Button, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
import { Chunk, Effect, Match, Option, Ref, Runtime, SubscriptionRef } from "effect"
|
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
|
||||||
import { Component, Hook, Memoized } from "effect-fc"
|
import { Component, Form, Subscribable } from "effect-fc"
|
||||||
import { SubscriptionSubRef } from "effect-fc/types"
|
|
||||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||||
import { FaDeleteLeft } from "react-icons/fa6"
|
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"
|
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(
|
const makeTodo = makeUuid4.pipe(
|
||||||
Effect.map(id => Domain.Todo.Todo.make({
|
Effect.map(id => Domain.Todo.Todo.make({
|
||||||
id,
|
id,
|
||||||
@@ -20,96 +26,110 @@ const makeTodo = makeUuid4.pipe(
|
|||||||
|
|
||||||
|
|
||||||
export type TodoProps = (
|
export type TodoProps = (
|
||||||
| { readonly _tag: "new", readonly index?: never }
|
| { readonly _tag: "new" }
|
||||||
| { readonly _tag: "edit", readonly index: number }
|
| { readonly _tag: "edit", readonly id: string }
|
||||||
)
|
)
|
||||||
|
|
||||||
export class Todo extends Component.make(function* Todo(props: TodoProps) {
|
export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoProps) {
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
const state = yield* TodosState
|
const state = yield* TodosState
|
||||||
|
|
||||||
const [ref, contentRef] = yield* Hook.useMemo(() => Match.value(props).pipe(
|
const [
|
||||||
Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)),
|
indexRef,
|
||||||
Match.tag("edit", ({ index }) => Effect.succeed(SubscriptionSubRef.makeFromChunkRef(state.ref, index))),
|
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,
|
Match.exhaustive,
|
||||||
|
)
|
||||||
|
|
||||||
Effect.map(ref => [
|
const form = yield* Form.service({
|
||||||
ref,
|
schema: TodoFormSchema,
|
||||||
SubscriptionSubRef.makeFromPath(ref, ["content"]),
|
initialEncodedValue: yield* Schema.encode(TodoFormSchema)(
|
||||||
] as const),
|
yield* Match.value(props).pipe(
|
||||||
), [props._tag, props.index])
|
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
|
||||||
|
|
||||||
const [content, size] = yield* Hook.useSubscribeRefs(contentRef, state.sizeRef)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" align="stretch" gap="2">
|
|
||||||
<Flex direction="row" align="center" gap="2">
|
<Flex direction="row" align="center" gap="2">
|
||||||
<Box flexGrow="1">
|
<Box flexGrow="1">
|
||||||
<TextArea
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
value={content}
|
<TextFieldFormInputFC field={contentField} />
|
||||||
onChange={e => Runtime.runSync(runtime)(Ref.set(contentRef, e.target.value))}
|
|
||||||
|
<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>
|
</Box>
|
||||||
|
|
||||||
{props._tag === "edit" &&
|
{props._tag === "edit" &&
|
||||||
<Flex direction="column" justify="center" align="center" gap="1">
|
<Flex direction="column" justify="center" align="center" gap="1">
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={props.index <= 0}
|
disabled={index <= 0}
|
||||||
onClick={() => Runtime.runSync(runtime)(
|
onClick={() => runSync(state.moveLeft(props.id))}
|
||||||
SubscriptionRef.updateEffect(state.ref, todos => Effect.gen(function*() {
|
|
||||||
if (props.index <= 0) return yield* Option.none()
|
|
||||||
return todos.pipe(
|
|
||||||
Chunk.replace(props.index, yield* Chunk.get(todos, props.index - 1)),
|
|
||||||
Chunk.replace(props.index - 1, yield* ref),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<FaArrowUp />
|
<FaArrowUp />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={props.index >= size - 1}
|
disabled={index >= size - 1}
|
||||||
onClick={() => Runtime.runSync(runtime)(
|
onClick={() => runSync(state.moveRight(props.id))}
|
||||||
SubscriptionRef.updateEffect(state.ref, todos => Effect.gen(function*() {
|
|
||||||
if (props.index >= size - 1) return yield* Option.none()
|
|
||||||
return todos.pipe(
|
|
||||||
Chunk.replace(props.index, yield* Chunk.get(todos, props.index + 1)),
|
|
||||||
Chunk.replace(props.index + 1, yield* ref),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<FaArrowDown />
|
<FaArrowDown />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<IconButton
|
<IconButton onClick={() => runSync(state.remove(props.id))}>
|
||||||
onClick={() => Runtime.runSync(runtime)(
|
|
||||||
Ref.update(state.ref, Chunk.remove(props.index))
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<FaDeleteLeft />
|
<FaDeleteLeft />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{props._tag === "new" &&
|
|
||||||
<Flex direction="row" justify="center">
|
|
||||||
<Button
|
|
||||||
onClick={() => ref.pipe(
|
|
||||||
Effect.andThen(todo => Ref.update(state.ref, Chunk.prepend(todo))),
|
|
||||||
Effect.andThen(makeTodo),
|
|
||||||
Effect.andThen(todo => Ref.set(ref, todo)),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
</Flex>
|
|
||||||
)
|
)
|
||||||
}).pipe(
|
}) {}
|
||||||
Memoized.memo
|
|
||||||
) {}
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Container, Flex, Heading } from "@radix-ui/themes"
|
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||||
import { Chunk, Console, Effect } from "effect"
|
import { Chunk, Console, Effect } from "effect"
|
||||||
import { Component, Hook } from "effect-fc"
|
import { Component, Subscribable } from "effect-fc"
|
||||||
import { Todo } from "./Todo"
|
import { Todo } from "./Todo"
|
||||||
import { TodosState } from "./TodosState.service"
|
import { TodosState } from "./TodosState.service"
|
||||||
|
|
||||||
|
|
||||||
export class Todos extends Component.make(function* Todos() {
|
export class Todos extends Component.makeUntraced("Todos")(function*() {
|
||||||
const state = yield* TodosState
|
const state = yield* TodosState
|
||||||
const [todos] = yield* Hook.useSubscribeRefs(state.ref)
|
const [todos] = yield* Subscribable.useSubscribables([state.ref])
|
||||||
|
|
||||||
yield* Hook.useOnce(() => Effect.andThen(
|
yield* Component.useOnMount(() => Effect.andThen(
|
||||||
Console.log("Todos mounted"),
|
Console.log("Todos mounted"),
|
||||||
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||||
))
|
))
|
||||||
@@ -23,8 +23,8 @@ export class Todos extends Component.make(function* Todos() {
|
|||||||
<Flex direction="column" align="stretch" gap="2" mt="2">
|
<Flex direction="column" align="stretch" gap="2" mt="2">
|
||||||
<TodoFC _tag="new" />
|
<TodoFC _tag="new" />
|
||||||
|
|
||||||
{Chunk.map(todos, (v, k) =>
|
{Chunk.map(todos, todo =>
|
||||||
<TodoFC key={v.id} _tag="edit" index={k} />
|
<TodoFC key={todo.id} _tag="edit" id={todo.id} />
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Todo } from "@/domain"
|
|
||||||
import { KeyValueStore } from "@effect/platform"
|
import { KeyValueStore } from "@effect/platform"
|
||||||
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||||
import { Chunk, Console, Effect, Option, Schema, Stream, SubscriptionRef } from "effect"
|
import { Chunk, Console, Effect, Option, Schema, Stream, SubscriptionRef } from "effect"
|
||||||
import { SubscriptionSubRef } from "effect-fc/types"
|
import { Subscribable, SubscriptionSubRef } from "effect-fc"
|
||||||
|
import { Todo } from "@/domain"
|
||||||
|
|
||||||
|
|
||||||
export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||||
effect: Effect.fn("TodosState")(function*(key: string) {
|
scoped: Effect.fnUntraced(function*(key: string) {
|
||||||
const kv = yield* KeyValueStore.KeyValueStore
|
const kv = yield* KeyValueStore.KeyValueStore
|
||||||
|
|
||||||
const readFromLocalStorage = Console.log("Reading todos from local storage...").pipe(
|
const readFromLocalStorage = Console.log("Reading todos from local storage...").pipe(
|
||||||
@@ -18,7 +18,6 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
|||||||
onNone: () => Effect.succeed(Chunk.empty()),
|
onNone: () => Effect.succeed(Chunk.empty()),
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveToLocalStorage = (todos: Chunk.Chunk<Todo.Todo>) => Effect.andThen(
|
const saveToLocalStorage = (todos: Chunk.Chunk<Todo.Todo>) => Effect.andThen(
|
||||||
Console.log("Saving todos to local storage..."),
|
Console.log("Saving todos to local storage..."),
|
||||||
Chunk.isNonEmpty(todos)
|
Chunk.isNonEmpty(todos)
|
||||||
@@ -32,8 +31,6 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ref = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
const ref = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
||||||
const sizeRef = SubscriptionSubRef.makeFromPath(ref, ["length"])
|
|
||||||
|
|
||||||
yield* Effect.forkScoped(ref.changes.pipe(
|
yield* Effect.forkScoped(ref.changes.pipe(
|
||||||
Stream.debounce("500 millis"),
|
Stream.debounce("500 millis"),
|
||||||
Stream.runForEach(saveToLocalStorage),
|
Stream.runForEach(saveToLocalStorage),
|
||||||
@@ -43,7 +40,54 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
|||||||
Effect.ignore,
|
Effect.ignore,
|
||||||
))
|
))
|
||||||
|
|
||||||
return { ref, sizeRef } as const
|
const sizeSubscribable = Subscribable.make({
|
||||||
|
get: Effect.andThen(ref, Chunk.size),
|
||||||
|
get changes() { return Stream.map(ref.changes, Chunk.size) },
|
||||||
|
})
|
||||||
|
const getElementRef = (id: string) => SubscriptionSubRef.makeFromChunkFindFirst(ref, v => v.id === id)
|
||||||
|
const getIndexSubscribable = (id: string) => Subscribable.make({
|
||||||
|
get: Effect.flatMap(ref, Chunk.findFirstIndex(v => v.id === id)),
|
||||||
|
get changes() { return Stream.flatMap(ref.changes, Chunk.findFirstIndex(v => v.id === id)) },
|
||||||
|
})
|
||||||
|
|
||||||
|
const moveLeft = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.Do.pipe(
|
||||||
|
Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === id)),
|
||||||
|
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
||||||
|
Effect.bind("previous", ({ index }) => Chunk.get(todos, index - 1)),
|
||||||
|
Effect.andThen(({ todo, index, previous }) => index > 0
|
||||||
|
? todos.pipe(
|
||||||
|
Chunk.replace(index, previous),
|
||||||
|
Chunk.replace(index - 1, todo),
|
||||||
|
)
|
||||||
|
: todos
|
||||||
|
),
|
||||||
|
))
|
||||||
|
const moveRight = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.Do.pipe(
|
||||||
|
Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === id)),
|
||||||
|
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
||||||
|
Effect.bind("next", ({ index }) => Chunk.get(todos, index + 1)),
|
||||||
|
Effect.andThen(({ todo, index, next }) => index < Chunk.size(todos) - 1
|
||||||
|
? todos.pipe(
|
||||||
|
Chunk.replace(index, next),
|
||||||
|
Chunk.replace(index + 1, todo),
|
||||||
|
)
|
||||||
|
: todos
|
||||||
|
),
|
||||||
|
))
|
||||||
|
const remove = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.andThen(
|
||||||
|
Chunk.findFirstIndex(todos, v => v.id === id),
|
||||||
|
index => Chunk.remove(todos, index),
|
||||||
|
))
|
||||||
|
|
||||||
|
return {
|
||||||
|
ref,
|
||||||
|
sizeSubscribable,
|
||||||
|
getElementRef,
|
||||||
|
getIndexSubscribable,
|
||||||
|
moveLeft,
|
||||||
|
moveRight,
|
||||||
|
remove,
|
||||||
|
} as const
|
||||||
}),
|
}),
|
||||||
|
|
||||||
dependencies: [BrowserKeyValueStore.layerLocalStorage],
|
dependencies: [BrowserKeyValueStore.layerLocalStorage],
|
||||||
|
|||||||
@@ -22,15 +22,15 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
},
|
||||||
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{ "name": "@effect/language-service" }
|
||||||
"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