Compare commits
23 Commits
result
...
renovate/b
| Author | SHA1 | Date | |
|---|---|---|---|
| 95966ac7ee | |||
|
|
2457b1c536 | ||
|
|
3cb3f6d103 | ||
|
|
46d7aacc69 | ||
|
|
2f118c5f98 | ||
|
|
0ba00a0b4f | ||
|
|
c644f8c44b | ||
|
|
6917c72101 | ||
|
|
9b3ce62d3e | ||
|
|
8b69d4e500 | ||
|
|
a9c0590b7c | ||
|
|
b63d1ab2c7 | ||
|
|
df86af839e | ||
|
|
dbe42aadb1 | ||
|
|
355e179fbd | ||
|
|
8dd40d3365 | ||
|
|
929f835e94 | ||
|
|
1f47887643 | ||
|
|
3794f56a86 | ||
|
|
7f8f91bfc5 | ||
|
|
45b38d6c1f | ||
|
|
2080d35b2c | ||
|
|
346ba9066b |
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.80.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.80.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-dKMATT1fDzaCpNrICpXga7sjJBtFLpKCAoE/1MiGXI8UwcHA9rmAZ2t52JO9g/kJpERWyomkJ+rl+VFlwNIofg=="],
|
||||||
|
|
||||||
"@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.80.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"
|
||||||
|
|||||||
@@ -1,31 +1,47 @@
|
|||||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
import { Effect, Function, Predicate, Runtime, Scope } from "effect"
|
import { Effect, type Equivalence, Function, Predicate, Runtime, Scope } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as Component from "./Component.js"
|
import * as Component from "./Component.js"
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Async/Async")
|
export const AsyncTypeId: unique symbol = Symbol.for("@effect-fc/Async/Async")
|
||||||
export type TypeId = typeof TypeId
|
export type AsyncTypeId = typeof AsyncTypeId
|
||||||
|
|
||||||
export interface Async extends AsyncOptions {
|
|
||||||
readonly [TypeId]: TypeId
|
/**
|
||||||
|
* A trait for `Component`'s that allows them running asynchronous effects.
|
||||||
|
*/
|
||||||
|
export interface Async extends AsyncPrototype, AsyncOptions {}
|
||||||
|
|
||||||
|
export interface AsyncPrototype {
|
||||||
|
readonly [AsyncTypeId]: AsyncTypeId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for `Async` components.
|
||||||
|
*/
|
||||||
export interface AsyncOptions {
|
export interface AsyncOptions {
|
||||||
|
/**
|
||||||
|
* The default fallback React node to display while the async operation is pending.
|
||||||
|
* Used if no fallback is provided to the component when rendering.
|
||||||
|
*/
|
||||||
readonly defaultFallback?: React.ReactNode
|
readonly defaultFallback?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for `Async` components.
|
||||||
|
*/
|
||||||
export type AsyncProps = Omit<React.SuspenseProps, "children">
|
export type AsyncProps = Omit<React.SuspenseProps, "children">
|
||||||
|
|
||||||
|
|
||||||
export const AsyncPrototype = Object.freeze({
|
export const AsyncPrototype: AsyncPrototype = Object.freeze({
|
||||||
[TypeId]: TypeId,
|
[AsyncTypeId]: AsyncTypeId,
|
||||||
|
|
||||||
asFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
asFunctionComponent<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 Inner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
||||||
|
|
||||||
return ({ fallback, name, ...props }: AsyncProps) => {
|
return ({ fallback, name, ...props }: AsyncProps) => {
|
||||||
const promise = Runtime.runPromise(runtimeRef.current)(
|
const promise = Runtime.runPromise(runtimeRef.current)(
|
||||||
@@ -38,17 +54,64 @@ export const AsyncPrototype = Object.freeze({
|
|||||||
return React.createElement(
|
return React.createElement(
|
||||||
React.Suspense,
|
React.Suspense,
|
||||||
{ fallback: fallback ?? this.defaultFallback, name },
|
{ fallback: fallback ?? this.defaultFallback, name },
|
||||||
React.createElement(SuspenseInner, { promise }),
|
React.createElement(Inner, { promise }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} as const)
|
} as const)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An equivalence function for comparing `AsyncProps` that ignores the `fallback` property.
|
||||||
|
* Used by default by async components with `Memoized.memoized` applied.
|
||||||
|
*/
|
||||||
|
export const defaultPropsEquivalence: Equivalence.Equivalence<AsyncProps> = (
|
||||||
|
self: Record<string, unknown>,
|
||||||
|
that: Record<string, unknown>,
|
||||||
|
) => {
|
||||||
|
if (self === that)
|
||||||
|
return true
|
||||||
|
|
||||||
export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, TypeId)
|
for (const key in self) {
|
||||||
|
if (key === "fallback")
|
||||||
|
continue
|
||||||
|
if (!(key in that) || !Object.is(self[key], that[key]))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in that) {
|
||||||
|
if (key === "fallback")
|
||||||
|
continue
|
||||||
|
if (!(key in self))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, AsyncTypeId)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Component into an `Async` component that supports running asynchronous effects.
|
||||||
|
*
|
||||||
|
* Note: The component cannot have a prop named "promise" as it's reserved for internal use.
|
||||||
|
*
|
||||||
|
* @param self - The component to convert to an Async component
|
||||||
|
* @returns A new `Async` component with the same body, error, and context types as the input
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const MyAsyncComponent = MyComponent.pipe(
|
||||||
|
* Async.async,
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
export const async = <T extends Component.Component<any, any, any, any>>(
|
export const async = <T extends Component.Component<any, any, any, any>>(
|
||||||
self: T
|
self: T & (
|
||||||
|
"promise" extends keyof Component.Component.Props<T>
|
||||||
|
? "The 'promise' prop name is restricted for Async components. Please rename the 'promise' prop to something else."
|
||||||
|
: T
|
||||||
|
)
|
||||||
): (
|
): (
|
||||||
& Omit<T, keyof Component.Component.AsComponent<T>>
|
& Omit<T, keyof Component.Component.AsComponent<T>>
|
||||||
& Component.Component<
|
& Component.Component<
|
||||||
@@ -59,13 +122,37 @@ export const async = <T extends Component.Component<any, any, any, any>>(
|
|||||||
>
|
>
|
||||||
& Async
|
& Async
|
||||||
) => Object.setPrototypeOf(
|
) => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, self),
|
Object.assign(function() {}, self, { propsEquivalence: defaultPropsEquivalence }),
|
||||||
Object.freeze(Object.setPrototypeOf(
|
Object.freeze(Object.setPrototypeOf(
|
||||||
Object.assign({}, AsyncPrototype),
|
Object.assign({}, AsyncPrototype),
|
||||||
Object.getPrototypeOf(self),
|
Object.getPrototypeOf(self),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies options to an Async component, returning a new Async component with the updated configuration.
|
||||||
|
*
|
||||||
|
* Supports both curried and uncurried application styles.
|
||||||
|
*
|
||||||
|
* @param self - The Async component to apply options to (in uncurried form)
|
||||||
|
* @param options - The options to apply to the component
|
||||||
|
* @returns An Async component with the applied options
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Curried
|
||||||
|
* const MyAsyncComponent = MyComponent.pipe(
|
||||||
|
* Async.async,
|
||||||
|
* Async.withOptions({ defaultFallback: <p>Loading...</p> }),
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* // Uncurried
|
||||||
|
* const MyAsyncComponent = Async.withOptions(
|
||||||
|
* Async.async(MyComponent),
|
||||||
|
* { defaultFallback: <p>Loading...</p> },
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
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<AsyncOptions>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Context, type Duration, Effect, Equivalence, ExecutionStrategy, Exit, F
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
export const ComponentTypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
||||||
export type TypeId = typeof TypeId
|
export type ComponentTypeId = typeof ComponentTypeId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an Effect-based React Component that integrates the Effect system with React.
|
* Represents an Effect-based React Component that integrates the Effect system with React.
|
||||||
@@ -13,7 +13,7 @@ export type TypeId = typeof TypeId
|
|||||||
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 ComponentPrototype<P, A, R>, ComponentOptions {
|
||||||
new(_: never): Record<string, never>
|
new(_: never): Record<string, never>
|
||||||
readonly [TypeId]: TypeId
|
readonly [ComponentTypeId]: ComponentTypeId
|
||||||
readonly "~Props": P
|
readonly "~Props": P
|
||||||
readonly "~Success": A
|
readonly "~Success": A
|
||||||
readonly "~Error": E
|
readonly "~Error": E
|
||||||
@@ -34,7 +34,7 @@ export declare namespace Component {
|
|||||||
|
|
||||||
export interface ComponentPrototype<P extends {}, A extends React.ReactNode, R>
|
export interface ComponentPrototype<P extends {}, A extends React.ReactNode, R>
|
||||||
extends Pipeable.Pipeable {
|
extends Pipeable.Pipeable {
|
||||||
readonly [TypeId]: TypeId
|
readonly [ComponentTypeId]: ComponentTypeId
|
||||||
readonly use: Effect.Effect<(props: P) => A, never, Exclude<R, Scope.Scope>>
|
readonly use: Effect.Effect<(props: P) => A, never, Exclude<R, Scope.Scope>>
|
||||||
|
|
||||||
asFunctionComponent(
|
asFunctionComponent(
|
||||||
@@ -46,7 +46,7 @@ extends Pipeable.Pipeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ComponentPrototype: ComponentPrototype<any, any, any> = Object.freeze({
|
export const ComponentPrototype: ComponentPrototype<any, any, any> = Object.freeze({
|
||||||
[TypeId]: TypeId,
|
[ComponentTypeId]: ComponentTypeId,
|
||||||
...Pipeable.Prototype,
|
...Pipeable.Prototype,
|
||||||
|
|
||||||
get use() { return use(this) },
|
get use() { return use(this) },
|
||||||
@@ -88,7 +88,7 @@ const use = Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode
|
|||||||
}),
|
}),
|
||||||
Equivalence.array(Equivalence.strict()),
|
Equivalence.array(Equivalence.strict()),
|
||||||
)))[0](Array.from(
|
)))[0](Array.from(
|
||||||
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
Context.omit(...self.nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -96,10 +96,16 @@ const use = Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode
|
|||||||
export interface ComponentOptions {
|
export interface ComponentOptions {
|
||||||
/**
|
/**
|
||||||
* Custom display name for the component in React DevTools and debugging utilities.
|
* Custom display name for the component in React DevTools and debugging utilities.
|
||||||
* Improves developer experience by providing meaningful component identification.
|
|
||||||
*/
|
*/
|
||||||
readonly displayName?: string
|
readonly displayName?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context tags that should not trigger component remount when their values change.
|
||||||
|
*
|
||||||
|
* @default [Tracer.ParentSpan]
|
||||||
|
*/
|
||||||
|
readonly nonReactiveTags: readonly Context.Tag<any, any>[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies the execution strategy for finalizers when the component unmounts or its scope closes.
|
* Specifies the execution strategy for finalizers when the component unmounts or its scope closes.
|
||||||
* Determines whether finalizers execute sequentially or in parallel.
|
* Determines whether finalizers execute sequentially or in parallel.
|
||||||
@@ -119,15 +125,13 @@ export interface ComponentOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const defaultOptions: ComponentOptions = {
|
export const defaultOptions: ComponentOptions = {
|
||||||
|
nonReactiveTags: [Tracer.ParentSpan],
|
||||||
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
||||||
finalizerExecutionDebounce: "100 millis",
|
finalizerExecutionDebounce: "100 millis",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const nonReactiveTags = [Tracer.ParentSpan] as const
|
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, ComponentTypeId)
|
||||||
|
|
||||||
|
|
||||||
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
|
||||||
|
|
||||||
export declare namespace make {
|
export declare namespace make {
|
||||||
export type Gen = {
|
export type Gen = {
|
||||||
@@ -507,7 +511,7 @@ export const makeUntraced: (
|
|||||||
* const MyComponentWithCustomOptions = MyComponent.pipe(
|
* const MyComponentWithCustomOptions = MyComponent.pipe(
|
||||||
* Component.withOptions({
|
* Component.withOptions({
|
||||||
* finalizerExecutionStrategy: ExecutionStrategy.parallel,
|
* finalizerExecutionStrategy: ExecutionStrategy.parallel,
|
||||||
* finalizerExecutionDebounce: "50 millis"
|
* finalizerExecutionDebounce: "50 millis",
|
||||||
* })
|
* })
|
||||||
* )
|
* )
|
||||||
* ```
|
* ```
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { type Cause, Context, Effect, Exit, Layer, Option, Pipeable, Predicate, PubSub, type Queue, type Scope, Supervisor } from "effect"
|
import { type Cause, Context, Effect, Exit, Layer, Option, Pipeable, Predicate, PubSub, type Queue, type Scope, Supervisor } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/ErrorObserver/ErrorObserver")
|
export const ErrorObserverTypeId: unique symbol = Symbol.for("@effect-fc/ErrorObserver/ErrorObserver")
|
||||||
export type TypeId = typeof TypeId
|
export type ErrorObserverTypeId = typeof ErrorObserverTypeId
|
||||||
|
|
||||||
export interface ErrorObserver<in out E = never> extends Pipeable.Pipeable {
|
export interface ErrorObserver<in out E = never> extends Pipeable.Pipeable {
|
||||||
readonly [TypeId]: TypeId
|
readonly [ErrorObserverTypeId]: ErrorObserverTypeId
|
||||||
handle<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
handle<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||||
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ErrorObserver = <E = never>(): Context.Tag<ErrorObserver, ErrorObserver<E>> => Context.GenericTag("@effect-fc/ErrorObserver/ErrorObserver")
|
export const ErrorObserver = <E = never>(): Context.Tag<ErrorObserver, ErrorObserver<E>> => Context.GenericTag("@effect-fc/ErrorObserver/ErrorObserver")
|
||||||
|
|
||||||
class ErrorObserverImpl<in out E = never>
|
export class ErrorObserverImpl<in out E = never>
|
||||||
extends Pipeable.Class() implements ErrorObserver<E> {
|
extends Pipeable.Class() implements ErrorObserver<E> {
|
||||||
readonly [TypeId]: TypeId = TypeId
|
readonly [ErrorObserverTypeId]: ErrorObserverTypeId = ErrorObserverTypeId
|
||||||
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -29,7 +29,7 @@ extends Pipeable.Class() implements ErrorObserver<E> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor<void> {
|
export class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor<void> {
|
||||||
readonly value = Effect.void
|
readonly value = Effect.void
|
||||||
constructor(readonly pubsub: PubSub.PubSub<Cause.Cause<never>>) {
|
constructor(readonly pubsub: PubSub.PubSub<Cause.Cause<never>>) {
|
||||||
super()
|
super()
|
||||||
@@ -43,7 +43,7 @@ class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const isErrorObserver = (u: unknown): u is ErrorObserver<unknown> => Predicate.hasProperty(u, TypeId)
|
export const isErrorObserver = (u: unknown): u is ErrorObserver<unknown> => Predicate.hasProperty(u, ErrorObserverTypeId)
|
||||||
|
|
||||||
export const layer: Layer.Layer<ErrorObserver> = Layer.unwrapEffect(Effect.map(
|
export const layer: Layer.Layer<ErrorObserver> = Layer.unwrapEffect(Effect.map(
|
||||||
PubSub.unbounded<Cause.Cause<never>>(),
|
PubSub.unbounded<Cause.Cause<never>>(),
|
||||||
|
|||||||
@@ -4,20 +4,38 @@ 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 MemoizedTypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized")
|
||||||
export type TypeId = typeof TypeId
|
export type MemoizedTypeId = typeof MemoizedTypeId
|
||||||
|
|
||||||
export interface Memoized<P> extends MemoizedOptions<P> {
|
|
||||||
readonly [TypeId]: TypeId
|
/**
|
||||||
|
* A trait for `Component`'s that uses `React.memo` to optimize re-renders based on prop equality.
|
||||||
|
*
|
||||||
|
* @template P The props type of the component
|
||||||
|
*/
|
||||||
|
export interface Memoized<P> extends MemoizedPrototype, MemoizedOptions<P> {}
|
||||||
|
|
||||||
|
export interface MemoizedPrototype {
|
||||||
|
readonly [MemoizedTypeId]: MemoizedTypeId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for Memoized components.
|
||||||
|
*
|
||||||
|
* @template P The props type of the component
|
||||||
|
*/
|
||||||
export interface MemoizedOptions<P> {
|
export interface MemoizedOptions<P> {
|
||||||
|
/**
|
||||||
|
* An optional equivalence function for comparing component props.
|
||||||
|
* If provided, this function is used by React.memo to determine if props have changed.
|
||||||
|
* Returns `true` if props are equivalent (no re-render), `false` if they differ (re-render).
|
||||||
|
*/
|
||||||
readonly propsEquivalence?: Equivalence.Equivalence<P>
|
readonly propsEquivalence?: Equivalence.Equivalence<P>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const MemoizedPrototype = Object.freeze({
|
export const MemoizedPrototype: MemoizedPrototype = Object.freeze({
|
||||||
[TypeId]: TypeId,
|
[MemoizedTypeId]: MemoizedTypeId,
|
||||||
|
|
||||||
transformFunctionComponent<P extends {}>(
|
transformFunctionComponent<P extends {}>(
|
||||||
this: Memoized<P>,
|
this: Memoized<P>,
|
||||||
@@ -28,8 +46,21 @@ export const MemoizedPrototype = Object.freeze({
|
|||||||
} as const)
|
} as const)
|
||||||
|
|
||||||
|
|
||||||
export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasProperty(u, TypeId)
|
export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasProperty(u, MemoizedTypeId)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Component into a `Memoized` component that optimizes re-renders using `React.memo`.
|
||||||
|
*
|
||||||
|
* @param self - The component to convert to a Memoized component
|
||||||
|
* @returns A new `Memoized` component with the same body, error, and context types as the input
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const MyMemoizedComponent = MyComponent.pipe(
|
||||||
|
* Memoized.memoized,
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
export const memoized = <T extends Component.Component<any, any, any, any>>(
|
export const memoized = <T extends Component.Component<any, any, any, any>>(
|
||||||
self: T
|
self: T
|
||||||
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
||||||
@@ -40,6 +71,30 @@ export const memoized = <T extends Component.Component<any, any, any, any>>(
|
|||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies options to a Memoized component, returning a new Memoized component with the updated configuration.
|
||||||
|
*
|
||||||
|
* Supports both curried and uncurried application styles.
|
||||||
|
*
|
||||||
|
* @param self - The Memoized component to apply options to (in uncurried form)
|
||||||
|
* @param options - The options to apply to the component
|
||||||
|
* @returns A Memoized component with the applied options
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Curried
|
||||||
|
* const MyMemoizedComponent = MyComponent.pipe(
|
||||||
|
* Memoized.memoized,
|
||||||
|
* Memoized.withOptions({ propsEquivalence: (a, b) => a.id === b.id }),
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* // Uncurried
|
||||||
|
* const MyMemoizedComponent = Memoized.withOptions(
|
||||||
|
* Memoized.memoized(MyComponent),
|
||||||
|
* { propsEquivalence: (a, b) => a.id === b.id },
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
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<MemoizedOptions<Component.Component.Props<T>>>
|
||||||
|
|||||||
@@ -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 KE = never, in out KR = never, 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 | KR | R>
|
||||||
readonly key: Stream.Stream<K, KE, KR>
|
readonly key: Stream.Stream<K, KE, KR>
|
||||||
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 KE = never, in out KR = never, 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, KE, KR, 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 | KR | R>,
|
||||||
readonly key: Stream.Stream<K, KE, KR>,
|
readonly key: Stream.Stream<K, KE, KR>,
|
||||||
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,
|
||||||
) {
|
) {
|
||||||
@@ -86,7 +88,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 +152,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 +174,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(
|
||||||
@@ -300,7 +287,7 @@ export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE =
|
|||||||
> {
|
> {
|
||||||
const client = yield* QueryClient.QueryClient
|
const client = yield* QueryClient.QueryClient
|
||||||
|
|
||||||
return new QueryImpl(
|
return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>(
|
||||||
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
||||||
options.key,
|
options.key,
|
||||||
options.f as any,
|
options.f as any,
|
||||||
|
|||||||
@@ -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.get(ref),
|
||||||
|
set: v => Effect.andThen(Ref.set(ref, v), PubSub.publish(pubsub, v))
|
||||||
|
})),
|
||||||
|
)),
|
||||||
|
))),
|
||||||
|
Effect.map(({ ref, pubsub, fiber }) => [
|
||||||
|
Subscribable.make({
|
||||||
|
get: Ref.get(ref),
|
||||||
|
changes: Stream.unwrapScoped(Effect.map(
|
||||||
|
Effect.all([Ref.get(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
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import { Route as ResultRouteImport } from './routes/result'
|
|||||||
import { Route as QueryRouteImport } from './routes/query'
|
import { Route as QueryRouteImport } from './routes/query'
|
||||||
import { Route as FormRouteImport } from './routes/form'
|
import { Route as FormRouteImport } from './routes/form'
|
||||||
import { Route as BlankRouteImport } from './routes/blank'
|
import { Route as BlankRouteImport } from './routes/blank'
|
||||||
|
import { Route as AsyncRouteImport } from './routes/async'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as DevMemoRouteImport } from './routes/dev/memo'
|
import { Route as DevMemoRouteImport } from './routes/dev/memo'
|
||||||
import { Route as DevContextRouteImport } from './routes/dev/context'
|
import { Route as DevContextRouteImport } from './routes/dev/context'
|
||||||
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
|
|
||||||
|
|
||||||
const ResultRoute = ResultRouteImport.update({
|
const ResultRoute = ResultRouteImport.update({
|
||||||
id: '/result',
|
id: '/result',
|
||||||
@@ -38,6 +38,11 @@ const BlankRoute = BlankRouteImport.update({
|
|||||||
path: '/blank',
|
path: '/blank',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AsyncRoute = AsyncRouteImport.update({
|
||||||
|
id: '/async',
|
||||||
|
path: '/async',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -53,40 +58,35 @@ const DevContextRoute = DevContextRouteImport.update({
|
|||||||
path: '/dev/context',
|
path: '/dev/context',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
|
|
||||||
id: '/dev/async-rendering',
|
|
||||||
path: '/dev/async-rendering',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/async': typeof AsyncRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/form': typeof FormRoute
|
'/form': typeof FormRoute
|
||||||
'/query': typeof QueryRoute
|
'/query': typeof QueryRoute
|
||||||
'/result': typeof ResultRoute
|
'/result': typeof ResultRoute
|
||||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
|
||||||
'/dev/context': typeof DevContextRoute
|
'/dev/context': typeof DevContextRoute
|
||||||
'/dev/memo': typeof DevMemoRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/async': typeof AsyncRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/form': typeof FormRoute
|
'/form': typeof FormRoute
|
||||||
'/query': typeof QueryRoute
|
'/query': typeof QueryRoute
|
||||||
'/result': typeof ResultRoute
|
'/result': typeof ResultRoute
|
||||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
|
||||||
'/dev/context': typeof DevContextRoute
|
'/dev/context': typeof DevContextRoute
|
||||||
'/dev/memo': typeof DevMemoRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/async': typeof AsyncRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/form': typeof FormRoute
|
'/form': typeof FormRoute
|
||||||
'/query': typeof QueryRoute
|
'/query': typeof QueryRoute
|
||||||
'/result': typeof ResultRoute
|
'/result': typeof ResultRoute
|
||||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
|
||||||
'/dev/context': typeof DevContextRoute
|
'/dev/context': typeof DevContextRoute
|
||||||
'/dev/memo': typeof DevMemoRoute
|
'/dev/memo': typeof DevMemoRoute
|
||||||
}
|
}
|
||||||
@@ -94,42 +94,42 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/async'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/form'
|
| '/form'
|
||||||
| '/query'
|
| '/query'
|
||||||
| '/result'
|
| '/result'
|
||||||
| '/dev/async-rendering'
|
|
||||||
| '/dev/context'
|
| '/dev/context'
|
||||||
| '/dev/memo'
|
| '/dev/memo'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/async'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/form'
|
| '/form'
|
||||||
| '/query'
|
| '/query'
|
||||||
| '/result'
|
| '/result'
|
||||||
| '/dev/async-rendering'
|
|
||||||
| '/dev/context'
|
| '/dev/context'
|
||||||
| '/dev/memo'
|
| '/dev/memo'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/async'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/form'
|
| '/form'
|
||||||
| '/query'
|
| '/query'
|
||||||
| '/result'
|
| '/result'
|
||||||
| '/dev/async-rendering'
|
|
||||||
| '/dev/context'
|
| '/dev/context'
|
||||||
| '/dev/memo'
|
| '/dev/memo'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
AsyncRoute: typeof AsyncRoute
|
||||||
BlankRoute: typeof BlankRoute
|
BlankRoute: typeof BlankRoute
|
||||||
FormRoute: typeof FormRoute
|
FormRoute: typeof FormRoute
|
||||||
QueryRoute: typeof QueryRoute
|
QueryRoute: typeof QueryRoute
|
||||||
ResultRoute: typeof ResultRoute
|
ResultRoute: typeof ResultRoute
|
||||||
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
|
|
||||||
DevContextRoute: typeof DevContextRoute
|
DevContextRoute: typeof DevContextRoute
|
||||||
DevMemoRoute: typeof DevMemoRoute
|
DevMemoRoute: typeof DevMemoRoute
|
||||||
}
|
}
|
||||||
@@ -164,6 +164,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof BlankRouteImport
|
preLoaderRoute: typeof BlankRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/async': {
|
||||||
|
id: '/async'
|
||||||
|
path: '/async'
|
||||||
|
fullPath: '/async'
|
||||||
|
preLoaderRoute: typeof AsyncRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -185,23 +192,16 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DevContextRouteImport
|
preLoaderRoute: typeof DevContextRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/dev/async-rendering': {
|
|
||||||
id: '/dev/async-rendering'
|
|
||||||
path: '/dev/async-rendering'
|
|
||||||
fullPath: '/dev/async-rendering'
|
|
||||||
preLoaderRoute: typeof DevAsyncRenderingRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
AsyncRoute: AsyncRoute,
|
||||||
BlankRoute: BlankRoute,
|
BlankRoute: BlankRoute,
|
||||||
FormRoute: FormRoute,
|
FormRoute: FormRoute,
|
||||||
QueryRoute: QueryRoute,
|
QueryRoute: QueryRoute,
|
||||||
ResultRoute: ResultRoute,
|
ResultRoute: ResultRoute,
|
||||||
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
|
|
||||||
DevContextRoute: DevContextRoute,
|
DevContextRoute: DevContextRoute,
|
||||||
DevMemoRoute: DevMemoRoute,
|
DevMemoRoute: DevMemoRoute,
|
||||||
}
|
}
|
||||||
|
|||||||
71
packages/example/src/routes/async.tsx
Normal file
71
packages/example/src/routes/async.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { HttpClient } from "@effect/platform"
|
||||||
|
import { Container, Flex, Heading, Slider, Text, TextField } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Array, Effect, flow, Option, Schema } from "effect"
|
||||||
|
import { Async, Component, Memoized } from "effect-fc"
|
||||||
|
import * as React from "react"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
|
const Post = Schema.Struct({
|
||||||
|
userId: Schema.Int,
|
||||||
|
id: Schema.Int,
|
||||||
|
title: Schema.String,
|
||||||
|
body: Schema.String,
|
||||||
|
})
|
||||||
|
|
||||||
|
interface AsyncFetchPostViewProps {
|
||||||
|
readonly id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class AsyncFetchPostView extends Component.make("AsyncFetchPostView")(function*(props: AsyncFetchPostViewProps) {
|
||||||
|
const post = yield* Component.useOnChange(() => HttpClient.HttpClient.pipe(
|
||||||
|
Effect.tap(Effect.sleep("500 millis")),
|
||||||
|
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ props.id }`)),
|
||||||
|
Effect.andThen(response => response.json),
|
||||||
|
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||||
|
), [props.id])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Heading>{post.title}</Heading>
|
||||||
|
<Text>{post.body}</Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Async.async,
|
||||||
|
Async.withOptions({ defaultFallback: <Text>Default fallback</Text> }),
|
||||||
|
Memoized.memoized,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
const AsyncRouteComponent = Component.make("AsyncRouteView")(function*() {
|
||||||
|
const [text, setText] = React.useState("Typing here should not trigger a refetch of the post")
|
||||||
|
const [id, setId] = React.useState(1)
|
||||||
|
|
||||||
|
const AsyncFetchPost = yield* AsyncFetchPostView.use
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
|
<TextField.Root
|
||||||
|
value={text}
|
||||||
|
onChange={e => setText(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Slider
|
||||||
|
value={[id]}
|
||||||
|
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AsyncFetchPost id={id} fallback={<Text>Loading post...</Text>} />
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Component.withRuntime(runtime.context)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/async")({
|
||||||
|
component: AsyncRouteComponent,
|
||||||
|
})
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { Flex, Text, TextField } from "@radix-ui/themes"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
import { Async, Component, Memoized } from "effect-fc"
|
|
||||||
import * as React from "react"
|
|
||||||
import { runtime } from "@/runtime"
|
|
||||||
|
|
||||||
|
|
||||||
// Generator version
|
|
||||||
const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
|
||||||
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent.use
|
|
||||||
const AsyncComponentFC = yield* AsyncComponent.use
|
|
||||||
const [input, setInput] = React.useState("")
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column" align="stretch" gap="2">
|
|
||||||
<TextField.Root
|
|
||||||
value={input}
|
|
||||||
onChange={e => setInput(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MemoizedAsyncComponentFC fallback={React.useMemo(() => <p>Loading memoized...</p>, [])} />
|
|
||||||
<AsyncComponentFC />
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}).pipe(
|
|
||||||
Component.withRuntime(runtime.context)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Pipeline version
|
|
||||||
// const RouteComponent = Component.make("RouteComponent")(() => Effect.Do,
|
|
||||||
// Effect.bind("VMemoizedAsyncComponent", () => Component.useFC(MemoizedAsyncComponent)),
|
|
||||||
// Effect.bind("VAsyncComponent", () => Component.useFC(AsyncComponent)),
|
|
||||||
// Effect.let("input", () => React.useState("")),
|
|
||||||
|
|
||||||
// Effect.map(({ input: [input, setInput], VAsyncComponent, VMemoizedAsyncComponent }) =>
|
|
||||||
// <Flex direction="column" align="stretch" gap="2">
|
|
||||||
// <TextField.Root
|
|
||||||
// value={input}
|
|
||||||
// onChange={e => setInput(e.target.value)}
|
|
||||||
// />
|
|
||||||
|
|
||||||
// <VMemoizedAsyncComponent />
|
|
||||||
// <VAsyncComponent />
|
|
||||||
// </Flex>
|
|
||||||
// ),
|
|
||||||
// ).pipe(
|
|
||||||
// Component.withRuntime(runtime.context)
|
|
||||||
// )
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
|
|
||||||
const SubComponentFC = yield* SubComponent.use
|
|
||||||
|
|
||||||
yield* Effect.sleep("500 millis") // Async operation
|
|
||||||
// Cannot use React hooks after the async operation
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column" align="stretch">
|
|
||||||
<Text>Rendered!</Text>
|
|
||||||
<SubComponentFC />
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}).pipe(
|
|
||||||
Async.async,
|
|
||||||
Async.withOptions({ defaultFallback: <p>Loading...</p> }),
|
|
||||||
) {}
|
|
||||||
class MemoizedAsyncComponent extends Memoized.memoized(AsyncComponent) {}
|
|
||||||
|
|
||||||
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
|
||||||
const [state] = React.useState(yield* Component.useOnMount(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
|
||||||
return <Text>{state}</Text>
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/dev/async-rendering")({
|
|
||||||
component: RouteComponent
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user