Compare commits
1 Commits
result
...
28bd0c2473
| Author | SHA1 | Date | |
|---|---|---|---|
| 28bd0c2473 |
13
bun.lock
13
bun.lock
@@ -6,7 +6,7 @@
|
|||||||
"name": "@effect-fc/monorepo",
|
"name": "@effect-fc/monorepo",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.11",
|
"@biomejs/biome": "^2.3.11",
|
||||||
"@effect/language-service": "^0.75.0",
|
"@effect/language-service": "^0.73.0",
|
||||||
"@types/bun": "^1.3.6",
|
"@types/bun": "^1.3.6",
|
||||||
"npm-check-updates": "^19.3.1",
|
"npm-check-updates": "^19.3.1",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
"@effect/platform-browser": "^0.74.0",
|
"@effect/platform-browser": "^0.74.0",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@effect-atom/atom": "^0.5.0",
|
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"effect": "^3.19.0",
|
"effect": "^3.19.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
@@ -115,20 +114,14 @@
|
|||||||
|
|
||||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
|
||||||
|
|
||||||
"@effect-atom/atom": ["@effect-atom/atom@0.5.3", "", { "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "effect": "^3.19.15" } }, "sha512-TRZv/i+YT3TtnN0oFORJqXdxSs1fc7lrJlH+1xZvDFyjC9hgoVnrcKbeZsDFmr6r0wYRqVo7U3IftxiQNjpNZA=="],
|
|
||||||
|
|
||||||
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
|
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
|
||||||
|
|
||||||
"@effect/experimental": ["@effect/experimental@0.58.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-IEP9sapjF6rFy5TkoqDPc86st/fnqUfjT7Xa3pWJrFGr1hzaMXHo+mWsYOZS9LAOVKnpHuVziDK97EP5qsCHVA=="],
|
"@effect/language-service": ["@effect/language-service@0.73.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-/SGoq50VDm/XwQI6d0cReYcLjwfdqwZv7uEJp92+ssx5vLCsm+QvHf4Ul6l6PYVzorsgAp/b6fhAwI2VSkqcJQ=="],
|
||||||
|
|
||||||
"@effect/language-service": ["@effect/language-service@0.75.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DxRN8+b5IEQ/x8hukpV39kJe7fs6er7LDWp1PvKjOxPkN5UJ8VJovUVzoHtOX6XWzMmJBRCN9/j0s8jujXTduw=="],
|
|
||||||
|
|
||||||
"@effect/platform": ["@effect/platform@0.94.2", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.15" } }, "sha512-85vdwpnK4oH/rJ3EuX/Gi2Hkt+K4HvXWr9bxCuqvty9hxyEcRxkJcqTesYrcVoQB6aULb1Za2B0MKoTbvffB3Q=="],
|
"@effect/platform": ["@effect/platform@0.94.2", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.15" } }, "sha512-85vdwpnK4oH/rJ3EuX/Gi2Hkt+K4HvXWr9bxCuqvty9hxyEcRxkJcqTesYrcVoQB6aULb1Za2B0MKoTbvffB3Q=="],
|
||||||
|
|
||||||
"@effect/platform-browser": ["@effect/platform-browser@0.74.0", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13" } }, "sha512-PAgkg5L5cASQpScA0SZTSy543MVA4A9kmpVCjo2fCINLRpTeuCFAOQHgPmw8dKHnYS0yGs2TYn7AlrhhqQ5o3g=="],
|
"@effect/platform-browser": ["@effect/platform-browser@0.74.0", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13" } }, "sha512-PAgkg5L5cASQpScA0SZTSy543MVA4A9kmpVCjo2fCINLRpTeuCFAOQHgPmw8dKHnYS0yGs2TYn7AlrhhqQ5o3g=="],
|
||||||
|
|
||||||
"@effect/rpc": ["@effect/rpc@0.73.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.94.5", "effect": "^3.19.18" } }, "sha512-td7LHDgBOYKg+VgGWEelD8rSAmvjXz7am17vfxZROX5qIYuvH7drL/z4p5xQFadhHZ7DYdlFpqdO9ggc77OCIw=="],
|
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||||
@@ -641,8 +634,6 @@
|
|||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
|
|
||||||
|
|
||||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.11",
|
"@biomejs/biome": "^2.3.11",
|
||||||
"@effect/language-service": "^0.75.0",
|
"@effect/language-service": "^0.73.0",
|
||||||
"@types/bun": "^1.3.6",
|
"@types/bun": "^1.3.6",
|
||||||
"npm-check-updates": "^19.3.1",
|
"npm-check-updates": "^19.3.1",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
"@effect/platform-browser": "^0.74.0"
|
"@effect/platform-browser": "^0.74.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@effect-atom/atom": "^0.5.0",
|
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"effect": "^3.19.0",
|
"effect": "^3.19.0",
|
||||||
"react": "^19.2.0"
|
"react": "^19.2.0"
|
||||||
|
|||||||
@@ -7,27 +7,29 @@ import * as Component from "./Component.js"
|
|||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Async/Async")
|
export const TypeId: unique symbol = Symbol.for("@effect-fc/Async/Async")
|
||||||
export type TypeId = typeof TypeId
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
export interface Async extends AsyncOptions {
|
export interface Async extends Async.Options {
|
||||||
readonly [TypeId]: TypeId
|
readonly [TypeId]: TypeId
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsyncOptions {
|
export namespace Async {
|
||||||
readonly defaultFallback?: React.ReactNode
|
export interface Options {
|
||||||
|
readonly defaultFallback?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = Omit<React.SuspenseProps, "children">
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AsyncProps = Omit<React.SuspenseProps, "children">
|
|
||||||
|
|
||||||
|
const AsyncProto = Object.freeze({
|
||||||
export const AsyncPrototype = Object.freeze({
|
|
||||||
[TypeId]: TypeId,
|
[TypeId]: TypeId,
|
||||||
|
|
||||||
asFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
||||||
this: Component.Component<P, A, E, R> & Async,
|
this: Component.Component<P, A, E, R> & Async,
|
||||||
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||||
) {
|
) {
|
||||||
const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
||||||
|
|
||||||
return ({ fallback, name, ...props }: AsyncProps) => {
|
return ({ fallback, name, ...props }: Async.Props) => {
|
||||||
const promise = Runtime.runPromise(runtimeRef.current)(
|
const promise = Runtime.runPromise(runtimeRef.current)(
|
||||||
Effect.andThen(
|
Effect.andThen(
|
||||||
Component.useScope([], this),
|
Component.useScope([], this),
|
||||||
@@ -52,7 +54,7 @@ export const async = <T extends Component.Component<any, any, any, any>>(
|
|||||||
): (
|
): (
|
||||||
& Omit<T, keyof Component.Component.AsComponent<T>>
|
& Omit<T, keyof Component.Component.AsComponent<T>>
|
||||||
& Component.Component<
|
& Component.Component<
|
||||||
Component.Component.Props<T> & AsyncProps,
|
Component.Component.Props<T> & Async.Props,
|
||||||
Component.Component.Success<T>,
|
Component.Component.Success<T>,
|
||||||
Component.Component.Error<T>,
|
Component.Component.Error<T>,
|
||||||
Component.Component.Context<T>
|
Component.Component.Context<T>
|
||||||
@@ -61,22 +63,22 @@ export const async = <T extends Component.Component<any, any, any, any>>(
|
|||||||
) => Object.setPrototypeOf(
|
) => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, self),
|
Object.assign(function() {}, self),
|
||||||
Object.freeze(Object.setPrototypeOf(
|
Object.freeze(Object.setPrototypeOf(
|
||||||
Object.assign({}, AsyncPrototype),
|
Object.assign({}, AsyncProto),
|
||||||
Object.getPrototypeOf(self),
|
Object.getPrototypeOf(self),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const withOptions: {
|
export const withOptions: {
|
||||||
<T extends Component.Component<any, any, any, any> & Async>(
|
<T extends Component.Component<any, any, any, any> & Async>(
|
||||||
options: Partial<AsyncOptions>
|
options: Partial<Async.Options>
|
||||||
): (self: T) => T
|
): (self: T) => T
|
||||||
<T extends Component.Component<any, any, any, any> & Async>(
|
<T extends Component.Component<any, any, any, any> & Async>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<AsyncOptions>,
|
options: Partial<Async.Options>,
|
||||||
): T
|
): T
|
||||||
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Async>(
|
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Async>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<AsyncOptions>,
|
options: Partial<Async.Options>,
|
||||||
): T => Object.setPrototypeOf(
|
): T => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, self, options),
|
Object.assign(function() {}, self, options),
|
||||||
Object.getPrototypeOf(self),
|
Object.getPrototypeOf(self),
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
|
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
|
||||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
import { Context, type Duration, Effect, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, identity, Layer, ManagedRuntime, Option, Pipeable, Predicate, Ref, Runtime, Scope, Tracer, type Utils } from "effect"
|
import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Tracer, type Utils } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { Memoized } from "./index.js"
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
||||||
export type TypeId = typeof TypeId
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an Effect-based React Component that integrates the Effect system with React.
|
* 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>
|
export interface Component<P extends {}, A extends React.ReactNode, E, R>
|
||||||
extends ComponentPrototype<P, A, R>, ComponentOptions {
|
extends
|
||||||
|
Effect.Effect<(props: P) => A, never, Exclude<R, Scope.Scope>>,
|
||||||
|
Component.Options
|
||||||
|
{
|
||||||
new(_: never): Record<string, never>
|
new(_: never): Record<string, never>
|
||||||
readonly [TypeId]: TypeId
|
readonly [TypeId]: TypeId
|
||||||
readonly "~Props": P
|
readonly "~Props": P
|
||||||
@@ -20,6 +28,11 @@ extends ComponentPrototype<P, A, R>, ComponentOptions {
|
|||||||
readonly "~Context": R
|
readonly "~Context": R
|
||||||
|
|
||||||
readonly body: (props: P) => Effect.Effect<A, E, 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 declare namespace Component {
|
||||||
@@ -29,29 +42,56 @@ export declare namespace Component {
|
|||||||
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 Context<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer _A, infer _E, infer R>] ? R : never
|
||||||
|
|
||||||
export type AsComponent<T extends Component<any, any, any, any>> = Component<Props<T>, Success<T>, Error<T>, Context<T>>
|
export 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ComponentPrototype<P extends {}, A extends React.ReactNode, R>
|
const ComponentProto = Object.freeze({
|
||||||
extends Pipeable.Pipeable {
|
...Effectable.CommitPrototype,
|
||||||
readonly [TypeId]: TypeId
|
|
||||||
readonly use: Effect.Effect<(props: P) => A, never, Exclude<R, Scope.Scope>>
|
|
||||||
|
|
||||||
asFunctionComponent(
|
|
||||||
runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>
|
|
||||||
): (props: P) => A
|
|
||||||
|
|
||||||
setFunctionComponentName(f: React.FC<P>): void
|
|
||||||
transformFunctionComponent(f: React.FC<P>): React.FC<P>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ComponentPrototype: ComponentPrototype<any, any, any> = Object.freeze({
|
|
||||||
[TypeId]: TypeId,
|
[TypeId]: TypeId,
|
||||||
...Pipeable.Prototype,
|
|
||||||
|
|
||||||
get use() { return use(this) },
|
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>>()
|
||||||
|
|
||||||
asFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
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>,
|
this: Component<P, A, E, R>,
|
||||||
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||||
) {
|
) {
|
||||||
@@ -62,69 +102,14 @@ export const ComponentPrototype: ComponentPrototype<any, any, any> = Object.free
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
setFunctionComponentName<P extends {}, A extends React.ReactNode, E, R>(
|
|
||||||
this: Component<P, A, E, R>,
|
|
||||||
f: React.FC<P>,
|
|
||||||
) {
|
|
||||||
f.displayName = this.displayName ?? "Anonymous"
|
|
||||||
},
|
|
||||||
|
|
||||||
transformFunctionComponent: identity,
|
|
||||||
} as const)
|
} as const)
|
||||||
|
|
||||||
const use = Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode, E, R>(
|
const defaultOptions: Component.Options = {
|
||||||
self: 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> = self.asFunctionComponent(runtimeRef)
|
|
||||||
self.setFunctionComponentName(f)
|
|
||||||
return self.transformFunctionComponent(f)
|
|
||||||
}),
|
|
||||||
Equivalence.array(Equivalence.strict()),
|
|
||||||
)))[0](Array.from(
|
|
||||||
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
|
||||||
))
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export interface ComponentOptions {
|
|
||||||
/**
|
|
||||||
* Custom display name for the component in React DevTools and debugging utilities.
|
|
||||||
* Improves developer experience by providing meaningful component identification.
|
|
||||||
*/
|
|
||||||
readonly displayName?: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies the execution strategy for finalizers when the component unmounts or its scope closes.
|
|
||||||
* Determines whether finalizers execute sequentially or in parallel.
|
|
||||||
*
|
|
||||||
* @default ExecutionStrategy.sequential
|
|
||||||
*/
|
|
||||||
readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debounce duration before executing finalizers after component unmount.
|
|
||||||
* Prevents unnecessary cleanup work during rapid remount/unmount cycles,
|
|
||||||
* which is common in development and certain UI patterns.
|
|
||||||
*
|
|
||||||
* @default "100 millis"
|
|
||||||
*/
|
|
||||||
readonly finalizerExecutionDebounce: Duration.DurationInput
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultOptions: ComponentOptions = {
|
|
||||||
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
||||||
finalizerExecutionDebounce: "100 millis",
|
finalizerExecutionDebounce: "100 millis",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||||
export const nonReactiveTags = [Tracer.ParentSpan] as const
|
|
||||||
|
|
||||||
|
|
||||||
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||||
@@ -355,51 +340,17 @@ export declare namespace make {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Effect-FC Component using the same overloads and pipeline composition style as `Effect.fn`.
|
* Creates an Effect-FC Component following the same overloads and pipeline style as `Effect.fn`.
|
||||||
*
|
*
|
||||||
* This is the **recommended** approach for defining Effect-FC components. It provides comprehensive
|
* This is the **recommended** way to define components. It supports:
|
||||||
* support for multiple component definition patterns:
|
* - 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`
|
||||||
*
|
*
|
||||||
* - **Generator syntax** (yield* style): Most ergonomic and readable approach for sequential operations
|
* When you provide a `spanName` as the first argument, two things happen automatically:
|
||||||
* - **Direct Effect return**: For simple components that return an Effect directly
|
* 1. A tracing span is created with that name (unless using `makeUntraced`)
|
||||||
* - **Chained transformation functions**: Enables Effect.fn-style pipelines for composable transformations
|
* 2. The resulting React component gets `displayName = spanName`
|
||||||
* - **Automatic tracing**: Optional tracing span creation with automatic `displayName` assignment
|
|
||||||
*
|
|
||||||
* When a `spanName` string is provided, the following occurs automatically:
|
|
||||||
* 1. A distributed tracing span is created with the specified name
|
|
||||||
* 2. The resulting React component receives `displayName = spanName` for DevTools visibility
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.make("MyComponent")(function* (props: { count: number }) {
|
|
||||||
* const value = yield* someEffect
|
|
||||||
* return <div>{value}</div>
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example As an opaque type using class syntax
|
|
||||||
* ```tsx
|
|
||||||
* class MyComponent extends Component.make("MyComponent")(function* (props: { count: number }) {
|
|
||||||
* const value = yield* someEffect
|
|
||||||
* return <div>{value}</div>
|
|
||||||
* }) {}
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example Without name
|
|
||||||
* ```tsx
|
|
||||||
* class MyComponent extends Component.make(function* (props: { count: number }) {
|
|
||||||
* const value = yield* someEffect
|
|
||||||
* return <div>{value}</div>
|
|
||||||
* }) {}
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example Using pipeline
|
|
||||||
* ```tsx
|
|
||||||
* class MyComponent extends Component.make("MyComponent")(
|
|
||||||
* (props: { count: number }) => someEffect,
|
|
||||||
* Effect.map(value => <div>{value}</div>),
|
|
||||||
* ) {}
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const make: (
|
export const make: (
|
||||||
& make.Gen
|
& make.Gen
|
||||||
@@ -414,7 +365,7 @@ export const make: (
|
|||||||
Object.assign(function() {}, defaultOptions, {
|
Object.assign(function() {}, defaultOptions, {
|
||||||
body: Effect.fn(spanNameOrBody as any, ...pipeables),
|
body: Effect.fn(spanNameOrBody as any, ...pipeables),
|
||||||
}),
|
}),
|
||||||
ComponentPrototype,
|
ComponentProto,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -424,56 +375,21 @@ export const make: (
|
|||||||
body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []),
|
body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []),
|
||||||
displayName: spanNameOrBody,
|
displayName: spanNameOrBody,
|
||||||
}),
|
}),
|
||||||
ComponentPrototype,
|
ComponentProto,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Effect-FC Component without automatic distributed tracing.
|
* Same as `make`, but creates an **untraced** version — no automatic tracing span is created.
|
||||||
*
|
*
|
||||||
* This function provides the same API surface as `make`, but does not create automatic tracing spans.
|
* Follows the exact same API shape as `Effect.fnUntraced`.
|
||||||
* It follows the exact same overload structure as `Effect.fnUntraced`.
|
* Useful for:
|
||||||
|
* - Components where you want full manual control over tracing
|
||||||
|
* - Avoiding span noise in deeply nested UI
|
||||||
*
|
*
|
||||||
* Use this variant when you need:
|
* When a string is provided as first argument, it is **only** used as the React component's `displayName`
|
||||||
* - Full manual control over tracing instrumentation
|
* (no tracing span is created).
|
||||||
* - To reduce tracing overhead in deeply nested component hierarchies
|
|
||||||
* - To avoid span noise in performance-sensitive applications
|
|
||||||
*
|
|
||||||
* When a `spanName` string is provided, it is used **exclusively** as the React component's
|
|
||||||
* `displayName` for DevTools identification. No tracing span is created.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.makeUntraced("MyComponent")(function* (props: { count: number }) {
|
|
||||||
* const value = yield* someEffect
|
|
||||||
* return <div>{value}</div>
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example As an opaque type using class syntax
|
|
||||||
* ```tsx
|
|
||||||
* class MyComponent extends Component.makeUntraced("MyComponent")(function* (props: { count: number }) {
|
|
||||||
* const value = yield* someEffect
|
|
||||||
* return <div>{value}</div>
|
|
||||||
* }) {}
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example Without name
|
|
||||||
* ```tsx
|
|
||||||
* class MyComponent extends Component.makeUntraced(function* (props: { count: number }) {
|
|
||||||
* const value = yield* someEffect
|
|
||||||
* return <div>{value}</div>
|
|
||||||
* }) {}
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example Using pipeline
|
|
||||||
* ```tsx
|
|
||||||
* class MyComponent extends Component.makeUntraced("MyComponent")(
|
|
||||||
* (props: { count: number }) => someEffect,
|
|
||||||
* Effect.map(value => <div>{value}</div>),
|
|
||||||
* ) {}
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const makeUntraced: (
|
export const makeUntraced: (
|
||||||
& make.Gen
|
& make.Gen
|
||||||
@@ -485,71 +401,52 @@ export const makeUntraced: (
|
|||||||
Object.assign(function() {}, defaultOptions, {
|
Object.assign(function() {}, defaultOptions, {
|
||||||
body: Effect.fnUntraced(spanNameOrBody as any, ...pipeables as []),
|
body: Effect.fnUntraced(spanNameOrBody as any, ...pipeables as []),
|
||||||
}),
|
}),
|
||||||
ComponentPrototype,
|
ComponentProto,
|
||||||
)
|
)
|
||||||
: (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
: (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, defaultOptions, {
|
Object.assign(function() {}, defaultOptions, {
|
||||||
body: Effect.fnUntraced(body, ...pipeables as []),
|
body: Effect.fnUntraced(body, ...pipeables as []),
|
||||||
displayName: spanNameOrBody,
|
displayName: spanNameOrBody,
|
||||||
}),
|
}),
|
||||||
ComponentPrototype,
|
ComponentProto,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new component with modified configuration options while preserving all original behavior.
|
* Creates a new component with modified options while preserving original behavior.
|
||||||
*
|
|
||||||
* This function allows you to customize component-level options such as finalizer execution strategy
|
|
||||||
* and debounce timing.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponentWithCustomOptions = MyComponent.pipe(
|
|
||||||
* Component.withOptions({
|
|
||||||
* finalizerExecutionStrategy: ExecutionStrategy.parallel,
|
|
||||||
* finalizerExecutionDebounce: "50 millis"
|
|
||||||
* })
|
|
||||||
* )
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const withOptions: {
|
export const withOptions: {
|
||||||
<T extends Component<any, any, any, any>>(
|
<T extends Component<any, any, any, any>>(
|
||||||
options: Partial<ComponentOptions>
|
options: Partial<Component.Options>
|
||||||
): (self: T) => T
|
): (self: T) => T
|
||||||
<T extends Component<any, any, any, any>>(
|
<T extends Component<any, any, any, any>>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<ComponentOptions>,
|
options: Partial<Component.Options>,
|
||||||
): T
|
): T
|
||||||
} = Function.dual(2, <T extends Component<any, any, any, any>>(
|
} = Function.dual(2, <T extends Component<any, any, any, any>>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<ComponentOptions>,
|
options: Partial<Component.Options>,
|
||||||
): T => Object.setPrototypeOf(
|
): T => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, self, options),
|
Object.assign(function() {}, self, options),
|
||||||
Object.getPrototypeOf(self),
|
Object.getPrototypeOf(self),
|
||||||
))
|
))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps an Effect-FC Component and converts it into a standard React function component,
|
* Wraps an Effect-FC `Component` and turns it into a regular React function component
|
||||||
* serving as an **entrypoint** into an Effect-FC component hierarchy.
|
* that serves as an **entrypoint** into an Effect-FC component hierarchy.
|
||||||
*
|
*
|
||||||
* This is how Effect-FC components are integrated with the broader React ecosystem,
|
* This is the recommended way to connect Effect-FC components to the rest of your React app,
|
||||||
* particularly when:
|
* especially when using routers (TanStack Router, React Router, etc.), lazy-loaded routes,
|
||||||
* - Using client-side routers (TanStack Router, React Router, etc.)
|
* or any place where a standard React component is expected.
|
||||||
* - Implementing lazy-loaded or code-split routes
|
|
||||||
* - Connecting to third-party libraries expecting standard React components
|
|
||||||
* - Creating component boundaries between Effect-FC and non-Effect-FC code
|
|
||||||
*
|
*
|
||||||
* The Effect runtime is obtained from the provided React Context.
|
* 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
|
||||||
*
|
*
|
||||||
* @param self - The Effect-FC Component to be rendered as a standard React component
|
* @example Using TanStack Router
|
||||||
* @param context - React Context providing the Effect Runtime for this component tree.
|
|
||||||
* Create this using the `ReactRuntime` module.
|
|
||||||
*
|
|
||||||
* @example Integration with TanStack Router
|
|
||||||
* ```tsx
|
* ```tsx
|
||||||
* // Application root
|
* // Main
|
||||||
* export const runtime = ReactRuntime.make(Layer.empty)
|
* export const runtime = ReactRuntime.make(Layer.empty)
|
||||||
*
|
|
||||||
* function App() {
|
* function App() {
|
||||||
* return (
|
* return (
|
||||||
* <ReactRuntime.Provider runtime={runtime}>
|
* <ReactRuntime.Provider runtime={runtime}>
|
||||||
@@ -558,12 +455,14 @@ export const withOptions: {
|
|||||||
* )
|
* )
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* // Route definition
|
* // Route
|
||||||
* export const Route = createFileRoute("/")({
|
* export const Route = createFileRoute("/")({
|
||||||
* component: Component.withRuntime(HomePage, runtime.context)
|
* 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: {
|
export const withRuntime: {
|
||||||
<P extends {}, A extends React.ReactNode, E, R>(
|
<P extends {}, A extends React.ReactNode, E, R>(
|
||||||
@@ -578,17 +477,15 @@ export const withRuntime: {
|
|||||||
context: React.Context<Runtime.Runtime<R>>,
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
) => function WithRuntime(props: P) {
|
) => function WithRuntime(props: P) {
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
Runtime.runSync(React.useContext(context))(self.use),
|
Runtime.runSync(React.useContext(context))(self),
|
||||||
props,
|
props,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal Effect service that maintains a registry of scopes associated with React component instances.
|
* Service that keeps track of scopes associated with React components
|
||||||
*
|
* (used internally by the `useScope` hook).
|
||||||
* This service is used internally by the `useScope` hook to manage the lifecycle of component scopes,
|
|
||||||
* including tracking active scopes and coordinating their cleanup when components unmount or dependencies change.
|
|
||||||
*/
|
*/
|
||||||
export class ScopeMap extends Effect.Service<ScopeMap>()("@effect-fc/Component/ScopeMap", {
|
export class ScopeMap extends Effect.Service<ScopeMap>()("@effect-fc/Component/ScopeMap", {
|
||||||
effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<object, ScopeMap.Entry>()))
|
effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<object, ScopeMap.Entry>()))
|
||||||
@@ -610,22 +507,13 @@ export declare namespace useScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect hook that creates and manages a `Scope` for the current component instance.
|
* Hook that creates and manages a `Scope` for the current component instance.
|
||||||
*
|
*
|
||||||
* This hook establishes a new scope that is automatically closed when:
|
* Automatically closes the scope whenever `deps` changes or the component unmounts.
|
||||||
* - The component unmounts
|
|
||||||
* - The dependency array `deps` changes
|
|
||||||
*
|
*
|
||||||
* The scope provides a resource management boundary for any Effects executed within the component,
|
* @param deps - dependency array like in `React.useEffect`
|
||||||
* ensuring proper cleanup of resources and execution of finalizers.
|
* @param options - finalizer execution control
|
||||||
*
|
*/
|
||||||
* @param deps - Dependency array following React.useEffect semantics. The scope is recreated
|
|
||||||
* whenever any dependency changes.
|
|
||||||
* @param options - Configuration for finalizer execution behavior, including execution strategy
|
|
||||||
* and debounce timing.
|
|
||||||
*
|
|
||||||
* @returns An Effect that produces a `Scope` for resource management
|
|
||||||
*/
|
|
||||||
export const useScope = Effect.fnUntraced(function*(
|
export const useScope = Effect.fnUntraced(function*(
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
options?: useScope.Options,
|
options?: useScope.Options,
|
||||||
@@ -679,23 +567,7 @@ export const useScope = Effect.fnUntraced(function*(
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect hook that executes an Effect once when the component mounts and caches the result.
|
* Runs an effect and returns its result only once on component mount.
|
||||||
*
|
|
||||||
* This hook is useful for one-time initialization logic that should not be re-executed
|
|
||||||
* when the component re-renders. The Effect is executed exactly once during the component's
|
|
||||||
* initial mount, and the cached result is returned on all subsequent renders.
|
|
||||||
*
|
|
||||||
* @param f - A function that returns the Effect to execute on mount
|
|
||||||
*
|
|
||||||
* @returns An Effect that produces the cached result of the Effect
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.make(function*() {
|
|
||||||
* const initialData = yield* Component.useOnMount(() => getData)
|
|
||||||
* return <div>{initialData}</div>
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const useOnMount = Effect.fnUntraced(function* <A, E, R>(
|
export const useOnMount = Effect.fnUntraced(function* <A, E, R>(
|
||||||
f: () => Effect.Effect<A, E, R>
|
f: () => Effect.Effect<A, E, R>
|
||||||
@@ -709,33 +581,9 @@ export declare namespace useOnChange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect hook that executes an Effect whenever dependencies change and caches the result.
|
* Runs an effect and returns its result whenever dependencies change.
|
||||||
*
|
*
|
||||||
* This hook combines the dependency-tracking behavior of React.useEffect with Effect caching.
|
* Provides its own `Scope` which closes whenever `deps` changes or the component unmounts.
|
||||||
* The Effect is re-executed whenever any dependency in the `deps` array changes, and the result
|
|
||||||
* is cached until the next dependency change.
|
|
||||||
*
|
|
||||||
* A dedicated scope is created for each dependency change, ensuring proper resource cleanup:
|
|
||||||
* - The scope closes when dependencies change
|
|
||||||
* - The scope closes when the component unmounts
|
|
||||||
* - All finalizers are executed according to the configured execution strategy
|
|
||||||
*
|
|
||||||
* @param f - A function that returns the Effect to execute
|
|
||||||
* @param deps - Dependency array following React.useEffect semantics
|
|
||||||
* @param options - Configuration for scope and finalizer behavior
|
|
||||||
*
|
|
||||||
* @returns An Effect that produces the cached result of the Effect
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.make(function* (props: { userId: string }) {
|
|
||||||
* const userData = yield* Component.useOnChange(
|
|
||||||
* getUser(props.userId),
|
|
||||||
* [props.userId],
|
|
||||||
* )
|
|
||||||
* return <div>{userData.name}</div>
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const useOnChange = Effect.fnUntraced(function* <A, E, R>(
|
export const useOnChange = Effect.fnUntraced(function* <A, E, R>(
|
||||||
f: () => Effect.Effect<A, E, R>,
|
f: () => Effect.Effect<A, E, R>,
|
||||||
@@ -759,36 +607,9 @@ export declare namespace useReactEffect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect hook that provides Effect-based semantics for React.useEffect.
|
* Like `React.useEffect` but accepts an effect.
|
||||||
*
|
*
|
||||||
* This hook bridges React's useEffect with the Effect system, allowing you to use Effects
|
* Cleanup logic is handled through the `Scope` API rather than using imperative cleanup.
|
||||||
* for React side effects while maintaining React's dependency tracking and lifecycle semantics.
|
|
||||||
*
|
|
||||||
* Unlike React.useEffect which uses imperative cleanup functions, this hook leverages the
|
|
||||||
* Effect Scope API for resource management. Cleanup logic is expressed declaratively through
|
|
||||||
* finalizers registered with the scope, providing better composability and error handling.
|
|
||||||
*
|
|
||||||
* @param f - A function that returns an Effect to execute as a side effect
|
|
||||||
* @param deps - Optional dependency array following React.useEffect semantics.
|
|
||||||
* If omitted, the effect runs after every render.
|
|
||||||
* @param options - Configuration for finalizer execution mode (sync or fork) and strategy
|
|
||||||
*
|
|
||||||
* @returns An Effect that produces void
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.make(function* (props: { id: string }) {
|
|
||||||
* yield* Component.useReactEffect(
|
|
||||||
* () => getNotificationStreamForUser(props.id).pipe(
|
|
||||||
* Stream.unwrap,
|
|
||||||
* Stream.runForEach(notification => Console.log(`Notification received: ${ notification }`),
|
|
||||||
* Effect.forkScoped,
|
|
||||||
* ),
|
|
||||||
* [props.id],
|
|
||||||
* )
|
|
||||||
* return <div>Subscribed to notifications for {props.id}</div>
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const useReactEffect = Effect.fnUntraced(function* <E, R>(
|
export const useReactEffect = Effect.fnUntraced(function* <E, R>(
|
||||||
f: () => Effect.Effect<void, E, R>,
|
f: () => Effect.Effect<void, E, R>,
|
||||||
@@ -827,43 +648,9 @@ export declare namespace useReactLayoutEffect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect hook that provides Effect-based semantics for React.useLayoutEffect.
|
* Like `React.useReactLayoutEffect` but accepts an effect.
|
||||||
*
|
*
|
||||||
* This hook is identical to `useReactEffect` but executes synchronously after DOM mutations
|
* Cleanup logic is handled through the `Scope` API rather than using imperative cleanup.
|
||||||
* but before the browser paints, following React.useLayoutEffect semantics.
|
|
||||||
*
|
|
||||||
* Use this hook when you need to:
|
|
||||||
* - Measure DOM elements (e.g., for layout calculations)
|
|
||||||
* - Synchronously update state based on DOM measurements
|
|
||||||
* - Avoid visual flicker from asynchronous updates
|
|
||||||
*
|
|
||||||
* Like `useReactEffect`, cleanup logic is handled through the Effect Scope API rather than
|
|
||||||
* imperative cleanup functions, providing declarative and composable resource management.
|
|
||||||
*
|
|
||||||
* @param f - A function that returns an Effect to execute as a layout side effect
|
|
||||||
* @param deps - Optional dependency array following React.useLayoutEffect semantics.
|
|
||||||
* If omitted, the effect runs after every render.
|
|
||||||
* @param options - Configuration for finalizer execution mode (sync or fork) and strategy
|
|
||||||
*
|
|
||||||
* @returns An Effect that produces void
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.make(function*() {
|
|
||||||
* const ref = React.useRef<HTMLDivElement>(null)
|
|
||||||
* yield* Component.useReactLayoutEffect(
|
|
||||||
* () => Effect.gen(function* () {
|
|
||||||
* const element = ref.current
|
|
||||||
* if (element) {
|
|
||||||
* const rect = element.getBoundingClientRect()
|
|
||||||
* yield* Console.log(`Element dimensions: ${ rect.width }x${ rect.height }`)
|
|
||||||
* }
|
|
||||||
* }),
|
|
||||||
* [],
|
|
||||||
* )
|
|
||||||
* return <div ref={ref}>Content</div>
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const useReactLayoutEffect = Effect.fnUntraced(function* <E, R>(
|
export const useReactLayoutEffect = Effect.fnUntraced(function* <E, R>(
|
||||||
f: () => Effect.Effect<void, E, R>,
|
f: () => Effect.Effect<void, E, R>,
|
||||||
@@ -876,23 +663,7 @@ export const useReactLayoutEffect = Effect.fnUntraced(function* <E, R>(
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect hook that provides a synchronous function to execute Effects within the current runtime context.
|
* Get a synchronous run function for the current runtime context.
|
||||||
*
|
|
||||||
* This hook returns a function that can execute Effects synchronously, blocking until completion.
|
|
||||||
* Use this when you need to run Effects from non-Effect code (e.g., event handlers, callbacks)
|
|
||||||
* within a component.
|
|
||||||
*
|
|
||||||
* @returns An Effect that produces a function capable of synchronously executing Effects
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.make(function*() {
|
|
||||||
* const runSync = yield* Component.useRunSync<SomeService>() // Specify required services
|
|
||||||
* const runSync = yield* Component.useRunSync() // Or no service requirements
|
|
||||||
*
|
|
||||||
* return <button onClick={() => runSync(someEffect)}>Click me</button>
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const useRunSync = <R = never>(): Effect.Effect<
|
export const useRunSync = <R = never>(): Effect.Effect<
|
||||||
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => A,
|
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => A,
|
||||||
@@ -901,23 +672,7 @@ export const useRunSync = <R = never>(): Effect.Effect<
|
|||||||
> => Effect.andThen(Effect.runtime(), Runtime.runSync)
|
> => Effect.andThen(Effect.runtime(), Runtime.runSync)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect hook that provides an asynchronous function to execute Effects within the current runtime context.
|
* Get a Promise-based run function for the current runtime context.
|
||||||
*
|
|
||||||
* This hook returns a function that executes Effects asynchronously, returning a Promise that resolves
|
|
||||||
* with the Effect's result. Use this when you need to run Effects from non-Effect code (e.g., event handlers,
|
|
||||||
* async callbacks) and want to handle the result asynchronously.
|
|
||||||
*
|
|
||||||
* @returns An Effect that produces a function capable of asynchronously executing Effects
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.make(function*() {
|
|
||||||
* const runPromise = yield* Component.useRunPromise<SomeService>() // Specify required services
|
|
||||||
* const runPromise = yield* Component.useRunPromise() // Or no service requirements
|
|
||||||
*
|
|
||||||
* return <button onClick={() => runPromise(someEffect)}>Click me</button>
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const useRunPromise = <R = never>(): Effect.Effect<
|
export const useRunPromise = <R = never>(): Effect.Effect<
|
||||||
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => Promise<A>,
|
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => Promise<A>,
|
||||||
@@ -926,32 +681,7 @@ export const useRunPromise = <R = never>(): Effect.Effect<
|
|||||||
> => Effect.andThen(Effect.runtime(), context => Runtime.runPromise(context))
|
> => Effect.andThen(Effect.runtime(), context => Runtime.runPromise(context))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect hook that memoizes a function that returns an Effect, providing synchronous execution.
|
* Turns a function returning an effect into a memoized synchronous function.
|
||||||
*
|
|
||||||
* This hook wraps a function that returns an Effect and returns a memoized version that:
|
|
||||||
* - Executes the Effect synchronously when called
|
|
||||||
* - Is memoized based on the provided dependency array
|
|
||||||
* - Maintains referential equality across renders when dependencies don't change
|
|
||||||
*
|
|
||||||
* Use this to create stable callback references for event handlers and other scenarios
|
|
||||||
* where you need to execute Effects synchronously from non-Effect code.
|
|
||||||
*
|
|
||||||
* @param f - A function that accepts arguments and returns an Effect
|
|
||||||
* @param deps - Dependency array. The memoized function is recreated when dependencies change.
|
|
||||||
*
|
|
||||||
* @returns An Effect that produces a memoized function with the same signature as `f`
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.make(function* (props: { onSave: (data: Data) => void }) {
|
|
||||||
* const handleSave = yield* Component.useCallbackSync(
|
|
||||||
* (data: Data) => Effect.sync(() => props.onSave(data)),
|
|
||||||
* [props.onSave],
|
|
||||||
* )
|
|
||||||
*
|
|
||||||
* return <button onClick={() => handleSave(myData)}>Save</button>
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||||
f: (...args: Args) => Effect.Effect<A, E, R>,
|
f: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
@@ -966,32 +696,7 @@ export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect hook that memoizes a function that returns an Effect, providing asynchronous execution.
|
* Turns a function returning an effect into a memoized Promise-based asynchronous function.
|
||||||
*
|
|
||||||
* This hook wraps a function that returns an Effect and returns a memoized version that:
|
|
||||||
* - Executes the Effect asynchronously when called, returning a Promise
|
|
||||||
* - Is memoized based on the provided dependency array
|
|
||||||
* - Maintains referential equality across renders when dependencies don't change
|
|
||||||
*
|
|
||||||
* Use this to create stable callback references for async event handlers and other scenarios
|
|
||||||
* where you need to execute Effects asynchronously from non-Effect code.
|
|
||||||
*
|
|
||||||
* @param f - A function that accepts arguments and returns an Effect
|
|
||||||
* @param deps - Dependency array. The memoized function is recreated when dependencies change.
|
|
||||||
*
|
|
||||||
* @returns An Effect that produces a memoized function that returns a Promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.make(function* (props: { onSave: (data: Data) => void }) {
|
|
||||||
* const handleSave = yield* Component.useCallbackPromise(
|
|
||||||
* (data: Data) => Effect.promise(() => props.onSave(data)),
|
|
||||||
* [props.onSave],
|
|
||||||
* )
|
|
||||||
*
|
|
||||||
* return <button onClick={() => handleSave(myData)}>Save</button>
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const useCallbackPromise = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
export const useCallbackPromise = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||||
f: (...args: Args) => Effect.Effect<A, E, R>,
|
f: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
@@ -1010,70 +715,16 @@ export declare namespace useContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect hook that constructs an Effect Layer and returns the resulting context.
|
* Hook that constructs a layer and returns the created context.
|
||||||
*
|
*
|
||||||
* This hook creates a managed runtime from the provided layer and returns the context it produces.
|
* The layer gets reconstructed everytime `layer` changes, so make sure its value is stable.
|
||||||
* The layer is reconstructed whenever its value changes, so ensure the layer reference is stable
|
|
||||||
* (typically by memoizing it or defining it outside the component).
|
|
||||||
*
|
*
|
||||||
* The hook automatically manages the layer's lifecycle:
|
* Building a layer containing asynchronous effects require the component calling this hook to be made async using `Async.async`.
|
||||||
* - The layer is built when the component mounts or when the layer reference changes
|
|
||||||
* - Resources are properly released when the component unmounts or dependencies change
|
|
||||||
* - Finalizers are executed according to the configured execution strategy
|
|
||||||
*
|
|
||||||
* @param layer - The Effect Layer to construct. Should be a stable reference to avoid unnecessary
|
|
||||||
* reconstruction. Consider memoizing with React.useMemo if defined inline.
|
|
||||||
* @param options - Configuration for scope and finalizer behavior
|
|
||||||
*
|
|
||||||
* @returns An Effect that produces the context created by the layer
|
|
||||||
*
|
|
||||||
* @throws If the layer contains asynchronous effects, the component must be wrapped with `Async.async`
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const MyLayer = Layer.succeed(MyService, new MyServiceImpl())
|
|
||||||
* const MyComponent = Component.make(function*() {
|
|
||||||
* const context = yield* Component.useContextFromLayer(MyLayer)
|
|
||||||
* const Sub = yield* SubComponent.use.pipe(
|
|
||||||
* Effect.provide(context)
|
|
||||||
* )
|
|
||||||
*
|
|
||||||
* return <Sub />
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example With memoized layer
|
|
||||||
* ```tsx
|
|
||||||
* const MyComponent = Component.make(function*(props: { id: string })) {
|
|
||||||
* const context = yield* Component.useContextFromLayer(
|
|
||||||
* React.useMemo(() => Layer.succeed(MyService, new MyServiceImpl(props.id)), [props.id])
|
|
||||||
* )
|
|
||||||
* const Sub = yield* SubComponent.use.pipe(
|
|
||||||
* Effect.provide(context)
|
|
||||||
* )
|
|
||||||
*
|
|
||||||
* return <Sub />
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example With async layer
|
|
||||||
* ```tsx
|
|
||||||
* const MyAsyncLayer = Layer.effect(MyService, someAsyncEffect)
|
|
||||||
* const MyComponent = Component.make(function*() {
|
|
||||||
* const context = yield* Component.useContextFromLayer(MyAsyncLayer)
|
|
||||||
* const Sub = yield* SubComponent.use.pipe(
|
|
||||||
* Effect.provide(context)
|
|
||||||
* )
|
|
||||||
*
|
|
||||||
* return <Sub />
|
|
||||||
* }).pipe(
|
|
||||||
* Async.async // Required to handle async layer effects
|
|
||||||
* )
|
|
||||||
*/
|
*/
|
||||||
export const useContextFromLayer = <ROut, E, RIn>(
|
export const useContext = <ROut, E, RIn>(
|
||||||
layer: Layer.Layer<ROut, E, RIn>,
|
layer: Layer.Layer<ROut, E, RIn>,
|
||||||
options?: useContext.Options,
|
options?: useContext.Options,
|
||||||
): Effect.Effect<Context.Context<ROut>, E, RIn | Scope.Scope> => useOnChange(() => Effect.context<RIn>().pipe(
|
): 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.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))),
|
||||||
Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
|
Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
|
||||||
Effect.andThen(runtime => runtime.runtimeEffect),
|
Effect.andThen(runtime => runtime.runtimeEffect),
|
||||||
|
|||||||
@@ -1,30 +1,24 @@
|
|||||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
import { type Equivalence, Function, Predicate } from "effect"
|
import { type Equivalence, Function, Predicate } from "effect"
|
||||||
import * as React from "react"
|
|
||||||
import type * as Component from "./Component.js"
|
import type * as Component from "./Component.js"
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized")
|
export const TypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized")
|
||||||
export type TypeId = typeof TypeId
|
export type TypeId = typeof TypeId
|
||||||
|
|
||||||
export interface Memoized<P> extends MemoizedOptions<P> {
|
export interface Memoized<P> extends Memoized.Options<P> {
|
||||||
readonly [TypeId]: TypeId
|
readonly [TypeId]: TypeId
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemoizedOptions<P> {
|
export namespace Memoized {
|
||||||
readonly propsEquivalence?: Equivalence.Equivalence<P>
|
export interface Options<P> {
|
||||||
|
readonly propsAreEqual?: Equivalence.Equivalence<P>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const MemoizedPrototype = Object.freeze({
|
const MemoizedProto = Object.freeze({
|
||||||
[TypeId]: TypeId,
|
[TypeId]: TypeId
|
||||||
|
|
||||||
transformFunctionComponent<P extends {}>(
|
|
||||||
this: Memoized<P>,
|
|
||||||
f: React.FC<P>,
|
|
||||||
) {
|
|
||||||
return React.memo(f, this.propsEquivalence)
|
|
||||||
},
|
|
||||||
} as const)
|
} as const)
|
||||||
|
|
||||||
|
|
||||||
@@ -35,22 +29,22 @@ export const memoized = <T extends Component.Component<any, any, any, any>>(
|
|||||||
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, self),
|
Object.assign(function() {}, self),
|
||||||
Object.freeze(Object.setPrototypeOf(
|
Object.freeze(Object.setPrototypeOf(
|
||||||
Object.assign({}, MemoizedPrototype),
|
Object.assign({}, MemoizedProto),
|
||||||
Object.getPrototypeOf(self),
|
Object.getPrototypeOf(self),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const withOptions: {
|
export const withOptions: {
|
||||||
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
||||||
options: Partial<MemoizedOptions<Component.Component.Props<T>>>
|
options: Partial<Memoized.Options<Component.Component.Props<T>>>
|
||||||
): (self: T) => T
|
): (self: T) => T
|
||||||
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<MemoizedOptions<Component.Component.Props<T>>>,
|
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
||||||
): T
|
): T
|
||||||
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
||||||
self: T,
|
self: T,
|
||||||
options: Partial<MemoizedOptions<Component.Component.Props<T>>>,
|
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
||||||
): T => Object.setPrototypeOf(
|
): T => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, self, options),
|
Object.assign(function() {}, self, options),
|
||||||
Object.getPrototypeOf(self),
|
Object.getPrototypeOf(self),
|
||||||
|
|||||||
@@ -6,27 +6,28 @@ import * as Result from "./Result.js"
|
|||||||
export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
|
export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
|
||||||
export type QueryTypeId = typeof QueryTypeId
|
export type QueryTypeId = typeof QueryTypeId
|
||||||
|
|
||||||
export interface Query<in out K extends Query.AnyKey, in out A, in out KE = never, in out KR = never, in out E = never, in out R = never>
|
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 {
|
extends Pipeable.Pipeable {
|
||||||
readonly [QueryTypeId]: QueryTypeId
|
readonly [QueryTypeId]: QueryTypeId
|
||||||
|
|
||||||
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>
|
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>
|
||||||
readonly key: Stream.Stream<K, KE, KR>
|
readonly key: Stream.Stream<K>
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>
|
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||||
|
readonly initialProgress: P
|
||||||
|
|
||||||
readonly staleTime: Duration.DurationInput
|
readonly staleTime: Duration.DurationInput
|
||||||
readonly refreshOnWindowFocus: boolean
|
readonly refreshOnWindowFocus: boolean
|
||||||
|
|
||||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
||||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
||||||
readonly result: Subscribable.Subscribable<Result.Result<A, E>>
|
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Success<A, E> | Result.Failure<A, E>>>
|
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
||||||
|
|
||||||
readonly run: Effect.Effect<void>
|
readonly run: Effect.Effect<void>
|
||||||
fetch(key: K): Effect.Effect<Result.Success<A, E> | Result.Failure<A, E>>
|
fetch(key: K): Effect.Effect<Result.Final<A, E, P>>
|
||||||
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E>>>
|
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
||||||
readonly refresh: Effect.Effect<Result.Success<A, E> | Result.Failure<A, E>, Cause.NoSuchElementException>
|
readonly refresh: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
|
||||||
readonly refreshSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E>>, Cause.NoSuchElementException>
|
readonly refreshSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
|
||||||
|
|
||||||
readonly invalidateCache: Effect.Effect<void>
|
readonly invalidateCache: Effect.Effect<void>
|
||||||
invalidateCacheEntry(key: K): Effect.Effect<void>
|
invalidateCacheEntry(key: K): Effect.Effect<void>
|
||||||
@@ -36,22 +37,23 @@ export declare namespace Query {
|
|||||||
export type AnyKey = readonly any[]
|
export type AnyKey = readonly any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryImpl<in out K extends Query.AnyKey, in out A, in out KE = never, in out KR = never, in out E = never, in out R = never>
|
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, KE, KR, E, R> {
|
extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
||||||
readonly [QueryTypeId]: QueryTypeId = QueryTypeId
|
readonly [QueryTypeId]: QueryTypeId = QueryTypeId
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | KR | R>,
|
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>,
|
||||||
readonly key: Stream.Stream<K, KE, KR>,
|
readonly key: Stream.Stream<K>,
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
||||||
|
readonly initialProgress: P,
|
||||||
|
|
||||||
readonly staleTime: Duration.DurationInput,
|
readonly staleTime: Duration.DurationInput,
|
||||||
readonly refreshOnWindowFocus: boolean,
|
readonly refreshOnWindowFocus: boolean,
|
||||||
|
|
||||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
||||||
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
||||||
readonly result: SubscriptionRef.SubscriptionRef<Result.Success<A, E> | Result.Failure<A, E>>,
|
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
||||||
readonly latestFinalResult: SubscriptionRef.SubscriptionRef<Option.Option<Result.Success<A, E> | Result.Failure<A, E>>>,
|
readonly latestFinalResult: SubscriptionRef.SubscriptionRef<Option.Option<Result.Final<A, E, P>>>,
|
||||||
|
|
||||||
readonly runSemaphore: Effect.Semaphore,
|
readonly runSemaphore: Effect.Semaphore,
|
||||||
) {
|
) {
|
||||||
@@ -75,7 +77,6 @@ extends Pipeable.Class() implements Query<K, A, KE, KR, E, R> {
|
|||||||
], { concurrency: "unbounded" }).pipe(
|
], { concurrency: "unbounded" }).pipe(
|
||||||
Effect.ignore,
|
Effect.ignore,
|
||||||
this.runSemaphore.withPermits(1),
|
this.runSemaphore.withPermits(1),
|
||||||
Effect.provide(this.context),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ extends Pipeable.Class() implements Query<K, A, KE, KR, E, R> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(key: K): Effect.Effect<Result.Success<A, E> | Result.Failure<A, E>> {
|
fetch(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
return this.interrupt.pipe(
|
return this.interrupt.pipe(
|
||||||
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
||||||
Effect.andThen(this.latestFinalResult),
|
Effect.andThen(this.latestFinalResult),
|
||||||
@@ -150,7 +151,7 @@ extends Pipeable.Class() implements Query<K, A, KE, KR, E, R> {
|
|||||||
|
|
||||||
startCached(
|
startCached(
|
||||||
key: K,
|
key: K,
|
||||||
previous: Result.Success<A, E> | Result.Failure<A, E>,
|
initial: Result.Initial | Result.Final<A, E, P>,
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
never,
|
never,
|
||||||
@@ -172,46 +173,31 @@ extends Pipeable.Class() implements Query<K, A, KE, KR, E, R> {
|
|||||||
|
|
||||||
start(
|
start(
|
||||||
key: K,
|
key: K,
|
||||||
previous: Result.Success<A, E> | Result.Failure<A, E>,
|
initial: Result.Initial | Result.Final<A, E, P>,
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
Subscribable.Subscribable<Result.Result<A, E>>,
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | R
|
Scope.Scope | R
|
||||||
> {
|
> {
|
||||||
return Effect.Do.pipe(
|
return Result.unsafeForkEffect(
|
||||||
Effect.bind("ref", () => SubscriptionRef.make<Result.Result<A, E>>(Result.initial())),
|
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),
|
||||||
)
|
)
|
||||||
|
|
||||||
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,
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
|
|
||||||
// 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(
|
watch(
|
||||||
@@ -279,11 +265,11 @@ extends Pipeable.Class() implements Query<K, A, KE, KR, E, R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown> => Predicate.hasProperty(u, QueryTypeId)
|
export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, QueryTypeId)
|
||||||
|
|
||||||
export declare namespace make {
|
export declare namespace make {
|
||||||
export interface Options<K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never> {
|
export interface Options<K extends Query.AnyKey, A, E = never, R = never, P = never> {
|
||||||
readonly key: Stream.Stream<K, KE, KR>
|
readonly key: Stream.Stream<K>
|
||||||
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
||||||
readonly initialProgress?: P
|
readonly initialProgress?: P
|
||||||
readonly staleTime?: Duration.DurationInput
|
readonly staleTime?: Duration.DurationInput
|
||||||
@@ -291,17 +277,17 @@ export declare namespace make {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
|
export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, E = never, R = never, P = never>(
|
||||||
options: make.Options<K, A, KE, KR, E, R, P>
|
options: make.Options<K, A, E, R, P>
|
||||||
): Effect.fn.Return<
|
): Effect.fn.Return<
|
||||||
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>
|
Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
|
||||||
> {
|
> {
|
||||||
const client = yield* QueryClient.QueryClient
|
const client = yield* QueryClient.QueryClient
|
||||||
|
|
||||||
return new QueryImpl(
|
return new QueryImpl(
|
||||||
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
||||||
options.key,
|
options.key,
|
||||||
options.f as any,
|
options.f as any,
|
||||||
options.initialProgress as P,
|
options.initialProgress as P,
|
||||||
@@ -318,12 +304,12 @@ export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE =
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const service = <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
|
export const service = <K extends Query.AnyKey, A, E = never, R = never, P = never>(
|
||||||
options: make.Options<K, A, KE, KR, E, R, P>
|
options: make.Options<K, A, E, R, P>
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>
|
Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
|
||||||
> => Effect.tap(
|
> => Effect.tap(
|
||||||
make(options),
|
make(options),
|
||||||
query => Effect.forkScoped(query.run),
|
query => Effect.forkScoped(query.run),
|
||||||
|
|||||||
@@ -1 +1,279 @@
|
|||||||
export * from "@effect-atom/atom/Result"
|
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
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ extends Omit<TextField.RootProps, "optional" | "defaultValue">, Form.useOptional
|
|||||||
export type TextFieldFormInputProps = Props | OptionalProps
|
export type TextFieldFormInputProps = Props | OptionalProps
|
||||||
|
|
||||||
|
|
||||||
export class TextFieldFormInputView extends Component.make("TextFieldFormInputView")(function*(props: TextFieldFormInputProps) {
|
export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInput")(function*(props: TextFieldFormInputProps) {
|
||||||
const input: (
|
const input: (
|
||||||
| { readonly optional: true } & Form.useOptionalInput.Success<string>
|
| { readonly optional: true } & Form.useOptionalInput.Success<string>
|
||||||
| { readonly optional: false } & Form.useInput.Success<string>
|
| { readonly optional: false } & Form.useInput.Success<string>
|
||||||
@@ -9,8 +9,8 @@ import { runtime } from "@/runtime"
|
|||||||
|
|
||||||
// Generator version
|
// Generator version
|
||||||
const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
||||||
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent.use
|
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent
|
||||||
const AsyncComponentFC = yield* AsyncComponent.use
|
const AsyncComponentFC = yield* AsyncComponent
|
||||||
const [input, setInput] = React.useState("")
|
const [input, setInput] = React.useState("")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,7 +51,7 @@ const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
|||||||
|
|
||||||
|
|
||||||
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
|
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
|
||||||
const SubComponentFC = yield* SubComponent.use
|
const SubComponentFC = yield* SubComponent
|
||||||
|
|
||||||
yield* Effect.sleep("500 millis") // Async operation
|
yield* Effect.sleep("500 millis") // Async operation
|
||||||
// Cannot use React hooks after the async operation
|
// Cannot use React hooks after the async operation
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const SubComponent = Component.makeUntraced("SubComponent")(function*() {
|
|||||||
const ContextView = Component.makeUntraced("ContextView")(function*() {
|
const ContextView = Component.makeUntraced("ContextView")(function*() {
|
||||||
const [serviceValue, setServiceValue] = React.useState("test")
|
const [serviceValue, setServiceValue] = React.useState("test")
|
||||||
const SubServiceLayer = React.useMemo(() => SubService.Default(serviceValue), [serviceValue])
|
const SubServiceLayer = React.useMemo(() => SubService.Default(serviceValue), [serviceValue])
|
||||||
const SubComponentFC = yield* Effect.provide(SubComponent.use, yield* Component.useContextFromLayer(SubServiceLayer))
|
const SubComponentFC = yield* Effect.provide(SubComponent, yield* Component.useContext(SubServiceLayer))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ const RouteComponent = Component.makeUntraced("RouteComponent")(function*() {
|
|||||||
onChange={e => setValue(e.target.value)}
|
onChange={e => setValue(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{yield* Effect.map(SubComponent.use, FC => <FC />)}
|
{yield* Effect.map(SubComponent, FC => <FC />)}
|
||||||
{yield* Effect.map(MemoizedSubComponent.use, FC => <FC />)}
|
{yield* Effect.map(MemoizedSubComponent, FC => <FC />)}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}).pipe(
|
}).pipe(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
|||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
||||||
import { Component, Form, Subscribable } from "effect-fc"
|
import { Component, Form, Subscribable } from "effect-fc"
|
||||||
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||||
import { runtime } from "@/runtime"
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ const RegisterFormSubmitSchema = Schema.Struct({
|
|||||||
birth: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
|
birth: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
|
||||||
})
|
})
|
||||||
|
|
||||||
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
|
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||||
scoped: Form.service({
|
scoped: Form.service({
|
||||||
schema: RegisterFormSchema.pipe(
|
schema: RegisterFormSchema.pipe(
|
||||||
Schema.compose(
|
Schema.compose(
|
||||||
@@ -62,15 +62,15 @@ class RegisterFormService extends Effect.Service<RegisterFormService>()("Registe
|
|||||||
})
|
})
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
class RegisterFormView extends Component.make("RegisterFormView")(function*() {
|
class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() {
|
||||||
const form = yield* RegisterFormService
|
const form = yield* RegisterForm
|
||||||
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
|
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
|
||||||
form.canSubmit,
|
form.canSubmit,
|
||||||
form.mutation.result,
|
form.mutation.result,
|
||||||
])
|
])
|
||||||
|
|
||||||
const runPromise = yield* Component.useRunPromise()
|
const runPromise = yield* Component.useRunPromise()
|
||||||
const TextFieldFormInput = yield* TextFieldFormInputView.use
|
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||||
|
|
||||||
yield* Component.useOnMount(() => Effect.gen(function*() {
|
yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
yield* Effect.addFinalizer(() => Console.log("RegisterFormView unmounted"))
|
yield* Effect.addFinalizer(() => Console.log("RegisterFormView unmounted"))
|
||||||
@@ -85,15 +85,15 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {
|
|||||||
void runPromise(form.submit)
|
void runPromise(form.submit)
|
||||||
}}>
|
}}>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<TextFieldFormInput
|
<TextFieldFormInputFC
|
||||||
field={yield* form.field(["email"])}
|
field={yield* form.field(["email"])}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextFieldFormInput
|
<TextFieldFormInputFC
|
||||||
field={yield* form.field(["password"])}
|
field={yield* form.field(["password"])}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextFieldFormInput
|
<TextFieldFormInputFC
|
||||||
optional
|
optional
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
field={yield* form.field(["birth"])}
|
field={yield* form.field(["birth"])}
|
||||||
@@ -115,13 +115,13 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {
|
|||||||
)
|
)
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
const RegisterPage = Component.make("RegisterPageView")(function*() {
|
const RegisterPage = Component.makeUntraced("RegisterPage")(function*() {
|
||||||
const RegisterForm = yield* Effect.provide(
|
const RegisterFormViewFC = yield* Effect.provide(
|
||||||
RegisterFormView.use,
|
RegisterFormView,
|
||||||
yield* Component.useContextFromLayer(RegisterFormService.Default),
|
yield* Component.useContext(RegisterForm.Default),
|
||||||
)
|
)
|
||||||
|
|
||||||
return <RegisterForm />
|
return <RegisterFormViewFC />
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Component.withRuntime(runtime.context)
|
Component.withRuntime(runtime.context)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ import { createFileRoute } from "@tanstack/react-router"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Component } from "effect-fc"
|
import { Component } from "effect-fc"
|
||||||
import { runtime } from "@/runtime"
|
import { runtime } from "@/runtime"
|
||||||
import { TodosState } from "@/todo/TodosState"
|
import { Todos } from "@/todo/Todos"
|
||||||
import { TodosView } from "@/todo/TodosView"
|
import { TodosState } from "@/todo/TodosState.service"
|
||||||
|
|
||||||
|
|
||||||
const TodosStateLive = TodosState.Default("todos")
|
const TodosStateLive = TodosState.Default("todos")
|
||||||
|
|
||||||
const Index = Component.make("IndexView")(function*() {
|
const Index = Component.makeUntraced("Index")(function*() {
|
||||||
const Todos = yield* Effect.provide(
|
const TodosFC = yield* Effect.provide(
|
||||||
TodosView.use,
|
Todos,
|
||||||
yield* Component.useContextFromLayer(TodosStateLive),
|
yield* Component.useContext(TodosStateLive),
|
||||||
)
|
)
|
||||||
|
|
||||||
return <Todos />
|
return <TodosFC />
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Component.withRuntime(runtime.context)
|
Component.withRuntime(runtime.context)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,16 +13,15 @@ const Post = Schema.Struct({
|
|||||||
body: Schema.String,
|
body: Schema.String,
|
||||||
})
|
})
|
||||||
|
|
||||||
const ResultView = Component.make("ResultView")(function*() {
|
const ResultView = Component.makeUntraced("Result")(function*() {
|
||||||
const runPromise = yield* Component.useRunPromise()
|
const runPromise = yield* Component.useRunPromise()
|
||||||
|
|
||||||
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
const idRef = yield* SubscriptionRef.make(1)
|
const idRef = yield* SubscriptionRef.make(1)
|
||||||
const key = Stream.map(idRef.changes, id => [id] as const)
|
|
||||||
|
|
||||||
const query = yield* Query.service({
|
const query = yield* Query.service({
|
||||||
key,
|
key: Stream.zipLatest(Stream.make("posts" as const), idRef.changes),
|
||||||
f: ([id]) => HttpClient.HttpClient.pipe(
|
f: ([, id]) => HttpClient.HttpClient.pipe(
|
||||||
Effect.tap(Effect.sleep("500 millis")),
|
Effect.tap(Effect.sleep("500 millis")),
|
||||||
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||||
Effect.andThen(response => response.json),
|
Effect.andThen(response => response.json),
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { Component, Form, Subscribable } from "effect-fc"
|
|||||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||||
import { FaDeleteLeft } from "react-icons/fa6"
|
import { FaDeleteLeft } from "react-icons/fa6"
|
||||||
import * as Domain from "@/domain"
|
import * as Domain from "@/domain"
|
||||||
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||||
import { TodosState } from "./TodosState"
|
import { TodosState } from "./TodosState.service"
|
||||||
|
|
||||||
|
|
||||||
const TodoFormSchema = Schema.compose(Schema.Struct({
|
const TodoFormSchema = Schema.compose(Schema.Struct({
|
||||||
@@ -30,7 +30,7 @@ export type TodoProps = (
|
|||||||
| { readonly _tag: "edit", readonly id: string }
|
| { readonly _tag: "edit", readonly id: string }
|
||||||
)
|
)
|
||||||
|
|
||||||
export class TodoView extends Component.make("TodoView")(function*(props: TodoProps) {
|
export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoProps) {
|
||||||
const state = yield* TodosState
|
const state = yield* TodosState
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@@ -83,17 +83,17 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
|
|||||||
|
|
||||||
const runSync = yield* Component.useRunSync()
|
const runSync = yield* Component.useRunSync()
|
||||||
const runPromise = yield* Component.useRunPromise<DateTime.CurrentTimeZone>()
|
const runPromise = yield* Component.useRunPromise<DateTime.CurrentTimeZone>()
|
||||||
const TextFieldFormInput = yield* TextFieldFormInputView.use
|
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="row" align="center" gap="2">
|
<Flex direction="row" align="center" gap="2">
|
||||||
<Box flexGrow="1">
|
<Box flexGrow="1">
|
||||||
<Flex direction="column" align="stretch" gap="2">
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
<TextFieldFormInput field={contentField} />
|
<TextFieldFormInputFC field={contentField} />
|
||||||
|
|
||||||
<Flex direction="row" justify="center" align="center" gap="2">
|
<Flex direction="row" justify="center" align="center" gap="2">
|
||||||
<TextFieldFormInput
|
<TextFieldFormInputFC
|
||||||
optional
|
optional
|
||||||
field={completedAtField}
|
field={completedAtField}
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Container, Flex, Heading } from "@radix-ui/themes"
|
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||||
import { Chunk, Console, Effect } from "effect"
|
import { Chunk, Console, Effect } from "effect"
|
||||||
import { Component, Subscribable } from "effect-fc"
|
import { Component, Subscribable } from "effect-fc"
|
||||||
import { TodosState } from "./TodosState"
|
import { Todo } from "./Todo"
|
||||||
import { TodoView } from "./TodoView"
|
import { TodosState } from "./TodosState.service"
|
||||||
|
|
||||||
|
|
||||||
export class TodosView extends Component.make("TodosView")(function*() {
|
export class Todos extends Component.makeUntraced("Todos")(function*() {
|
||||||
const state = yield* TodosState
|
const state = yield* TodosState
|
||||||
const [todos] = yield* Subscribable.useSubscribables([state.ref])
|
const [todos] = yield* Subscribable.useSubscribables([state.ref])
|
||||||
|
|
||||||
@@ -14,17 +14,17 @@ export class TodosView extends Component.make("TodosView")(function*() {
|
|||||||
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||||
))
|
))
|
||||||
|
|
||||||
const Todo = yield* TodoView.use
|
const TodoFC = yield* Todo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Heading align="center">Todos</Heading>
|
<Heading align="center">Todos</Heading>
|
||||||
|
|
||||||
<Flex direction="column" align="stretch" gap="2" mt="2">
|
<Flex direction="column" align="stretch" gap="2" mt="2">
|
||||||
<Todo _tag="new" />
|
<TodoFC _tag="new" />
|
||||||
|
|
||||||
{Chunk.map(todos, todo =>
|
{Chunk.map(todos, todo =>
|
||||||
<Todo key={todo.id} _tag="edit" id={todo.id} />
|
<TodoFC key={todo.id} _tag="edit" id={todo.id} />
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
Reference in New Issue
Block a user