├── .nvmrc ├── src ├── runtime │ ├── index.tsx │ ├── run-sync │ │ └── index.tsx │ ├── run-async │ │ └── index.tsx │ ├── run-sync-exit │ │ └── index.tsx │ ├── create-effect-resource │ │ └── index.tsx │ ├── run-async-exit │ │ └── index.tsx │ └── make-runtime │ │ ├── index.mdx │ │ └── index.tsx ├── data │ ├── index.tsx │ └── match-tag │ │ ├── index.tsx │ │ ├── index.test.tsx │ │ ├── index.stories.tsx │ │ └── index.mdx ├── exit │ ├── index.tsx │ └── map-exit │ │ ├── index.test.tsx │ │ ├── index.mdx │ │ ├── index.stories.tsx │ │ └── index.tsx ├── either │ ├── index.tsx │ └── map-either │ │ ├── index.mdx │ │ ├── index.test.tsx │ │ ├── index.stories.tsx │ │ └── index.tsx ├── option │ ├── index.tsx │ └── map-option │ │ ├── index.mdx │ │ ├── index.test.tsx │ │ ├── index.tsx │ │ └── index.stories.tsx ├── index.tsx ├── effect │ ├── index.tsx │ ├── run-sync │ │ ├── index.mdx │ │ ├── index.stories.tsx │ │ └── index.test.tsx │ ├── run-sync-exit │ │ ├── index.mdx │ │ ├── index.stories.tsx │ │ └── index.test.tsx │ ├── run-async │ │ ├── index.mdx │ │ ├── index.stories.tsx │ │ └── index.test.tsx │ ├── run-async-exit │ │ ├── index.mdx │ │ ├── index.stories.tsx │ │ └── index.test.tsx │ └── create-effect-resource │ │ └── index.mdx └── index.mdx ├── vitest.setup.ts ├── .gitignore ├── .editorconfig ├── vitest.config.ts ├── .storybook ├── preview.ts └── main.ts ├── eslint.config.js ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── deploy-storybook.yml │ ├── ci.yml │ └── publish.yml ├── tsconfig.json ├── public ├── logo-dark.svg └── logo-light.svg ├── LICENSE-MIT ├── tsup.config.ts ├── README.md ├── package.json └── LICENSE-APACHE /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.18.0 2 | -------------------------------------------------------------------------------- /src/runtime/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./make-runtime"; 2 | -------------------------------------------------------------------------------- /src/data/index.tsx: -------------------------------------------------------------------------------- 1 | export { MatchTag } from "./match-tag"; 2 | -------------------------------------------------------------------------------- /src/exit/index.tsx: -------------------------------------------------------------------------------- 1 | export { MapExit } from "./map-exit"; 2 | -------------------------------------------------------------------------------- /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/vitest"; 2 | -------------------------------------------------------------------------------- /src/either/index.tsx: -------------------------------------------------------------------------------- 1 | export { MapEither } from "./map-either"; 2 | -------------------------------------------------------------------------------- /src/option/index.tsx: -------------------------------------------------------------------------------- 1 | export { MapOption } from "./map-option"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | dist 4 | 5 | # vite 6 | vite.config.*.timestamp-* 7 | 8 | *storybook.log 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./data"; 2 | export * from "./effect"; 3 | export * from "./either"; 4 | export * from "./exit"; 5 | export * from "./option"; 6 | export * from "./runtime"; 7 | -------------------------------------------------------------------------------- /src/effect/index.tsx: -------------------------------------------------------------------------------- 1 | import { Runtime } from "effect"; 2 | import { makeRuntime } from "../runtime"; 3 | 4 | const { createEffectResource, RunAsync, RunAsyncExit, RunSync, RunSyncExit } = 5 | makeRuntime(Runtime.defaultRuntime); 6 | 7 | export { createEffectResource, RunAsync, RunAsyncExit, RunSync, RunSyncExit }; 8 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import solid from "vite-plugin-solid"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | plugins: [solid()], 6 | resolve: { 7 | conditions: ["development", "browser"], 8 | }, 9 | test: { 10 | setupFiles: ["./vitest.setup.ts"], 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | const preview: Preview = { 2 | parameters: { 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/i, 7 | }, 8 | }, 9 | docs: { 10 | canvas: { sourceState: "shown" }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default preview; 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | 5 | export default [ 6 | { ignores: ["**/dist/"] }, 7 | { files: ["**/*.{ts,tsx}"] }, 8 | { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, 9 | pluginJs.configs.recommended, 10 | ...tseslint.configs.recommended, 11 | ]; 12 | -------------------------------------------------------------------------------- /src/data/match-tag/index.tsx: -------------------------------------------------------------------------------- 1 | import { type JSX, Match } from "solid-js"; 2 | 3 | function MatchTag(props: { 4 | on: T | undefined; 5 | tag: S; 6 | children: (value: Extract) => JSX.Element; 7 | }): JSX.Element { 8 | return ( 9 | 10 | {(v) => props.children(v() as Extract)} 11 | 12 | ); 13 | } 14 | 15 | export { MatchTag }; 16 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Standard Workflow 2 | description: Setups the repo for CI 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Setup pnpm 7 | uses: pnpm/action-setup@v4 8 | with: 9 | run_install: false 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version-file: ".nvmrc" 13 | cache: "pnpm" 14 | registry-url: "https://registry.npmjs.org/" 15 | - name: Print versions 16 | shell: bash 17 | run: node --version && pnpm --version 18 | - name: Install project dependencies 19 | shell: bash 20 | run: pnpm install 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "newLine": "LF", 5 | "lib": ["DOM", "ESNext"], 6 | "moduleResolution": "Bundler", 7 | "noEmit": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "isolatedModules": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noUncheckedIndexedAccess": true, 14 | "skipLibCheck": true, 15 | "jsx": "preserve", 16 | "jsxImportSource": "solid-js", 17 | "types": ["vitest/globals", "@testing-library/jest-dom"] 18 | }, 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy-storybook.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Storybook Docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - src/** 8 | - public/** 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | jobs: 14 | deploy: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: ./.github/actions/setup 19 | - uses: bitovi/github-actions-storybook-to-github-pages@v1.0.3 20 | with: 21 | install_command: pnpm install 22 | build_command: pnpm build-storybook --docs 23 | path: storybook-static 24 | checkout: false 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | merge_group: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | jobs: 9 | format: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/setup 14 | - name: Run formatter 15 | run: pnpm check:format 16 | lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: ./.github/actions/setup 21 | - name: Run linter 22 | run: pnpm check:lint 23 | test: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: ./.github/actions/setup 28 | - name: Run tests 29 | run: pnpm check:test 30 | -------------------------------------------------------------------------------- /src/runtime/run-sync/index.tsx: -------------------------------------------------------------------------------- 1 | import { Effect, Runtime } from "effect"; 2 | import { createRenderEffect, createSignal, type JSX } from "solid-js"; 3 | 4 | interface RunSyncProps { 5 | effect: Effect.Effect; 6 | children: (v: A) => JSX.Element; 7 | } 8 | 9 | type RunSyncSignature = (props: RunSyncProps) => JSX.Element; 10 | 11 | function makeRunSync(runtime: Runtime.Runtime): RunSyncSignature { 12 | return function RunSync(props: RunSyncProps): JSX.Element { 13 | const [value, setValue] = createSignal(); 14 | 15 | createRenderEffect(() => { 16 | const effect = Effect.andThen(props.effect, props.children); 17 | setValue(() => Runtime.runSync(runtime)(effect)); 18 | }); 19 | 20 | return <>{value()}; 21 | }; 22 | } 23 | 24 | export { type RunSyncProps, type RunSyncSignature, makeRunSync }; 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM Package 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: ./.github/actions/setup 12 | - name: Set version to release tag 📝 13 | run: pnpm version from-git --no-commit-hooks --no-git-tag-version --allow-same-version 14 | - name: Publish to NPM 15 | run: pnpm publish --access public --no-git-checks 16 | env: 17 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 18 | - name: Push version changes to main branch 19 | uses: stefanzweifel/git-auto-commit-action@v5 20 | with: 21 | commit_message: "chore: release ${{ github.event.release.tag_name }}" 22 | branch: ${{ github.event.repository.default_branch }} 23 | file_pattern: package.json 24 | -------------------------------------------------------------------------------- /src/effect/run-sync/index.mdx: -------------------------------------------------------------------------------- 1 | import { ArgTypes, Canvas, Meta } from "@storybook/blocks"; 2 | 3 | import * as RunSyncStories from "./index.stories"; 4 | 5 | 6 | 7 | # `RunSync` 8 | 9 | `RunSync` allows for running a synchronous `Effect` and mapping the successful value to a JSX element. 10 | 11 | [`Effect.runSync` Documentation {"\u{21D2}"}](https://effect.website/docs/getting-started/running-effects/#runsync) 12 | 13 | [`Effect.runSync` Reference {"\u{21D2}"}](https://effect-ts.github.io/effect/effect/Effect.ts.html#runsync) 14 | 15 | `RunSync` runs the `Effect` synchronously. 16 | If the effect fails, the `Cause` is thrown and can be caught in an `ErrorBoundary`. 17 | 18 | 19 | 20 | ## Examples 21 | 22 | The following are wrapped in an `ErrorBoundary`. 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/effect/run-sync-exit/index.mdx: -------------------------------------------------------------------------------- 1 | import { ArgTypes, Canvas, Meta } from "@storybook/blocks"; 2 | 3 | import * as RunSyncExitStories from "./index.stories"; 4 | 5 | 6 | 7 | # `RunSyncExit` 8 | 9 | `RunSyncExit` allows for running a synchronous `Effect` and mapping the result to a JSX element. 10 | 11 | [`Effect.runSyncExit` Documentation {"\u{21D2}"}](https://effect.website/docs/getting-started/running-effects/#runsyncexit) 12 | 13 | [`Effect.runSyncExit` Reference {"\u{21D2}"}](https://effect-ts.github.io/effect/effect/Effect.ts.html#runsyncexit) 14 | 15 | `RunSyncExit` runs the `Effect` synchronously. 16 | If the effect succeeds, the result is handled by `onSuccess`. 17 | If the effect fails, the result is handled by `onFailure`. 18 | 19 | 20 | 21 | ## Examples 22 | 23 | The following are wrapped in an `ErrorBoundary`. 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/exit/map-exit/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { MapExit } from "./index.jsx"; 3 | import { Exit } from "effect"; 4 | import { render } from "@solidjs/testing-library"; 5 | 6 | const testFactory = ( 7 | expected: string, 8 | value: Exit.Exit | undefined, 9 | ) => { 10 | const Component = () => ( 11 | 12 | {{ 13 | onSuccess: (t) => <>Success: {t()}, 14 | onFailure: (f) => <>Failure: {f().toString()}, 15 | }} 16 | 17 | ); 18 | 19 | const { getByText } = render(Component); 20 | 21 | expect(getByText(expected)).toBeInTheDocument(); 22 | }; 23 | 24 | describe.concurrent("MapOption", () => { 25 | test("failure", () => 26 | testFactory("Failure: Error: Hello", Exit.fail("Hello"))); 27 | 28 | test("success", () => testFactory("Success: 5", Exit.succeed(5))); 29 | 30 | test("undefined", () => testFactory("Fallback", undefined)); 31 | }); 32 | -------------------------------------------------------------------------------- /public/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/effect/run-async/index.mdx: -------------------------------------------------------------------------------- 1 | import { ArgTypes, Canvas, Meta } from "@storybook/blocks"; 2 | 3 | import * as RunAsyncStories from "./index.stories"; 4 | 5 | 6 | 7 | # `RunAsync` 8 | 9 | `RunAsync` allows for running an asynchronous `Effect` and mapping the successful value to a JSX element. 10 | 11 | [`Effect.runPromise` Documentation {"\u{21D2}"}](https://effect.website/docs/getting-started/running-effects/#runpromise) 12 | 13 | [`Effect.runPromise` Reference {"\u{21D2}"}](https://effect-ts.github.io/effect/effect/Effect.ts.html#runpromise) 14 | 15 | `RunAsync` runs the `Effect` as a promise and integrates with `Suspense`. 16 | If the effect fails, the `Cause` is thrown and can be caught in an `ErrorBoundary`. 17 | 18 | 19 | 20 | ## Examples 21 | 22 | The following are wrapped in an `ErrorBoundary` and a `Suspense`. Reload to watch the examples load in. 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "storybook-solidjs-vite"; 2 | 3 | import { join, dirname } from "node:path"; 4 | 5 | /** 6 | * This function is used to resolve the absolute path of a package. 7 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 8 | */ 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | function getAbsolutePath(value: string): any { 11 | return dirname(require.resolve(join(value, "package.json"))); 12 | } 13 | const config: StorybookConfig = { 14 | stories: [ 15 | "../src/index.mdx", 16 | "../src/**/*.mdx", 17 | "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)", 18 | ], 19 | addons: [ 20 | getAbsolutePath("@storybook/addon-essentials"), 21 | getAbsolutePath("@chromatic-com/storybook"), 22 | getAbsolutePath("@storybook/addon-interactions"), 23 | ], 24 | framework: { 25 | name: getAbsolutePath("storybook-solidjs-vite"), 26 | options: {}, 27 | }, 28 | staticDirs: ["../public"], 29 | }; 30 | export default config; 31 | -------------------------------------------------------------------------------- /src/effect/run-async-exit/index.mdx: -------------------------------------------------------------------------------- 1 | import { ArgTypes, Canvas, Meta } from "@storybook/blocks"; 2 | 3 | import * as RunAsyncExitStories from "./index.stories"; 4 | 5 | 6 | 7 | # `RunAsyncExit` 8 | 9 | `RunAsyncExit` allows for running an asynchronous `Effect` and mapping the result to a JSX element. 10 | 11 | [`Effect.runPromiseExit` Documentation {"\u{21D2}"}](https://effect.website/docs/getting-started/running-effects/#runpromiseexit) 12 | 13 | [`Effect.runPromiseExit` Reference {"\u{21D2}"}](https://effect-ts.github.io/effect/effect/Effect.ts.html#runpromiseexit) 14 | 15 | `RunAsyncExit` runs the `Effect` as a promise and integrates with `Suspense`. 16 | If the effect succeeds, the result is handled by `onSuccess`. 17 | If the effect fails, the result is handled by `onFailure`. 18 | 19 | 20 | 21 | ## Examples 22 | 23 | The following are wrapped in an `ErrorBoundary` and a `Suspense`. Reload to watch the examples load in. 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/runtime/run-async/index.tsx: -------------------------------------------------------------------------------- 1 | import { Effect, pipe } from "effect"; 2 | import { createRenderEffect, type JSX } from "solid-js"; 3 | import { CreateEffectResourceSignature } from "../create-effect-resource"; 4 | 5 | interface RunAsyncProps { 6 | effect: Effect.Effect; 7 | children: (v: A) => JSX.Element; 8 | } 9 | 10 | type RunAsyncSignature = ( 11 | props: RunAsyncProps, 12 | ) => JSX.Element; 13 | 14 | function makeRunAsync( 15 | createEffectResource: CreateEffectResourceSignature, 16 | ): RunAsyncSignature { 17 | return function RunAsync( 18 | props: RunAsyncProps, 19 | ): JSX.Element { 20 | const [value, setEffect] = createEffectResource(); 21 | 22 | createRenderEffect(() => { 23 | const effect = pipe(props.effect, Effect.map(props.children)); 24 | setEffect(effect); 25 | }); 26 | 27 | const error = () => { 28 | throw value.error; 29 | }; 30 | 31 | return <>{value.error ? error() : value()}; 32 | }; 33 | } 34 | 35 | export { type RunAsyncProps, type RunAsyncSignature, makeRunAsync }; 36 | -------------------------------------------------------------------------------- /src/option/map-option/index.mdx: -------------------------------------------------------------------------------- 1 | import { ArgTypes, Canvas, Meta } from "@storybook/blocks"; 2 | 3 | import * as MapOptionStories from "./index.stories"; 4 | 5 | 6 | 7 | # `MapOption` 8 | 9 | `MapOption` allows for mapping an `Option` to an element. 10 | 11 | [`Option` Documentation {"\u{21D2}"}](https://effect.website/docs/data-types/option/) 12 | 13 | [`Option` Reference {"\u{21D2}"}](https://effect-ts.github.io/effect/effect/Option.ts.html) 14 | 15 | The `Option` represents a value that may not exist (think `T | null` but on steroids). 16 | 17 | `MapOption` takes an `Option` valie (via the `on` property) and transforms it (via the `children` property) to a JSX element. 18 | Should there not be an `Option` value or if the value is `None`, it can provide a fallback element via the `fallback` property. 19 | When the `keyed` property is present, the mapping function in `children` uses a value instead of an accessor. 20 | 21 | 22 | 23 | ## Examples 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 JonahPlusPlus (Jonah Henriksson) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/runtime/run-sync-exit/index.tsx: -------------------------------------------------------------------------------- 1 | import { type Cause, Effect, Exit, pipe, Runtime } from "effect"; 2 | import { createRenderEffect, createSignal, type JSX } from "solid-js"; 3 | 4 | interface RunSyncExitProps { 5 | effect: Effect.Effect; 6 | children: { 7 | readonly onFailure: (error: Cause.Cause) => JSX.Element; 8 | readonly onSuccess: (value: A) => JSX.Element; 9 | }; 10 | } 11 | 12 | type RunSyncExitSignature = ( 13 | props: RunSyncExitProps, 14 | ) => JSX.Element; 15 | 16 | function makeRunSyncExit( 17 | runtime: Runtime.Runtime, 18 | ): RunSyncExitSignature { 19 | return function RunSync(props: RunSyncExitProps): JSX.Element { 20 | const [value, setValue] = createSignal(); 21 | 22 | createRenderEffect(() => { 23 | const effect = pipe( 24 | props.effect, 25 | Effect.exit, 26 | Effect.andThen(Exit.match(props.children)), 27 | ); 28 | setValue(() => Runtime.runSync(runtime)(effect)); 29 | }); 30 | 31 | return <>{value()!}; 32 | }; 33 | } 34 | 35 | export { type RunSyncExitProps, type RunSyncExitSignature, makeRunSyncExit }; 36 | -------------------------------------------------------------------------------- /src/option/map-option/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { MapOption } from "./index.jsx"; 3 | import { Option } from "effect"; 4 | import { render } from "@solidjs/testing-library"; 5 | 6 | const testFactory = ( 7 | expected: string, 8 | value: Option.Option | undefined, 9 | ) => { 10 | const Component = () => ( 11 | 12 | {(x) => <>{x()}} 13 | 14 | ); 15 | 16 | const { getByText } = render(Component); 17 | 18 | expect(getByText(expected)).toBeInTheDocument(); 19 | }; 20 | 21 | describe.concurrent("MapOption", () => { 22 | test("some", () => testFactory("5", Option.some(5))); 23 | 24 | test("none", () => testFactory("Fallback", Option.none())); 25 | 26 | test("undefined", () => testFactory("Fallback", undefined)); 27 | 28 | test("keyed", () => { 29 | const Component = () => ( 30 | 31 | {(x) => <>{x}} 32 | 33 | ); 34 | 35 | const { getByText } = render(Component); 36 | 37 | expect(getByText("Hello")).toBeInTheDocument(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/runtime/create-effect-resource/index.tsx: -------------------------------------------------------------------------------- 1 | import { Effect, Runtime } from "effect"; 2 | import { 3 | type Setter, 4 | type Resource, 5 | type ResourceActions, 6 | createResource, 7 | createSignal, 8 | ResourceOptions, 9 | } from "solid-js"; 10 | 11 | type CreateEffectResourceSignature = () => [ 12 | Resource, 13 | Setter | null>, 14 | ResourceActions, 15 | ]; 16 | 17 | function makeCreateEffectResource( 18 | runtime: Runtime.Runtime, 19 | ): CreateEffectResourceSignature { 20 | return function createEffectResource( 21 | options?: ResourceOptions, 22 | ): [ 23 | Resource, 24 | Setter | null>, 25 | ResourceActions, 26 | ] { 27 | const [effect, setEffect] = createSignal | null>( 28 | null, 29 | ); 30 | const [resource, actions] = createResource>( 31 | effect, 32 | (src) => Runtime.runPromise(runtime)(src), 33 | options, 34 | ); 35 | return [resource, setEffect, actions]; 36 | }; 37 | } 38 | 39 | export { type CreateEffectResourceSignature, makeCreateEffectResource }; 40 | -------------------------------------------------------------------------------- /src/exit/map-exit/index.mdx: -------------------------------------------------------------------------------- 1 | import { ArgTypes, Canvas, Meta } from "@storybook/blocks"; 2 | 3 | import * as MapExitStories from "./index.stories"; 4 | 5 | 6 | 7 | # `MapExit` 8 | 9 | `MapExit` allows for mapping an `Exit` to an element. 10 | 11 | [`Exit` Documentation {"\u{21D2}"}](https://effect.website/docs/data-types/exit/) 12 | 13 | [`Exit` Reference {"\u{21D2}"}](https://effect-ts.github.io/effect/effect/Exit.ts.html) 14 | 15 | The `Exit` type represents the result of executing an effect. 16 | It is similar to `Either` but more constrained, since the error type gets wrapped in a `Cause`. 17 | `Cause` encapsulates variants of error states. 18 | 19 | `MapExit` takes an `Exit` value (via the `on` property) and transforms it (via the `children` property) to a JSX element. 20 | Should there not be an `Exit` value, it can provide a fallback element via the `fallback` property. 21 | When the `keyed` property is present, the mapping functions in `children` use values instead of accessors. 22 | 23 | 24 | 25 | ## Examples 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/runtime/run-async-exit/index.tsx: -------------------------------------------------------------------------------- 1 | import { Cause, Effect, Exit, pipe } from "effect"; 2 | import { createEffect, type JSX } from "solid-js"; 3 | import { CreateEffectResourceSignature } from "../create-effect-resource"; 4 | 5 | interface RunAsyncExitProps { 6 | effect: Effect.Effect; 7 | children: { 8 | readonly onSuccess: (value: A) => JSX.Element; 9 | readonly onFailure: (error: Cause.Cause) => JSX.Element; 10 | }; 11 | } 12 | 13 | type RunAsyncExitSignature = ( 14 | props: RunAsyncExitProps, 15 | ) => JSX.Element; 16 | 17 | function makeRunAsyncExit( 18 | createEffectResource: CreateEffectResourceSignature, 19 | ): RunAsyncExitSignature { 20 | return function RunAsyncExit( 21 | props: RunAsyncExitProps, 22 | ): JSX.Element { 23 | const [value, setEffect] = createEffectResource(); 24 | 25 | createEffect(() => { 26 | const effect = pipe( 27 | props.effect, 28 | Effect.exit, 29 | Effect.andThen(Exit.match(props.children)), 30 | ); 31 | setEffect(effect); 32 | }); 33 | 34 | return <>{value()}; 35 | }; 36 | } 37 | 38 | export { type RunAsyncExitProps, type RunAsyncExitSignature, makeRunAsyncExit }; 39 | -------------------------------------------------------------------------------- /src/either/map-either/index.mdx: -------------------------------------------------------------------------------- 1 | import { ArgTypes, Canvas, Meta } from "@storybook/blocks"; 2 | 3 | import * as MapEitherStories from "./index.stories"; 4 | 5 | 6 | 7 | # `MapEither` 8 | 9 | `MapEither` allows for mapping an `Either` to an element. 10 | 11 | [`Either` Documentation {"\u{21D2}"}](https://effect.website/docs/data-types/either/) 12 | 13 | [`Either` Reference {"\u{21D2}"}](https://effect-ts.github.io/effect/effect/Either.ts.html) 14 | 15 | The `Either` type is useful for cases where a value might be one of two types. 16 | The most popular use of it is for success/error types (but can also be used for representing other things e.g. UUID/handle identifiers, or individual/group accounts). 17 | 18 | `MapEither` takes an `Either` value (via the `on` property) and transforms it (via the `children` property) to a JSX element. 19 | Should there not be an `Either` value, it can provide a fallback element via the `fallback` property. 20 | When the `keyed` property is present, the mapping functions in `children` use values instead of accessors. 21 | 22 | 23 | 24 | ## Examples 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | import * as preset from "tsup-preset-solid"; 3 | 4 | const preset_options: preset.PresetOptions = { 5 | // array or single object 6 | entries: [ 7 | // default entry (index) 8 | { 9 | // entries with '.tsx' extension will have `solid` export condition generated 10 | entry: "src/index.tsx", 11 | // set `true` or pass a specific path to generate a server-only entry 12 | server_entry: false, 13 | }, 14 | ], 15 | // Set to `true` to remove all `console.*` calls and `debugger` statements in prod builds 16 | drop_console: true, 17 | // Set to `true` to generate a CommonJS build alongside ESM 18 | cjs: true, 19 | }; 20 | 21 | export default defineConfig((config) => { 22 | const watching = !!config.watch; 23 | 24 | const parsed_data = preset.parsePresetOptions(preset_options, watching); 25 | 26 | if (!watching) { 27 | const package_fields = preset.generatePackageExports(parsed_data); 28 | 29 | console.log( 30 | `\npackage.json: \n${JSON.stringify(package_fields, null, 2)}\n\n`, 31 | ); 32 | 33 | // Will update ./package.json with the correct export fields 34 | preset.writePackageJson(package_fields); 35 | } 36 | 37 | return preset.generateTsupOptions(parsed_data); 38 | }); 39 | -------------------------------------------------------------------------------- /src/runtime/make-runtime/index.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/blocks"; 2 | 3 | 4 | 5 | # `makeRuntime` 6 | 7 | `makeRuntime` allows for creating variants of utilities found in `Effect` that use a custom runtime, rather than the default runtime. 8 | 9 | ### Signature 10 | 11 | `(runtime: Runtime) => SolidRuntime` 12 | 13 | ## Example 14 | 15 | ```tsx 16 | // runtime.tsx 17 | 18 | import { makeRuntime } from "solid-effect"; 19 | import { Context, Effect, Layer, ManagedRuntime } from "effect"; 20 | 21 | // Our custom service that our runtime provides. 22 | class NumberGenerator extends Context.Tag("NumberGeneratorService")< 23 | NumberGenerator, 24 | { readonly generate: Effect.Effect } 25 | >() {} 26 | 27 | const NumberGeneratorLive = Layer.succeed( 28 | NumberGenerator, 29 | NumberGenerator.of({ 30 | generate: Effect.succeed(5), 31 | }), 32 | ); 33 | 34 | export const rt = ManagedRuntime.make(NumberGeneratorLive); 35 | 36 | const { createEffectResource, RunAsync, RunAsyncExit, RunSync, RunSyncExit } = 37 | makeRuntime(await rt.runtime()); 38 | 39 | export { createEffectResource, RunAsync, RunAsyncExit, RunSync, RunSyncExit }; 40 | ``` 41 | 42 | It is necessary to destructure and restructure the exports in ESM, to ensure the exports are static. 43 | -------------------------------------------------------------------------------- /src/data/match-tag/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from "solid-js"; 2 | import { describe, expect, test } from "vitest"; 3 | import { MatchTag } from "./index.jsx"; 4 | import { Data } from "effect"; 5 | import { render } from "@solidjs/testing-library"; 6 | 7 | const testFactory = (expected: string, value: Tagged | undefined) => { 8 | const Component = () => ( 9 | 10 | 11 | {(v) => `Foo: ${v.value}`} 12 | 13 | 14 | {(v) => `Bar: ${v.value}`} 15 | 16 | 17 | ); 18 | 19 | const { getByText } = render(Component); 20 | 21 | expect(getByText(expected)).toBeInTheDocument(); 22 | }; 23 | 24 | type Tagged = Data.TaggedEnum<{ 25 | Foo: { value: number }; 26 | Bar: { value: string }; 27 | Qux: { value: null }; 28 | }>; 29 | const Tagged = Data.taggedEnum(); 30 | 31 | describe.concurrent("MatchTag", () => { 32 | test("Case 'Foo'", () => testFactory("Foo: 5", Tagged.Foo({ value: 5 }))); 33 | 34 | test("Case 'Bar'", () => 35 | testFactory("Bar: Hello", Tagged.Bar({ value: "Hello" }))); 36 | 37 | test("Case 'Qux'", () => 38 | testFactory("Fallback", Tagged.Qux({ value: null }))); 39 | 40 | test("undefined", () => testFactory("Fallback", undefined)); 41 | }); 42 | -------------------------------------------------------------------------------- /src/runtime/make-runtime/index.tsx: -------------------------------------------------------------------------------- 1 | import { Runtime } from "effect"; 2 | import { 3 | makeCreateEffectResource, 4 | type CreateEffectResourceSignature, 5 | } from "../create-effect-resource"; 6 | import { makeRunAsync, type RunAsyncSignature } from "../run-async"; 7 | import { 8 | makeRunAsyncExit, 9 | type RunAsyncExitSignature, 10 | } from "../run-async-exit"; 11 | import { makeRunSync, type RunSyncSignature } from "../run-sync"; 12 | import { makeRunSyncExit, type RunSyncExitSignature } from "../run-sync-exit"; 13 | 14 | interface SolidRuntime { 15 | createEffectResource: CreateEffectResourceSignature; 16 | RunAsync: RunAsyncSignature; 17 | RunAsyncExit: RunAsyncExitSignature; 18 | RunSync: RunSyncSignature; 19 | RunSyncExit: RunSyncExitSignature; 20 | } 21 | 22 | function makeRuntime(runtime: Runtime.Runtime): SolidRuntime { 23 | const createEffectResource = makeCreateEffectResource(runtime); 24 | const RunAsync = makeRunAsync(createEffectResource); 25 | const RunAsyncExit = makeRunAsyncExit(createEffectResource); 26 | const RunSync = makeRunSync(runtime); 27 | const RunSyncExit = makeRunSyncExit(runtime); 28 | 29 | return { 30 | createEffectResource, 31 | RunAsync, 32 | RunAsyncExit, 33 | RunSync, 34 | RunSyncExit, 35 | }; 36 | } 37 | 38 | export { type SolidRuntime, makeRuntime }; 39 | -------------------------------------------------------------------------------- /src/effect/run-sync/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | 3 | import { RunSync } from ".."; 4 | 5 | const meta: Meta = { 6 | component: RunSync, 7 | title: "effect/RunSync", 8 | argTypes: { 9 | effect: { 10 | description: "A synchronous effect to run.", 11 | type: { name: "Effect", required: true }, 12 | }, 13 | children: { 14 | description: 15 | "A function for mapping the successful Effect value from `effect` to a JSX element.", 16 | type: { name: "(v: A) => JSX.Element", required: true }, 17 | }, 18 | }, 19 | decorators: [ 20 | (Story: Component) => ( 21 | <>{e.toString()}}> 22 | 23 | 24 | ), 25 | ], 26 | }; 27 | 28 | export default meta; 29 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 30 | type Story = StoryObj>; 31 | 32 | import { Effect } from "effect"; 33 | import { Component, ErrorBoundary } from "solid-js"; 34 | 35 | export const Basic: Story = { 36 | args: { 37 | effect: Effect.succeed(42), 38 | children: (x: number) => x, 39 | }, 40 | }; 41 | 42 | export const Error: Story = { 43 | args: { 44 | effect: Effect.fail("something went wrong"), 45 | children: (x: number) => x, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/either/map-either/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { MapEither } from "./index.jsx"; 3 | import { Either } from "effect"; 4 | import { render } from "@solidjs/testing-library"; 5 | 6 | const testFactory = ( 7 | expected: string, 8 | value: Either.Either | undefined, 9 | ) => { 10 | const Component = () => ( 11 | 12 | {{ 13 | onLeft: (x) => <>Left: {x()}, 14 | onRight: (x) => <>Right: {x()}, 15 | }} 16 | 17 | ); 18 | 19 | const { getByText } = render(Component); 20 | 21 | expect(getByText(expected)).toBeInTheDocument(); 22 | }; 23 | 24 | describe.concurrent("MapOption", () => { 25 | test("left", () => testFactory("Left: Hello", Either.left("Hello"))); 26 | 27 | test("right", () => testFactory("Right: 5", Either.right(5))); 28 | 29 | test("undefined", () => testFactory("Fallback", undefined)); 30 | 31 | test("keyed", () => { 32 | const Component = () => ( 33 | 34 | {{ 35 | onLeft: (x) => <>Left: {x}, 36 | onRight: (x) => <>Right: {x}, 37 | }} 38 | 39 | ); 40 | 41 | const { getByText } = render(Component); 42 | 43 | expect(getByText("Left: Hello")).toBeInTheDocument(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/index.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/blocks"; 2 | 3 | 4 | 5 |
13 | 14 |
15 | 16 | # solid-effect 17 | 18 | [Effect](https://effect.website/) is a standard library for TypeScript, offering great data structures and utilities for a variety of problems. 19 | 20 | [SolidJS](https://www.solidjs.com/) is a frontend framework that offers fine-grained reactivity and incredible performance. 21 | 22 | Wouldn't it be cool if the experience between these two libraries was seamless? 23 | 24 | That's where solid-effect comes in, providing utilities for integrating Effect into SolidJS. 25 | 26 | Many of the components simplify what could be achieved with built-in SolidJS components using Effect utilities. 27 | 28 | For example, mapping an `Option` to a JSX element: 29 | 30 | ```tsx 31 | const value = Option.some("Text"); 32 | 33 | const result = () => 34 | pipe( 35 | value, 36 | Option.map((x) => x.toUpperCase()), 37 | Option.getOrUndefined, 38 | ); 39 | 40 | {result()}; 41 | ``` 42 | 43 | is simplified to: 44 | 45 | ```tsx 46 | const value = Option.some("Text"); 47 | 48 | {(x) => x.toUpperCase()}; 49 | ``` 50 | -------------------------------------------------------------------------------- /src/data/match-tag/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | 3 | import { MatchTag } from ".."; 4 | import { Data } from "effect"; 5 | import { Component, JSX, Switch } from "solid-js"; 6 | 7 | const meta: Meta = { 8 | component: MatchTag, 9 | title: "data/MatchTag", 10 | decorators: [ 11 | (Story: Component) => ( 12 | 13 | 14 | 15 | ), 16 | ], 17 | argTypes: { 18 | on: { 19 | description: "A tagged value.", 20 | type: { name: "T extends { _tag: string }", required: true }, 21 | }, 22 | tag: { 23 | description: "The tag being matched.", 24 | type: { name: 'S extends T["_tag"]', required: true }, 25 | }, 26 | children: { 27 | description: "A function for mapping `on`", 28 | type: { 29 | name: "(value: Extract) => JSX.Element", 30 | required: true, 31 | }, 32 | }, 33 | }, 34 | }; 35 | 36 | export default meta; 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 38 | type Story = StoryObj>; 39 | 40 | type Tagged = Data.TaggedEnum<{ 41 | Foo: { value: JSX.Element }; 42 | Bar: { value: string }; 43 | Qux: { value: null }; 44 | }>; 45 | const Tagged = Data.taggedEnum(); 46 | 47 | export const Basic: Story = { 48 | args: { 49 | on: Tagged.Bar({ value: "Hello" }), 50 | tag: "Bar", 51 | children: (s: Tagged & { _tag: "Bar" }) => s.value.toUpperCase(), 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /src/effect/run-async/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | 3 | import { RunAsync } from ".."; 4 | 5 | const meta: Meta = { 6 | component: RunAsync, 7 | title: "effect/RunAsync", 8 | argTypes: { 9 | effect: { 10 | description: "An asynchronous effect to run.", 11 | type: { name: "Effect", required: true }, 12 | }, 13 | children: { 14 | description: 15 | "A function for mapping the successful Effect value from `effect` to a JSX element.", 16 | type: { name: "(v: A) => JSX.Element", required: true }, 17 | }, 18 | }, 19 | decorators: [ 20 | (Story: Component) => ( 21 | <>{e.toString()}}> 22 | 23 | 24 | 25 | 26 | ), 27 | ], 28 | }; 29 | 30 | export default meta; 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 32 | type Story = StoryObj>; 33 | 34 | import { Effect, pipe } from "effect"; 35 | import { Component, ErrorBoundary, Suspense } from "solid-js"; 36 | 37 | export const Basic: Story = { 38 | args: { 39 | effect: pipe(Effect.sleep(2000), Effect.andThen(Effect.succeed(32))), 40 | children: (x: number) => x, 41 | }, 42 | }; 43 | 44 | export const Error: Story = { 45 | args: { 46 | effect: pipe( 47 | Effect.sleep(3000), 48 | Effect.andThen(Effect.fail("something went wrong")), 49 | ), 50 | children: (x: number) => x, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/option/map-option/index.tsx: -------------------------------------------------------------------------------- 1 | import { Option } from "effect"; 2 | import { createMemo, type JSX, untrack } from "solid-js"; 3 | 4 | /** 5 | * 6 | * @param props 7 | * @returns An element mapped from some value or nothing. 8 | */ 9 | function MapOption(props: { 10 | /** The optional value to map on. */ 11 | on: Option.Option | undefined; 12 | children: (accessor: () => T) => JSX.Element; 13 | fallback?: JSX.Element; 14 | keyed?: false; 15 | }): JSX.Element; 16 | function MapOption(props: { 17 | /** The optional value to map on. */ 18 | on: Option.Option | undefined; 19 | children: (value: T) => JSX.Element; 20 | fallback?: JSX.Element; 21 | keyed: true; 22 | }): JSX.Element; 23 | function MapOption(props: { 24 | /** The optional value to map on. */ 25 | on: Option.Option | undefined; 26 | children: (accessor: T | (() => T)) => JSX.Element; 27 | fallback?: JSX.Element; 28 | keyed?: boolean; 29 | }): JSX.Element { 30 | const keyed = props.keyed; 31 | const condition = createMemo( 32 | () => props.on?._tag == "Some" && props.on.value, 33 | ); 34 | return createMemo(() => { 35 | const c = condition(); 36 | if (c) { 37 | const child = props.children; 38 | return untrack(() => 39 | child( 40 | keyed 41 | ? (c as T) 42 | : () => { 43 | if (!untrack(condition)) throw "Stale read from "; 44 | return (props.on! as Option.Option & { _tag: "Some" }).value; 45 | }, 46 | ), 47 | ); 48 | } 49 | return props.fallback; 50 | }) as unknown as JSX.Element; 51 | } 52 | 53 | export { MapOption }; 54 | -------------------------------------------------------------------------------- /src/effect/run-sync-exit/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | 3 | import { RunSyncExit } from ".."; 4 | 5 | const meta: Meta = { 6 | component: RunSyncExit, 7 | title: "effect/RunSyncExit", 8 | argTypes: { 9 | effect: { 10 | description: "A synchronous effect to run.", 11 | type: { name: "Effect", required: true }, 12 | }, 13 | children: { 14 | description: 15 | "An object containing functions `onSuccess` and `onFailure` for mapping the exit value from `effect` to a JSX element.", 16 | type: { 17 | name: "{ onSuccess: (value: A) => JSX.Element; onFailure: (error: Cause) => JSX.Element; }", 18 | required: true, 19 | }, 20 | }, 21 | }, 22 | decorators: [ 23 | (Story: Component) => ( 24 | <>{e.toString()}}> 25 | 26 | 27 | ), 28 | ], 29 | }; 30 | 31 | export default meta; 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | type Story = StoryObj>; 34 | 35 | import { Effect } from "effect"; 36 | import { Component, ErrorBoundary } from "solid-js"; 37 | 38 | export const Basic: Story = { 39 | args: { 40 | effect: Effect.succeed(32), 41 | children: { 42 | onSuccess: (x: number) => x, 43 | onFailure: () => "Something bad happened", 44 | }, 45 | }, 46 | }; 47 | 48 | export const Error: Story = { 49 | args: { 50 | effect: Effect.fail("something went wrong"), 51 | children: { 52 | onSuccess: (x: number) => x, 53 | onFailure: () => "Something bad happened", 54 | }, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/option/map-option/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | 3 | import { MapOption } from ".."; 4 | import { Option } from "effect"; 5 | 6 | const meta: Meta = { 7 | component: MapOption, 8 | title: "option/MapOption", 9 | argTypes: { 10 | on: { 11 | description: "An either value or undefined.", 12 | type: { name: "Either | undefined", required: true }, 13 | }, 14 | children: { 15 | description: 16 | "An object containing functions `onLeft` and `onRight` for mapping `on`.", 17 | type: { 18 | name: "{ onLeft: (accessor: L | (() => L)) => JSX.Element, onRight: (accessor: R | (() => R)) => JSX.Element }", 19 | required: true, 20 | }, 21 | }, 22 | fallback: { 23 | description: "A fallback value displayed when `on` is undefined.", 24 | type: "JSX.Element", 25 | }, 26 | keyed: { 27 | description: "Whether `children` should not use accessors.", 28 | type: "boolean", 29 | }, 30 | }, 31 | }; 32 | 33 | export default meta; 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | type Story = StoryObj>; 36 | 37 | export const Basic: Story = { 38 | args: { 39 | on: Option.some(3), 40 | children: (x: () => number) =>

{x()}

, 41 | }, 42 | }; 43 | 44 | export const Fallback: Story = { 45 | args: { 46 | on: undefined, 47 | children: (x: () => number) =>

{x()}

, 48 | fallback: Hello!, 49 | }, 50 | }; 51 | 52 | export const Keyed: Story = { 53 | args: { 54 | on: Option.none(), 55 | children: (x: number) =>

{x}

, 56 | keyed: true, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /src/effect/run-sync/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { RunSync } from ".."; 3 | import { Context, Effect, Layer, ManagedRuntime } from "effect"; 4 | import { render } from "@solidjs/testing-library"; 5 | import { makeRuntime } from "../../runtime"; 6 | 7 | describe("RunSync", async () => { 8 | test("succeed", () => { 9 | const effect = Effect.succeed("Hello"); 10 | 11 | const Component = () => ( 12 | {(x) => <>{x}} 13 | ); 14 | 15 | const { getByText } = render(Component); 16 | 17 | expect(getByText("Hello")).toBeInTheDocument(); 18 | }); 19 | 20 | test("fail", () => { 21 | const effect = Effect.fail("Fail"); 22 | 23 | const Component = () => ( 24 | {(x) => <>{x}} 25 | ); 26 | 27 | expect(() => { 28 | render(Component); 29 | }).toThrowError("Fail"); 30 | }); 31 | 32 | class NumberGenerator extends Context.Tag("NumberGeneratorService")< 33 | NumberGenerator, 34 | { readonly generate: Effect.Effect } 35 | >() {} 36 | 37 | const NumberGeneratorLive = Layer.succeed( 38 | NumberGenerator, 39 | NumberGenerator.of({ 40 | generate: Effect.succeed(5), 41 | }), 42 | ); 43 | 44 | const rt = ManagedRuntime.make(NumberGeneratorLive); 45 | 46 | const solidRt = makeRuntime(await rt.runtime()); 47 | 48 | test("with dependency", () => { 49 | const effect = Effect.gen(function* () { 50 | const numberGenerator = yield* NumberGenerator; 51 | const number = yield* numberGenerator.generate; 52 | return number; 53 | }); 54 | 55 | const Component = () => ( 56 | {(x) => <>{x}} 57 | ); 58 | 59 | const { getByText } = render(Component); 60 | 61 | expect(getByText("5")).toBeInTheDocument(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/effect/run-async-exit/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | 3 | import { RunAsyncExit } from ".."; 4 | 5 | const meta: Meta = { 6 | component: RunAsyncExit, 7 | title: "effect/RunAsyncExit", 8 | argTypes: { 9 | effect: { 10 | description: "An asynchronous effect to run.", 11 | type: { name: "Effect", required: true }, 12 | }, 13 | children: { 14 | description: 15 | "An object containing functions `onSuccess` and `onFailure` for mapping the exit value from `effect` to a JSX element.", 16 | type: { 17 | name: "{ onSuccess: (value: A) => JSX.Element; onFailure: (error: Cause) => JSX.Element; }", 18 | required: true, 19 | }, 20 | }, 21 | }, 22 | decorators: [ 23 | (Story: Component) => ( 24 | <>{e.toString()}}> 25 | 26 | 27 | 28 | 29 | ), 30 | ], 31 | }; 32 | 33 | export default meta; 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | type Story = StoryObj>; 36 | 37 | import { Effect, pipe } from "effect"; 38 | import { Component, ErrorBoundary, Suspense } from "solid-js"; 39 | 40 | export const Basic: Story = { 41 | args: { 42 | effect: pipe(Effect.sleep(2000), Effect.andThen(Effect.succeed(32))), 43 | children: { 44 | onSuccess: (x: number) => x, 45 | onFailure: () => "Something bad happened", 46 | }, 47 | }, 48 | }; 49 | 50 | export const Error: Story = { 51 | args: { 52 | effect: pipe( 53 | Effect.sleep(3000), 54 | Effect.andThen(Effect.fail("something went wrong")), 55 | ), 56 | children: { 57 | onSuccess: (x: number) => x, 58 | onFailure: () => "Something bad happened", 59 | }, 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /src/either/map-either/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | 3 | import { MapEither } from ".."; 4 | import { Either } from "effect"; 5 | 6 | const meta: Meta = { 7 | component: MapEither, 8 | title: "either/MapEither", 9 | argTypes: { 10 | on: { 11 | description: "An either value or undefined.", 12 | type: { name: "Either | undefined", required: true }, 13 | }, 14 | children: { 15 | description: 16 | "An object containing functions `onLeft` and `onRight` for mapping `on`.", 17 | type: { 18 | name: "{ onLeft: (accessor: L | (() => L)) => JSX.Element, onRight: (accessor: R | (() => R)) => JSX.Element }", 19 | required: true, 20 | }, 21 | }, 22 | fallback: { 23 | description: "A fallback value displayed when `on` is undefined.", 24 | type: "JSX.Element", 25 | }, 26 | keyed: { 27 | description: "Whether `children` should not use accessors.", 28 | type: "boolean", 29 | }, 30 | }, 31 | }; 32 | 33 | export default meta; 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | type Story = StoryObj>; 36 | 37 | export const Basic: Story = { 38 | args: { 39 | on: Either.left(32), 40 | children: { 41 | onLeft: (x: () => number) =>

{x()}

, 42 | onRight: (x: () => number) =>

{x()}

, 43 | }, 44 | fallback: Hello!, 45 | }, 46 | }; 47 | 48 | export const Fallback: Story = { 49 | args: { 50 | on: undefined, 51 | children: { 52 | onLeft: (x: () => number) =>

{x()}

, 53 | onRight: (x: () => number) =>

{x()}

, 54 | }, 55 | fallback: Hello!, 56 | }, 57 | }; 58 | 59 | export const Keyed: Story = { 60 | args: { 61 | on: Either.right(6), 62 | children: { 63 | onLeft: (x: number) =>

{x}

, 64 | onRight: (x: number) =>

{x}

, 65 | }, 66 | keyed: true, 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /src/data/match-tag/index.mdx: -------------------------------------------------------------------------------- 1 | import { ArgTypes, Canvas, Meta } from "@storybook/blocks"; 2 | 3 | import * as MatchTagStories from "./index.stories"; 4 | 5 | 6 | 7 | # `MatchTag` 8 | 9 | `MatchTag` allows for matching and mapping tagged values in `Switch` components. 10 | 11 | [`Data` Documentation {"\u{21D2}"}](https://effect.website/docs/data-types/data/) 12 | 13 | [`Data` Reference {"\u{21D2}"}](https://effect-ts.github.io/effect/effect/Data.ts.html) 14 | 15 | `Data.TaggedEnum` and `Data.taggedEnum` are useful for creating tagged enums 16 | ([also known as tagged unions, sum types, Rust-style enums, among other names](https://en.wikipedia.org/wiki/Tagged_union)). 17 | Tagged enums are useful for modeling data where there are variants without much overlap. 18 | 19 | `MatchTag` takes a tagged value (via the `on` property) and when the tag matches the `tag` property, 20 | maps it via `children` to produce an element. 21 | 22 | 23 | 24 | ## Examples 25 | 26 | Here's a basic example of using `MatchTag` inside of `Switch`: 27 | 28 | ```tsx 29 | // Basic usage of MatchTag with Switch. 30 | import { MatchTag } from 'solid-effect'; 31 | import { Data } from 'effect'; 32 | 33 | // Create our tagged enum type. 34 | type Tagged = Data.TaggedEnum<{ 35 | Foo: { el: JSX.Element }; 36 | Bar: { msg: string }; 37 | Qux: { }; 38 | }>; 39 | export const Tagged = Data.taggedEnum(); 40 | 41 | export function Component(props: { data: Tagged }) { 42 | return ( 43 | 44 | 45 | {(x) => x.el} 46 | 47 | 48 | {(x) => `Bar says "${x.msg}"`} 49 | 50 | 51 | {() => "Hello from Qux!"} 52 | 53 | 54 | ); 55 | } 56 | 57 | // Then later on... 58 | 59 | ``` 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/exit/map-exit/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | 3 | import { MapExit } from ".."; 4 | import { Cause, Exit } from "effect"; 5 | 6 | const meta: Meta = { 7 | component: MapExit, 8 | title: "exit/MapExit", 9 | argTypes: { 10 | on: { 11 | description: "An exit value or undefined.", 12 | type: { name: "Exit | undefined", required: true }, 13 | }, 14 | children: { 15 | description: 16 | "An object containing functions `onLeft` and `onRight` for mapping `on`.", 17 | type: { 18 | name: "{ onSuccess: (accessor: T | (() => T)) => JSX.Element, onFailure: (accessor: Cause | (() => Cause)) => JSX.Element }", 19 | required: true, 20 | }, 21 | }, 22 | fallback: { 23 | description: "A fallback value displayed when `on` is undefined.", 24 | type: "JSX.Element", 25 | }, 26 | keyed: { 27 | description: "Whether `children` should not use accessors.", 28 | type: "boolean", 29 | }, 30 | }, 31 | }; 32 | 33 | export default meta; 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | type Story = StoryObj>; 36 | 37 | export const Basic: Story = { 38 | args: { 39 | on: Exit.succeed(42), 40 | children: { 41 | onSuccess: (x: () => number) =>

{x()}

, 42 | onFailure: (x: () => Cause.Cause) =>

{x().toString()}

, 43 | }, 44 | }, 45 | }; 46 | 47 | export const Fallback: Story = { 48 | args: { 49 | on: undefined, 50 | children: { 51 | onSuccess: (x: () => number) =>

{x()}

, 52 | onFailure: (x: () => Cause.Cause) =>

{x().toString()}

, 53 | }, 54 | fallback: Hello!, 55 | }, 56 | }; 57 | 58 | export const Keyed: Story = { 59 | args: { 60 | on: Exit.fail("not found"), 61 | children: { 62 | onSuccess: (x: number) =>

{x}

, 63 | onFailure: (x: Cause.Cause) =>

{x.toString()}

, 64 | }, 65 | keyed: true, 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /src/effect/run-async/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { RunAsync } from ".."; 3 | import { Context, Effect, Layer, ManagedRuntime } from "effect"; 4 | import { render, waitFor } from "@solidjs/testing-library"; 5 | import { ErrorBoundary } from "solid-js"; 6 | import { makeRuntime } from "../../runtime"; 7 | 8 | describe("RunAsync", async () => { 9 | test("succeed", async () => { 10 | const effect = Effect.succeed("Success"); 11 | 12 | const Component = () => ( 13 | {(x) => <>{x}} 14 | ); 15 | 16 | const { getByText } = render(Component); 17 | 18 | await waitFor(async () => expect(getByText("Success")).toBeInTheDocument()); 19 | }); 20 | 21 | test("fail", async () => { 22 | const effect = Effect.fail("Fail"); 23 | 24 | const Component = () => ( 25 | 26 | {(x) => <>{x}} 27 | 28 | ); 29 | 30 | const { getByText } = render(Component); 31 | 32 | await waitFor(async () => expect(getByText("Failure")).toBeInTheDocument()); 33 | }); 34 | 35 | class NumberGenerator extends Context.Tag("NumberGeneratorService")< 36 | NumberGenerator, 37 | { readonly generate: Effect.Effect } 38 | >() {} 39 | 40 | const NumberGeneratorLive = Layer.succeed( 41 | NumberGenerator, 42 | NumberGenerator.of({ 43 | generate: Effect.succeed(5), 44 | }), 45 | ); 46 | 47 | const rt = ManagedRuntime.make(NumberGeneratorLive); 48 | 49 | const solidRt = makeRuntime(await rt.runtime()); 50 | 51 | test("with dependency", async () => { 52 | const effect = Effect.gen(function* () { 53 | const numberGenerator = yield* NumberGenerator; 54 | const number = yield* numberGenerator.generate; 55 | return number; 56 | }); 57 | 58 | const Component = () => ( 59 | {(x) => <>{x}} 60 | ); 61 | 62 | const { getByText } = render(Component); 63 | 64 | await waitFor(async () => expect(getByText("5")).toBeInTheDocument()); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/effect/create-effect-resource/index.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/blocks"; 2 | 3 | 4 | 5 | # `createEffectResource` 6 | 7 | `createEffectResource` is a utility for creating a `Resource` that runs an asynchronous `Effect`. 8 | This is used inside of `RunAsync` and `RunAsyncExit` to convert the effect into a resource. 9 | 10 | ### Signature 11 | 12 | `(options?: ResourceOptions
) => [Resource, Setter | null>, ResourceActions]` 13 | 14 | It takes in [resource options](https://docs.solidjs.com/reference/basic-reactivity/create-resource#options) (optional), 15 | and it returns (in an array), a resource, a signal setter for the effect, and resource actions (`{ mutate, refetch }`). 16 | 17 | It is equivalent to the following: 18 | 19 | ```tsx 20 | // Where runtime is the default runtime or custom runtime. 21 | const [effect, setEffect] = createSignal | null>(null); 22 | const [resource, options] = createResource>( 23 | effect, 24 | (src) => Runtime.runPromise(runtime)(src), 25 | ); 26 | ``` 27 | 28 | It is useful when you need run an `Effect` but the result might not be mapped directly to a JSX element. 29 | For example, in cases where the resource is used by many components. 30 | 31 | ## Example 32 | 33 | ```tsx 34 | // This example is in a complex social media web app that fetches info based on the path. 35 | // On navigation, routes set the effect to fetch the correct data and put it into a form consumable by app components. 36 | import { createEffectResource } from "solid-effect"; 37 | 38 | export const [appMeta, setAppMetaEffect] = createEffectResource(); 39 | 40 | // Later, in a route that contains an effect called QUERY. 41 | export function RootPath() { 42 | onMount(() => { 43 | setAppMetaEffect(QUERY); 44 | }); 45 | 46 | return null; // The route doesn't do anything, the app handles the rest. 47 | } 48 | 49 | // Later, in an app component. 50 | export function UserInfo() { 51 | return ( 52 | 53 |

{appMeta().userInfo.name}

54 |

{appMeta().userInfo.score}

55 |
56 | ); 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | solid-effect logo 5 | 6 |

7 | 8 | # solid-effect 9 | 10 | [![NPM Version](https://img.shields.io/npm/v/solid-effect)](https://www.npmjs.com/package/solid-effect) 11 | [![Documentation](https://img.shields.io/badge/documentation-FF4785?logo=storybook&logoColor=white)](https://jonahplusplus.dev/solid-effect/) 12 | ![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-green) 13 | 14 | solid-effect is a utility library for working with [effect-ts](https://effect.website/) in [solid-js](https://www.solidjs.com/). 15 | 16 | With Effect, you can compose your program as "effects", small programs that include the return type, error types, and requirements as part of their type signature, which establish an API contract, making defensive programming unnecessary. 17 | 18 | Effect comes out of the box with utilities for error handling, caching, retry, interruption, concurrency, and observability, among others. 19 | 20 | What solid-effect does is allow you to use these utilities closer to the edge of your SolidJS app. 21 | 22 | ## Documentation 23 | 24 | [Read the docs to see all the features + examples!](https://jonahplusplus.dev/solid-effect/) 25 | 26 | ## Any examples of use-cases for using Effect with SolidJS? 27 | 28 | So far, I've been using Effect in my SolidJS app for my GraphQL client (service layers allow for defining configuration and better error handling) and for i18n/l10n (e.g. caching formatters based on locale). 29 | 30 | With solid-effect, I was able to pull out some of the utilities I created for displaying results (`MapOption` and `MatchTag`) and put them into their own library and improve them, while adding more utilities that I had yet to explore. 31 | 32 | ## Roadmap 33 | 34 | Once I integrate this package into my own app and use it a bit more, I think I'll have a better idea of where to go. 35 | That said, I think the next direction will be observability (logging, metrics, and tracing). 36 | 37 | For example, it could mean having `ErrorBoundary`s that log errors into Effect, metrics that can be retrieved as signals, and tha ability to express spans as JSX. 38 | 39 | I would love to hear feedback! (`@jonahplusplus` on Discord and `@jonahplusplus.bsky.social` on Bluesky!) 40 | -------------------------------------------------------------------------------- /src/effect/run-sync-exit/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { RunSyncExit } from ".."; 3 | import { Cause, Context, Effect, Layer, ManagedRuntime, Option } from "effect"; 4 | import { render } from "@solidjs/testing-library"; 5 | import { makeRuntime } from "../../runtime"; 6 | 7 | describe("RunSyncExit", async () => { 8 | test("succeed", () => { 9 | const effect = Effect.succeed("Hello"); 10 | 11 | const Component = () => ( 12 | 13 | {{ 14 | onSuccess: (x) => <>Success: {x}, 15 | onFailure: (x) => ( 16 | <>Failure: {Cause.failureOption(x).pipe(Option.getOrUndefined)} 17 | ), 18 | }} 19 | 20 | ); 21 | 22 | const { getByText } = render(Component); 23 | 24 | expect(getByText("Success: Hello")).toBeInTheDocument(); 25 | }); 26 | 27 | test("fail", () => { 28 | const effect = Effect.fail(5); 29 | 30 | const Component = () => ( 31 | 32 | {{ 33 | onSuccess: (x) => <>Success: {x}, 34 | onFailure: (x) => ( 35 | <>Failure: {Cause.failureOption(x).pipe(Option.getOrUndefined)} 36 | ), 37 | }} 38 | 39 | ); 40 | 41 | const { getByText } = render(Component); 42 | 43 | expect(getByText("Failure: 5")).toBeInTheDocument(); 44 | }); 45 | 46 | class NumberGenerator extends Context.Tag("NumberGeneratorService")< 47 | NumberGenerator, 48 | { readonly generate: Effect.Effect } 49 | >() {} 50 | 51 | const NumberGeneratorLive = Layer.succeed( 52 | NumberGenerator, 53 | NumberGenerator.of({ 54 | generate: Effect.succeed(5), 55 | }), 56 | ); 57 | 58 | const rt = ManagedRuntime.make(NumberGeneratorLive); 59 | 60 | const solidRt = makeRuntime(await rt.runtime()); 61 | 62 | test("with dependency", () => { 63 | const effect = Effect.gen(function* () { 64 | const numberGenerator = yield* NumberGenerator; 65 | const number = yield* numberGenerator.generate; 66 | return number; 67 | }); 68 | 69 | const Component = () => ( 70 | 71 | {{ 72 | onSuccess: (x) => <>Success: {x}, 73 | onFailure: (x) => ( 74 | <>Failure: {Cause.failureOption(x).pipe(Option.getOrUndefined)} 75 | ), 76 | }} 77 | 78 | ); 79 | 80 | const { getByText } = render(Component); 81 | 82 | expect(getByText("Success: 5")).toBeInTheDocument(); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-effect", 3 | "version": "1.0.0", 4 | "description": "A collection of utilities for working with EffectTS in SolidJS", 5 | "type": "module", 6 | "scripts": { 7 | "prepack": "pnpm build", 8 | "build": "tsup", 9 | "check:all": "pnpm run check:lint && pnpm run check:test && pnpm run check:format", 10 | "check:format": "prettier . --check", 11 | "check:lint": "eslint .", 12 | "check:test": "vitest run", 13 | "fix:all": "pnpm run fix:lint && pnpm run fix:format", 14 | "fix:format": "prettier . --write", 15 | "fix:lint": "eslint . --fix", 16 | "storybook": "storybook dev -p 6006", 17 | "build-storybook": "storybook build", 18 | "chromatic": "npx chromatic" 19 | }, 20 | "sideEffects": false, 21 | "keywords": [ 22 | "solid", 23 | "solidjs", 24 | "effect" 25 | ], 26 | "author": { 27 | "name": "JonahPlusPlus" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/JonahPlusPlus/solid-effect.git" 32 | }, 33 | "license": "(MIT OR Apache-2.0)", 34 | "engines": { 35 | "pnpm": ">=9" 36 | }, 37 | "devDependencies": { 38 | "@chromatic-com/storybook": "3.2.2", 39 | "@eslint/js": "^9.14.0", 40 | "@solidjs/testing-library": "^0.8.10", 41 | "@storybook/addon-essentials": "8.5.0-alpha.2", 42 | "@storybook/addon-interactions": "8.5.0-alpha.2", 43 | "@storybook/blocks": "8.5.0-alpha.2", 44 | "@storybook/test": "8.5.0-alpha.2", 45 | "@testing-library/jest-dom": "^6.6.2", 46 | "@types/node": "^22.8.6", 47 | "chromatic": "^11.18.1", 48 | "eslint": "^9.14.0", 49 | "globals": "^15.12.0", 50 | "prettier": "^3.3.3", 51 | "storybook": "8.5.0-alpha.2", 52 | "storybook-solidjs": "1.0.0-beta.6", 53 | "storybook-solidjs-vite": "1.0.0-beta.6", 54 | "tsup": "^8.3.0", 55 | "tsup-preset-solid": "^2.2.0", 56 | "typescript": "^5.6.2", 57 | "typescript-eslint": "^8.14.0", 58 | "vite-plugin-solid": "^2.10.2", 59 | "vitest": "^2.1.2" 60 | }, 61 | "peerDependencies": { 62 | "effect": "^3.8.4", 63 | "solid-js": "^1.9.3" 64 | }, 65 | "files": [ 66 | "dist", 67 | "LICENSE-*" 68 | ], 69 | "main": "./dist/index.cjs", 70 | "module": "./dist/index.js", 71 | "types": "./dist/index.d.ts", 72 | "browser": {}, 73 | "exports": { 74 | "solid": "./dist/index.jsx", 75 | "import": { 76 | "types": "./dist/index.d.ts", 77 | "default": "./dist/index.js" 78 | }, 79 | "require": { 80 | "types": "./dist/index.d.cts", 81 | "default": "./dist/index.cjs" 82 | } 83 | }, 84 | "typesVersions": {}, 85 | "packageManager": "pnpm@9.1.4+sha512.9df9cf27c91715646c7d675d1c9c8e41f6fce88246f1318c1aa6a1ed1aeb3c4f032fcdf4ba63cc69c4fe6d634279176b5358727d8f2cc1e65b65f43ce2f8bfb0" 86 | } 87 | -------------------------------------------------------------------------------- /src/effect/run-async-exit/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { RunAsyncExit } from ".."; 3 | import { Cause, Context, Effect, Layer, ManagedRuntime, Option } from "effect"; 4 | import { render, waitFor } from "@solidjs/testing-library"; 5 | import { ErrorBoundary } from "solid-js"; 6 | import { makeRuntime } from "../../runtime"; 7 | 8 | describe("RunAsyncExit", async () => { 9 | test("succeed", async () => { 10 | const effect = Effect.succeed("Success"); 11 | 12 | const Component = () => ( 13 | 14 | {{ 15 | onSuccess: (x) => <>{x}, 16 | onFailure: (x) => ( 17 | <>{Cause.failureOption(x).pipe(Option.getOrUndefined)} 18 | ), 19 | }} 20 | 21 | ); 22 | 23 | const { getByText } = render(Component); 24 | 25 | await waitFor(async () => expect(getByText("Success")).toBeInTheDocument()); 26 | }); 27 | 28 | test("fail", async () => { 29 | const effect = Effect.fail("Fail"); 30 | 31 | const Component = () => ( 32 | 33 | 34 | {{ 35 | onSuccess: (x) => <>Success: {x}, 36 | onFailure: (x) => ( 37 | <>{Cause.failureOption(x).pipe(Option.getOrUndefined)} 38 | ), 39 | }} 40 | 41 | 42 | ); 43 | 44 | const { getByText } = render(Component); 45 | 46 | await waitFor(async () => expect(getByText("Fail")).toBeInTheDocument()); 47 | }); 48 | 49 | class NumberGenerator extends Context.Tag("NumberGeneratorService")< 50 | NumberGenerator, 51 | { readonly generate: Effect.Effect } 52 | >() {} 53 | 54 | const NumberGeneratorLive = Layer.succeed( 55 | NumberGenerator, 56 | NumberGenerator.of({ 57 | generate: Effect.succeed(5), 58 | }), 59 | ); 60 | 61 | const rt = ManagedRuntime.make(NumberGeneratorLive); 62 | 63 | const solidRt = makeRuntime(await rt.runtime()); 64 | 65 | test("with dependency", async () => { 66 | const effect = Effect.gen(function* () { 67 | const numberGenerator = yield* NumberGenerator; 68 | const number = yield* numberGenerator.generate; 69 | return number; 70 | }); 71 | 72 | const Component = () => ( 73 | 74 | {{ 75 | onSuccess: (x) => <>{x}, 76 | onFailure: (x) => ( 77 | <>{Cause.failureOption(x).pipe(Option.getOrUndefined)} 78 | ), 79 | }} 80 | 81 | ); 82 | 83 | const { getByText } = render(Component); 84 | 85 | await waitFor(async () => expect(getByText("5")).toBeInTheDocument()); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/either/map-either/index.tsx: -------------------------------------------------------------------------------- 1 | import { Either } from "effect"; 2 | import { createMemo, type JSX, untrack } from "solid-js"; 3 | 4 | /** 5 | * 6 | * @param props 7 | * @returns An element mapped from a left or right value. 8 | */ 9 | function MapEither(props: { 10 | /** An either value or undefined. */ 11 | on: Either.Either | undefined; 12 | /** An object containing functions `onLeft` and `onRight` for mapping `on`. */ 13 | children: { 14 | readonly onLeft: (accessor: () => L) => JSX.Element; 15 | readonly onRight: (accessor: () => R) => JSX.Element; 16 | }; 17 | /** A fallback value displayed when `on` is undefined. */ 18 | fallback?: JSX.Element; 19 | /** Whether `children` should not use accessors. */ 20 | keyed?: false; 21 | }): JSX.Element; 22 | function MapEither(props: { 23 | /** An either value or undefined. */ 24 | on: Either.Either | undefined; 25 | /** An object containing functions `onLeft` and `onRight` for mapping `on`. */ 26 | children: { 27 | readonly onLeft: (accessor: L) => JSX.Element; 28 | readonly onRight: (accessor: R) => JSX.Element; 29 | }; 30 | /** A fallback value displayed when `on` is undefined. */ 31 | fallback?: JSX.Element; 32 | /** Whether `children` should not use accessors. */ 33 | keyed: true; 34 | }): JSX.Element; 35 | function MapEither(props: { 36 | on: Either.Either | undefined; 37 | children: { 38 | readonly onLeft: (accessor: L | (() => L)) => JSX.Element; 39 | readonly onRight: (accessor: R | (() => R)) => JSX.Element; 40 | }; 41 | fallback?: JSX.Element; 42 | keyed?: boolean; 43 | }): JSX.Element { 44 | const keyed = props.keyed; 45 | const lCondition = createMemo( 46 | () => props.on?._tag == "Left" && props.on.left, 47 | ); 48 | const rCondition = createMemo( 49 | () => props.on?._tag == "Right" && props.on.right, 50 | ); 51 | return createMemo(() => { 52 | const lC = lCondition(); 53 | if (lC) { 54 | const left = props.children.onLeft; 55 | return untrack(() => 56 | left( 57 | keyed 58 | ? lC 59 | : () => { 60 | if (!untrack(lCondition)) throw "Stale read from "; 61 | return (props.on! as Either.Either & { _tag: "Left" }) 62 | .left; 63 | }, 64 | ), 65 | ); 66 | } 67 | const rC = rCondition(); 68 | if (rC) { 69 | const right = props.children.onRight; 70 | return untrack(() => 71 | right( 72 | keyed 73 | ? rC 74 | : () => { 75 | if (!untrack(rCondition)) throw "Stale read from "; 76 | return (props.on! as Either.Either & { _tag: "Right" }) 77 | .right; 78 | }, 79 | ), 80 | ); 81 | } 82 | return props.fallback; 83 | }) as unknown as JSX.Element; 84 | } 85 | 86 | export { MapEither }; 87 | -------------------------------------------------------------------------------- /src/exit/map-exit/index.tsx: -------------------------------------------------------------------------------- 1 | import { Cause, Exit } from "effect"; 2 | import { createMemo, type JSX, untrack } from "solid-js"; 3 | 4 | /** 5 | * 6 | * @param props 7 | * @returns An element mapped from a successful value or a cause of failure. 8 | */ 9 | function MapExit(props: { 10 | /** An exit value or undefined. */ 11 | on: Exit.Exit | undefined; 12 | /** An object containing functions `onLeft` and `onRight` for mapping `on`. */ 13 | children: { 14 | readonly onSuccess: (accessor: () => T) => JSX.Element; 15 | readonly onFailure: (accessor: () => Cause.Cause) => JSX.Element; 16 | }; 17 | /** A fallback value displayed when `on` is undefined. */ 18 | fallback?: JSX.Element; 19 | /** Whether `children` should not use accessors. */ 20 | keyed?: false; 21 | }): JSX.Element; 22 | function MapExit(props: { 23 | /** An exit value or undefined. */ 24 | on: Exit.Exit | undefined; 25 | /** An object containing functions `onLeft` and `onRight` for mapping `on`. */ 26 | children: { 27 | readonly onSuccess: (accessor: T) => JSX.Element; 28 | readonly onFailure: (accessor: Cause.Cause) => JSX.Element; 29 | }; 30 | /** A fallback value displayed when `on` is undefined. */ 31 | fallback?: JSX.Element; 32 | /** Whether `children` should not use accessors. */ 33 | keyed: true; 34 | }): JSX.Element; 35 | function MapExit(props: { 36 | on: Exit.Exit | undefined; 37 | children: { 38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 39 | readonly onSuccess: (accessor: any) => JSX.Element; 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | readonly onFailure: (accessor: any) => JSX.Element; 42 | }; 43 | fallback?: JSX.Element; 44 | keyed?: boolean; 45 | }): JSX.Element { 46 | const keyed = props.keyed; 47 | const tCondition = createMemo( 48 | () => props.on?._tag == "Success" && props.on.value, 49 | ); 50 | const eCondition = createMemo( 51 | () => props.on?._tag == "Failure" && props.on.cause, 52 | ); 53 | return createMemo(() => { 54 | const tC = tCondition(); 55 | if (tC) { 56 | const succeed = props.children.onSuccess; 57 | return untrack(() => 58 | succeed( 59 | keyed 60 | ? tC 61 | : () => { 62 | if (!untrack(tCondition)) throw "Stale read from "; 63 | return (props.on! as Exit.Exit & { _tag: "Success" }) 64 | .value; 65 | }, 66 | ), 67 | ); 68 | } 69 | const eC = eCondition(); 70 | if (eC) { 71 | const fail = props.children.onFailure; 72 | return untrack(() => 73 | fail( 74 | keyed 75 | ? eC 76 | : () => { 77 | if (!untrack(eCondition)) throw "Stale read from "; 78 | return (props.on! as Exit.Exit & { _tag: "Failure" }) 79 | .cause; 80 | }, 81 | ), 82 | ); 83 | } 84 | return props.fallback; 85 | }) as unknown as JSX.Element; 86 | } 87 | 88 | export { MapExit }; 89 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | --------------------------------------------------------------------------------