115
bun.lock
Normal file
115
bun.lock
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "@effect-docker/monorepo",
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^2.3.11",
|
||||||
|
"@effect/language-service": "^0.72.0",
|
||||||
|
"@types/bun": "^1.3.6",
|
||||||
|
"npm-check-updates": "^19.3.1",
|
||||||
|
"npm-sort": "^0.0.4",
|
||||||
|
"turbo": "^2.7.5",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages/effect-docker": {
|
||||||
|
"name": "effect-docker",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@effect/platform-browser": "^0.74.0",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"effect": "^3.19.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
|
||||||
|
|
||||||
|
"@effect/language-service": ["@effect/language-service@0.72.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-MWkyTPCXSs5Q3OIBWR3q24SA+ipkdWW7EBJBt6EPUzlzZxjJLXtLBhXpMoCFheSEM0FTWOHT4BRLh5lufsmjVw=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="],
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"effect": ["effect@3.19.15", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-vzMmgfZKLcojmUjBdlQx+uaKryO7yULlRxjpDnHdnvcp1NPHxJyoM6IOXBLlzz2I/uPtZpGKavt5hBv7IvGZkA=="],
|
||||||
|
|
||||||
|
"effect-docker": ["effect-docker@workspace:packages/effect-docker"],
|
||||||
|
|
||||||
|
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
||||||
|
|
||||||
|
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
|
||||||
|
|
||||||
|
"msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="],
|
||||||
|
|
||||||
|
"msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="],
|
||||||
|
|
||||||
|
"multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
|
||||||
|
|
||||||
|
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
||||||
|
|
||||||
|
"npm-check-updates": ["npm-check-updates@19.3.1", "", { "bin": { "npm-check-updates": "build/cli.js", "ncu": "build/cli.js" } }, "sha512-v92fHH8fmf9VVmQwwL5JWpX8GDEe8BDyrz4w3GF6D6JBUZKpQNcTfBBgxVkCcAPzVUjCHSZEXYmZAAKfLTsDBA=="],
|
||||||
|
|
||||||
|
"npm-sort": ["npm-sort@0.0.4", "", { "bin": { "npm-sort": "./index.js" } }, "sha512-S5Id/3Jvr7Cf/QnWjRteprngERCBhhEFOM+wMhUrAYP060/HUBC1aL5GoXS3xITlgacJCWaSmP4HQaAt91nNYQ=="],
|
||||||
|
|
||||||
|
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||||
|
|
||||||
|
"turbo": ["turbo@2.7.5", "", { "optionalDependencies": { "turbo-darwin-64": "2.7.5", "turbo-darwin-arm64": "2.7.5", "turbo-linux-64": "2.7.5", "turbo-linux-arm64": "2.7.5", "turbo-windows-64": "2.7.5", "turbo-windows-arm64": "2.7.5" }, "bin": { "turbo": "bin/turbo" } }, "sha512-7Imdmg37joOloTnj+DPrab9hIaQcDdJ5RwSzcauo/wMOSAgO+A/I/8b3hsGGs6PWQz70m/jkPgdqWsfNKtwwDQ=="],
|
||||||
|
|
||||||
|
"turbo-darwin-64": ["turbo-darwin-64@2.7.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-nN3wfLLj4OES/7awYyyM7fkU8U8sAFxsXau2bYJwAWi6T09jd87DgHD8N31zXaJ7LcpyppHWPRI2Ov9MuZEwnQ=="],
|
||||||
|
|
||||||
|
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.7.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wCoDHMiTf3FgLAbZHDDx/unNNonSGhsF5AbbYODbxnpYyoKDpEYacUEPjZD895vDhNvYCH0Nnk24YsP4n/cD6g=="],
|
||||||
|
|
||||||
|
"turbo-linux-64": ["turbo-linux-64@2.7.5", "", { "os": "linux", "cpu": "x64" }, "sha512-KKPvhOmJMmzWj/yjeO4LywkQ85vOJyhru7AZk/+c4B6OUh/odQ++SiIJBSbTG2lm1CuV5gV5vXZnf/2AMlu3Zg=="],
|
||||||
|
|
||||||
|
"turbo-linux-arm64": ["turbo-linux-arm64@2.7.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-8PIva4L6BQhiPikUTds9lSFSHXVDAsEvV6QUlgwPsXrtXVQMVi6Sv9p+IxtlWQFvGkdYJUgX9GnK2rC030Xcmw=="],
|
||||||
|
|
||||||
|
"turbo-windows-64": ["turbo-windows-64@2.7.5", "", { "os": "win32", "cpu": "x64" }, "sha512-rupskv/mkIUgQXzX/wUiK00mKMorQcK8yzhGFha/D5lm05FEnLx8dsip6rWzMcVpvh+4GUMA56PgtnOgpel2AA=="],
|
||||||
|
|
||||||
|
"turbo-windows-arm64": ["turbo-windows-arm64@2.7.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-G377Gxn6P42RnCzfMyDvsqQV7j69kVHKlhz9J4RhtJOB5+DyY4yYh/w0oTIxZQ4JRMmhjwLu3w9zncMoQ6nNDw=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
|
||||||
import { Effect, Function, Predicate, Runtime, Scope } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import * as Component from "./Component.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Async/Async")
|
|
||||||
export type TypeId = typeof TypeId
|
|
||||||
|
|
||||||
export interface Async extends Async.Options {
|
|
||||||
readonly [TypeId]: TypeId
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace Async {
|
|
||||||
export interface Options {
|
|
||||||
readonly defaultFallback?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Props = Omit<React.SuspenseProps, "children">
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const AsyncProto = Object.freeze({
|
|
||||||
[TypeId]: TypeId,
|
|
||||||
|
|
||||||
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
|
||||||
this: Component.Component<P, A, E, R> & Async,
|
|
||||||
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
|
||||||
) {
|
|
||||||
const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
|
||||||
|
|
||||||
return ({ fallback, name, ...props }: Async.Props) => {
|
|
||||||
const promise = Runtime.runPromise(runtimeRef.current)(
|
|
||||||
Effect.andThen(
|
|
||||||
Component.useScope([], this),
|
|
||||||
scope => Effect.provideService(this.body(props as P), Scope.Scope, scope),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
React.Suspense,
|
|
||||||
{ fallback: fallback ?? this.defaultFallback, name },
|
|
||||||
React.createElement(SuspenseInner, { promise }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} as const)
|
|
||||||
|
|
||||||
|
|
||||||
export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, TypeId)
|
|
||||||
|
|
||||||
export const async = <T extends Component.Component<any, any, any, any>>(
|
|
||||||
self: T
|
|
||||||
): (
|
|
||||||
& Omit<T, keyof Component.Component.AsComponent<T>>
|
|
||||||
& Component.Component<
|
|
||||||
Component.Component.Props<T> & Async.Props,
|
|
||||||
Component.Component.Success<T>,
|
|
||||||
Component.Component.Error<T>,
|
|
||||||
Component.Component.Context<T>
|
|
||||||
>
|
|
||||||
& Async
|
|
||||||
) => Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, self),
|
|
||||||
Object.freeze(Object.setPrototypeOf(
|
|
||||||
Object.assign({}, AsyncProto),
|
|
||||||
Object.getPrototypeOf(self),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const withOptions: {
|
|
||||||
<T extends Component.Component<any, any, any, any> & Async>(
|
|
||||||
options: Partial<Async.Options>
|
|
||||||
): (self: T) => T
|
|
||||||
<T extends Component.Component<any, any, any, any> & Async>(
|
|
||||||
self: T,
|
|
||||||
options: Partial<Async.Options>,
|
|
||||||
): T
|
|
||||||
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Async>(
|
|
||||||
self: T,
|
|
||||||
options: Partial<Async.Options>,
|
|
||||||
): T => Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, self, options),
|
|
||||||
Object.getPrototypeOf(self),
|
|
||||||
))
|
|
||||||
@@ -1,732 +0,0 @@
|
|||||||
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
|
|
||||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
|
||||||
import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Tracer, type Utils } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import { Memoized } from "./index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
|
||||||
export type TypeId = typeof TypeId
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing an Effect-based React Component.
|
|
||||||
*
|
|
||||||
* This is both:
|
|
||||||
* - an Effect that produces a React function component
|
|
||||||
* - a constructor-like object with component metadata and options
|
|
||||||
*/
|
|
||||||
export interface Component<P extends {}, A extends React.ReactNode, E, R>
|
|
||||||
extends
|
|
||||||
Effect.Effect<(props: P) => A, never, Exclude<R, Scope.Scope>>,
|
|
||||||
Component.Options
|
|
||||||
{
|
|
||||||
new(_: never): Record<string, never>
|
|
||||||
readonly [TypeId]: TypeId
|
|
||||||
readonly "~Props": P
|
|
||||||
readonly "~Success": A
|
|
||||||
readonly "~Error": E
|
|
||||||
readonly "~Context": R
|
|
||||||
|
|
||||||
readonly body: (props: P) => Effect.Effect<A, E, R>
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
makeFunctionComponent(
|
|
||||||
runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>
|
|
||||||
): (props: P) => A
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace Component {
|
|
||||||
export type Props<T extends Component<any, any, any, any>> = [T] extends [Component<infer P, infer _A, infer _E, infer _R>] ? P : never
|
|
||||||
export type Success<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer A, infer _E, infer _R>] ? A : never
|
|
||||||
export type Error<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer _A, infer E, infer _R>] ? E : never
|
|
||||||
export type Context<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer _A, infer _E, infer R>] ? R : never
|
|
||||||
|
|
||||||
export type AsComponent<T extends Component<any, any, any, any>> = Component<Props<T>, Success<T>, Error<T>, Context<T>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options that can be set on the component
|
|
||||||
*/
|
|
||||||
export interface Options {
|
|
||||||
/** Custom displayName for React DevTools and debugging. */
|
|
||||||
readonly displayName?: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strategy used when executing finalizers on unmount/scope close.
|
|
||||||
* @default ExecutionStrategy.sequential
|
|
||||||
*/
|
|
||||||
readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debounce time before executing finalizers after component unmount.
|
|
||||||
* Helps avoid unnecessary work during fast remount/remount cycles.
|
|
||||||
* @default "100 millis"
|
|
||||||
*/
|
|
||||||
readonly finalizerExecutionDebounce: Duration.DurationInput
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const ComponentProto = Object.freeze({
|
|
||||||
...Effectable.CommitPrototype,
|
|
||||||
[TypeId]: TypeId,
|
|
||||||
|
|
||||||
commit: Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode, E, R>(
|
|
||||||
this: Component<P, A, E, R>
|
|
||||||
) {
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: React ref initialization
|
|
||||||
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
|
|
||||||
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
return yield* React.useState(() => Runtime.runSync(runtimeRef.current)(Effect.cachedFunction(
|
|
||||||
(_services: readonly any[]) => Effect.sync(() => {
|
|
||||||
const f: React.FC<P> = this.makeFunctionComponent(runtimeRef)
|
|
||||||
f.displayName = this.displayName ?? "Anonymous"
|
|
||||||
return Memoized.isMemoized(this)
|
|
||||||
? React.memo(f, this.propsAreEqual)
|
|
||||||
: f
|
|
||||||
}),
|
|
||||||
Equivalence.array(Equivalence.strict()),
|
|
||||||
)))[0](Array.from(
|
|
||||||
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
|
|
||||||
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
|
||||||
this: Component<P, A, E, R>,
|
|
||||||
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
|
||||||
) {
|
|
||||||
return (props: P) => Runtime.runSync(runtimeRef.current)(
|
|
||||||
Effect.andThen(
|
|
||||||
useScope([], this),
|
|
||||||
scope => Effect.provideService(this.body(props), Scope.Scope, scope),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
} as const)
|
|
||||||
|
|
||||||
const defaultOptions: Component.Options = {
|
|
||||||
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
|
||||||
finalizerExecutionDebounce: "100 millis",
|
|
||||||
}
|
|
||||||
|
|
||||||
const nonReactiveTags = [Tracer.ParentSpan] as const
|
|
||||||
|
|
||||||
|
|
||||||
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
|
||||||
|
|
||||||
export declare namespace make {
|
|
||||||
export type Gen = {
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A extends React.ReactNode, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>
|
|
||||||
): Component<
|
|
||||||
P, A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<B>>, Effect.Effect.Error<B>, Effect.Effect.Context<B>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<C>>, Effect.Effect.Error<C>, Effect.Effect.Context<C>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<D>>, Effect.Effect.Error<D>, Effect.Effect.Context<D>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<E>>, Effect.Effect.Error<E>, Effect.Effect.Context<E>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<F>>, Effect.Effect.Error<F>, Effect.Effect.Context<F>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<G>>, Effect.Effect.Error<G>, Effect.Effect.Context<G>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<H>>, Effect.Effect.Error<H>, Effect.Effect.Context<H>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
|
||||||
h: (_: H, props: NoInfer<P>) => I,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<I>>, Effect.Effect.Error<I>, Effect.Effect.Context<I>>
|
|
||||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I, J extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Generator<Eff, A, never>,
|
|
||||||
a: (
|
|
||||||
_: Effect.Effect<
|
|
||||||
A,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
|
|
||||||
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
|
|
||||||
>,
|
|
||||||
props: NoInfer<P>,
|
|
||||||
) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
|
||||||
h: (_: H, props: NoInfer<P>) => I,
|
|
||||||
i: (_: I, props: NoInfer<P>) => J,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<J>>, Effect.Effect.Error<J>, Effect.Effect.Context<J>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NonGen = {
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
|
|
||||||
body: (props: P) => Eff
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
|
||||||
h: (_: H, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, I, P extends {} = {}>(
|
|
||||||
body: (props: P) => A,
|
|
||||||
a: (_: A, props: NoInfer<P>) => B,
|
|
||||||
b: (_: B, props: NoInfer<P>) => C,
|
|
||||||
c: (_: C, props: NoInfer<P>) => D,
|
|
||||||
d: (_: D, props: NoInfer<P>) => E,
|
|
||||||
e: (_: E, props: NoInfer<P>) => F,
|
|
||||||
f: (_: F, props: NoInfer<P>) => G,
|
|
||||||
g: (_: G, props: NoInfer<P>) => H,
|
|
||||||
h: (_: H, props: NoInfer<P>) => I,
|
|
||||||
i: (_: I, props: NoInfer<P>) => Eff,
|
|
||||||
): Component<P, Effect.Effect.Success<Effect.Effect.AsEffect<Eff>>, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an Effect-FC Component following the same overloads and pipeline style as `Effect.fn`.
|
|
||||||
*
|
|
||||||
* This is the **recommended** way to define components. It supports:
|
|
||||||
* - Generator syntax (yield* style) — most ergonomic and readable
|
|
||||||
* - Direct Effect return (non-generator)
|
|
||||||
* - Chained transformation functions (like Effect.fn pipelines)
|
|
||||||
* - Optional tracing span with automatic `displayName`
|
|
||||||
*
|
|
||||||
* When you provide a `spanName` as the first argument, two things happen automatically:
|
|
||||||
* 1. A tracing span is created with that name (unless using `makeUntraced`)
|
|
||||||
* 2. The resulting React component gets `displayName = spanName`
|
|
||||||
*/
|
|
||||||
export const make: (
|
|
||||||
& make.Gen
|
|
||||||
& make.NonGen
|
|
||||||
& ((
|
|
||||||
spanName: string,
|
|
||||||
spanOptions?: Tracer.SpanOptions,
|
|
||||||
) => make.Gen & make.NonGen)
|
|
||||||
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => {
|
|
||||||
if (typeof spanNameOrBody !== "string") {
|
|
||||||
return Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, defaultOptions, {
|
|
||||||
body: Effect.fn(spanNameOrBody as any, ...pipeables),
|
|
||||||
}),
|
|
||||||
ComponentProto,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const spanOptions = pipeables[0]
|
|
||||||
return (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, defaultOptions, {
|
|
||||||
body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []),
|
|
||||||
displayName: spanNameOrBody,
|
|
||||||
}),
|
|
||||||
ComponentProto,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as `make`, but creates an **untraced** version — no automatic tracing span is created.
|
|
||||||
*
|
|
||||||
* Follows the exact same API shape as `Effect.fnUntraced`.
|
|
||||||
* Useful for:
|
|
||||||
* - Components where you want full manual control over tracing
|
|
||||||
* - Avoiding span noise in deeply nested UI
|
|
||||||
*
|
|
||||||
* When a string is provided as first argument, it is **only** used as the React component's `displayName`
|
|
||||||
* (no tracing span is created).
|
|
||||||
*/
|
|
||||||
export const makeUntraced: (
|
|
||||||
& make.Gen
|
|
||||||
& make.NonGen
|
|
||||||
& ((name: string) => make.Gen & make.NonGen)
|
|
||||||
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => (
|
|
||||||
typeof spanNameOrBody !== "string"
|
|
||||||
? Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, defaultOptions, {
|
|
||||||
body: Effect.fnUntraced(spanNameOrBody as any, ...pipeables as []),
|
|
||||||
}),
|
|
||||||
ComponentProto,
|
|
||||||
)
|
|
||||||
: (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, defaultOptions, {
|
|
||||||
body: Effect.fnUntraced(body, ...pipeables as []),
|
|
||||||
displayName: spanNameOrBody,
|
|
||||||
}),
|
|
||||||
ComponentProto,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new component with modified options while preserving original behavior.
|
|
||||||
*/
|
|
||||||
export const withOptions: {
|
|
||||||
<T extends Component<any, any, any, any>>(
|
|
||||||
options: Partial<Component.Options>
|
|
||||||
): (self: T) => T
|
|
||||||
<T extends Component<any, any, any, any>>(
|
|
||||||
self: T,
|
|
||||||
options: Partial<Component.Options>,
|
|
||||||
): T
|
|
||||||
} = Function.dual(2, <T extends Component<any, any, any, any>>(
|
|
||||||
self: T,
|
|
||||||
options: Partial<Component.Options>,
|
|
||||||
): T => Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, self, options),
|
|
||||||
Object.getPrototypeOf(self),
|
|
||||||
))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps an Effect-FC `Component` and turns it into a regular React function component
|
|
||||||
* that serves as an **entrypoint** into an Effect-FC component hierarchy.
|
|
||||||
*
|
|
||||||
* This is the recommended way to connect Effect-FC components to the rest of your React app,
|
|
||||||
* especially when using routers (TanStack Router, React Router, etc.), lazy-loaded routes,
|
|
||||||
* or any place where a standard React component is expected.
|
|
||||||
*
|
|
||||||
* The runtime is obtained from the provided React Context, allowing you to:
|
|
||||||
* - Provide dependencies once at a high level
|
|
||||||
* - Use the same runtime across an entire route tree or feature
|
|
||||||
*
|
|
||||||
* @example Using TanStack Router
|
|
||||||
* ```tsx
|
|
||||||
* // Main
|
|
||||||
* export const runtime = ReactRuntime.make(Layer.empty)
|
|
||||||
* function App() {
|
|
||||||
* return (
|
|
||||||
* <ReactRuntime.Provider runtime={runtime}>
|
|
||||||
* <RouterProvider router={router} />
|
|
||||||
* </ReactRuntime.Provider>
|
|
||||||
* )
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // Route
|
|
||||||
* export const Route = createFileRoute("/")({
|
|
||||||
* component: Component.withRuntime(HomePage, runtime.context)
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param self - The Effect-FC Component you want to render as a regular React component.
|
|
||||||
* @param context - React Context that holds the Runtime to use for this component tree. See the `ReactRuntime` module to create one.
|
|
||||||
*/
|
|
||||||
export const withRuntime: {
|
|
||||||
<P extends {}, A extends React.ReactNode, E, R>(
|
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
|
||||||
): (self: Component<P, A, E, Scope.Scope | NoInfer<R>>) => (props: P) => A
|
|
||||||
<P extends {}, A extends React.ReactNode, E, R>(
|
|
||||||
self: Component<P, A, E, Scope.Scope | NoInfer<R>>,
|
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
|
||||||
): (props: P) => A
|
|
||||||
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R>(
|
|
||||||
self: Component<P, A, E, R>,
|
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
|
||||||
) => function WithRuntime(props: P) {
|
|
||||||
return React.createElement(
|
|
||||||
Runtime.runSync(React.useContext(context))(self),
|
|
||||||
props,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service that keeps track of scopes associated with React components
|
|
||||||
* (used internally by the `useScope` hook).
|
|
||||||
*/
|
|
||||||
export class ScopeMap extends Effect.Service<ScopeMap>()("@effect-fc/Component/ScopeMap", {
|
|
||||||
effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<object, ScopeMap.Entry>()))
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export declare namespace ScopeMap {
|
|
||||||
export interface Entry {
|
|
||||||
readonly scope: Scope.CloseableScope
|
|
||||||
readonly closeFiber: Option.Option<Fiber.RuntimeFiber<void>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export declare namespace useScope {
|
|
||||||
export interface Options {
|
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
|
||||||
readonly finalizerExecutionDebounce?: Duration.DurationInput
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook that creates and manages a `Scope` for the current component instance.
|
|
||||||
*
|
|
||||||
* Automatically closes the scope whenever `deps` changes or the component unmounts.
|
|
||||||
*
|
|
||||||
* @param deps - dependency array like in `React.useEffect`
|
|
||||||
* @param options - finalizer execution control
|
|
||||||
*/
|
|
||||||
export const useScope = Effect.fnUntraced(function*(
|
|
||||||
deps: React.DependencyList,
|
|
||||||
options?: useScope.Options,
|
|
||||||
): Effect.fn.Return<Scope.Scope> {
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
|
||||||
const runtimeRef = React.useRef<Runtime.Runtime<never>>(null!)
|
|
||||||
runtimeRef.current = yield* Effect.runtime()
|
|
||||||
|
|
||||||
const { key, scope } = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.Do.pipe(
|
|
||||||
Effect.bind("scopeMapRef", () => Effect.map(
|
|
||||||
ScopeMap as unknown as Effect.Effect<ScopeMap>,
|
|
||||||
scopeMap => scopeMap.ref,
|
|
||||||
)),
|
|
||||||
Effect.let("key", () => ({})),
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)),
|
|
||||||
Effect.tap(({ scopeMapRef, key, scope }) =>
|
|
||||||
Ref.update(scopeMapRef, HashMap.set(key, {
|
|
||||||
scope,
|
|
||||||
closeFiber: Option.none(),
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
|
||||||
)), deps)
|
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "key"
|
|
||||||
React.useEffect(() => Runtime.runSync(runtimeRef.current)((ScopeMap as unknown as Effect.Effect<ScopeMap>).pipe(
|
|
||||||
Effect.map(scopeMap => scopeMap.ref),
|
|
||||||
Effect.tap(ref => ref.pipe(
|
|
||||||
Effect.andThen(HashMap.get(key)),
|
|
||||||
Effect.andThen(entry => Option.match(entry.closeFiber, {
|
|
||||||
onSome: Fiber.interruptFork,
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
})),
|
|
||||||
)),
|
|
||||||
Effect.map(ref =>
|
|
||||||
() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
|
|
||||||
Effect.sleep(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce).pipe(
|
|
||||||
Effect.andThen(Scope.close(scope, Exit.void)),
|
|
||||||
Effect.onExit(() => Ref.update(ref, HashMap.remove(key))),
|
|
||||||
Effect.forkDaemon,
|
|
||||||
),
|
|
||||||
fiber => Ref.update(ref, HashMap.set(key, {
|
|
||||||
scope,
|
|
||||||
closeFiber: Option.some(fiber),
|
|
||||||
})),
|
|
||||||
))
|
|
||||||
),
|
|
||||||
)), [key])
|
|
||||||
|
|
||||||
return scope
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs an effect and returns its result only once on component mount.
|
|
||||||
*/
|
|
||||||
export const useOnMount = Effect.fnUntraced(function* <A, E, R>(
|
|
||||||
f: () => Effect.Effect<A, E, R>
|
|
||||||
): Effect.fn.Return<A, E, R> {
|
|
||||||
const runtime = yield* Effect.runtime<R>()
|
|
||||||
return yield* React.useState(() => Runtime.runSync(runtime)(Effect.cached(f())))[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
export declare namespace useOnChange {
|
|
||||||
export interface Options extends useScope.Options {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs an effect and returns its result whenever dependencies change.
|
|
||||||
*
|
|
||||||
* Provides its own `Scope` which closes whenever `deps` changes or the component unmounts.
|
|
||||||
*/
|
|
||||||
export const useOnChange = Effect.fnUntraced(function* <A, E, R>(
|
|
||||||
f: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
options?: useOnChange.Options,
|
|
||||||
): Effect.fn.Return<A, E, Exclude<R, Scope.Scope>> {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
const scope = yield* useScope(deps, options)
|
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "scope"
|
|
||||||
return yield* React.useMemo(() => Runtime.runSync(runtime)(
|
|
||||||
Effect.cached(Effect.provideService(f(), Scope.Scope, scope))
|
|
||||||
), [scope])
|
|
||||||
})
|
|
||||||
|
|
||||||
export declare namespace useReactEffect {
|
|
||||||
export interface Options {
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like `React.useEffect` but accepts an effect.
|
|
||||||
*
|
|
||||||
* Cleanup logic is handled through the `Scope` API rather than using imperative cleanup.
|
|
||||||
*/
|
|
||||||
export const useReactEffect = Effect.fnUntraced(function* <E, R>(
|
|
||||||
f: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: useReactEffect.Options,
|
|
||||||
): Effect.fn.Return<void, never, Exclude<R, Scope.Scope>> {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
|
||||||
React.useEffect(() => runReactEffect(runtime, f, options), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
const runReactEffect = <E, R>(
|
|
||||||
runtime: Runtime.Runtime<Exclude<R, Scope.Scope>>,
|
|
||||||
f: () => Effect.Effect<void, E, R>,
|
|
||||||
options?: useReactEffect.Options,
|
|
||||||
) => Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)),
|
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(f(), Scope.Scope, scope))),
|
|
||||||
Effect.map(({ scope }) =>
|
|
||||||
() => {
|
|
||||||
switch (options?.finalizerExecutionMode ?? "fork") {
|
|
||||||
case "sync":
|
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
)
|
|
||||||
|
|
||||||
export declare namespace useReactLayoutEffect {
|
|
||||||
export interface Options extends useReactEffect.Options {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like `React.useReactLayoutEffect` but accepts an effect.
|
|
||||||
*
|
|
||||||
* Cleanup logic is handled through the `Scope` API rather than using imperative cleanup.
|
|
||||||
*/
|
|
||||||
export const useReactLayoutEffect = Effect.fnUntraced(function* <E, R>(
|
|
||||||
f: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: useReactLayoutEffect.Options,
|
|
||||||
): Effect.fn.Return<void, never, Exclude<R, Scope.Scope>> {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
|
||||||
React.useLayoutEffect(() => runReactEffect(runtime, f, options), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a synchronous run function for the current runtime context.
|
|
||||||
*/
|
|
||||||
export const useRunSync = <R = never>(): Effect.Effect<
|
|
||||||
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => A,
|
|
||||||
never,
|
|
||||||
Scope.Scope | R
|
|
||||||
> => Effect.andThen(Effect.runtime(), Runtime.runSync)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a Promise-based run function for the current runtime context.
|
|
||||||
*/
|
|
||||||
export const useRunPromise = <R = never>(): Effect.Effect<
|
|
||||||
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => Promise<A>,
|
|
||||||
never,
|
|
||||||
Scope.Scope | R
|
|
||||||
> => Effect.andThen(Effect.runtime(), context => Runtime.runPromise(context))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns a function returning an effect into a memoized synchronous function.
|
|
||||||
*/
|
|
||||||
export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
|
||||||
f: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.fn.Return<(...args: Args) => A, never, R> {
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
|
||||||
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
|
|
||||||
runtimeRef.current = yield* Effect.runtime<R>()
|
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
|
||||||
return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(f(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns a function returning an effect into a memoized Promise-based asynchronous function.
|
|
||||||
*/
|
|
||||||
export const useCallbackPromise = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
|
||||||
f: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.fn.Return<(...args: Args) => Promise<A>, never, R> {
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
|
||||||
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
|
|
||||||
runtimeRef.current = yield* Effect.runtime<R>()
|
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
|
||||||
return React.useCallback((...args: Args) => Runtime.runPromise(runtimeRef.current)(f(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export declare namespace useContext {
|
|
||||||
export interface Options extends useOnChange.Options {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook that constructs a layer and returns the created context.
|
|
||||||
*
|
|
||||||
* The layer gets reconstructed everytime `layer` changes, so make sure its value is stable.
|
|
||||||
*
|
|
||||||
* Building a layer containing asynchronous effects require the component calling this hook to be made async using `Async.async`.
|
|
||||||
*/
|
|
||||||
export const useContext = <ROut, E, RIn>(
|
|
||||||
layer: Layer.Layer<ROut, E, RIn>,
|
|
||||||
options?: useContext.Options,
|
|
||||||
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>> => useOnChange(() => Effect.context<RIn>().pipe(
|
|
||||||
Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))),
|
|
||||||
Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
|
|
||||||
Effect.andThen(runtime => runtime.runtimeEffect),
|
|
||||||
Effect.andThen(runtime => runtime.context),
|
|
||||||
), [layer], options)
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { type Cause, Context, Effect, Exit, Layer, Option, Pipeable, Predicate, PubSub, type Queue, type Scope, Supervisor } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/ErrorObserver/ErrorObserver")
|
|
||||||
export type TypeId = typeof TypeId
|
|
||||||
|
|
||||||
export interface ErrorObserver<in out E = never> extends Pipeable.Pipeable {
|
|
||||||
readonly [TypeId]: TypeId
|
|
||||||
handle<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
|
||||||
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErrorObserver = <E = never>(): Context.Tag<ErrorObserver, ErrorObserver<E>> => Context.GenericTag("@effect-fc/ErrorObserver/ErrorObserver")
|
|
||||||
|
|
||||||
class ErrorObserverImpl<in out E = never>
|
|
||||||
extends Pipeable.Class() implements ErrorObserver<E> {
|
|
||||||
readonly [TypeId]: TypeId = TypeId
|
|
||||||
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly pubsub: PubSub.PubSub<Cause.Cause<E>>
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this.subscribe = pubsub.subscribe
|
|
||||||
}
|
|
||||||
|
|
||||||
handle<A, EffE, R>(effect: Effect.Effect<A, EffE, R>): Effect.Effect<A, EffE, R> {
|
|
||||||
return Effect.tapErrorCause(effect, cause => PubSub.publish(this.pubsub, cause as Cause.Cause<E>))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor<void> {
|
|
||||||
readonly value = Effect.void
|
|
||||||
constructor(readonly pubsub: PubSub.PubSub<Cause.Cause<never>>) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnd<A, E>(_value: Exit.Exit<A, E>): void {
|
|
||||||
if (Exit.isFailure(_value)) {
|
|
||||||
Effect.runSync(PubSub.publish(this.pubsub, _value.cause as Cause.Cause<never>))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const isErrorObserver = (u: unknown): u is ErrorObserver<unknown> => Predicate.hasProperty(u, TypeId)
|
|
||||||
|
|
||||||
export const layer: Layer.Layer<ErrorObserver> = Layer.unwrapEffect(Effect.map(
|
|
||||||
PubSub.unbounded<Cause.Cause<never>>(),
|
|
||||||
pubsub => Layer.merge(
|
|
||||||
Supervisor.addSupervisor(new ErrorObserverSupervisorImpl(pubsub)),
|
|
||||||
Layer.succeed(ErrorObserver(), new ErrorObserverImpl(pubsub)),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
|
|
||||||
export const handle = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => Effect.andThen(
|
|
||||||
Effect.serviceOption(ErrorObserver()),
|
|
||||||
Option.match({
|
|
||||||
onSome: observer => observer.handle(effect),
|
|
||||||
onNone: () => effect,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, flow, Hash, HashMap, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect"
|
|
||||||
import type * as React from "react"
|
|
||||||
import * as Component from "./Component.js"
|
|
||||||
import * as Mutation from "./Mutation.js"
|
|
||||||
import * as PropertyPath from "./PropertyPath.js"
|
|
||||||
import * as Result from "./Result.js"
|
|
||||||
import * as Subscribable from "./Subscribable.js"
|
|
||||||
import * as SubscriptionRef from "./SubscriptionRef.js"
|
|
||||||
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
|
|
||||||
export type FormTypeId = typeof FormTypeId
|
|
||||||
|
|
||||||
export interface Form<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
|
||||||
extends Pipeable.Pipeable {
|
|
||||||
readonly [FormTypeId]: FormTypeId
|
|
||||||
|
|
||||||
readonly schema: Schema.Schema<A, I, R>
|
|
||||||
readonly context: Context.Context<Scope.Scope | R>
|
|
||||||
readonly mutation: Mutation.Mutation<
|
|
||||||
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
|
|
||||||
MA, ME, MR, MP
|
|
||||||
>
|
|
||||||
readonly autosubmit: boolean
|
|
||||||
readonly debounce: Option.Option<Duration.DurationInput>
|
|
||||||
|
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>>
|
|
||||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
|
||||||
readonly error: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
|
|
||||||
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>
|
|
||||||
|
|
||||||
readonly canSubmit: Subscribable.Subscribable<boolean>
|
|
||||||
|
|
||||||
field<const P extends PropertyPath.Paths<I>>(
|
|
||||||
path: P
|
|
||||||
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>>
|
|
||||||
|
|
||||||
readonly run: Effect.Effect<void>
|
|
||||||
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FormImpl<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
|
||||||
extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|
||||||
readonly [FormTypeId]: FormTypeId = FormTypeId
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly schema: Schema.Schema<A, I, R>,
|
|
||||||
readonly context: Context.Context<Scope.Scope | R>,
|
|
||||||
readonly mutation: Mutation.Mutation<
|
|
||||||
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
|
|
||||||
MA, ME, MR, MP
|
|
||||||
>,
|
|
||||||
readonly autosubmit: boolean,
|
|
||||||
readonly debounce: Option.Option<Duration.DurationInput>,
|
|
||||||
|
|
||||||
readonly value: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
|
||||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
|
||||||
readonly error: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
|
||||||
readonly validationFiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>,
|
|
||||||
|
|
||||||
readonly runSemaphore: Effect.Semaphore,
|
|
||||||
readonly fieldCache: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
|
|
||||||
this.canSubmit = Subscribable.map(
|
|
||||||
Subscribable.zipLatestAll(this.value, this.error, this.validationFiber, this.mutation.result),
|
|
||||||
([value, error, validationFiber, result]) => (
|
|
||||||
Option.isSome(value) &&
|
|
||||||
Option.isNone(error) &&
|
|
||||||
Option.isNone(validationFiber) &&
|
|
||||||
!(Result.isRunning(result) || Result.hasRefreshingFlag(result))
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
field<const P extends PropertyPath.Paths<I>>(
|
|
||||||
path: P
|
|
||||||
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>> {
|
|
||||||
const key = new FormFieldKey(path)
|
|
||||||
return this.fieldCache.pipe(
|
|
||||||
Effect.map(HashMap.get(key)),
|
|
||||||
Effect.flatMap(Option.match({
|
|
||||||
onSome: v => Effect.succeed(v as FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>),
|
|
||||||
onNone: () => Effect.tap(
|
|
||||||
Effect.succeed(makeFormField(this as Form<A, I, R, MA, ME, MR, MP>, path)),
|
|
||||||
v => Ref.update(this.fieldCache, HashMap.set(key, v as FormField<unknown, unknown>)),
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly canSubmit: Subscribable.Subscribable<boolean>
|
|
||||||
|
|
||||||
get run(): Effect.Effect<void> {
|
|
||||||
return this.runSemaphore.withPermits(1)(Stream.runForEach(
|
|
||||||
this.encodedValue.changes.pipe(
|
|
||||||
Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity
|
|
||||||
),
|
|
||||||
|
|
||||||
encodedValue => this.validationFiber.pipe(
|
|
||||||
Effect.andThen(Option.match({
|
|
||||||
onSome: Fiber.interrupt,
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
})),
|
|
||||||
Effect.andThen(
|
|
||||||
Effect.forkScoped(Effect.onExit(
|
|
||||||
Schema.decode(this.schema, { errors: "all" })(encodedValue),
|
|
||||||
exit => Effect.andThen(
|
|
||||||
Exit.matchEffect(exit, {
|
|
||||||
onSuccess: v => Effect.andThen(
|
|
||||||
Ref.set(this.value, Option.some(v)),
|
|
||||||
Ref.set(this.error, Option.none()),
|
|
||||||
),
|
|
||||||
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
|
||||||
onSome: e => Ref.set(this.error, Option.some(e)),
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
Ref.set(this.validationFiber, Option.none()),
|
|
||||||
),
|
|
||||||
)).pipe(
|
|
||||||
Effect.tap(fiber => Ref.set(this.validationFiber, Option.some(fiber))),
|
|
||||||
Effect.andThen(Fiber.join),
|
|
||||||
Effect.andThen(value => this.autosubmit
|
|
||||||
? Effect.asVoid(Effect.forkScoped(this.submitValue(value)))
|
|
||||||
: Effect.void
|
|
||||||
),
|
|
||||||
Effect.forkScoped,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Effect.provide(this.context),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
|
|
||||||
return this.value.pipe(
|
|
||||||
Effect.andThen(identity),
|
|
||||||
Effect.andThen(value => this.submitValue(value)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
submitValue(value: A): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>> {
|
|
||||||
return Effect.whenEffect(
|
|
||||||
Effect.tap(
|
|
||||||
this.mutation.mutate([value, this as any]),
|
|
||||||
result => Result.isFailure(result)
|
|
||||||
? Option.match(
|
|
||||||
Chunk.findFirst(
|
|
||||||
Cause.failures(result.cause as Cause.Cause<ParseResult.ParseError>),
|
|
||||||
e => e._tag === "ParseError",
|
|
||||||
),
|
|
||||||
{
|
|
||||||
onSome: e => Ref.set(this.error, Option.some(e)),
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: Effect.void
|
|
||||||
),
|
|
||||||
this.canSubmit.get,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isForm = (u: unknown): u is Form<unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
|
|
||||||
|
|
||||||
export declare namespace make {
|
|
||||||
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
|
||||||
extends Mutation.make.Options<
|
|
||||||
readonly [value: NoInfer<A>, form: Form<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>],
|
|
||||||
MA, ME, MR, MP
|
|
||||||
> {
|
|
||||||
readonly schema: Schema.Schema<A, I, R>
|
|
||||||
readonly initialEncodedValue: NoInfer<I>
|
|
||||||
readonly autosubmit?: boolean
|
|
||||||
readonly debounce?: Duration.DurationInput
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
|
|
||||||
options: make.Options<A, I, R, MA, ME, MR, MP>
|
|
||||||
): Effect.fn.Return<
|
|
||||||
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
|
|
||||||
never,
|
|
||||||
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
|
||||||
> {
|
|
||||||
return new FormImpl(
|
|
||||||
options.schema,
|
|
||||||
yield* Effect.context<Scope.Scope | R>(),
|
|
||||||
yield* Mutation.make(options),
|
|
||||||
options.autosubmit ?? false,
|
|
||||||
Option.fromNullable(options.debounce),
|
|
||||||
|
|
||||||
yield* SubscriptionRef.make(Option.none<A>()),
|
|
||||||
yield* SubscriptionRef.make(options.initialEncodedValue),
|
|
||||||
yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()),
|
|
||||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()),
|
|
||||||
|
|
||||||
yield* Effect.makeSemaphore(1),
|
|
||||||
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export declare namespace service {
|
|
||||||
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
|
||||||
extends make.Options<A, I, R, MA, ME, MR, MP> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const service = <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
|
|
||||||
options: service.Options<A, I, R, MA, ME, MR, MP>
|
|
||||||
): Effect.Effect<
|
|
||||||
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
|
|
||||||
never,
|
|
||||||
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
|
||||||
> => Effect.tap(
|
|
||||||
make(options),
|
|
||||||
form => Effect.forkScoped(form.run),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
export const FormFieldTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormField")
|
|
||||||
export type FormFieldTypeId = typeof FormFieldTypeId
|
|
||||||
|
|
||||||
export interface FormField<in out A, in out I = A>
|
|
||||||
extends Pipeable.Pipeable {
|
|
||||||
readonly [FormFieldTypeId]: FormFieldTypeId
|
|
||||||
|
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>
|
|
||||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
|
||||||
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
|
||||||
readonly isValidating: Subscribable.Subscribable<boolean>
|
|
||||||
readonly isSubmitting: Subscribable.Subscribable<boolean>
|
|
||||||
}
|
|
||||||
|
|
||||||
class FormFieldImpl<in out A, in out I = A>
|
|
||||||
extends Pipeable.Class() implements FormField<A, I> {
|
|
||||||
readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>,
|
|
||||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
|
||||||
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
|
|
||||||
readonly isValidating: Subscribable.Subscribable<boolean>,
|
|
||||||
readonly isSubmitting: Subscribable.Subscribable<boolean>,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormFieldKeyTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormFieldKey")
|
|
||||||
type FormFieldKeyTypeId = typeof FormFieldKeyTypeId
|
|
||||||
|
|
||||||
class FormFieldKey implements Equal.Equal {
|
|
||||||
readonly [FormFieldKeyTypeId]: FormFieldKeyTypeId = FormFieldKeyTypeId
|
|
||||||
constructor(readonly path: PropertyPath.PropertyPath) {}
|
|
||||||
|
|
||||||
[Equal.symbol](that: Equal.Equal) {
|
|
||||||
return isFormFieldKey(that) && PropertyPath.equivalence(this.path, that.path)
|
|
||||||
}
|
|
||||||
[Hash.symbol]() {
|
|
||||||
return Hash.array(this.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isFormField = (u: unknown): u is FormField<unknown, unknown> => Predicate.hasProperty(u, FormFieldTypeId)
|
|
||||||
const isFormFieldKey = (u: unknown): u is FormFieldKey => Predicate.hasProperty(u, FormFieldKeyTypeId)
|
|
||||||
|
|
||||||
export const makeFormField = <A, I, R, MA, ME, MR, MP, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
|
||||||
self: Form<A, I, R, MA, ME, MR, MP>,
|
|
||||||
path: P,
|
|
||||||
): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => {
|
|
||||||
return new FormFieldImpl(
|
|
||||||
Subscribable.mapEffect(self.value, Option.match({
|
|
||||||
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
|
|
||||||
onNone: () => Option.some(Option.none()),
|
|
||||||
})),
|
|
||||||
SubscriptionSubRef.makeFromPath(self.encodedValue, path),
|
|
||||||
Subscribable.mapEffect(self.error, Option.match({
|
|
||||||
onSome: flow(
|
|
||||||
ParseResult.ArrayFormatter.formatError,
|
|
||||||
Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))),
|
|
||||||
),
|
|
||||||
onNone: () => Effect.succeed([]),
|
|
||||||
})),
|
|
||||||
Subscribable.map(self.validationFiber, Option.isSome),
|
|
||||||
Subscribable.map(self.mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export namespace useInput {
|
|
||||||
export interface Options {
|
|
||||||
readonly debounce?: Duration.DurationInput
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Success<T> {
|
|
||||||
readonly value: T
|
|
||||||
readonly setValue: React.Dispatch<React.SetStateAction<T>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useInput = Effect.fnUntraced(function* <A, I>(
|
|
||||||
field: FormField<A, I>,
|
|
||||||
options?: useInput.Options,
|
|
||||||
): Effect.fn.Return<useInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
|
|
||||||
const internalValueRef = yield* Component.useOnChange(() => Effect.tap(
|
|
||||||
Effect.andThen(field.encodedValue, SubscriptionRef.make),
|
|
||||||
internalValueRef => Effect.forkScoped(Effect.all([
|
|
||||||
Stream.runForEach(
|
|
||||||
Stream.drop(field.encodedValue, 1),
|
|
||||||
upstreamEncodedValue => Effect.whenEffect(
|
|
||||||
Ref.set(internalValueRef, upstreamEncodedValue),
|
|
||||||
Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Stream.runForEach(
|
|
||||||
internalValueRef.changes.pipe(
|
|
||||||
Stream.drop(1),
|
|
||||||
Stream.changesWith(Equal.equivalence()),
|
|
||||||
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
|
||||||
),
|
|
||||||
internalValue => Ref.set(field.encodedValue, internalValue),
|
|
||||||
),
|
|
||||||
], { concurrency: "unbounded" })),
|
|
||||||
), [field, options?.debounce])
|
|
||||||
|
|
||||||
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef)
|
|
||||||
return { value, setValue }
|
|
||||||
})
|
|
||||||
|
|
||||||
export namespace useOptionalInput {
|
|
||||||
export interface Options<T> extends useInput.Options {
|
|
||||||
readonly defaultValue: T
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Success<T> extends useInput.Success<T> {
|
|
||||||
readonly enabled: boolean
|
|
||||||
readonly setEnabled: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
|
|
||||||
field: FormField<A, Option.Option<I>>,
|
|
||||||
options: useOptionalInput.Options<I>,
|
|
||||||
): Effect.fn.Return<useOptionalInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
|
|
||||||
const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap(
|
|
||||||
Effect.andThen(
|
|
||||||
field.encodedValue,
|
|
||||||
Option.match({
|
|
||||||
onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]),
|
|
||||||
onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([
|
|
||||||
Stream.runForEach(
|
|
||||||
Stream.drop(field.encodedValue, 1),
|
|
||||||
|
|
||||||
upstreamEncodedValue => Effect.whenEffect(
|
|
||||||
Option.match(upstreamEncodedValue, {
|
|
||||||
onSome: v => Effect.andThen(
|
|
||||||
Ref.set(enabledRef, true),
|
|
||||||
Ref.set(internalValueRef, v),
|
|
||||||
),
|
|
||||||
onNone: () => Effect.andThen(
|
|
||||||
Ref.set(enabledRef, false),
|
|
||||||
Ref.set(internalValueRef, options.defaultValue),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
Effect.andThen(
|
|
||||||
Effect.all([enabledRef, internalValueRef]),
|
|
||||||
([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Stream.runForEach(
|
|
||||||
enabledRef.changes.pipe(
|
|
||||||
Stream.zipLatest(internalValueRef.changes),
|
|
||||||
Stream.drop(1),
|
|
||||||
Stream.changesWith(Equal.equivalence()),
|
|
||||||
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
|
||||||
),
|
|
||||||
([enabled, internalValue]) => Ref.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
|
||||||
),
|
|
||||||
], { concurrency: "unbounded" })),
|
|
||||||
), [field, options.debounce])
|
|
||||||
|
|
||||||
const [enabled, setEnabled] = yield* SubscriptionRef.useSubscriptionRefState(enabledRef)
|
|
||||||
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef)
|
|
||||||
return { enabled, setEnabled, value, setValue }
|
|
||||||
})
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
|
||||||
import { type Equivalence, Function, Predicate } from "effect"
|
|
||||||
import type * as Component from "./Component.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized")
|
|
||||||
export type TypeId = typeof TypeId
|
|
||||||
|
|
||||||
export interface Memoized<P> extends Memoized.Options<P> {
|
|
||||||
readonly [TypeId]: TypeId
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace Memoized {
|
|
||||||
export interface Options<P> {
|
|
||||||
readonly propsAreEqual?: Equivalence.Equivalence<P>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const MemoizedProto = Object.freeze({
|
|
||||||
[TypeId]: TypeId
|
|
||||||
} as const)
|
|
||||||
|
|
||||||
|
|
||||||
export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasProperty(u, TypeId)
|
|
||||||
|
|
||||||
export const memoized = <T extends Component.Component<any, any, any, any>>(
|
|
||||||
self: T
|
|
||||||
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, self),
|
|
||||||
Object.freeze(Object.setPrototypeOf(
|
|
||||||
Object.assign({}, MemoizedProto),
|
|
||||||
Object.getPrototypeOf(self),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const withOptions: {
|
|
||||||
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
|
||||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>
|
|
||||||
): (self: T) => T
|
|
||||||
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
|
||||||
self: T,
|
|
||||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
|
||||||
): T
|
|
||||||
} = Function.dual(2, <T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
|
||||||
self: T,
|
|
||||||
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
|
|
||||||
): T => Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, self, options),
|
|
||||||
Object.getPrototypeOf(self),
|
|
||||||
))
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { type Context, Effect, Equal, type Fiber, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect"
|
|
||||||
import * as Result from "./Result.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const MutationTypeId: unique symbol = Symbol.for("@effect-fc/Mutation/Mutation")
|
|
||||||
export type MutationTypeId = typeof MutationTypeId
|
|
||||||
|
|
||||||
export interface Mutation<in out K extends Mutation.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
|
|
||||||
extends Pipeable.Pipeable {
|
|
||||||
readonly [MutationTypeId]: MutationTypeId
|
|
||||||
|
|
||||||
readonly context: Context.Context<Scope.Scope | R>
|
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>
|
|
||||||
readonly initialProgress: P
|
|
||||||
|
|
||||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
|
||||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
|
||||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
|
||||||
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
|
||||||
|
|
||||||
mutate(key: K): Effect.Effect<Result.Final<A, E, P>>
|
|
||||||
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace Mutation {
|
|
||||||
export type AnyKey = readonly any[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MutationImpl<in out K extends Mutation.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
|
|
||||||
extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
|
|
||||||
readonly [MutationTypeId]: MutationTypeId = MutationTypeId
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly context: Context.Context<Scope.Scope | R>,
|
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
|
||||||
readonly initialProgress: P,
|
|
||||||
|
|
||||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
|
||||||
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
|
||||||
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
|
||||||
readonly latestFinalResult: SubscriptionRef.SubscriptionRef<Option.Option<Result.Final<A, E, P>>>,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
mutate(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
|
||||||
return SubscriptionRef.set(this.latestKey, Option.some(key)).pipe(
|
|
||||||
Effect.andThen(this.start(key)),
|
|
||||||
Effect.andThen(sub => this.watch(sub)),
|
|
||||||
Effect.provide(this.context),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
|
||||||
return SubscriptionRef.set(this.latestKey, Option.some(key)).pipe(
|
|
||||||
Effect.andThen(this.start(key)),
|
|
||||||
Effect.tap(sub => Effect.forkScoped(this.watch(sub))),
|
|
||||||
Effect.provide(this.context),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
start(key: K): Effect.Effect<
|
|
||||||
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
|
||||||
never,
|
|
||||||
Scope.Scope | R
|
|
||||||
> {
|
|
||||||
return this.latestFinalResult.pipe(
|
|
||||||
Effect.andThen(initial => Result.unsafeForkEffect(
|
|
||||||
Effect.onExit(this.f(key), () => Effect.andThen(
|
|
||||||
Effect.all([Effect.fiberId, this.fiber]),
|
|
||||||
([currentFiberId, fiber]) => Option.match(fiber, {
|
|
||||||
onSome: v => Equal.equals(currentFiberId, v.id())
|
|
||||||
? SubscriptionRef.set(this.fiber, Option.none())
|
|
||||||
: Effect.void,
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
|
|
||||||
{
|
|
||||||
initial: Option.isSome(initial) ? Result.willFetch(initial.value) : Result.initial(),
|
|
||||||
initialProgress: this.initialProgress,
|
|
||||||
} as Result.unsafeForkEffect.Options<A, E, P>,
|
|
||||||
)),
|
|
||||||
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
|
||||||
Effect.map(([sub]) => sub),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
|
||||||
): Effect.Effect<Result.Final<A, E, P>> {
|
|
||||||
return sub.get.pipe(
|
|
||||||
Effect.andThen(initial => Stream.runFoldEffect(
|
|
||||||
sub.changes,
|
|
||||||
initial,
|
|
||||||
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
|
||||||
) as Effect.Effect<Result.Final<A, E, P>>),
|
|
||||||
Effect.tap(result => SubscriptionRef.set(this.latestFinalResult, Option.some(result))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isMutation = (u: unknown): u is Mutation<readonly unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, MutationTypeId)
|
|
||||||
|
|
||||||
export declare namespace make {
|
|
||||||
export interface Options<K extends Mutation.AnyKey = never, A = void, E = never, R = never, P = never> {
|
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
|
||||||
readonly initialProgress?: P
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = Effect.fnUntraced(function* <const K extends Mutation.AnyKey = never, A = void, E = never, R = never, P = never>(
|
|
||||||
options: make.Options<K, A, E, R, P>
|
|
||||||
): Effect.fn.Return<
|
|
||||||
Mutation<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
|
||||||
never,
|
|
||||||
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
|
|
||||||
> {
|
|
||||||
return new MutationImpl(
|
|
||||||
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
|
||||||
options.f as any,
|
|
||||||
options.initialProgress as P,
|
|
||||||
|
|
||||||
yield* SubscriptionRef.make(Option.none<K>()),
|
|
||||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
|
||||||
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
|
||||||
yield* SubscriptionRef.make(Option.none<Result.Final<A, E, P>>()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { Array, Equivalence, Function, Option, Predicate } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export type PropertyPath = readonly PropertyKey[]
|
|
||||||
|
|
||||||
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
||||||
|
|
||||||
export type Paths<T, D extends number = 5, Seen = never> = readonly [] | (
|
|
||||||
D extends never ? readonly [] :
|
|
||||||
T extends Seen ? readonly [] :
|
|
||||||
T extends readonly any[] ? {
|
|
||||||
[K in keyof T as K extends number ? K : never]:
|
|
||||||
| readonly [K]
|
|
||||||
| readonly [K, ...Paths<T[K], Prev[D], Seen | T>]
|
|
||||||
} extends infer O
|
|
||||||
? O[keyof O]
|
|
||||||
: never
|
|
||||||
:
|
|
||||||
T extends object ? {
|
|
||||||
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
|
||||||
NonNullable<T[K]> extends infer V
|
|
||||||
? readonly [K] | readonly [K, ...Paths<V, Prev[D], Seen>]
|
|
||||||
: never
|
|
||||||
} extends infer O
|
|
||||||
? O[keyof O]
|
|
||||||
: never
|
|
||||||
:
|
|
||||||
never
|
|
||||||
)
|
|
||||||
|
|
||||||
export type ValueFromPath<T, P extends readonly any[]> = P extends readonly [infer Head, ...infer Tail]
|
|
||||||
? Head extends keyof T
|
|
||||||
? ValueFromPath<T[Head], Tail>
|
|
||||||
: T extends readonly any[]
|
|
||||||
? Head extends number
|
|
||||||
? ValueFromPath<T[number], Tail>
|
|
||||||
: never
|
|
||||||
: never
|
|
||||||
: T
|
|
||||||
|
|
||||||
|
|
||||||
export const equivalence: Equivalence.Equivalence<PropertyPath> = Equivalence.array(Equivalence.strict())
|
|
||||||
|
|
||||||
export const unsafeGet: {
|
|
||||||
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
|
|
||||||
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
|
|
||||||
path.reduce((acc: any, key: any) => acc?.[key], self)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const get: {
|
|
||||||
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
|
|
||||||
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
|
|
||||||
path.reduce(
|
|
||||||
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
|
|
||||||
? Predicate.hasProperty(acc.value, key)
|
|
||||||
? Option.some(acc.value[key])
|
|
||||||
: Option.none()
|
|
||||||
: acc,
|
|
||||||
|
|
||||||
Option.some(self),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const immutableSet: {
|
|
||||||
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => Option.Option<T>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
|
||||||
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
|
||||||
const key = Array.head(path as PropertyPath)
|
|
||||||
if (Option.isNone(key))
|
|
||||||
return Option.some(value as T)
|
|
||||||
if (!Predicate.hasProperty(self, key.value))
|
|
||||||
return Option.none()
|
|
||||||
|
|
||||||
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as PropertyPath)), value)
|
|
||||||
if (Option.isNone(child))
|
|
||||||
return child
|
|
||||||
|
|
||||||
if (Array.isArray(self))
|
|
||||||
return typeof key.value === "number"
|
|
||||||
? Option.some([
|
|
||||||
...self.slice(0, key.value),
|
|
||||||
child.value,
|
|
||||||
...self.slice(key.value + 1),
|
|
||||||
] as T)
|
|
||||||
: Option.none()
|
|
||||||
|
|
||||||
if (typeof self === "object")
|
|
||||||
return Option.some(
|
|
||||||
Object.assign(
|
|
||||||
Object.create(Object.getPrototypeOf(self)),
|
|
||||||
{ ...self, [key.value]: child.value },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Option.none()
|
|
||||||
})
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Effect, PubSub, type Scope } from "effect"
|
|
||||||
import type * as React from "react"
|
|
||||||
import * as Component from "./Component.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const usePubSubFromReactiveValues = Effect.fnUntraced(function* <const A extends React.DependencyList>(
|
|
||||||
values: A
|
|
||||||
): Effect.fn.Return<PubSub.PubSub<A>, never, Scope.Scope> {
|
|
||||||
const pubsub = yield* Component.useOnMount(() => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown))
|
|
||||||
yield* Component.useReactEffect(() => Effect.unlessEffect(PubSub.publish(pubsub, values), PubSub.isShutdown(pubsub)), values)
|
|
||||||
return pubsub
|
|
||||||
})
|
|
||||||
|
|
||||||
export * from "effect/PubSub"
|
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
import { type Cause, type Context, type Duration, Effect, Equal, Fiber, identity, Option, Pipeable, Predicate, type Scope, Stream, Subscribable, SubscriptionRef } from "effect"
|
|
||||||
import * as QueryClient from "./QueryClient.js"
|
|
||||||
import * as Result from "./Result.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
|
|
||||||
export type QueryTypeId = typeof QueryTypeId
|
|
||||||
|
|
||||||
export interface Query<in out K extends Query.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
|
|
||||||
extends Pipeable.Pipeable {
|
|
||||||
readonly [QueryTypeId]: QueryTypeId
|
|
||||||
|
|
||||||
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>
|
|
||||||
readonly key: Stream.Stream<K>
|
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>
|
|
||||||
readonly initialProgress: P
|
|
||||||
|
|
||||||
readonly staleTime: Duration.DurationInput
|
|
||||||
readonly refreshOnWindowFocus: boolean
|
|
||||||
|
|
||||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
|
||||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
|
||||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
|
||||||
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
|
||||||
|
|
||||||
readonly run: Effect.Effect<void>
|
|
||||||
fetch(key: K): Effect.Effect<Result.Final<A, E, P>>
|
|
||||||
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
|
||||||
readonly refresh: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
|
|
||||||
readonly refreshSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
|
|
||||||
|
|
||||||
readonly invalidateCache: Effect.Effect<void>
|
|
||||||
invalidateCacheEntry(key: K): Effect.Effect<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace Query {
|
|
||||||
export type AnyKey = readonly any[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QueryImpl<in out K extends Query.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
|
|
||||||
extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
|
||||||
readonly [QueryTypeId]: QueryTypeId = QueryTypeId
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>,
|
|
||||||
readonly key: Stream.Stream<K>,
|
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
|
||||||
readonly initialProgress: P,
|
|
||||||
|
|
||||||
readonly staleTime: Duration.DurationInput,
|
|
||||||
readonly refreshOnWindowFocus: boolean,
|
|
||||||
|
|
||||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
|
||||||
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
|
||||||
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
|
||||||
readonly latestFinalResult: SubscriptionRef.SubscriptionRef<Option.Option<Result.Final<A, E, P>>>,
|
|
||||||
|
|
||||||
readonly runSemaphore: Effect.Semaphore,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
get run(): Effect.Effect<void> {
|
|
||||||
return Effect.all([
|
|
||||||
Stream.runForEach(this.key, key => this.fetchSubscribable(key)),
|
|
||||||
|
|
||||||
Effect.promise(() => import("@effect/platform-browser")).pipe(
|
|
||||||
Effect.andThen(({ BrowserStream }) => this.refreshOnWindowFocus
|
|
||||||
? Stream.runForEach(
|
|
||||||
BrowserStream.fromEventListenerWindow("focus"),
|
|
||||||
() => this.refreshSubscribable,
|
|
||||||
)
|
|
||||||
: Effect.void
|
|
||||||
),
|
|
||||||
Effect.catchAllDefect(() => Effect.void),
|
|
||||||
),
|
|
||||||
], { concurrency: "unbounded" }).pipe(
|
|
||||||
Effect.ignore,
|
|
||||||
this.runSemaphore.withPermits(1),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get interrupt(): Effect.Effect<void> {
|
|
||||||
return Effect.andThen(this.fiber, Option.match({
|
|
||||||
onSome: Fiber.interrupt,
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
|
||||||
return this.interrupt.pipe(
|
|
||||||
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
|
||||||
Effect.andThen(this.latestFinalResult),
|
|
||||||
Effect.andThen(previous => this.startCached(key, Option.isSome(previous)
|
|
||||||
? Result.willFetch(previous.value) as Result.Final<A, E, P>
|
|
||||||
: Result.initial()
|
|
||||||
)),
|
|
||||||
Effect.andThen(sub => this.watch(key, sub)),
|
|
||||||
Effect.provide(this.context),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
|
||||||
return this.interrupt.pipe(
|
|
||||||
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
|
||||||
Effect.andThen(this.latestFinalResult),
|
|
||||||
Effect.andThen(previous => this.startCached(key, Option.isSome(previous)
|
|
||||||
? Result.willFetch(previous.value) as Result.Final<A, E, P>
|
|
||||||
: Result.initial()
|
|
||||||
)),
|
|
||||||
Effect.tap(sub => Effect.forkScoped(this.watch(key, sub))),
|
|
||||||
Effect.provide(this.context),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get refresh(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> {
|
|
||||||
return this.interrupt.pipe(
|
|
||||||
Effect.andThen(Effect.Do),
|
|
||||||
Effect.bind("latestKey", () => Effect.andThen(this.latestKey, identity)),
|
|
||||||
Effect.bind("latestFinalResult", () => this.latestFinalResult),
|
|
||||||
Effect.bind("subscribable", ({ latestKey, latestFinalResult }) =>
|
|
||||||
this.startCached(latestKey, Option.isSome(latestFinalResult)
|
|
||||||
? Result.willRefresh(latestFinalResult.value) as Result.Final<A, E, P>
|
|
||||||
: Result.initial()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Effect.andThen(({ latestKey, subscribable }) => this.watch(latestKey, subscribable)),
|
|
||||||
Effect.provide(this.context),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get refreshSubscribable(): Effect.Effect<
|
|
||||||
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
|
||||||
Cause.NoSuchElementException
|
|
||||||
> {
|
|
||||||
return this.interrupt.pipe(
|
|
||||||
Effect.andThen(Effect.Do),
|
|
||||||
Effect.bind("latestKey", () => Effect.andThen(this.latestKey, identity)),
|
|
||||||
Effect.bind("latestFinalResult", () => this.latestFinalResult),
|
|
||||||
Effect.bind("subscribable", ({ latestKey, latestFinalResult }) =>
|
|
||||||
this.startCached(latestKey, Option.isSome(latestFinalResult)
|
|
||||||
? Result.willRefresh(latestFinalResult.value) as Result.Final<A, E, P>
|
|
||||||
: Result.initial()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Effect.tap(({ latestKey, subscribable }) => Effect.forkScoped(this.watch(latestKey, subscribable))),
|
|
||||||
Effect.map(({ subscribable }) => subscribable),
|
|
||||||
Effect.provide(this.context),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
startCached(
|
|
||||||
key: K,
|
|
||||||
initial: Result.Initial | Result.Final<A, E, P>,
|
|
||||||
): Effect.Effect<
|
|
||||||
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
|
||||||
never,
|
|
||||||
Scope.Scope | QueryClient.QueryClient | R
|
|
||||||
> {
|
|
||||||
return Effect.andThen(this.getCacheEntry(key), Option.match({
|
|
||||||
onSome: entry => Effect.andThen(
|
|
||||||
QueryClient.isQueryClientCacheEntryStale(entry),
|
|
||||||
isStale => isStale
|
|
||||||
? this.start(key, Result.willRefresh(entry.result) as Result.Final<A, E, P>)
|
|
||||||
: Effect.succeed(Subscribable.make({
|
|
||||||
get: Effect.succeed(entry.result as Result.Result<A, E, P>),
|
|
||||||
get changes() { return Stream.make(entry.result as Result.Result<A, E, P>) },
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
onNone: () => this.start(key, initial),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
start(
|
|
||||||
key: K,
|
|
||||||
initial: Result.Initial | Result.Final<A, E, P>,
|
|
||||||
): Effect.Effect<
|
|
||||||
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
|
||||||
never,
|
|
||||||
Scope.Scope | R
|
|
||||||
> {
|
|
||||||
return Result.unsafeForkEffect(
|
|
||||||
Effect.onExit(this.f(key), () => Effect.andThen(
|
|
||||||
Effect.all([Effect.fiberId, this.fiber]),
|
|
||||||
([currentFiberId, fiber]) => Option.match(fiber, {
|
|
||||||
onSome: v => Equal.equals(currentFiberId, v.id())
|
|
||||||
? SubscriptionRef.set(this.fiber, Option.none())
|
|
||||||
: Effect.void,
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
|
|
||||||
{
|
|
||||||
initial,
|
|
||||||
initialProgress: this.initialProgress,
|
|
||||||
} as Result.unsafeForkEffect.Options<A, E, P>,
|
|
||||||
).pipe(
|
|
||||||
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
|
||||||
Effect.map(([sub]) => sub),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
key: K,
|
|
||||||
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
|
||||||
): Effect.Effect<Result.Final<A, E, P>, never, QueryClient.QueryClient> {
|
|
||||||
return sub.get.pipe(
|
|
||||||
Effect.andThen(initial => Stream.runFoldEffect(
|
|
||||||
sub.changes,
|
|
||||||
initial,
|
|
||||||
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
|
||||||
) as Effect.Effect<Result.Final<A, E, P>>),
|
|
||||||
Effect.tap(result => SubscriptionRef.set(this.latestFinalResult, Option.some(result))),
|
|
||||||
Effect.tap(result => Result.isSuccess(result)
|
|
||||||
? this.setCacheEntry(key, result)
|
|
||||||
: Effect.void
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
makeCacheKey(key: K): QueryClient.QueryClientCacheKey {
|
|
||||||
return new QueryClient.QueryClientCacheKey(key, this.f as (key: Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>)
|
|
||||||
}
|
|
||||||
|
|
||||||
getCacheEntry(
|
|
||||||
key: K
|
|
||||||
): Effect.Effect<Option.Option<QueryClient.QueryClientCacheEntry>, never, QueryClient.QueryClient> {
|
|
||||||
return Effect.andThen(
|
|
||||||
Effect.all([
|
|
||||||
Effect.succeed(this.makeCacheKey(key)),
|
|
||||||
QueryClient.QueryClient,
|
|
||||||
]),
|
|
||||||
([key, client]) => client.getCacheEntry(key),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setCacheEntry(
|
|
||||||
key: K,
|
|
||||||
result: Result.Success<A>,
|
|
||||||
): Effect.Effect<QueryClient.QueryClientCacheEntry, never, QueryClient.QueryClient> {
|
|
||||||
return Effect.andThen(
|
|
||||||
Effect.all([
|
|
||||||
Effect.succeed(this.makeCacheKey(key)),
|
|
||||||
QueryClient.QueryClient,
|
|
||||||
]),
|
|
||||||
([key, client]) => client.setCacheEntry(key, result, this.staleTime),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get invalidateCache(): Effect.Effect<void> {
|
|
||||||
return QueryClient.QueryClient.pipe(
|
|
||||||
Effect.andThen(client => client.invalidateCacheEntries(this.f as (key: Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>)),
|
|
||||||
Effect.provide(this.context),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidateCacheEntry(key: K): Effect.Effect<void> {
|
|
||||||
return Effect.all([
|
|
||||||
Effect.succeed(this.makeCacheKey(key)),
|
|
||||||
QueryClient.QueryClient,
|
|
||||||
]).pipe(
|
|
||||||
Effect.andThen(([key, client]) => client.invalidateCacheEntry(key)),
|
|
||||||
Effect.provide(this.context),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, QueryTypeId)
|
|
||||||
|
|
||||||
export declare namespace make {
|
|
||||||
export interface Options<K extends Query.AnyKey, A, E = never, R = never, P = never> {
|
|
||||||
readonly key: Stream.Stream<K>
|
|
||||||
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
|
||||||
readonly initialProgress?: P
|
|
||||||
readonly staleTime?: Duration.DurationInput
|
|
||||||
readonly refreshOnWindowFocus?: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, E = never, R = never, P = never>(
|
|
||||||
options: make.Options<K, A, E, R, P>
|
|
||||||
): Effect.fn.Return<
|
|
||||||
Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
|
||||||
never,
|
|
||||||
Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
|
|
||||||
> {
|
|
||||||
const client = yield* QueryClient.QueryClient
|
|
||||||
|
|
||||||
return new QueryImpl(
|
|
||||||
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
|
||||||
options.key,
|
|
||||||
options.f as any,
|
|
||||||
options.initialProgress as P,
|
|
||||||
|
|
||||||
options.staleTime ?? client.defaultStaleTime,
|
|
||||||
options.refreshOnWindowFocus ?? client.defaultRefreshOnWindowFocus,
|
|
||||||
|
|
||||||
yield* SubscriptionRef.make(Option.none<K>()),
|
|
||||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
|
||||||
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
|
||||||
yield* SubscriptionRef.make(Option.none<Result.Final<A, E, P>>()),
|
|
||||||
|
|
||||||
yield* Effect.makeSemaphore(1),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const service = <K extends Query.AnyKey, A, E = never, R = never, P = never>(
|
|
||||||
options: make.Options<K, A, E, R, P>
|
|
||||||
): Effect.Effect<
|
|
||||||
Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
|
||||||
never,
|
|
||||||
Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
|
|
||||||
> => Effect.tap(
|
|
||||||
make(options),
|
|
||||||
query => Effect.forkScoped(query.run),
|
|
||||||
)
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import { DateTime, Duration, Effect, Equal, Equivalence, Hash, HashMap, type Option, Pipeable, Predicate, Schedule, type Scope, type Subscribable, SubscriptionRef } from "effect"
|
|
||||||
import type * as Query from "./Query.js"
|
|
||||||
import type * as Result from "./Result.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const QueryClientServiceTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientService")
|
|
||||||
export type QueryClientServiceTypeId = typeof QueryClientServiceTypeId
|
|
||||||
|
|
||||||
export interface QueryClientService extends Pipeable.Pipeable {
|
|
||||||
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId
|
|
||||||
|
|
||||||
readonly cache: Subscribable.Subscribable<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>
|
|
||||||
readonly cacheGcTime: Duration.DurationInput
|
|
||||||
readonly defaultStaleTime: Duration.DurationInput
|
|
||||||
readonly defaultRefreshOnWindowFocus: boolean
|
|
||||||
|
|
||||||
readonly run: Effect.Effect<void>
|
|
||||||
getCacheEntry(key: QueryClientCacheKey): Effect.Effect<Option.Option<QueryClientCacheEntry>>
|
|
||||||
setCacheEntry(
|
|
||||||
key: QueryClientCacheKey,
|
|
||||||
result: Result.Success<unknown>,
|
|
||||||
staleTime: Duration.DurationInput,
|
|
||||||
): Effect.Effect<QueryClientCacheEntry>
|
|
||||||
invalidateCacheEntries(f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>): Effect.Effect<void>
|
|
||||||
invalidateCacheEntry(key: QueryClientCacheKey): Effect.Effect<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QueryClient extends Effect.Service<QueryClient>()("@effect-fc/QueryClient/QueryClient", {
|
|
||||||
scoped: Effect.suspend(() => service())
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class QueryClientServiceImpl
|
|
||||||
extends Pipeable.Class()
|
|
||||||
implements QueryClientService {
|
|
||||||
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId = QueryClientServiceTypeId
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly cache: SubscriptionRef.SubscriptionRef<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>,
|
|
||||||
readonly cacheGcTime: Duration.DurationInput,
|
|
||||||
readonly defaultStaleTime: Duration.DurationInput,
|
|
||||||
readonly defaultRefreshOnWindowFocus: boolean,
|
|
||||||
readonly runSemaphore: Effect.Semaphore,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
get run(): Effect.Effect<void> {
|
|
||||||
return this.runSemaphore.withPermits(1)(Effect.repeat(
|
|
||||||
Effect.andThen(
|
|
||||||
DateTime.now,
|
|
||||||
now => SubscriptionRef.update(this.cache, HashMap.filter(entry =>
|
|
||||||
Duration.lessThan(
|
|
||||||
DateTime.distanceDuration(entry.lastAccessedAt, now),
|
|
||||||
Duration.sum(entry.staleTime, this.cacheGcTime),
|
|
||||||
)
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
Schedule.spaced("30 second"),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
getCacheEntry(key: QueryClientCacheKey): Effect.Effect<Option.Option<QueryClientCacheEntry>> {
|
|
||||||
return Effect.all([
|
|
||||||
Effect.andThen(this.cache, HashMap.get(key)),
|
|
||||||
DateTime.now,
|
|
||||||
]).pipe(
|
|
||||||
Effect.map(([entry, now]) => new QueryClientCacheEntry(entry.result, entry.staleTime, entry.createdAt, now)),
|
|
||||||
Effect.tap(entry => SubscriptionRef.update(this.cache, HashMap.set(key, entry))),
|
|
||||||
Effect.option,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setCacheEntry(
|
|
||||||
key: QueryClientCacheKey,
|
|
||||||
result: Result.Success<unknown>,
|
|
||||||
staleTime: Duration.DurationInput,
|
|
||||||
): Effect.Effect<QueryClientCacheEntry> {
|
|
||||||
return DateTime.now.pipe(
|
|
||||||
Effect.map(now => new QueryClientCacheEntry(result, staleTime, now, now)),
|
|
||||||
Effect.tap(entry => SubscriptionRef.update(this.cache, HashMap.set(key, entry))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidateCacheEntries(f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>): Effect.Effect<void> {
|
|
||||||
return SubscriptionRef.update(this.cache, HashMap.filter((_, key) => !Equivalence.strict()(key.f, f)))
|
|
||||||
}
|
|
||||||
invalidateCacheEntry(key: QueryClientCacheKey): Effect.Effect<void> {
|
|
||||||
return SubscriptionRef.update(this.cache, HashMap.remove(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isQueryClientService = (u: unknown): u is QueryClientService => Predicate.hasProperty(u, QueryClientServiceTypeId)
|
|
||||||
|
|
||||||
export declare namespace make {
|
|
||||||
export interface Options {
|
|
||||||
readonly cacheGcTime?: Duration.DurationInput
|
|
||||||
readonly defaultStaleTime?: Duration.DurationInput
|
|
||||||
readonly defaultRefreshOnWindowFocus?: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = Effect.fnUntraced(function* (options: make.Options = {}): Effect.fn.Return<QueryClientService> {
|
|
||||||
return new QueryClientServiceImpl(
|
|
||||||
yield* SubscriptionRef.make(HashMap.empty<QueryClientCacheKey, QueryClientCacheEntry>()),
|
|
||||||
options.cacheGcTime ?? "5 minutes",
|
|
||||||
options.defaultStaleTime ?? "0 minutes",
|
|
||||||
options.defaultRefreshOnWindowFocus ?? true,
|
|
||||||
yield* Effect.makeSemaphore(1),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export declare namespace service {
|
|
||||||
export interface Options extends make.Options {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const service = (options?: service.Options): Effect.Effect<QueryClientService, never, Scope.Scope> => Effect.tap(
|
|
||||||
make(options),
|
|
||||||
client => Effect.forkScoped(client.run),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
export const QueryClientCacheKeyTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientCacheKey")
|
|
||||||
export type QueryClientCacheKeyTypeId = typeof QueryClientCacheKeyTypeId
|
|
||||||
|
|
||||||
export class QueryClientCacheKey
|
|
||||||
extends Pipeable.Class()
|
|
||||||
implements Pipeable.Pipeable, Equal.Equal {
|
|
||||||
readonly [QueryClientCacheKeyTypeId]: QueryClientCacheKeyTypeId = QueryClientCacheKeyTypeId
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly key: Query.Query.AnyKey,
|
|
||||||
readonly f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
[Equal.symbol](that: Equal.Equal) {
|
|
||||||
return isQueryClientCacheKey(that) && Equivalence.array(Equal.equivalence())(this.key, that.key) && Equivalence.strict()(this.f, that.f)
|
|
||||||
}
|
|
||||||
[Hash.symbol]() {
|
|
||||||
return Hash.combine(Hash.hash(this.f))(Hash.array(this.key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isQueryClientCacheKey = (u: unknown): u is QueryClientCacheKey => Predicate.hasProperty(u, QueryClientCacheKeyTypeId)
|
|
||||||
|
|
||||||
|
|
||||||
export const QueryClientCacheEntryTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientCacheEntry")
|
|
||||||
export type QueryClientCacheEntryTypeId = typeof QueryClientCacheEntryTypeId
|
|
||||||
|
|
||||||
export class QueryClientCacheEntry
|
|
||||||
extends Pipeable.Class()
|
|
||||||
implements Pipeable.Pipeable {
|
|
||||||
readonly [QueryClientCacheEntryTypeId]: QueryClientCacheEntryTypeId = QueryClientCacheEntryTypeId
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly result: Result.Success<unknown>,
|
|
||||||
readonly staleTime: Duration.DurationInput,
|
|
||||||
readonly createdAt: DateTime.DateTime,
|
|
||||||
readonly lastAccessedAt: DateTime.DateTime,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isQueryClientCacheEntry = (u: unknown): u is QueryClientCacheEntry => Predicate.hasProperty(u, QueryClientCacheEntryTypeId)
|
|
||||||
|
|
||||||
export const isQueryClientCacheEntryStale = (
|
|
||||||
self: QueryClientCacheEntry
|
|
||||||
): Effect.Effect<boolean> => Effect.andThen(
|
|
||||||
DateTime.now,
|
|
||||||
now => Duration.greaterThanOrEqualTo(DateTime.distanceDuration(self.createdAt, now), self.staleTime),
|
|
||||||
)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
|
||||||
import { Effect, Layer, ManagedRuntime, Predicate, Runtime, Scope } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import * as Component from "./Component.js"
|
|
||||||
import * as ErrorObserver from "./ErrorObserver.js"
|
|
||||||
import * as QueryClient from "./QueryClient.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/ReactRuntime/ReactRuntime")
|
|
||||||
export type TypeId = typeof TypeId
|
|
||||||
|
|
||||||
export interface ReactRuntime<R, ER> {
|
|
||||||
new(_: never): Record<string, never>
|
|
||||||
readonly [TypeId]: TypeId
|
|
||||||
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
|
||||||
readonly context: React.Context<Runtime.Runtime<R>>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReactRuntimeProto = Object.freeze({ [TypeId]: TypeId } as const)
|
|
||||||
|
|
||||||
export const Prelude: Layer.Layer<
|
|
||||||
| Component.ScopeMap
|
|
||||||
| ErrorObserver.ErrorObserver
|
|
||||||
| QueryClient.QueryClient
|
|
||||||
> = Layer.mergeAll(
|
|
||||||
Component.ScopeMap.Default,
|
|
||||||
ErrorObserver.layer,
|
|
||||||
QueryClient.QueryClient.Default,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
export const isReactRuntime = (u: unknown): u is ReactRuntime<unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
|
||||||
|
|
||||||
export const make = <R, ER>(
|
|
||||||
layer: Layer.Layer<R, ER>,
|
|
||||||
memoMap?: Layer.MemoMap,
|
|
||||||
): ReactRuntime<Layer.Layer.Success<typeof Prelude> | R, ER> => Object.setPrototypeOf(
|
|
||||||
Object.assign(function() {}, {
|
|
||||||
runtime: ManagedRuntime.make(
|
|
||||||
Layer.merge(layer, Prelude),
|
|
||||||
memoMap,
|
|
||||||
),
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
|
||||||
context: React.createContext<Runtime.Runtime<R>>(null!),
|
|
||||||
}),
|
|
||||||
ReactRuntimeProto,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
export namespace Provider {
|
|
||||||
export interface Props<R, ER> extends React.SuspenseProps {
|
|
||||||
readonly runtime: ReactRuntime<R, ER>
|
|
||||||
readonly children?: React.ReactNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Provider = <R, ER>(
|
|
||||||
{ runtime, children, ...suspenseProps }: Provider.Props<R, ER>
|
|
||||||
): React.ReactNode => {
|
|
||||||
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
React.Suspense,
|
|
||||||
suspenseProps,
|
|
||||||
React.createElement(ProviderInner<R, ER>, { runtime, promise, children }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProviderInner = <R, ER>(
|
|
||||||
{ runtime, promise, children }: {
|
|
||||||
readonly runtime: ReactRuntime<R, ER>
|
|
||||||
readonly promise: Promise<Runtime.Runtime<R>>
|
|
||||||
readonly children?: React.ReactNode
|
|
||||||
}
|
|
||||||
): React.ReactNode => {
|
|
||||||
const effectRuntime = React.use(promise)
|
|
||||||
const scope = Runtime.runSync(effectRuntime)(Component.useScope([effectRuntime]))
|
|
||||||
Runtime.runSync(effectRuntime)(Effect.provideService(
|
|
||||||
Component.useOnChange(() => Effect.addFinalizer(() => runtime.runtime.disposeEffect), [scope]),
|
|
||||||
Scope.Scope,
|
|
||||||
scope,
|
|
||||||
))
|
|
||||||
|
|
||||||
return React.createElement(runtime.context, { value: effectRuntime }, children)
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Function } from "effect"
|
|
||||||
import type * as React from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const value: {
|
|
||||||
<S>(prevState: S): (self: React.SetStateAction<S>) => S
|
|
||||||
<S>(self: React.SetStateAction<S>, prevState: S): S
|
|
||||||
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
|
|
||||||
typeof self === "function"
|
|
||||||
? (self as (prevState: S) => S)(prevState)
|
|
||||||
: self
|
|
||||||
)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Effect, Equivalence, Option, Stream } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import * as Component from "./Component.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const useStream: {
|
|
||||||
<A, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>
|
|
||||||
): Effect.Effect<Option.Option<A>, never, R>
|
|
||||||
<A extends NonNullable<unknown>, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue: A,
|
|
||||||
): Effect.Effect<Option.Some<A>, never, R>
|
|
||||||
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue?: A,
|
|
||||||
) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(() => initialValue
|
|
||||||
? Option.some(initialValue)
|
|
||||||
: Option.none()
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* Component.useReactEffect(() => Effect.forkScoped(
|
|
||||||
Stream.runForEach(
|
|
||||||
Stream.changesWith(stream, Equivalence.strict()),
|
|
||||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
|
||||||
)
|
|
||||||
), [stream])
|
|
||||||
|
|
||||||
return reactStateValue as Option.Some<A>
|
|
||||||
})
|
|
||||||
|
|
||||||
export * from "effect/Stream"
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Effect, Equivalence, Stream, Subscribable } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import * as Component from "./Component.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
|
||||||
...elements: T
|
|
||||||
): Subscribable.Subscribable<
|
|
||||||
[T[number]] extends [never]
|
|
||||||
? never
|
|
||||||
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never },
|
|
||||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never,
|
|
||||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never
|
|
||||||
> => Subscribable.make({
|
|
||||||
get: Effect.all(elements.map(v => v.get)),
|
|
||||||
changes: Stream.zipLatestAll(...elements.map(v => v.changes)),
|
|
||||||
}) as any
|
|
||||||
|
|
||||||
export declare namespace useSubscribables {
|
|
||||||
export type Success<T extends readonly Subscribable.Subscribable<any, any, any>[]> = [T[number]] extends [never]
|
|
||||||
? never
|
|
||||||
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never }
|
|
||||||
|
|
||||||
export interface Options<A> {
|
|
||||||
readonly equivalence?: Equivalence.Equivalence<A>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSubscribables = Effect.fnUntraced(function* <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
|
||||||
elements: T,
|
|
||||||
options?: useSubscribables.Options<useSubscribables.Success<NoInfer<T>>>,
|
|
||||||
): Effect.fn.Return<
|
|
||||||
useSubscribables.Success<T>,
|
|
||||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never,
|
|
||||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never
|
|
||||||
> {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(
|
|
||||||
yield* Component.useOnMount(() => Effect.all(elements.map(v => v.get)))
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* Component.useReactEffect(() => Stream.zipLatestAll(...elements.map(ref => ref.changes)).pipe(
|
|
||||||
Stream.changesWith((options?.equivalence as Equivalence.Equivalence<any[]> | undefined) ?? Equivalence.array(Equivalence.strict())),
|
|
||||||
Stream.runForEach(v =>
|
|
||||||
Effect.sync(() => setReactStateValue(v))
|
|
||||||
),
|
|
||||||
Effect.forkScoped,
|
|
||||||
), elements)
|
|
||||||
|
|
||||||
return reactStateValue as any
|
|
||||||
})
|
|
||||||
|
|
||||||
export * from "effect/Subscribable"
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import * as Component from "./Component.js"
|
|
||||||
import * as SetStateAction from "./SetStateAction.js"
|
|
||||||
|
|
||||||
|
|
||||||
export declare namespace useSubscriptionRefState {
|
|
||||||
export interface Options<A> {
|
|
||||||
readonly equivalence?: Equivalence.Equivalence<A>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSubscriptionRefState = Effect.fnUntraced(function* <A>(
|
|
||||||
ref: SubscriptionRef.SubscriptionRef<A>,
|
|
||||||
options?: useSubscriptionRefState.Options<NoInfer<A>>,
|
|
||||||
): Effect.fn.Return<readonly [A, React.Dispatch<React.SetStateAction<A>>]> {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(yield* Component.useOnMount(() => ref))
|
|
||||||
|
|
||||||
yield* Component.useReactEffect(() => Effect.forkScoped(
|
|
||||||
Stream.runForEach(
|
|
||||||
Stream.changesWith(ref.changes, options?.equivalence ?? Equivalence.strict()),
|
|
||||||
v => Effect.sync(() => setReactStateValue(v)),
|
|
||||||
)
|
|
||||||
), [ref])
|
|
||||||
|
|
||||||
const setValue = yield* Component.useCallbackSync(
|
|
||||||
(setStateAction: React.SetStateAction<A>) => Effect.andThen(
|
|
||||||
Ref.updateAndGet(ref, prevState => SetStateAction.value(setStateAction, prevState)),
|
|
||||||
v => setReactStateValue(v),
|
|
||||||
),
|
|
||||||
[ref],
|
|
||||||
)
|
|
||||||
|
|
||||||
return [reactStateValue, setValue]
|
|
||||||
})
|
|
||||||
|
|
||||||
export declare namespace useSubscriptionRefFromState {
|
|
||||||
export interface Options<A> {
|
|
||||||
readonly equivalence?: Equivalence.Equivalence<A>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSubscriptionRefFromState = Effect.fnUntraced(function* <A>(
|
|
||||||
[value, setValue]: readonly [A, React.Dispatch<React.SetStateAction<A>>],
|
|
||||||
options?: useSubscriptionRefFromState.Options<NoInfer<A>>,
|
|
||||||
): Effect.fn.Return<SubscriptionRef.SubscriptionRef<A>> {
|
|
||||||
const ref = yield* Component.useOnChange(() => Effect.tap(
|
|
||||||
SubscriptionRef.make(value),
|
|
||||||
ref => Effect.forkScoped(
|
|
||||||
Stream.runForEach(
|
|
||||||
Stream.changesWith(ref.changes, options?.equivalence ?? Equivalence.strict()),
|
|
||||||
v => Effect.sync(() => setValue(v)),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
), [setValue])
|
|
||||||
|
|
||||||
yield* Component.useReactEffect(() => Ref.set(ref, value), [value])
|
|
||||||
return ref
|
|
||||||
})
|
|
||||||
|
|
||||||
export * from "effect/SubscriptionRef"
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import { Chunk, Effect, Effectable, Option, Predicate, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
|
||||||
import * as PropertyPath from "./PropertyPath.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("@effect-fc/SubscriptionSubRef/SubscriptionSubRef")
|
|
||||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
|
||||||
|
|
||||||
export interface SubscriptionSubRef<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
|
|
||||||
extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
readonly parent: B
|
|
||||||
|
|
||||||
readonly [Unify.typeSymbol]?: unknown
|
|
||||||
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
|
||||||
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace SubscriptionSubRef {
|
|
||||||
export interface Variance<in out A, in out B> {
|
|
||||||
readonly [SubscriptionSubRefTypeId]: {
|
|
||||||
readonly _A: Types.Invariant<A>
|
|
||||||
readonly _B: Types.Invariant<B>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
|
|
||||||
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
|
|
||||||
SubscriptionRef?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const refVariance = { _A: (_: any) => _ }
|
|
||||||
const synchronizedRefVariance = { _A: (_: any) => _ }
|
|
||||||
const subscriptionRefVariance = { _A: (_: any) => _ }
|
|
||||||
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
|
||||||
|
|
||||||
class SubscriptionSubRefImpl<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
|
|
||||||
extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
|
||||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
|
||||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
|
||||||
readonly [Ref.RefTypeId] = refVariance
|
|
||||||
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
|
|
||||||
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
|
|
||||||
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
|
|
||||||
|
|
||||||
readonly get: Effect.Effect<A>
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly parent: B,
|
|
||||||
readonly getter: (parentValue: Effect.Effect.Success<B>) => A,
|
|
||||||
readonly setter: (parentValue: Effect.Effect.Success<B>, value: A) => Effect.Effect.Success<B>,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this.get = Effect.map(this.parent, this.getter)
|
|
||||||
}
|
|
||||||
|
|
||||||
commit() {
|
|
||||||
return this.get
|
|
||||||
}
|
|
||||||
|
|
||||||
get changes(): Stream.Stream<A> {
|
|
||||||
return Stream.unwrap(
|
|
||||||
Effect.map(this.get, a => Stream.concat(
|
|
||||||
Stream.make(a),
|
|
||||||
Stream.map(this.parent.changes, this.getter),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
|
|
||||||
return this.modifyEffect(a => Effect.succeed(f(a)))
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
|
||||||
return Effect.Do.pipe(
|
|
||||||
Effect.bind("b", (): Effect.Effect<Effect.Effect.Success<B>> => this.parent),
|
|
||||||
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
|
||||||
Effect.tap(({ b, ca: [, a] }) => SubscriptionRef.set(this.parent, this.setter(b, a))),
|
|
||||||
Effect.map(({ ca: [c] }) => c),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const isSubscriptionSubRef = (u: unknown): u is SubscriptionSubRef<unknown, SubscriptionRef.SubscriptionRef<unknown>> => Predicate.hasProperty(u, SubscriptionSubRefTypeId)
|
|
||||||
|
|
||||||
export const makeFromGetSet = <A, B extends SubscriptionRef.SubscriptionRef<any>>(
|
|
||||||
parent: B,
|
|
||||||
options: {
|
|
||||||
readonly get: (parentValue: Effect.Effect.Success<B>) => A
|
|
||||||
readonly set: (parentValue: Effect.Effect.Success<B>, value: A) => Effect.Effect.Success<B>
|
|
||||||
},
|
|
||||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, options.get, options.set)
|
|
||||||
|
|
||||||
export const makeFromPath = <
|
|
||||||
B extends SubscriptionRef.SubscriptionRef<any>,
|
|
||||||
const P extends PropertyPath.Paths<Effect.Effect.Success<B>>,
|
|
||||||
>(
|
|
||||||
parent: B,
|
|
||||||
path: P,
|
|
||||||
): SubscriptionSubRef<PropertyPath.ValueFromPath<Effect.Effect.Success<B>, P>, B> => new SubscriptionSubRefImpl(
|
|
||||||
parent,
|
|
||||||
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
|
||||||
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const makeFromChunkIndex: {
|
|
||||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
|
|
||||||
parent: B,
|
|
||||||
index: number,
|
|
||||||
): SubscriptionSubRef<
|
|
||||||
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
|
|
||||||
B
|
|
||||||
>
|
|
||||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
|
|
||||||
parent: B,
|
|
||||||
index: number,
|
|
||||||
): SubscriptionSubRef<
|
|
||||||
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
|
|
||||||
B
|
|
||||||
>
|
|
||||||
} = (
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>,
|
|
||||||
index: number,
|
|
||||||
) => new SubscriptionSubRefImpl(
|
|
||||||
parent,
|
|
||||||
parentValue => Chunk.unsafeGet(parentValue, index),
|
|
||||||
(parentValue, value) => Chunk.replace(parentValue, index, value),
|
|
||||||
) as any
|
|
||||||
|
|
||||||
export const makeFromChunkFindFirst: {
|
|
||||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
|
|
||||||
parent: B,
|
|
||||||
findFirstPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never>,
|
|
||||||
): SubscriptionSubRef<
|
|
||||||
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
|
|
||||||
B
|
|
||||||
>
|
|
||||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
|
|
||||||
parent: B,
|
|
||||||
findFirstPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never>,
|
|
||||||
): SubscriptionSubRef<
|
|
||||||
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
|
|
||||||
B
|
|
||||||
>
|
|
||||||
} = (
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<never>>,
|
|
||||||
findFirstPredicate: Predicate.Predicate.Any,
|
|
||||||
) => new SubscriptionSubRefImpl(
|
|
||||||
parent,
|
|
||||||
parentValue => Option.getOrThrow(Chunk.findFirst(parentValue, findFirstPredicate)),
|
|
||||||
(parentValue, value) => Option.getOrThrow(Option.andThen(
|
|
||||||
Chunk.findFirstIndex(parentValue, findFirstPredicate),
|
|
||||||
index => Chunk.replace(parentValue, index, value),
|
|
||||||
)),
|
|
||||||
) as any
|
|
||||||
|
|
||||||
export const makeFromChunkFindLast: {
|
|
||||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
|
|
||||||
parent: B,
|
|
||||||
findLastPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never>,
|
|
||||||
): SubscriptionSubRef<
|
|
||||||
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
|
|
||||||
B
|
|
||||||
>
|
|
||||||
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
|
|
||||||
parent: B,
|
|
||||||
findLastPredicate: Predicate.Predicate<Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never>,
|
|
||||||
): SubscriptionSubRef<
|
|
||||||
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
|
|
||||||
B
|
|
||||||
>
|
|
||||||
} = (
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<never>>,
|
|
||||||
findLastPredicate: Predicate.Predicate.Any,
|
|
||||||
) => new SubscriptionSubRefImpl(
|
|
||||||
parent,
|
|
||||||
parentValue => Option.getOrThrow(Chunk.findLast(parentValue, findLastPredicate)),
|
|
||||||
(parentValue, value) => Option.getOrThrow(Option.andThen(
|
|
||||||
Chunk.findLastIndex(parentValue, findLastPredicate),
|
|
||||||
index => Chunk.replace(parentValue, index, value),
|
|
||||||
)),
|
|
||||||
) as any
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export * as Async from "./Async.js"
|
|
||||||
export * as Component from "./Component.js"
|
|
||||||
export * as ErrorObserver from "./ErrorObserver.js"
|
|
||||||
export * as Form from "./Form.js"
|
|
||||||
export * as Memoized from "./Memoized.js"
|
|
||||||
export * as Mutation from "./Mutation.js"
|
|
||||||
export * as PropertyPath from "./PropertyPath.js"
|
|
||||||
export * as PubSub from "./PubSub.js"
|
|
||||||
export * as Query from "./Query.js"
|
|
||||||
export * as QueryClient from "./QueryClient.js"
|
|
||||||
export * as ReactRuntime from "./ReactRuntime.js"
|
|
||||||
export * as Result from "./Result.js"
|
|
||||||
export * as SetStateAction from "./SetStateAction.js"
|
|
||||||
export * as Stream from "./Stream.js"
|
|
||||||
export * as Subscribable from "./Subscribable.js"
|
|
||||||
export * as SubscriptionRef from "./SubscriptionRef.js"
|
|
||||||
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export type ExcludeKeys<T, K extends PropertyKey> = K extends keyof T ? (
|
|
||||||
{ [P in K]?: never } & Omit<T, K>
|
|
||||||
) : T
|
|
||||||
Reference in New Issue
Block a user