From 9f2704333b4e375ad8da4e3c436fe2cdfb459248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 23 Jan 2026 02:53:59 +0100 Subject: [PATCH] Cleanup --- bun.lock | 115 +++ packages/effect-docker/src/Async.ts | 85 -- packages/effect-docker/src/Component.ts | 732 ------------------ packages/effect-docker/src/ErrorObserver.ts | 62 -- packages/effect-docker/src/Form.ts | 396 ---------- packages/effect-docker/src/Memoized.ts | 51 -- packages/effect-docker/src/Mutation.ts | 128 --- packages/effect-docker/src/PropertyPath.ts | 98 --- packages/effect-docker/src/PubSub.ts | 14 - packages/effect-docker/src/Query.ts | 316 -------- packages/effect-docker/src/QueryClient.ts | 173 ----- packages/effect-docker/src/ReactRuntime.ts | 85 -- packages/effect-docker/src/Result.ts | 279 ------- packages/effect-docker/src/SetStateAction.ts | 12 - packages/effect-docker/src/Stream.ts | 33 - packages/effect-docker/src/Subscribable.ts | 52 -- packages/effect-docker/src/SubscriptionRef.ts | 61 -- .../effect-docker/src/SubscriptionSubRef.ts | 186 ----- packages/effect-docker/src/index.ts | 17 - packages/effect-docker/src/utils.ts | 3 - 20 files changed, 115 insertions(+), 2783 deletions(-) create mode 100644 bun.lock delete mode 100644 packages/effect-docker/src/Async.ts delete mode 100644 packages/effect-docker/src/Component.ts delete mode 100644 packages/effect-docker/src/ErrorObserver.ts delete mode 100644 packages/effect-docker/src/Form.ts delete mode 100644 packages/effect-docker/src/Memoized.ts delete mode 100644 packages/effect-docker/src/Mutation.ts delete mode 100644 packages/effect-docker/src/PropertyPath.ts delete mode 100644 packages/effect-docker/src/PubSub.ts delete mode 100644 packages/effect-docker/src/Query.ts delete mode 100644 packages/effect-docker/src/QueryClient.ts delete mode 100644 packages/effect-docker/src/ReactRuntime.ts delete mode 100644 packages/effect-docker/src/Result.ts delete mode 100644 packages/effect-docker/src/SetStateAction.ts delete mode 100644 packages/effect-docker/src/Stream.ts delete mode 100644 packages/effect-docker/src/Subscribable.ts delete mode 100644 packages/effect-docker/src/SubscriptionRef.ts delete mode 100644 packages/effect-docker/src/SubscriptionSubRef.ts delete mode 100644 packages/effect-docker/src/utils.ts diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..f2c4e5d --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/packages/effect-docker/src/Async.ts b/packages/effect-docker/src/Async.ts deleted file mode 100644 index 3d1f02b..0000000 --- a/packages/effect-docker/src/Async.ts +++ /dev/null @@ -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 -} - - -const AsyncProto = Object.freeze({ - [TypeId]: TypeId, - - makeFunctionComponent

( - this: Component.Component & Async, - runtimeRef: React.RefObject>>, - ) { - const SuspenseInner = (props: { readonly promise: Promise }) => 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 = >( - self: T -): ( - & Omit> - & Component.Component< - Component.Component.Props & Async.Props, - Component.Component.Success, - Component.Component.Error, - Component.Component.Context - > - & Async -) => Object.setPrototypeOf( - Object.assign(function() {}, self), - Object.freeze(Object.setPrototypeOf( - Object.assign({}, AsyncProto), - Object.getPrototypeOf(self), - )), -) - -export const withOptions: { - & Async>( - options: Partial - ): (self: T) => T - & Async>( - self: T, - options: Partial, - ): T -} = Function.dual(2, & Async>( - self: T, - options: Partial, -): T => Object.setPrototypeOf( - Object.assign(function() {}, self, options), - Object.getPrototypeOf(self), -)) diff --git a/packages/effect-docker/src/Component.ts b/packages/effect-docker/src/Component.ts deleted file mode 100644 index 3cd4b2d..0000000 --- a/packages/effect-docker/src/Component.ts +++ /dev/null @@ -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

-extends - Effect.Effect<(props: P) => A, never, Exclude>, - Component.Options -{ - new(_: never): Record - readonly [TypeId]: TypeId - readonly "~Props": P - readonly "~Success": A - readonly "~Error": E - readonly "~Context": R - - readonly body: (props: P) => Effect.Effect - - /** @internal */ - makeFunctionComponent( - runtimeRef: React.Ref>> - ): (props: P) => A -} - -export declare namespace Component { - export type Props> = [T] extends [Component] ? P : never - export type Success> = [T] extends [Component] ? A : never - export type Error> = [T] extends [Component] ? E : never - export type Context> = [T] extends [Component] ? R : never - - export type AsComponent> = Component, Success, Error, Context> - - /** - * 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*

( - this: Component - ) { - // biome-ignore lint/style/noNonNullAssertion: React ref initialization - const runtimeRef = React.useRef>>(null!) - runtimeRef.current = yield* Effect.runtime>() - - return yield* React.useState(() => Runtime.runSync(runtimeRef.current)(Effect.cachedFunction( - (_services: readonly any[]) => Effect.sync(() => { - const f: React.FC

= 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

( - this: Component, - runtimeRef: React.RefObject>>, - ) { - 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 = { - >, A extends React.ReactNode, P extends {} = {}>( - body: (props: P) => Generator - ): Component< - P, A, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never - > - >, A, B extends Effect.Effect, P extends {} = {}>( - body: (props: P) => Generator, - a: ( - _: Effect.Effect< - A, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never - >, - props: NoInfer

, - ) => B, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - >, A, B, C extends Effect.Effect, P extends {} = {}>( - body: (props: P) => Generator, - a: ( - _: Effect.Effect< - A, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never - >, - props: NoInfer

, - ) => B, - b: (_: B, props: NoInfer

) => C, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - >, A, B, C, D extends Effect.Effect, P extends {} = {}>( - body: (props: P) => Generator, - a: ( - _: Effect.Effect< - A, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never - >, - props: NoInfer

, - ) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - >, A, B, C, D, E extends Effect.Effect, P extends {} = {}>( - body: (props: P) => Generator, - a: ( - _: Effect.Effect< - A, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never - >, - props: NoInfer

, - ) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - >, A, B, C, D, E, F extends Effect.Effect, P extends {} = {}>( - body: (props: P) => Generator, - a: ( - _: Effect.Effect< - A, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never - >, - props: NoInfer

, - ) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - e: (_: E, props: NoInfer

) => F, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - >, A, B, C, D, E, F, G extends Effect.Effect, P extends {} = {}>( - body: (props: P) => Generator, - a: ( - _: Effect.Effect< - A, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never - >, - props: NoInfer

, - ) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - e: (_: E, props: NoInfer

) => F, - f: (_: F, props: NoInfer

) => G, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - >, A, B, C, D, E, F, G, H extends Effect.Effect, P extends {} = {}>( - body: (props: P) => Generator, - a: ( - _: Effect.Effect< - A, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never - >, - props: NoInfer

, - ) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - e: (_: E, props: NoInfer

) => F, - f: (_: F, props: NoInfer

) => G, - g: (_: G, props: NoInfer

) => H, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - >, A, B, C, D, E, F, G, H, I extends Effect.Effect, P extends {} = {}>( - body: (props: P) => Generator, - a: ( - _: Effect.Effect< - A, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never - >, - props: NoInfer

, - ) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - e: (_: E, props: NoInfer

) => F, - f: (_: F, props: NoInfer

) => G, - g: (_: G, props: NoInfer

) => H, - h: (_: H, props: NoInfer

) => I, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - >, A, B, C, D, E, F, G, H, I, J extends Effect.Effect, P extends {} = {}>( - body: (props: P) => Generator, - a: ( - _: Effect.Effect< - A, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? E : never, - [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap>] ? R : never - >, - props: NoInfer

, - ) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - e: (_: E, props: NoInfer

) => F, - f: (_: F, props: NoInfer

) => G, - g: (_: G, props: NoInfer

) => H, - h: (_: H, props: NoInfer

) => I, - i: (_: I, props: NoInfer

) => J, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - } - - export type NonGen = { - , P extends {} = {}>( - body: (props: P) => Eff - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - , A, P extends {} = {}>( - body: (props: P) => A, - a: (_: A, props: NoInfer

) => Eff, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - , A, B, P extends {} = {}>( - body: (props: P) => A, - a: (_: A, props: NoInfer

) => B, - b: (_: B, props: NoInfer

) => Eff, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - , A, B, C, P extends {} = {}>( - body: (props: P) => A, - a: (_: A, props: NoInfer

) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => Eff, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - , A, B, C, D, P extends {} = {}>( - body: (props: P) => A, - a: (_: A, props: NoInfer

) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => Eff, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - , A, B, C, D, E, P extends {} = {}>( - body: (props: P) => A, - a: (_: A, props: NoInfer

) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - e: (_: E, props: NoInfer

) => Eff, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - , A, B, C, D, E, F, P extends {} = {}>( - body: (props: P) => A, - a: (_: A, props: NoInfer

) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - e: (_: E, props: NoInfer

) => F, - f: (_: F, props: NoInfer

) => Eff, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - , A, B, C, D, E, F, G, P extends {} = {}>( - body: (props: P) => A, - a: (_: A, props: NoInfer

) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - e: (_: E, props: NoInfer

) => F, - f: (_: F, props: NoInfer

) => G, - g: (_: G, props: NoInfer

) => Eff, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - , A, B, C, D, E, F, G, H, P extends {} = {}>( - body: (props: P) => A, - a: (_: A, props: NoInfer

) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - e: (_: E, props: NoInfer

) => F, - f: (_: F, props: NoInfer

) => G, - g: (_: G, props: NoInfer

) => H, - h: (_: H, props: NoInfer

) => Eff, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - , A, B, C, D, E, F, G, H, I, P extends {} = {}>( - body: (props: P) => A, - a: (_: A, props: NoInfer

) => B, - b: (_: B, props: NoInfer

) => C, - c: (_: C, props: NoInfer

) => D, - d: (_: D, props: NoInfer

) => E, - e: (_: E, props: NoInfer

) => F, - f: (_: F, props: NoInfer

) => G, - g: (_: G, props: NoInfer

) => H, - h: (_: H, props: NoInfer

) => I, - i: (_: I, props: NoInfer

) => Eff, - ): Component>, Effect.Effect.Error, Effect.Effect.Context> - } -} - -/** - * 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: { - >( - options: Partial - ): (self: T) => T - >( - self: T, - options: Partial, - ): T -} = Function.dual(2, >( - self: T, - options: Partial, -): 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 ( - * - * - * - * ) - * } - * - * // 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: { -

( - context: React.Context>, - ): (self: Component>) => (props: P) => A -

( - self: Component>, - context: React.Context>, - ): (props: P) => A -} = Function.dual(2,

( - self: Component, - context: React.Context>, -) => 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()("@effect-fc/Component/ScopeMap", { - effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty())) -}) {} - -export declare namespace ScopeMap { - export interface Entry { - readonly scope: Scope.CloseableScope - readonly closeFiber: Option.Option> - } -} - - -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 { - // biome-ignore lint/style/noNonNullAssertion: context initialization - const runtimeRef = React.useRef>(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.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).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* ( - f: () => Effect.Effect -): Effect.fn.Return { - const runtime = yield* Effect.runtime() - 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* ( - f: () => Effect.Effect, - deps: React.DependencyList, - options?: useOnChange.Options, -): Effect.fn.Return> { - const runtime = yield* Effect.runtime>() - 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* ( - f: () => Effect.Effect, - deps?: React.DependencyList, - options?: useReactEffect.Options, -): Effect.fn.Return> { - const runtime = yield* Effect.runtime>() - // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList - React.useEffect(() => runReactEffect(runtime, f, options), deps) -}) - -const runReactEffect = ( - runtime: Runtime.Runtime>, - f: () => Effect.Effect, - 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* ( - f: () => Effect.Effect, - deps?: React.DependencyList, - options?: useReactLayoutEffect.Options, -): Effect.fn.Return> { - const runtime = yield* Effect.runtime>() - // 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 = (): Effect.Effect< - (effect: Effect.Effect) => 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 = (): Effect.Effect< - (effect: Effect.Effect) => Promise, - 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* ( - f: (...args: Args) => Effect.Effect, - deps: React.DependencyList, -): Effect.fn.Return<(...args: Args) => A, never, R> { - // biome-ignore lint/style/noNonNullAssertion: context initialization - const runtimeRef = React.useRef>(null!) - runtimeRef.current = yield* Effect.runtime() - - // 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* ( - f: (...args: Args) => Effect.Effect, - deps: React.DependencyList, -): Effect.fn.Return<(...args: Args) => Promise, never, R> { - // biome-ignore lint/style/noNonNullAssertion: context initialization - const runtimeRef = React.useRef>(null!) - runtimeRef.current = yield* Effect.runtime() - - // 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 = ( - layer: Layer.Layer, - options?: useContext.Options, -): Effect.Effect, E, Exclude> => useOnChange(() => Effect.context().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) diff --git a/packages/effect-docker/src/ErrorObserver.ts b/packages/effect-docker/src/ErrorObserver.ts deleted file mode 100644 index 30f5424..0000000 --- a/packages/effect-docker/src/ErrorObserver.ts +++ /dev/null @@ -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 extends Pipeable.Pipeable { - readonly [TypeId]: TypeId - handle(effect: Effect.Effect): Effect.Effect - readonly subscribe: Effect.Effect>, never, Scope.Scope> -} - -export const ErrorObserver = (): Context.Tag> => Context.GenericTag("@effect-fc/ErrorObserver/ErrorObserver") - -class ErrorObserverImpl -extends Pipeable.Class() implements ErrorObserver { - readonly [TypeId]: TypeId = TypeId - readonly subscribe: Effect.Effect>, never, Scope.Scope> - - constructor( - readonly pubsub: PubSub.PubSub> - ) { - super() - this.subscribe = pubsub.subscribe - } - - handle(effect: Effect.Effect): Effect.Effect { - return Effect.tapErrorCause(effect, cause => PubSub.publish(this.pubsub, cause as Cause.Cause)) - } -} - -class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor { - readonly value = Effect.void - constructor(readonly pubsub: PubSub.PubSub>) { - super() - } - - onEnd(_value: Exit.Exit): void { - if (Exit.isFailure(_value)) { - Effect.runSync(PubSub.publish(this.pubsub, _value.cause as Cause.Cause)) - } - } -} - - -export const isErrorObserver = (u: unknown): u is ErrorObserver => Predicate.hasProperty(u, TypeId) - -export const layer: Layer.Layer = Layer.unwrapEffect(Effect.map( - PubSub.unbounded>(), - pubsub => Layer.merge( - Supervisor.addSupervisor(new ErrorObserverSupervisorImpl(pubsub)), - Layer.succeed(ErrorObserver(), new ErrorObserverImpl(pubsub)), - ), -)) - -export const handle = (effect: Effect.Effect): Effect.Effect => Effect.andThen( - Effect.serviceOption(ErrorObserver()), - Option.match({ - onSome: observer => observer.handle(effect), - onNone: () => effect, - }), -) diff --git a/packages/effect-docker/src/Form.ts b/packages/effect-docker/src/Form.ts deleted file mode 100644 index a44da20..0000000 --- a/packages/effect-docker/src/Form.ts +++ /dev/null @@ -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 -extends Pipeable.Pipeable { - readonly [FormTypeId]: FormTypeId - - readonly schema: Schema.Schema - readonly context: Context.Context - readonly mutation: Mutation.Mutation< - readonly [value: A, form: Form], - MA, ME, MR, MP - > - readonly autosubmit: boolean - readonly debounce: Option.Option - - readonly value: Subscribable.Subscribable> - readonly encodedValue: SubscriptionRef.SubscriptionRef - readonly error: Subscribable.Subscribable> - readonly validationFiber: Subscribable.Subscribable>> - - readonly canSubmit: Subscribable.Subscribable - - field>( - path: P - ): Effect.Effect, PropertyPath.ValueFromPath>> - - readonly run: Effect.Effect - readonly submit: Effect.Effect>, Cause.NoSuchElementException> -} - -export class FormImpl -extends Pipeable.Class() implements Form { - readonly [FormTypeId]: FormTypeId = FormTypeId - - constructor( - readonly schema: Schema.Schema, - readonly context: Context.Context, - readonly mutation: Mutation.Mutation< - readonly [value: A, form: Form], - MA, ME, MR, MP - >, - readonly autosubmit: boolean, - readonly debounce: Option.Option, - - readonly value: SubscriptionRef.SubscriptionRef>, - readonly encodedValue: SubscriptionRef.SubscriptionRef, - readonly error: SubscriptionRef.SubscriptionRef>, - readonly validationFiber: SubscriptionRef.SubscriptionRef>>, - - readonly runSemaphore: Effect.Semaphore, - readonly fieldCache: Ref.Ref>>, - ) { - 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>( - path: P - ): Effect.Effect, PropertyPath.ValueFromPath>> { - 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>), - onNone: () => Effect.tap( - Effect.succeed(makeFormField(this as Form, path)), - v => Ref.update(this.fieldCache, HashMap.set(key, v as FormField)), - ), - })), - ) - } - - readonly canSubmit: Subscribable.Subscribable - - get run(): Effect.Effect { - 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>, Cause.NoSuchElementException> { - return this.value.pipe( - Effect.andThen(identity), - Effect.andThen(value => this.submitValue(value)), - ) - } - - submitValue(value: A): Effect.Effect>> { - 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), - 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 => Predicate.hasProperty(u, FormTypeId) - -export declare namespace make { - export interface Options - extends Mutation.make.Options< - readonly [value: NoInfer, form: Form, NoInfer, NoInfer, unknown, unknown, unknown>], - MA, ME, MR, MP - > { - readonly schema: Schema.Schema - readonly initialEncodedValue: NoInfer - readonly autosubmit?: boolean - readonly debounce?: Duration.DurationInput - } -} - -export const make = Effect.fnUntraced(function* ( - options: make.Options -): Effect.fn.Return< - Form, MP>, - never, - Scope.Scope | R | Result.forkEffect.OutputContext -> { - return new FormImpl( - options.schema, - yield* Effect.context(), - yield* Mutation.make(options), - options.autosubmit ?? false, - Option.fromNullable(options.debounce), - - yield* SubscriptionRef.make(Option.none()), - yield* SubscriptionRef.make(options.initialEncodedValue), - yield* SubscriptionRef.make(Option.none()), - yield* SubscriptionRef.make(Option.none>()), - - yield* Effect.makeSemaphore(1), - yield* Ref.make(HashMap.empty>()), - ) -}) - -export declare namespace service { - export interface Options - extends make.Options {} -} - -export const service = ( - options: service.Options -): Effect.Effect< - Form, MP>, - never, - Scope.Scope | R | Result.forkEffect.OutputContext -> => 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 -extends Pipeable.Pipeable { - readonly [FormFieldTypeId]: FormFieldTypeId - - readonly value: Subscribable.Subscribable, Cause.NoSuchElementException> - readonly encodedValue: SubscriptionRef.SubscriptionRef - readonly issues: Subscribable.Subscribable - readonly isValidating: Subscribable.Subscribable - readonly isSubmitting: Subscribable.Subscribable -} - -class FormFieldImpl -extends Pipeable.Class() implements FormField { - readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId - - constructor( - readonly value: Subscribable.Subscribable, Cause.NoSuchElementException>, - readonly encodedValue: SubscriptionRef.SubscriptionRef, - readonly issues: Subscribable.Subscribable, - readonly isValidating: Subscribable.Subscribable, - readonly isSubmitting: Subscribable.Subscribable, - ) { - 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 => Predicate.hasProperty(u, FormFieldTypeId) -const isFormFieldKey = (u: unknown): u is FormFieldKey => Predicate.hasProperty(u, FormFieldKeyTypeId) - -export const makeFormField = >>( - self: Form, - path: P, -): FormField, PropertyPath.ValueFromPath> => { - 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 { - readonly value: T - readonly setValue: React.Dispatch> - } -} - -export const useInput = Effect.fnUntraced(function* ( - field: FormField, - options?: useInput.Options, -): Effect.fn.Return, 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 extends useInput.Options { - readonly defaultValue: T - } - - export interface Success extends useInput.Success { - readonly enabled: boolean - readonly setEnabled: React.Dispatch> - } -} - -export const useOptionalInput = Effect.fnUntraced(function* ( - field: FormField>, - options: useOptionalInput.Options, -): Effect.fn.Return, 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 } -}) diff --git a/packages/effect-docker/src/Memoized.ts b/packages/effect-docker/src/Memoized.ts deleted file mode 100644 index 61cea91..0000000 --- a/packages/effect-docker/src/Memoized.ts +++ /dev/null @@ -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

extends Memoized.Options

{ - readonly [TypeId]: TypeId -} - -export namespace Memoized { - export interface Options

{ - readonly propsAreEqual?: Equivalence.Equivalence

- } -} - - -const MemoizedProto = Object.freeze({ - [TypeId]: TypeId -} as const) - - -export const isMemoized = (u: unknown): u is Memoized => Predicate.hasProperty(u, TypeId) - -export const memoized = >( - self: T -): T & Memoized> => Object.setPrototypeOf( - Object.assign(function() {}, self), - Object.freeze(Object.setPrototypeOf( - Object.assign({}, MemoizedProto), - Object.getPrototypeOf(self), - )), -) - -export const withOptions: { - & Memoized>( - options: Partial>> - ): (self: T) => T - & Memoized>( - self: T, - options: Partial>>, - ): T -} = Function.dual(2, & Memoized>( - self: T, - options: Partial>>, -): T => Object.setPrototypeOf( - Object.assign(function() {}, self, options), - Object.getPrototypeOf(self), -)) diff --git a/packages/effect-docker/src/Mutation.ts b/packages/effect-docker/src/Mutation.ts deleted file mode 100644 index be4b71e..0000000 --- a/packages/effect-docker/src/Mutation.ts +++ /dev/null @@ -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 -extends Pipeable.Pipeable { - readonly [MutationTypeId]: MutationTypeId - - readonly context: Context.Context - readonly f: (key: K) => Effect.Effect - readonly initialProgress: P - - readonly latestKey: Subscribable.Subscribable> - readonly fiber: Subscribable.Subscribable>> - readonly result: Subscribable.Subscribable> - readonly latestFinalResult: Subscribable.Subscribable>> - - mutate(key: K): Effect.Effect> - mutateSubscribable(key: K): Effect.Effect>> -} - -export declare namespace Mutation { - export type AnyKey = readonly any[] -} - -export class MutationImpl -extends Pipeable.Class() implements Mutation { - readonly [MutationTypeId]: MutationTypeId = MutationTypeId - - constructor( - readonly context: Context.Context, - readonly f: (key: K) => Effect.Effect, - readonly initialProgress: P, - - readonly latestKey: SubscriptionRef.SubscriptionRef>, - readonly fiber: SubscriptionRef.SubscriptionRef>>, - readonly result: SubscriptionRef.SubscriptionRef>, - readonly latestFinalResult: SubscriptionRef.SubscriptionRef>>, - ) { - super() - } - - mutate(key: K): Effect.Effect> { - 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>> { - 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>, - 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, - )), - Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))), - Effect.map(([sub]) => sub), - ) - } - - watch( - sub: Subscribable.Subscribable> - ): Effect.Effect> { - return sub.get.pipe( - Effect.andThen(initial => Stream.runFoldEffect( - sub.changes, - initial, - (_, result) => Effect.as(SubscriptionRef.set(this.result, result), result), - ) as Effect.Effect>), - Effect.tap(result => SubscriptionRef.set(this.latestFinalResult, Option.some(result))), - ) - } -} - -export const isMutation = (u: unknown): u is Mutation => Predicate.hasProperty(u, MutationTypeId) - -export declare namespace make { - export interface Options { - readonly f: (key: K) => Effect.Effect>> - readonly initialProgress?: P - } -} - -export const make = Effect.fnUntraced(function* ( - options: make.Options -): Effect.fn.Return< - Mutation, P>, - never, - Scope.Scope | Result.forkEffect.OutputContext -> { - return new MutationImpl( - yield* Effect.context>(), - options.f as any, - options.initialProgress as P, - - yield* SubscriptionRef.make(Option.none()), - yield* SubscriptionRef.make(Option.none>()), - yield* SubscriptionRef.make(Result.initial()), - yield* SubscriptionRef.make(Option.none>()), - ) -}) diff --git a/packages/effect-docker/src/PropertyPath.ts b/packages/effect-docker/src/PropertyPath.ts deleted file mode 100644 index b73d24d..0000000 --- a/packages/effect-docker/src/PropertyPath.ts +++ /dev/null @@ -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 = 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] - } extends infer O - ? O[keyof O] - : never - : - T extends object ? { - [K in keyof T as K extends string | number | symbol ? K : never]-?: - NonNullable extends infer V - ? readonly [K] | readonly [K, ...Paths] - : never - } extends infer O - ? O[keyof O] - : never - : - never -) - -export type ValueFromPath = P extends readonly [infer Head, ...infer Tail] - ? Head extends keyof T - ? ValueFromPath - : T extends readonly any[] - ? Head extends number - ? ValueFromPath - : never - : never - : T - - -export const equivalence: Equivalence.Equivalence = Equivalence.array(Equivalence.strict()) - -export const unsafeGet: { - >(path: P): (self: T) => ValueFromPath - >(self: T, path: P): ValueFromPath -} = Function.dual(2, >(self: T, path: P): ValueFromPath => - path.reduce((acc: any, key: any) => acc?.[key], self) -) - -export const get: { - >(path: P): (self: T) => Option.Option> - >(self: T, path: P): Option.Option> -} = Function.dual(2, >(self: T, path: P): Option.Option> => - path.reduce( - (acc: Option.Option, key: any): Option.Option => Option.isSome(acc) - ? Predicate.hasProperty(acc.value, key) - ? Option.some(acc.value[key]) - : Option.none() - : acc, - - Option.some(self), - ) -) - -export const immutableSet: { - >(path: P, value: ValueFromPath): (self: T) => Option.Option - >(self: T, path: P, value: ValueFromPath): Option.Option -} = Function.dual(3, >(self: T, path: P, value: ValueFromPath): Option.Option => { - 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(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() -}) diff --git a/packages/effect-docker/src/PubSub.ts b/packages/effect-docker/src/PubSub.ts deleted file mode 100644 index 36eccbe..0000000 --- a/packages/effect-docker/src/PubSub.ts +++ /dev/null @@ -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* ( - values: A -): Effect.fn.Return, never, Scope.Scope> { - const pubsub = yield* Component.useOnMount(() => Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown)) - yield* Component.useReactEffect(() => Effect.unlessEffect(PubSub.publish(pubsub, values), PubSub.isShutdown(pubsub)), values) - return pubsub -}) - -export * from "effect/PubSub" diff --git a/packages/effect-docker/src/Query.ts b/packages/effect-docker/src/Query.ts deleted file mode 100644 index 9c306b7..0000000 --- a/packages/effect-docker/src/Query.ts +++ /dev/null @@ -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 -extends Pipeable.Pipeable { - readonly [QueryTypeId]: QueryTypeId - - readonly context: Context.Context - readonly key: Stream.Stream - readonly f: (key: K) => Effect.Effect - readonly initialProgress: P - - readonly staleTime: Duration.DurationInput - readonly refreshOnWindowFocus: boolean - - readonly latestKey: Subscribable.Subscribable> - readonly fiber: Subscribable.Subscribable>> - readonly result: Subscribable.Subscribable> - readonly latestFinalResult: Subscribable.Subscribable>> - - readonly run: Effect.Effect - fetch(key: K): Effect.Effect> - fetchSubscribable(key: K): Effect.Effect>> - readonly refresh: Effect.Effect, Cause.NoSuchElementException> - readonly refreshSubscribable: Effect.Effect>, Cause.NoSuchElementException> - - readonly invalidateCache: Effect.Effect - invalidateCacheEntry(key: K): Effect.Effect -} - -export declare namespace Query { - export type AnyKey = readonly any[] -} - -export class QueryImpl -extends Pipeable.Class() implements Query { - readonly [QueryTypeId]: QueryTypeId = QueryTypeId - - constructor( - readonly context: Context.Context, - readonly key: Stream.Stream, - readonly f: (key: K) => Effect.Effect, - readonly initialProgress: P, - - readonly staleTime: Duration.DurationInput, - readonly refreshOnWindowFocus: boolean, - - readonly latestKey: SubscriptionRef.SubscriptionRef>, - readonly fiber: SubscriptionRef.SubscriptionRef>>, - readonly result: SubscriptionRef.SubscriptionRef>, - readonly latestFinalResult: SubscriptionRef.SubscriptionRef>>, - - readonly runSemaphore: Effect.Semaphore, - ) { - super() - } - - get run(): Effect.Effect { - 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 { - return Effect.andThen(this.fiber, Option.match({ - onSome: Fiber.interrupt, - onNone: () => Effect.void, - })) - } - - fetch(key: K): Effect.Effect> { - 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 - : Result.initial() - )), - Effect.andThen(sub => this.watch(key, sub)), - Effect.provide(this.context), - ) - } - - fetchSubscribable(key: K): Effect.Effect>> { - 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 - : Result.initial() - )), - Effect.tap(sub => Effect.forkScoped(this.watch(key, sub))), - Effect.provide(this.context), - ) - } - - get refresh(): Effect.Effect, 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 - : Result.initial() - ) - ), - Effect.andThen(({ latestKey, subscribable }) => this.watch(latestKey, subscribable)), - Effect.provide(this.context), - ) - } - - get refreshSubscribable(): Effect.Effect< - Subscribable.Subscribable>, - 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 - : 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, - ): Effect.Effect< - Subscribable.Subscribable>, - 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) - : Effect.succeed(Subscribable.make({ - get: Effect.succeed(entry.result as Result.Result), - get changes() { return Stream.make(entry.result as Result.Result) }, - })), - ), - onNone: () => this.start(key, initial), - })) - } - - start( - key: K, - initial: Result.Initial | Result.Final, - ): Effect.Effect< - Subscribable.Subscribable>, - 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, - ).pipe( - Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))), - Effect.map(([sub]) => sub), - ) - } - - watch( - key: K, - sub: Subscribable.Subscribable> - ): Effect.Effect, 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>), - 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) - } - - getCacheEntry( - key: K - ): Effect.Effect, 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, - ): Effect.Effect { - return Effect.andThen( - Effect.all([ - Effect.succeed(this.makeCacheKey(key)), - QueryClient.QueryClient, - ]), - ([key, client]) => client.setCacheEntry(key, result, this.staleTime), - ) - } - - get invalidateCache(): Effect.Effect { - return QueryClient.QueryClient.pipe( - Effect.andThen(client => client.invalidateCacheEntries(this.f as (key: Query.AnyKey) => Effect.Effect)), - Effect.provide(this.context), - ) - } - - invalidateCacheEntry(key: K): Effect.Effect { - 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 => Predicate.hasProperty(u, QueryTypeId) - -export declare namespace make { - export interface Options { - readonly key: Stream.Stream - readonly f: (key: NoInfer) => Effect.Effect>> - readonly initialProgress?: P - readonly staleTime?: Duration.DurationInput - readonly refreshOnWindowFocus?: boolean - } -} - -export const make = Effect.fnUntraced(function* ( - options: make.Options -): Effect.fn.Return< - Query, P>, - never, - Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext -> { - const client = yield* QueryClient.QueryClient - - return new QueryImpl( - yield* Effect.context>(), - options.key, - options.f as any, - options.initialProgress as P, - - options.staleTime ?? client.defaultStaleTime, - options.refreshOnWindowFocus ?? client.defaultRefreshOnWindowFocus, - - yield* SubscriptionRef.make(Option.none()), - yield* SubscriptionRef.make(Option.none>()), - yield* SubscriptionRef.make(Result.initial()), - yield* SubscriptionRef.make(Option.none>()), - - yield* Effect.makeSemaphore(1), - ) -}) - -export const service = ( - options: make.Options -): Effect.Effect< - Query, P>, - never, - Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext -> => Effect.tap( - make(options), - query => Effect.forkScoped(query.run), -) diff --git a/packages/effect-docker/src/QueryClient.ts b/packages/effect-docker/src/QueryClient.ts deleted file mode 100644 index fde7005..0000000 --- a/packages/effect-docker/src/QueryClient.ts +++ /dev/null @@ -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> - readonly cacheGcTime: Duration.DurationInput - readonly defaultStaleTime: Duration.DurationInput - readonly defaultRefreshOnWindowFocus: boolean - - readonly run: Effect.Effect - getCacheEntry(key: QueryClientCacheKey): Effect.Effect> - setCacheEntry( - key: QueryClientCacheKey, - result: Result.Success, - staleTime: Duration.DurationInput, - ): Effect.Effect - invalidateCacheEntries(f: (key: Query.Query.AnyKey) => Effect.Effect): Effect.Effect - invalidateCacheEntry(key: QueryClientCacheKey): Effect.Effect -} - -export class QueryClient extends Effect.Service()("@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>, - readonly cacheGcTime: Duration.DurationInput, - readonly defaultStaleTime: Duration.DurationInput, - readonly defaultRefreshOnWindowFocus: boolean, - readonly runSemaphore: Effect.Semaphore, - ) { - super() - } - - get run(): Effect.Effect { - 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> { - 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, - staleTime: Duration.DurationInput, - ): Effect.Effect { - 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): Effect.Effect { - return SubscriptionRef.update(this.cache, HashMap.filter((_, key) => !Equivalence.strict()(key.f, f))) - } - invalidateCacheEntry(key: QueryClientCacheKey): Effect.Effect { - 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 { - return new QueryClientServiceImpl( - yield* SubscriptionRef.make(HashMap.empty()), - 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 => 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, - ) { - 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, - 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 => Effect.andThen( - DateTime.now, - now => Duration.greaterThanOrEqualTo(DateTime.distanceDuration(self.createdAt, now), self.staleTime), -) diff --git a/packages/effect-docker/src/ReactRuntime.ts b/packages/effect-docker/src/ReactRuntime.ts deleted file mode 100644 index a1a858b..0000000 --- a/packages/effect-docker/src/ReactRuntime.ts +++ /dev/null @@ -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 { - new(_: never): Record - readonly [TypeId]: TypeId - readonly runtime: ManagedRuntime.ManagedRuntime - readonly context: React.Context> -} - -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 => Predicate.hasProperty(u, TypeId) - -export const make = ( - layer: Layer.Layer, - memoMap?: Layer.MemoMap, -): ReactRuntime | 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>(null!), - }), - ReactRuntimeProto, -) - - -export namespace Provider { - export interface Props extends React.SuspenseProps { - readonly runtime: ReactRuntime - readonly children?: React.ReactNode - } -} - -export const Provider = ( - { runtime, children, ...suspenseProps }: Provider.Props -): React.ReactNode => { - const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime]) - - return React.createElement( - React.Suspense, - suspenseProps, - React.createElement(ProviderInner, { runtime, promise, children }), - ) -} - -const ProviderInner = ( - { runtime, promise, children }: { - readonly runtime: ReactRuntime - readonly promise: Promise> - 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) -} diff --git a/packages/effect-docker/src/Result.ts b/packages/effect-docker/src/Result.ts deleted file mode 100644 index 3cef66c..0000000 --- a/packages/effect-docker/src/Result.ts +++ /dev/null @@ -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 = ( - | Initial - | Running

- | Final -) - -// biome-ignore lint/complexity/noBannedTypes: "{}" is relevant here -export type Final = (Success | Failure) & ({} | Flags

) -export type Flags

= WillFetch | WillRefresh | Refreshing

- -export declare namespace Result { - export interface Prototype extends Pipeable.Pipeable, Equal.Equal { - readonly [ResultTypeId]: ResultTypeId - } - - export type Success> = [R] extends [Result] ? A : never - export type Failure> = [R] extends [Result] ? E : never - export type Progress> = [R] extends [Result] ? P : never -} - -export declare namespace Flags { - export type Keys = keyof WillFetch & WillRefresh & Refreshing -} - -export interface Initial extends Result.Prototype { - readonly _tag: "Initial" -} - -export interface Running

extends Result.Prototype { - readonly _tag: "Running" - readonly progress: P -} - -export interface Success extends Result.Prototype { - readonly _tag: "Success" - readonly value: A -} - -export interface Failure extends Result.Prototype { - readonly _tag: "Failure" - readonly cause: Cause.Cause -} - -export interface WillFetch { - readonly _flag: "WillFetch" -} - -export interface WillRefresh { - readonly _flag: "WillRefresh" -} - -export interface Refreshing

{ - readonly _flag: "Refreshing" - readonly progress: P -} - - -const ResultPrototype = Object.freeze({ - ...Pipeable.Prototype, - [ResultTypeId]: ResultTypeId, - - [Equal.symbol](this: Result, that: Result): 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).progress)) - return false - return Match.value(this).pipe( - Match.tag("Initial", () => true), - Match.tag("Running", self => Equal.equals(self.progress, (that as Running).progress)), - Match.tag("Success", self => Equal.equals(self.value, (that as Success).value)), - Match.tag("Failure", self => Equal.equals(self.cause, (that as Failure).cause)), - Match.exhaustive, - ) - }, - - [Hash.symbol](this: Result): 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 => Predicate.hasProperty(u, ResultTypeId) -export const isFinal = (u: unknown): u is Final => 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 => isResult(u) && u._tag === "Running" -export const isSuccess = (u: unknown): u is Success => isResult(u) && u._tag === "Success" -export const isFailure = (u: unknown): u is Failure => 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 => isResult(u) && Predicate.hasProperty(u, "_flag") && u._flag === "Refreshing" - -export const initial: { - (): Initial - (): Result -} = (): Initial => Object.setPrototypeOf({ _tag: "Initial" }, ResultPrototype) -export const running =

(progress?: P): Running

=> Object.setPrototypeOf({ _tag: "Running", progress }, ResultPrototype) -export const succeed = (value: A): Success => Object.setPrototypeOf({ _tag: "Success", value }, ResultPrototype) -export const fail = (cause: Cause.Cause ): Failure => Object.setPrototypeOf({ _tag: "Failure", cause }, ResultPrototype) - -export const willFetch = >( - result: R -): Omit & WillFetch => Object.setPrototypeOf( - Object.assign({}, result, { _flag: "WillFetch" }), - Object.getPrototypeOf(result), -) - -export const willRefresh = >( - result: R -): Omit & WillRefresh => Object.setPrototypeOf( - Object.assign({}, result, { _flag: "WillRefresh" }), - Object.getPrototypeOf(result), -) - -export const refreshing = , P = never>( - result: R, - progress?: P, -): Omit & Refreshing

=> Object.setPrototypeOf( - Object.assign({}, result, { _flag: "Refreshing", progress }), - Object.getPrototypeOf(result), -) - -export const fromExit: { - (exit: Exit.Success): Success - (exit: Exit.Failure): Failure - (exit: Exit.Exit): Success | Failure -} = exit => (exit._tag === "Success" ? succeed(exit.value) : fail(exit.cause)) as any - -export const toExit: { - (self: Success): Exit.Success - (self: Failure): Exit.Failure - (self: Final): Exit.Exit - (self: Result): Exit.Exit -} = (self: Result): 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 { - readonly get: Effect.Effect> - readonly set: (v: Result) => Effect.Effect -} - -export const State = (): Context.Tag, State> => Context.GenericTag("@effect-fc/Result/State") - -export interface Progress

{ - readonly update: ( - f: (previous: P) => Effect.Effect - ) => Effect.Effect -} - -export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{ - readonly previous: Result -}> {} - -export const Progress =

(): Context.Tag, Progress

> => Context.GenericTag("@effect-fc/Result/Progress") - -export const makeProgressLayer = (): Layer.Layer< - Progress

, - never, - State -> => Layer.effect(Progress

(), Effect.gen(function*() { - const state = yield* State() - - return { - update: (f: (previous: P) => Effect.Effect) => 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 & Refreshing

- ), - Effect.andThen(({ next }) => state.set(next)), - ), - } -})) - - -export namespace unsafeForkEffect { - export type OutputContext = Exclude | Progress

| Progress> - - export interface Options { - readonly initial?: Initial | Final - readonly initialProgress?: P - } -} - -export const unsafeForkEffect = ( - effect: Effect.Effect, - options?: unsafeForkEffect.Options, NoInfer, P>, -): Effect.Effect< - readonly [result: Subscribable.Subscribable, never, never>, fiber: Fiber.Fiber], - never, - Scope.Scope | unsafeForkEffect.OutputContext -> => Effect.Do.pipe( - Effect.bind("ref", () => Ref.make(options?.initial ?? initial())), - Effect.bind("pubsub", () => PubSub.unbounded>()), - Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State().pipe( - Effect.andThen(state => state.set( - (isFinal(options?.initial) && hasWillRefreshFlag(options?.initial)) - ? refreshing(options.initial, options?.initialProgress) as Result - : 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()), - Layer.provideMerge(Layer.succeed(State(), { - 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, never, never>, fiber: Fiber.Fiber], - never, - Scope.Scope | unsafeForkEffect.OutputContext -> - -export namespace forkEffect { - export type InputContext = R extends Progress ? [X] extends [P] ? R : never : R - export type OutputContext = unsafeForkEffect.OutputContext - export interface Options extends unsafeForkEffect.Options {} -} - -export const forkEffect: { - ( - effect: Effect.Effect>>, - options?: forkEffect.Options, NoInfer, P>, - ): Effect.Effect< - readonly [result: Subscribable.Subscribable, never, never>, fiber: Fiber.Fiber], - never, - Scope.Scope | forkEffect.OutputContext - > -} = unsafeForkEffect diff --git a/packages/effect-docker/src/SetStateAction.ts b/packages/effect-docker/src/SetStateAction.ts deleted file mode 100644 index 10a596b..0000000 --- a/packages/effect-docker/src/SetStateAction.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Function } from "effect" -import type * as React from "react" - - -export const value: { - (prevState: S): (self: React.SetStateAction) => S - (self: React.SetStateAction, prevState: S): S -} = Function.dual(2, (self: React.SetStateAction, prevState: S): S => - typeof self === "function" - ? (self as (prevState: S) => S)(prevState) - : self -) diff --git a/packages/effect-docker/src/Stream.ts b/packages/effect-docker/src/Stream.ts deleted file mode 100644 index 4eab704..0000000 --- a/packages/effect-docker/src/Stream.ts +++ /dev/null @@ -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: { - ( - stream: Stream.Stream - ): Effect.Effect, never, R> - , E, R>( - stream: Stream.Stream, - initialValue: A, - ): Effect.Effect, never, R> -} = Effect.fnUntraced(function* , E, R>( - stream: Stream.Stream, - 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 -}) - -export * from "effect/Stream" diff --git a/packages/effect-docker/src/Subscribable.ts b/packages/effect-docker/src/Subscribable.ts deleted file mode 100644 index 138f16e..0000000 --- a/packages/effect-docker/src/Subscribable.ts +++ /dev/null @@ -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 = []>( - ...elements: T -): Subscribable.Subscribable< - [T[number]] extends [never] - ? never - : { [K in keyof T]: T[K] extends Subscribable.Subscribable ? A : never }, - [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? E : never, - [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? 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[number]] extends [never] - ? never - : { [K in keyof T]: T[K] extends Subscribable.Subscribable ? A : never } - - export interface Options { - readonly equivalence?: Equivalence.Equivalence - } -} - -export const useSubscribables = Effect.fnUntraced(function* []>( - elements: T, - options?: useSubscribables.Options>>, -): Effect.fn.Return< - useSubscribables.Success, - [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? E : never, - [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? 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 | undefined) ?? Equivalence.array(Equivalence.strict())), - Stream.runForEach(v => - Effect.sync(() => setReactStateValue(v)) - ), - Effect.forkScoped, - ), elements) - - return reactStateValue as any -}) - -export * from "effect/Subscribable" diff --git a/packages/effect-docker/src/SubscriptionRef.ts b/packages/effect-docker/src/SubscriptionRef.ts deleted file mode 100644 index 0671ec5..0000000 --- a/packages/effect-docker/src/SubscriptionRef.ts +++ /dev/null @@ -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 { - readonly equivalence?: Equivalence.Equivalence - } -} - -export const useSubscriptionRefState = Effect.fnUntraced(function* ( - ref: SubscriptionRef.SubscriptionRef, - options?: useSubscriptionRefState.Options>, -): Effect.fn.Return>]> { - 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) => Effect.andThen( - Ref.updateAndGet(ref, prevState => SetStateAction.value(setStateAction, prevState)), - v => setReactStateValue(v), - ), - [ref], - ) - - return [reactStateValue, setValue] -}) - -export declare namespace useSubscriptionRefFromState { - export interface Options { - readonly equivalence?: Equivalence.Equivalence - } -} - -export const useSubscriptionRefFromState = Effect.fnUntraced(function* ( - [value, setValue]: readonly [A, React.Dispatch>], - options?: useSubscriptionRefFromState.Options>, -): Effect.fn.Return> { - 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" diff --git a/packages/effect-docker/src/SubscriptionSubRef.ts b/packages/effect-docker/src/SubscriptionSubRef.ts deleted file mode 100644 index 95e8b0e..0000000 --- a/packages/effect-docker/src/SubscriptionSubRef.ts +++ /dev/null @@ -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> -extends SubscriptionSubRef.Variance, SubscriptionRef.SubscriptionRef { - readonly parent: B - - readonly [Unify.typeSymbol]?: unknown - readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify - readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore -} - -export declare namespace SubscriptionSubRef { - export interface Variance { - readonly [SubscriptionSubRefTypeId]: { - readonly _A: Types.Invariant - readonly _B: Types.Invariant - } - } -} - -export interface SubscriptionSubRefUnify extends SubscriptionRef.SubscriptionRefUnify { - SubscriptionSubRef?: () => Extract> -} - -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> -extends Effectable.Class implements SubscriptionSubRef { - 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 - - constructor( - readonly parent: B, - readonly getter: (parentValue: Effect.Effect.Success) => A, - readonly setter: (parentValue: Effect.Effect.Success, value: A) => Effect.Effect.Success, - ) { - super() - this.get = Effect.map(this.parent, this.getter) - } - - commit() { - return this.get - } - - get changes(): Stream.Stream { - return Stream.unwrap( - Effect.map(this.get, a => Stream.concat( - Stream.make(a), - Stream.map(this.parent.changes, this.getter), - )) - ) - } - - modify(f: (a: A) => readonly [C, A]): Effect.Effect { - return this.modifyEffect(a => Effect.succeed(f(a))) - } - - modifyEffect(f: (a: A) => Effect.Effect): Effect.Effect { - return Effect.Do.pipe( - Effect.bind("b", (): Effect.Effect> => 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> => Predicate.hasProperty(u, SubscriptionSubRefTypeId) - -export const makeFromGetSet = >( - parent: B, - options: { - readonly get: (parentValue: Effect.Effect.Success) => A - readonly set: (parentValue: Effect.Effect.Success, value: A) => Effect.Effect.Success - }, -): SubscriptionSubRef => new SubscriptionSubRefImpl(parent, options.get, options.set) - -export const makeFromPath = < - B extends SubscriptionRef.SubscriptionRef, - const P extends PropertyPath.Paths>, ->( - parent: B, - path: P, -): SubscriptionSubRef, P>, B> => new SubscriptionSubRefImpl( - parent, - parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)), - (parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)), -) - -export const makeFromChunkIndex: { - >>( - parent: B, - index: number, - ): SubscriptionSubRef< - Effect.Effect.Success extends Chunk.NonEmptyChunk ? A : never, - B - > - >>( - parent: B, - index: number, - ): SubscriptionSubRef< - Effect.Effect.Success extends Chunk.Chunk ? A : never, - B - > -} = ( - parent: SubscriptionRef.SubscriptionRef>, - index: number, -) => new SubscriptionSubRefImpl( - parent, - parentValue => Chunk.unsafeGet(parentValue, index), - (parentValue, value) => Chunk.replace(parentValue, index, value), -) as any - -export const makeFromChunkFindFirst: { - >>( - parent: B, - findFirstPredicate: Predicate.Predicate extends Chunk.NonEmptyChunk ? A : never>, - ): SubscriptionSubRef< - Effect.Effect.Success extends Chunk.NonEmptyChunk ? A : never, - B - > - >>( - parent: B, - findFirstPredicate: Predicate.Predicate extends Chunk.Chunk ? A : never>, - ): SubscriptionSubRef< - Effect.Effect.Success extends Chunk.Chunk ? A : never, - B - > -} = ( - parent: SubscriptionRef.SubscriptionRef>, - 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: { - >>( - parent: B, - findLastPredicate: Predicate.Predicate extends Chunk.NonEmptyChunk ? A : never>, - ): SubscriptionSubRef< - Effect.Effect.Success extends Chunk.NonEmptyChunk ? A : never, - B - > - >>( - parent: B, - findLastPredicate: Predicate.Predicate extends Chunk.Chunk ? A : never>, - ): SubscriptionSubRef< - Effect.Effect.Success extends Chunk.Chunk ? A : never, - B - > -} = ( - parent: SubscriptionRef.SubscriptionRef>, - 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 diff --git a/packages/effect-docker/src/index.ts b/packages/effect-docker/src/index.ts index b5fe27e..e69de29 100644 --- a/packages/effect-docker/src/index.ts +++ b/packages/effect-docker/src/index.ts @@ -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" diff --git a/packages/effect-docker/src/utils.ts b/packages/effect-docker/src/utils.ts deleted file mode 100644 index 44ed408..0000000 --- a/packages/effect-docker/src/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type ExcludeKeys = K extends keyof T ? ( - { [P in K]?: never } & Omit -) : T