Add initial files
Some checks failed
Lint / lint (push) Failing after 10s

This commit is contained in:
Julien Valverdé
2026-01-23 02:46:22 +01:00
commit b85151e958
34 changed files with 3278 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
name: Lint
run-name: Lint
on: [push]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Clone repo
uses: actions/checkout@v6
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint TypeScript
run: bun lint:tsc
- name: Lint Biome
run: bun lint:biome

View File

@@ -0,0 +1,30 @@
# name: Publish
# run-name: Publish
# on:
# push:
# branches:
# - master
# jobs:
# publish:
# runs-on: ubuntu-latest
# steps:
# - name: Setup Bun
# uses: oven-sh/setup-bun@v2
# - name: Clone repo
# uses: actions/checkout@v6
# - name: Install dependencies
# run: bun install --frozen-lockfile
# - name: Lint TypeScript
# run: bun lint:tsc
# - name: Lint Biome
# run: bun lint:biome
# - name: Build
# run: bun run build
# - name: Publish effect-docker
# uses: JS-DevTools/npm-publish@v4
# with:
# package: packages/effect-docker
# access: public
# token: ${{ secrets.NPM_TOKEN }}
# registry: https://registry.npmjs.org

View File

@@ -0,0 +1,27 @@
name: Test build
run-name: Test build
on:
pull_request:
jobs:
test-build:
runs-on: ubuntu-latest
steps:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "24"
- name: Clone repo
uses: actions/checkout@v6
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint TypeScript
run: bun lint:tsc
- name: Lint Biome
run: bun lint:biome
- name: Build
run: bun run build
- name: Pack
run: bun pack

133
.gitignore vendored Normal file
View File

@@ -0,0 +1,133 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.turbo

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit"
}
}

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Thilawyn
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
# Effect FC Monorepo
[Effect-TS](https://effect.website/) integration for React 19.2+ that allows you to write function components using Effect generators.
This monorepo contains:
- [The `effect-fc` library](packages/effect-fc)
- [An example project](packages/example)

40
biome.json Normal file
View 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"
}
}
}
}

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "@effect-docker/monorepo",
"packageManager": "bun@1.3.6",
"private": true,
"workspaces": [
"./packages/*"
],
"scripts": {
"build": "turbo build",
"lint:tsc": "turbo lint:tsc",
"lint:biome": "turbo lint:biome",
"pack": "turbo pack",
"clean:cache": "turbo clean:cache",
"clean:dist": "turbo clean:dist",
"clean:modules": "turbo clean:modules && rm -rf node_modules"
},
"devDependencies": {
"@biomejs/biome": "^2.3.11",
"@effect/language-service": "^0.72.0",
"@types/bun": "^1.3.6",
"npm-check-updates": "^19.3.1",
"npm-sort": "^0.0.4",
"turbo": "^2.7.5",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,58 @@
# Effect FC
[Effect-TS](https://effect.website/) integration for React 19.2+ that allows you to write function components using Effect generators.
This library is in early development. While it is (almost) feature complete and mostly usable, expect bugs and quirks. Things are still being ironed out, so ideas and criticisms are more than welcome.
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
## Peer dependencies
- `effect` 3.19+
- `react` & `@types/react` 19.2+
## Known issues
- React Refresh doesn't work for Effect FC's yet. Page reload is required to view changes. Regular React components are unaffected.
## What writing components looks like
```typescript
export class Todos extends Component.make("Todos")(function*() {
const state = yield* TodosState
const [todos] = yield* useSubscribables(state.ref)
yield* useOnMount(() => Effect.andThen(
Console.log("Todos mounted"),
Effect.addFinalizer(() => Console.log("Todos unmounted")),
))
const TodoFC = yield* Todo
return (
<Container>
<Heading align="center">Todos</Heading>
<Flex direction="column" align="stretch" gap="2" mt="2">
<TodoFC _tag="new" />
{Chunk.map(todos, todo =>
<TodoFC key={todo.id} _tag="edit" id={todo.id} />
)}
</Flex>
</Container>
)
}) {}
const TodosStateLive = TodosState.Default("todos")
const Index = Component.make("Index")(function*() {
const context = yield* useContext(TodosStateLive)
const TodosFC = yield* Effect.provide(Todos, context)
return <TodosFC />
}).pipe(
Component.withRuntime(runtime.context)
)
export const Route = createFileRoute("/")({
component: Index
})
```

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
"root": false,
"extends": "//",
"files": {
"includes": ["./src/**"]
}
}

View File

@@ -0,0 +1,46 @@
{
"name": "effect-docker",
"description": "Effect-based client to talk to the Docker API",
"version": "0.1.0",
"type": "module",
"files": [
"./README.md",
"./dist"
],
"license": "MIT",
"repository": {
"url": "git+https://github.com/Thiladev/effect-docker.git"
},
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./*": [
{
"types": "./dist/*/index.d.ts",
"default": "./dist/*/index.js"
},
{
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
}
]
},
"scripts": {
"build": "tsc",
"lint:tsc": "tsc --noEmit",
"lint:biome": "biome lint",
"pack": "npm pack",
"clean:cache": "rm -rf .turbo tsconfig.tsbuildinfo",
"clean:dist": "rm -rf dist",
"clean:modules": "rm -rf node_modules"
},
"devDependencies": {
"@effect/platform-browser": "^0.74.0"
},
"peerDependencies": {
"effect": "^3.19.0"
}
}

View 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),
))

View File

@@ -0,0 +1,732 @@
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Tracer, type Utils } from "effect"
import * as React from "react"
import { Memoized } from "./index.js"
export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
export type TypeId = typeof TypeId
/**
* Interface representing an Effect-based React Component.
*
* This is both:
* - an Effect that produces a React function component
* - a constructor-like object with component metadata and options
*/
export interface Component<P extends {}, A extends React.ReactNode, E, R>
extends
Effect.Effect<(props: P) => A, never, Exclude<R, Scope.Scope>>,
Component.Options
{
new(_: never): Record<string, never>
readonly [TypeId]: TypeId
readonly "~Props": P
readonly "~Success": A
readonly "~Error": E
readonly "~Context": R
readonly body: (props: P) => Effect.Effect<A, E, R>
/** @internal */
makeFunctionComponent(
runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>
): (props: P) => A
}
export declare namespace Component {
export type Props<T extends Component<any, any, any, any>> = [T] extends [Component<infer P, infer _A, infer _E, infer _R>] ? P : never
export type Success<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer A, infer _E, infer _R>] ? A : never
export type Error<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer _A, infer E, infer _R>] ? E : never
export type Context<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer _A, infer _E, infer R>] ? R : never
export type AsComponent<T extends Component<any, any, any, any>> = Component<Props<T>, Success<T>, Error<T>, Context<T>>
/**
* Options that can be set on the component
*/
export interface Options {
/** Custom displayName for React DevTools and debugging. */
readonly displayName?: string
/**
* Strategy used when executing finalizers on unmount/scope close.
* @default ExecutionStrategy.sequential
*/
readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy
/**
* Debounce time before executing finalizers after component unmount.
* Helps avoid unnecessary work during fast remount/remount cycles.
* @default "100 millis"
*/
readonly finalizerExecutionDebounce: Duration.DurationInput
}
}
const ComponentProto = Object.freeze({
...Effectable.CommitPrototype,
[TypeId]: TypeId,
commit: Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode, E, R>(
this: Component<P, A, E, R>
) {
// biome-ignore lint/style/noNonNullAssertion: React ref initialization
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
return yield* React.useState(() => Runtime.runSync(runtimeRef.current)(Effect.cachedFunction(
(_services: readonly any[]) => Effect.sync(() => {
const f: React.FC<P> = this.makeFunctionComponent(runtimeRef)
f.displayName = this.displayName ?? "Anonymous"
return Memoized.isMemoized(this)
? React.memo(f, this.propsAreEqual)
: f
}),
Equivalence.array(Equivalence.strict()),
)))[0](Array.from(
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
))
}),
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
this: Component<P, A, E, R>,
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
) {
return (props: P) => Runtime.runSync(runtimeRef.current)(
Effect.andThen(
useScope([], this),
scope => Effect.provideService(this.body(props), Scope.Scope, scope),
)
)
},
} as const)
const defaultOptions: Component.Options = {
finalizerExecutionStrategy: ExecutionStrategy.sequential,
finalizerExecutionDebounce: "100 millis",
}
const nonReactiveTags = [Tracer.ParentSpan] as const
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId)
export declare namespace make {
export type Gen = {
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A extends React.ReactNode, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>
): Component<
P, A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<B>>, Effect.Effect.Error<B>, Effect.Effect.Context<B>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<C>>, Effect.Effect.Error<C>, Effect.Effect.Context<C>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<D>>, Effect.Effect.Error<D>, Effect.Effect.Context<D>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<E>>, Effect.Effect.Error<E>, Effect.Effect.Context<E>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<F>>, Effect.Effect.Error<F>, Effect.Effect.Context<F>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<G>>, Effect.Effect.Error<G>, Effect.Effect.Context<G>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => H,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<H>>, Effect.Effect.Error<H>, Effect.Effect.Context<H>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => H,
h: (_: H, props: NoInfer<P>) => I,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<I>>, Effect.Effect.Error<I>, Effect.Effect.Context<I>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I, J extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => H,
h: (_: H, props: NoInfer<P>) => I,
i: (_: I, props: NoInfer<P>) => J,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<J>>, Effect.Effect.Error<J>, Effect.Effect.Context<J>>
}
export type NonGen = {
<Eff extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Eff
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => H,
h: (_: H, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, I, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => H,
h: (_: H, props: NoInfer<P>) => I,
i: (_: I, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
}
}
/**
* Creates an Effect-FC Component following the same overloads and pipeline style as `Effect.fn`.
*
* This is the **recommended** way to define components. It supports:
* - Generator syntax (yield* style) — most ergonomic and readable
* - Direct Effect return (non-generator)
* - Chained transformation functions (like Effect.fn pipelines)
* - Optional tracing span with automatic `displayName`
*
* When you provide a `spanName` as the first argument, two things happen automatically:
* 1. A tracing span is created with that name (unless using `makeUntraced`)
* 2. The resulting React component gets `displayName = spanName`
*/
export const make: (
& make.Gen
& make.NonGen
& ((
spanName: string,
spanOptions?: Tracer.SpanOptions,
) => make.Gen & make.NonGen)
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => {
if (typeof spanNameOrBody !== "string") {
return Object.setPrototypeOf(
Object.assign(function() {}, defaultOptions, {
body: Effect.fn(spanNameOrBody as any, ...pipeables),
}),
ComponentProto,
)
}
else {
const spanOptions = pipeables[0]
return (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
Object.assign(function() {}, defaultOptions, {
body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []),
displayName: spanNameOrBody,
}),
ComponentProto,
)
}
}
/**
* Same as `make`, but creates an **untraced** version — no automatic tracing span is created.
*
* Follows the exact same API shape as `Effect.fnUntraced`.
* Useful for:
* - Components where you want full manual control over tracing
* - Avoiding span noise in deeply nested UI
*
* When a string is provided as first argument, it is **only** used as the React component's `displayName`
* (no tracing span is created).
*/
export const makeUntraced: (
& make.Gen
& make.NonGen
& ((name: string) => make.Gen & make.NonGen)
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => (
typeof spanNameOrBody !== "string"
? Object.setPrototypeOf(
Object.assign(function() {}, defaultOptions, {
body: Effect.fnUntraced(spanNameOrBody as any, ...pipeables as []),
}),
ComponentProto,
)
: (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
Object.assign(function() {}, defaultOptions, {
body: Effect.fnUntraced(body, ...pipeables as []),
displayName: spanNameOrBody,
}),
ComponentProto,
)
)
/**
* Creates a new component with modified options while preserving original behavior.
*/
export const withOptions: {
<T extends Component<any, any, any, any>>(
options: Partial<Component.Options>
): (self: T) => T
<T extends Component<any, any, any, any>>(
self: T,
options: Partial<Component.Options>,
): T
} = Function.dual(2, <T extends Component<any, any, any, any>>(
self: T,
options: Partial<Component.Options>,
): T => Object.setPrototypeOf(
Object.assign(function() {}, self, options),
Object.getPrototypeOf(self),
))
/**
* Wraps an Effect-FC `Component` and turns it into a regular React function component
* that serves as an **entrypoint** into an Effect-FC component hierarchy.
*
* This is the recommended way to connect Effect-FC components to the rest of your React app,
* especially when using routers (TanStack Router, React Router, etc.), lazy-loaded routes,
* or any place where a standard React component is expected.
*
* The runtime is obtained from the provided React Context, allowing you to:
* - Provide dependencies once at a high level
* - Use the same runtime across an entire route tree or feature
*
* @example Using TanStack Router
* ```tsx
* // Main
* export const runtime = ReactRuntime.make(Layer.empty)
* function App() {
* return (
* <ReactRuntime.Provider runtime={runtime}>
* <RouterProvider router={router} />
* </ReactRuntime.Provider>
* )
* }
*
* // Route
* export const Route = createFileRoute("/")({
* component: Component.withRuntime(HomePage, runtime.context)
* })
* ```
*
* @param self - The Effect-FC Component you want to render as a regular React component.
* @param context - React Context that holds the Runtime to use for this component tree. See the `ReactRuntime` module to create one.
*/
export const withRuntime: {
<P extends {}, A extends React.ReactNode, E, R>(
context: React.Context<Runtime.Runtime<R>>,
): (self: Component<P, A, E, Scope.Scope | NoInfer<R>>) => (props: P) => A
<P extends {}, A extends React.ReactNode, E, R>(
self: Component<P, A, E, Scope.Scope | NoInfer<R>>,
context: React.Context<Runtime.Runtime<R>>,
): (props: P) => A
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R>(
self: Component<P, A, E, R>,
context: React.Context<Runtime.Runtime<R>>,
) => function WithRuntime(props: P) {
return React.createElement(
Runtime.runSync(React.useContext(context))(self),
props,
)
})
/**
* Service that keeps track of scopes associated with React components
* (used internally by the `useScope` hook).
*/
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
}
}
/**
* Hook that creates and manages a `Scope` for the current component instance.
*
* Automatically closes the scope whenever `deps` changes or the component unmounts.
*
* @param deps - dependency array like in `React.useEffect`
* @param options - finalizer execution control
*/
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 { key, scope } = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.Do.pipe(
Effect.bind("scopeMapRef", () => Effect.map(
ScopeMap as unknown as Effect.Effect<ScopeMap>,
scopeMap => scopeMap.ref,
)),
Effect.let("key", () => ({})),
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)),
Effect.tap(({ scopeMapRef, key, scope }) =>
Ref.update(scopeMapRef, HashMap.set(key, {
scope,
closeFiber: Option.none(),
}))
),
// 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 as unknown as Effect.Effect<ScopeMap>).pipe(
Effect.map(scopeMap => scopeMap.ref),
Effect.tap(ref => ref.pipe(
Effect.andThen(HashMap.get(key)),
Effect.andThen(entry => Option.match(entry.closeFiber, {
onSome: Fiber.interruptFork,
onNone: () => Effect.void,
})),
)),
Effect.map(ref =>
() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
Effect.sleep(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce).pipe(
Effect.andThen(Scope.close(scope, Exit.void)),
Effect.onExit(() => Ref.update(ref, HashMap.remove(key))),
Effect.forkDaemon,
),
fiber => Ref.update(ref, HashMap.set(key, {
scope,
closeFiber: Option.some(fiber),
})),
))
),
)), [key])
return scope
})
/**
* Runs an effect and returns its result only once on component mount.
*/
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 {}
}
/**
* Runs an effect and returns its result whenever dependencies change.
*
* Provides its own `Scope` which closes whenever `deps` changes or the component unmounts.
*/
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
}
}
/**
* Like `React.useEffect` but accepts an effect.
*
* Cleanup logic is handled through the `Scope` API rather than using imperative cleanup.
*/
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 {}
}
/**
* Like `React.useReactLayoutEffect` but accepts an effect.
*
* Cleanup logic is handled through the `Scope` API rather than using imperative cleanup.
*/
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)
})
/**
* Get a synchronous run function for the current runtime context.
*/
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)
/**
* Get a Promise-based run function for the current runtime context.
*/
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))
/**
* Turns a function returning an effect into a memoized synchronous function.
*/
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)
})
/**
* Turns a function returning an effect into a memoized Promise-based asynchronous function.
*/
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 {}
}
/**
* Hook that constructs a layer and returns the created context.
*
* The layer gets reconstructed everytime `layer` changes, so make sure its value is stable.
*
* Building a layer containing asynchronous effects require the component calling this hook to be made async using `Async.async`.
*/
export const useContext = <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>,
options?: useContext.Options,
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>> => 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)

View 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,
}),
)

View 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 }
})

View File

@@ -0,0 +1,51 @@
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
import { type Equivalence, Function, Predicate } from "effect"
import type * as Component from "./Component.js"
export const TypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized")
export type TypeId = typeof TypeId
export interface Memoized<P> extends Memoized.Options<P> {
readonly [TypeId]: TypeId
}
export namespace Memoized {
export interface Options<P> {
readonly propsAreEqual?: Equivalence.Equivalence<P>
}
}
const MemoizedProto = Object.freeze({
[TypeId]: TypeId
} as const)
export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasProperty(u, TypeId)
export const memoized = <T extends Component.Component<any, any, any, any>>(
self: T
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
Object.assign(function() {}, self),
Object.freeze(Object.setPrototypeOf(
Object.assign({}, MemoizedProto),
Object.getPrototypeOf(self),
)),
)
export const withOptions: {
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
options: Partial<Memoized.Options<Component.Component.Props<T>>>
): (self: T) => T
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
self: T,
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
): T
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Memoized<any>>(
self: T,
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
): T => Object.setPrototypeOf(
Object.assign(function() {}, self, options),
Object.getPrototypeOf(self),
))

View 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>>()),
)
})

View File

@@ -0,0 +1,98 @@
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]
export type Paths<T, D extends number = 5, Seen = never> = readonly [] | (
D extends never ? readonly [] :
T extends Seen ? readonly [] :
T extends readonly any[] ? {
[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
)
export type ValueFromPath<T, P extends readonly any[]> = P extends readonly [infer Head, ...infer Tail]
? Head extends keyof T
? ValueFromPath<T[Head], Tail>
: T extends readonly any[]
? Head extends number
? ValueFromPath<T[number], Tail>
: never
: never
: T
export const equivalence: Equivalence.Equivalence<PropertyPath> = Equivalence.array(Equivalence.strict())
export const unsafeGet: {
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
path.reduce((acc: any, key: any) => acc?.[key], self)
)
export const get: {
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
path.reduce(
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
? Predicate.hasProperty(acc.value, key)
? Option.some(acc.value[key])
: Option.none()
: acc,
Option.some(self),
)
)
export const immutableSet: {
<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>
} = 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 PropertyPath)
if (Option.isNone(key))
return Option.some(value as T)
if (!Predicate.hasProperty(self, key.value))
return Option.none()
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as PropertyPath)), value)
if (Option.isNone(child))
return child
if (Array.isArray(self))
return typeof key.value === "number"
? Option.some([
...self.slice(0, key.value),
child.value,
...self.slice(key.value + 1),
] as T)
: Option.none()
if (typeof self === "object")
return Option.some(
Object.assign(
Object.create(Object.getPrototypeOf(self)),
{ ...self, [key.value]: child.value },
)
)
return Option.none()
})

View 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"

View File

@@ -0,0 +1,316 @@
import { type Cause, type Context, type Duration, Effect, Equal, Fiber, 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> {
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),
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.setCacheEntry(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 Effect.andThen(
Effect.all([
Effect.succeed(this.makeCacheKey(key)),
QueryClient.QueryClient,
]),
([key, client]) => client.getCacheEntry(key),
)
}
setCacheEntry(
key: K,
result: Result.Success<A>,
): Effect.Effect<QueryClient.QueryClientCacheEntry, never, QueryClient.QueryClient> {
return Effect.andThen(
Effect.all([
Effect.succeed(this.makeCacheKey(key)),
QueryClient.QueryClient,
]),
([key, client]) => client.setCacheEntry(key, result, this.staleTime),
)
}
get invalidateCache(): Effect.Effect<void> {
return QueryClient.QueryClient.pipe(
Effect.andThen(client => client.invalidateCacheEntries(this.f as (key: Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>)),
Effect.provide(this.context),
)
}
invalidateCacheEntry(key: K): Effect.Effect<void> {
return Effect.all([
Effect.succeed(this.makeCacheKey(key)),
QueryClient.QueryClient,
]).pipe(
Effect.andThen(([key, client]) => client.invalidateCacheEntry(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),
)

View File

@@ -0,0 +1,173 @@
import { DateTime, Duration, Effect, Equal, Equivalence, Hash, HashMap, type Option, Pipeable, Predicate, Schedule, type Scope, type Subscribable, 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/QueryClientService")
export type QueryClientServiceTypeId = typeof QueryClientServiceTypeId
export interface QueryClientService extends Pipeable.Pipeable {
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId
readonly cache: Subscribable.Subscribable<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>
readonly cacheGcTime: Duration.DurationInput
readonly defaultStaleTime: Duration.DurationInput
readonly defaultRefreshOnWindowFocus: boolean
readonly run: Effect.Effect<void>
getCacheEntry(key: QueryClientCacheKey): Effect.Effect<Option.Option<QueryClientCacheEntry>>
setCacheEntry(
key: QueryClientCacheKey,
result: Result.Success<unknown>,
staleTime: Duration.DurationInput,
): Effect.Effect<QueryClientCacheEntry>
invalidateCacheEntries(f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>): Effect.Effect<void>
invalidateCacheEntry(key: QueryClientCacheKey): Effect.Effect<void>
}
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 cacheGcTime: Duration.DurationInput,
readonly defaultStaleTime: Duration.DurationInput,
readonly defaultRefreshOnWindowFocus: boolean,
readonly runSemaphore: Effect.Semaphore,
) {
super()
}
get run(): Effect.Effect<void> {
return this.runSemaphore.withPermits(1)(Effect.repeat(
Effect.andThen(
DateTime.now,
now => SubscriptionRef.update(this.cache, HashMap.filter(entry =>
Duration.lessThan(
DateTime.distanceDuration(entry.lastAccessedAt, now),
Duration.sum(entry.staleTime, this.cacheGcTime),
)
)),
),
Schedule.spaced("30 second"),
))
}
getCacheEntry(key: QueryClientCacheKey): Effect.Effect<Option.Option<QueryClientCacheEntry>> {
return Effect.all([
Effect.andThen(this.cache, HashMap.get(key)),
DateTime.now,
]).pipe(
Effect.map(([entry, now]) => new QueryClientCacheEntry(entry.result, entry.staleTime, entry.createdAt, now)),
Effect.tap(entry => SubscriptionRef.update(this.cache, HashMap.set(key, entry))),
Effect.option,
)
}
setCacheEntry(
key: QueryClientCacheKey,
result: Result.Success<unknown>,
staleTime: Duration.DurationInput,
): Effect.Effect<QueryClientCacheEntry> {
return DateTime.now.pipe(
Effect.map(now => new QueryClientCacheEntry(result, staleTime, now, now)),
Effect.tap(entry => SubscriptionRef.update(this.cache, HashMap.set(key, entry))),
)
}
invalidateCacheEntries(f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>): Effect.Effect<void> {
return SubscriptionRef.update(this.cache, HashMap.filter((_, key) => !Equivalence.strict()(key.f, f)))
}
invalidateCacheEntry(key: QueryClientCacheKey): Effect.Effect<void> {
return SubscriptionRef.update(this.cache, HashMap.remove(key))
}
}
export const isQueryClientService = (u: unknown): u is QueryClientService => Predicate.hasProperty(u, QueryClientServiceTypeId)
export declare namespace make {
export interface Options {
readonly cacheGcTime?: 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.cacheGcTime ?? "5 minutes",
options.defaultStaleTime ?? "0 minutes",
options.defaultRefreshOnWindowFocus ?? true,
yield* Effect.makeSemaphore(1),
)
})
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(client.run),
)
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 staleTime: Duration.DurationInput,
readonly createdAt: DateTime.DateTime,
readonly lastAccessedAt: DateTime.DateTime,
) {
super()
}
}
export const isQueryClientCacheEntry = (u: unknown): u is QueryClientCacheEntry => Predicate.hasProperty(u, QueryClientCacheEntryTypeId)
export const isQueryClientCacheEntryStale = (
self: QueryClientCacheEntry
): Effect.Effect<boolean> => Effect.andThen(
DateTime.now,
now => Duration.greaterThanOrEqualTo(DateTime.distanceDuration(self.createdAt, now), self.staleTime),
)

View 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)
}

View 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

View File

@@ -0,0 +1,12 @@
import { Function } from "effect"
import type * as React from "react"
export const value: {
<S>(prevState: S): (self: React.SetStateAction<S>) => S
<S>(self: React.SetStateAction<S>, prevState: S): S
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
typeof self === "function"
? (self as (prevState: S) => S)(prevState)
: self
)

View 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"

View 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"

View 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"

View 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

View File

@@ -0,0 +1,17 @@
export * as Async from "./Async.js"
export * as Component from "./Component.js"
export * as ErrorObserver from "./ErrorObserver.js"
export * as Form from "./Form.js"
export * as Memoized from "./Memoized.js"
export * as Mutation from "./Mutation.js"
export * as PropertyPath from "./PropertyPath.js"
export * as PubSub from "./PubSub.js"
export * as Query from "./Query.js"
export * as QueryClient from "./QueryClient.js"
export * as ReactRuntime from "./ReactRuntime.js"
export * as Result from "./Result.js"
export * as SetStateAction from "./SetStateAction.js"
export * as Stream from "./Stream.js"
export * as Subscribable from "./Subscribable.js"
export * as SubscriptionRef from "./SubscriptionRef.js"
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"

View File

@@ -0,0 +1,3 @@
export type ExcludeKeys<T, K extends PropertyKey> = K extends keyof T ? (
{ [P in K]?: never } & Omit<T, K>
) : T

View File

@@ -0,0 +1,38 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "NodeNext",
"moduleDetection": "force",
"jsx": "react-jsx",
// "allowJs": true,
// Bundler mode
"moduleResolution": "NodeNext",
// "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
// "noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
// Build
"outDir": "./dist",
"declaration": true,
"sourceMap": true,
"plugins": [
{ "name": "@effect/language-service" }
]
},
"include": ["./src"]
}

19
renovate.json Normal file
View 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"
}
]
}

30
turbo.json Normal file
View File

@@ -0,0 +1,30 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"lint:tsc": {
"cache": false
},
"lint:biome": {
"cache": false
},
"build": {
"dependsOn": ["^build"],
"inputs": ["./src/**"],
"outputs": ["./dist/**"]
},
"pack": {
"dependsOn": ["^pack"],
"cache": false
},
"clean:cache": {
"cache": false
},
"clean:dist": {
"cache": false
},
"clean:modules": {
"cache": false
}
}
}